@erclx/aitk 3.37.0 → 3.38.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "3.37.0",
4
+ "version": "3.38.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -88,7 +88,7 @@ Behavior paths carry two spellings, the one a surface authors at and the one it
88
88
 
89
89
  - `claude/skills/` and `.claude/skills/`
90
90
  - `governance/rules/` and `.claude/rules/`
91
- - `standards/` and `.claude/standards/`, which is the authoring root and this repository's generated mirror rather than an install destination, since the corpus installs into no project
91
+ - `standards/`, which is the authoring root and reaches a reader by resolution rather than by an install, so it carries no `.claude/` spelling
92
92
  - `snippets/` and `.claude/snippets/`
93
93
  - `internal/` and `tooling/`, which hold the stack references and the seed documents a target is handed
94
94
  - `CLAUDE.md` at the repository root, named as a file because a path prefix reaches nothing that sits in no folder
@@ -77,7 +77,7 @@ Where an item's call is the operator's, pair it with a suggestion rather than a
77
77
 
78
78
  ## Step 6: write the index
79
79
 
80
- Write `00-overview.md` last, once the clusters are filed and the counts are real. It carries the format block, the cluster table, the verdict counts, the ready list, and the open questions.
80
+ Write `00-overview.md` last, once the clusters are filed and the counts are real. It carries the cluster table, the verdict counts, the ready list, and the open questions.
81
81
 
82
82
  Each open question in the index is a labeled markdown link to its owning item's heading anchor. The index points and the item owns, so no answer slot appears in the index. One question in two answerable places has no rule for which wins, and retrieval walks item headings, so an answer typed into the index is found by nothing and lost silently.
83
83
 
@@ -9,7 +9,7 @@ description: Why a standard needs its shape read from the meta-standard before a
9
9
 
10
10
  Without this skill, a standard is written from memory of what other standards look like. It arrives without the scope section that says what the file does not govern, so the next author cannot tell whether a rule belongs to it or to a sibling, and two standards end up claiming the same subject with no way to settle which one wins.
11
11
 
12
- The consumed copy is the second failure. This repository mirrors `standards/` into `.claude/standards/` and asserts the two match, so a toolkit standard written into the mirror fails that assertion while looking like a correct file. One write surface removes the choice: `standards/` is what the resolver reads first and what every author writes to, in the toolkit and in a project alike.
12
+ The second write surface is the second failure. A standard put under `.claude/standards/` looks like a correct file and reaches no resolver, since nothing installs the corpus into a project and no repository generates a copy there. One write surface removes the choice: `standards/` is what the resolver reads first and what every author writes to, in the toolkit and in a project alike.
13
13
 
14
14
  A standard written and left there is the third. The index entry and the context table are both generated or maintained downstream of the write, so a file that ships without them is catalogued nowhere and reached only by whoever already knew the path.
15
15
 
@@ -19,13 +19,13 @@ A standard written and left there is the third. The index entry and the context
19
19
  - Write to `standards/`, creating it when absent, since it is the one root the resolver reads and the only surface either kind of author has
20
20
  - Confirm the slug and the full body with the user before writing
21
21
  - Emit the written path in full, so the terminal can resolve it
22
- - State what the write implies for whoever owns the folder, since a toolkit standard needs the consumed copy and the index regenerated and a project-local one needs copying to the toolkit to ship
22
+ - State what the write implies for whoever owns the folder, since a toolkit standard needs the index regenerated and a project-local one needs copying to the toolkit to ship
23
23
 
24
24
  ## Must not
25
25
 
26
26
  - Work the standard's shape or frontmatter from memory
27
27
  - Write a snippet, which carries no frontmatter and answers to a different authoring contract
28
- - Write into `.claude/standards/`, which is this repository's generated mirror and reaches no resolver
28
+ - Write into `.claude/standards/`, which no repository generates and no resolver reads
29
29
 
30
30
  ## Out of scope
31
31
 
@@ -9,7 +9,7 @@ What each finding from `aitk context audit` means. The command surface, its flag
9
9
 
10
10
  ## Required sections
11
11
 
12
- The required-section check reports what does not declare `## Overview` and `## Layout`, the two sections `.claude/standards/context.md` marks required. The list is held in code beside the numeric checkpoints rather than parsed out of the standard, so it fails on a defect rather than on a rewrite of the wording around it. A heading at any level counts, because a domain that split into a folder carries its overview in a sibling named `overview.md` where the section is the `#` title and an `##` beneath it would repeat the filename. Matching exactly is what keeps `## Layout catalog` from satisfying `Layout`.
12
+ The required-section check reports what does not declare `## Overview` and `## Layout`, the two sections `standards/context.md` marks required. The list is held in code beside the numeric checkpoints rather than parsed out of the standard, so it fails on a defect rather than on a rewrite of the wording around it. A heading at any level counts, because a domain that split into a folder carries its overview in a sibling named `overview.md` where the section is the `#` title and an `##` beneath it would repeat the filename. Matching exactly is what keeps `## Layout catalog` from satisfying `Layout`.
13
13
 
14
14
  Which unit answers depends on the folder. Entries of the folder named under `.claude/` are one domain each, so each answers for itself and a finding names the entry. Entries of a folder a domain split into describe that one domain between them, so any sibling answers and a finding names the folder. Holding a split folder to the rule per file would report every child beside its `overview.md`, and rolling the named folder up would let one conforming entry stand in for every other domain sitting next to it.
15
15
 
@@ -19,7 +19,7 @@ It reports rather than gates by default, the closer call because a missing secti
19
19
 
20
20
  ## Reference form
21
21
 
22
- The reference-form check reports an entry naming a sibling entry by bare filename where `.claude/standards/context.md` asks for the path it sits at. A bare name resolves against whichever folder its reader is already in, so a domain that splits into subfolders strands every inbound reference and the break surfaces nowhere. A path is checkable, which is what makes the form rule worth measuring at all: once a reference spells its path, the citation gate resolves it and a split that moves the file fails the push.
22
+ The reference-form check reports an entry naming a sibling entry by bare filename where `standards/context.md` asks for the path it sits at. A bare name resolves against whichever folder its reader is already in, so a domain that splits into subfolders strands every inbound reference and the break surfaces nowhere. A path is checkable, which is what makes the form rule worth measuring at all: once a reference spells its path, the citation gate resolves it and a split that moves the file fails the push.
23
23
 
24
24
  A finding is a backticked filename carrying no folder, matched against the entries beside the one that wrote it. The backticks are required, since a filename in running prose is not a reference a reader follows.
25
25
 
@@ -33,7 +33,7 @@ It reports and never gates, which is the split the citation check already draws.
33
33
 
34
34
  ## Length
35
35
 
