@erclx/canon 4.100.0 → 4.101.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/README.md +4 -1
- package/claude/.claude-plugin/plugin.json +1 -1
- package/docs/agents/demo.md +2 -0
- package/docs/agents/tasks.md +24 -0
- package/package.json +1 -1
- package/src/commands/tasks.ts +69 -0
- package/src/tasks/list.ts +98 -0
- package/standards/tasks.md +2 -0
- package/tooling/web/configs/e2e/screenshot.ts +8 -3
package/README.md
CHANGED
|
@@ -99,7 +99,10 @@ Two of the outputs are documents you open rather than files you run. A `SLIDES.m
|
|
|
99
99
|
|
|
100
100
|
The workflow this toolkit ships is the workflow that built it. Several Claude Code sessions run at once, each in its own git worktree on its own branch, and each opens its own pull request.
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
<picture>
|
|
103
|
+
<source media="(prefers-color-scheme: dark)" srcset="demos/agent-view.gif">
|
|
104
|
+
<img src="demos/agent-view-light.gif" alt="The canon landing page scrolling from the dispatch section to the branch graph, three workers and a planner on disjoint file sets with the pull request each opened">
|
|
105
|
+
</picture>
|
|
103
106
|
|
|
104
107
|
That recording is the landing page's own `dispatch` and `workers` sections, driven by `canon demo run` against a local build. The branch graph beside the pull request numbers is authored rather than read, because nothing on a build machine records which files four sessions held.
|
|
105
108
|
|
package/docs/agents/demo.md
CHANGED
|
@@ -26,6 +26,8 @@ The plan is committed, not scratch. Its timing is a starting point tuned by watc
|
|
|
26
26
|
|
|
27
27
|
A plan can carry an optional `colorScheme` of `light` or `dark`, which the recording context starts in so a page following the system preference paints that scheme with no click beat of its own. An absent field keeps the engine's default, which is light, and `compile` never seeds it. Any other value makes the plan unreadable. A page keyed on a stored theme rather than the preference ignores the field.
|
|
28
28
|
|
|
29
|
+
A recording that has to match both themes takes one plan per scheme, committed side by side, rather than one plan run twice. The pair shares its beats and differs in `colorScheme`, `slug`, and the `output` paths. Neither plan clicks a theme toggle, since a stored theme overrides the preference. Nothing checks that the two plans keep the same beats, so an edit to one is made to both.
|
|
30
|
+
|
|
29
31
|
`record-screencast` is the routed way to run both verbs from a draft path: it compiles only when no plan exists yet, then runs, stopping to report any field still unresolved rather than guessing one.
|
|
30
32
|
|
|
31
33
|
| Option | Behavior |
|
package/docs/agents/tasks.md
CHANGED
|
@@ -292,6 +292,30 @@ Exit codes: `0` closed, `1` refused. The `reason` field adds `no-outcomes`, `out
|
|
|
292
292
|
|
|
293
293
|
Both verbs exist because the write is an edit inside a file that already exists. `Edit` and `Write` refuse a main-root path from a linked worktree, and a shell stream editor is banned for in-place edits, so a verb resolving the board root in-process is the only route a skill body has. Creating a whole file needs no verb, because a heredoc through `Bash` writes it safely.
|
|
294
294
|
|
|
295
|
+
## List
|
|
296
|
+
|
|
297
|
+
`canon tasks list` reports each live task file with its readiness, so a reader learns which file is parked and which is live without opening `priority.md` or `backlog.md`. It reads and never writes, and every file stays in the folder it is in.
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
canon tasks list
|
|
301
|
+
canon tasks list --json
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
| Option | Effect |
|
|
305
|
+
| --------------- | ------------------------------------------- |
|
|
306
|
+
| `--json` | Emit a machine-readable record on stdout |
|
|
307
|
+
| `--root <path>` | Board root, defaulting to the main worktree |
|
|
308
|
+
|
|
309
|
+
The record carries `tasks`, one entry per file with its `stem` and its `readiness`. Readiness is one of `Run now`, `Up next`, `Needs a plan`, `backlog`, `unplaced`, or `both`. The first three are the group names the board standard fixes, `backlog` is a line on `backlog.md`, `unplaced` is a file neither surface names, and `both` is a file both surfaces name, which `validate` reports as a finding and this verb does not judge.
|
|
310
|
+
|
|
311
|
+
The verb calls the parsers `validate` uses, so the two cannot disagree on what a surface names. It lists the live root only, never `archive/` or `declined/`, and skips the same siblings `validate` skips.
|
|
312
|
+
|
|
313
|
+
Exit codes: `0` derived, `1` refused with `no-board`.
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
canon tasks list --json | jq -r '.tasks[] | select(.readiness == "backlog") | .stem'
|
|
317
|
+
```
|
|
318
|
+
|
|
295
319
|
## Validate
|
|
296
320
|
|
|
297
321
|
`canon tasks validate` reports what each row of `priority.md` claims against what the tree holds. It reads and never writes, because a row is a session's claim about readiness and a validator that repaired one would be asserting the claim it exists to test.
|
package/package.json
CHANGED
package/src/commands/tasks.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
planCitations,
|
|
15
15
|
} from '@/tasks/archive'
|
|
16
16
|
import { type LabelOutcome, nextLabel } from '@/tasks/label'
|
|
17
|
+
import { listTasks } from '@/tasks/list'
|
|
17
18
|
import {
|
|
18
19
|
type Claim,
|
|
19
20
|
type Holder,
|
|
@@ -114,6 +115,11 @@ interface OutcomeCommandOptions {
|
|
|
114
115
|
readonly root?: string
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
interface ListCommandOptions {
|
|
119
|
+
readonly json?: boolean
|
|
120
|
+
readonly root?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
117
123
|
interface NextLabelCommandOptions {
|
|
118
124
|
readonly json?: boolean
|
|
119
125
|
readonly root?: string
|
|
@@ -522,6 +528,37 @@ export function register(program: Command): void {
|
|
|
522
528
|
.action(async (opts: NextLabelCommandOptions) => {
|
|
523
529
|
process.exitCode = await runNextLabel(opts)
|
|
524
530
|
})
|
|
531
|
+
|
|
532
|
+
tasks
|
|
533
|
+
.command('list')
|
|
534
|
+
.description('Report each live task file with its readiness')
|
|
535
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
536
|
+
.option('--json', 'Emit a machine-readable record on stdout')
|
|
537
|
+
.option('--root <path>', 'Board root, defaulting to the main worktree')
|
|
538
|
+
.addHelpText(
|
|
539
|
+
'after',
|
|
540
|
+
[
|
|
541
|
+
'',
|
|
542
|
+
'Readiness is one of Run now, Up next, Needs a plan, backlog, unplaced,',
|
|
543
|
+
'or both, read from priority.md and backlog.md through the parsers',
|
|
544
|
+
'validate uses. It lists the live root only, never archive/ or declined/.',
|
|
545
|
+
'',
|
|
546
|
+
'Exit codes:',
|
|
547
|
+
' 0 the list is derived',
|
|
548
|
+
' 1 refused with no-board',
|
|
549
|
+
'',
|
|
550
|
+
'It reports and never writes, and judges nothing: a file named on both',
|
|
551
|
+
'surfaces reads as both, and validate owns the finding.',
|
|
552
|
+
'',
|
|
553
|
+
'Examples:',
|
|
554
|
+
' canon tasks list',
|
|
555
|
+
' canon tasks list --json',
|
|
556
|
+
'',
|
|
557
|
+
].join('\n'),
|
|
558
|
+
)
|
|
559
|
+
.action(async (opts: ListCommandOptions) => {
|
|
560
|
+
process.exitCode = await runList(opts)
|
|
561
|
+
})
|
|
525
562
|
}
|
|
526
563
|
|
|
527
564
|
function collectPosition(value: string, previous: string[]): string[] {
|
|
@@ -792,6 +829,38 @@ function reportOutcome(
|
|
|
792
829
|
return 0
|
|
793
830
|
}
|
|
794
831
|
|
|
832
|
+
async function runList(opts: ListCommandOptions): Promise<number> {
|
|
833
|
+
const root = opts.root ?? (await mainWorktreeRoot())
|
|
834
|
+
const outcome = await listTasks(root)
|
|
835
|
+
|
|
836
|
+
if (!outcome.ok) {
|
|
837
|
+
if (opts.json) {
|
|
838
|
+
process.stdout.write(
|
|
839
|
+
`${JSON.stringify({ ok: false, reason: outcome.reason, message: outcome.message })}\n`,
|
|
840
|
+
)
|
|
841
|
+
return 1
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
intro('canon tasks list')
|
|
845
|
+
logStep('Refused')
|
|
846
|
+
logError(outcome.message)
|
|
847
|
+
outro()
|
|
848
|
+
return 1
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (opts.json) {
|
|
852
|
+
process.stdout.write(`${JSON.stringify({ ...outcome, root })}\n`)
|
|
853
|
+
return 0
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
intro('canon tasks list')
|
|
857
|
+
logStep(`${outcome.tasks.length} task files`)
|
|
858
|
+
for (const task of outcome.tasks) logInfo(`${task.readiness}: ${task.stem}`)
|
|
859
|
+
outro()
|
|
860
|
+
|
|
861
|
+
return 0
|
|
862
|
+
}
|
|
863
|
+
|
|
795
864
|
async function runNextLabel(opts: NextLabelCommandOptions): Promise<number> {
|
|
796
865
|
const root = opts.root ?? (await mainWorktreeRoot())
|
|
797
866
|
const outcome = await nextLabel(root)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readFile } from 'node:fs/promises'
|
|
3
|
+
import { listTaskStems, tasksDir } from '@/tasks/archive'
|
|
4
|
+
import {
|
|
5
|
+
BOARD_GROUPS,
|
|
6
|
+
type BoardGroup,
|
|
7
|
+
backlogPath,
|
|
8
|
+
orderingPath,
|
|
9
|
+
readBacklog,
|
|
10
|
+
readBoard,
|
|
11
|
+
} from '@/tasks/validate'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The three board group names plus the states outside them. A file named on
|
|
15
|
+
* both surfaces is `validate`'s finding, so it reports as `both` rather than
|
|
16
|
+
* being judged here.
|
|
17
|
+
*/
|
|
18
|
+
export const READINESS_VALUES = [
|
|
19
|
+
...BOARD_GROUPS,
|
|
20
|
+
'backlog',
|
|
21
|
+
'unplaced',
|
|
22
|
+
'both',
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
export type Readiness = (typeof READINESS_VALUES)[number]
|
|
26
|
+
|
|
27
|
+
export interface ListedTask {
|
|
28
|
+
readonly stem: string
|
|
29
|
+
readonly readiness: Readiness
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TaskList {
|
|
33
|
+
readonly ok: true
|
|
34
|
+
readonly tasks: readonly ListedTask[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ListRefused {
|
|
38
|
+
readonly ok: false
|
|
39
|
+
readonly reason: 'no-board'
|
|
40
|
+
readonly message: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ListOutcome = TaskList | ListRefused
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reports each live task file's readiness from the board and the backlog. It
|
|
47
|
+
* reads through the parsers `validate` exports rather than reading either file
|
|
48
|
+
* a second time, so the two verbs cannot disagree on what a surface names. An
|
|
49
|
+
* absent ordering file or backlog reads as empty, since a file in neither
|
|
50
|
+
* surface is reported `unplaced` rather than refused.
|
|
51
|
+
*/
|
|
52
|
+
export async function listTasks(root: string): Promise<ListOutcome> {
|
|
53
|
+
const dir = tasksDir(root)
|
|
54
|
+
if (!existsSync(dir)) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
reason: 'no-board',
|
|
58
|
+
message: `No task board at ${dir}.`,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ordering = orderingPath(root)
|
|
63
|
+
const { rows } = existsSync(ordering)
|
|
64
|
+
? readBoard(await readFile(ordering, 'utf8'))
|
|
65
|
+
: { rows: [] }
|
|
66
|
+
|
|
67
|
+
const backlogFile = backlogPath(root)
|
|
68
|
+
const backlog = existsSync(backlogFile)
|
|
69
|
+
? readBacklog(await readFile(backlogFile, 'utf8'))
|
|
70
|
+
: []
|
|
71
|
+
|
|
72
|
+
const groupOf = new Map<string, BoardGroup>()
|
|
73
|
+
for (const row of rows) {
|
|
74
|
+
if (row.stem) groupOf.set(row.stem, row.group)
|
|
75
|
+
}
|
|
76
|
+
const parked = new Set(backlog.flatMap((row) => (row.stem ? [row.stem] : [])))
|
|
77
|
+
|
|
78
|
+
const stems = await listTaskStems(dir)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
ok: true,
|
|
82
|
+
tasks: stems.map((stem) => ({
|
|
83
|
+
stem,
|
|
84
|
+
readiness: readinessOf(groupOf.get(stem), parked.has(stem)),
|
|
85
|
+
})),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readinessOf(
|
|
90
|
+
group: BoardGroup | undefined,
|
|
91
|
+
isParked: boolean,
|
|
92
|
+
): Readiness {
|
|
93
|
+
if (group && isParked) return 'both'
|
|
94
|
+
if (group) return group
|
|
95
|
+
if (isParked) return 'backlog'
|
|
96
|
+
|
|
97
|
+
return 'unplaced'
|
|
98
|
+
}
|
package/standards/tasks.md
CHANGED
|
@@ -37,6 +37,8 @@ Does not govern:
|
|
|
37
37
|
└── v13.0-toolkit-drift.md # canon-allow-reference: illustrates the vXX.Y-slug filename this section defines
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
`canon tasks list` reports each file's readiness from those two surfaces, so a reader never infers it from the folder.
|
|
41
|
+
|
|
40
42
|
One file per task is what keeps the board safe under parallel sessions. Two sessions working different tasks never write the same file, which matters because a gitignored board has no history to recover a clobbered write from.
|
|
41
43
|
|
|
42
44
|
Siblings sit in the folder without being tasks, and each earns its place by being governed somewhere. `index.md`, `priority.md`, and `backlog.md` are governed here. Every `session-` file is a pre-compaction handoff governed by `session.md`, and each is optional: a project whose sessions never approach a compaction carries none. Anything filtering the folder to tasks skips all of them, so a name outside the set is a task whatever it holds.
|
|
@@ -137,10 +137,15 @@ async function settle(
|
|
|
137
137
|
for (const frame of document.querySelectorAll('iframe')) {
|
|
138
138
|
try {
|
|
139
139
|
const inner = frame.contentDocument
|
|
140
|
-
if (inner
|
|
140
|
+
if (inner === null) continue
|
|
141
|
+
if (inner.readyState !== 'complete') return false
|
|
142
|
+
// A frame's own fonts load after its `readyState` reads complete, and
|
|
143
|
+
// the swap reflows its text. The outer document's fonts never cover
|
|
144
|
+
// them.
|
|
145
|
+
if (inner.fonts.status !== 'loaded') return false
|
|
141
146
|
} catch {
|
|
142
|
-
// Cross-origin, so there is
|
|
143
|
-
//
|
|
147
|
+
// Cross-origin, so there is nothing here to read and nothing to
|
|
148
|
+
// wait on.
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
151
|
return true
|