@erclx/aitk 0.100.0 → 0.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.
@@ -51,8 +51,6 @@ Does not govern:
51
51
 
52
52
  The character bans sit in `markdown.md` under `## Punctuation` rather than here, because an em dash and a semicolon are typography and the bans here reach the words a sentence chooses and the claims it makes. A surface applying both reads both files.
53
53
 
54
- Illustrate a pattern ban with a multi-word phrase. An audit reading this section harvests the single lowercase backticked words out of every `- Do not use ` bullet into a literal ban set, so a one-word example bans that word everywhere it appears rather than banning the pattern it stands for. Both the toolkit command and the audit hook a project installs parse that shape, so the constraint holds wherever this file lands.
55
-
56
54
  ## Frontmatter descriptions
57
55
 
58
56
  When frontmatter carries a short `title` or `description` used for catalog display:
@@ -13,7 +13,7 @@ The folder is gitignored. Board state changes when work ships rather than when a
13
13
 
14
14
  ## Scope
15
15
 
16
- Governs the task board under `.claude/tasks/`: folder layout, filenames, frontmatter, file format, origin lines, execution ordering, and archiving.
16
+ Governs the task board under `.claude/tasks/`: folder layout, filenames, frontmatter, file format, origin lines, execution ordering, the backlog beside it, and archiving.
17
17
 
18
18
  Does not govern:
19
19
 
@@ -30,6 +30,7 @@ Does not govern:
30
30
  .claude/tasks/
31
31
  ├── index.md ← generated, never hand-edited
32
32
  ├── priority.md ← hand-maintained execution order
33
+ ├── backlog.md ← unordered, what is not being scheduled
33
34
  ├── session-<slug>.md ← optional, what a compaction is about to destroy
34
35
  ├── v09.0-sync-paths.md
35
36
  └── v13.0-toolkit-drift.md
@@ -37,7 +38,9 @@ Does not govern:
37
38
 
38
39
  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.
39
40
 
40
- Siblings sit in the folder without being tasks, and each earns its place by being governed somewhere. `index.md` and `priority.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.
41
+ 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.
42
+
43
+ `backlog.md` is optional too. A project small enough that every task it holds would be planned soon carries none, and the ordering rules below then describe the whole board.
41
44
 
42
45
  The handoff takes one file per session for the reason a task does. A single shared path puts two sessions closing near each other on one file that neither can watch the other write, and the loser leaves no trace on a board with no history behind it.
43
46
 
@@ -49,7 +52,7 @@ The `claude-tasks` skill creates and archives task files. `claude-docs` marks ou
49
52
 
50
53
  ## Ordering
51
54
 
52
- `priority.md` carries execution order and what each task is waiting on. The generated index sorts by filename and says nothing about order, so without this file board state gets reconstructed by hand every session. Why the order is what it is belongs in `.claude/ROADMAP.md`, which is committed because that rationale has no substitute record.
55
+ `priority.md` carries execution order and what each task is waiting on. The generated index sorts by filename and says nothing about order, so without this file board state gets reconstructed by hand every session. Why one version sequences against another belongs in `.claude/ROADMAP.md`, which is committed because that rationale has no substitute record. Why a row sits where it does inside its group is stated on the row itself, in the column that already carries what the task is waiting on.
53
56
 
54
57
  Group tasks by readiness rather than by status, one row per task, under the columns each group fixes below. Keep it to links and blockers: tables, plus at most one sentence per section. A paragraph in `priority.md` is a defect whatever it says. Stating the shape this way is what lets a single diff fail, since a size cap only trips after the fact and every addition looks defensible on its own.
55
58
 
@@ -61,11 +64,15 @@ Readiness is three groups under fixed headings, `## Run now`, `## Up next`, and
61
64
 
62
65
  Each group fixes its own columns, which follow from the test above it rather than from preference. Neither half of the `## Run now` test is checkable without the file set and the plan sitting beside the task.
