@erclx/canon 5.72.1 → 5.74.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 (79) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/claude/.claude-plugin/plugin.json +1 -1
  3. package/claude/skills/api-design/references/adopted.md +5 -3
  4. package/claude/skills/app-drive/REQUIREMENT.md +2 -2
  5. package/claude/skills/app-drive/SKILL.md +22 -5
  6. package/claude/skills/app-drive/references/config.md +2 -1
  7. package/claude/skills/app-drive/references/drive-record.md +13 -0
  8. package/claude/skills/app-drive/references/isolate.md +1 -1
  9. package/claude/skills/build-in-slices/references/adopted.md +4 -2
  10. package/claude/skills/code-craft/references/adopted.md +9 -7
  11. package/claude/skills/codebase-layout/references/adopted.md +8 -6
  12. package/claude/skills/create-skill/references/skill-practice.md +5 -0
  13. package/claude/skills/deprecation-migration/references/adopted.md +5 -3
  14. package/claude/skills/design-pick/references/board.md +2 -2
  15. package/claude/skills/plan-feature/SKILL.md +3 -2
  16. package/claude/skills/plan-feature/references/plan.md +12 -2
  17. package/claude/skills/plan-groundwork/SKILL.md +2 -0
  18. package/claude/skills/plan-intake/references/intake.md +3 -2
  19. package/claude/skills/pr-evidence/REQUIREMENT.md +1 -0
  20. package/claude/skills/pr-evidence/SKILL.md +2 -0
  21. package/claude/skills/pr-evidence/references/evidence.md +11 -4
  22. package/claude/skills/{ui-film/references/pr-clip.md → pr-evidence/references/reel.md} +21 -21
  23. package/claude/skills/proposal-critique/REQUIREMENT.md +34 -0
  24. package/claude/skills/proposal-critique/SKILL.md +126 -0
  25. package/claude/skills/review-craft/references/adopted.md +4 -4
  26. package/claude/skills/role-orchestrator/SKILL.md +3 -2
  27. package/claude/skills/role-orchestrator/references/orchestrator-filing.md +8 -0
  28. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +4 -1
  29. package/claude/skills/role-orchestrator/references/orchestrator-review-dispatch.md +26 -2
  30. package/claude/skills/role-orchestrator/references/orchestrator-reviewer-launch.md +22 -3
  31. package/claude/skills/role-planner/SKILL.md +3 -1
  32. package/claude/skills/role-reviewer/SKILL.md +15 -4
  33. package/claude/skills/search-craft/references/adopted.md +9 -7
  34. package/claude/skills/systematic-debugging/REQUIREMENT.md +1 -0
  35. package/claude/skills/systematic-debugging/SKILL.md +21 -8
  36. package/claude/skills/systematic-debugging/references/adopted.md +15 -4
  37. package/claude/skills/teach-craft/references/adopted.md +8 -6
  38. package/claude/skills/test-craft/references/adopted.md +14 -12
  39. package/claude/skills/test-first/SKILL.md +1 -1
  40. package/claude/skills/test-first/references/adopted.md +9 -5
  41. package/claude/skills/ui-film/REQUIREMENT.md +5 -6
  42. package/claude/skills/ui-film/SKILL.md +21 -13
  43. package/claude/skills/ui-film/references/compose-craft.md +3 -2
  44. package/claude/skills/ui-film/references/handoff.md +12 -8
  45. package/claude/skills/ui-film/references/take-craft.md +10 -0
  46. package/claude/skills/write-human/references/adopted.md +8 -3
  47. package/docs/agents/records.md +1 -1
  48. package/docs/target/index.md +1 -0
  49. package/docs/target/outside-tools.md +39 -0
  50. package/docs/workflow/operating-model.md +8 -8
  51. package/docs/workflow/parallel-features.md +1 -1
  52. package/docs/workflow/skill-map.md +2 -1
  53. package/governance/ledger/canon/plan.md +20 -0
  54. package/governance/ledger/skills/build-in-slices/SKILL.md +55 -0
  55. package/governance/ledger/skills/proposal-critique/SKILL.md +66 -0
  56. package/governance/ledger/skills/search-craft/SKILL.md +54 -0
  57. package/governance/ledger/skills/systematic-debugging/SKILL.md +64 -0
  58. package/governance/ledger/skills/test-first/SKILL.md +28 -0
  59. package/governance/ledger/skills/write-human/SKILL.md +62 -0
  60. package/governance/ledger/skills/write-human/density.md +36 -0
  61. package/governance/ledger/skills/write-human/machine-tells.md +30 -0
  62. package/governance/rules/canon/plan.md +1 -0
  63. package/package.json +1 -1
  64. package/scripts/ci/docs-only.sh +24 -0
  65. package/src/claude/cases/misc.ts +14 -0
  66. package/src/claude/skills-audit.ts +89 -8
  67. package/src/commands/intake.ts +4 -1
  68. package/src/commands/pr.ts +110 -13
  69. package/src/gate/measures.ts +1 -1
  70. package/src/gate/sequencer.ts +7 -1
  71. package/src/gate/stages.ts +36 -24
  72. package/src/intake/drive-check.ts +75 -4
  73. package/src/markdown/scope.ts +2 -0
  74. package/src/pr/checks.ts +75 -3
  75. package/src/pr/evidence.ts +62 -1
  76. package/src/pr/lift.ts +23 -2
  77. package/src/pr/merge.ts +13 -3
  78. package/src/records/backup.ts +1 -0
  79. package/src/targets/pulls.ts +42 -0
