@erclx/canon 4.72.1 → 4.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 (42) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/plan-feature/SKILL.md +2 -2
  3. package/claude/skills/plan-groundwork/SKILL.md +6 -5
  4. package/claude/skills/plan-intake/SKILL.md +1 -1
  5. package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
  6. package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -1
  7. package/claude/skills/session-relay/REQUIREMENT.md +2 -0
  8. package/claude/skills/session-relay/SKILL.md +1 -0
  9. package/claude/skills/task-board/SKILL.md +37 -3
  10. package/docs/agents/commands.md +1 -0
  11. package/docs/agents/index.md +1 -1
  12. package/docs/agents/install-and-sync.md +10 -6
  13. package/docs/agents/records.md +29 -9
  14. package/docs/agents/tasks.md +2 -0
  15. package/docs/target-projects.md +5 -1
  16. package/governance/rules/core/091-channel.md +13 -0
  17. package/package.json +1 -1
  18. package/src/claude/skills-reach.ts +13 -3
  19. package/src/commands/context.ts +7 -4
  20. package/src/commands/design.ts +6 -1
  21. package/src/commands/records.ts +138 -0
  22. package/src/commands/transcripts.ts +20 -3
  23. package/src/context/architecture.ts +15 -8
  24. package/src/context/citations.ts +31 -6
  25. package/src/context/folders.ts +27 -8
  26. package/src/gate/measures.ts +17 -8
  27. package/src/init/plan.ts +8 -1
  28. package/src/init/steps.ts +17 -0
  29. package/src/intake/folder.ts +1 -1
  30. package/src/migrate/records.ts +11 -3
  31. package/src/records/backup.ts +110 -38
  32. package/src/records/ordinal.ts +243 -0
  33. package/src/records/validate.ts +28 -0
  34. package/src/surface-root.ts +101 -0
  35. package/src/sync/stamp.ts +20 -5
  36. package/src/tasks/answers.ts +10 -1
  37. package/src/transcripts/fetch.ts +31 -1
  38. package/standards/groundwork.md +1 -1
  39. package/standards/intake.md +1 -1
  40. package/standards/plan.md +1 -0
  41. package/tooling/base/configs/.husky/post-merge +27 -0
  42. package/tooling/claude/seeds/.claude/hooks/tasks-index.sh +5 -4
package/src/sync/stamp.ts CHANGED
@@ -84,17 +84,30 @@ export function retiredNameStampPath(target: string): string {
84
84
  return join(target, '.claude', 'aitk', 'config.json')
85
85
  }
86
86
 
87
+ /**
88
+ * The stamp path under the new surface root, `canon/config/config.json`.
89
+ *
90
+ * Read ahead of `stampPath`, extending the same mechanism rather than adding a
91
+ * new one: a target that has moved reads its config from the root it moved
92
+ * to, and one that has not falls through to the spellings below unchanged.
93
+ * The write destination does not move to it in this batch.
94
+ */
95
+ function surfaceStampPath(target: string): string {
96
+ return join(target, 'canon', 'config', 'config.json')
97
+ }
98
+
87
99
  /**
88
100
  * Every spelling a stamp has been written under, current first. The order is
89
101
  * the read order, so a target carrying more than one resolves to the newest.
90
102
  *
91
- * The fallback carries no end date. It costs two path reads on a command that
92
- * already touches the filesystem, and dropping it later is a second breaking
93
- * change aimed at exactly the targets that were slowest to migrate the first
94
- * time.
103
+ * The fallback carries no end date. It costs three path reads on a command
104
+ * that already touches the filesystem, and dropping any of them later is a
105
+ * second breaking change aimed at exactly the targets that were slowest to
106
+ * migrate the first time.
95
107
  */