63
66
 
67
+ Row position inside `## Needs a plan` is the order those tasks get planned in, top first. The three tests answer whether a task can start, which is mechanical, and none of them answers which task is worth starting, which is a judgment no column holds. Position is where that judgment is recorded, so the top row is the answer to what to plan next and a reader needs no other surface to get it. The other two groups take the same reading, and it costs them little, since a group holding what is already planned is short by construction.
68
+
69
+ Position alone carries it, and no rank column exists. A number beside each row is a second thing to keep in step with the order it duplicates, and the file is edited by one session at a time, so the order the rows are written in is already unambiguous. State on each row why it sits where it does, in the same cell that carries what it is waiting on. A position with no stated reason is re-derived from memory by the next session, which is the failure the ordering replaces rather than moves.
70
+
64
71
  The `Waiting on` column under `## Up next` carries that reason in one of three forms. `## Needs a plan` states no file set at all, because a task with no plan has no bounded one to state. A group with no rows keeps its heading and its header row.
65
72
 
66
73
  Under `## Up next` a collision names the file held by the task already running, a sibling task names that task, and an external condition names both the condition and what would satisfy it. Naming what would satisfy it is what separates a blocked row from one nobody has examined, so a cell stating a condition with no way out of it fails the test. The header text is the contract the way the group names are, because anything reading the cell resolves the column by header rather than by position.
67
74
 
68
- `aitk tasks validate` reads those columns and reports where a row's claim and the tree disagree: a plan pointer resolving to no file, a row and a task file that do not map one to one, a task in two groups, and two `## Run now` rows touching a path in common. It also re-takes the two blocker kinds a command can settle, reporting a parked row whose cited task is archived or has closed every outcome and one whose cited file nothing under `## Run now` still holds. Both halves read a citation out of the cell rather than parsing it into fields, and a row citing neither is reported as untested, which is where the three kinds resting on a person's judgment land. Run it when the readiness claim is made rather than on a schedule, since the board is gitignored per-machine scratch and no shared moment exists to hang it on. It reports and never writes, so a session fixes the row it names.
75
+ Under `## Needs a plan` the cell carries two halves and each takes one clause: what the task needs before it can be planned, then why it sits at this position. A cell running past that is the paragraph this file already deletes, arriving one row at a time rather than all at once, and the group is where it costs the most, since it holds the rows nobody has read recently and is the longest group on any board that needs a backlog at all.
69
76
 
70
77
  ```markdown
71
78
  ---
@@ -88,13 +95,47 @@ description: One line on what the board covers
88
95
 
89
96
  ## Needs a plan
90
97
 
91
- | Task | Waiting on |
92
- | ------------------------------- | ---------------------------------------------- |
93
- | [vXX.Y <slug>](vXX.Y-<slug>.md) | <what the task needs before it can be planned> |
98
+ | Task | Waiting on |
99
+ | ------------------------------- | -------------------------------------------------------------- |
100
+ | [vXX.Y <slug>](vXX.Y-<slug>.md) | <what it needs before it can be planned, and why it sits here> |
94
101
  ```
95
102
 
96
103
  The tests live here so the board does not carry them. Writing them as a sentence under each heading produces the paragraph the rule above deletes, and a criterion with no home gets restated from memory every time the board is touched.
97
104
 
