@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.
Files changed (158) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/api-design/REQUIREMENT.md +15 -31
  3. package/claude/skills/auto-ship/REQUIREMENT.md +14 -25
  4. package/claude/skills/backlog-triage/REQUIREMENT.md +12 -23
  5. package/claude/skills/build-in-slices/REQUIREMENT.md +15 -25
  6. package/claude/skills/canon-cli/REQUIREMENT.md +15 -27
  7. package/claude/skills/canon-feedback/REQUIREMENT.md +10 -18
  8. package/claude/skills/canon-feedback-triage/REQUIREMENT.md +10 -15
  9. package/claude/skills/canvas-board/REQUIREMENT.md +14 -28
  10. package/claude/skills/ci-workflow/REQUIREMENT.md +12 -21
  11. package/claude/skills/code-craft/REQUIREMENT.md +14 -28
  12. package/claude/skills/codebase-layout/REQUIREMENT.md +14 -31
  13. package/claude/skills/context-fold/REQUIREMENT.md +15 -39
  14. package/claude/skills/context-fold/references/context.md +3 -3
  15. package/claude/skills/create-rule/REQUIREMENT.md +10 -24
  16. package/claude/skills/create-rule/SKILL.md +3 -0
  17. package/claude/skills/create-skill/REQUIREMENT.md +10 -21
  18. package/claude/skills/decision-escalate/REQUIREMENT.md +12 -24
  19. package/claude/skills/deploy-app/REQUIREMENT.md +12 -22
  20. package/claude/skills/deprecation-migration/REQUIREMENT.md +14 -42
  21. package/claude/skills/design-identity/REQUIREMENT.md +14 -20
  22. package/claude/skills/design-taste/REQUIREMENT.md +16 -57
  23. package/claude/skills/docs-sync/REQUIREMENT.md +14 -23
  24. package/claude/skills/document-health/REQUIREMENT.md +15 -29
  25. package/claude/skills/document-health/SKILL.md +2 -2
  26. package/claude/skills/document-repair/REQUIREMENT.md +32 -0
  27. package/claude/skills/document-repair/SKILL.md +76 -0
  28. package/claude/skills/document-repair/references/batches.md +44 -0
  29. package/claude/skills/draft-and-pick/REQUIREMENT.md +4 -3
  30. package/claude/skills/draft-doc/REQUIREMENT.md +4 -3
  31. package/claude/skills/draft-figure/REQUIREMENT.md +4 -3
  32. package/claude/skills/draft-identity/REQUIREMENT.md +4 -3
  33. package/claude/skills/draft-ready/REQUIREMENT.md +4 -3
  34. package/claude/skills/draft-screencast/REQUIREMENT.md +4 -3
  35. package/claude/skills/draft-slides/REQUIREMENT.md +4 -3
  36. package/claude/skills/draw-figure/REQUIREMENT.md +15 -39
  37. package/claude/skills/git-branch/REQUIREMENT.md +12 -26
  38. package/claude/skills/git-commit/REQUIREMENT.md +11 -15
  39. package/claude/skills/git-followup/REQUIREMENT.md +11 -22
  40. package/claude/skills/git-followup/SKILL.md +1 -0
  41. package/claude/skills/git-issue/REQUIREMENT.md +9 -15
  42. package/claude/skills/git-pr/REQUIREMENT.md +13 -32
  43. package/claude/skills/git-pr/SKILL.md +11 -2
  44. package/claude/skills/git-pr/references/closing-issues.md +47 -0
  45. package/claude/skills/git-pr/references/pr.md +62 -7
  46. package/claude/skills/git-ship/REQUIREMENT.md +13 -26
  47. package/claude/skills/git-split/REQUIREMENT.md +10 -17
  48. package/claude/skills/git-stage/REQUIREMENT.md +12 -18
  49. package/claude/skills/git-worktree/REQUIREMENT.md +15 -19
  50. package/claude/skills/make-deck/REQUIREMENT.md +13 -21
  51. package/claude/skills/markdown-craft/REQUIREMENT.md +9 -3
  52. package/claude/skills/plan-feature/REQUIREMENT.md +15 -24
  53. package/claude/skills/plan-groundwork/REQUIREMENT.md +12 -28
  54. package/claude/skills/plan-groundwork/SKILL.md +5 -2
  55. package/claude/skills/plan-groundwork/references/output.md +47 -1
  56. package/claude/skills/plan-intake/REQUIREMENT.md +11 -26
  57. package/claude/skills/plan-intake/references/intake.md +2 -2
  58. package/claude/skills/plan-intake-answer/REQUIREMENT.md +14 -29
  59. package/claude/skills/plan-intake-answer/SKILL.md +7 -1
  60. package/claude/skills/plan-ready/REQUIREMENT.md +17 -36
  61. package/claude/skills/pr-evidence/REQUIREMENT.md +11 -17
  62. package/claude/skills/read-frames/REQUIREMENT.md +3 -8
  63. package/claude/skills/record-screencast/REQUIREMENT.md +14 -17
  64. package/claude/skills/review-address/REQUIREMENT.md +14 -38
  65. package/claude/skills/review-address/SKILL.md +18 -6
  66. package/claude/skills/review-address/references/post-review-findings.md +4 -1
  67. package/claude/skills/review-address/references/rebase-only.md +14 -2
  68. package/claude/skills/review-craft/REQUIREMENT.md +14 -32
  69. package/claude/skills/review-pr/REQUIREMENT.md +15 -34
  70. package/claude/skills/review-pr/SKILL.md +9 -2
  71. package/claude/skills/review-pr/references/body-variants.md +29 -8
  72. package/claude/skills/review-pr/references/post.md +29 -10
  73. package/claude/skills/review-ui/REQUIREMENT.md +14 -39
  74. package/claude/skills/review-ui/SKILL.md +1 -0
  75. package/claude/skills/review-ui/references/post.md +2 -0
  76. package/claude/skills/review-ui/references/tick.md +3 -0
  77. package/claude/skills/role-orchestrator/REQUIREMENT.md +2 -2
  78. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +5 -0
  79. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +9 -0
  80. package/claude/skills/role-reviewer/REQUIREMENT.md +15 -65
  81. package/claude/skills/role-reviewer/SKILL.md +8 -1
  82. package/claude/skills/role-worker/REQUIREMENT.md +2 -2
  83. package/claude/skills/role-worker/SKILL.md +3 -0
  84. package/claude/skills/script-screencast/REQUIREMENT.md +10 -16
  85. package/claude/skills/search-craft/REQUIREMENT.md +12 -36
  86. package/claude/skills/search-craft/SKILL.md +1 -1
  87. package/claude/skills/search-craft/references/adopted.md +2 -2
  88. package/claude/skills/session-compact/REQUIREMENT.md +13 -23
  89. package/claude/skills/session-map/REQUIREMENT.md +13 -29
  90. package/claude/skills/session-relay/REQUIREMENT.md +14 -58
  91. package/claude/skills/session-relay/SKILL.md +6 -0
  92. package/claude/skills/session-resume/REQUIREMENT.md +13 -27
  93. package/claude/skills/session-worktree/REQUIREMENT.md +14 -43
  94. package/claude/skills/systematic-debugging/REQUIREMENT.md +13 -34
  95. package/claude/skills/target-check/REQUIREMENT.md +16 -23
  96. package/claude/skills/target-setup/REQUIREMENT.md +11 -34
  97. package/claude/skills/task-board/REQUIREMENT.md +13 -22
  98. package/claude/skills/task-board/SKILL.md +7 -0
  99. package/claude/skills/task-board/references/decline.md +2 -2
  100. package/claude/skills/task-board/references/tasks.md +1 -1
  101. package/claude/skills/task-complete/REQUIREMENT.md +11 -24
  102. package/claude/skills/task-complete/references/archive.md +4 -2
  103. package/claude/skills/teach-workspace/REQUIREMENT.md +14 -45
  104. package/claude/skills/test-craft/REQUIREMENT.md +15 -46
  105. package/claude/skills/test-first/REQUIREMENT.md +14 -20
  106. package/claude/skills/ui-checklist/REQUIREMENT.md +14 -29
  107. package/claude/skills/ui-checklist/SKILL.md +3 -3
  108. package/claude/skills/ux-audit/REQUIREMENT.md +3 -7
  109. package/claude/skills/ux-measure/REQUIREMENT.md +14 -28
  110. package/claude/skills/video-craft/REQUIREMENT.md +14 -27
  111. package/claude/skills/write-doc/REQUIREMENT.md +14 -24
  112. package/claude/skills/write-human/REQUIREMENT.md +15 -27
  113. package/claude/skills/youtube-transcripts/REQUIREMENT.md +9 -15
  114. package/docs/agents/context-audit.md +19 -12
  115. package/docs/agents/markdown-audit.md +15 -10
  116. package/docs/target/projects.md +2 -0
  117. package/docs/workflow/skill-map.md +6 -5
  118. package/docs/workflow/zshrc-aliases.md +3 -0
  119. package/governance/renames.toml +0 -2
  120. package/governance/rules/claude/output.md +5 -1
  121. package/governance/rules/code/routing.md +5 -1
  122. package/governance/stacks/base.toml +0 -1
  123. package/package.json +1 -1
  124. package/scripts/core/bootstrap.sh +1 -0
  125. package/src/claude/cases/workflow.ts +12 -0
  126. package/src/commands/context.ts +57 -3
  127. package/src/commands/labels.ts +149 -4
  128. package/src/commands/markdown.ts +188 -21
  129. package/src/commands/pr.ts +113 -4
  130. package/src/commands/sessions.ts +58 -11
  131. package/src/commands/tasks.ts +182 -0
  132. package/src/commands/worktrees.ts +2 -0
  133. package/src/context/coverage.ts +210 -0
  134. package/src/context/gate.ts +12 -3
  135. package/src/gate/stages.ts +1 -1
  136. package/src/init/steps.ts +45 -0
  137. package/src/intake/items.ts +14 -0
  138. package/src/labels/closing.ts +46 -0
  139. package/src/labels/density.ts +363 -0
  140. package/src/markdown/gated.ts +250 -0
  141. package/src/markdown/scope.ts +115 -2
  142. package/src/migrate/rename.ts +6 -0
  143. package/src/pr/evidence.ts +1 -1
  144. package/src/pr/rest.ts +2 -0
  145. package/src/pr/tick.ts +89 -0
  146. package/src/sessions/claim.ts +7 -0
  147. package/src/sessions/live.ts +79 -6
  148. package/src/sessions/registry.ts +11 -0
  149. package/src/sessions/resolve.ts +85 -7
  150. package/src/tasks/archive.ts +15 -4
  151. package/src/tasks/closes.ts +136 -0
  152. package/src/tasks/record.ts +1 -1
  153. package/src/worktrees/reclaim.ts +16 -1
  154. package/tooling/base/configs/scripts/verify.ts +1 -1
  155. package/tooling/base/seeds/.github/pull_request_template.md +1 -1
  156. package/tooling/claude/seeds/.claude/hooks/board-validate.sh +1 -1
  157. package/tooling/claude/seeds/CLAUDE.md +2 -2
  158. package/governance/rules/canon/task-scope.md +0 -12
@@ -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: SessionRecord[] = []
217
- let confidence: Confidence = 'confirmed'
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) continue
222
- if (state.confidence === 'unverified') confidence = 'unverified'
223
- live.push(record)
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
- live.map(async (record) => present(record, await find(record.cwd), now)),
297
+ upgraded.map(async ({ record, via, confidence }) =>
298
+ present(record, await find(record.cwd), now, via, confidence),
299
+ ),
228
300
  )
229
301
 
230
- return { kind: 'resolved', dir: registry.dir, confidence, sessions }
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. */
@@ -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 declined task's bullet from the backlog. A backlog line is a
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
- return /^[-*]\s/.test(trimmed) && trimmed.includes(target)
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 removal, and
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
+ }
@@ -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> {
@@ -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 = 'gh-missing' | 'gh-failed' | 'sessions-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, or a relative link resolves to nothing on disk',
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
- <!-- - <Why this approach was taken, not what changed> -->
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 // empty' 2>/dev/null)
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/<domain>.md`. Pick which from the index anchor below.
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
- - [Command to run before committing]
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.