@erclx/canon 5.27.0 → 5.29.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 (177) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/draft-ready/SKILL.md +3 -1
  3. package/claude/skills/git-pr/references/evidence.md +1 -1
  4. package/claude/skills/review-address/REQUIREMENT.md +4 -2
  5. package/claude/skills/review-address/SKILL.md +16 -7
  6. package/claude/skills/review-address/references/rebase-conflicts.md +4 -2
  7. package/claude/skills/review-craft/references/ui.md +4 -4
  8. package/claude/skills/review-ui/REQUIREMENT.md +2 -2
  9. package/claude/skills/review-ui/SKILL.md +13 -11
  10. package/claude/skills/review-ui/references/driving.md +8 -8
  11. package/claude/skills/review-ui/references/post.md +19 -7
  12. package/claude/skills/review-ui/references/tick.md +22 -0
  13. package/claude/skills/role-orchestrator/REQUIREMENT.md +15 -15
  14. package/claude/skills/role-orchestrator/SKILL.md +24 -26
  15. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +18 -16
  16. package/claude/skills/role-orchestrator/references/orchestrator-handback.md +12 -2
  17. package/claude/skills/role-orchestrator/references/orchestrator-inbound.md +1 -1
  18. package/claude/skills/role-orchestrator/references/orchestrator-launch.md +1 -1
  19. package/claude/skills/role-orchestrator/references/orchestrator-output.md +4 -4
  20. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +5 -5
  21. package/claude/skills/role-orchestrator/references/orchestrator-poll.md +11 -2
  22. package/claude/skills/role-orchestrator/references/orchestrator-refill.md +6 -6
  23. package/claude/skills/role-orchestrator/references/orchestrator-sweep.md +2 -2
  24. package/claude/skills/role-orchestrator/scripts/poll.ts +304 -10
  25. package/claude/skills/role-planner/REQUIREMENT.md +1 -1
  26. package/claude/skills/role-planner/SKILL.md +2 -2
  27. package/claude/skills/role-reviewer/SKILL.md +1 -1
  28. package/claude/skills/role-worker/SKILL.md +3 -2
  29. package/claude/skills/ui-checklist/SKILL.md +1 -1
  30. package/docs/agents/canvas.md +12 -4
  31. package/docs/agents/gate.md +3 -1
  32. package/docs/agents/index.md +2 -1
  33. package/docs/agents/pr-evidence.md +15 -14
  34. package/docs/agents/pr-tick.md +72 -0
  35. package/docs/agents/sandbox.md +2 -2
  36. package/docs/agents/tasks-plans.md +1 -1
  37. package/docs/agents/tasks-validate.md +18 -18
  38. package/docs/agents/tasks.md +1 -1
  39. package/docs/workflow/operating-model.md +10 -15
  40. package/docs/workflow/parallel-features.md +7 -7
  41. package/package.json +1 -1
  42. package/src/audits/baseline.ts +1 -1
  43. package/src/audits/run.ts +1 -1
  44. package/src/canvas/capture.ts +1 -1
  45. package/src/canvas/client/app.tsx +105 -4
  46. package/src/canvas/client/inspector/field.tsx +61 -2
  47. package/src/canvas/client/inspector/fields.css +78 -0
  48. package/src/canvas/client/inspector/token-select.tsx +109 -0
  49. package/src/canvas/client/inspector/values.ts +74 -2
  50. package/src/canvas/client/inspector.tsx +134 -15
  51. package/src/canvas/client/keys.ts +71 -0
  52. package/src/canvas/client/pages-panel.tsx +5 -1
  53. package/src/canvas/client/panel-handle.tsx +90 -0
  54. package/src/canvas/client/selection.tsx +3 -1
  55. package/src/canvas/client/shell.css +194 -4
  56. package/src/canvas/client/state.ts +255 -11
  57. package/src/canvas/client/surface.tsx +231 -18
  58. package/src/canvas/client/tools.tsx +153 -0
  59. package/src/canvas/content.ts +66 -1
  60. package/src/canvas/edit.ts +207 -25
  61. package/src/canvas/history.ts +116 -0
  62. package/src/canvas/server.ts +119 -13
  63. package/src/canvas/tokens.ts +1 -1
  64. package/src/claude/seeds.ts +2 -2
  65. package/src/claude/skills-reach.ts +1 -1
  66. package/src/{exec.ts → cli/exec.ts} +2 -2
  67. package/src/{help.ts → cli/help.ts} +1 -1
  68. package/src/cli.ts +1 -1
  69. package/src/commands/audits.ts +9 -1
  70. package/src/commands/autoship.ts +1 -1
  71. package/src/commands/canvas.ts +2 -2
  72. package/src/commands/capture.ts +1 -1
  73. package/src/commands/census.ts +1 -1
  74. package/src/commands/claude.ts +5 -5
  75. package/src/commands/comments.ts +1 -1
  76. package/src/commands/context.ts +1 -1
  77. package/src/commands/demo.ts +9 -1
  78. package/src/commands/deps.ts +1 -1
  79. package/src/commands/design.ts +12 -4
  80. package/src/commands/docs.ts +2 -2
  81. package/src/commands/driver.ts +9 -1
  82. package/src/commands/feedback.ts +3 -3
  83. package/src/commands/gate.ts +10 -3
  84. package/src/commands/gov.ts +3 -3
  85. package/src/commands/indexes.ts +1 -1
  86. package/src/commands/init.ts +11 -3
  87. package/src/commands/intake.ts +1 -1
  88. package/src/commands/inventory.ts +9 -1
  89. package/src/commands/labels.ts +1 -1
  90. package/src/commands/markdown.ts +1 -1
  91. package/src/commands/migrate.ts +2 -2
  92. package/src/commands/pr.ts +324 -9
  93. package/src/commands/records.ts +1 -1
  94. package/src/commands/repo.ts +1 -1
  95. package/src/commands/sandbox.ts +11 -14
  96. package/src/commands/secrets.ts +9 -1
  97. package/src/commands/sessions.ts +1 -1
  98. package/src/commands/slides.ts +2 -2
  99. package/src/commands/standards.ts +1 -1
  100. package/src/commands/sync.ts +3 -3
  101. package/src/commands/targets.ts +1 -1
  102. package/src/commands/tasks.ts +20 -3
  103. package/src/commands/teach.ts +1 -1
  104. package/src/commands/tooling.ts +2 -2
  105. package/src/commands/transcripts.ts +2 -2
  106. package/src/commands/upgrade.ts +2 -2
  107. package/src/commands/wiki.ts +2 -2
  108. package/src/commands/worktrees.ts +1 -1
  109. package/src/context/architecture.ts +1 -1
  110. package/src/context/citations.ts +2 -2
  111. package/src/context/classify/extract.ts +1 -1
  112. package/src/context/folders.ts +2 -2
  113. package/src/context/requirements.ts +1 -1
  114. package/src/docs/read.ts +1 -1
  115. package/src/feedback/github.ts +1 -1
  116. package/src/gate/measures.ts +2 -2
  117. package/src/gate/sequencer.ts +55 -3
  118. package/src/gate/stages.ts +8 -0
  119. package/src/gate/suite-lock.ts +214 -0
  120. package/src/init/run.ts +1 -1
  121. package/src/intake/folder.ts +1 -1
  122. package/src/labels/map.ts +1 -1
  123. package/src/labels/phase.ts +3 -3
  124. package/src/migrate/record-layout.ts +1 -1
  125. package/src/migrate/records.ts +11 -11
  126. package/src/migrate/rename.ts +1 -1
  127. package/src/migrate/scratch-evidence.ts +1 -1
  128. package/src/migrate/surface-roots.ts +6 -6
  129. package/src/pr/evidence.ts +188 -16
  130. package/src/pr/tick.ts +56 -0
  131. package/src/process/harness.ts +1 -1
  132. package/src/records/backup.ts +1 -1
  133. package/src/records/ordinal.ts +1 -1
  134. package/src/records/prune.ts +1 -1
  135. package/src/records/size.ts +1 -1
  136. package/src/records/stale-canonical.ts +1 -1
  137. package/src/records/stale.ts +1 -1
  138. package/src/records/validate.ts +2 -2
  139. package/src/{project-root.ts → roots/project.ts} +2 -2
  140. package/src/{record-root.ts → roots/record.ts} +1 -1
  141. package/src/sandbox/coverage.ts +11 -12
  142. package/src/sandbox/equivalence.ts +57 -26
  143. package/src/sandbox/headless/run.ts +1 -1
  144. package/src/sandbox/hooks.ts +1 -1
  145. package/src/sandbox/provision.ts +79 -29
  146. package/src/sandbox/scenario.ts +588 -0
  147. package/src/serve/report.ts +1 -1
  148. package/src/sessions/live.ts +4 -1
  149. package/src/sessions/transfer/bundle.ts +1 -1
  150. package/src/slides/decks.ts +1 -1
  151. package/src/standards/read.ts +1 -1
  152. package/src/sync/engine.ts +2 -2
  153. package/src/sync/git.ts +1 -1
  154. package/src/sync/layout.ts +2 -2
  155. package/src/sync/stamp.ts +1 -1
  156. package/src/sync/workflow.ts +1 -1
  157. package/src/targets/validate.ts +2 -2
  158. package/src/tasks/answers.ts +1 -1
  159. package/src/tasks/archive.ts +1 -1
  160. package/src/tasks/label.ts +1 -1
  161. package/src/tasks/reach.ts +1 -1
  162. package/src/tasks/validate.ts +38 -14
  163. package/src/teach/nav.ts +1 -1
  164. package/src/teach/workspace.ts +1 -1
  165. package/src/tooling/inject.ts +2 -2
  166. package/src/tooling/read.ts +1 -1
  167. package/src/tooling/scan.ts +1 -1
  168. package/src/tooling/verify-stack.ts +1 -1
  169. package/src/version/installed.ts +1 -1
  170. package/src/version/skew.ts +1 -1
  171. package/standards/board.md +3 -3
  172. package/standards/plan.md +13 -7
  173. package/tsconfig.json +1 -1
  174. /package/src/{cli-run.ts → cli/run.ts} +0 -0
  175. /package/src/{ui.ts → cli/ui.ts} +0 -0
  176. /package/src/{legacy-path.ts → roots/legacy.ts} +0 -0
  177. /package/src/{surface-root.ts → roots/surface.ts} +0 -0