105
+ ## The backlog
106
+
107
+ A task sits on the board when it would plausibly be planned within the next few waves, and on `backlog.md` otherwise. The call is a judgment, so restate it whenever the board is swept rather than making it once: a backlogged task rises when the work in front of it lands or the world changes under it, and a board row falls to the backlog when it stops being near-term.
108
+
109
+ A mechanical test over age or origin was the alternative and neither predicts what gets picked next, which is the judgment the ordering exists to carry. Leaving everything on the board is the other alternative, and it is what produces a group too long to rank, where the ordering means nothing because nobody can hold the whole list in one reading.
110
+
111
+ What the split buys is that the board is short enough for its order to be read, and what it costs is a second surface to keep. That trade only pays while the backlog stays honest about what it is, which is why it carries no order, no groups, and no readiness claim.
112
+
113
+ Nothing is deleted. A backlogged task keeps its file, its findings, and its frontmatter, and the backlog row is a pointer at that file. The folder is gitignored and has no history behind it, so a row dropped without landing somewhere readable is gone with nothing to recover it from.
114
+
115
+ The backlog is a flat list of links under one heading, sorted by filename. Sorting mechanically is what keeps it from reading as a queue: the order is the same order the index already sorts in, so no position on it means anything.
116
+
117
+ ```markdown
118
+ ---
119
+ title: Backlog
120
+ description: One line on what the backlog holds
121
+ ---
122
+
123
+ # Backlog
124
+
125
+ Unordered. Nothing here is scheduled, and a task rises to `priority.md` when it becomes near-term.
126
+
127
+ - [vXX.Y <slug>](vXX.Y-<slug>.md)
128
+ - [vXX.Y <slug>](vXX.Y-<slug>.md)
129
+ ```
130
+
131
+ Add no fourth readiness group in place of this file. The three group names are the contract, and a backlog is a separate surface rather than a group because it makes no readiness claim at all: it says nobody has scheduled the task, which is a fact about attention rather than about whether the work can start.
132
+
133
+ ## Validation
134
+
135
+ `aitk tasks validate` reads the columns above and reports where a row's claim and the tree disagree: a plan pointer resolving to no file, a task file reached by neither surface, a task on both surfaces or in two groups, and two `## Run now` rows touching a path in common. It also re-takes the two blocker kinds a command can settle, reporting a parked row whose cited task is archived or has closed every outcome and one whose cited file nothing under `## Run now` still holds. Both halves read a citation out of the cell rather than parsing it into fields, and a row citing neither is reported as untested, which is where the three kinds resting on a person's judgment land. Run it when the readiness claim is made rather than on a schedule, since the board is gitignored per-machine scratch and no shared moment exists to hang it on. It reports and never writes, so a session fixes the row it names.
136
+
137
+ A task file is accounted for when a row on `priority.md` or a line on `backlog.md` names it, and reported when neither does. One check across both surfaces is what lets a task move between them without the move looking like a dropped file, and a task named by both is reported for the same reason a task in two groups is: it claims two things about itself and only one of them can hold. A project carrying no `backlog.md` is read as an empty backlog rather than refused, which leaves the one-to-one mapping this check ran before the second surface existed.
138
+
98
139
  ## Filenames
99
140
 
100
141
  `vXX.Y-<slug>.md`, where the version is the phase label zero-padded to two digits and the slug is kebab-case.
@@ -195,7 +236,7 @@ The line is what lets a merge close its own task. Every merge on `main` is a squ
195
236
  - Architectural reasoning that outlives the task. A finding explains why this task is shaped as it is. A decision the system keeps after the task closes belongs in `.claude/ARCHITECTURE.md`.
196
237
  - Narrative of the session that produced the task. A finding states what constrains the task, so what was probed, what it cost, and who decided belongs in the groundwork folder the `Groundwork:` line names. A task with no groundwork folder cuts the narrative rather than relocating it, since the board is not the fallback destination for it.
197
238
  - "In progress" or "Blocked" headings. Note status inline on the outcome instead.
198
- - Sequencing rationale or which version is active. Those belong in `.claude/ROADMAP.md`, which is committed because that reasoning has no substitute record.
239
+ - Sequencing rationale or which version is active. Why this task is planned before its neighbors goes on its row in `priority.md`, in the cell that already carries what it is waiting on. Why one version sequences against another belongs in `.claude/ROADMAP.md`, which is committed because that reasoning has no substitute record.
199
240
 
200
241
  ## Archiving
201
242
 