36
- Length quotes its checkpoint from `.claude/standards/context.md`: roughly 150 rendered lines for an entry, applied to each of the 60 entries the audit measures, 51 of them under `.claude/context/` and 9 diagram files beside them. It counts rendered lines rather than source lines, wrapping each line at 80 columns and summing the heights.
36
+ Length quotes its checkpoint from `standards/context.md`: roughly 150 rendered lines for an entry, applied to each of the 60 entries the audit measures, 51 of them under `.claude/context/` and 9 diagram files beside them. It counts rendered lines rather than source lines, wrapping each line at 80 columns and summing the heights.
37
37
 
38
38
  A line is wrapped at the width it renders at, so a link counts as its anchor text and a backticked path counts whole, which parts the rendered count from a source count wherever an entry cites a path in backticks rather than as a link. Entries here are authored one line per bullet, so a block of fifteen paragraph-bullets occupies fifteen source lines and renders past sixty, which source counting cannot see.
39
39
 
@@ -45,7 +45,7 @@ Only the third is mechanical, and it is the provenance check already reported be
45
45
 
46
46
  It reports and never gates, which the standard settles rather than the usual judgment-against-fact line: there is no hard cap, so an entry that answers all three and is still long is a correct outcome. The JSON record carries the joined findings as `length`, each with a `causes` array in the standard's order, and the jurisdiction the join reads as `entries[].governed`. That flag is what parts a provenance list measured and empty from one never scanned, which the count alone cannot say. The join is published rather than left to a consumer, since deriving it means restating which question the provenance count answers.
47
47
 
48
- Depth and bullet weight are quoted from `.claude/standards/markdown.md`, which states both over every markdown file rather than over a context entry, so `aitk markdown audit` measures them and this command no longer does. They share `renderedHeight` with the length measure, since the two checkpoints sit in one section of that standard and a reader compares them. What the split costs is that a session wanting both numbers for one entry runs two commands, and what it buys is that either number can be had for a file in a folder this audit refuses to resolve.
48
+ Depth and bullet weight are quoted from `standards/markdown.md`, which states both over every markdown file rather than over a context entry, so `aitk markdown audit` measures them and this command no longer does. They share `renderedHeight` with the length measure, since the two checkpoints sit in one section of that standard and a reader compares them. What the split costs is that a session wanting both numbers for one entry runs two commands, and what it buys is that either number can be had for a file in a folder this audit refuses to resolve.
49
49
 
50
50
  ## Tables
51
51
 
@@ -63,7 +63,7 @@ Fenced blocks are excluded, which keeps a pinned version in an install command f
63
63
 
64
64
  ## Narration
65
65
 
66
- The narration check reports a bullet that states the design a sibling bullet replaced instead of rewriting it. `.claude/standards/context.md` asks for the rewrite because the subject is still live and two bullets on one subject leave a reader to work out which of them is current, and no other measure sees that shape.
66
+ The narration check reports a bullet that states the design a sibling bullet replaced instead of rewriting it. `standards/context.md` asks for the rewrite because the subject is still live and two bullets on one subject leave a reader to work out which of them is current, and no other measure sees that shape.
67
67
 
68
68
  It reads structure rather than words, which the corpus decided. Measured across the 39 entries this toolkit held the day it shipped, the terms carrying clean signal for a supersession are too rare to catch anything: `superseded` appears twice, `previously` three times, and `formerly`, `originally`, and `at first` never. The one term that would have caught the case a review caught by hand is `now`, which appears 57 times across 24 entries in correct present-tense prose. A list including it reports 57 lines to catch one, and a list excluding it reports nothing.
69
69
 
@@ -101,13 +101,13 @@ The coverage report classifies each decision as carrying a countable claim, a st
101
101
 
102
102
  Three limits are stated on every run rather than hidden. The countable signal reads digits alone, so a measured claim written in words reads as uncounted. Entries are counted by heading, and one heading holding several decisions counts once. Nothing is stored, so an entry rewritten tomorrow is classified afresh the next time the verb runs and no verdict goes stale.
103
103
 
104
- The report gates nothing. Deciding whether a sentence states a claim is a judgment no parser settles, so the output names candidates for a reader. This answers a different question from the verification anchors `.claude/standards/architecture.md` describes, which record that one cited number was re-read. That mechanism says whether a marked figure held, and this one says how much of the record could be checked at all.
104
+ The report gates nothing. Deciding whether a sentence states a claim is a judgment no parser settles, so the output names candidates for a reader. This answers a different question from the verification anchors `standards/architecture.md` describes, which record that one cited number was re-read. That mechanism says whether a marked figure held, and this one says how much of the record could be checked at all.
105
105
 
106
106
  ## Which folders each check reaches
107
107
 
108
108
  The provenance, required-section, and narration checks cover `.claude/context/` alone, the reference-form check covers the split folders inside it, and length and the table finding reach every audited folder.
109
109
 
110
- What narrows the three is stated in `.claude/standards/context.md`, which opens its scope by handing diagrams and wireframes to `diagrams.md` and `wireframes.md`, and the sibling standards do not restate it. A marker reported in a diagram entry would cite a rule that entry's own standard routes elsewhere, and a diagram entry carries a heading per kind rather than a run of bullets deciding anything. The split is between kinds of rule rather than kinds of folder, and what decides it is which tier states the rule rather than what the check measures.
110
+ What narrows the three is stated in `standards/context.md`, which opens its scope by handing diagrams and wireframes to `diagrams.md` and `wireframes.md`, and the sibling standards do not restate it. A marker reported in a diagram entry would cite a rule that entry's own standard routes elsewhere, and a diagram entry carries a heading per kind rather than a run of bullets deciding anything. The split is between kinds of rule rather than kinds of folder, and what decides it is which tier states the rule rather than what the check measures.
111
111
 
112
112
  Length and the table finding generalize as judgments about how far a reader travels, so both reach wherever the audit is pointed. Required sections narrow for a plainer reason: the names are the context standard's own, and neither sibling standard states a required section at all.
113
113
 
@@ -76,7 +76,7 @@ standard exits 1 after listing the catalog on stderr.
76
76
  Two roots answer, in order: `standards/` at the working directory, then the
77
77
  corpus inside the package. A project that authors standards of its own uses the
78
78
  first, and this repository's own authoring root is the same path. `.claude/standards/`
79
- is not among them. It is this repository's generated mirror, and a copy an older
79
+ is not among them, and no repository writes one any more. A copy an older
80
80
  toolkit installed into a target resolves nothing.
81
81
 
82
82
  There is no citation closure to compute, since nothing is copied. A standard
@@ -78,4 +78,4 @@ Skills branch on the reason rather than on the exit code:
78
78
  aitk intake list toolkit-overview --unread --json | jq -r '.clusters[] | .cluster'