@@ -22,7 +22,7 @@ import {
22
22
  palette,
23
23
  pipeOutput,
24
24
  select,
25
- } from '@/ui'
25
+ } from '@/cli/ui'
26
26
 
27
27
  const PROTECTED_BRANCHES: readonly string[] = ['main', 'master']
28
28
 
@@ -1,7 +1,7 @@
1
1
  import { statSync } from 'node:fs'
2
2
  import { resolve } from 'node:path'
3
- import { checkoutMismatchWarning } from '@/project-root'
4
- import { logError, logWarn, outro } from '@/ui'
3
+ import { checkoutMismatchWarning } from '@/roots/project'
4
+ import { logError, logWarn, outro } from '@/cli/ui'
5
5
 
6
6
  export function isDirectory(path: string): boolean {
7
7
  try {
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { readFile } from 'node:fs/promises'
3
3
  import { isAbsolute, relative, resolve } from 'node:path'
4
4
  import { isUnder } from '@/tasks/paths'
5
- import { recordDir, recordDirs } from '@/record-root'
5
+ import { recordDir, recordDirs } from '@/roots/record'
6
6
  import {
7
7
  hasStagedBatches,
8
8
  normalizeOperatorCall,
@@ -3,7 +3,7 @@ import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises'
3
3
  import { basename, dirname, join, relative, resolve, sep } from 'node:path'
4
4
  import { regenOne } from '@/indexes/regen'
5
5
  import { isUnder } from '@/tasks/paths'
6
- import { recordDir, recordDirs } from '@/record-root'
6
+ import { recordDir, recordDirs } from '@/roots/record'
7
7
 
8
8
  const TASKS = 'tasks'
9
9
  const PLANS = 'plans'
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs'
2
2
  import { mkdir, readdir } from 'node:fs/promises'
3
3
  import { relative } from 'node:path'
4
- import { recordDir } from '@/record-root'
4
+ import { recordDir } from '@/roots/record'
5
5
  import {
6
6
  archiveDir,
7
7
  declinedDir,
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs'
2
2
  import { readdir, readFile } from 'node:fs/promises'
3
3
  import { basename, join } from 'node:path'
4
4
  import { listChangedFiles, resolveBaseRef } from '@/git/files'
5
- import { recordDir } from '@/record-root'
5
+ import { recordDir } from '@/roots/record'
6
6
  import { splitPlanSections } from '@/records/validate'
7
7
  import { type AnswersRefused, resolvePlanReference } from '@/tasks/answers'
8
8
  import { orderingPath, readBoard } from '@/tasks/validate'
@@ -50,7 +50,6 @@ export const FINDING_KINDS = [
50
50
  'row-misordered',
51
51
  'row-unranked',
52
52
  'touches-unstated',
53
- 'touches-collided',
54
53
  'blocker-settled',
55
54
  'blocker-unresolved',
56
55
  'blocker-declined',
@@ -89,6 +88,20 @@ export interface FolderClaim {
89
88
  readonly message: string
90
89
  }
91
90
 
91
+ /**
92
+ * A path two `## Run now` rows both name in their `Touches` cells. A shared
93
+ * file is no hold, since the dispatch gate starts both rows and the branch
94
+ * merging second rebases, so the pair reports beside the findings and moves no
95
+ * exit code. It stays listed because merge order reads it, and because it is
96
+ * the candidate list a dispatcher reads for the dependency, contract,
97
+ * relocation, and sweep holds a `Touches` cell cannot state.
98
+ */
99
+ export interface SharedFiles {
100
+ readonly group: BoardGroup
101
+ readonly subject: string
102
+ readonly message: string
103
+ }
104
+
92
105
  /**
93
106
  * A code span in a board cell that the markdown preview cannot break, so it
94
107
  * pushes the table past the pane and makes it scroll sideways. Either the
@@ -151,6 +164,7 @@ export interface ValidateReport {
151
164
  readonly findings: readonly Finding[]
152
165
  readonly untested: readonly Untested[]
153
166
  readonly claims: readonly FolderClaim[]
167
+ readonly shared: readonly SharedFiles[]
154
168
  readonly wide: readonly WideToken[]
155
169
  readonly unplaced: readonly Unplaced[]
156
170
  }
@@ -892,17 +906,16 @@ function planPath(target: string, dir: string, root: string): string {
892
906
  }
893
907
 
894
908
  /**
895
- * The half of the `## Run now` test a person cannot check by eye. Two rows a
896
- * worker may be handed at once must touch disjoint files, and the `Touches`
897
- * column is the only place either set is written down.
909
+ * Reports a `## Run now` row stating no file set. The `Touches` column is the
910
+ * only place a row's set is written down, so a row stating none drops out of
911
+ * every pair `checkSharedFiles` reads, and its merge order and reach go unread.
898
912
  */
899
- function checkCollisions(rows: readonly BoardRow[]): Finding[] {
913
+ function checkTouches(rows: readonly BoardRow[]): Finding[] {
900
914
  const findings: Finding[] = []
901
- const ready = rows.filter((row) => row.group === 'Run now')
902
915
 
903
- for (const row of ready) {
904
- // An absent column and an unreadable one both leave the row untested by
905
- // the loop below, so reporting only the second would pass a board whose
916
+ for (const row of rows.filter((candidate) => candidate.group === 'Run now')) {
917
+ // An absent column and an unreadable one both leave the row out of every
918
+ // pair, so reporting only the second would pass a board whose
906
919
  // `## Run now` table declares no file set at all.
907
920
  if (!row.touches || row.touches.length === 0) {
908
921
  findings.push({
@@ -914,6 +927,17 @@ function checkCollisions(rows: readonly BoardRow[]): Finding[] {
914
927
  }
915
928
  }
916
929
 
930
+ return findings
931
+ }
932
+
933
+ /**
934
+ * Lists every path two `## Run now` rows both name, one entry per pair of
935
+ * rows. It reports rather than fails, since sharing a file holds nothing.
936
+ */
937
+ function checkSharedFiles(rows: readonly BoardRow[]): SharedFiles[] {
938
+ const pairs: SharedFiles[] = []
939
+ const ready = rows.filter((row) => row.group === 'Run now')
940
+
917
941
  for (let i = 0; i < ready.length; i += 1) {
918
942
  for (let j = i + 1; j < ready.length; j += 1) {
919
943
  const left = ready[i]
@@ -929,8 +953,7 @@ function checkCollisions(rows: readonly BoardRow[]): Finding[] {
929
953
 
930
954
  if (shared.length === 0) continue
931
955
 
932
- findings.push({
933
- kind: 'touches-collided',
956
+ pairs.push({
934
957
  group: 'Run now',
935
958
  subject: `${subjectOf(left)} and ${subjectOf(right)}`,
936
959
  message: `both touch ${joinShared(shared)}.`,
@@ -938,7 +961,7 @@ function checkCollisions(rows: readonly BoardRow[]): Finding[] {
938
961
  }
939
962
  }
940
963
 
941
- return findings
964
+ return pairs
942
965
  }
943
966
 
944
967
  function subjectOf(row: BoardRow): string {
@@ -982,7 +1005,7 @@ function joinShared(clauses: readonly string[]): string {
982
1005
  * the row does rewrite the directory, so this states the reach rather than
983
1006
  * calling it a defect.
984
1007
  *
985
- * The scan takes `## Run now` alone, where `checkCollisions` takes it. A cell in
1008
+ * The scan takes `## Run now` alone, where `checkSharedFiles` takes it. A cell in
986
1009
  * another group describes work nobody has planned, so it is written as a
987
1010
  * sentence and rewritten at planning time, and a claim read off one reports on
988
1011
  * prose rather than on a file set. That is the shape that teaches a reader to
@@ -1377,7 +1400,7 @@ export async function validateBoard(
1377
1400
  ...checkPlans(rows, dir, root),
1378
1401
  ...(await checkPlanAgreement(rows, dir, root)),
1379
1402
  ...(await checkGroupClaims(rows, dir, root)),
1380
- ...checkCollisions(rows),
1403
+ ...checkTouches(rows),
1381
1404
  ...checkOrdering(rows),
1382
1405
  ...parked.findings,
1383
1406
  ]
@@ -1391,6 +1414,7 @@ export async function validateBoard(
1391
1414
  findings,
1392
1415
  untested: parked.untested,
1393
1416
  claims: checkFolderClaims(rows, root),
1417
+ shared: checkSharedFiles(rows),
1394
1418
  wide: checkWideTokens(rows),
1395
1419
  unplaced: checkUnplaced(rows, backlog, stems),
1396
1420
  }
package/src/teach/nav.ts CHANGED
@@ -9,7 +9,7 @@ import { buildDesignCss } from '@/design/css'
9
9
  import { FAVICON_COLORS, faviconLink, renderFavicon } from '@/design/favicon'
10
10
  import { stripFrontmatter } from '@/markdown/frontmatter'
11
11
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
12
- import { PROJECT_ROOT } from '@/project-root'
12
+ import { PROJECT_ROOT } from '@/roots/project'
13
13
  import { compileScript } from '@/teach/browser/compile'
14
14
  import { dismiss } from '@/teach/browser/dismiss'
15
15
  import { focusLine } from '@/teach/browser/focus-line'
@@ -5,7 +5,7 @@ import { TEACH_STYLESHEET_COMPONENTS } from '@/design/components'
5
5
  import { buildDesignCss } from '@/design/css'
6
6
  import { parseFrontmatter, readField } from '@/indexes/frontmatter'
7
7
  import { type BodyLine, bodyLines } from '@/markdown/scan'
8
- import { recordDir } from '@/record-root'
8
+ import { recordDir } from '@/roots/record'
9
9
  import { TEACH_FONT_FACES } from '@/teach/fonts'
10
10
  import { writeIfChanged } from '@/teach/write-if-changed'
11
11
 
@@ -4,7 +4,7 @@ import { dirname, join, relative } from 'node:path'
4
4
  import { $ } from 'bun'
5
5
  import { copyPreservingMode } from '@/sync/copy'
6
6
  import { rewritesOnInstall, stripSeedMarker } from '@/tooling/seed-marker'
7
- import { resolveSurfacePath } from '@/surface-root'
7
+ import { resolveSurfacePath } from '@/roots/surface'
8
8
  import { mergeSections, pruneSections } from '@/tooling/gitignore'
9
9
  import {
10
10
  ancestorsFirst,
@@ -27,7 +27,7 @@ import {
27
27
  SUBFOLDER_SPELL_CONFIG,
28
28
  subfolderSpellConfig,
29
29
  } from '@/tooling/subfolder'
30
- import { logAdd, logRemove, logStep, logWarn } from '@/ui'
30
+ import { logAdd, logRemove, logStep, logWarn } from '@/cli/ui'
31
31
 
32
32
  export async function injectConfigs(
33
33
  chain: readonly Manifest[],
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { PROJECT_ROOT } from '@/project-root'
3
+ import { PROJECT_ROOT } from '@/roots/project'
4
4
 
5
5
  /**
6
6
  * Spells the package root in a report where every other root spells a
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { resolveSurfacePath } from '@/surface-root'
3
+ import { resolveSurfacePath } from '@/roots/surface'
4
4
  import { mergeSections } from '@/tooling/gitignore'
5
5
  import {
6
6
  ancestorsFirst,
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { execa, type Options } from 'execa'
4
4
  import { isStackExcluded, loadManifest } from '@/tooling/manifest'
5
- import { intro, logError, logInfo, logStep, logWarn, outro } from '@/ui'
5
+ import { intro, logError, logInfo, logStep, logWarn, outro } from '@/cli/ui'
6
6
 
7
7
  const SCREENSHOT_TIMEOUT_MS = 60_000
8
8
  const TAIL_LINES = 20
@@ -1,6 +1,6 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { PROJECT_ROOT } from '@/project-root'
3
+ import { PROJECT_ROOT } from '@/roots/project'
4
4
 
5
5
  /** What a reader prints in place of a field the manifest did not carry. */
6
6
  export const UNKNOWN_LABEL = 'unknown'
@@ -1,4 +1,4 @@
1
- import { PROJECT_ROOT } from '@/project-root'
1
+ import { PROJECT_ROOT } from '@/roots/project'
2
2
  import { compareVersions, parseVersion } from '@/version/compare'
3
3
  import {
4
4
  type InstalledPackage,
@@ -21,7 +21,7 @@ Does not govern:
21
21
 
22
22
  A board works when a session reading it alone can answer:
23
23
 
24
- - Which task can a worker be handed now, and which files would it collide on?
24
+ - Which task can a worker be handed now, and which files does it share with work already running?
25
25
  - What is each blocked task waiting on, and what would clear it?
26
26
  - Which task gets planned next, and what is it ranked against?
27
27
 
@@ -49,7 +49,7 @@ Group tasks by readiness rather than by status, one row per task, under the colu
49
49
 
50
50
  Readiness is three groups under fixed headings, `## Run now`, `## Up next`, and `## Needs a plan`, in that order. The names are the contract rather than a suggestion, because a board grouped by readiness under names of its own satisfies every other rule here and still reads as empty to anything counting rows under a heading. Add no fourth group. A task belongs to exactly one, and the tests are read in order.
51
51
 
52
- - `## Run now`: a written plan covers every open outcome, and the task carries no reason it cannot start. A collision against the files something already running touches is one such reason, and the `Touches` column is what states it. A worker is handed a task from this group alone.
52
+ - `## Run now`: a written plan covers every open outcome, and the task carries no reason it cannot start. A collision with running work is one such reason only where no merge settles it, being a contract, a relocation, or a sweep. A shared file alone is none, since the branch merging second rebases. The `Touches` column states the set to read. A worker is handed a task from this group alone.
53
53
  - `## Up next`: a written plan exists, and the task carries a stated reason it cannot start. The `Waiting on` column names that reason.
54
54
  - `## Needs a plan`: everything else. The task has no plan, or the plan it carries no longer describes the work.
55
55
 
@@ -69,7 +69,7 @@ Position alone carries it, and no rank column exists. A number beside each row i
69
69
 
70
70
  The `Waiting on` column under `## Up next` carries that reason in one of three forms. `## Needs a plan` states no file set at all, because a task with no plan has no bounded one to state. A group with no rows keeps its heading and its header row.
71
71
 
72
- Under `## Up next` a collision names the file held by the task already running, a sibling task names that task, and an external condition names both the condition and what would satisfy it. Naming what would satisfy it is what separates a blocked row from one nobody has examined, so a cell stating a condition with no way out of it fails the test. The header text is the contract the way the group names are, because anything reading the cell resolves the column by header rather than by position.
72
+ Under `## Up next` a collision names the file held by the task already running and which hold it is: a contract, a relocation, or a sweep. A sibling task names that task, and an external condition names both the condition and what would satisfy it. Naming what would satisfy it is what separates a blocked row from one nobody has examined, so a cell stating a condition with no way out of it fails the test. The header text is the contract the way the group names are, because anything reading the cell resolves the column by header rather than by position.
73
73
 
74
74
  Under `## Needs a plan` the cell carries two halves and each takes one clause: what the task needs before it can be planned, then why it sits at this position. The second clause is comparative, and it names what the row is ranked against in one of two forms: a sibling row, or the class of rows it sits in. The row `leads`, `heads`, `opens`, `closes`, `trails`, `precedes`, `follows`, or `outranks` whichever it names, or `sits under`, `above`, or `below` it. Either form puts something other than this row in the clause, which is what makes the position readable by anyone but its author.
75
75
 
package/standards/plan.md CHANGED
@@ -18,6 +18,8 @@ Applies to a feature plan at `.canon/plans/feature-<slug>.md`. One file holds on
18
18
 
19
19
  The folder is gitignored, and backed by `canon records push` wherever a records remote is configured. Nothing backs a plan deleted before a push, which is why the archive step is a move rather than a cleanup.
20
20
 
21
+ <!-- canon-length-exempt: the constraint forms and the answer contract share one reader, and a split into a second standard is queued as its own change -->
22
+
21
23
  ## Scope
22
24
 
23
25
  Governs a feature plan under `.canon/plans/feature-<slug>.md`: the filename, the required sections, what each holds, and the suggested-and-answer contract.
@@ -40,7 +42,7 @@ A plan works when a session holding none of the conversation that produced it ca
40
42
  - What is being built, and which files does it change?
41
43
  - Which decisions are already settled, and which are still open?
42
44
  - For each open decision, what happens when nobody answers it?
43
- - What is likely to go wrong, and where does the work collide with something already in flight?
45
+ - What is likely to go wrong, what does the work share with something already in flight, and what must it wait on?
44
46
  - How is each outcome proven, and which input would break it?
45
47
 
46
48
  A plan failing these is non-conforming even when it satisfies every shape rule below.
@@ -81,20 +83,22 @@ The document opens with `# Feature: <short title>` and one paragraph stating wha
81
83
 
82
84
  ### Constraints
83
85
 
84
- A constraint naming a surface to leave alone says which of two acts it forbids, rather than leaving the executing session to guess.
86
+ Work in flight reaches a plan as a hold or as a shared path. A hold leaves a surface alone and says which of two acts it forbids, rather than leaving the executing session to guess. Neither act is forbidden over a shared path.
85
87
 
88
+ - Write a hold only for a dependency, a contract the other track consumes, a relocation, or a sweep, which a rebase cannot settle mechanically. A bare shared file is none of these, and forbidding it sends a worker to concede a file the change needs.
86
89
  - Forbid conforming that surface to whatever shape the change introduces, which keeps the branch to one concern.
87
90
  - Never forbid retargeting a pointer the change breaks. A rename, a split, or a deletion that leaves a citation behind ships a dangling reference, so repairing it is required work rather than scope creep.
88
- - Decide both acts for every surface the constraint names, not for one file among its siblings.
91
+ - Decide both acts for every surface a hold names, not for one file among its siblings.
92
+ - Write a shared path as the track, the path, and the branch that rebases, being the one merging second. The executing session edits the file and keeps both sides on the rebase.
89
93
 
90
- A constraint measured against work in flight expires when that work merges, so stamp the block with what it was measured against.
94
+ A constraint measured against work in flight expires when that work merges, as a shared-path line does, so stamp the block with what it was measured against.
91
95
 
92
96
  - Give the stamp its own leading bullet, written as Measured against `<commit>` on <YYYY-MM-DD>. One stamp covers the whole block.
93
97
  - Stamp the commit rather than the track names or a date alone. The constraint already names each track, and a date does not separate two plans written on either side of a merge.
94
- - Re-test a stamped constraint before honoring it with `git fetch origin main --quiet && git log <commit>..origin/main --oneline -- <the paths the constraint names>`. Any commit it reports means the track landed and the constraint is dead. Fetch in the same command and scope the log to the paths, since a stale remote-tracking ref reads a dead constraint as live and a squashed subject names no track.
98
+ - Re-test a stamped constraint before honoring it with `git fetch origin main --quiet && git log <commit>..origin/main --oneline -- <the paths the constraint names>`. Any commit it reports means the track landed and the constraint is dead. Fetch and scope the log to the paths in the same command, since a stale ref reads a dead constraint as live and a squashed subject names no track.
95
99
  - Read an unstamped constraint as unverified rather than as live, and confirm it against the open pull request list for the named paths.
96
100
 
97
- A constraint naming a `.canon/ready/` folder is a third shape beside the two above. It makes that folder's files the verbatim source for the paths this plan's `**Files to touch:**` lists, per `ready.md`, so the executing session copies those paths rather than authoring them.
101
+ A constraint naming a `.canon/ready/` folder is a third shape beside the hold and the shared path. It makes that folder's files the verbatim source for the paths this plan's `**Files to touch:**` lists, per `ready.md`, so the executing session copies those paths rather than authoring them.
98
102
 
99
103
  ### Verification
100
104
 
@@ -106,6 +110,7 @@ A constraint naming a `.canon/ready/` folder is a third shape beside the two abo
106
110
 
107
111
  - Name the collision rather than the category. A risk a reader cannot act on is padding.
108
112
  - Where the work establishes a resource with more than one consumer, list the consumers and mark each one read or write. A policy stated over that resource has to hold for the writers and not only for the consumer that prompted it.
113
+ - Where the work changes a contract another track consumes, such as an exported signature, a verb's flags or record, or a config key, name the contract and list each consumer. The dispatcher reads this bullet for the contract hold, so a change left out reaches it as a clean shared file.
109
114
 
110
115
  ### Review focus
111
116
 
@@ -161,7 +166,8 @@ This contract inverts the one an intake folder keeps, where an empty slot means
161
166
  **Constraints:**
162
167
 
163
168
  - Measured against `<commit>` on <YYYY-MM-DD>
164
- - <the work in flight, its file set, and which of the two acts the constraint forbids>
169
+ - <a hold: the work in flight, its file set, and which of the two acts the constraint forbids>
170
+ - <a shared path: the work in flight, the path, and which branch rebases>
165
171
  - <durable rule the work must respect>
166
172
 
167
173
  **Files to touch:**
package/tsconfig.json CHANGED
@@ -11,5 +11,5 @@
11
11
  "@/*": ["./src/*"]
12
12
  }
13
13
  },
14
- "include": ["src", "claude/skills/*/scripts/**/*.ts"]
14
+ "include": ["src", "claude/skills/*/scripts/**/*.ts", "sandbox/*/*.ts"]
15
15
  }
File without changes
File without changes
File without changes
File without changes