@@ -205,6 +246,8 @@ Two callers reach that command. The `claude-tasks` skill runs it inside a sessio
205
246
 
206
247
  One destination rather than a per-project choice is what lets the move happen without asking. It mirrors the plans archive at `.claude/plans-archive/` and stays gitignored, so an archived task does not start appearing in diffs. The cost is that the folder is unbacked, which is the same cost the plans archive already carries.
207
248
 
249
+ The archive clears a row from `priority.md` and reads no other surface, which holds because a task reaches a merge by being planned and handed out, and both steps move it onto the board first. A task archived straight off the backlog therefore leaves its line standing, and the validator reports that line as naming a file that is gone rather than the board losing it silently.
250
+
208
251
  Archiving a task does not archive its plan. `claude-docs` owns the plans sweep and moves a plan only when the closing task is its last live citation. The archive clears the task's row from `priority.md` itself, since a shipped task left in the ordering reads as ready to hand a worker. It leaves prose naming the task alone for a person to resolve.
209
252
 
210
253
  The row is matched by the link in its first cell rather than by a pattern against the whole line. A row names the task it is about in the first cell, so a link anywhere after that is a reference, such as a blocker pointing at what it waits on. Matching the line would delete the referring task's row too, on a board that is gitignored and has nothing to recover it from.
@@ -24,66 +24,62 @@ esac
24
24
 
25
25
  [ -f "$file" ] || exit 0
26
26
 
27
- # Read the closed-set word bans from the standard so the hook never carries a
28
- # second copy. Every "Do not use ... (`a`, `b`)" bullet contributes its
29
- # single-word backticked terms, which skips the multi-word and punctuation bans.
27
+ # The audit verb owns the ban sets, so this hook carries no copy of them. The
28
+ # awk this replaces parsed the word bans out of the project's own prose.md,
29
+ # hardcoded the em-dash and semicolon, and reached none of the spellings, so a
30
+ # British spelling passed at edit time and a corpus check caught it later with
31
+ # nothing in between explaining the difference. A ban class added to the verb
32
+ # now reaches this hook without an edit here.
30
33
  #
31
- # The word bans sit in prose.md and the em-dash and semicolon bans in
32
- # markdown.md, so this parses the first and hardcodes the second. A "Do not use"
33
- # bullet added to markdown.md is parsed by nothing and enforces silently.
34
- standard="${CLAUDE_PROJECT_DIR:-.}/.claude/standards/prose.md"
35
- words=""
34
+ # The verb resolves its own paths under the cwd, so the project root is named
35
+ # rather than inherited. The payload carries an absolute file path, which is
36
+ # what lets the runner move without taking the argument out of reach.
37
+ root="${CLAUDE_PROJECT_DIR:-.}"
38
+
39
+ # A machine without the binary reports that nothing ran rather than exiting
40
+ # clean. An edit nobody checked and an edit carrying no violation are the same
41
+ # silence to a reader, so the enforcement a machine lacks is reported.
42
+ #
43
+ # A completed run always writes the record, and a refusal writes nothing, so an
44
+ # empty one means the verb declined to measure rather than measured and found
45
+ # nothing. It refuses outside a git repository, which is a project this hook can
46
+ # be installed into, and reading the findings alone reports that as a clean file.
36
47
  unread=""
37
- if [ -f "$standard" ]; then
38
- words=$(grep '^- Do not use ' "$standard" |
39
- grep -o '`[^`]*`' |
40
- tr -d '`' |
41
- grep -x '[a-z][a-z]*' |
42
- sort -u |
43
- paste -sd '|' -)
48
+ record=""
49
+ if command -v aitk >/dev/null 2>&1; then
50
+ record=$(cd "$root" 2>/dev/null && aitk markdown audit "$file" --json 2>/dev/null) || true
51
+ [ -n "$record" ] || unread="record"
44
52
  else
