@erclx/canon 5.45.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/api-design/REQUIREMENT.md +15 -31
- package/claude/skills/auto-ship/REQUIREMENT.md +14 -25
- package/claude/skills/backlog-triage/REQUIREMENT.md +12 -23
- package/claude/skills/build-in-slices/REQUIREMENT.md +15 -25
- package/claude/skills/canon-cli/REQUIREMENT.md +15 -27
- package/claude/skills/canon-feedback/REQUIREMENT.md +10 -18
- package/claude/skills/canon-feedback-triage/REQUIREMENT.md +10 -15
- package/claude/skills/canvas-board/REQUIREMENT.md +14 -28
- package/claude/skills/ci-workflow/REQUIREMENT.md +12 -21
- package/claude/skills/code-craft/REQUIREMENT.md +14 -28
- package/claude/skills/codebase-layout/REQUIREMENT.md +14 -31
- package/claude/skills/context-fold/REQUIREMENT.md +15 -39
- package/claude/skills/context-fold/references/context.md +3 -3
- package/claude/skills/create-rule/REQUIREMENT.md +10 -24
- package/claude/skills/create-rule/SKILL.md +3 -0
- package/claude/skills/create-skill/REQUIREMENT.md +10 -21
- package/claude/skills/decision-escalate/REQUIREMENT.md +12 -24
- package/claude/skills/deploy-app/REQUIREMENT.md +12 -22
- package/claude/skills/deprecation-migration/REQUIREMENT.md +14 -42
- package/claude/skills/design-identity/REQUIREMENT.md +14 -20
- package/claude/skills/design-taste/REQUIREMENT.md +16 -57
- package/claude/skills/docs-sync/REQUIREMENT.md +14 -23
- package/claude/skills/document-health/REQUIREMENT.md +15 -29
- package/claude/skills/document-health/SKILL.md +2 -2
- package/claude/skills/document-repair/REQUIREMENT.md +32 -0
- package/claude/skills/document-repair/SKILL.md +76 -0
- package/claude/skills/document-repair/references/batches.md +44 -0
- package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -3
- package/claude/skills/draft-doc/REQUIREMENT.md +4 -3
- package/claude/skills/draft-figure/REQUIREMENT.md +4 -3
- package/claude/skills/draft-identity/REQUIREMENT.md +4 -3
- package/claude/skills/draft-ready/REQUIREMENT.md +4 -3
- package/claude/skills/draft-screencast/REQUIREMENT.md +4 -3
- package/claude/skills/draft-slides/REQUIREMENT.md +4 -3
- package/claude/skills/draw-figure/REQUIREMENT.md +15 -39
- package/claude/skills/git-branch/REQUIREMENT.md +12 -26
- package/claude/skills/git-commit/REQUIREMENT.md +11 -15
- package/claude/skills/git-followup/REQUIREMENT.md +11 -22
- package/claude/skills/git-followup/SKILL.md +1 -0
- package/claude/skills/git-issue/REQUIREMENT.md +9 -15
- package/claude/skills/git-pr/REQUIREMENT.md +13 -32
- package/claude/skills/git-pr/SKILL.md +11 -2
- package/claude/skills/git-pr/references/closing-issues.md +47 -0
- package/claude/skills/git-pr/references/pr.md +62 -7
- package/claude/skills/git-ship/REQUIREMENT.md +13 -26
- package/claude/skills/git-split/REQUIREMENT.md +10 -17
- package/claude/skills/git-stage/REQUIREMENT.md +12 -18
- package/claude/skills/git-worktree/REQUIREMENT.md +15 -19
- package/claude/skills/make-deck/REQUIREMENT.md +13 -21
- package/claude/skills/markdown-craft/REQUIREMENT.md +9 -3
- package/claude/skills/plan-feature/REQUIREMENT.md +15 -24
- package/claude/skills/plan-groundwork/REQUIREMENT.md +12 -28
- package/claude/skills/plan-groundwork/SKILL.md +5 -2
- package/claude/skills/plan-groundwork/references/output.md +47 -1
- package/claude/skills/plan-intake/REQUIREMENT.md +11 -26
- package/claude/skills/plan-intake/references/intake.md +2 -2
- package/claude/skills/plan-intake-answer/REQUIREMENT.md +14 -29
- package/claude/skills/plan-intake-answer/SKILL.md +7 -1
- package/claude/skills/plan-ready/REQUIREMENT.md +17 -36
- package/claude/skills/pr-evidence/REQUIREMENT.md +11 -17
- package/claude/skills/read-frames/REQUIREMENT.md +3 -8
- package/claude/skills/record-screencast/REQUIREMENT.md +14 -17
- package/claude/skills/review-address/REQUIREMENT.md +14 -38
- 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-craft/REQUIREMENT.md +14 -32
- package/claude/skills/review-pr/REQUIREMENT.md +15 -34
- 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/REQUIREMENT.md +14 -39
- 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-orchestrator/REQUIREMENT.md +2 -2
- package/claude/skills/role-orchestrator/references/orchestrator-refill.md +5 -0
- package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +9 -0
- package/claude/skills/role-reviewer/REQUIREMENT.md +15 -65
- package/claude/skills/role-reviewer/SKILL.md +8 -1
- package/claude/skills/role-worker/REQUIREMENT.md +2 -2
- package/claude/skills/role-worker/SKILL.md +3 -0
- package/claude/skills/script-screencast/REQUIREMENT.md +10 -16
- package/claude/skills/search-craft/REQUIREMENT.md +12 -36
- package/claude/skills/search-craft/SKILL.md +1 -1
- package/claude/skills/search-craft/references/adopted.md +2 -2
- package/claude/skills/session-compact/REQUIREMENT.md +13 -23
- package/claude/skills/session-map/REQUIREMENT.md +13 -29
- package/claude/skills/session-relay/REQUIREMENT.md +14 -58
- package/claude/skills/session-relay/SKILL.md +6 -0
- package/claude/skills/session-resume/REQUIREMENT.md +13 -27
- package/claude/skills/session-worktree/REQUIREMENT.md +14 -43
- package/claude/skills/systematic-debugging/REQUIREMENT.md +13 -34
- package/claude/skills/target-check/REQUIREMENT.md +16 -23
- package/claude/skills/target-setup/REQUIREMENT.md +11 -34
- package/claude/skills/task-board/REQUIREMENT.md +13 -22
- package/claude/skills/task-board/SKILL.md +7 -0
- package/claude/skills/task-board/references/decline.md +2 -2
- package/claude/skills/task-board/references/tasks.md +1 -1
- package/claude/skills/task-complete/REQUIREMENT.md +11 -24
- package/claude/skills/task-complete/references/archive.md +4 -2
- package/claude/skills/teach-workspace/REQUIREMENT.md +14 -45
- package/claude/skills/test-craft/REQUIREMENT.md +15 -46
- package/claude/skills/test-first/REQUIREMENT.md +14 -20
- package/claude/skills/ui-checklist/REQUIREMENT.md +14 -29
- package/claude/skills/ui-checklist/SKILL.md +3 -3
- package/claude/skills/ux-audit/REQUIREMENT.md +3 -7
- package/claude/skills/ux-measure/REQUIREMENT.md +14 -28
- package/claude/skills/video-craft/REQUIREMENT.md +14 -27
- package/claude/skills/write-doc/REQUIREMENT.md +14 -24
- package/claude/skills/write-human/REQUIREMENT.md +15 -27
- package/claude/skills/youtube-transcripts/REQUIREMENT.md +9 -15
- package/docs/agents/context-audit.md +19 -12
- package/docs/agents/markdown-audit.md +15 -10
- package/docs/target/projects.md +2 -0
- package/docs/workflow/skill-map.md +6 -5
- package/docs/workflow/zshrc-aliases.md +3 -0
- package/governance/renames.toml +0 -2
- package/governance/rules/claude/output.md +5 -1
- package/governance/rules/code/routing.md +5 -1
- package/governance/stacks/base.toml +0 -1
- package/package.json +1 -1
- package/scripts/core/bootstrap.sh +1 -0
- package/src/claude/cases/workflow.ts +12 -0
- package/src/commands/context.ts +57 -3
- package/src/commands/labels.ts +149 -4
- package/src/commands/markdown.ts +188 -21
- package/src/commands/pr.ts +113 -4
- package/src/commands/sessions.ts +58 -11
- package/src/commands/tasks.ts +182 -0
- package/src/commands/worktrees.ts +2 -0
- package/src/context/coverage.ts +210 -0
- package/src/context/gate.ts +12 -3
- package/src/gate/stages.ts +1 -1
- package/src/init/steps.ts +45 -0
- package/src/intake/items.ts +14 -0
- package/src/labels/closing.ts +46 -0
- package/src/labels/density.ts +363 -0
- package/src/markdown/gated.ts +250 -0
- package/src/markdown/scope.ts +115 -2
- package/src/migrate/rename.ts +6 -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/claim.ts +7 -0
- package/src/sessions/live.ts +79 -6
- package/src/sessions/registry.ts +11 -0
- package/src/sessions/resolve.ts +85 -7
- package/src/tasks/archive.ts +15 -4
- package/src/tasks/closes.ts +136 -0
- package/src/tasks/record.ts +1 -1
- package/src/worktrees/reclaim.ts +16 -1
- package/tooling/base/configs/scripts/verify.ts +1 -1
- package/tooling/base/seeds/.github/pull_request_template.md +1 -1
- package/tooling/claude/seeds/.claude/hooks/board-validate.sh +1 -1
- package/tooling/claude/seeds/CLAUDE.md +2 -2
- package/governance/rules/canon/task-scope.md +0 -12
package/src/sessions/resolve.ts
CHANGED
|
@@ -22,6 +22,19 @@ export interface ResolvedSession {
|
|
|
22
22
|
readonly cwd: string
|
|
23
23
|
readonly kind: string
|
|
24
24
|
readonly status: string
|
|
25
|
+
/**
|
|
26
|
+
* What decided the row was live. `pid` is a start time matched or a pid that
|
|
27
|
+
* answered, `socket` means the record belongs to another pid namespace and
|
|
28
|
+
* only its messaging socket file existed, so the row can be a crashed
|
|
29
|
+
* session's leftover, and `listener` means the messaging socket accepted a
|
|
30
|
+
* connection, which confirms the row wherever the start time cannot be read.
|
|
31
|
+
*/
|
|
32
|
+
readonly via: 'pid' | 'socket' | 'listener'
|
|
33
|
+
/**
|
|
34
|
+
* How this row alone was decided, so a caller scoping the roster derives its
|
|
35
|
+
* confidence over the rows it keeps rather than over the whole machine.
|
|
36
|
+
*/
|
|
37
|
+
readonly confidence: Confidence
|
|
25
38
|
readonly startedAt: string | null
|
|
26
39
|
/**
|
|
27
40
|
* The stamp the record carries beside `status`, kept alongside the dwell it
|
|
@@ -64,9 +77,32 @@ export type SessionReport =
|
|
|
64
77
|
* tells a caller whether the roster can be trusted as an identity.
|
|
65
78
|
*/
|
|
66
79
|
readonly confidence: Confidence
|
|
80
|
+
/**
|
|
81
|
+
* Foreign-namespace records whose socket is gone. They read dead, but the
|
|
82
|
+
* reader could not check their pid, so an empty roster beside a count
|
|
83
|
+
* above zero is a roster that could not be checked rather than one that
|
|
84
|
+
* ended.
|
|
85
|
+
*/
|
|
86
|
+
readonly foreignDead: number
|
|
67
87
|
readonly sessions: readonly ResolvedSession[]
|
|
68
88
|
}
|
|
69
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The weakest confidence among the rows, so one read tells a caller whether
|
|
92
|
+
* every row it holds is an identity. A caller scoping the roster calls this on
|
|
93
|
+
* the rows it keeps, since one unrelated row it dropped must not weaken it.
|
|
94
|
+
*/
|
|
95
|
+
export function confidenceOf(
|
|
96
|
+
sessions: readonly Pick<ResolvedSession, 'confidence'>[],
|
|
97
|
+
): Confidence {
|
|
98
|
+
return sessions.every((session) => session.confidence === 'confirmed')
|
|
99
|
+
? 'confirmed'
|
|
100
|
+
: 'unverified'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A live socket here accepted in 2 ms, and every probe runs in parallel. */
|
|
104
|
+
const LISTEN_TIMEOUT_MS = 250
|
|
105
|
+
|
|
70
106
|
export interface ResolveOptions {
|
|
71
107
|
readonly dir?: string
|
|
72
108
|
readonly probes?: LivenessProbes
|
|
@@ -167,6 +203,8 @@ function present(
|
|
|
167
203
|
record: SessionRecord,
|
|
168
204
|
located: Located,
|
|
169
205
|
now: number,
|
|
206
|
+
via: ResolvedSession['via'],
|
|
207
|
+
confidence: Confidence,
|
|
170
208
|
): ResolvedSession {
|
|
171
209
|
return {
|
|
172
210
|
name: record.name,
|
|
@@ -175,6 +213,8 @@ function present(
|
|
|
175
213
|
cwd: record.cwd,
|
|
176
214
|
kind: record.kind ?? 'unknown',
|
|
177
215
|
status: record.status ?? 'unknown',
|
|
216
|
+
via,
|
|
217
|
+
confidence,
|
|
178
218
|
startedAt:
|
|
179
219
|
record.startedAt === undefined
|
|
180
220
|
? null
|
|
@@ -213,21 +253,59 @@ export async function resolveSessions(
|
|
|
213
253
|
|
|
214
254
|
if (registry.kind === 'absent') return { kind: 'absent', dir: registry.dir }
|
|
215
255
|
|
|
216
|
-
const live:
|
|
217
|
-
|
|
256
|
+
const live: {
|
|
257
|
+
record: SessionRecord
|
|
258
|
+
via: ResolvedSession['via']
|
|
259
|
+
confidence: Confidence
|
|
260
|
+
}[] = []
|
|
261
|
+
let foreignDead = 0
|
|
218
262
|
|
|
219
263
|
for (const record of registry.records) {
|
|
220
264
|
const state = liveness(record, probes)
|
|
221
|
-
if (!state.alive)
|
|
222
|
-
|
|
223
|
-
|
|
265
|
+
if (!state.alive) {
|
|
266
|
+
if (state.foreign) foreignDead += 1
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
live.push({
|
|
270
|
+
record,
|
|
271
|
+
via: state.foreign ? 'socket' : 'pid',
|
|
272
|
+
confidence: state.confidence,
|
|
273
|
+
})
|
|
224
274
|
}
|
|
225
275
|
|
|
276
|
+
// Only a row the start time left unverified is probed, so a machine that reads
|
|
277
|
+
// /proc pays nothing. An accept proves the session is running where a pid
|
|
278
|
+
// answering or a socket file existing does not.
|
|
279
|
+
const upgraded = await Promise.all(
|
|
280
|
+
live.map(async (row) => {
|
|
281
|
+
const socket = row.record.messagingSocketPath
|
|
282
|
+
if (
|
|
283
|
+
row.confidence === 'confirmed' ||
|
|
284
|
+
socket === undefined ||
|
|
285
|
+
probes.listens === undefined
|
|
286
|
+
) {
|
|
287
|
+
return row
|
|
288
|
+
}
|
|
289
|
+
const accepted = await probes.listens(socket, LISTEN_TIMEOUT_MS)
|
|
290
|
+
return accepted
|
|
291
|
+
? { ...row, via: 'listener' as const, confidence: 'confirmed' as const }
|
|
292
|
+
: row
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
295
|
+
|
|
226
296
|
const sessions = await Promise.all(
|
|
227
|
-
|
|
297
|
+
upgraded.map(async ({ record, via, confidence }) =>
|
|
298
|
+
present(record, await find(record.cwd), now, via, confidence),
|
|
299
|
+
),
|
|
228
300
|
)
|
|
229
301
|
|
|
230
|
-
return {
|
|
302
|
+
return {
|
|
303
|
+
kind: 'resolved',
|
|
304
|
+
dir: registry.dir,
|
|
305
|
+
confidence: confidenceOf(sessions),
|
|
306
|
+
foreignDead,
|
|
307
|
+
sessions,
|
|
308
|
+
}
|
|
231
309
|
}
|
|
232
310
|
|
|
233
311
|
/** The environment slice the caller's own identity is read from. */
|
package/src/tasks/archive.ts
CHANGED
|
@@ -107,6 +107,7 @@ export interface ArchiveSuccess {
|
|
|
107
107
|
readonly from: string
|
|
108
108
|
readonly to: string
|
|
109
109
|
readonly priorityRowRemoved: boolean
|
|
110
|
+
readonly backlogRowRemoved: boolean
|
|
110
111
|
readonly indexRegenerated: boolean
|
|
111
112
|
/** Every live plan the task cited that no other live task still holds, in line order. */
|
|
112
113
|
readonly plans: readonly PlanMove[]
|
|
@@ -556,7 +557,7 @@ function isRowFor(line: string, target: string): boolean {
|
|
|
556
557
|
}
|
|
557
558
|
|
|
558
559
|
/**
|
|
559
|
-
* Drops the
|
|
560
|
+
* Drops the task's bullet from the backlog. A backlog line is a
|
|
560
561
|
* bullet carrying a link rather than a table row, so the match is a bullet
|
|
561
562
|
* prefix and the link target rather than `isRowFor`'s pipe-delimited cell.
|
|
562
563
|
*/
|
|
@@ -571,9 +572,17 @@ export function removeBacklogRow(
|
|
|
571
572
|
return { text: kept.join('\n'), removed: kept.length !== lines.length }
|
|
572
573
|
}
|
|
573
574
|
|
|
575
|
+
/**
|
|
576
|
+
* A bullet is about a task when its first link targets it. A later link is a
|
|
577
|
+
* reference, such as a note on what the bullet waits for, and matching anywhere
|
|
578
|
+
* would drop that other task's line from a file no history backs.
|
|
579
|
+
*/
|
|
574
580
|
function isBulletFor(line: string, target: string): boolean {
|
|
575
581
|
const trimmed = line.trimStart()
|
|
576
|
-
|
|
582
|
+
if (!/^[-*]\s/.test(trimmed)) return false
|
|
583
|
+
|
|
584
|
+
const first = trimmed.indexOf('](')
|
|
585
|
+
return first !== -1 && trimmed.startsWith(target, first)
|
|
577
586
|
}
|
|
578
587
|
|
|
579
588
|
/**
|
|
@@ -896,8 +905,8 @@ async function resolveStem(
|
|
|
896
905
|
}
|
|
897
906
|
|
|
898
907
|
/**
|
|
899
|
-
* Archives one task as a single unit: the move, the ordering-row
|
|
900
|
-
* the index regen. The hook and `task-complete` both call this, so every gate
|
|
908
|
+
* Archives one task as a single unit: the move, the ordering-row and backlog
|
|
909
|
+
* bullet removal, and the index regen. The hook and `task-complete` both call this, so every gate
|
|
901
910
|
* refuses rather than reports. A caller with nobody watching cannot act on a
|
|
902
911
|
* warning, and two callers gating differently is the drift this exists to stop.
|
|
903
912
|
*/
|
|
@@ -996,6 +1005,7 @@ export async function archiveTask(
|
|
|
996
1005
|
|
|
997
1006
|
const { relinked, failed } = await repointInboundLinks(root, from, to)
|
|
998
1007
|
const priorityRowRemoved = await clearPriorityRow(dir, stem)
|
|
1008
|
+
const backlogRowRemoved = await clearBacklogRow(dir, stem)
|
|
999
1009
|
const regen = await regenOne(dir, { dryRun: false })
|
|
1000
1010
|
|
|
1001
1011
|
return {
|
|
@@ -1004,6 +1014,7 @@ export async function archiveTask(
|
|
|
1004
1014
|
from,
|
|
1005
1015
|
to,
|
|
1006
1016
|
priorityRowRemoved,
|
|
1017
|
+
backlogRowRemoved,
|
|
1007
1018
|
indexRegenerated: regen.action === 'written',
|
|
1008
1019
|
plans: plans.moves,
|
|
1009
1020
|
ready: ready || undefined,
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import {
|
|
3
|
+
fenceMask,
|
|
4
|
+
readOutcomes,
|
|
5
|
+
readPendingBranches,
|
|
6
|
+
readPullRequest,
|
|
7
|
+
} from '@/tasks/archive'
|
|
8
|
+
import {
|
|
9
|
+
openTask,
|
|
10
|
+
type RecordRefused,
|
|
11
|
+
type RecordSelector,
|
|
12
|
+
} from '@/tasks/record'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Why a task does or does not close its issues. `last-slice` is the only
|
|
16
|
+
* reason that closes. The rest mirror the conditions `archiveTask` applies at
|
|
17
|
+
* merge time, so an issue closes exactly when the task archives.
|
|
18
|
+
*/
|
|
19
|
+
export type ClosingReason =
|
|
20
|
+
| 'earlier-slice'
|
|
21
|
+
| 'last-slice'
|
|
22
|
+
| 'lookup-failed'
|
|
23
|
+
| 'no-issues'
|
|
24
|
+
| 'no-outcomes'
|
|
25
|
+
| 'open-outcomes'
|
|
26
|
+
| 'pending-branch'
|
|
27
|
+
|
|
28
|
+
export interface Closure {
|
|
29
|
+
readonly issues: readonly number[]
|
|
30
|
+
readonly closing: boolean
|
|
31
|
+
readonly reason: ClosingReason
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface TaskClosure extends Closure {
|
|
35
|
+
readonly ok: true
|
|
36
|
+
readonly stem: string
|
|
37
|
+
readonly path: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ClosureOutcome = TaskClosure | RecordRefused
|
|
41
|
+
|
|
42
|
+
const ISSUE_LINE = /^Issue:[ \t]*(#\d+(?:[ \t]*,[ \t]*#\d+)*)[ \t]*$/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reads every `Issue:` line outside a fence, each a bare `#NNN` or a comma
|
|
46
|
+
* list of them. A line carrying anything else, such as the older bare-path
|
|
47
|
+
* form, names no issue rather than the numbers that happened to parse, since a
|
|
48
|
+
* wrong number would close an unrelated issue.
|
|
49
|
+
*/
|
|
50
|
+
export function readIssues(text: string): readonly number[] {
|
|
51
|
+
const lines = text.split('\n')
|
|
52
|
+
const fenced = fenceMask(lines)
|
|
53
|
+
const issues = new Set<number>()
|
|
54
|
+
|
|
55
|
+
for (const [index, line] of lines.entries()) {
|
|
56
|
+
if (fenced[index]) continue
|
|
57
|
+
|
|
58
|
+
const match = ISSUE_LINE.exec(line)
|
|
59
|
+
if (!match) continue
|
|
60
|
+
|
|
61
|
+
for (const entry of match[1].split(',')) {
|
|
62
|
+
issues.add(Number(entry.trim().slice(1)))
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return [...issues]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decides whether the pull request for `branches` closes the task's issues.
|
|
71
|
+
* `branches` is the working branch and every name it carried, since a rename
|
|
72
|
+
* leaves the `Pending branch:` marker under the old one. Any other pending
|
|
73
|
+
* branch is a slice still to ship, so the task stays open behind it.
|
|
74
|
+
*/
|
|
75
|
+
export function decideClosing(
|
|
76
|
+
text: string,
|
|
77
|
+
branches: readonly string[],
|
|
78
|
+
pulls: readonly number[] | null = [],
|
|
79
|
+
): Closure {
|
|
80
|
+
const issues = readIssues(text)
|
|
81
|
+
if (issues.length === 0) {
|
|
82
|
+
return { issues, closing: false, reason: 'no-issues' }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const listed = readPullRequest(text)
|
|
86
|
+
const last = listed.at(-1)
|
|
87
|
+
|
|
88
|
+
// A failed lookup cannot rule out that this branch is an earlier slice, and
|
|
89
|
+
// closing early is the unsafe direction. With nothing recorded yet there is
|
|
90
|
+
// no earlier slice to confuse it with, so that case still closes.
|
|
91
|
+
if (pulls === null && listed.length > 0) {
|
|
92
|
+
return { issues, closing: false, reason: 'lookup-failed' }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
last !== undefined &&
|
|
97
|
+
pulls !== null &&
|
|
98
|
+
!pulls.includes(last) &&
|
|
99
|
+
pulls.some((number) => listed.includes(number))
|
|
100
|
+
) {
|
|
101
|
+
return { issues, closing: false, reason: 'earlier-slice' }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const others = readPendingBranches(text).filter(
|
|
105
|
+
(branch) => !branches.includes(branch),
|
|
106
|
+
)
|
|
107
|
+
if (others.length > 0) {
|
|
108
|
+
return { issues, closing: false, reason: 'pending-branch' }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { open, closed, cut } = readOutcomes(text)
|
|
112
|
+
if (open.length > 0) {
|
|
113
|
+
return { issues, closing: false, reason: 'open-outcomes' }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (closed.length === 0 && cut.length === 0) {
|
|
117
|
+
return { issues, closing: false, reason: 'no-outcomes' }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { issues, closing: true, reason: 'last-slice' }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Resolves the task through the same selector `pull-request` takes. */
|
|
124
|
+
export async function readClosure(
|
|
125
|
+
root: string,
|
|
126
|
+
selector: RecordSelector,
|
|
127
|
+
branches: readonly string[],
|
|
128
|
+
pulls: readonly number[] | null = [],
|
|
129
|
+
): Promise<ClosureOutcome> {
|
|
130
|
+
const opened = await openTask(root, selector)
|
|
131
|
+
if ('ok' in opened) return opened
|
|
132
|
+
|
|
133
|
+
const text = await readFile(opened.path, 'utf8')
|
|
134
|
+
|
|
135
|
+
return { ok: true, ...opened, ...decideClosing(text, branches, pulls) }
|
|
136
|
+
}
|
package/src/tasks/record.ts
CHANGED
|
@@ -400,7 +400,7 @@ async function resolveStem(
|
|
|
400
400
|
return matched[0]
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
async function openTask(
|
|
403
|
+
export async function openTask(
|
|
404
404
|
root: string,
|
|
405
405
|
selector: RecordSelector,
|
|
406
406
|
): Promise<{ readonly stem: string; readonly path: string } | RecordRefused> {
|
package/src/worktrees/reclaim.ts
CHANGED
|
@@ -37,7 +37,11 @@ export type Refusal =
|
|
|
37
37
|
| 'held-by-session'
|
|
38
38
|
|
|
39
39
|
/** Why the whole reading was refused, so no verdict was produced at all. */
|
|
40
|
-
export type Unreadable =
|
|
40
|
+
export type Unreadable =
|
|
41
|
+
| 'gh-missing'
|
|
42
|
+
| 'gh-failed'
|
|
43
|
+
| 'sessions-unreadable'
|
|
44
|
+
| 'sessions-unchecked'
|
|
41
45
|
|
|
42
46
|
/**
|
|
43
47
|
* Which removal shape applies. `session` removes the background session and its
|
|
@@ -395,6 +399,17 @@ export async function reclaimReport(
|
|
|
395
399
|
}
|
|
396
400
|
}
|
|
397
401
|
|
|
402
|
+
// A foreign-namespace record whose socket is gone reads dead, but a reader
|
|
403
|
+
// that cannot see the pid may also be unable to see the socket, so it may be
|
|
404
|
+
// a live holder. The roster drops it, and a removal on that reading is final.
|
|
405
|
+
if (sessions.foreignDead > 0) {
|
|
406
|
+
return {
|
|
407
|
+
kind: 'unreadable',
|
|
408
|
+
reason: 'sessions-unchecked',
|
|
409
|
+
detail: `${sessions.foreignDead} session records come from another pid namespace and no socket answers for them, so nothing was read about whether they still hold a worktree.`,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
398
413
|
const byBranch = new Map(
|
|
399
414
|
merged.merged.map((request) => [request.branch, request.number]),
|
|
400
415
|
)
|
|
@@ -323,7 +323,7 @@ function checkMarkdownBans(): void {
|
|
|
323
323
|
case 2:
|
|
324
324
|
pipeOutput(output)
|
|
325
325
|
fail(
|
|
326
|
-
'Markdown prose carries a banned character,
|
|
326
|
+
'Markdown prose carries a banned character, a relative link resolves to nothing on disk, or a gated path breaks a density limit',
|
|
327
327
|
)
|
|
328
328
|
break
|
|
329
329
|
case 3:
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
- <Verb> <specific component/file/function>
|
|
9
9
|
|
|
10
10
|
<!-- ## Technical context (optional, omit for docs/config/trivial changes) -->
|
|
11
|
-
<!--
|
|
11
|
+
<!-- **<Decision in a few words>.** <Why this approach, what the code can't show, and any shortcoming> -->
|
|
12
12
|
|
|
13
13
|
## Testing
|
|
14
14
|
|
|
@@ -51,7 +51,7 @@ fi
|
|
|
51
51
|
# Exit 2 carries findings, so the status is read off the record rather than
|
|
52
52
|
# the exit code.
|
|
53
53
|
record=$(canon tasks validate --json --root "$root" 2>/dev/null)
|
|
54
|
-
ok=$(printf '%s' "$record" | jq -r '.ok
|
|
54
|
+
ok=$(printf '%s' "$record" | jq -r '.ok | tostring' 2>/dev/null)
|
|
55
55
|
|
|
56
56
|
if [ "$ok" = false ]; then
|
|
57
57
|
report "canon tasks validate refused the board after this edit: $(printf '%s' "$record" | jq -r '.message // .reason')"
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
## Context
|
|
6
6
|
|
|
7
|
-
- Before non-trivial work in a domain read `canon/context
|
|
7
|
+
- Before non-trivial work in a domain, read its entry under `canon/context/`. Find it from the index anchor below.
|
|
8
8
|
|
|
9
9
|
@canon/REQUIREMENTS.md
|
|
10
10
|
@canon/context/index.md
|
|
11
11
|
|
|
12
12
|
## Commands
|
|
13
13
|
|
|
14
|
-
-
|
|
14
|
+
- Run `bun run check` to verify and `bun run format` to fix before committing. The pre-push hook runs `check`, which writes nothing.
|
|
15
15
|
|
|
16
16
|
## Key paths
|
|
17
17
|
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Scope, size, and link a task file correctly
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# Tasks standards
|
|
6
|
-
|
|
7
|
-
## Task files
|
|
8
|
-
|
|
9
|
-
- Only create a task for work that spans multiple sessions or has real dependencies. Handle small edits immediately without a task entry.
|
|
10
|
-
- Do not add tasks retroactively for work already completed. Completed work is visible in git.
|
|
11
|
-
- Write a plan in `.canon/plans/` when a task needs execution detail beyond its own file, and link it from the task's intro paragraph.
|
|
12
|
-
- Write that plan in the same session as the task, since the executing session would otherwise re-derive the reasoning behind it.
|