@erclx/canon 4.100.0 → 4.102.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/claude/skills/auto-ship/SKILL.md +5 -6
- package/claude/skills/canon-cli/SKILL.md +1 -1
- package/claude/skills/git-ship/SKILL.md +3 -7
- package/claude/skills/memory-capture/SKILL.md +0 -6
- package/claude/skills/memory-review/REQUIREMENT.md +1 -1
- package/claude/skills/memory-review/SKILL.md +2 -3
- package/claude/skills/plan-groundwork/SKILL.md +3 -2
- package/docs/agents/demo.md +2 -0
- package/docs/agents/install-and-sync.md +7 -1
- package/docs/agents/scripting.md +1 -1
- package/docs/agents/tasks.md +24 -0
- package/docs/target-projects.md +1 -1
- package/docs/workflow/ai-workflow.md +5 -3
- package/package.json +1 -1
- package/scripts/core/regen-web-favicon.ts +9 -40
- package/src/commands/tasks.ts +69 -0
- package/src/commands/tooling.ts +92 -19
- package/src/design/favicon.ts +33 -0
- package/src/design/tokens.ts +1 -1
- package/src/tasks/list.ts +98 -0
- package/standards/groundwork.md +8 -2
- package/standards/tasks.md +2 -0
- package/tooling/base/manifest.toml +2 -2
- package/tooling/base/reference.md +1 -1
- 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
|
|
|
@@ -11,7 +11,7 @@ Chain the post-plan pipeline in a single run. Every step has a stop condition. S
|
|
|
11
11
|
## Guards
|
|
12
12
|
|
|
13
13
|
- All `.canon/plans/` and `.canon/review/` reads resolve at the main worktree root, not the current worktree. Resolve that root the way `session-worktree` does.
|
|
14
|
-
- Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. This skill takes the stop rather than the `latest` fallback, since it commits and opens a pull request. If empty, stop: `❌ Detached HEAD. Checkout the feature branch first.` This slug is provisional. It is superseded once `session-worktree` runs, whether at Step 0 or before this chain began. Every later step keys its output on the slug that run resolves, being the worktree, the review receipt,
|
|
14
|
+
- Derive `<slug>` per `${CLAUDE_SKILL_DIR}/../../standards/slug.md`. This skill takes the stop rather than the `latest` fallback, since it commits and opens a pull request. If empty, stop: `❌ Detached HEAD. Checkout the feature branch first.` This slug is provisional. It is superseded once `session-worktree` runs, whether at Step 0 or before this chain began. Every later step keys its output on the slug that run resolves, being the worktree, the review receipt, and the branch, regardless of which plan Step 1 reads.
|
|
15
15
|
- Resolve `<plan>` in Step 1, ahead of any other read.
|
|
16
16
|
- If the working tree has uncommitted changes unrelated to the plan, stop: `❌ Uncommitted changes outside the plan. Commit or stash before autoshipping.`
|
|
17
17
|
|
|
@@ -188,7 +188,7 @@ This chain owns the receipt's lifetime, which is what makes the Output block's c
|
|
|
188
188
|
|
|
189
189
|
## Step 8: ship
|
|
190
190
|
|
|
191
|
-
Invoke `canon:git-ship`. That body owns the sequence, being the verify gate, memory capture, both doc syncs, staging, the commit grouping, the branch rename, the pull request, the CI watch,
|
|
191
|
+
Invoke `canon:git-ship`. That body owns the sequence, being the verify gate, memory capture, both doc syncs, staging, the commit grouping, the branch rename, the pull request, and the CI watch, along with the reason each step sits where it does. This step used to restate that list and the two drifted apart with nothing comparing them, so read the order there and never here.
|
|
192
192
|
|
|
193
193
|
One thing this chain adds. Mark the pull request as a draft as soon as `git-ship`'s pull request step returns, ahead of its CI watch, then read the flag back:
|
|
194
194
|
|
|
@@ -207,21 +207,20 @@ Placement is why the call sits ahead of the watch rather than after it. Marking
|
|
|
207
207
|
|
|
208
208
|
## Output
|
|
209
209
|
|
|
210
|
-
Respond with up to
|
|
210
|
+
Respond with up to four lines:
|
|
211
211
|
|
|
212
212
|
```plaintext
|
|
213
213
|
✅ Autoshipped (<state>): <PR url>
|
|
214
214
|
<N minor findings kept in .canon/review/branch-<slug>.md>
|
|
215
215
|
<N facts routed to context entries>
|
|
216
216
|
<N memories captured in .canon/memory/>
|
|
217
|
-
<Memory proposal at .canon/memory/review/memory-review-<slug>.md>
|
|
218
217
|
```
|
|
219
218
|
|
|
220
219
|
`<state>` is whatever the Step 8 read returned, being `draft` or `ready, unsupervised`, rather than the state the undo asked for. Writing the word `draft` there unconditionally is what this line used to do, and it named a state no step had read.
|
|
221
220
|
|
|
222
|
-
Omit the second line if there were no minor findings, and the third if nothing routed. Omit the fourth
|
|
221
|
+
Omit the second line if there were no minor findings, and the third if nothing routed. Omit the fourth if `memory-capture` wrote no memory file this session. A run that routes every fact and writes none is the shape to expect, and it reports three lines.
|
|
223
222
|
|
|
224
|
-
This block replaces the one `git-ship` closes on rather than following it. The two carry the same
|
|
223
|
+
This block replaces the one `git-ship` closes on rather than following it. The two carry the same two trailing lines and differ on the two above them, since the first names the state the read returned and the second reports the minor findings Step 7 kept, neither of which that body has a counterpart for. Emitting both reports one run twice and buries the state under a `✅ Shipped` that does not name it.
|
|
225
224
|
|
|
226
225
|
## Failure recovery
|
|
227
226
|
|
|
@@ -31,7 +31,7 @@ Consult before running an unfamiliar `canon` verb, before a sync or install, or
|
|
|
31
31
|
|
|
32
32
|
A golden config is any file a stack ships under `configs/`, and the category is wider than its name suggests. It carries the CI workflow, the git hooks, the end-to-end harness, the shell scripts under `scripts/`, and the editor settings, alongside the linters and compilers a reader expects. A stack inherits its parent's configs, so syncing `astro` also writes everything `web` and `base` hold.
|
|
33
33
|
|
|
34
|
-
Run `canon tooling
|
|
34
|
+
Run `canon tooling diff <stack> <target>` for the list resolved against a real target. It reports every path, writes nothing, and exits 1 when any differs. The list below is what the stacks hold as shipped, before any chain resolution.
|
|
35
35
|
|
|
36
36
|
<!-- generated:tooling-paths -->
|
|
37
37
|
|
|
@@ -32,7 +32,6 @@ Run `git diff --cached --name-only 2>/dev/null` to check for staged files. If ou
|
|
|
32
32
|
7. Invoke `canon:git-branch` to rename branch to match conventional format
|
|
33
33
|
8. Invoke `canon:git-pr` to push branch and open pull request
|
|
34
34
|
9. After the PR opens, watch CI. Poll `canon pr checks <number> --json` until the record's `state` leaves `pending`, branching on that field rather than on the exit, and fall back to `gh pr checks <number>` when no record comes back at all, which is a target whose CLI predates the verb. On `passing`, continue. On `failing`, stop the sequence and report the failing check with its URL. Do not auto-fix. This step may output on failure, the one exception to the no-text-between-steps rule.
|
|
35
|
-
10. If step 1 wrote or updated at least one memory file, invoke `canon:memory-review` scoped to those entries to propose fixes while session context is fresh. If the pen got nothing, skip this step.
|
|
36
35
|
|
|
37
36
|
A caller wrapping this sequence may act between step 8 and step 9, which is the one gap the order leaves open, since the pull request exists there and nothing has read its checks yet. `auto-ship` marks the pull request draft in it. Nothing else may go there, and a caller that needs a step anywhere else in the sequence is asking for a change to this body rather than for a place to stand.
|
|
38
37
|
|
|
@@ -48,19 +47,16 @@ An installed binary carrying no `plan-reach` subcommand reports the reach unread
|
|
|
48
47
|
|
|
49
48
|
Capture leads the sequence because a routed fact lands in a context entry, which is a tracked file. Running it after the pull request opens leaves that edit off the branch entirely, so the fact reaches nothing. Memory files are gitignored either way, which is what hid the ordering while capture wrote only those.
|
|
50
49
|
|
|
51
|
-
Stop at the Propose phase. Do not run Apply. Promoting an entry to an always-loaded rule or a skill body ships as its own change, separate from this feature.
|
|
52
|
-
|
|
53
50
|
## After completion
|
|
54
51
|
|
|
55
|
-
Output up to
|
|
52
|
+
Output up to three lines:
|
|
56
53
|
|
|
57
54
|
```plaintext
|
|
58
55
|
✅ Shipped
|
|
59
56
|
<N facts routed to context entries>
|
|
60
57
|
<N memories captured in .canon/memory/>
|
|
61
|
-
<Memory proposal at .canon/memory/review/memory-review-<slug>.md>
|
|
62
58
|
```
|
|
63
59
|
|
|
64
|
-
Omit the second line if nothing routed
|
|
60
|
+
Omit the second line if nothing routed and the third if `memory-capture` wrote no memory file this session.
|
|
65
61
|
|
|
66
|
-
Emit nothing here when a wrapping caller states it closes on its own block. `auto-ship` is that caller and its block carries these same
|
|
62
|
+
Emit nothing here when a wrapping caller states it closes on its own block. `auto-ship` is that caller and its block carries these same two trailing lines above a first line naming the draft state, so emitting both reports one run twice. A caller that states no such thing gets this block, which is every direct invocation.
|
|
@@ -100,12 +100,6 @@ When anything routed, add a line naming the handoff so the caller knows a `docs-
|
|
|
100
100
|
|
|
101
101
|
Omit that line when the caller runs `docs-fold` itself later in its own chain.
|
|
102
102
|
|
|
103
|
-
When at least one memory file was written or updated, add a closing line so the standalone caller proposes fixes while context is fresh:
|
|
104
|
-
|
|
105
|
-
`→ Run /memory-review to propose fixes for the pen before the session ends.`
|
|
106
|
-
|
|
107
|
-
The ship skills run review Propose themselves, so this line is for the standalone `/memory-capture` path.
|
|
108
|
-
|
|
109
103
|
If nothing was captured, output:
|
|
110
104
|
|
|
111
105
|
`✅ Nothing worth capturing.`
|
|
@@ -9,7 +9,7 @@ description: What memory review is for, the gaps it closes, and why every action
|
|
|
9
9
|
|
|
10
10
|
Without this skill, the memory folder grows and never drains. Entries pile up restating rules a durable surface already carries, nothing decides which memory has earned a place in one, and a rule that does get promoted arrives verbatim in a file whose voice it does not match. The folder then reads as a second source of truth that no surface points at.
|
|
11
11
|
|
|
12
|
-
A pen the caller cannot face is the same gap wearing a different shape. Routing at capture takes the domain facts and whatever the ownership test leaves stays, in whatever mix of types that is, so the folder still reaches a size nobody reads in one sitting.
|
|
12
|
+
A pen the caller cannot face is the same gap wearing a different shape. Routing at capture takes the domain facts and whatever the ownership test leaves stays, in whatever mix of types that is, so the folder still reaches a size nobody reads in one sitting. The full sweep is what an operator asks for when the cross-session duplicates are the point. An entry leaving the pen is archived rather than deleted, because a folder git does not hold gives a wrong bulk call no undo.
|
|
13
13
|
|
|
14
14
|
## Must
|
|
15
15
|
|
|
@@ -30,12 +30,11 @@ If the user re-pings the skill with no new phrase and a receipt exists, default
|
|
|
30
30
|
|
|
31
31
|
## Propose phase
|
|
32
32
|
|
|
33
|
-
Propose is the
|
|
33
|
+
Propose is the entry point for a standalone run. The ship skills stop at capture and never invoke it.
|
|
34
34
|
|
|
35
35
|
### Scope
|
|
36
36
|
|
|
37
|
-
- **
|
|
38
|
-
- **Full sweep:** when invoked standalone with no named set, classify every entry in the pen, including entries carried from earlier sessions, so cross-session duplicates merge into one rule.
|
|
37
|
+
- **Full sweep:** classify every entry in the pen, including entries carried from earlier sessions, so cross-session duplicates merge into one rule.
|
|
39
38
|
|
|
40
39
|
### Step 1: read the memory folder
|
|
41
40
|
|
|
@@ -21,7 +21,7 @@ Read `${CLAUDE_SKILL_DIR}/../../standards/groundwork.md` before writing any file
|
|
|
21
21
|
|
|
22
22
|
- Write only inside `.canon/groundwork/<nn>-<slug>/`. A feature plan, source changes, a standard, a rule, and a reference doc all live outside that folder, so this one rule forbids every one of them.
|
|
23
23
|
- One exception, at close only: write one task file recording what the track concluded.
|
|
24
|
-
- A second exception, for what a spike reads: write
|
|
24
|
+
- A second exception, for what a spike reads as bulk input: write a large fixture under `.canon/tmp/runs/groundwork-fixtures/<slug>/`. Keep it out of `.canon/groundwork/` so mode detection never matches a fixture as a track. A fixture a headless run is pointed at goes outside the repository instead, per the rule in `## Running a spike`. An arm script and a checkout a claim rests on are not fixtures, and go inside the track under `scripts/` and `clones/`, per the standard.
|
|
25
25
|
- What a spike produces stays inside the track rather than joining the two exceptions above: write evidence a spike file cites under `.canon/groundwork/<nn>-<slug>/evidence/`, which the first rule already permits. Mode detection matches entries at the top level of the tracks directory, so the sibling the fixtures rule guards against cannot be a folder nested inside a track, and the scratch path holds only what can be deleted without loss, which a recording a finding rests on is not.
|
|
26
26
|
- A `draft-and-pick` run invoked from inside a live track follows its own branch for this, writing to `evidence/` here rather than `.canon/tmp/<slug>/`.
|
|
27
27
|
- Reading is not restricted. External research is in scope, so read documentation, comparable projects, and papers whenever a live question needs them.
|
|
@@ -33,12 +33,13 @@ Read `${CLAUDE_SKILL_DIR}/../../standards/groundwork.md` before writing any file
|
|
|
33
33
|
A track may run an experiment to settle an open question without stopping to ask. What it takes depends on what the experiment does.
|
|
34
34
|
|
|
35
35
|
- Reading or computing: run it. Unrestricted reading already covers this, and it is what most spikes turn out to be.
|
|
36
|
+
- Anything that processes an input: start against a sample and scale up after it works, per the sample rule in the standard's spikes section, which bounds it on size, duration, and spend.
|
|
36
37
|
- Writing a fixture this session reads or provisions itself: run it, under the fixtures path above. Provisioning is the usual blocker rather than spend, and it costs nothing.
|
|
37
38
|
- Spawning a billed headless session: run up to three, then ask before spawning more. The bound is a run count rather than a budget, because a headless run reports its total cost only after it finishes, so a dollar ceiling is reportable and not enforceable while a run count is checkable before spawning.
|
|
38
39
|
|
|
39
40
|
A fixture a headless run is pointed at goes outside the repository, under `mktemp -d`, and never under `.canon/tmp/`. A session started anywhere beneath the project root loads that project's `CLAUDE.md` and `.claude/rules/` through the ancestor chain, so an in-repo fixture measures the repository instead of the arm. Record the fixture as contaminated and re-run it outside if this is discovered after the fact, because the result reads as evidence either way.
|
|
40
41
|
|
|
41
|
-
Record method, result, measured cost, and caveats in `08-spikes.md`. Put whatever the run produced that the record cites in `evidence/` beside it, so a reader checking a claim opens the artifact rather than taking the prose for it
|
|
42
|
+
Record method, result, measured cost, and caveats in `08-spikes.md`. Put whatever the run produced that the record cites in `evidence/` beside it, so a reader checking a claim opens the artifact rather than taking the prose for it, and keep the arm scripts in `scripts/`. Reach for a test harness the project already carries before building one, and copy how it isolates its fixture rather than only what it asserts.
|
|
42
43
|
|
|
43
44
|
## Step 1: detect the mode
|
|
44
45
|
|
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 |
|
|
@@ -75,6 +75,12 @@ toolkit's own `snippets/`, so a session reaches one at its `@` reference with
|
|
|
75
75
|
no copy to reconcile. `canon snippets list --json` carries the catalog and
|
|
76
76
|
`canon snippets create` is the one verb left that still writes a file.
|
|
77
77
|
|
|
78
|
+
## Tooling diff and sync
|
|
79
|
+
|
|
80
|
+
`canon tooling diff <stack> [target]` is the read-only comparison, and a session asking what differs reaches for it by default. It writes nothing and exits 1 when anything differs and 0 when nothing does, so it gates CI. `--json` adds the scan record on stdout with `ok: true`, and the frame goes to stderr. A refusal such as an unknown stack, an excluded stack, or a toolkit-root target still exits 1 and answers `{ ok: false, reason, message }`, so a caller branches on the record and reads a typo'd stack name as a refusal rather than as drift.
|
|
81
|
+
|
|
82
|
+
`canon tooling sync <stack> [target] --check` reports the same list and always exits 0. It stays for scripts that already call it. `sync` itself writes only under `--write`, and a headless run with neither flag reports and exits 1.
|
|
83
|
+
|
|
78
84
|
## Install guards
|
|
79
85
|
|
|
80
86
|
`canon gov install` requires its first argument under
|
|
@@ -188,7 +194,7 @@ since `src/tooling/` runs its own inject machinery and has no walk to attribute.
|
|
|
188
194
|
The chain is ordered nearest stack first, which is what a `--skip` run needs:
|
|
189
195
|
recording the leaf alone would send the next report measuring against a layer the
|
|
190
196
|
target deliberately does not carry. The report loads exactly those stacks, scans
|
|
191
|
-
them the way `canon tooling
|
|
197
|
+
them the way `canon tooling diff` would, and counts what differs per category
|
|
192
198
|
under `tooling.counts`.
|
|
193
199
|
|
|
194
200
|
`measured` is the field the section exists for. A target carrying no chain
|
package/docs/agents/scripting.md
CHANGED
|
@@ -78,7 +78,7 @@ surface is in `skills-audit.md`.
|
|
|
78
78
|
CANON_NON_INTERACTIVE=1 canon tooling create astro
|
|
79
79
|
|
|
80
80
|
# Report what a stack would change, writing nothing
|
|
81
|
-
CANON_NON_INTERACTIVE=1 canon tooling
|
|
81
|
+
CANON_NON_INTERACTIVE=1 canon tooling diff astro /path/to/project
|
|
82
82
|
|
|
83
83
|
# Sync a stack into a target project, overwriting its golden configs
|
|
84
84
|
CANON_NON_INTERACTIVE=1 canon tooling sync astro /path/to/project --write
|
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/docs/target-projects.md
CHANGED
|
@@ -265,7 +265,7 @@ Add `--json` for the machine-readable report, and `--exit-code` to fail a CI job
|
|
|
265
265
|
|
|
266
266
|
Tooling reports under a section of its own, and `measured` there says whether the target ever recorded a chain. One that never ran a tooling sync reports unmeasured rather than clean, which is what separates tooling nobody has looked at from tooling that is current. A workspace root records nothing either way, since each package resolves its own chain.
|
|
267
267
|
|
|
268
|
-
Reconcile the configs with `canon tooling
|
|
268
|
+
Reconcile the configs with `canon tooling diff <stack> <path>` to read which files differ, then run `canon tooling sync <stack> <path> --write` to apply them. The drift report counts categories and the diff names paths, which is the difference worth knowing before a golden config the project edited is replaced.
|
|
269
269
|
|
|
270
270
|
### Catch-all
|
|
271
271
|
|
|
@@ -65,7 +65,9 @@ The routing test is whether the repository can answer an item today. A session g
|
|
|
65
65
|
|
|
66
66
|
A groundwork track may run experiments to settle a question, writing a fixture it reads itself under `.canon/tmp/runs/groundwork-fixtures/<slug>/` and spawning up to three billed headless runs before it asks. A fixture a headless run is pointed at sits outside the repository, since a session started under the project root inherits that project's `CLAUDE.md` and rules and would measure them instead of the arm.
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
A spike starts against a sample, bounded on input size, duration, and spend, and scales up once the sample shows the method works. Nothing checks this, so it holds while a session reads the standard.
|
|
69
|
+
|
|
70
|
+
What a spike leaves behind splits on whether a later reader needs the file to check a claim. Bulk input, such as a large fixture, is re-runnable and cited by nothing, so the scratch path above is the right lifetime for it. Everything a reader opens to verify a result lives inside the track in one of three folders: `evidence/` for a recording or a render, `scripts/` for the arm scripts, and `clones/` for checkouts and copies of outside material a claim rests on. The scratch tree holds only what can be deleted without loss, which none of those is.
|
|
69
71
|
|
|
70
72
|
#### Session 2
|
|
71
73
|
|
|
@@ -166,7 +168,7 @@ The list stays written in the skill body as the fallback for a target whose inst
|
|
|
166
168
|
|
|
167
169
|
Capture leads rather than trails because a routed fact edits a tracked file, which has to reach the branch before the commit steps run.
|
|
168
170
|
|
|
169
|
-
|
|
171
|
+
The chain ends at capture, and review is run on its own.
|
|
170
172
|
|
|
171
173
|
Run `memory-review` standalone to curate the whole pen. An entry it retires moves to `.canon/memory/archive/` rather than being deleted, since a bulk pass has no undo.
|
|
172
174
|
|
|
@@ -260,7 +262,7 @@ This section is the corpus the coverage claim is measured against: every name `c
|
|
|
260
262
|
| `canon:git-commit` | When the staged set is one concern, or was staged hunk by hand |
|
|
261
263
|
| `canon:git-branch` | When a branch name needs generating or renaming to conventional form |
|
|
262
264
|
| `canon:git-pr` | When a pull request needs a title and body written from the diff |
|
|
263
|
-
| `canon:memory-review` |
|
|
265
|
+
| `canon:memory-review` | When the pen has grown, to propose where each entry belongs |
|
|
264
266
|
|
|
265
267
|
### After the pull request opens
|
|
266
268
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Writes web/public/favicon.svg from assets/brand/mark.svg and the
|
|
3
|
-
*
|
|
2
|
+
* Writes web/public/favicon.svg from assets/brand/mark.svg and the favicon's
|
|
3
|
+
* own colors in src/design/favicon.ts, apart from the page accent tokens.
|
|
4
4
|
*
|
|
5
5
|
* The page is the fourth surface to carry the mark as a favicon and the only
|
|
6
6
|
* one that can answer for itself. `regen-hero.sh` and `src/design/render.ts`
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { writeFileSync } from 'node:fs'
|
|
19
19
|
|
|
20
|
+
import { FAVICON_COLORS, renderFavicon } from '../../src/design/favicon'
|
|
21
|
+
|
|
20
22
|
const root = new URL('../..', import.meta.url).pathname
|
|
21
23
|
|
|
22
24
|
const mark = (await Bun.file(`${root}assets/brand/mark.svg`).text()).trim()
|
|
@@ -25,43 +27,10 @@ if (!mark) {
|
|
|
25
27
|
process.exit(1)
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
if (!match) {
|
|
34
|
-
console.error(
|
|
35
|
-
`regen-web-favicon: tokens.css carries no --color-${name}, refusing to write a colorless favicon`,
|
|
36
|
-
)
|
|
37
|
-
process.exit(1)
|
|
38
|
-
}
|
|
39
|
-
return match[1] as string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const dark = read('accent')
|
|
43
|
-
const light = read('light-accent')
|
|
44
|
-
|
|
45
|
-
// The shapes only, with the source's authoring comment dropped. Both fills are
|
|
46
|
-
// replaced by a rule rather than an attribute so one branch can flip both.
|
|
47
|
-
const shapes = mark
|
|
48
|
-
.replace(/<!--[\s\S]*?-->/, '')
|
|
49
|
-
.trim()
|
|
50
|
-
.replace(/^<svg[^>]*>/, '')
|
|
51
|
-
.replace(/<\/svg>$/, '')
|
|
52
|
-
.replaceAll(' fill="currentColor"', '')
|
|
53
|
-
.trim()
|
|
54
|
-
|
|
55
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80">
|
|
56
|
-
<style>
|
|
57
|
-
path, rect { fill: ${light}; }
|
|
58
|
-
@media (prefers-color-scheme: dark) { path, rect { fill: ${dark}; } }
|
|
59
|
-
</style>
|
|
60
|
-
${shapes.replace(/\n\s*/g, '\n ')}
|
|
61
|
-
</svg>
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
writeFileSync(`${root}web/public/favicon.svg`, svg)
|
|
30
|
+
writeFileSync(
|
|
31
|
+
`${root}web/public/favicon.svg`,
|
|
32
|
+
renderFavicon(mark, FAVICON_COLORS),
|
|
33
|
+
)
|
|
65
34
|
console.log(
|
|
66
|
-
`regen-web-favicon: wrote web/public/favicon.svg (${light} / ${dark})`,
|
|
35
|
+
`regen-web-favicon: wrote web/public/favicon.svg (${FAVICON_COLORS.light} / ${FAVICON_COLORS.dark})`,
|
|
67
36
|
)
|
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)
|
package/src/commands/tooling.ts
CHANGED
|
@@ -39,6 +39,12 @@ interface SyncOptions {
|
|
|
39
39
|
readonly skip?: string
|
|
40
40
|
readonly check?: boolean
|
|
41
41
|
readonly write?: boolean
|
|
42
|
+
readonly diff?: { readonly json: boolean }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DiffOptions {
|
|
46
|
+
readonly skip?: string
|
|
47
|
+
readonly json?: boolean
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
interface InjectOptions {
|
|
@@ -59,7 +65,7 @@ interface ListOptions {
|
|
|
59
65
|
|
|
60
66
|
type Prepared =
|
|
61
67
|
| { readonly ok: true; readonly chain: Manifest[]; readonly target: string }
|
|
62
|
-
| { readonly ok: false; readonly error: string }
|
|
68
|
+
| { readonly ok: false; readonly reason: string; readonly error: string }
|
|
63
69
|
|
|
64
70
|
export function register(program: Command): void {
|
|
65
71
|
const tooling = program
|
|
@@ -74,7 +80,10 @@ export function register(program: Command): void {
|
|
|
74
80
|
.argument('[target]', 'Target directory', '.')
|
|
75
81
|
.helpOption('-h, --help', 'Show this help message')
|
|
76
82
|
.option('--skip <stack>', 'Drop a layer from the extends chain')
|
|
77
|
-
.option(
|
|
83
|
+
.option(
|
|
84
|
+
'--check',
|
|
85
|
+
'Report what would change and write nothing, always exiting 0. Prefer `canon tooling diff`',
|
|
86
|
+
)
|
|
78
87
|
.option('--write', 'Apply every change without prompting')
|
|
79
88
|
.addHelpText(
|
|
80
89
|
'after',
|
|
@@ -98,6 +107,42 @@ export function register(program: Command): void {
|
|
|
98
107
|
},
|
|
99
108
|
)
|
|
100
109
|
|
|
110
|
+
tooling
|
|
111
|
+
.command('diff')
|
|
112
|
+
.description('Report how a target differs from a stack and write nothing')
|
|
113
|
+
.argument('[stack]', 'Tooling stack name (e.g. base, vite-react)')
|
|
114
|
+
.argument('[target]', 'Target directory', '.')
|
|
115
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
116
|
+
.option('--skip <stack>', 'Drop a layer from the extends chain')
|
|
117
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
118
|
+
.addHelpText(
|
|
119
|
+
'after',
|
|
120
|
+
[
|
|
121
|
+
'',
|
|
122
|
+
'The comparison `canon tooling sync --check` reports, under a name that',
|
|
123
|
+
'says it only asks. Exit codes:',
|
|
124
|
+
' 0 the target matches the stack',
|
|
125
|
+
' 1 a file, script, dependency, or gitignore entry differs, or a refusal',
|
|
126
|
+
'',
|
|
127
|
+
'`sync --check` reports the same list and always exits 0. Use `diff` to',
|
|
128
|
+
'gate CI, and keep `--check` for scripts that already call it.',
|
|
129
|
+
'',
|
|
130
|
+
'Examples:',
|
|
131
|
+
' canon tooling diff base',
|
|
132
|
+
' canon tooling diff base --json',
|
|
133
|
+
'',
|
|
134
|
+
].join('\n'),
|
|
135
|
+
)
|
|
136
|
+
.action(
|
|
137
|
+
async (stack: string | undefined, target: string, opts: DiffOptions) => {
|
|
138
|
+
process.exitCode = await runSync(stack, target, {
|
|
139
|
+
skip: opts.skip,
|
|
140
|
+
check: true,
|
|
141
|
+
diff: { json: opts.json === true },
|
|
142
|
+
})
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
101
146
|
tooling
|
|
102
147
|
.command('inject')
|
|
103
148
|
.description('Apply one stack to a target without scanning or prompting')
|
|
@@ -239,18 +284,27 @@ function prepare(stack: string, target: string, skip?: string): Prepared {
|
|
|
239
284
|
if (mismatch !== undefined) logWarn(mismatch)
|
|
240
285
|
|
|
241
286
|
if (!stackExists(PROJECT_ROOT, stack)) {
|
|
242
|
-
return {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
reason: 'unknown-stack',
|
|
290
|
+
error: `Stack not found: ${stack}`,
|
|
291
|
+
}
|
|
243
292
|
}
|
|
244
293
|
|
|
245
294
|
if (skip !== undefined) {
|
|
246
295
|
if (skip === stack) {
|
|
247
296
|
return {
|
|
248
297
|
ok: false,
|
|
298
|
+
reason: 'bad-skip',
|
|
249
299
|
error: `Cannot --skip the stack being synced: ${skip}`,
|
|
250
300
|
}
|
|
251
301
|
}
|
|
252
302
|
if (!stackExists(PROJECT_ROOT, skip)) {
|
|
253
|
-
return {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
reason: 'unknown-skip',
|
|
306
|
+
error: `Stack to skip not found: ${skip}`,
|
|
307
|
+
}
|
|
254
308
|
}
|
|
255
309
|
}
|
|
256
310
|
|
|
@@ -258,6 +312,7 @@ function prepare(stack: string, target: string, skip?: string): Prepared {
|
|
|
258
312
|
if (resolved === PROJECT_ROOT) {
|
|
259
313
|
return {
|
|
260
314
|
ok: false,
|
|
315
|
+
reason: 'toolkit-root',
|
|
261
316
|
error:
|
|
262
317
|
'Cannot run against toolkit root. Files here are the source of truth.',
|
|
263
318
|
}
|
|
@@ -275,40 +330,44 @@ async function runSync(
|
|
|
275
330
|
target: string,
|
|
276
331
|
opts: SyncOptions,
|
|
277
332
|
): Promise<number> {
|
|
278
|
-
intro('canon tooling sync')
|
|
333
|
+
intro(opts.diff === undefined ? 'canon tooling sync' : 'canon tooling diff')
|
|
279
334
|
|
|
280
335
|
if (opts.check === true && opts.write === true) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
336
|
+
return refuse(
|
|
337
|
+
opts,
|
|
338
|
+
'conflicting-flags',
|
|
339
|
+
'Pass --check or --write, not both.',
|
|
340
|
+
)
|
|
284
341
|
}
|
|
285
342
|
|
|
286
343
|
const selected = stack ?? (await promptForStack())
|
|
287
344
|
if (selected === undefined) {
|
|
288
|
-
|
|
289
|
-
outro()
|
|
290
|
-
return 1
|
|
345
|
+
return refuse(opts, 'no-stacks', 'No tooling stacks found')
|
|
291
346
|
}
|
|
292
347
|
|
|
293
348
|
if (isStackExcluded(selected)) {
|
|
294
|
-
|
|
349
|
+
return refuse(
|
|
350
|
+
opts,
|
|
351
|
+
'excluded-stack',
|
|
295
352
|
'Claude is managed by `canon claude`, not `canon tooling`. Run `canon claude sync` instead.',
|
|
296
353
|
)
|
|
297
|
-
outro()
|
|
298
|
-
return 1
|
|
299
354
|
}
|
|
300
355
|
|
|
301
356
|
const prepared = prepare(selected, target, opts.skip)
|
|
302
|
-
if (!prepared.ok)
|
|
303
|
-
logWarn(prepared.error)
|
|
304
|
-
outro()
|
|
305
|
-
return 1
|
|
306
|
-
}
|
|
357
|
+
if (!prepared.ok) return refuse(opts, prepared.reason, prepared.error)
|
|
307
358
|
|
|
308
359
|
const result = scan(prepared.chain, prepared.target)
|
|
309
360
|
|
|
310
361
|
report(result)
|
|
311
362
|
|
|
363
|
+
if (opts.diff !== undefined) {
|
|
364
|
+
if (opts.diff.json) {
|
|
365
|
+
process.stdout.write(`${JSON.stringify({ ok: true, ...result })}\n`)
|
|
366
|
+
}
|
|
367
|
+
outro()
|
|
368
|
+
return result.totalChanges === 0 ? 0 : 1
|
|
369
|
+
}
|
|
370
|
+
|
|
312
371
|
const mode = resolveWriteMode(opts)
|
|
313
372
|
const { GREEN, NC } = palette(process.stderr)
|
|
314
373
|
|
|
@@ -353,6 +412,20 @@ async function runSync(
|
|
|
353
412
|
return 0
|
|
354
413
|
}
|
|
355
414
|
|
|
415
|
+
/**
|
|
416
|
+
* A diff caller branches on the record rather than the exit, since a shell
|
|
417
|
+
* wrapper can flatten the status. Every refusal therefore answers with a reason
|
|
418
|
+
* on stdout under `--json`, beside the exit 1 that keeps a gate failing.
|
|
419
|
+
*/
|
|
420
|
+
function refuse(opts: SyncOptions, reason: string, message: string): number {
|
|
421
|
+
logWarn(message)
|
|
422
|
+
if (opts.diff?.json) {
|
|
423
|
+
process.stdout.write(`${JSON.stringify({ ok: false, reason, message })}\n`)
|
|
424
|
+
}
|
|
425
|
+
outro()
|
|
426
|
+
return 1
|
|
427
|
+
}
|
|
428
|
+
|
|
356
429
|
type WriteMode = 'apply' | 'prompt' | 'report' | 'unauthorized'
|
|
357
430
|
|
|
358
431
|
type ApplyDecision = 'apply' | 'cancelled' | 'reported' | 'unauthorized'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface FaviconColors {
|
|
2
|
+
readonly light: string
|
|
3
|
+
readonly dark: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** The favicon's own fills, chosen for tab size and apart from the page accent tokens. */
|
|
7
|
+
export const FAVICON_COLORS: FaviconColors = {
|
|
8
|
+
light: '#c42938',
|
|
9
|
+
dark: '#e54e40',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The shapes only, with the source's authoring comment dropped. Both fills are
|
|
14
|
+
* replaced by a rule rather than an attribute so one branch can flip both.
|
|
15
|
+
*/
|
|
16
|
+
export function renderFavicon(mark: string, colors: FaviconColors): string {
|
|
17
|
+
const shapes = mark
|
|
18
|
+
.replace(/<!--[\s\S]*?-->/, '')
|
|
19
|
+
.trim()
|
|
20
|
+
.replace(/^<svg[^>]*>/, '')
|
|
21
|
+
.replace(/<\/svg>$/, '')
|
|
22
|
+
.replaceAll(' fill="currentColor"', '')
|
|
23
|
+
.trim()
|
|
24
|
+
|
|
25
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 80 80">
|
|
26
|
+
<style>
|
|
27
|
+
path, rect { fill: ${colors.light}; }
|
|
28
|
+
@media (prefers-color-scheme: dark) { path, rect { fill: ${colors.dark}; } }
|
|
29
|
+
</style>
|
|
30
|
+
${shapes.replace(/\n\s*/g, '\n ')}
|
|
31
|
+
</svg>
|
|
32
|
+
`
|
|
33
|
+
}
|
package/src/design/tokens.ts
CHANGED
|
@@ -377,7 +377,7 @@ export const TOKENS: DesignTokens = {
|
|
|
377
377
|
iconography: [
|
|
378
378
|
'No icon library is installed. `assets/brand/mark.svg` is the one authored icon, embedded inline in the hero topbar, and the surfaces otherwise draw literal glyph characters: `│ ├ ✓ ! ✗ + - ◆ ◇ ❯` for the terminal framing.',
|
|
379
379
|
'',
|
|
380
|
-
'The same mark ships
|
|
380
|
+
'The same mark ships in six independently-maintained copies that carry different colors by design rather than by drift, each fitted to the chrome it renders on: the dark accent for a dark-chrome surface, the light accent for a light-chrome one, and the pair the web favicon takes from `src/design/favicon.ts`. Unifying them or repairing the one that looks drifted would break the fit each was chosen for. `canon/context/design.md` carries which file holds each copy.',
|
|
381
381
|
].join('\n'),
|
|
382
382
|
}
|
|
383
383
|
|
|
@@ -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/groundwork.md
CHANGED
|
@@ -130,9 +130,15 @@ Each spike carries four things:
|
|
|
130
130
|
|
|
131
131
|
Cost is a report rather than a limit, and it is what makes the next spike estimable before anyone commits to it. Record it even when it comes to a single read.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
Start a spike against a sample and scale up only once the sample shows the method works. Bound the sample on all three of input size, duration, and spend, since fifty minutes of audio is a runtime and a bill before it is a file size, and three minutes of it usually shows whether the pipeline runs at all. The same holds for a dataset, an image set, or a batch of queries. Record the sample's bounds and the figure for the full input in the spike, so a reader can tell what the result was measured on and what the scale-up costs. The rule reaches only a track, since that is the surface that reads this standard, and nothing checks that a spike ran small first, so it holds only while a session reads it.
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
A spike also leaves files behind, and they split on whether a later reader needs the file to check a claim rather than on whether it is markdown or re-runnable. A file a reader opens to verify how a result was produced or what it showed lives inside the track. A file that is only bulk input, such as a large fixture or a copied asset, stays outside it wherever the surface driving the spike puts it. Three subfolders inside the track carry the first kind, and none takes a number, since numbering is the read order over the files a reader opens in sequence and these are reached from the claim that cites them instead:
|
|
136
|
+
|
|
137
|
+
- `evidence/`: what a run produced and the record cites, being a recording, a render, or a frame pulled from one, beside the file citing it.
|
|
138
|
+
- `scripts/`: the arm scripts and harnesses a spike ran. An arm script is re-runnable, and it is also what a reader opens to check the method, so this refines the earlier split, which sorted on re-runnable alone, and does not contradict it.
|
|
139
|
+
- `clones/`: checkouts and copies of outside material a claim rests on, kept small enough to hold in the folder. A checkout too large to keep is cited by its address and commit instead.
|
|
140
|
+
|
|
141
|
+
`spikes/`, `web/`, and `research/` were considered and folded into these, so do not reopen them. A spike's files sit under the three above, and a fetched page belongs in `clones/`. The `web/` case is the arguable one, since a screenshot of a reference site could sit apart, and it folds into `evidence/` because that is where a cited artifact already lives.
|
|
136
142
|
|
|
137
143
|
Reach for a test harness the project already carries before building one. A track needing an experiment no existing harness can express has found a finding, and it belongs in the folder rather than in a new abstraction.
|
|
138
144
|
|
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.
|
|
@@ -18,9 +18,9 @@ packages = [
|
|
|
18
18
|
|
|
19
19
|
[scripts]
|
|
20
20
|
"check:spell" = "cspell '**' '.*/**' '.*' --no-progress --color --show-context"
|
|
21
|
-
"check:format" = "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --diff --indent 2
|
|
21
|
+
"check:format" = "prettier --check --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && find scripts -name '*.sh' -exec shfmt --diff --indent 2 {} +"
|
|
22
22
|
"check:shell" = "find scripts -name '*.sh' -exec shellcheck --severity=warning {} +"
|
|
23
|
-
"format" = "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && shfmt --write --indent 2
|
|
23
|
+
"format" = "prettier --write --log-level warn --ignore-path .gitignore --ignore-path .prettierignore . && find scripts -name '*.sh' -exec shfmt --write --indent 2 {} +"
|
|
24
24
|
"prepare" = "husky"
|
|
25
25
|
"check" = "./scripts/verify.sh"
|
|
26
26
|
|
|
@@ -35,7 +35,7 @@ Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and n
|
|
|
35
35
|
- Runtime: `bun` as package manager and script runner, `bunx` over `npx` for a one-off executable.
|
|
36
36
|
- Formatting: Prettier for what it parses, shfmt for shell. Two formatters because Prettier has no shell parser.
|
|
37
37
|
- Spelling: cspell over the whole tree, with project vocabulary split into a project-terms dictionary and a tech-stack one.
|
|
38
|
-
- Shell: shfmt formats and shellcheck lints at warning severity.
|
|
38
|
+
- Shell: shfmt formats and shellcheck lints at warning severity. Both take their files from `find scripts -name '*.sh'`, since `scripts/` can mix shell with TypeScript and shellcheck has no directory mode. A shell script with a shebang and no `.sh` extension is covered by neither, so give it the extension.
|
|
39
39
|
- Commits: commitlint against conventional commits, wired through the husky `commit-msg` hook. Format is `<type>(<scope>): <subject>` in imperative mood with no trailing period.
|
|
40
40
|
- Dev dependencies: `prettier`, `cspell`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`. Install via `bun add -D`.
|
|
41
41
|
|
|
@@ -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
|