45
- # An absent standard empties the word list, and the awk below reads an empty
46
- # list as nothing to look for rather than as nothing found. The path is kept
47
- # so the report names what could not be read, since a file carrying no banned
48
- # word and a file nobody checked produce the same silence otherwise.
49
- unread="$standard"
53
+ unread="runner"
50
54
  fi
51
55
 
52
- hits=$(awk -v words="$words" '
53
- BEGIN { if (words != "") banned = "(^|[^a-z])(" words ")([^a-z]|$)" }
54
- /^```/ { in_code = !in_code; next }
55
- in_code { next }
56
- /—/ { print NR ": em-dash: " $0 }
57
- /;/ { print NR ": semicolon: " $0 }
58
- banned != "" {
59
- prose = tolower($0)
60
- gsub(/`[^`]*`/, "", prose)
61
- found = ""
62
- delete seen
63
- while (match(prose, banned)) {
64
- word = substr(prose, RSTART, RLENGTH)
65
- gsub(/[^a-z]/, "", word)
66
- if (word != "" && !(word in seen)) {
67
- seen[word] = 1
68
- found = found (found == "" ? "" : ", ") word
69
- }
70
- prose = substr(prose, RSTART + RLENGTH)
71
- }
72
- if (found != "") print NR ": banned word (" found "): " $0
73
- }
74
- ' "$file")
56
+ # The record decides rather than the exit code, so a binary predating a ban
57
+ # class still reports what it does measure. A refusal and an unparseable payload
58
+ # both yield nothing, which is the same silence a clean file produces.
59
+ hits=$(printf '%s' "$record" |
60
+ jq -r '.entries[]?.bans[]? | ":\(.line):\(.column + 1) \(.kind) \(.term)"' 2>/dev/null)
61
+
62
+ # A set the verb shipped empty measures nothing and would report a clean file.
63
+ # Reading the findings alone turns that narrowed check into a pass, so the field
64
+ # is read beside them.
65
+ empty=$(printf '%s' "$record" |
66
+ jq -r '[.bans.emptySets[]?] | join(", ")' 2>/dev/null)
75
67
 
76
- [ -z "$hits" ] && [ -z "$unread" ] && exit 0
68
+ [ -z "$hits" ] && [ -z "$unread" ] && [ -z "$empty" ] && exit 0
77
69
 
78
70
  nl=$'\n'
79
71
  msg=""
80
72
 
81
- if [ -n "$unread" ]; then
82
- msg=$(printf 'Standards-audit: no word ban checked in %s. Found no standard at %s, so only the character bans ran. Restore it with `aitk standards install`.' "$file" "$unread")
73
+ if [ "$unread" = "runner" ]; then
74
+ msg=$(printf 'Standards-audit: nothing checked in %s. Found no `aitk` binary on PATH. Install one with `bun add -g @erclx/aitk`.' "$file")
75
+ elif [ "$unread" = "record" ]; then
76
+ msg=$(printf 'Standards-audit: nothing checked in %s. `aitk markdown audit` returned no record, which it does when it declines to measure. It needs a git repository to build its corpus.' "$file")
77
+ elif [ -n "$empty" ]; then
78
+ msg=$(printf 'Standards-audit: the shipped ban set is empty for %s, so %s was checked against a narrowed set. Reinstall the toolkit with `bun add -g @erclx/aitk`.' "$empty" "$file")
83
79
  fi
84
80
 
85
81
  if [ -n "$hits" ]; then
86
- found=$(printf 'Standards-audit: prose.md and markdown.md violations in %s. Rewrite or restructure (do not lazy-swap).\n%s' "$file" "$hits")
82
+ found=$(printf 'Standards-audit: prose.md and markdown.md violations in %s. Rewrite the sentence (do not lazy-swap). A code span is the answer only where the token is genuinely an identifier under discussion.\n%s' "$file" "$hits")
87
83
  msg="${msg:+$msg$nl}$found"
88
84
  fi
89
85