@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/plan-feature/SKILL.md +2 -2
- package/claude/skills/plan-groundwork/SKILL.md +6 -5
- package/claude/skills/plan-intake/SKILL.md +1 -1
- package/claude/skills/role-orchestrator/references/orchestrator-dispatch.md +1 -0
- package/claude/skills/role-orchestrator/references/orchestrator-parked.md +2 -1
- package/claude/skills/session-relay/REQUIREMENT.md +2 -0
- package/claude/skills/session-relay/SKILL.md +1 -0
- package/claude/skills/task-board/SKILL.md +37 -3
- package/docs/agents/commands.md +1 -0
- package/docs/agents/index.md +1 -1
- package/docs/agents/install-and-sync.md +10 -6
- package/docs/agents/records.md +29 -9
- package/docs/agents/tasks.md +2 -0
- package/docs/target-projects.md +5 -1
- package/governance/rules/core/091-channel.md +13 -0
- package/package.json +1 -1
- package/src/claude/skills-reach.ts +13 -3
- package/src/commands/context.ts +7 -4
- package/src/commands/design.ts +6 -1
- package/src/commands/records.ts +138 -0
- package/src/commands/transcripts.ts +20 -3
- package/src/context/architecture.ts +15 -8
- package/src/context/citations.ts +31 -6
- package/src/context/folders.ts +27 -8
- package/src/gate/measures.ts +17 -8
- package/src/init/plan.ts +8 -1
- package/src/init/steps.ts +17 -0
- package/src/intake/folder.ts +1 -1
- package/src/migrate/records.ts +11 -3
- package/src/records/backup.ts +110 -38
- package/src/records/ordinal.ts +243 -0
- package/src/records/validate.ts +28 -0
- package/src/surface-root.ts +101 -0
- package/src/sync/stamp.ts +20 -5
- package/src/tasks/answers.ts +10 -1
- package/src/transcripts/fetch.ts +31 -1
- package/standards/groundwork.md +1 -1
- package/standards/intake.md +1 -1
- package/standards/plan.md +1 -0
- package/tooling/base/configs/.husky/post-merge +27 -0
- 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
|
|
92
|
-
* already touches the filesystem, and dropping
|
|
93
|
-
* change aimed at exactly the targets that were slowest to
|
|
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)))
|
|
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))
|
package/src/tasks/answers.ts
CHANGED
|
@@ -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
|
|
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,
|
package/src/transcripts/fetch.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
package/standards/groundwork.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
|
package/standards/intake.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
|
38
|
-
# a regen fired on one would rebuild the index
|
|
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
|