@erclx/canon 4.59.0 → 4.60.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/claude-groundwork/REQUIREMENT.md +1 -0
- package/claude/skills/claude-groundwork/SKILL.md +1 -1
- package/claude/skills/claude-memory-review/SKILL.md +2 -2
- package/claude/skills/claude-tasks/REQUIREMENT.md +4 -0
- package/claude/skills/claude-tasks/SKILL.md +8 -2
- package/docs/agents/tasks.md +18 -3
- package/package.json +1 -1
- package/src/commands/labels.ts +7 -9
- package/src/commands/tasks.ts +17 -0
- package/src/tasks/validate.ts +46 -19
- package/tooling/nextjs/manifest.toml +3 -0
- package/tooling/nextjs/reference.md +6 -0
|
@@ -22,6 +22,7 @@ A track that closes with several separable findings writes only one task, and th
|
|
|
22
22
|
- Carry a lean and the finding that would overturn it on every open question, or admit that a measurement is missing
|
|
23
23
|
- Confine writes to the track folder, with the close-time task file, the experiment fixture, and the intake routing below as the only exceptions
|
|
24
24
|
- Route a closing-track finding the required task does not cover through `claude-intake`, rather than leaving it to be asked about. The route runs in the same session, so it is a write outside the folder rather than a handoff to a later one.
|
|
25
|
+
- Place the closing task's row through `claude-tasks` Step 4 rather than writing `priority.md` or `backlog.md` directly
|
|
25
26
|
- Link every claim about a source outside the project, and list an unread source as a lead rather than citing it
|
|
26
27
|
- Put a fixture a headless run is pointed at outside the repository
|
|
27
28
|
- Write the next-session file self-contained, since the folder is unbacked and dies with the machine
|
|
@@ -92,7 +92,7 @@ The standard sets the open question format and requires it inside a topic file a
|
|
|
92
92
|
1. Write `06-decision.md`. It states the problem once, names the goal, lists what to do, and lists what was considered and dropped.
|
|
93
93
|
2. Write `07-next-session.md` self-contained, so it survives a compaction that loses the conversation.
|
|
94
94
|
3. Update the file map in `README.md`.
|
|
95
|
-
4. Write one task file in `.canon/tasks/` recording what the track concluded, even when the conclusion is to do nothing. Follow `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` for the filename and frontmatter. Aside from an experiment fixture, this and the routing in Step 5 are the only ways close mode reaches outside the folder.
|
|
95
|
+
4. Write one task file in `.canon/tasks/` recording what the track concluded, even when the conclusion is to do nothing. Follow `${CLAUDE_SKILL_DIR}/../../standards/tasks.md` for the filename and frontmatter. Place the row through `claude-tasks` Step 4, which checks the roster for a live orchestrator before writing `priority.md` or `backlog.md` directly. Aside from an experiment fixture, this and the routing in Step 5 are the only ways close mode reaches outside the folder.
|
|
96
96
|
5. When the task written in Step 4 does not cover every finding the track surfaced, route what it leaves out through `claude-intake`. Skip this step when it does.
|
|
97
97
|
6. Report uncited external claims. Closing already reads every file in the folder, so list any statement about a source outside the project that carries neither a link nor a lead entry. Report and do not block, because judging whether a sentence makes an external claim is the call a checker gets wrong.
|
|
98
98
|
|
|
@@ -65,7 +65,7 @@ For each in-scope entry (see Scope), pick one action:
|
|
|
65
65
|
- **Promote to `CLAUDE.md`**: the rule is cross-domain behavior or a design principle applied across the whole project.
|
|
66
66
|
- **Promote to a skill body**: the rule fires only when editing a specific path-scoped domain. Name the target skill.
|
|
67
67
|
- **Promote to a standards file**: the rule is an authoring reference that belongs in the project's own standards folder as `<domain>.md`.
|
|
68
|
-
- **Promote to a context entry**: the entry states a fact about a domain carrying an entry in `.claude/context/index.md`. Append it to `.canon/tmp/memory-routing/<slug>.md` in the format `claude-memory-capture` writes, and tell the user to run `/claude-docs` from a branch. Do not edit the context entry here.
|
|
68
|
+
- **Promote to a context entry**: the entry states a fact about a domain carrying an entry in `.claude/context/index.md`. Append it to `.canon/tmp/memory-routing/<slug>.md` at the main worktree root, in the format `claude-memory-capture` writes, and tell the user to run `/claude-docs` from a branch. Do not edit the context entry here.
|
|
69
69
|
- **Hand off to governance**: the rule is coding-standards class (typescript, testing, naming, error-handling, performance, logging, concurrency, planning). Do not author the rule file inline. Never edit the synced `.claude/rules/` copies of toolkit rules, because `canon gov sync` overwrites them. Stop at handoff.
|
|
70
70
|
- In the toolkit repo, point the user at `internal-governance` and `${CLAUDE_SKILL_DIR}/../../standards/rule.md`, which own the source-of-truth rules under `governance/rules/`.
|
|
71
71
|
- In a target project, point the user at the `create-rule` skill, which scaffolds a project-local rule under `.claude/rules/`.
|
|
@@ -150,7 +150,7 @@ Free-form text after the verb is a reason. Capture it in the receipt but do not
|
|
|
150
150
|
Action by action type:
|
|
151
151
|
|
|
152
152
|
- **Promote**: use `Edit` to insert the rewritten rule into the target surface, then archive the memory file.
|
|
153
|
-
- **Promote to a context entry**: append the fact to `.canon/tmp/memory-routing/<slug>.md
|
|
153
|
+
- **Promote to a context entry**: append the fact to `.canon/tmp/memory-routing/<slug>.md` at the main worktree root, then archive the memory file. `claude-docs` folds it in on its next run from a branch, which is what keeps one skill writing context entries.
|
|
154
154
|
- **Hand off**: do not edit governance. Archive the memory file only if the user confirmed the handoff explicitly. Otherwise leave it in place.
|
|
155
155
|
- **Retire**: archive the memory file.
|
|
156
156
|
|
|
@@ -11,12 +11,15 @@ Without this skill, a task file gets a filename and frontmatter invented on the
|
|
|
11
11
|
|
|
12
12
|
Archiving fails in two ways that both lose work. Moving the file, editing the ordering file, and regenerating the index as three separate acts drifts from the one command the merge hook calls, so the attended and unattended paths stop agreeing. And an all-`[x]` task gets archived while its pull request is still open, because marking outcomes happens on the branch as the first step of shipping. The board is gitignored, so nothing restores a task archived early.
|
|
13
13
|
|
|
14
|
+
Placing a row without checking for another writer collides the same way. Two sessions filing work at once can claim one phase label twice or land two rows beside each other unread, since neither reads the board before writing it.
|
|
15
|
+
|
|
14
16
|
## Must
|
|
15
17
|
|
|
16
18
|
- Resolve the board at the main worktree root, since a linked worktree writing to `pwd` creates a second board nothing reads
|
|
17
19
|
- Read the tasks standard before writing, rather than working the filename and frontmatter from memory
|
|
18
20
|
- Require an origin at creation, because that is the only moment the invariant is enforceable
|
|
19
21
|
- Propose the phase label from what is already on the board and pad it to two digits
|
|
22
|
+
- Check the roster for a live orchestrator before writing a row, and hand off rather than write when one is found
|
|
20
23
|
- Confirm the work reached the default branch before archiving
|
|
21
24
|
- Run the archive command and route on the reason it refuses, since each reason has one resolution
|
|
22
25
|
- Report an origin that carries no task, as a list rather than a prompt
|
|
@@ -28,6 +31,7 @@ Archiving fails in two ways that both lose work. Moving the file, editing the or
|
|
|
28
31
|
- Write a pull request line at creation, when any number is a guess at someone else's work
|
|
29
32
|
- Archive around a refusal
|
|
30
33
|
- Split the legacy single-file archive into per-task files nobody wrote
|
|
34
|
+
- Fall through to a direct write when no orchestrator is found and this session's own name starts with `worker-` or `planner-`, since those role bodies ban the write with no exception
|
|
31
35
|
|
|
32
36
|
## Guards
|
|
33
37
|
|
|
@@ -47,9 +47,15 @@ Write it immediately. Claude Code's tool permission dialog is the confirmation g
|
|
|
47
47
|
|
|
48
48
|
A task file with no row is a dropped task, so name the surface it lands on in the same pass that creates it. A task that would plausibly be planned within the next few waves takes a row under `## Needs a plan` in `.canon/tasks/priority.md`, positioned by where it sits against the rows already there, with the reason for that position in its `Waiting on` cell. Anything else takes a line in `.canon/tasks/backlog.md`, which is unordered and where in the file it goes means nothing.
|
|
49
49
|
|
|
50
|
-
The test and both file shapes are in `${CLAUDE_SKILL_DIR}/../../standards/tasks.md`.
|
|
50
|
+
The test and both file shapes are in `${CLAUDE_SKILL_DIR}/../../standards/tasks.md`.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Check the roster for a live orchestrator before writing either file. Read `canon sessions list --self --json` for this session's own `sessionId` and `name`, then read `canon sessions list --json` and match a row whose `repository` matches this session's own, whose `sessionId` differs from it, and whose `name` starts with `orchestrator-`. That is the convention every hand-launched controller on this machine currently uses, and it excludes every `worker-` and `planner-` session cleanly. Treat a refusal from either call the same as a roster read that failed.
|
|
53
|
+
|
|
54
|
+
- **Found.** Do not write `priority.md` or `backlog.md`. Report the surface the row would take and the reason worked out above, then hand off by messaging that session with the same information so it places the row itself rather than two sessions writing the board at once.
|
|
55
|
+
- **Not found, and this session's own name (already read above) starts with `worker-` or `planner-`.** Do not write either file. Those two role bodies ban a board write with no exception, and an orchestrator absent from the roster is not consent to break it. Report the row and the surface it would take, so the operator or a later orchestrator places it.
|
|
56
|
+
- **Not found otherwise, or the roster read fails.** Write the row directly, as today. A failed read is indistinguishable from a solo project holding no orchestrator, and stopping would strand the row on the one path this section otherwise keeps unconditional. From a linked worktree the file-editing tools refuse the main root, so the row lands through the same `Bash` route the file itself took.
|
|
57
|
+
|
|
58
|
+
Say which branch fired and why in the report. The call is a judgment restated on every sweep rather than a property of the task, and a placement with no stated reason is one the next sweep re-derives from nothing.
|
|
53
59
|
|
|
54
60
|
### Step 5: report unlinked origins
|
|
55
61
|
|
package/docs/agents/tasks.md
CHANGED
|
@@ -214,7 +214,7 @@ Seven checks run. Plan and Collisions reach one half each of the `## Run now` te
|
|
|
214
214
|
| ---------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
215
215
|
| Shape | A row whose cell count disagrees with its table's header, or one stranded behind a table a blank line already closed |
|
|
216
216
|
| Plan | A `## Run now` row whose Plan column carries no link, resolves to no file, or disagrees with the task's own line |
|
|
217
|
-
| Mapping | A row or backlog line naming no task file
|
|
217
|
+
| Mapping | A row or backlog line naming no task file |
|
|
218
218
|
| Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
|
|
219
219
|
| Ordering | A `## Needs a plan` row whose stated position disagrees with where it actually sits, or which states no position at all |
|
|
220
220
|
| Collisions | Two `## Run now` rows whose Touches columns name a path in common |
|
|
@@ -224,9 +224,9 @@ Shape runs before any other check reads a row, since a row failing it carries no
|
|
|
224
224
|
|
|
225
225
|
The Plan check reads the row and the task file both, because the two are written by different hands and only the task's own `Plan:` line reaches the archive. A row carrying a plan whose task states none is `plan-uncited`, and a pair naming two different plans is `plan-mismatched`. Both sides resolve against the board and against the project root before they compare, so a row writing `../plans/x.md` and a task writing `.canon/plans/x.md` name one file rather than two.
|
|
226
226
|
|
|
227
|
-
Mapping spans two surfaces, because a task sits on `priority.md` when it would plausibly be planned soon and on `backlog.md` otherwise. A
|
|
227
|
+
Mapping spans two surfaces, because a task sits on `priority.md` when it would plausibly be planned soon and on `backlog.md` otherwise. A row or a backlog line naming no task file is `task-unresolved`, and a file both surfaces name is `row-duplicated` for the reason a task in two groups is: it claims two things about itself and only one can hold. A task file neither surface names is `unplaced` rather than a finding, since that is the normal state between a session filing it and a live orchestrator placing it on the board. One check across both is what lets a task move between them without the move reading as a dropped file.
|
|
228
228
|
|
|
229
|
-
A backlog line is a bullet carrying a link to a sibling task, since the backlog is a flat unordered list rather than a table. A bullet holding prose is skipped rather than reported, which keeps the file's own intro out of the findings, and the task that bullet meant to name
|
|
229
|
+
A backlog line is a bullet carrying a link to a sibling task, since the backlog is a flat unordered list rather than a table. A bullet holding prose is skipped rather than reported, which keeps the file's own intro out of the findings, and the task that bullet meant to name still lands in the `unplaced` array below rather than the findings. A project carrying no `backlog.md` reads as an empty backlog rather than a refusal, which leaves the one-to-one mapping this check ran before the second surface existed.
|
|
230
230
|
|
|
231
231
|
Ordering reads a `## Needs a plan` row's `Waiting on` cell for the position it claims, and reports two failures off one walk. A row stating an ordinal is checked against where it actually sits, which is `row-misordered`. A row claiming no position in either form it may take is `row-unranked`, since its cell argues the task matters and ranks it against nothing, which leaves the order recording when each row was filed.
|
|
232
232
|
|
|
@@ -282,6 +282,21 @@ That claim collides with every row a later session writes under the folder, and
|
|
|
282
282
|
|
|
283
283
|
The scan reaches `## Run now` and stops, where the collision check stops. A cell in another group describes work nobody has planned, written as a sentence and rewritten once a plan exists, so a claim read off one reports on prose rather than on a file set. A parked folder claim surfaces when its row is promoted, which is when the cell becomes something a dispatcher can act on.
|
|
284
284
|
|
|
285
|
+
A task file neither surface names lands in a fourth array, on the same reasoning:
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{
|
|
289
|
+
"unplaced": [
|
|
290
|
+
{
|
|
291
|
+
"subject": "v50.6-a-standard-no-skill-reads", // canon-allow-reference: shows the subject field's real vXX.Y-slug shape, not a citation of a real task
|
|
292
|
+
"message": "is a task file with no row on the board and no line on the backlog."
|
|
293
|
+
}
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Under the roster-checked hand-off `claude-tasks` and `claude-groundwork` state, filing a task and placing its row are two acts a different session each may take, so a task caught between the two is ordinary rather than a finding and this array moves no exit code. It still reports, since it is the only local detector for a row a hand-edit dropped, a handoff message that never arrived, or an orchestrator that ended before placing it.
|
|
299
|
+
|
|
285
300
|
Exit codes: `0` every check passed, `1` refused, `2` at least one finding. The `reason` field carries which gate refused: `no-board`, `no-ordering`, or `no-groups`. A board grouping under headings of its own trips `no-groups` rather than being read against columns it never declared.
|
|
286
301
|
|
|
287
302
|
Columns are read from each table's own header rather than by position, so a project whose board differs from this one is reported for what it lacks. The `index`, `priority`, and `backlog` siblings are skipped, along with every pre-compaction handoff, which takes one file per session under a `session-` prefix. None of them is a task, and a handoff counted as one would be reported as a task carrying no row on every session that wrote one.
|
package/package.json
CHANGED
package/src/commands/labels.ts
CHANGED
|
@@ -155,9 +155,9 @@ export function register(program: Command): void {
|
|
|
155
155
|
"It also grades the title alone against standards/pr.md's ## Title",
|
|
156
156
|
'section: the `<type>(<scope>): <subject>` structure, lowercase casing',
|
|
157
157
|
'for the type, the scope, and the first subject word, and a 72-',
|
|
158
|
-
'character length cap. A
|
|
159
|
-
'
|
|
160
|
-
'string.',
|
|
158
|
+
'character length cap. A scan given no title, whether from a review',
|
|
159
|
+
'or from a bare --body or --body-file invocation, skips this check',
|
|
160
|
+
'rather than grading an empty string.',
|
|
161
161
|
'',
|
|
162
162
|
'Exit codes:',
|
|
163
163
|
' 0 none of the five found',
|
|
@@ -287,12 +287,10 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
287
287
|
|
|
288
288
|
const result = scanPhaseLabels(resolved)
|
|
289
289
|
const spelling = await scanTitleSpelling(resolved.title, process.cwd())
|
|
290
|
-
// A
|
|
291
|
-
//
|
|
290
|
+
// A scan given no title, whatever produced that absence, has nothing to
|
|
291
|
+
// grade and would otherwise fail as `structure` for the wrong reason.
|
|
292
292
|
const titleFormat =
|
|
293
|
-
resolved.
|
|
294
|
-
? checkTitleFormat(resolved.title)
|
|
295
|
-
: undefined
|
|
293
|
+
resolved.title !== '' ? checkTitleFormat(resolved.title) : undefined
|
|
296
294
|
|
|
297
295
|
logStep(resolved.source === 'review' ? 'Review comment' : 'Pull request')
|
|
298
296
|
logInfo(
|
|
@@ -373,7 +371,7 @@ async function runScan(opts: ScanOptions): Promise<number> {
|
|
|
373
371
|
: 'Title format issue found',
|
|
374
372
|
)
|
|
375
373
|
if (titleFormat === undefined) {
|
|
376
|
-
logInfo('
|
|
374
|
+
logInfo('the scan carries no title, so there is no format to grade')
|
|
377
375
|
} else if (titleFormat.conforms) {
|
|
378
376
|
logInfo(
|
|
379
377
|
'the title matches <type>(<scope>): <subject> and its casing and length rules',
|
package/src/commands/tasks.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
type Finding,
|
|
24
24
|
type FolderClaim,
|
|
25
|
+
type Unplaced,
|
|
25
26
|
type Untested,
|
|
26
27
|
type ValidateOutcome,
|
|
27
28
|
validateBoard,
|
|
@@ -887,6 +888,17 @@ function reportValidation(
|
|
|
887
888
|
} else {
|
|
888
889
|
for (const claim of outcome.claims) logWarn(describeClaim(claim))
|
|
889
890
|
}
|
|
891
|
+
|
|
892
|
+
// Unplaced is the normal state between a session filing a task and an
|
|
893
|
+
// orchestrator placing its row, so it moves no exit code. It still reports,
|
|
894
|
+
// since it is the only local detector for a row a hand-edit dropped or a
|
|
895
|
+
// handoff that never arrived.
|
|
896
|
+
logStep('Unplaced')
|
|
897
|
+
if (outcome.unplaced.length === 0) {
|
|
898
|
+
logInfo('every task file sits on the board or the backlog')
|
|
899
|
+
} else {
|
|
900
|
+
for (const task of outcome.unplaced) logWarn(describeUnplaced(task))
|
|
901
|
+
}
|
|
890
902
|
outro()
|
|
891
903
|
}
|
|
892
904
|
|
|
@@ -901,6 +913,7 @@ function reportValidation(
|
|
|
901
913
|
findings: outcome.findings,
|
|
902
914
|
untested: outcome.untested,
|
|
903
915
|
claims: outcome.claims,
|
|
916
|
+
unplaced: outcome.unplaced,
|
|
904
917
|
})}\n`,
|
|
905
918
|
)
|
|
906
919
|
}
|
|
@@ -921,6 +934,10 @@ function describeClaim(claim: FolderClaim): string {
|
|
|
921
934
|
return `${claim.group}: ${claim.subject} ${claim.message}`
|
|
922
935
|
}
|
|
923
936
|
|
|
937
|
+
function describeUnplaced(task: Unplaced): string {
|
|
938
|
+
return `${task.subject} ${task.message}`
|
|
939
|
+
}
|
|
940
|
+
|
|
924
941
|
async function runArchive(
|
|
925
942
|
task: string | undefined,
|
|
926
943
|
opts: ArchiveCommandOptions,
|
package/src/tasks/validate.ts
CHANGED
|
@@ -38,7 +38,6 @@ export const FINDING_KINDS = [
|
|
|
38
38
|
'plan-uncited',
|
|
39
39
|
'plan-mismatched',
|
|
40
40
|
'task-unresolved',
|
|
41
|
-
'row-missing',
|
|
42
41
|
'row-duplicated',
|
|
43
42
|
'row-misshapen',
|
|
44
43
|
'row-untabled',
|
|
@@ -83,6 +82,18 @@ export interface FolderClaim {
|
|
|
83
82
|
readonly message: string
|
|
84
83
|
}
|
|
85
84
|
|
|
85
|
+
/**
|
|
86
|
+
* A task file neither surface names. That is the normal state between a
|
|
87
|
+
* session filing it and a live orchestrator placing it, so it reports beside
|
|
88
|
+
* the findings and moves no exit code. It is still the only local detector for
|
|
89
|
+
* a row lost to a hand-edit, a handoff message that never arrived, or an
|
|
90
|
+
* orchestrator that ended before placing it.
|
|
91
|
+
*/
|
|
92
|
+
export interface Unplaced {
|
|
93
|
+
readonly subject: string
|
|
94
|
+
readonly message: string
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
/**
|
|
87
98
|
* A backlog line, which carries a pointer and nothing else. The backlog is
|
|
88
99
|
* explicitly unordered, so a line has no position to read and no columns to
|
|
@@ -116,6 +127,7 @@ export interface ValidateReport {
|
|
|
116
127
|
readonly findings: readonly Finding[]
|
|
117
128
|
readonly untested: readonly Untested[]
|
|
118
129
|
readonly claims: readonly FolderClaim[]
|
|
130
|
+
readonly unplaced: readonly Unplaced[]
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
export interface ValidateRefused {
|
|
@@ -556,15 +568,15 @@ function resolves(target: string, dir: string, root: string): boolean {
|
|
|
556
568
|
}
|
|
557
569
|
|
|
558
570
|
/**
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
571
|
+
* Reports a task cited from two places on the board, which claims two
|
|
572
|
+
* contradictory things about itself: a task belongs to exactly one group,
|
|
573
|
+
* board or backlog. A task file neither surface names is no longer reported
|
|
574
|
+
* here, since that is the normal state between a session filing it and a live
|
|
575
|
+
* orchestrator placing it.
|
|
563
576
|
*/
|
|
564
577
|
function checkMapping(
|
|
565
578
|
rows: readonly BoardRow[],
|
|
566
579
|
backlog: readonly BacklogRow[],
|
|
567
|
-
stems: readonly string[],
|
|
568
580
|
dir: string,
|
|
569
581
|
): Finding[] {
|
|
570
582
|
const findings: Finding[] = []
|
|
@@ -638,18 +650,6 @@ function checkMapping(
|
|
|
638
650
|
}
|
|
639
651
|
}
|
|
640
652
|
|
|
641
|
-
for (const stem of stems) {
|
|
642
|
-
if (!seen.has(stem) && !listed.has(stem)) {
|
|
643
|
-
findings.push({
|
|
644
|
-
kind: 'row-missing',
|
|
645
|
-
group: undefined,
|
|
646
|
-
subject: stem,
|
|
647
|
-
message:
|
|
648
|
-
'is a task file with no row on the board and no line on the backlog.',
|
|
649
|
-
})
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
653
|
return findings
|
|
654
654
|
}
|
|
655
655
|
|
|
@@ -878,6 +878,32 @@ function checkFolderClaims(
|
|
|
878
878
|
return claims
|
|
879
879
|
}
|
|
880
880
|
|
|
881
|
+
/**
|
|
882
|
+
* Reports a task file neither the board nor the backlog names. Under the
|
|
883
|
+
* roster-checked hand-off, filing a task and placing its row are two acts a
|
|
884
|
+
* different session each may perform, so this state is ordinary rather than
|
|
885
|
+
* an error, and it moves no exit code. It still surfaces here rather than
|
|
886
|
+
* nowhere, since it is the only local detector for a row a hand-edit dropped,
|
|
887
|
+
* a handoff message that never arrived, or an orchestrator that ended before
|
|
888
|
+
* placing it.
|
|
889
|
+
*/
|
|
890
|
+
function checkUnplaced(
|
|
891
|
+
rows: readonly BoardRow[],
|
|
892
|
+
backlog: readonly BacklogRow[],
|
|
893
|
+
stems: readonly string[],
|
|
894
|
+
): Unplaced[] {
|
|
895
|
+
const seen = new Set(rows.flatMap((row) => (row.stem ? [row.stem] : [])))
|
|
896
|
+
const listed = new Set(backlog.flatMap((row) => (row.stem ? [row.stem] : [])))
|
|
897
|
+
|
|
898
|
+
return stems
|
|
899
|
+
.filter((stem) => !seen.has(stem) && !listed.has(stem))
|
|
900
|
+
.map((stem) => ({
|
|
901
|
+
subject: stem,
|
|
902
|
+
message:
|
|
903
|
+
'is a task file with no row on the board and no line on the backlog.',
|
|
904
|
+
}))
|
|
905
|
+
}
|
|
906
|
+
|
|
881
907
|
/**
|
|
882
908
|
* Reads the task a blocker cell cites. A task pointer is a bare sibling
|
|
883
909
|
* filename, the way every `Task` column spells one, so a target carrying a
|
|
@@ -1136,7 +1162,7 @@ export async function validateBoard(
|
|
|
1136
1162
|
|
|
1137
1163
|
const findings = [
|
|
1138
1164
|
...shapeFindings,
|
|
1139
|
-
...checkMapping(rows, backlog,
|
|
1165
|
+
...checkMapping(rows, backlog, dir),
|
|
1140
1166
|
...checkPlans(rows, dir, root),
|
|
1141
1167
|
...(await checkPlanAgreement(rows, dir, root)),
|
|
1142
1168
|
...checkCollisions(rows),
|
|
@@ -1152,5 +1178,6 @@ export async function validateBoard(
|
|
|
1152
1178
|
findings,
|
|
1153
1179
|
untested: parked.untested,
|
|
1154
1180
|
claims: checkFolderClaims(rows, root),
|
|
1181
|
+
unplaced: checkUnplaced(rows, backlog, stems),
|
|
1155
1182
|
}
|
|
1156
1183
|
}
|
|
@@ -17,3 +17,6 @@ packages = [
|
|
|
17
17
|
[scripts.override]
|
|
18
18
|
"dev" = "WORKTREE_PORT_OFFSET=$(bash scripts/worktree-port.sh) && export WORKTREE_PORT_OFFSET && next dev --port $((3000 + WORKTREE_PORT_OFFSET))"
|
|
19
19
|
"screenshot" = "PREVIEW_PORT=$(bash scripts/worktree-port.sh 3000) && export PREVIEW_PORT && bash scripts/screenshot.sh"
|
|
20
|
+
|
|
21
|
+
[gitignore]
|
|
22
|
+
"# Next" = [".next/"]
|
|
@@ -33,6 +33,12 @@ Next has no config-file port hook, unlike `astro.config.mjs`'s `server.port` or
|
|
|
33
33
|
|
|
34
34
|
`create-next-app`'s own default `tsconfig.json` needs no changes beyond project-specific path aliases, so a golden copy here would ship nothing the scaffold does not already write.
|
|
35
35
|
|
|
36
|
+
## Gitignore (extend)
|
|
37
|
+
|
|
38
|
+
`[gitignore]` groups this stack edits, restated here per the manifest-to-reference symmetry:
|
|
39
|
+
|
|
40
|
+
- `"# Next" = [".next/"]`
|
|
41
|
+
|
|
36
42
|
## Development docs (extend)
|
|
37
43
|
|
|
38
44
|
Append to the `## Scripts` table:
|