79
79
  ```
80
80
 
81
- For the folder layout, the item format, the answer contract, and retrieval, see `.claude/standards/intake.md`.
81
+ For the folder layout, the item format, the answer contract, and retrieval, see `standards/intake.md`.
@@ -41,7 +41,7 @@ Freezing the spellings gave up a property worth naming. They were derived by app
41
41
 
42
42
  A set shipped empty is reported rather than passed. It finds nothing and would exit clean, which reports a corpus nobody checked as a corpus carrying no violation, so the run names the empty set and exits `1`. The sets ship with the package, so a defect in the build is the only cause left.
43
43
 
44
- `aitk standards <name>` still resolves a standard under `.claude/standards/`, then the authoring root, then the package corpus, and prints it, so the human catalog reads without a project copy on disk.
44
+ `aitk standards <name>` still resolves a standard at the authoring root, then the package corpus, and prints it, so the human catalog reads without a project copy on disk.
45
45
 
46
46
  ## What each check reports
47
47
 
@@ -23,7 +23,7 @@ aitk records validate intake --json
23
23
 
24
24
  It reads and never writes, and the reason splits by kind. A session record is per-machine scratch with no history behind it, so a repair that guessed wrong could not be undone. A standard installs into every target and is cited by bare filename, so a rename the verb performed would reach further than the file it moved, which is why a finding naming one says so.
25
25
 
26
- `standards` reads the authoring root at `standards/` where it exists and the installed copy at `.claude/standards/` otherwise. The authoring root wins because the installed tree is generated from it in the toolkit, where a fix written to the copy is discarded by the next regen. A project that consumed the corpus holds only the second, so one precedence serves both. The walk stays flat, matching install and the catalog.
26
+ `standards` reads the authoring root at `standards/` where it exists and a copy at `.claude/standards/` otherwise. The authoring root wins because it is the only tree anyone authors in and the only one the resolver answers from, so a finding fixed anywhere else is fixed where nothing reads it. No repository generates the second candidate any more, which leaves it as a floor under a target holding a copy an older toolkit installed. The walk stays flat, matching the catalog.
27
27
 
28
28
  Nothing fires it automatically. The five record folders are gitignored, so the standards-audit hook exits early on them and any check reading changed files from git never lists one. The corpus is tracked and still unreached, since the markdown audit reads content across the files git lists and rules on no filename. The verb runs at the moment a session claims the record is finished, which is the same placement `aitk tasks validate` takes over the board.
29
29
 
@@ -70,7 +70,7 @@ Skills branch on the findings rather than on the exit code:
70
70
  aitk records validate plans --json | jq -r '.findings[] | "\(.kind): \(.subject)"'
71
71
  ```
72
72
 
73
- For the shapes each check enforces, see `.claude/standards/plan.md`, `.claude/standards/groundwork.md`, `.claude/standards/intake.md`, `.claude/standards/memory.md`, and `.claude/standards/standard.md`.
73
+ For the shapes each check enforces, see `standards/plan.md`, `standards/groundwork.md`, `standards/intake.md`, `standards/memory.md`, and `standards/standard.md`.
74
74
 
75
75
  ## Migrate
76
76
 
@@ -132,9 +132,9 @@ aitk records push --json
132
132
  aitk records pull
133
133
  ```
134
134
 
135
- The backed folders are `diagrams`, `groundwork`, `intake`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`, all under `.claude/`. Seven of them are the Claude ignore group the claude manifest ships, minus three entries: `.claude/.tmp`, which is deletable without loss, `.claude/worktrees/`, whose contents belong to the project repository already, and `.claude/.records.git/`, which is the history the rest are pushed into. `diagrams` and `proposals` are the two that group does not carry at all, since a target still tracks its own copies of both, which is why the list is spelled out rather than derived. Each name is a top-level record folder and every archive sits inside the one it archives, so the list stays at one entry per surface however many archives appear. It is a constant rather than configuration, and it deliberately does not match the six record kinds `validate` hardcodes.
135
+ The backed folders are `diagrams`, `groundwork`, `intake`, `memory`, `plans`, `proposals`, `review`, `tasks`, and `teach`, all under `.claude/`. Eight of them are the Claude ignore group the claude manifest ships, minus three entries: `.claude/.tmp`, which is deletable without loss, `.claude/worktrees/`, whose contents belong to the project repository already, and `.claude/.records.git/`, which is the history the rest are pushed into. `diagrams` is the one that group does not carry at all, since a target still tracks its own copies, which is why the list is spelled out rather than derived. Each name is a top-level record folder and every archive sits inside the one it archives, so the list stays at one entry per surface however many archives appear. It is a constant rather than configuration, and it deliberately does not match the six record kinds `validate` hardcodes.
136
136
 
137
- Records are gitignored by design, so the history lives in a second git directory at `.claude/.records.git` with `.claude/` as its work tree. Every path stays where it is, which is what a separate checkout could not do. The verbs stage the nine folders by explicit pathspec with `--force`, so nothing outside them can enter the index however the ignore rules read, and the project working tree and its index are never touched. Those pathspecs are relative to the process cwd, so run either verb from the project root rather than from a linked worktree, which sits inside the records work tree and makes every one of them resolve against the wrong directory.
137
+ Records are gitignored by design, so the history lives in a second git directory at `.claude/.records.git` with `.claude/` as its work tree. Every path stays where it is, which is what a separate checkout could not do. The verbs stage the nine folders by explicit pathspec with `--force`, so nothing outside them can enter the index however the ignore rules read, and the project working tree and its index are never touched. Each pathspec is a bare folder name and git reads it against the current directory rather than against the work tree the same call names, so the invocation carries `-C` at the work tree beside the other two flags. That is what lets either verb run from a linked worktree under `.claude/worktrees/`, which sits inside the records work tree and would otherwise prefix every name with its own path.
138
138
 
139
139
  ### Setup
140
140
 
@@ -146,7 +146,7 @@ git --git-dir=.claude/.records.git remote add origin <private-repo-url>
146
146
  printf '.claude/.records.git/\n' >> .gitignore
