@erclx/aitk 3.12.0 → 3.12.1
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 +12 -12
- package/claude/skills/claude-intake/REQUIREMENT.md +1 -0
- package/claude/skills/claude-intake/SKILL.md +7 -7
- package/docs/agents/install-and-sync.md +55 -28
- package/docs/agents/intake.md +3 -1
- package/docs/ai-workflow.md +2 -2
- package/docs/target-projects.md +7 -5
- package/governance/rules/claude/556-groundwork.md +5 -1
- package/governance/rules/claude/557-intake.md +5 -1
- package/package.json +1 -1
- package/src/commands/gov.ts +10 -1
- package/src/commands/sync.ts +18 -5
- package/src/demo/compile.ts +1 -1
- package/src/gov/adapter.ts +29 -0
- package/src/gov/install.ts +22 -1
- package/src/gov/stacks.ts +33 -1
- package/src/intake/folder.ts +49 -9
- package/src/sync/check.ts +53 -15
- package/src/sync/engine.ts +25 -0
- package/src/sync/stamp.ts +73 -24
- package/src/tooling/stamp.ts +1 -1
- package/standards/groundwork.md +10 -3
- package/standards/index.md +2 -2
- package/standards/intake.md +10 -3
|
@@ -14,6 +14,7 @@ Two failure modes cost more than the rest. A track that fans out to subagents re
|
|
|
14
14
|
## Must
|
|
15
15
|
|
|
16
16
|
- Detect open, resume, and close from the folder itself, matching the topic against tracks already there before deriving a slug
|
|
17
|
+
- Name a newly opened folder with a two-digit ordinal ahead of the slug, taken from the highest one already present across groundwork and intake
|
|
17
18
|
- Apply the qualifying test in open mode alone, so a track already measured is not refused by the test that admitted it
|
|
18
19
|
- Measure the current state now rather than carrying a figure from a previous session
|
|
19
20
|
- Carry a lean and the finding that would overturn it on every open question, or admit that a measurement is missing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-groundwork
|
|
3
|
-
description: Opens and runs a numbered groundwork folder under `.claude/groundwork/<slug>/` for a topic that has to be measured before it can be planned. Detects open, resume, and close from the folder itself. Use when asked to "research X", "dig into X", "work out what we should do about X", "measure this before we commit", or "open a groundwork folder". Do NOT use to write a feature plan or to implement. That is `claude-feature`.
|
|
3
|
+
description: Opens and runs a numbered groundwork folder under `.claude/groundwork/<nn>-<slug>/` for a topic that has to be measured before it can be planned. Detects open, resume, and close from the folder itself. Use when asked to "research X", "dig into X", "work out what we should do about X", "measure this before we commit", or "open a groundwork folder". Do NOT use to write a feature plan or to implement. That is `claude-feature`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Claude groundwork
|
|
@@ -19,10 +19,10 @@ Read `${CLAUDE_SKILL_DIR}/../../standards/groundwork.md` before writing any file
|
|
|
19
19
|
|
|
20
20
|
## Write scope
|
|
21
21
|
|
|
22
|
-
- Write only inside `.claude/groundwork/<slug>/`. A feature plan, source changes, a standard, a rule, and a reference doc all live outside that folder, so this one rule forbids every one of them.
|
|
22
|
+
- Write only inside `.claude/groundwork/<nn>-<slug>/`. A feature plan, source changes, a standard, a rule, and a reference doc all live outside that folder, so this one rule forbids every one of them.
|
|
23
23
|
- One exception, at close only: write one task file recording what the track concluded.
|
|
24
24
|
- A second exception, for what a spike reads: write an input under `.claude/.tmp/groundwork-fixtures/<slug>/`. Keep it out of `.claude/groundwork/` so mode detection never matches a fixture as a track. A fixture a headless run is pointed at goes outside the repository instead, per the rule in `## Running a spike`.
|
|
25
|
-
- What a spike produces stays inside the track rather than joining the two exceptions above: write evidence a spike file cites under `.claude/groundwork/<slug>/evidence/`, which the first rule already permits. Mode detection matches entries at the top level of the tracks directory, so the sibling the fixtures rule guards against cannot be a folder nested inside a track, and the scratch path holds only what can be deleted without loss, which a recording a finding rests on is not.
|
|
25
|
+
- What a spike produces stays inside the track rather than joining the two exceptions above: write evidence a spike file cites under `.claude/groundwork/<nn>-<slug>/evidence/`, which the first rule already permits. Mode detection matches entries at the top level of the tracks directory, so the sibling the fixtures rule guards against cannot be a folder nested inside a track, and the scratch path holds only what can be deleted without loss, which a recording a finding rests on is not.
|
|
26
26
|
- Reading is not restricted. External research is in scope, so read documentation, comparable projects, and papers whenever a live question needs them.
|
|
27
27
|
- Every claim about a source outside the project carries a link to it. A source found and not read is listed as a lead and is never cited.
|
|
28
28
|
- Treat the folder as gitignored and unbacked. It dies with the machine, so `07-next-session.md` repeats what it needs instead of pointing at its siblings.
|
|
@@ -41,11 +41,11 @@ Record method, result, measured cost, and caveats in `08-spikes.md`. Put whateve
|
|
|
41
41
|
|
|
42
42
|
## Step 1: detect the mode
|
|
43
43
|
|
|
44
|
-
List `.claude/groundwork/` from the project root and match the topic against the
|
|
44
|
+
List `.claude/groundwork/` from the project root and match the topic against the slug half of each `<nn>-<slug>` folder already there before deriving anything. A resume pass rarely phrases the topic the way the folder was named, so a fresh slug derived from the wording would miss a live track and restart it.
|
|
45
45
|
|
|
46
46
|
Never match against `.claude/` itself. That directory holds every other workflow surface, so a topic matched there lands on a folder that was never a track.
|
|
47
47
|
|
|
48
|
-
With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `ts-migration` over `migration-research`. Then route on `.claude/groundwork/<slug>/`:
|
|
48
|
+
With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `ts-migration` over `migration-research`. Also list `.claude/intake/` and take `<nn>` as the highest ordinal present across both listings, incremented, per `${CLAUDE_SKILL_DIR}/../../standards/groundwork.md`. Then route on `.claude/groundwork/<nn>-<slug>/`:
|
|
49
49
|
|
|
50
50
|
- Folder absent: open
|
|
51
51
|
- Folder present without `06-decision.md`: resume
|
|
@@ -72,7 +72,7 @@ The standard sets the open question format and requires it inside a topic file a
|
|
|
72
72
|
|
|
73
73
|
## Open mode
|
|
74
74
|
|
|
75
|
-
1. Create `.claude/groundwork/<slug
|
|
75
|
+
1. Create `.claude/groundwork/<nn>-<slug>/`, with `<nn>` and `<slug>` as derived in Step 1.
|
|
76
76
|
2. Write `README.md` first. Writing it first forces the question of what the track is for.
|
|
77
77
|
3. Write `01-current-state.md` by measuring now. Never carry a figure from a previous session or from recall without re-measuring it. Measure only what an open question in the folder needs. A number with no question attached is how groundwork turns into the work.
|
|
78
78
|
4. Write `00-scope.md` when the track is large enough to run away. Skip it on a small track.
|
|
@@ -103,12 +103,12 @@ Emit the full relative path from the project root for every file written or upda
|
|
|
103
103
|
Open and resume:
|
|
104
104
|
|
|
105
105
|
```plaintext
|
|
106
|
-
📂 Opened .claude/groundwork/<slug>/
|
|
106
|
+
📂 Opened .claude/groundwork/<nn>-<slug>/
|
|
107
107
|
|
|
108
108
|
**Written:**
|
|
109
109
|
|
|
110
|
-
- `.claude/groundwork/<slug>/README.md`
|
|
111
|
-
- `.claude/groundwork/<slug>/01-current-state.md`
|
|
110
|
+
- `.claude/groundwork/<nn>-<slug>/README.md`
|
|
111
|
+
- `.claude/groundwork/<nn>-<slug>/01-current-state.md`
|
|
112
112
|
|
|
113
113
|
**Open questions:**
|
|
114
114
|
|
|
@@ -122,12 +122,12 @@ Use `📂 Resumed` in place of `📂 Opened` on a resume pass.
|
|
|
122
122
|
Close:
|
|
123
123
|
|
|
124
124
|
```plaintext
|
|
125
|
-
✅ Closed .claude/groundwork/<slug>/
|
|
125
|
+
✅ Closed .claude/groundwork/<nn>-<slug>/
|
|
126
126
|
|
|
127
127
|
**Written:**
|
|
128
128
|
|
|
129
|
-
- `.claude/groundwork/<slug>/06-decision.md`
|
|
130
|
-
- `.claude/groundwork/<slug>/07-next-session.md`
|
|
129
|
+
- `.claude/groundwork/<nn>-<slug>/06-decision.md`
|
|
130
|
+
- `.claude/groundwork/<nn>-<slug>/07-next-session.md`
|
|
131
131
|
|
|
132
132
|
**Uncited external claims:** <count, or none>
|
|
133
133
|
|
|
@@ -22,6 +22,7 @@ A session with no numbering convention re-decides the folder shape per dump, so
|
|
|
22
22
|
- Close every item with a verdict, and pair every open question with a suggested pick
|
|
23
23
|
- Treat an empty operator slot as unread rather than as agreement
|
|
24
24
|
- Reserve the index number and carry the domain in every other filename, leaving the rest of the numbering as read order
|
|
25
|
+
- Name a newly opened folder with a two-digit ordinal ahead of the slug, taken from the highest one already present across intake and groundwork
|
|
25
26
|
- Keep answers on items alone, with the index pointing at them
|
|
26
27
|
- Name the heading and the act beside every path the pass wrote
|
|
27
28
|
- Confine writes to the intake folder
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-intake
|
|
3
|
-
description: Files a raw brain dump into a numbered intake folder under `.claude/intake/<slug>/`, one item per finding carrying a measured problem, a proposed fix, and a verdict. Use when asked to "file this dump", "triage my notes", "work through this list", "sort out this brain dump", or "run an intake pass". Do NOT use for one question that has to be measured before anyone can plan it. That is `claude-groundwork`.
|
|
3
|
+
description: Files a raw brain dump into a numbered intake folder under `.claude/intake/<nn>-<slug>/`, one item per finding carrying a measured problem, a proposed fix, and a verdict. Use when asked to "file this dump", "triage my notes", "work through this list", "sort out this brain dump", or "run an intake pass". Do NOT use for one question that has to be measured before anyone can plan it. That is `claude-groundwork`.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Claude intake
|
|
@@ -29,7 +29,7 @@ Using the wrong one fails in two shapes. Intake on a question that needs measuri
|
|
|
29
29
|
|
|
30
30
|
## Write scope
|
|
31
31
|
|
|
32
|
-
- Write only inside `.claude/intake/<slug>/`. A plan file, a task file, a source change, a standard, and a rule all live outside that folder, so this one rule forbids every one of them.
|
|
32
|
+
- Write only inside `.claude/intake/<nn>-<slug>/`. A plan file, a task file, a source change, a standard, and a rule all live outside that folder, so this one rule forbids every one of them.
|
|
33
33
|
- There is no exception. Promoting an item onto the board runs through `claude-tasks` after the operator has answered, which is a separate invocation.
|
|
34
34
|
- Reading is unrestricted inside the project. Measuring is the work.
|
|
35
35
|
- Treat the folder as gitignored and unbacked. No check reaches its contents, so every rule stated here holds only while a session reads it.
|
|
@@ -38,11 +38,11 @@ Nothing outside this body carries the write-scope floor, and no path-scoped rule
|
|
|
38
38
|
|
|
39
39
|
## Step 1: detect open or resume
|
|
40
40
|
|
|
41
|
-
List `.claude/intake/` from the project root and match the topic against the
|
|
41
|
+
List `.claude/intake/` from the project root and match the topic against the slug half of each `<nn>-<slug>` folder already there before deriving a slug. A second pass over the same subject rarely phrases the topic the way the folder was named, so a fresh slug would open a duplicate beside a live folder.
|
|
42
42
|
|
|
43
43
|
Never match against `.claude/` itself. That directory holds every other workflow surface, so a topic matched there lands on a folder that was never an intake.
|
|
44
44
|
|
|
45
|
-
With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `toolkit-overview` over `august-triage`. An absent folder opens, and a present one resumes by appending items and revising verdicts the tree has moved under.
|
|
45
|
+
With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `toolkit-overview` over `august-triage`. Also list `.claude/groundwork/` and take `<nn>` as the highest ordinal present across both listings, incremented, per `${CLAUDE_SKILL_DIR}/../../standards/intake.md`. An absent folder opens, and a present one resumes by appending items and revising verdicts the tree has moved under.
|
|
46
46
|
|
|
47
47
|
## Step 2: orient
|
|
48
48
|
|
|
@@ -90,12 +90,12 @@ Emit the full relative path from the project root for every file written, and na
|
|
|
90
90
|
A file the pass only read gets no line, which is what keeps the block short.
|
|
91
91
|
|
|
92
92
|
```plaintext
|
|
93
|
-
📂 Opened .claude/intake/<slug>/
|
|
93
|
+
📂 Opened .claude/intake/<nn>-<slug>/
|
|
94
94
|
|
|
95
95
|
**Filed:**
|
|
96
96
|
|
|
97
|
-
- `.claude/intake/<slug>/05-coverage.md` gains items 6 to 8 under a new `## What the merge gate covers`
|
|
98
|
-
- `.claude/intake/<slug>/00-overview.md` cluster rows and verdict counts updated
|
|
97
|
+
- `.claude/intake/<nn>-<slug>/05-coverage.md` gains items 6 to 8 under a new `## What the merge gate covers`
|
|
98
|
+
- `.claude/intake/<nn>-<slug>/00-overview.md` cluster rows and verdict counts updated
|
|
99
99
|
|
|
100
100
|
**Routing:** <N> plan-ready, <N> groundwork candidates, <N> already settled
|
|
101
101
|
|
|
@@ -16,6 +16,15 @@ is left alone regardless of its name, since that subfolder is project-authored
|
|
|
16
16
|
by location. It also removes a stale `.claude/GOV.md`
|
|
17
17
|
from the retired build. Use `aitk gov install` to add rules.
|
|
18
18
|
|
|
19
|
+
When the target's install recorded a stack, `aitk gov sync` also reports a
|
|
20
|
+
rule that stack lists and `.claude/rules/` does not hold, as a `missing` entry
|
|
21
|
+
carrying no change. This is what makes a target whose recorded sync point
|
|
22
|
+
postdates a rule joining its stack still see that rule: the report reads the
|
|
23
|
+
target's current entitlement against its current tree rather than diffing
|
|
24
|
+
from an anchor a later sync could advance past the rule's own commit. A
|
|
25
|
+
target whose install predates the recorded chain falls back to the same
|
|
26
|
+
band-inference `newRules` uses in `aitk sync --check`.
|
|
27
|
+
|
|
19
28
|
There is no `aitk standards sync` and no `aitk standards install`. The corpus
|
|
20
29
|
installs into no project, so the domain has nothing in a target to reconcile.
|
|
21
30
|
`aitk standards <name>` prints one, resolving `standards/` at the working root
|
|
@@ -94,16 +103,23 @@ It also refuses a target whose working tree is dirty, so commit or stash first.
|
|
|
94
103
|
and is safe to run at any time. Each file is classified as `stale` when it still
|
|
95
104
|
matches what the toolkit installed, `customized` when the project edited it,
|
|
96
105
|
`stranded` when it sits at a path the toolkit no longer installs to, `orphaned`
|
|
97
|
-
when the project authored it, or `drifted` when no stamp covers it.
|
|
106
|
+
when the project authored it, or `drifted` when no stamp covers it. Governance
|
|
107
|
+
also reports `missing`, for a rule the target's recorded stack lists that its
|
|
108
|
+
tree does not hold at all.
|
|
98
109
|
|
|
99
110
|
Use `--json` for the machine-readable report and `--exit-code` to fail a CI job. Orphaned
|
|
100
|
-
files are excluded from that exit code
|
|
101
|
-
converges
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
and missing files are both excluded from that exit code: a project-authored
|
|
112
|
+
rule never converges, and a sync that added a missing one silently changes
|
|
113
|
+
what the project is governed by, which stays a separate command an operator
|
|
114
|
+
chooses to run. Attribution reads `.claude/aitk/config.json`, which every
|
|
115
|
+
install and sync writes.
|
|
116
|
+
|
|
117
|
+
A target installed before stamping shipped has no such file, and neither does
|
|
118
|
+
that fallback do anything to migrate it. A target stamped before the file
|
|
119
|
+
moved into `.claude/aitk/` still carries it at the retired
|
|
120
|
+
`.claude/aitk.json`, and `aitk sync --check` reads that path when the current
|
|
121
|
+
one is absent, reporting it rather than moving it. Only a target carrying
|
|
122
|
+
neither path falls back to the toolkit's own git history.
|
|
107
123
|
Installed content matching any
|
|
108
124
|
version that history ever published proves the file is untouched, so it reports
|
|
109
125
|
`stale` naming the commit it came from, and content matching no published
|
|
@@ -207,10 +223,10 @@ file and proposes relocating the lot.
|
|
|
207
223
|
|
|
208
224
|
#### Rules the target never received
|
|
209
225
|
|
|
210
|
-
`newRules` names a rule the
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
226
|
+
`newRules` names a rule the target could receive and its tree does not hold. A
|
|
227
|
+
sync refreshes the files a target already holds and adds none, so without this
|
|
228
|
+
section a project's rule set freezes at its install date while every file it
|
|
229
|
+
does hold reports as current. That is the report's most confident wrong
|
|
214
230
|
answer, since a clean result reads as a target holding everything the toolkit
|
|
215
231
|
publishes.
|
|
216
232
|
|
|
@@ -225,11 +241,22 @@ installs, nothing counts toward `--exit-code`, and a target can read the list an
|
|
|
225
241
|
act on none of it. The value stops at an operator reading it, which is the same
|
|
226
242
|
contract the skills list already sets.
|
|
227
243
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
Since `aitk gov install` records the stack it resolved, a target carrying that
|
|
245
|
+
record answers this by comparing its current entitlement against its current
|
|
246
|
+
tree, with no anchor and no git diff involved. That is what lets the section
|
|
247
|
+
name a rule that shipped before the target's last sync: the anchor a sync
|
|
248
|
+
advances plays no part in the read, where an anchor-bound diff can never see a
|
|
249
|
+
rule on the far side of a window a later sync moved past it. The per-file
|
|
250
|
+
`missing` state `aitk gov sync` reports comes from the same comparison, so a
|
|
251
|
+
rule the chain lists reaches both surfaces the same way.
|
|
252
|
+
|
|
253
|
+
A target stamped before governance recorded a chain falls back to the older
|
|
254
|
+
band-inference read below. The measurement there anchors on governance's own
|
|
255
|
+
stamp rather than on the oldest anchor across domains, since rules are
|
|
256
|
+
domain-scoped and a shared anchor would let a snippets sync move the revision
|
|
257
|
+
rules are measured from. A target carrying no chain and no governance anchor
|
|
258
|
+
reports nothing at all: it has no date to measure against, and diffing from the
|
|
259
|
+
start of history would read the whole catalog as new.
|
|
233
260
|
|
|
234
261
|
An anchor this toolkit cannot resolve reports nothing by the same route, and that
|
|
235
262
|
one is not visible. A stamp naming a revision the running clone has never seen,
|
|
@@ -238,22 +265,22 @@ fails the read and yields an empty list rather than a stated absence. It looks
|
|
|
238
265
|
identical to a target holding every rule the toolkit publishes. `newSkills`
|
|
239
266
|
behaves the same way, and neither carries the `historyUnavailable` flag the
|
|
240
267
|
per-domain scan uses to separate the two. Treat an empty section on a toolkit
|
|
241
|
-
that is not a full clone as unmeasured rather than clean.
|
|
268
|
+
that is not a full clone as unmeasured rather than clean. This gap does not
|
|
269
|
+
reach the chain-based read above, since it consults no anchor at all.
|
|
242
270
|
|
|
243
|
-
|
|
244
|
-
stack takes the `core` and `claude` folders whole and
|
|
245
|
-
it with individually named rules, so an unfiltered
|
|
246
|
-
consumer about rules it can never receive and train the
|
|
247
|
-
section.
|
|
271
|
+
In the fallback, entitlement is filtered, because a stack does not receive
|
|
272
|
+
every rule. The base stack takes the `core` and `claude` folders whole and
|
|
273
|
+
every other stack extends it with individually named rules, so an unfiltered
|
|
274
|
+
list would tell a base consumer about rules it can never receive and train the
|
|
275
|
+
reader to skip the section.
|
|
248
276
|
|
|
249
277
|
The filter accepts a band on either of two grounds. A folder the base stack takes
|
|
250
278
|
whole is entitled to every target, read from the stack file so a folder added to
|
|
251
279
|
base later needs no code change. Every other band is read off the folders the
|
|
252
|
-
target already carries,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
under-reporting would cost the section its point.
|
|
280
|
+
target already carries, which is the fallback's only evidence of what a target
|
|
281
|
+
was entitled to before it recorded a chain. One band can be reached by more than
|
|
282
|
+
one stack, so the test over-reports inside a folder the target holds, which
|
|
283
|
+
costs a line where under-reporting would cost the section its point.
|
|
257
284
|
|
|
258
285
|
A rule the target already holds is dropped by name. That is what keeps a rule the
|
|
259
286
|
toolkit moved between band folders out of the list, since a rename reaches this
|
package/docs/agents/intake.md
CHANGED
|
@@ -9,6 +9,8 @@ description: Reading intake folder counts and items, the three read states an it
|
|
|
9
9
|
|
|
10
10
|
`aitk intake list` reports the intake folders under `.claude/intake/`, or the items one folder holds. It reads and never writes, because an answer belongs to the operator and a verb that filled one would decide what the folder exists to ask.
|
|
11
11
|
|
|
12
|
+
A folder carries a two-digit ordinal ahead of its slug, as in `21-toolkit-overview`, so a listing sorts by when each opened. A bare slug still resolves: passing `toolkit-overview` matches the one folder whose name is an ordinal ahead of it, and the folder's real name is what every command reports back.
|
|
13
|
+
|
|
12
14
|
```bash
|
|
13
15
|
aitk intake list
|
|
14
16
|
aitk intake list toolkit-overview --unread --json
|
|
@@ -58,7 +60,7 @@ One call writes one cluster. A call per selection is the alternative, and severa
|
|
|
58
60
|
|
|
59
61
|
An item already carrying an answer refuses rather than being overwritten, and one filled item refuses the whole batch, so a partly applied write never lands. Drop the named item and send the rest.
|
|
60
62
|
|
|
61
|
-
Exit codes: `0` every named item now carries its answer, `1` refused. The `reason` field carries `no-intake`, `no-folder`, `no-cluster`, `no-item`, `answered`, or `bad-input`.
|
|
63
|
+
Exit codes: `0` every named item now carries its answer, `1` refused. The `reason` field carries `no-intake`, `no-folder`, `ambiguous-slug`, `no-cluster`, `no-item`, `answered`, or `bad-input`. A bare slug matching more than one ordinal-prefixed folder refuses as `ambiguous-slug` rather than `no-folder`, naming every match in `detail`.
|
|
62
64
|
|
|
63
65
|
An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the record's `reason` rather than the exit when a skill consumes this, which matters most here because the verb writes.
|
|
64
66
|
|
package/docs/ai-workflow.md
CHANGED
|
@@ -44,8 +44,8 @@ One session works for most features. Prefer splitting across two sessions only w
|
|
|
44
44
|
|
|
45
45
|
Work in Claude Code directly. It reads `CLAUDE.md` automatically and has full file access, no pasting needed.
|
|
46
46
|
|
|
47
|
-
- When the input is a pile of findings rather than one feature, invoke `aitk:claude-intake` first. It files the dump into `.claude/intake/<slug>/`, one item per finding carrying a problem measured against the tree, a proposed fix, and a verdict, then names which items are plan-ready, which need measuring, and which are already settled.
|
|
48
|
-
- When the current state is unmeasured and more than one approach is live, invoke `aitk:claude-groundwork` first. It opens a track folder under `.claude/groundwork/<slug>/` and ends in a decision, which may be to do nothing. Skip it when the approach is already settled.
|
|
47
|
+
- When the input is a pile of findings rather than one feature, invoke `aitk:claude-intake` first. It files the dump into `.claude/intake/<nn>-<slug>/`, one item per finding carrying a problem measured against the tree, a proposed fix, and a verdict, then names which items are plan-ready, which need measuring, and which are already settled.
|
|
48
|
+
- When the current state is unmeasured and more than one approach is live, invoke `aitk:claude-groundwork` first. It opens a track folder under `.claude/groundwork/<nn>-<slug>/` and ends in a decision, which may be to do nothing. Skip it when the approach is already settled.
|
|
49
49
|
- Invoke `aitk:claude-feature` to scan for code-level conflicts and ambiguities, confirm approach before proceeding
|
|
50
50
|
- Implement the feature, then Claude Code runs the commands defined in `CLAUDE.md`, fixes failures, and iterates until all pass
|
|
51
51
|
- For UI changes, invoke `aitk:claude-ui-test` to generate and run Playwright e2e tests
|
package/docs/target-projects.md
CHANGED
|
@@ -124,7 +124,7 @@ The report opens by naming the binary running it. The installed version reads ag
|
|
|
124
124
|
|
|
125
125
|
A `stale` file still matches what the toolkit installed, so the update is mechanical. A `customized` file carries local edits, so taking the upstream version is a decision and `aitk:claude-seed-sync` is the tool for it. A `stranded` file sits where an older toolkit installed it and the toolkit has since moved, which is what `aitk:migration-standards` handles.
|
|
126
126
|
|
|
127
|
-
That attribution comes from `.claude/aitk/config.json`, a stamp every install and sync writes. Snippets and governance record a hash per installed file.
|
|
127
|
+
That attribution comes from `.claude/aitk/config.json`, a stamp every install and sync writes. A target stamped before that path shipped is read from the retired `.claude/aitk.json` instead, reported rather than migrated. Snippets and governance record a hash per installed file. Governance also records the stack `aitk gov install` was given, and tooling records the stack chain it resolved instead of any file hash, since its install runs no per-file walk to attribute.
|
|
128
128
|
|
|
129
129
|
Each domain holds its own toolkit commit, so syncing governance today does not move the revision snippets measures against, and each domain reports the upstream commits touching its own source path. Running any sync stamps that domain, and the report names the ones still unstamped.
|
|
130
130
|
|
|
@@ -136,13 +136,15 @@ That third one matters most on an older project. Before it existed, a target hol
|
|
|
136
136
|
|
|
137
137
|
#### Rules you never received
|
|
138
138
|
|
|
139
|
-
A sync refreshes the files you already hold and adds none, so your rule set is frozen at the date you installed governance while every file in it reports as current. `newRules` names
|
|
139
|
+
A sync refreshes the files you already hold and adds none, so your rule set is frozen at the date you installed governance while every file in it reports as current. `newRules` names a rule you could receive that your tree does not hold, which is the one section that reads your absence rather than your contents. `aitk gov sync` reports the same rules per file, marked `missing`, so you see them either way you check.
|
|
140
140
|
|
|
141
141
|
Take a clean section as reporting rather than as delivering. Nothing here installs, and it counts toward nothing, so pick the rules up with `aitk gov install <stack>` or take one with `--add <rule>`. That separation is deliberate: adopting a rule changes what your project is governed by, which is a choice a sync has no business making for you.
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
Since `aitk gov install` records the stack you gave it, the list is read by comparing that stack's current rules against what you hold right now, with no date involved at all. That is what lets it name a rule the toolkit shipped before you last synced: nothing here depends on when you installed. A target stamped before this recording shipped falls back to the older read below.
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
The fallback filters to what your stack can receive, read off the rule folders you already carry plus the folders the base stack takes whole. A rule under `lang/` or `ui/` belongs to some stacks and not others, so an unfiltered list would name rules you can never install. A target with neither a recorded stack nor a governance anchor reports nothing at all, since there is no date to measure against either.
|
|
146
|
+
|
|
147
|
+
In the fallback, an empty section is not proof either way when the toolkit running the check is not a full git clone. The read needs history to reach your anchor, and an install from the registry ships none, so the section goes quiet rather than saying it could not measure. Run the check from a clone before reading a clean result as a complete one. The stack-based read above is untouched by this, since it consults no history.
|
|
146
148
|
|
|
147
149
|
This also closes the case where a rule arrives citing a sibling you do not have. A sync refreshing a rule can land a version pointing at a file it never installs, and the section names the missing sibling rather than the broken citation, which is enough to act on.
|
|
148
150
|
|
|
@@ -177,7 +179,7 @@ Standards take no part in that run. Nothing installed them, so there is no copy
|
|
|
177
179
|
### Targeted
|
|
178
180
|
|
|
179
181
|
- Claude seed docs such as `CLAUDE.md` and `.claude/REQUIREMENTS.md`: invoke `aitk:claude-seed-sync`. The skill splits each file into a preamble (between the H1 and the first H2) plus one part per `##` section, then diffs part by part and proposes per-part edits. User customizations are preserved.
|
|
180
|
-
- Governance rules already installed: `aitk gov sync <path>` diffs and applies, and never adds new rules
|
|
182
|
+
- Governance rules already installed: `aitk gov sync <path>` diffs and applies, and never adds new rules. A rule your recorded stack lists reports as `missing` instead.
|
|
181
183
|
- Tooling configs and seeds: `aitk tooling <stack> <path>` overwrites golden configs and merges seeds
|
|
182
184
|
- Reference docs for a stack: `aitk tooling ref <stack> <path>`
|
|
183
185
|
- Index regeneration after markdown edits: `aitk indexes regen`
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Route .claude/groundwork/ edits to the groundwork standard for numbering, frontmatter, and required file contents
|
|
2
|
+
description: Route .claude/groundwork/ edits to the groundwork standard for ordinal naming, numbering, frontmatter, and required file contents
|
|
3
3
|
paths:
|
|
4
4
|
- '.claude/groundwork/**'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Groundwork standards
|
|
8
8
|
|
|
9
|
+
## Folder name
|
|
10
|
+
|
|
11
|
+
- Name a new track `.claude/groundwork/<nn>-<slug>/`, a two-digit zero-padded ordinal followed by a kebab-case slug. Take the ordinal from the highest one already present across both `.claude/groundwork/` and `.claude/intake/`, incremented, or `01` when neither folder holds an entry. Never renumber an existing folder.
|
|
12
|
+
|
|
9
13
|
## Measuring and closing
|
|
10
14
|
|
|
11
15
|
- Never carry a figure from an earlier session without re-measuring it. Mark an inference as an inference.
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Route .claude/intake/ edits to the intake standard for the item format, the answer contract, and
|
|
2
|
+
description: Route .claude/intake/ edits to the intake standard for the item format, the answer contract, and ordinal naming
|
|
3
3
|
paths:
|
|
4
4
|
- '.claude/intake/**'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Intake standards
|
|
8
8
|
|
|
9
|
+
## Folder name
|
|
10
|
+
|
|
11
|
+
- Name a new dump `.claude/intake/<nn>-<slug>/`, a two-digit zero-padded ordinal followed by a kebab-case slug. Take the ordinal from the highest one already present across both `.claude/intake/` and `.claude/groundwork/`, incremented, or `01` when neither folder holds an entry. Never renumber an existing folder.
|
|
12
|
+
|
|
9
13
|
## The answer contract
|
|
10
14
|
|
|
11
15
|
- Never fill a `You:` slot and never infer a disposition from an empty one. Empty means unread, never agreement.
|
package/package.json
CHANGED
package/src/commands/gov.ts
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type TestOrderReport,
|
|
36
36
|
} from '@/gov/test-order'
|
|
37
37
|
import { recordStamp, runDomainSync } from '@/sync/engine'
|
|
38
|
+
import { writeChainStamp } from '@/sync/stamp'
|
|
38
39
|
import { resolveTarget } from '@/target'
|
|
39
40
|
import {
|
|
40
41
|
intro,
|
|
@@ -757,7 +758,15 @@ async function runInstall(
|
|
|
757
758
|
|
|
758
759
|
logStep('Installing rules')
|
|
759
760
|
for (const rel of await installRules(found, resolved)) logAdd(rel)
|
|
760
|
-
|
|
761
|
+
|
|
762
|
+
const now = new Date()
|
|
763
|
+
await recordStamp(createGovAdapter(PROJECT_ROOT), resolved, now)
|
|
764
|
+
await writeChainStamp(
|
|
765
|
+
resolved,
|
|
766
|
+
{ domain: 'governance', toolkitRoot: PROJECT_ROOT },
|
|
767
|
+
[selected],
|
|
768
|
+
now,
|
|
769
|
+
)
|
|
761
770
|
|
|
762
771
|
const { GREEN, NC } = palette(process.stderr)
|
|
763
772
|
outro()
|
package/src/commands/sync.ts
CHANGED
|
@@ -99,6 +99,12 @@ function renderCheck(report: CheckReport): void {
|
|
|
99
99
|
|
|
100
100
|
renderSkew(report)
|
|
101
101
|
|
|
102
|
+
if (report.stampAtLegacyPath) {
|
|
103
|
+
logWarn(
|
|
104
|
+
'Stamp found at the retired .claude/aitk.json. Move it to .claude/aitk/config.json.',
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
102
108
|
if (!report.managed) {
|
|
103
109
|
logStep('Not a toolkit project')
|
|
104
110
|
logWarn('No .claude/ directory and no CLAUDE.md at the target.')
|
|
@@ -124,13 +130,20 @@ function renderCheck(report: CheckReport): void {
|
|
|
124
130
|
|
|
125
131
|
for (const entry of domain.entries) {
|
|
126
132
|
if (entry.state === 'matching' || entry.state === 'orphaned') continue
|
|
127
|
-
logWarn(`${entry.rel} (${entry.state})`)
|
|
133
|
+
logWarn(entry.notice ?? `${entry.rel} (${entry.state})`)
|
|
128
134
|
}
|
|
129
135
|
|
|
130
|
-
const { stale, customized, drifted, stranded, orphaned } =
|
|
136
|
+
const { stale, customized, drifted, stranded, orphaned, missing } =
|
|
137
|
+
domain.counts
|
|
131
138
|
if (stale + customized + drifted + stranded === 0) {
|
|
132
139
|
logInfo(orphaned === 0 ? 'up to date' : `up to date (${orphaned} local)`)
|
|
133
140
|
}
|
|
141
|
+
// A local file is not a deficiency and a missing rule is, so this never
|
|
142
|
+
// shares the up-to-date line's parenthetical. It prints on its own,
|
|
143
|
+
// regardless of whether anything above needs a sync.
|
|
144
|
+
if (missing > 0) {
|
|
145
|
+
logWarn(`${missing} listed by the stack, not installed`)
|
|
146
|
+
}
|
|
134
147
|
|
|
135
148
|
for (const commit of domain.upstream) {
|
|
136
149
|
logInfo(`${commit.sha} ${commit.subject}`)
|
|
@@ -168,10 +181,10 @@ function renderCheck(report: CheckReport): void {
|
|
|
168
181
|
if (report.newRules.length > 0) {
|
|
169
182
|
logStep('New rules, never installed')
|
|
170
183
|
for (const name of report.newRules) logWarn(name)
|
|
171
|
-
//
|
|
172
|
-
//
|
|
184
|
+
// `install` re-resolves the whole stack rather than adding one rule, so
|
|
185
|
+
// the remedy names both routes rather than assuming the reader wants all.
|
|
173
186
|
logInfo(
|
|
174
|
-
'Run `aitk gov install <stack
|
|
187
|
+
'Run `aitk gov install <stack>` to take the whole stack again, or `--add <rule>` to take one.',
|
|
175
188
|
)
|
|
176
189
|
}
|
|
177
190
|
|
package/src/demo/compile.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { Beat, Draft } from '@/demo/beats'
|
|
|
5
5
|
* separate artifacts on purpose: a beat carries no target, no wait condition,
|
|
6
6
|
* and no timing, and putting those four fields on every beat would destroy the
|
|
7
7
|
* property the draft was designed around. See
|
|
8
|
-
* `.claude/groundwork/demo-recorder/06-decision.md`.
|
|
8
|
+
* `.claude/groundwork/38-demo-recorder/06-decision.md`.
|
|
9
9
|
*
|
|
10
10
|
* A compiled plan is committed rather than scratch, because the timing below is
|
|
11
11
|
* a starting point the operator tunes and the draft cannot reproduce a tuned
|
package/src/gov/adapter.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { basename, join, relative, resolve } from 'node:path'
|
|
3
|
+
import { resolveMissingRules } from '@/gov/stacks'
|
|
3
4
|
import type { InstalledFile, RetiredSurface, SyncAdapter } from '@/sync/engine'
|
|
5
|
+
import { readStamp, stampedChain } from '@/sync/stamp'
|
|
4
6
|
|
|
5
7
|
const RETIRED_GOV_FILE = join('.claude', 'GOV.md')
|
|
6
8
|
|
|
@@ -54,11 +56,38 @@ export function createGovAdapter(root: string): SyncAdapter {
|
|
|
54
56
|
locateSource: (file: InstalledFile) =>
|
|
55
57
|
index.get(basename(file.path, '.md')),
|
|
56
58
|
collectRetired: (target: string) => collectRetiredGov(target),
|
|
59
|
+
collectMissing: (target: string) => collectMissingGov(root, target),
|
|
57
60
|
projectSubdir: 'project',
|
|
58
61
|
stamp: { domain: 'governance', toolkitRoot: root },
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Rules the target's recorded chain entitles it to and its tree does not
|
|
67
|
+
* hold. Reports as `notice` text through the same shape `collectRetired`
|
|
68
|
+
* already returns, since both are surfaces the file walk cannot see: one an
|
|
69
|
+
* absence to remove, this one an absence to add.
|
|
70
|
+
*/
|
|
71
|
+
function collectMissingGov(root: string, target: string): RetiredSurface[] {
|
|
72
|
+
const chain = stampedChain(readStamp(target), 'governance')
|
|
73
|
+
|
|
74
|
+
return resolveMissingRules(root, target, chain).map((source) => {
|
|
75
|
+
const dest = join(
|
|
76
|
+
target,
|
|
77
|
+
'.claude',
|
|
78
|
+
'rules',
|
|
79
|
+
source.subdir,
|
|
80
|
+
`${source.rule}.md`,
|
|
81
|
+
)
|
|
82
|
+
const rel = relative(target, dest)
|
|
83
|
+
return {
|
|
84
|
+
path: dest,
|
|
85
|
+
rel,
|
|
86
|
+
notice: `${rel} (listed by ${chain[0]}, not installed. Run aitk gov install ${chain[0]} to add it.)`,
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
62
91
|
function collectRetiredGov(target: string): RetiredSurface[] {
|
|
63
92
|
const path = join(target, RETIRED_GOV_FILE)
|
|
64
93
|
if (!existsSync(path)) return []
|
package/src/gov/install.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
|
-
import { dirname, join, relative } from 'node:path'
|
|
2
|
+
import { basename, dirname, join, relative } from 'node:path'
|
|
3
3
|
import { copyPreservingMode } from '@/copy'
|
|
4
4
|
|
|
5
5
|
export interface RuleSource {
|
|
@@ -21,6 +21,27 @@ export function installedRulesDir(target: string): string {
|
|
|
21
21
|
return join(target, '.claude', 'rules')
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Rule names a target already holds, read off the installed tree by basename
|
|
26
|
+
* rather than off a recorded stack, since a target may hold rules `--add`
|
|
27
|
+
* layered on that no stack lists.
|
|
28
|
+
*/
|
|
29
|
+
export function installedRuleNames(target: string): Set<string> {
|
|
30
|
+
const dir = installedRulesDir(target)
|
|
31
|
+
const names = new Set<string>()
|
|
32
|
+
if (!existsSync(dir)) return names
|
|
33
|
+
|
|
34
|
+
for (const rel of new Bun.Glob('**/*.md').scanSync({
|
|
35
|
+
cwd: dir,
|
|
36
|
+
onlyFiles: true,
|
|
37
|
+
dot: true,
|
|
38
|
+
})) {
|
|
39
|
+
names.add(basename(rel, '.md'))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return names
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
/**
|
|
25
46
|
* Mirrors `rule_subdir` in `scripts/lib/gov.sh`, which stays in bash for the
|
|
26
47
|
* sandbox loops. A rule sitting directly under `governance/rules/` has no
|
package/src/gov/stacks.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
2
2
|
import { basename, join } from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
installedRuleNames,
|
|
5
|
+
listRuleSourcePaths,
|
|
6
|
+
lookupRules,
|
|
7
|
+
type RuleSource,
|
|
8
|
+
rulesSourceDir,
|
|
9
|
+
} from '@/gov/install'
|
|
4
10
|
|
|
5
11
|
export interface GovStack {
|
|
6
12
|
readonly name: string
|
|
@@ -151,6 +157,32 @@ export function unreferencedRules(root: string): string[] {
|
|
|
151
157
|
.sort()
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Rules the target's recorded chain entitles it to that its installed tree
|
|
162
|
+
* does not hold. `resolveRules` already walks a stack's `extends` ancestors,
|
|
163
|
+
* so reading its leaf entry is enough; no second walk resolves the chain
|
|
164
|
+
* itself. A stack the toolkit no longer ships resolves to nothing rather than
|
|
165
|
+
* throwing, the same way `readNewRules`'s band fallback already treats it.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveMissingRules(
|
|
168
|
+
root: string,
|
|
169
|
+
target: string,
|
|
170
|
+
chain: readonly string[],
|
|
171
|
+
): readonly RuleSource[] {
|
|
172
|
+
const stack = chain[0]
|
|
173
|
+
if (stack === undefined) return []
|
|
174
|
+
|
|
175
|
+
const resolution = resolveRules(root, stack)
|
|
176
|
+
if (!resolution.ok) return []
|
|
177
|
+
|
|
178
|
+
const { found } = lookupRules(root, resolution.rules)
|
|
179
|
+
const held = installedRuleNames(target)
|
|
180
|
+
|
|
181
|
+
return found
|
|
182
|
+
.filter((source) => !held.has(source.rule))
|
|
183
|
+
.sort((left, right) => left.rule.localeCompare(right.rule))
|
|
184
|
+
}
|
|
185
|
+
|
|
154
186
|
/**
|
|
155
187
|
* Layers `--add` names on top of a resolved stack. The bash trimmed a single
|
|
156
188
|
* leading and trailing space per entry; trimming fully is the same result for
|
package/src/intake/folder.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { readdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
-
import { join, relative } from 'node:path'
|
|
3
|
+
import { basename, join, relative } from 'node:path'
|
|
4
4
|
import {
|
|
5
5
|
INDEX_FILE,
|
|
6
6
|
type IntakeItem,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
export const INTAKE_REFUSALS = [
|
|
14
14
|
'no-intake',
|
|
15
15
|
'no-folder',
|
|
16
|
+
'ambiguous-slug',
|
|
16
17
|
'no-cluster',
|
|
17
18
|
'no-item',
|
|
18
19
|
'answered',
|
|
@@ -110,6 +111,36 @@ async function listSlugs(dir: string): Promise<string[]> {
|
|
|
110
111
|
.sort()
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
type SlugMatch =
|
|
115
|
+
| { readonly kind: 'matched'; readonly name: string }
|
|
116
|
+
| { readonly kind: 'ambiguous'; readonly names: readonly string[] }
|
|
117
|
+
| { readonly kind: 'none' }
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A folder carries a `<nn>-<slug>` name, but a caller names the topic alone.
|
|
121
|
+
* An exact match wins first, since it is what a name with no ordinal, or one
|
|
122
|
+
* already copied in full from a listing, resolves against. Otherwise the one
|
|
123
|
+
* entry whose name is an ordinal ahead of the given slug wins, which is what
|
|
124
|
+
* lets a topic keep working as its folder's identity gains a prefix. Two or
|
|
125
|
+
* more such entries is a collision the caller needs told apart from a typo,
|
|
126
|
+
* not a folder silently picked or silently missing.
|
|
127
|
+
*/
|
|
128
|
+
function matchSlug(names: readonly string[], slug: string): SlugMatch {
|
|
129
|
+
if (names.includes(slug)) return { kind: 'matched', name: slug }
|
|
130
|
+
|
|
131
|
+
const suffixed = names.filter(
|
|
132
|
+
(name) => name === `${extractOrdinal(name)}-${slug}`,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if (suffixed.length === 1) return { kind: 'matched', name: suffixed[0] }
|
|
136
|
+
if (suffixed.length > 1) return { kind: 'ambiguous', names: suffixed }
|
|
137
|
+
return { kind: 'none' }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function extractOrdinal(name: string): string {
|
|
141
|
+
return /^\d{2,}-/.exec(name)?.[0].slice(0, -1) ?? ''
|
|
142
|
+
}
|
|
143
|
+
|
|
113
144
|
async function openFolder(
|
|
114
145
|
root: string,
|
|
115
146
|
slug: string,
|
|
@@ -120,17 +151,22 @@ async function openFolder(
|
|
|
120
151
|
return refuse('no-intake', `No intake at ${relative(root, dir)}.`)
|
|
121
152
|
}
|
|
122
153
|
|
|
123
|
-
const
|
|
154
|
+
const names = await listSlugs(dir)
|
|
155
|
+
const match = matchSlug(names, slug)
|
|
156
|
+
|
|
157
|
+
if (match.kind === 'none') {
|
|
158
|
+
return refuse('no-folder', `No intake folder named ${slug}.`, names)
|
|
159
|
+
}
|
|
124
160
|
|
|
125
|
-
if (
|
|
161
|
+
if (match.kind === 'ambiguous') {
|
|
126
162
|
return refuse(
|
|
127
|
-
'
|
|
128
|
-
`
|
|
129
|
-
|
|
163
|
+
'ambiguous-slug',
|
|
164
|
+
`More than one intake folder matches ${slug}.`,
|
|
165
|
+
match.names,
|
|
130
166
|
)
|
|
131
167
|
}
|
|
132
168
|
|
|
133
|
-
return
|
|
169
|
+
return join(dir, match.name)
|
|
134
170
|
}
|
|
135
171
|
|
|
136
172
|
/** Counts per folder, which is what a session picks a folder to work from. */
|
|
@@ -180,7 +216,11 @@ export async function readFolder(
|
|
|
180
216
|
const opened = await openFolder(root, slug)
|
|
181
217
|
if (typeof opened !== 'string') return opened
|
|
182
218
|
|
|
183
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
ok: true,
|
|
221
|
+
slug: basename(opened),
|
|
222
|
+
clusters: await readClusters(opened),
|
|
223
|
+
}
|
|
184
224
|
}
|
|
185
225
|
|
|
186
226
|
/**
|
|
@@ -272,7 +312,7 @@ export async function answerItems(
|
|
|
272
312
|
|
|
273
313
|
return {
|
|
274
314
|
ok: true,
|
|
275
|
-
slug,
|
|
315
|
+
slug: basename(opened),
|
|
276
316
|
cluster: name,
|
|
277
317
|
path,
|
|
278
318
|
answered: selections,
|
package/src/sync/check.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { basename, join, sep } from 'node:path'
|
|
|
3
3
|
import { execa } from 'execa'
|
|
4
4
|
import { gitEnv } from '@/git-env'
|
|
5
5
|
import { createGovAdapter, rulesSourceDir } from '@/gov/adapter'
|
|
6
|
-
import { loadGovStack } from '@/gov/stacks'
|
|
6
|
+
import { loadGovStack, resolveMissingRules, resolveRules } from '@/gov/stacks'
|
|
7
7
|
import { createSnippetsAdapter } from '@/snippets/adapter'
|
|
8
8
|
import { planSync, type ScanEntry, type SyncAdapter } from '@/sync/engine'
|
|
9
9
|
import {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from '@/sync/reverse'
|
|
20
20
|
import { buildSeedsReport, type SeedsReport } from '@/sync/seeds-report'
|
|
21
21
|
import {
|
|
22
|
+
isLegacyStamped,
|
|
22
23
|
readStamp,
|
|
23
24
|
type Stamp,
|
|
24
25
|
stampedChain,
|
|
@@ -71,6 +72,7 @@ export interface StateCounts {
|
|
|
71
72
|
readonly drifted: number
|
|
72
73
|
readonly orphaned: number
|
|
73
74
|
readonly stranded: number
|
|
75
|
+
readonly missing: number
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
export interface DomainReport {
|
|
@@ -139,6 +141,12 @@ const UNMEASURED_TOOLING: ToolingReport = {
|
|
|
139
141
|
|
|
140
142
|
export interface CheckReport {
|
|
141
143
|
readonly covers: readonly StampDomain[]
|
|
144
|
+
/**
|
|
145
|
+
* True when the stamp `readStamp` found sits at the retired
|
|
146
|
+
* `.claude/aitk.json` path rather than the current one. Read only: nothing
|
|
147
|
+
* in the check migrates a target's config as a side effect of reporting it.
|
|
148
|
+
*/
|
|
149
|
+
readonly stampAtLegacyPath: boolean
|
|
142
150
|
/** False when the target is not a toolkit project, so every section stays empty. */
|
|
143
151
|
readonly managed: boolean
|
|
144
152
|
readonly domains: readonly DomainReport[]
|
|
@@ -204,7 +212,7 @@ export function buildToolingReport(
|
|
|
204
212
|
target: string,
|
|
205
213
|
stamp: Stamp | undefined,
|
|
206
214
|
): ToolingReport {
|
|
207
|
-
const chain = stampedChain(stamp)
|
|
215
|
+
const chain = stampedChain(stamp, 'tooling')
|
|
208
216
|
const manifests = chain
|
|
209
217
|
.map((name) => loadManifest(toolkitRoot, name))
|
|
210
218
|
.filter((manifest) => manifest !== undefined)
|
|
@@ -270,6 +278,7 @@ export function countStates(entries: readonly ScanEntry[]): StateCounts {
|
|
|
270
278
|
drifted: count(entries, 'drifted'),
|
|
271
279
|
orphaned: count(entries, 'orphaned'),
|
|
272
280
|
stranded: count(entries, 'stranded'),
|
|
281
|
+
missing: count(entries, 'missing'),
|
|
273
282
|
}
|
|
274
283
|
}
|
|
275
284
|
|
|
@@ -293,6 +302,11 @@ export function countStates(entries: readonly ScanEntry[]): StateCounts {
|
|
|
293
302
|
* a file the project may own. `detectUnmigrated` already shipped that exact
|
|
294
303
|
* false positive once, failing a push with no action that cleared it, and a
|
|
295
304
|
* walk that reports `unattributed` by design would repeat it.
|
|
305
|
+
*
|
|
306
|
+
* `missing` is excluded on the same grounds `newRules` already reports on: a
|
|
307
|
+
* sync that adds a rule silently changes what a project is governed by, and
|
|
308
|
+
* nobody chose that, so gating CI on the count would pressure a target into
|
|
309
|
+
* adopting a rule nobody picked.
|
|
296
310
|
*/
|
|
297
311
|
export function hasDrift(report: CheckReport): boolean {
|
|
298
312
|
if (report.unmigrated.length > 0) return true
|
|
@@ -338,6 +352,7 @@ export async function buildCheckReport(
|
|
|
338
352
|
if (!managed) {
|
|
339
353
|
return {
|
|
340
354
|
covers: [],
|
|
355
|
+
stampAtLegacyPath: isLegacyStamped(target),
|
|
341
356
|
managed,
|
|
342
357
|
domains: [],
|
|
343
358
|
tooling: UNMEASURED_TOOLING,
|
|
@@ -353,6 +368,7 @@ export async function buildCheckReport(
|
|
|
353
368
|
|
|
354
369
|
return {
|
|
355
370
|
covers: stamp?.covers ?? [],
|
|
371
|
+
stampAtLegacyPath: isLegacyStamped(target),
|
|
356
372
|
managed,
|
|
357
373
|
domains,
|
|
358
374
|
tooling: buildToolingReport(toolkitRoot, target, stamp),
|
|
@@ -360,11 +376,7 @@ export async function buildCheckReport(
|
|
|
360
376
|
superseded: collectSuperseded(target),
|
|
361
377
|
unmigrated,
|
|
362
378
|
newSkills: await readNewSkills(toolkitRoot, anchors),
|
|
363
|
-
newRules: await readNewRules(
|
|
364
|
-
toolkitRoot,
|
|
365
|
-
target,
|
|
366
|
-
stampedCommit(stamp, 'governance'),
|
|
367
|
-
),
|
|
379
|
+
newRules: await readNewRules(toolkitRoot, target, stamp),
|
|
368
380
|
reverse: buildReverseReport(toolkitRoot, target),
|
|
369
381
|
skew: await skewRead,
|
|
370
382
|
}
|
|
@@ -529,14 +541,23 @@ export function baseBands(root: string): Set<string> {
|
|
|
529
541
|
}
|
|
530
542
|
|
|
531
543
|
/**
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
544
|
+
* A recorded chain answers this without the anchor at all: `resolveMissingRules`
|
|
545
|
+
* compares the entitled set against what the target holds right now, so a rule
|
|
546
|
+
* that shipped before the target's anchor is not a permanent blind spot the
|
|
547
|
+
* way the diff below leaves it. This is the primary path, and it is also what
|
|
548
|
+
* `collectMissing` reports per file through the domain scan, so a rule the
|
|
549
|
+
* chain names and the target lacks reaches both surfaces the same way.
|
|
550
|
+
*
|
|
551
|
+
* The diff-and-bands path stays as the fallback for a target stamped before
|
|
552
|
+
* governance recorded a chain. Rules are domain-scoped there too, so it
|
|
553
|
+
* measures from governance's own anchor rather than from the oldest anchor
|
|
554
|
+
* across domains the way `readNewSkills` does. A shared anchor would let a
|
|
555
|
+
* snippets sync move the revision rules are measured from and drop a rule out
|
|
556
|
+
* of the read.
|
|
536
557
|
*
|
|
537
|
-
* A target carrying no governance anchor reports nothing. It has
|
|
538
|
-
* measure against, and diffing from the beginning of history would
|
|
539
|
-
* rule the toolkit ships as new.
|
|
558
|
+
* A target carrying no chain and no governance anchor reports nothing. It has
|
|
559
|
+
* no date to measure against, and diffing from the beginning of history would
|
|
560
|
+
* read every rule the toolkit ships as new.
|
|
540
561
|
*
|
|
541
562
|
* An anchor this clone cannot resolve reports nothing by a different route and
|
|
542
563
|
* says so nowhere. `read` yields an empty string on a non-zero exit, so a stamp
|
|
@@ -544,12 +565,29 @@ export function baseBands(root: string): Set<string> {
|
|
|
544
565
|
* as a target holding everything. `readNewSkills` carries the same gap, and
|
|
545
566
|
* neither has the `historyUnavailable` flag the per-domain scan uses to tell an
|
|
546
567
|
* unmeasured result from a clean one.
|
|
568
|
+
*
|
|
569
|
+
* A recorded chain naming a stack the toolkit no longer ships falls through to
|
|
570
|
+
* the band-based path below rather than reporting the empty list an
|
|
571
|
+
* unresolved chain would otherwise produce. That empty list reads exactly
|
|
572
|
+
* like a target holding everything, which is the same failure this function
|
|
573
|
+
* exists to close, so a retired stack name is read the same as no chain at
|
|
574
|
+
* all instead of reintroducing it.
|
|
547
575
|
*/
|
|
548
576
|
export async function readNewRules(
|
|
549
577
|
root: string,
|
|
550
578
|
target: string,
|
|
551
|
-
|
|
579
|
+
stamp: Stamp | undefined,
|
|
552
580
|
): Promise<string[]> {
|
|
581
|
+
const chain = stampedChain(stamp, 'governance')
|
|
582
|
+
const stack = chain[0]
|
|
583
|
+
|
|
584
|
+
if (stack !== undefined && resolveRules(root, stack).ok) {
|
|
585
|
+
return resolveMissingRules(root, target, chain)
|
|
586
|
+
.map((source) => source.rule)
|
|
587
|
+
.sort()
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const since = stampedCommit(stamp, 'governance')
|
|
553
591
|
if (since === undefined) return []
|
|
554
592
|
|
|
555
593
|
const paths = await read(root, [
|
package/src/sync/engine.ts
CHANGED
|
@@ -61,6 +61,10 @@ export type SyncChange =
|
|
|
61
61
|
* separate because they need opposite treatment. A project-authored file is
|
|
62
62
|
* orphaned and stays that way forever. A stamped file the toolkit no longer
|
|
63
63
|
* installs to is stranded, which is a relocation waiting on a decision.
|
|
64
|
+
*
|
|
65
|
+
* `missing` is the one state the walk cannot produce on its own, since the
|
|
66
|
+
* walk only iterates files that exist. It comes from `collectMissing`
|
|
67
|
+
* instead, an adapter naming an entitled file the target does not hold.
|
|
64
68
|
*/
|
|
65
69
|
export type EntryState =
|
|
66
70
|
| 'matching'
|
|
@@ -69,12 +73,19 @@ export type EntryState =
|
|
|
69
73
|
| 'drifted'
|
|
70
74
|
| 'orphaned'
|
|
71
75
|
| 'stranded'
|
|
76
|
+
| 'missing'
|
|
72
77
|
|
|
73
78
|
export interface ScanEntry {
|
|
74
79
|
readonly state: EntryState
|
|
75
80
|
readonly rel: string
|
|
76
81
|
/** Toolkit revision this file's content came from, when history proved it. */
|
|
77
82
|
readonly since?: string
|
|
83
|
+
/**
|
|
84
|
+
* Overrides `report`'s generic text for this entry's state. `collectMissing`
|
|
85
|
+
* is the one producer: a stack name is only known to the adapter that
|
|
86
|
+
* resolved it, and the generic `missing` line cannot carry one.
|
|
87
|
+
*/
|
|
88
|
+
readonly notice?: string
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
export interface SyncPlan {
|
|
@@ -119,6 +130,12 @@ export interface SyncAdapter {
|
|
|
119
130
|
locateSource(file: InstalledFile): string | undefined
|
|
120
131
|
/** Surfaces the file walk cannot see, such as a retired doc to delete. */
|
|
121
132
|
collectRetired?(target: string): RetiredSurface[]
|
|
133
|
+
/**
|
|
134
|
+
* Entitled files the walk cannot see because they do not exist yet.
|
|
135
|
+
* Reported as `missing` and queued as no change, since installing one
|
|
136
|
+
* changes what the project is governed by and stays a separate command.
|
|
137
|
+
*/
|
|
138
|
+
collectMissing?(target: string): RetiredSurface[]
|
|
122
139
|
/** Dropped from the walk, so neither matching nor orphaned. */
|
|
123
140
|
isExcluded?(file: InstalledFile): boolean
|
|
124
141
|
/**
|
|
@@ -210,6 +227,10 @@ export function planSync(adapter: SyncAdapter, target: string): SyncPlan {
|
|
|
210
227
|
|
|
211
228
|
entries.push(...strandedByRelocation(target, hashes, walked))
|
|
212
229
|
|
|
230
|
+
for (const surface of adapter.collectMissing?.(target) ?? []) {
|
|
231
|
+
entries.push({ state: 'missing', rel: surface.rel, notice: surface.notice })
|
|
232
|
+
}
|
|
233
|
+
|
|
213
234
|
const retired = adapter.collectRetired?.(target) ?? []
|
|
214
235
|
for (const surface of retired) {
|
|
215
236
|
changes.push({ kind: 'delete', dest: surface.path, rel: surface.rel })
|
|
@@ -394,6 +415,10 @@ function report(adapter: SyncAdapter, plan: SyncPlan): void {
|
|
|
394
415
|
logWarn(`${entry.rel} (locally customized)`)
|
|
395
416
|
else if (entry.state === 'stranded')
|
|
396
417
|
logWarn(`${entry.rel} (installed here by an older toolkit, now moved)`)
|
|
418
|
+
else if (entry.state === 'missing')
|
|
419
|
+
logWarn(
|
|
420
|
+
entry.notice ?? `${entry.rel} (listed by the stack, not installed)`,
|
|
421
|
+
)
|
|
397
422
|
else logWarn(`${entry.rel} (not in toolkit source, skipping)`)
|
|
398
423
|
}
|
|
399
424
|
|
package/src/sync/stamp.ts
CHANGED
|
@@ -38,10 +38,12 @@ export interface DomainStamp {
|
|
|
38
38
|
readonly syncedAt: string
|
|
39
39
|
readonly files: DomainHashes
|
|
40
40
|
/**
|
|
41
|
-
* Stack names the install resolved
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* Stack names the install resolved. Tooling records the full ancestor chain,
|
|
42
|
+
* nearest stack first, because a stack that extends another cannot be
|
|
43
|
+
* reinstalled from its leaf alone, and a `--skip` run installs fewer layers
|
|
44
|
+
* than the leaf's own chain would reproduce. Governance records the single
|
|
45
|
+
* stack `aitk gov install` was given, since `resolveRules` walks its
|
|
46
|
+
* ancestors internally and a reader needs only the leaf to ask it again.
|
|
45
47
|
*/
|
|
46
48
|
readonly chain?: readonly string[]
|
|
47
49
|
}
|
|
@@ -56,6 +58,24 @@ export function stampPath(target: string): string {
|
|
|
56
58
|
return join(target, '.claude', 'aitk', 'config.json')
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Where `106115ba` moved the stamp from. No migration shipped with that move,
|
|
63
|
+
* so a target stamped before it still carries its config here, and `readStamp`
|
|
64
|
+
* falls back to this path when the current one is absent.
|
|
65
|
+
*/
|
|
66
|
+
export function legacyStampPath(target: string): string {
|
|
67
|
+
return join(target, '.claude', 'aitk.json')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether `readStamp` would resolve to the retired path, so a caller can
|
|
72
|
+
* report that a target's config still sits there. False when neither path
|
|
73
|
+
* exists, since there is nothing to migrate off of.
|
|
74
|
+
*/
|
|
75
|
+
export function isLegacyStamped(target: string): boolean {
|
|
76
|
+
return !existsSync(stampPath(target)) && existsSync(legacyStampPath(target))
|
|
77
|
+
}
|
|
78
|
+
|
|
59
79
|
export function hashContent(content: Buffer | string): string {
|
|
60
80
|
return `sha256:${createHash('sha256').update(content).digest('hex')}`
|
|
61
81
|
}
|
|
@@ -75,9 +95,22 @@ export function toStampKey(rel: string): string {
|
|
|
75
95
|
/**
|
|
76
96
|
* A missing or corrupt stamp reads as absent rather than failing, which is what
|
|
77
97
|
* keeps every unstamped target on the existing unattributed path.
|
|
98
|
+
*
|
|
99
|
+
* Falls back to the retired path only when the current one does not exist,
|
|
100
|
+
* read only: nothing here migrates a target's config as a side effect of a
|
|
101
|
+
* report. The check is existence rather than a successful parse, so a
|
|
102
|
+
* corrupt current stamp reads as absent rather than silently serving the
|
|
103
|
+
* retired one beside it. `isLegacyStamped` tests the same existence check,
|
|
104
|
+
* which is what keeps the two agreeing on which path a corrupt current file
|
|
105
|
+
* was read from.
|
|
78
106
|
*/
|
|
79
107
|
export function readStamp(target: string): Stamp | undefined {
|
|
80
|
-
|
|
108
|
+
return existsSync(stampPath(target))
|
|
109
|
+
? readStampFile(stampPath(target))
|
|
110
|
+
: readStampFile(legacyStampPath(target))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readStampFile(path: string): Stamp | undefined {
|
|
81
114
|
if (!existsSync(path)) return undefined
|
|
82
115
|
|
|
83
116
|
try {
|
|
@@ -105,17 +138,22 @@ export function stampedHashes(
|
|
|
105
138
|
}
|
|
106
139
|
|
|
107
140
|
/**
|
|
108
|
-
* The stack chain
|
|
109
|
-
* target predating
|
|
110
|
-
* unmeasured rather than as clean.
|
|
141
|
+
* The stack chain a domain's install last recorded. An empty result is the
|
|
142
|
+
* state every target predating that domain's chain recording sits in, and a
|
|
143
|
+
* reader treats it as unmeasured rather than as clean.
|
|
111
144
|
*/
|
|
112
|
-
export function stampedChain(
|
|
113
|
-
|
|
145
|
+
export function stampedChain(
|
|
146
|
+
stamp: Stamp | undefined,
|
|
147
|
+
domain: StampDomain,
|
|
148
|
+
): readonly string[] {
|
|
149
|
+
return stamp?.domains[domain]?.chain ?? []
|
|
114
150
|
}
|
|
115
151
|
|
|
116
152
|
/**
|
|
117
|
-
* Replaces one domain's
|
|
118
|
-
* install and sync independently but
|
|
153
|
+
* Replaces one domain's file hashes and leaves the others, including that
|
|
154
|
+
* domain's own chain, untouched. Domains install and sync independently but
|
|
155
|
+
* share the one file, and a chain an install recorded is a separate fact a
|
|
156
|
+
* later file-only sync must not erase.
|
|
119
157
|
*/
|
|
120
158
|
export async function writeStamp(
|
|
121
159
|
target: string,
|
|
@@ -127,37 +165,35 @@ export async function writeStamp(
|
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
/**
|
|
130
|
-
* Records
|
|
131
|
-
*
|
|
132
|
-
* the chain is
|
|
168
|
+
* Records the stack chain an install resolved and leaves that domain's file
|
|
169
|
+
* hashes untouched. Tooling calls this with no files ever recorded, since
|
|
170
|
+
* `src/tooling/` never runs the sync engine and the chain is its whole record.
|
|
171
|
+
* Governance calls it alongside `writeStamp`, since it records both.
|
|
133
172
|
*/
|
|
134
173
|
export async function writeChainStamp(
|
|
135
174
|
target: string,
|
|
136
|
-
|
|
175
|
+
source: StampSource,
|
|
137
176
|
chain: readonly string[],
|
|
138
177
|
now: Date,
|
|
139
178
|
): Promise<void> {
|
|
140
|
-
await putDomain(
|
|
141
|
-
target,
|
|
142
|
-
{ domain: 'tooling', toolkitRoot },
|
|
143
|
-
{ files: {}, chain: [...chain] },
|
|
144
|
-
now,
|
|
145
|
-
)
|
|
179
|
+
await putDomain(target, source, { chain: [...chain] }, now)
|
|
146
180
|
}
|
|
147
181
|
|
|
148
182
|
async function putDomain(
|
|
149
183
|
target: string,
|
|
150
184
|
source: StampSource,
|
|
151
|
-
payload: Pick<DomainStamp, 'files' | 'chain'
|
|
185
|
+
payload: Partial<Pick<DomainStamp, 'files' | 'chain'>>,
|
|
152
186
|
now: Date,
|
|
153
187
|
): Promise<void> {
|
|
154
188
|
const previous = readStamp(target)
|
|
189
|
+
const previousRecord = previous?.domains[source.domain]
|
|
155
190
|
const commit = await toolkitCommit(source.toolkitRoot)
|
|
156
191
|
|
|
157
192
|
const record: DomainStamp = {
|
|
158
193
|
...(commit === undefined ? {} : { commit }),
|
|
159
194
|
syncedAt: now.toISOString(),
|
|
160
|
-
|
|
195
|
+
files: payload.files ?? previousRecord?.files ?? {},
|
|
196
|
+
...resolveChainField(payload.chain, previousRecord?.chain),
|
|
161
197
|
}
|
|
162
198
|
|
|
163
199
|
const domains = sortDomains({
|
|
@@ -203,6 +239,19 @@ async function readCommit(root: string): Promise<string | undefined> {
|
|
|
203
239
|
: undefined
|
|
204
240
|
}
|
|
205
241
|
|
|
242
|
+
/**
|
|
243
|
+
* A write naming no chain keeps the domain's previous one rather than dropping
|
|
244
|
+
* it, since `writeStamp` and `writeChainStamp` each touch one half of a
|
|
245
|
+
* governance record and neither should erase what the other wrote.
|
|
246
|
+
*/
|
|
247
|
+
function resolveChainField(
|
|
248
|
+
chain: readonly string[] | undefined,
|
|
249
|
+
previous: readonly string[] | undefined,
|
|
250
|
+
): Pick<DomainStamp, 'chain'> {
|
|
251
|
+
const resolved = chain ?? previous
|
|
252
|
+
return resolved === undefined ? {} : { chain: resolved }
|
|
253
|
+
}
|
|
254
|
+
|
|
206
255
|
/** Deterministic key order keeps a re-sync diff empty and a merge conflict local. */
|
|
207
256
|
function sortKeys(hashes: DomainHashes): DomainHashes {
|
|
208
257
|
return Object.fromEntries(
|
package/src/tooling/stamp.ts
CHANGED
package/standards/groundwork.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Groundwork reference
|
|
3
|
-
description: Folder layout, reserved numbering, frontmatter and dating, required file contents, and conventions for a measurement track
|
|
3
|
+
description: Folder layout, ordinal naming, reserved numbering, frontmatter and dating, required file contents, and conventions for a measurement track
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Groundwork reference
|
|
7
7
|
|
|
8
|
-
Applies to a groundwork track at `.claude/groundwork/<slug>/`. A track measures one question that has to be settled before anyone can plan against it. The numbering is the table of contents, so a reader opens the folder and knows where to start and what follows without an index maintained inside each file.
|
|
8
|
+
Applies to a groundwork track at `.claude/groundwork/<nn>-<slug>/`. A track measures one question that has to be settled before anyone can plan against it. The numbering inside the folder is the table of contents, so a reader opens the folder and knows where to start and what follows without an index maintained inside each file.
|
|
9
9
|
|
|
10
10
|
The folder is gitignored and unbacked. No check reaches its contents and no history recovers a deleted one, so every rule here holds only while a session reads it, and the handoff file has to be self-contained.
|
|
11
11
|
|
|
12
12
|
## Scope
|
|
13
13
|
|
|
14
|
-
Governs a groundwork track under `.claude/groundwork/<slug>/`: folder layout, reserved numbering, frontmatter and dating, what each required file holds, and the conventions a track keeps.
|
|
14
|
+
Governs a groundwork track under `.claude/groundwork/<nn>-<slug>/`: folder layout, ordinal naming, reserved numbering, frontmatter and dating, what each required file holds, and the conventions a track keeps.
|
|
15
15
|
|
|
16
16
|
Does not govern:
|
|
17
17
|
|
|
@@ -22,6 +22,13 @@ Does not govern:
|
|
|
22
22
|
- Headings, punctuation, word choice, and file references: `markdown.md`
|
|
23
23
|
- When a project opens a track at all, and the procedure that runs one, which belong to the surface driving it
|
|
24
24
|
|
|
25
|
+
## Folder name
|
|
26
|
+
|
|
27
|
+
- Name the folder `<nn>-<slug>`, a two-digit zero-padded ordinal followed by a kebab-case slug.
|
|
28
|
+
- Take the ordinal from the highest one already present across both `.claude/groundwork/` and `.claude/intake/`, incremented. A listing then sorts by when each folder opened rather than alphabetically, and the count includes both kinds because the two share one creation-order line.
|
|
29
|
+
- With neither folder holding an entry, the first one opened takes `01`. Do not read this off the numbering inside a track, which starts at `00` on a large one and disagrees with intake's own first file.
|
|
30
|
+
- Never renumber an existing folder. The ordinal is the order it opened, and a later reader cites it by that name.
|
|
31
|
+
|
|
25
32
|
## What a working track looks like
|
|
26
33
|
|
|
27
34
|
A track works when a session that has never seen it re-enters from the folder alone and can answer each of these:
|
package/standards/index.md
CHANGED
|
@@ -14,8 +14,8 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
14
14
|
- [Design reference](design.md): Shape and content rules for .claude/DESIGN.md
|
|
15
15
|
- [Diagram reference](diagrams.md): Shape and content rules for .claude/diagrams/<kind>.md files
|
|
16
16
|
- [Glossary reference](glossary.md): Frontmatter, entry shape, ordering, and the rules deciding which terms a glossary carries
|
|
17
|
-
- [Groundwork reference](groundwork.md): Folder layout, reserved numbering, frontmatter and dating, required file contents, and conventions for a measurement track
|
|
18
|
-
- [Intake reference](intake.md): Folder layout, reserved index number, frontmatter and dating, the item template, the answer contract, and retrieval
|
|
17
|
+
- [Groundwork reference](groundwork.md): Folder layout, ordinal naming, reserved numbering, frontmatter and dating, required file contents, and conventions for a measurement track
|
|
18
|
+
- [Intake reference](intake.md): Folder layout, ordinal naming, reserved index number, frontmatter and dating, the item template, the answer contract, and retrieval
|
|
19
19
|
- [Issue reference](issue.md): GitHub issue title, labels, and body conventions
|
|
20
20
|
- [Markdown reference](markdown.md): Headings, paragraph and list structure, code spans, the date form, punctuation, emphasis, file references, banned words, and frontmatter wording
|
|
21
21
|
- [Memory reference](memory.md): Filename and type prefix, frontmatter, the body shape per type, links between entries, and the lifecycle from write to retire
|
package/standards/intake.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Intake reference
|
|
3
|
-
description: Folder layout, reserved index number, frontmatter and dating, the item template, the answer contract, and retrieval
|
|
3
|
+
description: Folder layout, ordinal naming, reserved index number, frontmatter and dating, the item template, the answer contract, and retrieval
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Intake reference
|
|
7
7
|
|
|
8
|
-
Applies to an intake folder at `.claude/intake/<slug>/`. One folder holds one dump, filed by domain, and every finding in it is an item carrying a measured problem, one proposed fix, and a verdict.
|
|
8
|
+
Applies to an intake folder at `.claude/intake/<nn>-<slug>/`. One folder holds one dump, filed by domain, and every finding in it is an item carrying a measured problem, one proposed fix, and a verdict.
|
|
9
9
|
|
|
10
10
|
The folder is gitignored and unbacked. No check reaches its contents, so the shape below survives only by being read.
|
|
11
11
|
|
|
12
12
|
## Scope
|
|
13
13
|
|
|
14
|
-
Governs an intake folder under `.claude/intake/<slug>/`: folder layout, the reserved index number, frontmatter and dating, the item format, the answer contract, and retrieval.
|
|
14
|
+
Governs an intake folder under `.claude/intake/<nn>-<slug>/`: folder layout, ordinal naming, the reserved index number, frontmatter and dating, the item format, the answer contract, and retrieval.
|
|
15
15
|
|
|
16
16
|
Does not govern:
|
|
17
17
|
|
|
@@ -22,6 +22,13 @@ Does not govern:
|
|
|
22
22
|
- Headings, punctuation, word choice, and file references: `markdown.md`
|
|
23
23
|
- Which findings belong in a dump at all, and the procedure that files one, which belong to the surface driving it
|
|
24
24
|
|
|
25
|
+
## Folder name
|
|
26
|
+
|
|
27
|
+
- Name the folder `<nn>-<slug>`, a two-digit zero-padded ordinal followed by a kebab-case slug. This is the folder's own ordinal, distinct from the `NN-<domain>.md` numbering a cluster file carries inside it.
|
|
28
|
+
- Take the ordinal from the highest one already present across both `.claude/intake/` and `.claude/groundwork/`, incremented. A listing then sorts by when each folder opened rather than alphabetically, and the count includes both kinds because the two share one creation-order line.
|
|
29
|
+
- With neither folder holding an entry, the first one opened takes `01`. Do not read this off the numbering inside a dump, which starts at `00` and disagrees with groundwork's own first required file.
|
|
30
|
+
- Never renumber an existing folder. The ordinal is the order it opened, and a later reader cites it by that name.
|
|
31
|
+
|
|
25
32
|
## What a working intake looks like
|
|
26
33
|
|
|
27
34
|
An intake works when a reader returning weeks later can act on it from the folder alone:
|