96
108
  export function stampPaths(target: string): readonly string[] {
97
109
  return [
110
+ surfaceStampPath(target),
98
111
  stampPath(target),
99
112
  retiredNameStampPath(target),
100
113
  legacyStampPath(target),
@@ -107,7 +120,9 @@ export function stampPaths(target: string): readonly string[] {
107
120
  * there is nothing to migrate off of.
108
121
  */
109
122
  export function isLegacyStamped(target: string): boolean {
110
- if (existsSync(stampPath(target))) return false
123
+ if (existsSync(surfaceStampPath(target)) || existsSync(stampPath(target))) {
124
+ return false
125
+ }
111
126
  return (
112
127
  existsSync(retiredNameStampPath(target)) ||
113
128
  existsSync(legacyStampPath(target))
@@ -4,6 +4,7 @@ import { isAbsolute, relative, resolve } from 'node:path'
4
4
  import { isUnder } from '@/paths'
5
5
  import { recordDir, recordDirs } from '@/record-root'
6
6
  import {
7
+ hasStagedBatches,
7
8
  normalizeOperatorCall,
8
9
  OPERATOR_CALL,
9
10
  readQuestions,
@@ -219,9 +220,17 @@ export async function planAnswers(
219
220
  const resolved = resolvePlanReference(root, reference)
220
221
  if (!resolved.ok) return resolved
221
222
 
222
- const sections = splitPlanSections(await readFile(resolved.path, 'utf8'))
223
+ const text = await readFile(resolved.path, 'utf8')
224
+ const sections = splitPlanSections(text)
223
225
  const open = openQuestions(sections.get('Questions') ?? [])
224
226
 
227
+ if (hasStagedBatches(text)) {
228
+ open.push({
229
+ label: 'Batch staging',
230
+ why: 'stages a batch inside one file and must split into one plan file per batch before it can dispatch.',
231
+ })
232
+ }
233
+
225
234
  return {
226
235
  ok: true,
227
236
  plan: resolved.plan,
@@ -1,4 +1,5 @@
1
1
  import {
2
+ existsSync,
2
3
  mkdirSync,
3
4
  mkdtempSync,
4
5
  readdirSync,
@@ -81,13 +82,42 @@ interface WriteOptions {
81
82
  readonly fetchedAt: string
82
83
  }
83
84
 
85
+ const INDEX_FILE = 'index.md'
86
+
87
+ /**
88
+ * Writes a minimal index.md stub if the output directory carries none yet,
89
+ * so `canon indexes regen` has frontmatter to render entries against. Left
90
+ * alone once present, since a project may have customized it.
91
+ */
92
+ function ensureIndex(outDir: string): void {
93
+ const indexPath = join(outDir, INDEX_FILE)
94
+ if (existsSync(indexPath)) return
95
+
96
+ const content = [
97
+ '---',
98
+ 'title: Transcripts',
99
+ 'subtitle: YouTube transcripts fetched by canon transcripts, one file per video.',
100
+ '---',
101
+ '',
102
+ '# Transcripts',
103
+ '',
104
+ 'YouTube transcripts fetched by canon transcripts, one file per video.',
105
+ '',
106
+ 'This folder is machine-written. Run canon indexes regen here to refresh its entries.',
107
+ '',
108
+ ].join('\n')
109
+
110
+ writeFileSync(indexPath, content)
111
+ }
112
+
84
113
  export function writeTranscript(
85
114
  metadata: VideoMetadata,
86
115
  subPath: string | null,
87
116
  { outDir, keepTimestamps, fetchedAt }: WriteOptions,
88
117
  ): string {
89
118
  mkdirSync(outDir, { recursive: true })
90
- const slug = `${slugify(metadata.title)}--${metadata.videoId}`
119
+ ensureIndex(outDir)
120
+ const slug = `${fetchedAt}--${slugify(metadata.title)}--${metadata.videoId}`
91
121
  const target = join(outDir, `${slug}.md`)
92
122
 
93
123
  const hasTranscript = subPath !== null
@@ -25,7 +25,7 @@ Does not govern:
25
25
  ## Folder name
26
26
 
27
27
  - Name the folder `<nn>-<slug>`, a two-digit zero-padded ordinal followed by a kebab-case slug.
28
- - Take the ordinal from the highest one already present across both `.canon/groundwork/` and `.canon/intake/`, incremented. A listing then sorts by when each folder opened rather than alphabetically, and the count includes both kinds because the two share one creation-order line.
28
+ - Claim the ordinal through `canon records ordinal groundwork <slug> --claim`, which reads the highest one already present across both `.canon/groundwork/` and `.canon/intake/` and creates the folder in the same atomic act, closing the race a read-then-create sequence leaves open between two sessions opening at once. A listing then sorts by when each folder opened rather than alphabetically, and the count includes both kinds because the two share one creation-order line.
29
29
  - With neither folder holding an entry, the first one opened takes `01`. Do not read this off the numbering inside a track, which starts at `00` on a large one and disagrees with intake's own first file.
30
30
  - Never renumber an existing folder. The ordinal is the order it opened, and a later reader cites it by that name.
31
31
 
@@ -25,7 +25,7 @@ Does not govern:
25
25
  ## Folder name
26
26
 
27
27
  - Name the folder `<nn>-<slug>`, a two-digit zero-padded ordinal followed by a kebab-case slug. This is the folder's own ordinal, distinct from the `NN-<domain>.md` numbering a cluster file carries inside it.
28
- - Take the ordinal from the highest one already present across both `.canon/intake/` and `.canon/groundwork/`, incremented. A listing then sorts by when each folder opened rather than alphabetically, and the count includes both kinds because the two share one creation-order line.
28
+ - Claim the ordinal through `canon records ordinal intake <slug> --claim`, which reads the highest one already present across both `.canon/intake/` and `.canon/groundwork/` and creates the folder in the same atomic act, closing the race a read-then-create sequence leaves open between two sessions opening at once. A listing then sorts by when each folder opened rather than alphabetically, and the count includes both kinds because the two share one creation-order line.
29
29
  - With neither folder holding an entry, the first one opened takes `01`. Do not read this off the numbering inside a dump, which starts at `00` and disagrees with groundwork's own first required file.
30
30
  - Never renumber an existing folder. The ordinal is the order it opened, and a later reader cites it by that name.
31
31
 
package/standards/plan.md CHANGED
@@ -38,6 +38,7 @@ A plan failing these is non-conforming even when it satisfies every shape rule b
38
38
 
39
39
  - Name the file `feature-<slug>.md`, with `<slug>` two to four kebab-case words naming the concern.
40
40
  - Write one concern per file. A request spanning two independent concerns takes two plans rather than one bundling both, since a bundled plan cannot be executed by two sessions or abandoned by half.
41
+ - A staged batch is a concern of its own and takes its own file rather than a `**Batch N**` sub-heading inside one plan's `**Files to touch:**`. `canon tasks plan-branch` derives one branch from one plan filename, so every batch sharing a file has nothing left to open a pull request against once an earlier batch merges under that name. State a batch's dependency on the ones before it in its own `**Constraints:**`.
41
42
  - Derive the slug from the concern rather than from a branch, because the plan is written before the branch exists.
42
43
  - Give the branch that executes the plan the same slug. A later surface finds the plan from the branch name and finds nothing when the two spellings differ.
43
44
 
@@ -59,3 +59,30 @@ done
59
59
  printf '\nšŸ“‹ %s archive candidate(s) on the board:\n%s\n' "$count" "$closed"
60
60
  printf 'Outcomes are marked on the branch, so each still needs its work confirmed\n'
61
61
  printf 'on main. Run /task-board, which checks that and sweeps the plan first.\n\n'
62
+
63
+ # The gitignored record folders live on one disk and nowhere else, and a merge
64
+ # is the event that changes them most. This runs on every merge rather than
65
+ # only on one that closes a task, since a review report and a memory entry
66
+ # both land on runs that archive nothing.
67
+ #
68
+ # Sits last so a slow or unreachable remote delays no candidate announcement,
69
+ # and inside an `if` so a push failing offline, or a machine that never ran
70
+ # the one-time setup, still leaves this hook exiting zero. Guarded by its own
71
+ # `command -v canon` check, since this file carries none above it.
72
+ if command -v canon >/dev/null 2>&1; then
73
+ if backup=$(canon records push --root "$root" --json 2>/dev/null); then
74
+ changed=$(printf '%s' "$backup" | sed -n 's/.*"changed":\([0-9][0-9]*\).*/\1/p')
75
+ if [ -n "$changed" ] && [ "$changed" != "0" ]; then
76
+ printf '\nšŸ—„ļø Backed up %s record path(s).\n\n' "$changed"
77
+ fi
78
+ else
79
+ reason=$(printf '%s' "$backup" | sed -n 's/.*"reason":"\([^"]*\)".*/\1/p')
80
+
81
+ # No records history on this machine, which is every checkout that never
82
+ # ran the one-time setup. Nothing to report.
83
+ if [ "$reason" != "no-repository" ]; then
84
+ printf '\nšŸ—„ļø Records not backed up: %s\n' "${reason:-unknown}"
85
+ printf 'Run canon records push when the remote is reachable.\n\n'
86
+ fi
87
+ fi
88
+ fi
@@ -34,11 +34,12 @@ case "$file_path" in
34
34
  esac
35
35
 
36
36
  # The board index covers the live folder alone. A shell pattern's wildcard
37
- # crosses a separator, so the guard above matches an archived task as well and
38
- # a regen fired on one would rebuild the index the archive was taken out of.
37
+ # crosses a separator, so the guard above matches an archived or declined task
38
+ # as well, and a regen fired on one would rebuild the index that task was
39
+ # taken out of.
39
40
  case "$file_path" in
40
- */.claude/tasks/index.md | */.claude/tasks/archive/*) exit 0 ;;
41
- */.canon/tasks/index.md | */.canon/tasks/archive/*) exit 0 ;;
41
+ */.claude/tasks/index.md | */.claude/tasks/archive/* | */.claude/tasks/declined/*) exit 0 ;;
42
+ */.canon/tasks/index.md | */.canon/tasks/archive/* | */.canon/tasks/declined/*) exit 0 ;;
42
43
  esac
43
44
 
44
45
  # The walk-up boundary has to come from the path, not from the session. Shared