147
147
  ```
148
148
 
149
- The ignore line is part of the setup rather than part of the installed gitignore group, because the person running these commands is the one who creates the directory. Every path that group ships is written by a toolkit command without anyone asking, and this one is not, so a project that never sets up records would carry a rule for a directory it will never hold. Leaving it out and adding it here puts the rule where whoever creates the directory reads it.
149
+ The ignore line is repeated here rather than left to the install, because the person running these commands is the one who creates the directory and the rule is worth reading beside the command that needs it. The claude manifest ships `.claude/.records.git/` as the first entry of its group, so a project that ran `aitk claude sync` already carries it and this line is a no-op there. What the group buys is the project that never sets records up: it holds a rule for a directory it will never create, which costs nothing, and the backed-folder derivation above subtracts that entry from the group rather than from a list it does not sit in.
150
150
 
151
151
  Point it at a private repository, and at one that is not a remote of the project. Records carry the memory pen, the review reports, and the groundwork trails, so a public project publishes all of it to anyone who fetches all refs. `push` compares the configured origin against every remote of the project and refuses on a match. A read of that list which fails refuses as well, since an empty list clears the comparison for every origin and a gate that passes on its own failure is no gate.
152
152
 
@@ -28,6 +28,10 @@ Exit codes: `0` archived, `1` refused. Every gate is a refusal rather than a war
28
28
 
29
29
  `bad-input` covers a malformed command line, which all three task verbs answer the same way. It is separate from `ambiguous` and `no-match` because those describe the board, and a caller that passed two selectors would otherwise be sent to repair a task citation that is fine.
30
30
 
31
+ 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 it is a reference, such as a blocker naming what it waits on, and matching the line would drop the referring task's row too.
32
+
33
+ The row removal reaches `priority.md` alone. A task gets to a merge by being planned and handed out, and both steps move it onto the board first, so one archived straight off `backlog.md` leaves its bullet standing and `aitk tasks validate` reports that bullet as naming a file that is gone.
34
+
31
35
  The board is shared scratch at the main worktree root, so `--root` defaults to the first entry of `git worktree list` rather than the working directory. A linked worktree archives against the same board every other session reads.
32
36
 
33
37
  Skills branch on the reason rather than on the exit code:
@@ -47,6 +51,8 @@ aitk tasks archive --pull-request 673 --json | jq -r 'if .ok then .task else .re
47
51
 
48
52
  The record carries `location`, one of `unstated`, `live`, `archived`, or `outside`, and `citedBy`, the other live tasks whose `Plan:` line lands on the same file. Exit codes: `0` read, `1` refused with `no-board` or `no-match`.
49
53
 
54
+ The target resolves against `.claude/tasks/` and against the project root both, so `../plans/x.md` and `.claude/plans/x.md` land on the same file and one plan two tasks spelled differently counts once.
55
+
50
56
  `aitk tasks archive` gates on this same answer, so a caller wanting the count reads it here rather than scanning the board. The `claude-docs` plans sweep is the exception and still states the rule in its own body, because a plugin skill reaches a target on merge while the CLI reaches one on release, so a sweep calling a verb the installed `aitk` predates gets no record back and archives nothing.
51
57
 
52
58
  Branch on `reason` rather than on the exit code, which is the rule the archive section above already states and which this verb needs for a second reason. An operator's shell profile may wrap `aitk` in a function that runs the binary and then another command and takes the second status, which masks every non-zero exit rather than only an absent verb. The binary exits 1 for an unknown subcommand and 1 for an ordinary refusal alike, so the record is the only signal that survives the wrapper.
@@ -185,4 +191,4 @@ Skills branch on the findings rather than on the exit code:
185
191
  aitk tasks validate --json | jq -r '.findings[] | "\(.kind): \(.subject)"'
186
192
  ```
187
193
 
188
- For the board format, the `Pull request:` line, and the archive rules, see `.claude/standards/tasks.md`.
194
+ For the board format, the `Pull request:` line, and the archive rules, see `standards/tasks.md`.
@@ -89,7 +89,7 @@ A constraint naming a track in flight carries the same problem past the handoff,
89
89
 
90
90
  `.claude/plans/`, `.claude/review/`, and `.claude/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. A session inside a worktree reads them directly, since the file-editing tools refuse a main-root path but `Read` resolves normally. It writes a whole file through the shell and makes a change inside an existing file through an `aitk` verb, which resolves the main root in-process. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
91
91
 
92
- The plan's shape is fixed by `.claude/standards/plan.md`: the section list, the filename, the lifecycle, and the contract its questions keep. Every question carries a `- Suggested:` line and an empty `- Answer:` slot, and a blank answer accepts the suggestion at execution time. That default is what makes a plan decision-ready in one pass, and it is the opposite of the contract an intake folder keeps, where an empty slot means nobody reached the item.
92
+ The plan's shape is fixed by `standards/plan.md`: the section list, the filename, the lifecycle, and the contract its questions keep. Every question carries a `- Suggested:` line and an empty `- Answer:` slot, and a blank answer accepts the suggestion at execution time. That default is what makes a plan decision-ready in one pass, and it is the opposite of the contract an intake folder keeps, where an empty slot means nobody reached the item.
93
93
 
94
94
  An execution that picks other than the suggestion rewrites the `- Suggested:` line as `overridden at execution to <pick>,` followed by the measurement that moved it, and leaves the slot blank. That fixed phrase is how a reader of the archived plan tells an override from a suggestion the execution accepted, since an authored suggestion often carries a number of its own. The same deviation takes one line in the open task's `## Findings`, which is the register that survives the plan being archived. A deviation from a question somebody already answered goes back to whoever answered it instead, since a filled slot is a decision already made.
95
95
 
@@ -20,7 +20,7 @@ Claude Code reads both and writes the implementation. Works for CLI tools, inter
20
20
 
21
21
  ### Seed shape
22
22
 
23
- The toolkit seed in `tooling/claude/seeds/.claude/DESIGN.md` ships a token-table template with a starting set of roles, and `.claude/standards/design.md` carries the same tables under `## Template` with placeholder rows. The column headers are what the renderer parses, so they stay verbatim in either, while the rows and values are the project's own.
23
+ The toolkit seed in `tooling/claude/seeds/.claude/DESIGN.md` ships a token-table template with a starting set of roles, and `standards/design.md` carries the same tables under `## Template` with placeholder rows. The column headers are what the renderer parses, so they stay verbatim in either, while the rows and values are the project's own.
24
24
 
25
25
  The `aitk:claude-design-extract` skill drafts the file, sourcing tokens from a project's existing prose and CLI UI surfaces, or proposing them from `.claude/REQUIREMENTS.md` and a `## Personality` paragraph when no UI code exists yet. `aitk design render` writes an HTML plus CSS preview to `.claude/review/design/` for eyeballing the current system without leaving Claude Code. See `.claude/context/design.md`.
26
26
 
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  description: Enforce shape, scope, and success criterion for authoring convention files
3
3
  paths:
4
- - '.claude/standards/**/*.md'
5
4
  - 'standards/**/*.md'
6
5
  ---