package/src/pr/checks.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { type ChecksState, type RawCheck, rollup } from '@/targets/pulls'
1
+ import {
2
+ type ChecksState,
3
+ type RawCheck,
4
+ rollupRequired,
5
+ } from '@/targets/pulls'
2
6
 
3
7
  /** One row of `repos/{owner}/{repo}/commits/<sha>/check-runs`. */
4
8
  export interface RawCheckRun {
@@ -9,6 +13,12 @@ export interface RawCheckRun {
9
13
  readonly id?: number
10
14
  }
11
15
 
16
+ /** One row of `repos/{owner}/{repo}/commits/<sha>/status`, newest per context already. */
17
+ export interface RawStatus {
18
+ readonly context?: string
19
+ readonly state?: string
20
+ }
21
+
12
22
  /** What that endpoint returns, count and rows apart. */
13
23
  export interface CheckRunListing {
14
24
  readonly total_count?: number
@@ -27,6 +37,41 @@ export interface ChecksReading {
27
37
  readonly reported: number
28
38
  /** Runs `matched` carried for a check name this reading already kept a newer run for. */
29
39
  readonly collapsed: number
40
+ /**
41
+ * Failing checks outside the required set, present only when a required set
42
+ * was read, so a reading over every check keeps its old shape.
43
+ */
44
+ readonly advisory?: readonly string[]
45
+ }
46
+
47
+ /**
48
+ * Reads the check names a base branch's rules require, from
49
+ * `repos/{owner}/{repo}/rules/branches/<base>`.
50
+ *
51
+ * Undefined when the answer is not a rule list, which a repository without
52
+ * rulesets or a refused read returns, so the caller falls back to every check.
53
+ * Classic branch protection is not read, and falls back the same way.
54
+ */
55
+ export function requiredChecks(
56
+ rules: unknown,
57
+ ): ReadonlySet<string> | undefined {
58
+ if (!Array.isArray(rules)) return undefined
59
+
60
+ const names = new Set<string>()
61
+ for (const rule of rules as readonly RequiredRule[]) {
62
+ if (rule?.type !== 'required_status_checks') continue
63
+ for (const check of rule.parameters?.required_status_checks ?? []) {
64
+ if (typeof check.context === 'string') names.add(check.context)
65
+ }
66
+ }
67
+ return names
68
+ }
69
+
70
+ interface RequiredRule {
71
+ readonly type?: string
72
+ readonly parameters?: {
73
+ readonly required_status_checks?: readonly { readonly context?: unknown }[]
74
+ }
30
75
  }
31
76
 
32
77
  export interface ConflictedReading extends ChecksReading {
@@ -68,6 +113,7 @@ export function withMergeState(
68
113
  */
69
114
  function adapt(run: RawCheckRun): RawCheck {
70
115
  return {
116
+ ...(run.name !== undefined && { name: run.name }),
71
117
  ...(run.status !== undefined && { status: run.status.toUpperCase() }),
72
118
  ...(run.conclusion !== undefined &&
73
119
  run.conclusion !== null && {
@@ -125,10 +171,22 @@ function newestPerCheck(runs: readonly RawCheckRun[]): {
125
171
  * rolls up from `newestPerCheck`'s kept rows rather than from `matched`
126
172
  * directly, so a stale failing run left behind by a rerun no longer outvotes
127
173
  * the passing run beside it.
174
+ *
175
+ * A required set narrows only that last rollup. The foreign and partial
176
+ * listing guards still read every run, since either says the set in hand is
177
+ * incomplete whichever checks gate.
178
+ *
179
+ * A ruleset can require a commit status, which the check-run listing never
180
+ * carries, so the statuses join the rollup by `context` once a non-empty
181
+ * required set was read. An unread or empty set keeps them out, since the
182
+ * every-check fallback is the reading these verbs gave before rules were read
183
+ * and it saw no status.
128
184
  */
129
185
  export function collapseChecks(
130
186
  tip: string,
131
187
  listing: CheckRunListing,
188
+ required?: ReadonlySet<string>,
189
+ statuses: readonly RawStatus[] = [],
132
190
  ): ChecksReading {
133
191
  const runs = listing.check_runs ?? []
134
192
  const matched = runs.filter((run) => run.head_sha === tip)
@@ -139,6 +197,7 @@ export function collapseChecks(
139
197
  foreign: runs.length - matched.length,
140
198
  reported: listing.total_count ?? runs.length,
141
199
  collapsed,
200
+ ...(required !== undefined && { advisory: [] }),
142
201
  }
143
202
 
144
203
  if (reading.foreign > 0) return { ...reading, state: 'pending' }
@@ -149,7 +208,20 @@ export function collapseChecks(
149
208
  // sitting outside of, which is the false green keyed on a sha alone.
150
209
  if (reading.reported > runs.length) return { ...reading, state: 'pending' }
151
210
 
152
- if (matched.length === 0) return { ...reading, state: 'pending' }
211
+ const statusRows: RawCheck[] =
212
+ required === undefined || required.size === 0
213
+ ? []
214
+ : statuses.map((status) => ({
215
+ ...(status.context !== undefined && { name: status.context }),
216
+ state: (status.state ?? '').toUpperCase(),
217
+ }))
218
+ const rows = [...kept.map(adapt), ...statusRows]
219
+ if (rows.length === 0) return { ...reading, state: 'pending' }
153
220
 
154
- return { ...reading, state: rollup(kept.map(adapt)) ?? 'pending' }
221
+ const { state, advisory } = rollupRequired(rows, required)
222
+ return {
223
+ ...reading,
224
+ state: state ?? 'pending',
225
+ ...(required !== undefined && { advisory }),
226
+ }
155
227
  }
@@ -25,6 +25,12 @@ const REEL_PREFIX = '**Reel** filmed at'
25
25
  const REEL_LINE =
26
26
  /^\*\*Reel\*\* filmed at `([0-9a-f]{7,40})`:<br><img src="([^"]+)"/
27
27
 
28
+ /** How the line naming why no local address was posted opens. */
29
+ const LOCAL_SKIP_PREFIX = '**No local preview:**'
30
+
31
+ /** How the line naming why no reel was filmed opens. */
32
+ const REEL_SKIP_PREFIX = '**No reel:**'
33
+
28
34
  /** The width a reel is filmed at, which it asks for in the comment. */
29
35
  const REEL_WIDTH = 960
30
36
 
@@ -198,6 +204,15 @@ function escapeCell(text: string): string {
198
204
  .replaceAll('|', '&#124;')
199
205
  }
200
206
 
207
+ function unescapeCell(text: string): string {
208
+ return text
209
+ .replaceAll('&#124;', '|')
210
+ .replaceAll('&quot;', '"')
211
+ .replaceAll('&gt;', '>')
212
+ .replaceAll('&lt;', '<')
213
+ .replaceAll('&amp;', '&')
214
+ }
215
+
201
216
  function evidenceImage(url: string, alt: string, width?: number): string {
202
217
  const shown = Math.min(width ?? IMAGE_WIDTH, IMAGE_WIDTH)
203
218
  return `<img src="${url}" width="${shown}" alt="${alt}">`
@@ -449,6 +464,18 @@ function reelLine(reel: EvidenceReel): string {
449
464
  return `${REEL_PREFIX} \`${head}\`:<br><img src="${escapeCell(reel.link)}" width="${REEL_WIDTH}" alt="reel filmed at ${head}">`
450
465
  }
451
466
 
467
+ /** Why a render carries no local address or no reel, each a reason code and any one-line message the caller joined to it. */
468
+ export interface EvidenceSkips {
469
+ readonly local?: string
470
+ readonly reel?: string
471
+ }
472
+
473
+ /** A skip line, or nothing for an empty reason. A reason is folded onto one line, since a line break would end the address region early. */
474
+ function skipLines(prefix: string, reason: string | undefined): string[] {
475
+ const text = reason?.replace(/\s+/g, ' ').trim() ?? ''
476
+ return text === '' ? [] : [`${prefix} ${escapeCell(text)}`, '']
477
+ }
478
+
452
479
  export function evidenceMarker(head: string): string {
453
480
  return `${MARKER_PREFIX} head=${head} -->`
454
481
  }
@@ -480,6 +507,10 @@ export function evidenceMarker(head: string): string {
480
507
  *
481
508
  * A reel sits on its own line under the addresses, labeled with the head it
482
509
  * was filmed at, so a reel carried past a later push never reads as current.
510
+ *
511
+ * A skip names why the local address or the reel is missing, on its own line
512
+ * where that address or reel would sit. Each is dropped whenever its address
513
+ * or reel renders, carried or passed, so a skip never outlives its cause.
483
514
  */
484
515
  export function renderEvidenceBody(
485
516
  states: readonly EvidenceState[],
@@ -490,6 +521,7 @@ export function renderEvidenceBody(
490
521
  checklist?: string,
491
522
  local?: string,
492
523
  reel?: EvidenceReel,
524
+ skips: EvidenceSkips = {},
493
525
  ): string {
494
526
  const segments = [
495
527
  ...(preview === undefined ? [] : [`${PREVIEW_PREFIX} ${preview}`]),
@@ -499,7 +531,10 @@ export function renderEvidenceBody(
499
531
  EVIDENCE_HEADING,
500
532
  '',
501
533
  ...(segments.length === 0 ? [] : [segments.join(ADDRESS_SEPARATOR), '']),
502
- ...(reel === undefined ? [] : [reelLine(reel), '']),
534
+ ...(local === undefined ? skipLines(LOCAL_SKIP_PREFIX, skips.local) : []),
535
+ ...(reel === undefined
536
+ ? skipLines(REEL_SKIP_PREFIX, skips.reel)
537
+ : [reelLine(reel), '']),
503
538
  ]
504
539
  const closing =
505
540
  checklist === undefined
@@ -756,6 +791,32 @@ export function findEvidenceReel(
756
791
  return undefined
757
792
  }
758
793
 
794
+ /**
795
+ * The skips the marked comment already carries, read from the address region
796
+ * only, so a line quoted in a checklist is never taken for one. A re-render
797
+ * passing no skip carries these forward the way it carries an address.
798
+ */
799
+ export function findEvidenceSkips(
800
+ comments: readonly EvidenceComment[],
801
+ ): EvidenceSkips {
802
+ const marked = findMarked(comments)
803
+ if (marked === undefined) return {}
804
+ let local: string | undefined
805
+ let reel: string | undefined
806
+ for (const raw of marked.body.split('\n')) {
807
+ const line = raw.trim()
808
+ if (REGION_ENDS.some((end) => line.startsWith(end))) break
809
+ const ownSkip = readSegment(line, LOCAL_SKIP_PREFIX)
810
+ const reelSkip = readSegment(line, REEL_SKIP_PREFIX)
811
+ if (ownSkip !== undefined) local ??= unescapeCell(ownSkip)
812
+ if (reelSkip !== undefined) reel ??= unescapeCell(reelSkip)
813
+ }
814
+ return {
815
+ ...(local !== undefined && { local }),
816
+ ...(reel !== undefined && { reel }),
817
+ }
818
+ }
819
+
759
820
  /**
760
821
  * The reel a render embeds: a passed link, filmed at this head, replaces
761
822
  * whatever the comment carries, and with none passed the posted reel carries
package/src/pr/lift.ts CHANGED
@@ -83,8 +83,19 @@ export interface EvidenceGate {
83
83
  }
84
84
 
85
85
  export interface ChecksGate {
86
+ /** The state over the checks the base requires, or every check when none were read. */
86
87
  readonly state: ChecksState
87
88
  readonly isConflicted: boolean
89
+ /** Failing checks outside the required set, which are reported and never gate. */
90
+ readonly advisory?: readonly string[]
91
+ }
92
+
93
+ /** The advisory field a record carries, absent when no advisory check failed. */
94
+ export function advisoryOf(reading: ChecksGate): {
95
+ advisory?: readonly string[]
96
+ } {
97
+ const advisory = reading.advisory ?? []
98
+ return advisory.length > 0 ? { advisory } : {}
88
99
  }
89
100
 
90
101
  /**
@@ -129,6 +140,8 @@ export interface LiftRecord {
129
140
  readonly carriedFrom?: string
130
141
  readonly paintedPaths?: readonly string[]
131
142
  readonly drivenPaths?: readonly string[]
143
+ /** Failing checks the base does not require, named so a reader sees them before merging. */
144
+ readonly advisory?: readonly string[]
132
145
  readonly isLifted: boolean
133
146
  /** Whether the caller can treat the pull request as ready afterwards. */
134
147
  readonly isOk: boolean
@@ -247,7 +260,11 @@ function standingGate(standing: Standing): GateResult {
247
260
  }
248
261
 
249
262
  type CiOutcome =
250
- | { readonly kind: 'passing'; readonly waited: number }
263
+ | {
264
+ readonly kind: 'passing'
265
+ readonly waited: number
266
+ readonly reading: ChecksGate
267
+ }
251
268
  | {
252
269
  readonly kind: 'refused'
253
270
  readonly reason: LiftRefusal
@@ -274,7 +291,9 @@ async function waitForChecks(
274
291
  if (reading === undefined) {
275
292
  return { kind: 'refused', reason: 'gh-failed', waited }
276
293
  }
277
- if (reading.state === 'passing') return { kind: 'passing', waited }
294
+ if (reading.state === 'passing') {
295
+ return { kind: 'passing', waited, reading }
296
+ }
278
297
  if (reading.state === 'failing') {
279
298
  return { kind: 'refused', reason: 'ci-failing', waited }
280
299
  }
@@ -422,6 +441,7 @@ export async function lift(
422
441
  gates: all,
423
442
  waitedSeconds: 0,
424
443
  ...carried(all),
444
+ ...advisoryOf(reading),
425
445
  }
426
446
  }
427
447
 
@@ -485,6 +505,7 @@ export async function lift(
485
505
  gates: cleared,
486
506
  waitedSeconds: ci.waited,
487
507
  ...carried(cleared),
508
+ ...advisoryOf(ci.reading),
488
509
  }
489
510
  }
490
511
 
package/src/pr/merge.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ChecksGate } from '@/pr/lift'
1
+ import { advisoryOf, type ChecksGate } from '@/pr/lift'
2
2
  import { readSection } from '@/pr/paths'
3
3
  import { UI_PASS_MARKER } from '@/pr/tick'
4
4
 
@@ -67,6 +67,8 @@ export interface MergeRecord {
67
67
  readonly description?: string
68
68
  /** The squash commit, present only on a merge. */
69
69
  readonly sha?: string
70
+ /** Failing checks the base does not require, which the merge went past. */
71
+ readonly advisory?: readonly string[]
70
72
  }
71
73
 
72
74
  export type Composed =
@@ -230,8 +232,15 @@ export async function merge(
230
232
  )
231
233
  }
232
234
 
233
- if (opts.isDryRun)
234
- return { reason: 'dry-run', isOk: true, number, ...message }
235
+ if (opts.isDryRun) {
236
+ return {
237
+ reason: 'dry-run',
238
+ isOk: true,
239
+ number,
240
+ ...message,
241
+ ...advisoryOf(checks),
242
+ }
243
+ }
235
244
 
236
245
  const outcome = await api.putMerge({ ...message, sha: pull.head })
237
246
  if (outcome.kind === 'merged') {
@@ -241,6 +250,7 @@ export async function merge(
241
250
  number,
242
251
  ...message,
243
252
  sha: outcome.sha,
253
+ ...advisoryOf(checks),
244
254
  }
245
255
  }
246
256
  return refused(
@@ -107,6 +107,7 @@ export const EXCLUDED_ENTRIES = [
107
107
  /** Media under a video workspace, remade from its plan and composition, so never backed up. */
108
108
  export const MEDIA_PATHSPECS = [
109
109
  ':(exclude,glob)video/*/takes/**',
110
+ ':(exclude,glob)video/*/scratch/**',
110
111
  ':(exclude,glob)video/**/renders/**',
111
112
  ':(exclude,glob)video/**/snapshots/**',
112
113
  ':(exclude,glob)video/**/node_modules/**',
@@ -94,6 +94,8 @@ interface RawPull {
94
94
  * rather than writing a second collapse beside `rollup`.
95
95
  */
96
96
  export interface RawCheck {
97
+ /** The check-run name, which a ruleset's required context matches. */
98
+ readonly name?: string
97
99
  readonly status?: string
98
100
  readonly conclusion?: string
99
101
  readonly state?: string
@@ -137,6 +139,46 @@ export function rollup(checks: readonly RawCheck[]): ChecksState | null {
137
139
  return running ? 'pending' : 'passing'
138
140
  }
139
141
 
142
+ /** A head's state over the checks the base requires, with failing others named apart. */
143
+ export interface RequiredRollup {
144
+ readonly state: ChecksState | null
145
+ /** Names of checks outside the required set that failed, which never gate. */
146
+ readonly advisory: readonly string[]
147
+ }
148
+
149
+ /**
150
+ * Collapses a head's checks over the set the base branch's rules require.
151
+ *
152
+ * An unread or empty set gates by every check, which is `rollup` itself and
153
+ * the safe default for a repository whose rules list nothing. A required name
154
+ * with no run on the head reads pending rather than passing, since a run not
155
+ * yet created has not cleared anything.
156
+ */
157
+ export function rollupRequired(
158
+ checks: readonly RawCheck[],
159
+ required: ReadonlySet<string> | undefined,
160
+ ): RequiredRollup {
161
+ if (required === undefined || required.size === 0) {
162
+ return { state: rollup(checks), advisory: [] }
163
+ }
164
+
165
+ const isRequired = (check: RawCheck): boolean =>
166
+ check.name !== undefined && required.has(check.name)
167
+ const gating = checks.filter(isRequired)
168
+ const advisory = checks
169
+ .filter((check) => !isRequired(check) && rollup([check]) === 'failing')
170
+ .map((check) => check.name ?? '')
171
+
172
+ const state = rollup(gating)
173
+ const isMissing = [...required].some(
174
+ (name) => !gating.some((check) => check.name === name),
175
+ )
176
+ return {
177
+ state: state !== 'failing' && isMissing ? 'pending' : state,
178
+ advisory,
179
+ }
180
+ }
181
+
140
182
  /**
141
183
  * Reads the heading of the newest pass carrying one, matching on the first line
142
184
  * alone the way `poll.ts` does.