7
6
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "3.37.0",
4
+ "version": "3.38.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+ set -o pipefail
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
7
+
8
+ GITIGNORE="$PROJECT_ROOT/.gitignore"
9
+ MANIFEST="$PROJECT_ROOT/tooling/claude/manifest.toml"
10
+ SECTION="# Claude"
11
+
12
+ # Divergences this repository has decided to keep, one `<entry>|<reason>` per
13
+ # line, matched after the trailing slash is stripped. Each names a path the
14
+ # enclosing repository ignores and the claude manifest deliberately does not
15
+ # ship, so a target keeps tracking it.
16
+ #
17
+ # This is the canonical list. `.gitignore` and the manifest carry a pointer
18
+ # beside their own entries rather than a second copy of the reason, and
19
+ # `.claude/context/tooling.md` carries the narrative behind both.
20
+ #
21
+ # A sanction that no longer describes a divergence fails the same as an
22
+ # unsanctioned one. An exception outliving its reason is what teaches the next
23
+ # reader to widen the list rather than to read it.
24
+ SANCTIONED=(
25
+ ".claude/diagrams|a target that tracks its diagrams keeps the context audit's default coverage of them, which ignoring the folder for every target would take away. This repository backs the folder through its records remote instead."
26
+ ".claude/README.md|the landing page a records pull writes back into a repository that set up a records remote. A target has not, so it never receives the file."
27
+ )
28
+
29
+ if [ ! -f "$GITIGNORE" ]; then
30
+ echo "No .gitignore at ${GITIGNORE#"$PROJECT_ROOT/"}, ignore parity unverifiable." >&2
31
+ exit 1
32
+ fi
33
+
34
+ if [ ! -f "$MANIFEST" ]; then
35
+ echo "No claude manifest at ${MANIFEST#"$PROJECT_ROOT/"}, ignore parity unverifiable." >&2
36
+ exit 1
37
+ fi
38
+
39
+ # A gitignore pattern and a manifest entry describe the same folder whether or
40
+ # not either spells the trailing slash, so presence is compared with it dropped.
41
+ # `.claude/.tmp` and `.claude/.tmp/` are the live instance of that pair.
42
+ normalize() {
43
+ printf '%s\n' "${1%/}"
44
+ }
45
+
46
+ # Every pattern the file carries, comments and blank lines dropped. The whole
47
+ # file rather than one header, because `.claude/teach/` sits under a header of
48
+ # its own and reading the `# Claude` header alone would report it missing from a
49
+ # list that carries it.
50
+ gitignore_patterns() {
51
+ awk '{ sub(/[[:space:]]+$/, "") } $0 ~ /^[[:space:]]*#/ || $0 == "" { next } { print }' "$GITIGNORE"
52
+ }
53
+
54
+ # Every entry of the named array in the manifest's `[gitignore]` table. The
55
+ # array is accumulated to its closing bracket before the quoted strings are read
56
+ # out, so a formatter wrapping it across lines does not silently empty this.
57
+ manifest_entries() {
58
+ awk -v key="\"$SECTION\"" '
59
+ /^\[/ { table = $0 }
60
+ table != "[gitignore]" { next }
61
+ index($0, key) == 1 { collecting = 1; buf = "" }
62
+ collecting { buf = buf $0 }
63
+ collecting && index($0, "]") > 0 {
64
+ collecting = 0
65
+ buf = substr(buf, index(buf, "["))
66
+ count = split(buf, parts, "\"")
67
+ for (i = 2; i <= count; i += 2) print parts[i]
68
+ }
69
+ ' "$MANIFEST"
70
+ }
71
+
72
+ contains() {
73
+ local needle=$1
74
+ shift
75
+ local candidate
76
+ for candidate in "$@"; do
77
+ [ "$candidate" = "$needle" ] || continue
78
+ return 0
79
+ done
80
+ return 1
81
+ }
82
+
83
+ ignored=()
84
+ while IFS= read -r pattern; do
85
+ [ -n "$pattern" ] || continue
86
+ ignored+=("$(normalize "$pattern")")
87
+ done < <(gitignore_patterns)
88
+
89
+ shipped=()
90
+ while IFS= read -r entry; do
91
+ [ -n "$entry" ] || continue
92
+ shipped+=("$(normalize "$entry")")
93
+ done < <(manifest_entries)
94
+
95
+ # An empty read on either side is a parse that failed rather than a repository
96
+ # ignoring nothing, and reporting parity off it would say the two lists agree
97
+ # having compared none of their entries.
98
+ if [ ${#ignored[@]} -eq 0 ]; then
99
+ echo "No patterns read from .gitignore, ignore parity unverifiable." >&2
100
+ exit 1
101
+ fi
102
+
103
+ if [ ${#shipped[@]} -eq 0 ]; then
104
+ echo "No entries read from the \"$SECTION\" array in ${MANIFEST#"$PROJECT_ROOT/"}, ignore parity unverifiable." >&2
105
+ exit 1
106
+ fi
107
+
108
+ failures=""
109
+ sanction_notes=""
110
+
111
+ # Entries the manifest ships that this repository does not ignore. A target is
112
+ # told to ignore a folder the toolkit itself tracks, which no decision sanctions,
113
+ # so this direction carries no exception list.
114
+ for entry in "${shipped[@]}"; do
115
+ contains "$entry" "${ignored[@]}" && continue
116
+ failures="$failures $entry is shipped by the manifest and absent from .gitignore"$'\n'
117
+ done
118
+
119
+ # Claude-scoped patterns this repository ignores that the manifest does not
120
+ # ship. Scoped to `.claude/` because the manifest is the claude stack and says
121
+ # nothing about `node_modules/` or `.env`.
122
+ for pattern in "${ignored[@]}"; do
123
+ case "$pattern" in
124
+ .claude/*) ;;
125
+ *) continue ;;
126
+ esac
127
+ contains "$pattern" "${shipped[@]}" && continue
128
+
129
+ matched=false
130
+ for sanction in "${SANCTIONED[@]}"; do
131
+ [ "${sanction%%|*}" = "$pattern" ] || continue
132
+ matched=true
133
+ sanction_notes="$sanction_notes $pattern stays out of the manifest: ${sanction#*|}"$'\n'
134
+ break
135
+ done
136
+
137
+ if [ "$matched" = false ]; then
138
+ failures="$failures $pattern is ignored here and absent from the manifest"$'\n'
139
+ fi
140
+ done
141
+
142
+ # A sanction naming a path that is no longer divergent, either because the
143
+ # manifest took it or because .gitignore dropped it.
144
+ for sanction in "${SANCTIONED[@]}"; do
145
+ entry="${sanction%%|*}"
146
+ if contains "$entry" "${ignored[@]}" && ! contains "$entry" "${shipped[@]}"; then
147
+ continue
148
+ fi
149
+ failures="$failures $entry is sanctioned as a divergence and is no longer one"$'\n'
150
+ done
151
+
152
+ if [ -n "$failures" ]; then
153
+ echo "The ignore set a target receives disagrees with this repository's own:" >&2
154
+ printf '%s' "$failures" >&2
155
+ echo "Add the entry to the \"$SECTION\" array in ${MANIFEST#"$PROJECT_ROOT/"} and to .gitignore, or record it in SANCTIONED in scripts/core/check-ignore-parity.sh with the reason it stays apart." >&2
156
+ exit 1
157
+ fi
158
+
159
+ if [ -n "$sanction_notes" ]; then
160
+ echo "Sanctioned divergences from the ignore set a target receives:"
161
+ printf '%s' "$sanction_notes"
162
+ fi
@@ -18,7 +18,6 @@ mirror_dir() {
18
18
  done < <(find "$src" -type f "${find_args[@]}" | sort)
19
19
  }
20
20
 
21
- mirror_dir "$PROJECT_ROOT/standards" "$PROJECT_ROOT/.claude/standards" -name "*.md" -not -path "*/bundled/*"
22
21
  mirror_dir "$PROJECT_ROOT/snippets" "$PROJECT_ROOT/.claude/snippets" -name "*.md"
23
22
 
24
23
  # `internal/` is the surface the plugin does not symlink. Mirrored on its own so
@@ -261,7 +261,7 @@ main() {
261
261
 
262
262
  log_step "Consumed copies"
263
263
  run_check "bash $PROJECT_ROOT/scripts/core/regen-claude-copies.sh" "Consumed-copy regen failed"
264
- assert_no_drift ".claude/standards .claude/snippets .claude/internal .claude/rules" "Consumed copies drifted. Run bun run check and commit .claude/standards, .claude/snippets, .claude/internal, and .claude/rules."
264
+ assert_no_drift ".claude/snippets .claude/internal .claude/rules" "Consumed copies drifted. Run bun run check and commit .claude/snippets, .claude/internal, and .claude/rules."
265
265
  log_info "Consumed copies clean"
266
266
 
267
267
  # Only the HTML is asserted. The PNG beside it is a chromium render whose bytes
@@ -278,6 +278,16 @@ main() {
278
278
  assert_no_drift "claude/skills/toolkit-cli/SKILL.md" "The overwrite contract drifted from what the stacks hold. Run bun run check and commit claude/skills/toolkit-cli/SKILL.md."
279
279
  log_info "Tooling paths clean"
280
280
 
281
+ # The claude manifest is the only route a target's ignore set travels, and it
282
+ # is hand-maintained beside this repository's own `.gitignore` with nothing
283
+ # comparing the two. A drift between them reaches every target on the next
284
+ # `aitk tooling sync` and surfaces to nobody, which is why this gates rather
285
+ # than reports. It is not an `assert_no_drift`: no generator produces either
286
+ # list, so there is nothing to regenerate and diff.
287
+ log_step "Ignore parity"
288
+ run_check "bash $PROJECT_ROOT/scripts/core/check-ignore-parity.sh" "The ignore set a target receives disagrees with this repository's own."
289
+ log_info "Ignore parity clean"
290
+
281
291
  log_step "Skill paths"
282
292
  run_check "bash $PROJECT_ROOT/scripts/core/check-skill-paths.sh" "Shipped skills reference a repo-local path."
283
293
  log_info "Skill paths clean"
@@ -570,7 +570,7 @@ export const AUDITS: readonly AuditSpec[] = [
570
570
  // `standards` is the one tracked kind here, so it takes none of the
571
571
  // per-machine default the other five inherit from their corpus. A target
572
572
  // reads standards through `aitk standards` rather than a copy in its own
573
- // tree, so carrying neither `standards/` nor `.claude/standards/` is the
573
+ // tree, so carrying no standards folder at all is the
574
574
  // ordinary state of every project but this repository.
575
575
  ...(kind === 'standards' && {
576
576
  absentReasons: ['no-folder'] satisfies RecordRefusal[],
@@ -5,11 +5,15 @@ import { INDEX_FILE, listIndexes } from '@/indexes/walk'
5
5
  /**
6
6
  * Folder names under `.claude/` audited by default.
7
7
  *
8
- * The index-plus-entry contract on its own over-collects. `.claude/standards/`
9
- * also carries a generated index beside entries with frontmatter, and auditing
10
- * it would measure the consumed copy of `standards/` against a rule written for
11
- * per-domain narrative. This list names the folders `standards/context.md`
12
- * governs, and `--folder` admits another without an edit here.
8
+ * A named list rather than the index-plus-entry contract read off disk, so a
9
+ * generated tree satisfying that contract is never measured against a rule
10
+ * written for per-domain narrative. This list names the folders
11
+ * `standards/context.md` governs, and `--folder` admits another without an edit
12
+ * here.
13
+ *
14
+ * It doubles as the citation check's scope, since `citationPattern` spells only
15
+ * these names. A `.claude/` folder left off the list is never resolved, so a
16
+ * path into one goes stale silently rather than failing a push.
13
17
  */
14
18
  export const DEFAULT_FOLDERS: readonly string[] = [
15
19
  'context',
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
2
+ import { join, resolve } from 'node:path'
3
3
  import { $ } from 'bun'
4
4
  import { gitEnv } from '@/git-env'
5
5
 
@@ -12,10 +12,12 @@ import { gitEnv } from '@/git-env'
12
12
  * list is spelled out rather than read off that group so adding an ignore entry
13
13
  * cannot silently enlarge the payload.
14
14
  *
15
- * `diagrams` and `proposals` are the two names the manifest group does not
16
- * carry, so a target tracks both where this repository ignores them. That is
17
- * the second reason to spell the list out: the manifest bounds it rather than
18
- * producing it, and no count taken off that group matches this one.
15
+ * `diagrams` is the one name the manifest group does not carry, so a target
16
+ * tracks it where this repository ignores it. That is the second reason to
17
+ * spell the list out: the manifest bounds it rather than producing it, and no
18
+ * count taken off that group matches this one. `scripts/core/check-ignore-parity.sh`
19
+ * compares the two lists and holds the reason `diagrams` stays apart, so the
20
+ * one difference is enforced rather than assumed.
19
21
  *
20
22
  * The manifest group is the one this reads rather than the enclosing
21
23
  * repository's own `.gitignore`, which spreads the same entries across two
@@ -124,19 +126,30 @@ interface GitResult {
124
126
  }
125
127
 
126
128
  /**
127
- * Runs one git command against the records history.
129
+ * Runs one git command against the records history, from the work tree.
128
130
  *
129
- * Both flags go on every call. `git --git-dir=<path> init` writes
131
+ * All three flags go on every call. `git --git-dir=<path> init` writes
130
132
  * `core.bare = true`, and an explicit `--work-tree` is what overrides it, so
131
- * dropping the flag on a single call reads the enclosing project as the tree
133
+ * dropping that flag on a single call reads the enclosing project as the tree
132
134
  * and stages everything in it.
135
+ *
136
+ * `-C` is what makes a bare pathspec like `groundwork` mean the work-tree root
137
+ * wherever the caller stands. Git derives a pathspec prefix from the current
138
+ * directory, so without it a caller sitting inside `.claude/`, which is every
139
+ * session in a linked worktree under `.claude/worktrees/<name>/`, prefixes
140
+ * each name with its own path and matches nothing. The root a caller names
141
+ * does not reach that prefix, so `--root` cannot stand in for this.
142
+ *
143
+ * Both paths are absolute because `-C` takes effect before the other two flags
144
+ * are read, so a relative root would otherwise send them looking inside the
145
+ * work tree.
133
146
  */
134
147
  async function records(root: string, args: string[]): Promise<GitResult> {
135
- const gitDir = join(root, RECORDS_GIT_DIR)
136
- const workTree = join(root, WORK_TREE)
148
+ const gitDir = resolve(root, RECORDS_GIT_DIR)
149
+ const workTree = resolve(root, WORK_TREE)
137
150
 
138
151
  const result =
139
- await $`git --git-dir=${gitDir} --work-tree=${workTree} ${args}`
152
+ await $`git -C ${workTree} --git-dir=${gitDir} --work-tree=${workTree} ${args}`
140
153
  .env(gitEnv())
141
154
  .quiet()
142
155
  .nothrow()
@@ -33,7 +33,8 @@ export interface ResolvedStandard {
33
33
 
34
34
  /**
35
35
  * The roots a standard resolves against. No toolkit standard installs into a
36
- * project any more, so `.claude/standards/` is gone from the list and a target
36
+ * project any more, so `.claude/standards/` is gone from the list, no repository
37
+ * generates one, and a target
37
38
  * carries neither entry below: the package corpus is the only root that
38
39
  * answers there, which is what leaves no precedence to reason about.
39
40
  *
@@ -13,7 +13,7 @@ const CLAUDE_DIR = '.claude'
13
13
 
14
14
  /**
15
15
  * A `CLAUDE.md` past this many lines has more in it than the always-load tier
16
- * is for. The figure is the checkpoint `.claude/standards/context.md` sets for
16
+ * is for. The figure is the checkpoint `standards/context.md` sets for
17
17
  * a context entry, raised because `CLAUDE.md` legitimately carries more than
18
18
  * one domain. It gates a proposal rather than a failure, so being approximate
19
19
  * costs a line an operator can ignore.
@@ -14,7 +14,7 @@ const ORDERING_FILE = 'priority.md'
14
14
  const BACKLOG_FILE = 'backlog.md'
15
15
 
16
16
  /**
17
- * The readiness headings `.claude/standards/tasks.md` fixes. The names are the
17
+ * The readiness headings `standards/tasks.md` fixes. The names are the
18
18
  * contract rather than a suggestion, so a board grouping under names of its own
19
19
  * reads as carrying no group at all and the run refuses instead of reporting a
20
20
  * clean board it never parsed.
@@ -42,11 +42,11 @@ An intake failing these is non-conforming even when it satisfies every shape rul
42
42
 
43
43
  ## Files
44
44
 
45
- | File | Holds | Required |
46
- | -------------------- | ------------------------------------------------------------ | -------- |
47
- | `00-overview.md` | Index: format block, cluster table, verdicts, open questions | Always |
48
- | `NN-<domain>.md` | One cluster of items, filed by the domain their fixes touch | Always |
49
- | `99-next-session.md` | What a compaction destroys that no cluster file carries | Optional |
45
+ | File | Holds | Required |
46
+ | -------------------- | ----------------------------------------------------------- | -------- |
47
+ | `00-overview.md` | Index: cluster table, verdicts, open questions | Always |
48
+ | `NN-<domain>.md` | One cluster of items, filed by the domain their fixes touch | Always |
49
+ | `99-next-session.md` | What a compaction destroys that no cluster file carries | Optional |
50
50
 
51
51
  `00` is the only reserved number. Everything else is read order, and the domain rides in the filename so a reader knows what `07-tooling.md` holds without opening it.
52
52
 
@@ -70,7 +70,6 @@ Date the folder once rather than every file. Twelve dated files leave eleven sta
70
70
 
71
71
  The index. It points at items and answers nothing itself.
72
72
 
73
- - The item format block, copied so a returning session picks the shape up from the folder
74
73
  - The answer contract stated out loud, since it inverts the plan file's
75
74
  - A cluster table of file, what it holds, item count, and open count
76
75
  - The verdict counts across the folder
@@ -168,10 +167,6 @@ date: <YYYY-MM-DD>
168
167
 
169
168
  <One line on what the dump covers and the commit it was measured against.>
170
169
 
171
- ## Item format
172
-
173
- <the item format block, copied so a returning session picks the shape up here>
174
-
175
170
  ## The answer contract
176
171
 
177
172
  `You:` ships empty and empty means unread. Accepting a verdict is typed as
@@ -9,7 +9,7 @@ Applies to each authored standard in the folder. Skip for `index.md`, which is g
9
9
 
10
10
  ## Overview
11
11
 
12
- A standard is a target-facing authoring convention for one document type, or for one attribute carried across every document. It installs into a project under `.claude/standards/` and is consumed by skills and developers alike. This file governs itself, so every rule below applies to it.
12
+ A standard is a target-facing authoring convention for one document type, or for one attribute carried across every document. Nothing installs it into a project, so a reader resolves it with `aitk standards <name>` or reads it at `standards/<name>.md`, and skills and developers consume it alike. This file governs itself, so every rule below applies to it.
13
13
 
14
14
  ## Scope
15
15
 
@@ -133,13 +133,7 @@ Add no fourth readiness group in place of this file. The three group names are t
133
133
 
134
134
  ## Validation
135
135
 
136
- `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 reached the trunk 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.
137
-
138
- A cited task is settled by being archived, or by closing every outcome and carrying a `Pull request:` line the trunk holds. The closed checkbox alone settles nothing, because the ship chain marks outcomes as its first step and opens the pull request several steps later, so a row read off the checkbox reports settled while the branch is still in review. A task that closed every outcome and names no pull request, and one whose pull request the run could not read against the trunk, are both reported as untested. Degrading either back to the checkbox would reproduce the defect under a name claiming it was fixed.
139
-
140
- The trunk is read as the clone already holds it, `origin/main` first and local `main` behind it, and no run fetches. A validate happens several times a sweep and a fetch per run is a cost this check does not carry, so a clone behind the remote under-reports rather than claiming work landed.
141
-
142
- 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.
136
+ `aitk tasks validate` reads the board against the tree, and what it checks, what it refuses on, and what it reports are at `docs/agents/tasks.md`. 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.
143
137
 
144
138
  ## Filenames
145
139
 
@@ -247,30 +241,14 @@ The line is what lets a merge close its own task. Every merge on `main` is a squ
247
241
 
248
242
  ## Archiving
249
243
 
250
- Never delete a task file. A shipped task moves to `.claude/tasks/archive/` under its own name, and the live index regenerates without it. `aitk tasks archive` owns the move, the ordering-row removal, and the index regen as one unit.
244
+ Never delete a task file. A shipped task moves to `.claude/tasks/archive/` under its own name, and the live index regenerates without it. `aitk tasks archive` owns that move, and what it does and what it refuses on are at `docs/agents/tasks.md`.
251
245
 
252
246
  The archive nests inside `.claude/tasks/` rather than sitting beside it as a flat `.claude/task-archive/`. Nesting is what lets a reader tell the two shapes apart on sight: the flat sibling is what a binary predating this convention still writes, so meeting one names an older checkout rather than a second archive to reconcile against this one.
253
247
 
254
- Two callers reach that command. The `claude-tasks` skill runs it inside a session, and the `post-merge` hook runs it unattended after a pull that merged the work. Both go through the command rather than moving the file themselves, so the two paths cannot drift into archiving differently. Every gate the command applies refuses with a non-zero exit rather than reporting, because a caller with nobody watching cannot act on a warning.
255
-
256
248
  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/`, sitting inside the folder it archives the same way, and it inherits the board's own ignore entry rather than needing one of its own. The cost is that an archived task does not appear in diffs, which is the cost the live board already carries.
257
249
 
258
- 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.
259
-
260
- 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.
261
-
262
- 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.
263
-
264
- Sweep the plan before archiving the task. The sweep finds its work by scanning the live folder, so a task archived first is beyond its reach for good, and the plan is left with no live task citing it and an archived task pointing at a path nothing will retarget. The archive refuses the last task pointing at a live plan for that reason, which puts the ordering under a gate rather than under a convention the unattended caller cannot follow.
265
-
266
- The gate counts the other live tasks citing the same plan rather than reading which folder the plan sits in. A plan several tasks share stays in the live folder by design, because the sweep is correct to leave a plan another live task still cites, so a gate reading the folder alone refuses every one of those tasks and the board and the sweep block each other with neither in the wrong. Counting the citations asks the question the gate means: a plan nothing else holds is one the sweep has yet to reach, and a plan a sibling still holds is one the sweep already decided about.
267
-
268
- The count resolves the target against `.claude/tasks/` and against the project root both, so `../plans/x.md` and `.claude/plans/x.md` land on the same file and one plan two tasks spelled differently counts once. `aitk tasks plan-citations` exposes that count for a caller that wants it, and the gate reads it.
269
-
270
- The `claude-docs` sweep states the rule rather than calling that verb, which is a duplication accepted with a reason rather than an oversight. A skill reaches a target the moment it merges and the CLI reaches one only when a release publishes, so a body calling a verb the installed `aitk` predates gets no record back and sweeps nothing. The two spellings therefore have to agree by hand until a release carries the verb, and the failure they guard against is a plan stranded by the form its citation was written in.
271
-
272
- A caller reads the outcome off the record's `reason` field and never off the exit code. An operator's shell profile may wrap `aitk` in a function that runs the binary and then another command, taking its status from the second, which masks an ordinary refusal exactly as it masks an absent verb.
250
+ 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, so the sweep runs before the archive rather than after it. The sweep finds its work by scanning the live folder, and a task archived first is beyond its reach for good, leaving the plan with no live task citing it and an archived task pointing at a path nothing will retarget.
273
251
 
274
- Surviving a shared plan is not the same as sanctioning one. `standards/plan.md` puts one concern in one plan file, so a plan serving several tasks is a shape to correct rather than to build on, and the gate only stops it from deadlocking the board.
252
+ The `claude-docs` sweep states that ordering in its own body rather than reading it back from a command, which is a duplication accepted with a reason rather than an oversight. A skill reaches a target the moment it merges and the CLI reaches one only when a release publishes, so a body calling a verb the installed `aitk` predates gets no record back and sweeps nothing. The two spellings therefore have to agree by hand until a release carries the verb, and the failure they guard against is a plan stranded by the form its citation was written in.
275
253
 
276
254
  A task with an open outcome stays on the board. Close it, or cut it from the task when the work is being abandoned, so what was dropped is recorded rather than inferred from an archived file. The sweep is gated on the same condition, so archiving around an open outcome also leaves the plan behind.
@@ -7,5 +7,8 @@ extends = ""
7
7
  runtime = ""
8
8
  scaffold = ""
9
9
 
10
+ # Checked against this repository's own `.gitignore` by
11
+ # scripts/core/check-ignore-parity.sh, which also holds the two `.claude/` paths
12
+ # this array deliberately omits and the reason each stays out.
10
13
  [gitignore]
11
- "# Claude" = [".claude/.records.git/", ".claude/.tmp/", ".claude/groundwork/", ".claude/intake/", ".claude/memory/", ".claude/plans/", ".claude/review/", ".claude/worktrees/", ".claude/tasks/", ".claude/teach/"]
14
+ "# Claude" = [".claude/.records.git/", ".claude/.tmp/", ".claude/groundwork/", ".claude/intake/", ".claude/memory/", ".claude/plans/", ".claude/proposals/", ".claude/review/", ".claude/worktrees/", ".claude/tasks/", ".claude/teach/"]
@@ -29,7 +29,7 @@ A project installed before the board became a folder still holds `.claude/TASKS.
29
29
 
30
30
  Convert by hand, once per project:
31
31
 
32
- 1. Split each `###` block into `.claude/tasks/vXX.Y-<slug>.md`, adding the `title` and `description` frontmatter that `.claude/standards/tasks.md` requires. Compare each new file against the block it came from rather than checking that every block landed somewhere.
32
+ 1. Split each `###` block into `.claude/tasks/vXX.Y-<slug>.md`, adding the `title` and `description` frontmatter the tasks standard requires, which `aitk standards tasks` prints. Compare each new file against the block it came from rather than checking that every block landed somewhere.
33
33
  2. Run `aitk indexes regen --no-stage --root . .claude/tasks/<any-task>.md` to build the catalog.
34
34
  3. Delete `.claude/TASKS.md`, and swap its `.gitignore` entry for `.claude/tasks/`.
35
35