@erclx/aitk 3.45.0 → 3.47.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.45.0",
4
+ "version": "3.47.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -82,7 +82,9 @@ The session also records nothing of what it learns. Both other callers of memory
82
82
  - Check a candidate branch is unclaimed by an existing worktree or a live session before dispatching a background worker for it, since the measured failure this closes is a worker colliding with someone else's work already sitting in the row
83
83
  - Dispatch only a candidate whose file set is disjoint from every track in flight, compared at the file path rather than a folder above it, since a count knows nothing about what two workers write
84
84
  - Hold a candidate whose sets are disjoint when a stated reason serializes it, and write that reason on the hold, since disjointness is necessary and not sufficient
85
- - Name each self-dispatched worker with the `orchestrator-` prefix, since that is what separates one from an operator's own launch in a session listing
85
+ - Name each self-dispatched worker with the `worker-` prefix, since that is the role the name marks and no controlling session has ever carried it
86
+ - Pass `-n` explicitly on every self-dispatch, since a launch that omits it leaves the client to derive a name and the fallback is a fragment of the session's own identifier
87
+ - Carry the dispatcher's `sessionId` in the launch prompt, since a worker owed two messages has no property on the roster that resolves a controller and a name goes stale inside the window it builds in
86
88
  - Report each self-dispatch and the row it fired against loudly enough to follow, since a person no longer watches the launch step happen
87
89
  - Stop dispatching once `## Run now` is empty or every row in it reads claimed, rather than waking again to fire on a board nobody is clearing
88
90
 
@@ -54,15 +54,31 @@ Name `<model>` on the launch, and pick it against the task rather than copying w
54
54
  ## Dispatch
55
55
 
56
56
  ```bash
57
- claude --bg --model <model> -n "orchestrator-<slug>" "Run /aitk:claude-worktree <type>/<slug>, then /aitk:claude-autoship <plan>"
57
+ claude --bg --model <model> -n "worker-<slug>" "Run /aitk:claude-worktree <type>/<slug>, then /aitk:claude-autoship <plan>. Your controller is the session whose sessionId is <dispatcher-id>. Resolve its current name from that id at the moment you send, and never resolve an addressee by name prefix. Message it when the pull request opens, carrying the number, the branch, the head sha, the CI state, and every point you departed from the plan on, and message it again if you stop on a question."
58
58
  ```
59
59
 
60
- `--bg, --background` starts the session as a background agent and returns immediately, `-n, --name` sets the display name that tells a self-dispatched worker from an operator's own launch in `aitk sessions list`, and `--model` overrides the inheritance the section above measured. Keep the `orchestrator-` prefix. It outlives the count it was introduced for, since the roster read still needs a way to separate the two kinds of launch.
60
+ `--bg, --background` starts the session as a background agent and returns immediately, `-n, --name` sets the display name that tells a self-dispatched worker from an operator's own launch in `aitk sessions list`, and `--model` overrides the inheritance the section above measured. Pass `-n` on every dispatch rather than letting the client derive one. A launch that omits it leaves the session named for a fragment of its own identifier, which is both its address on the send channel and the whole of what the operator sees for it in agent view.
61
+
62
+ The prefix reads `worker-` because that is the role it marks. It read `orchestrator-` until 2026-08-31, and no controlling session ever carried it, so a worker filtering the roster for that string found a sibling or itself on every row. Nothing matches the prefix programmatically, which is what kept the rename down to three strings.
63
+
64
+ Read `<dispatcher-id>` with `aitk sessions list --self --json` and interpolate the `sessionId` that row carries. Carry the id rather than the name. A name is derived from whatever the session turned out to be doing, and across the 181 records stamping both fields, nine were renamed after launch at a median of 5.4 minutes and a maximum of 509. Three landed more than ten minutes in, which is inside the window a worker announces its pull request in, so a name written into the prompt is aimed at a send that happens after it goes stale.
65
+
66
+ Where the installed CLI answers `--self` with an unknown option, that flag is newer than the release the target holds. Read the `sessionId` from the record the client writes for this session under its configuration directory, and say which route answered so the reader knows whether the id was read or inferred.
61
67
 
62
68
  The worktree call comes first and carries the branch as its argument, which is tier 0 of `claude-worktree` Step 2 and the only tier a caller can reach. `claude-autoship` Step 0 then finds the session already in a linked worktree and continues, which is a path it already documents. The autoship call carries `<plan>`, the same file this runbook already read to derive the branch, so its Step 1 takes it as the caller-supplied plan rather than re-deriving one from the slug the worker's branch happens to carry.
63
69
 
64
70
  Naming the branch in prose instead was tried and closes nothing, because no tier of that ladder reads the prompt. A worker launched onto `main` cannot match tier 1, a board carrying more than one plan puts tier 2 out of reach, and tier 3 tells it to ask a person who is not there. Four workers took the right branch that way, by inference rather than by contract, which is the same judgment both live disagreements came from.
65
71
 
72
+ ### What the brief may carry
73
+
74
+ The prompt carries pointers and standing context, and stops there. The branch and the plan stay arguments, because a skill resolves an argument through a documented ladder and reads no prose at all. What the prose reaches is the worker's judgment, so it holds only what a session has to weigh:
75
+
76
+ - Name the addressee and what it is owed, which the two message clauses above already do.
77
+ - Carry standing context this session holds that a cold one cannot derive, such as a constraint settled in conversation that never reached the plan.
78
+ - Leave out anything scope-shaped. A file list, a naming convention, or a check to run belongs in the plan, where the review reads it back against the diff.
79
+
80
+ The last bullet is the one under pressure, since the dispatch that first proved unattended work possible sent a prompt naming the task file, the likely files, the conventions, and the check to run. It shipped in 874 seconds and touched four files its task never named. Scope that arrives as prose is scope nothing verifies afterward.
81
+
66
82
  Report the dispatch as loudly as the human-launch line it replaces: name the branch, the model, the task, and the session name, so a person reading the transcript can follow what fired without watching it happen.
67
83
 
68
84
  ## Fall back to the human
@@ -13,6 +13,8 @@ It also produces one output where two are needed. A page written to be worked th
13
13
 
14
14
  A session that does record something records the wrong thing. It writes what was taught rather than what the learner retrieved, and a tally of errors carries none of the misconception a later session would work against.
15
15
 
16
+ The lesson then reaches nobody. It is a page carrying a stylesheet and a script, and the only thing a session hands over is a file path, which an editor preview opens with neither. The learner reads unstyled markup and takes it for the lesson, or opens nothing at all, and either way the session reports the lesson as delivered. A path is also the wrong unit once a workspace holds several pages, since the learner wants the one they are on rather than the folder it sits in.
17
+
16
18
  The durable half then has nowhere to go. A reference page and a glossary carry no learner and are ordinary reference prose, so they belong wherever the project already keeps prose on that subject, and a workspace holding them is a gitignored folder one person reads. A session moving them by hand picks a destination from the reader's activity rather than from who owns the subject, drops a page into a corpus without the source line that corpus requires, and has nothing stopping it from carrying a lesson across.
17
19
 
18
20
  Two failures land specifically on where the folder sits. A workspace resolved against the current directory forks into a copy per linked worktree, so the learning records split and no session sees the whole history. A body naming only the destination path reports success and loses the write, because the file-editing tools refuse a main-root path from a linked worktree and offer a worktree copy instead.
@@ -28,6 +30,7 @@ Two failures land specifically on where the folder sits. A workspace resolved ag
28
30
  - Split the output by lifetime, sending the worked-through half to lessons and the looked-up half to reference pages in the format the authoring gates read
29
31
  - Record the wrong answer itself rather than the count, since that is what the next session places the learner from
30
32
  - Report progress against the mission's success lines, so a mission can be called finished
33
+ - Hand the learner a link that opens the workspace in a real browser on every run, reading it back from the verb that served it rather than composing one, since the port a preview lands on is not the port it asked for
31
34
  - Propose a destination for each durable page by who owns its subject, and wait for the operator rather than routing on the session's own reading
32
35
  - Hand each confirmed page off through a named file of its own, so one skill owns the durable writes and no other producer's unread work is discarded with it
33
36
  - Name what the destination expects that the page does not carry yet, since the workspace runs none of the gates the destination does
@@ -113,6 +113,18 @@ aitk teach glossary <topic> --json \
113
113
 
114
114
  Follow `${CLAUDE_SKILL_DIR}/references/lesson-craft.md` for what makes a lesson worth returning to. Keep every quiz answer the same length, so formatting leaks no clue about which one is correct.
115
115
 
116
+ ### Hand over a link, never a path
117
+
118
+ A lesson is a page carrying a stylesheet and a script, and an editor preview opens it with neither, so a path alone delivers unstyled markup that reads as the lesson. Serve the teach root and give the learner a link they can click:
119
+
120
+ ```bash
121
+ aitk serve .claude/teach --entry <nn>-<topic>/index.html --json
122
+ ```
123
+
124
+ Start it in the background so the session keeps going, and read `url` off the record rather than composing one. The verb walks past a port already in use, so the port it took is exactly the half a guessed URL gets wrong. Report the refusal and its `reason` when `ok` is false, and report it rather than proceeding silently when the verb does not resolve at all, which is an installed CLI predating it.
125
+
126
+ Do this on every run that opens or resumes a workspace, including one that writes no lesson, since the learner's route into what is already there is the same link.
127
+
116
128
  ## Step 5: record what happened
117
129
 
118
130
  Write `learning-records/<nnnn>-<slug>.md` before the session ends, carrying the lessons covered, what the learner retrieved unaided, what they got wrong with the wrong answer itself, and what to revisit.
@@ -165,9 +177,14 @@ Lesson: .claude/teach/<nn>-<topic>/lessons/<nnnn>-<slug>.html
165
177
  Reference: .claude/teach/<nn>-<topic>/reference/<slug>.md
166
178
  Record: .claude/teach/<nn>-<topic>/learning-records/<nnnn>-<slug>.md
167
179
  Progress: <n> of <m> success lines met
180
+ Open: [<the url the serve verb reported>](<the same url>)
168
181
  ```
169
182
 
170
- Omit the reference line where the lesson produced no durable page. Emit every path from the project root, in the form the project's instruction file sets.
183
+ Omit the reference line where the lesson produced no durable page. The open line is the one line that is never omitted, since it is the only route the learner has into the page, and it carries what `aitk serve` reported rather than a URL composed here. Where the verb refused, that line names the refusal instead of a link.
184
+
185
+ Write that line as a markdown link carrying the URL as both its text and its target, rather than as a bare URL and never inside backticks. A code span renders as text the reader has to select and copy, which is the one thing the line exists to save them, and the path rule the project states governs a file path rather than a URL.
186
+
187
+ Emit every path from the project root, in the form the project's instruction file sets.
171
188
 
172
189
  A promotion pass reports its own shape instead, one line per page the operator confirmed and one naming the handoff:
173
190
 
@@ -41,10 +41,47 @@ nothing else.
41
41
  - Send a block out as a message before it becomes an interactive prompt. A session already waiting on input never reaches the tool round that drains an inbound message, so a relayed answer arrives under the open question and changes nothing.
42
42
  - Send nothing on progress. A worker reporting progress rebuilds, on this side of the channel, the poll the announcement retired on the other.
43
43
 
44
- Address the session the launch named. Fall back to `aitk sessions list --json`
45
- and the `orchestrator-` prefix when the launch named nobody, which is what an
46
- operator's own launch looks like, and say the addressee was inferred so the
47
- reader can correct it.
44
+ Address the session the launch named. It names a `sessionId` rather than a name,
45
+ so read `aitk sessions list --json`, find the row carrying that id, and send to
46
+ the `name` on it. Resolve that name at the moment of sending rather than at
47
+ launch, since a name is derived from what a session turned out to be doing and
48
+ has gone stale inside the hour that a build takes.
49
+
50
+ Check that name against the agent listing before sending it. A name is not
51
+ unique, and the roster carries no field that separates two live sessions holding
52
+ one, so the resolution can end on a string that reaches the wrong session. Send
53
+ the name bare where the listing shows one row under it. Where it shows more than
54
+ one, complete the address with the `[ref]` that listing prints beside each row
55
+ rather than sending to the name alone, which lands on whichever row the channel
56
+ resolves first. Two sessions differing only by a trailing ` (3)` is the shape
57
+ this meets in practice, and one of them being a controller is what makes the
58
+ wrong pick silent.
59
+
60
+ Ask the operator when the launch named nobody and a person is there to answer.
61
+ Put the candidate rows to them through the structured question surface, so they
62
+ pick a row rather than recalling a name. The ask halts the build, and the halt
63
+ is the cheaper error: a worker that cannot reach its controller has nothing
64
+ useful to do with the message it owes, where sending to the wrong session
65
+ reports success and loses it. A halt is only as visible as whatever watches for
66
+ one, so say what you are waiting on in the same turn you stop, and expect a
67
+ controller running no stall detector to find the question only when it next
68
+ looks.
69
+
70
+ Infer only where no operator is present. Read `aitk sessions list --json` and
71
+ take the sessions holding no feature branch as the candidates, since a
72
+ controlling session holds none. Say the addressee was inferred so the reader can
73
+ correct it. Never filter that roster by name prefix: every self-dispatched
74
+ worker is named `worker-<slug>`, so a prefix scan returns a sibling or this
75
+ session itself, which is the defect that sent messages owed to a controller
76
+ somewhere else.
77
+
78
+ Report a resolution that returns nothing rather than falling back to a guess.
79
+ The roster and the send channel enumerate different populations in both
80
+ directions, measured at one moment: a live background session sat on the roster
81
+ that the agent listing did not carry, and nine sessions were addressable there
82
+ with no roster row at all. Those nine were driving through Remote Control, which
83
+ writes no local process record, so a controller working from a phone is exactly
84
+ the case this read answers nothing for.
48
85
 
49
86
  ## Refusing is part of the job
50
87
 
@@ -63,8 +63,17 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
63
63
  | `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
64
64
  | `aitk inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`) |
65
65
  | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
66
+ | `aitk serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
66
67
  | `aitk upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
67
68
 
69
+ `aitk serve` ships and drives no browser, which is what separates it from the two that do. A generated page loses its stylesheet and its script to an editor preview and to a `file://` open, so the link is the delivery rather than a convenience, and every generated surface here reaches a reader through one. It binds `127.0.0.1` and never a wildcard, because what it is pointed at is routinely a gitignored record tree. It sends `cache-control: no-store`, since a preview exists to be edited and reloaded and a cached stylesheet reads as a fix that did not work.
70
+
71
+ A port already in use is the ordinary case rather than a refusal, so it walks forward to the next free one and reports which it took. That is why a caller reads `url` off the `--json` record instead of composing one from the port it asked for. Only contention is walked past. Any other bind failure refuses as `bind-failed` carrying the error's code, rather than being retried twenty times and reported as a range being full, which names a cause nothing checked.
72
+
73
+ A request naming a directory is redirected to its trailing-slash form rather than answered in place. A browser resolves a relative asset against the last slash of the URL it is on, so answering `/lesson` directly leaves the page asking for `/course.css` instead of `/lesson/course.css`, and it renders unstyled through the server that exists to prevent exactly that.
74
+
75
+ Containment is tested after symlinks are followed rather than on the path as written, and the test sits immediately before the read rather than beside the request that produced it. Resolving a request lexically clears a link pointing outside the served root, and this repository is a live instance of that shape, since `claude/standards` and `claude/snippets` are links out of `claude/`. Position is what makes the property hold: a directory request appends its index after the request path has been checked, so a check placed earlier leaves that index untested. An `--entry` that escapes the root refuses with `no-entry` before a port is taken, because `url` is the field a caller hands to a reader.
76
+
68
77
  `aitk demo` is the second browser command and the one that ships, since its purpose is running in a target rather than regenerating what this repository commits. It needs a browser binary the package does not carry, installed once with `bunx playwright install chromium`.
69
78
 
70
79
  `aitk inventory` is the third and takes the same answer for the same reason. It reads `inventory.toml` at the project root for its base URL, its routes, and the element query each subject runs over, so what it walks comes from the project rather than from the toolkit. It reports how many different answers a site gives for one property and never gates, because whether five focus rings across four routes is a defect is a judgment. A missing server and an unmatched query are both refusals rather than empty listings, since a listing with no rows reads as one consistent answer.
@@ -59,7 +59,7 @@ The article gate is looser than the verb list and carries a real cost. Re-runnin
59
59
 
60
60
  A delta phrased as a transition (`from fourteen to fifteen`), a fraction (`thirteen of sixteen`), and a total reached through an indirect noun (`denominator of sixty-one shipped skills`) are all catalog-size claims this corpus carries, and none matches the trigger shape this reads. Each stays a known gap. The false-positive rate that gated closing them has a measurement behind it now, and what it showed is that the widening these three need is the one that costs a false positive rather than the one that does not.
61
61
 
62
- A second figure in a sentence whose first figure already matched is a fourth gap and a structural one, since one match is taken per catalog per sentence. `authors 70 rules under governance/rules/ and consumes 55 into .claude/rules/` is read for its 70 alone, which is correct here because the two figures name different populations, and a sentence stating one catalog twice would go unread the same way.
62
+ A second figure in a sentence whose first figure already matched is a fourth gap and a structural one, since one match is taken per catalog per sentence. `authors 70 rules under governance/rules/ and consumes 62 into .claude/rules/` is read for its 70 alone, which is correct here because the two figures name different populations, and a sentence stating one catalog twice would go unread the same way.
63
63
 
64
64
  A calendar date (`2026-08-21`) or a backticked commit reference in the same sentence reads the whole sentence past, since that is how this corpus already marks a figure as a historical record rather than a live claim. `.claude/ARCHITECTURE.md` and the context entries carry a figure this way deliberately, and every one of them stays correct forever.
65
65
 
@@ -30,7 +30,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
30
30
  - [Routing report](routing.md): Reading per CLAUDE.md section how many bullets name a path, what counts as naming one, when a rule counts as covering it, the two refusals, and why the verb reports rather than gates
31
31
  - [Sandbox](sandbox.md): Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
32
32
  - [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
33
- - [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, the liveness confidence field, the unresolved reasons, and what the read depends on
33
+ - [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, reading which row is the caller, the liveness confidence field, and what each session surface can see
34
34
  - [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
35
35
  - [Citation reach](skills-reach.md): Reporting the skill bodies that cite a path no target project receives, which corpus the verb reads, the ownership key that decides what counts, the one-word qualifier that marks a citation as decided, and why the verb reports instead of gating
36
36
  - [Standard success criteria](standards-audit.md): Reading the corpus against the Success criterion gate, why the check scopes to arrival rather than the whole corpus, and the exit codes it sets
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Sessions
3
- description: Resolving live peer sessions to the worktree and branch each holds, the liveness confidence field, the unresolved reasons, and what the read depends on
3
+ description: Resolving live peer sessions to the worktree and branch each holds, reading which row is the caller, the liveness confidence field, and what each session surface can see
4
4
  ---
5
5
 
6
6
  # Sessions
@@ -14,6 +14,7 @@ aitk sessions list
14
14
  aitk sessions list --json
15
15
  aitk sessions list --branch feat/parser --json
16
16
  aitk sessions list --branch chore/agents --repository ../caret --json
17
+ aitk sessions list --self --json
17
18
  ```
18
19
 
19
20
  | Option | Behavior |
@@ -21,10 +22,11 @@ aitk sessions list --branch chore/agents --repository ../caret --json
21
22
  | `--json` | Add a machine-readable record on stdout |
22
23
  | `--branch <name>` | Report the sessions holding this branch |
23
24
  | `--repository <path>` | Answer about this project rather than the working one |
25
+ | `--self` | Report the caller's own row |
24
26
 
25
27
  It reads and never writes. The question it answers is which session to address when work has to reach the one holding a given branch, which a session listing cannot answer on its own.
26
28
 
27
- Exit codes: `0` the roster was read, `1` refused. The refusal carries a `reason` of `no-registry` or `no-repository`.
29
+ Exit codes: `0` the roster was read, `1` refused. The refusal carries a `reason` of `no-registry`, `no-repository`, `no-self-identity`, or `no-self-row`.
28
30
 
29
31
  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.
30
32
 
@@ -52,6 +54,29 @@ The ref read covers the local head and the remote-tracking ref, which means it s
52
54
 
53
55
  Two flags say which reading came up short. `sessionsReadable` is `false` when the session roster could not be read, and `refsReadable` is `false` when the ref read failed. Either one leaves `claimed` covering the readings around it alone, so a `false` there is a report that ran short of evidence rather than a report that the branch is clear. They stay separate fields because a caller told the roster failed goes and looks at the roster, and folding both into one flag would send it to the wrong place.
54
56
 
57
+ ## Which row is the caller
58
+
59
+ `--self` narrows the report to the row belonging to the session making the call. A dispatcher reads it to learn the `sessionId` it carries into a launch, because the roster returns every field on every row and marks none of them as the caller.
60
+
61
+ Three identifier namespaces reach a session and two of them join to a row. Four variables spell them:
62
+
63
+ - `CLAUDE_CODE_SESSION_ID` holds the roster's own `sessionId`, and is read first because it survives a rename.
64
+ - `CLAUDE_PID` holds the caller's process id, which every row already carries as `pid`.
65
+ - `CLAUDE_CODE_MESSAGING_SOCKET` spells that same pid in its basename. It is read last, since the spelling is a client convention rather than a published interface and a client that moves the socket drops this rung alone.
66
+ - `CLAUDE_CODE_HOST_SESSION_ID` is never read. It carries a `local_`-prefixed value from the harness namespace that matches no row, and it is the variable a reader searching the environment for a session id finds first.
67
+
68
+ The read refuses rather than returning an empty roster, on the same ground as `no-registry`. A `no-self-identity` refusal means the environment stated none of the three, which is a client identifying its sessions some other way. A `no-self-row` refusal means it stated one and no live row carries it, which is what a session running outside a local process gets.
69
+
70
+ ## What each surface can see
71
+
72
+ The roster this verb reads and the channel a session sends messages on enumerate different populations, and neither contains the other. Read at one moment, `aitk sessions list` returned 6 rows against the 13 peers plus caller the agent listing reported. The roster held a background session at confirmed confidence on two reads a minute apart that the listing never carried, so a live session sat here with no way to reach it. Nine Remote Control sessions ran the other way, reachable there with no row here at all, since they leave no local process for the registry to record.
73
+
74
+ That bounds what a carried identifier buys. Resolving a `sessionId` to a name here can produce a name the send channel rejects, and `--self` answers nothing for a controller driving from Remote Control, which is the operator working from their phone rather than an edge case. A caller that cannot resolve an addressee reports the failed resolution instead of falling back to a guess.
75
+
76
+ The `name` field is what both surfaces agree on, and that agreement is what makes the resolution work. `SendMessage` takes a name and carries no identifier parameter, so the carry runs id to name to send rather than sending an id anywhere.
77
+
78
+ Agreement is not uniqueness, and the last hop is where that bites. Two live sessions can hold one name, the roster carries no field that separates them, and a send to the bare name reaches whichever the channel resolves first. The agent listing prints a ref beside each row and the roster does not, so a name resolving to more than one live row is completed from that listing rather than from anything this verb returns.
79
+
55
80
  ## Why the verb exists
56
81
 
57
82
  A session listing reports a name, a kind, a status, and how long each session has been running. None of those names a branch. Resolving a branch to a session therefore meant ordering the roster by start time and matching it against the order the worktrees were created, which is an inference that fails whenever two sessions start inside the same minute.
@@ -129,6 +129,18 @@ Each `quiz` entry carries `order`, the authored option indices in presentation o
129
129
 
130
130
  The order is drawn here rather than instructed, and that is the point of the verb. An author told to vary the position still varies it by judgment, and the judgment settles on the first slot, which is the defect this design departs from. The draw is uniform over the options, so the position carries no information about which answer is correct.
131
131
 
132
+ ## Opening a workspace
133
+
134
+ No `aitk teach` verb serves the workspace. `aitk serve` does, taking the teach root as its directory and the workspace contents page as its entry:
135
+
136
+ ```bash
137
+ aitk serve .claude/teach --entry 03-fde-system-design/index.html --json
138
+ ```
139
+
140
+ It stays general rather than becoming `aitk teach serve`, because nothing about serving a directory is specific to a learning workspace, and the same verb carries a slide render and a design preview.
141
+
142
+ Read `url` off the record rather than building one from the port that was asked for. The verb walks forward past a port already in use, which is routine when a second workspace is already open, and the port it took is the one thing a composed URL gets wrong.
143
+
132
144
  ## Refusal reasons
133
145
 
134
146
  | Reason | Raised when |
@@ -9,5 +9,5 @@ paths:
9
9
  ## Authority
10
10
 
11
11
  - Follow the teach standard for the folder layout, ordinal naming, frontmatter, and the mission and learning-record formats. It is the single source. Read it with `aitk standards teach`.
12
- - Read `references/glossary.md` inside the `aitk:claude-teach` skill for the glossary every workspace carries at its root. It is the single source for the entry shape, the ordering, and which terms the file carries. Read it rather than invoking the skill, which resumes a workspace and runs lessons.
12
+ - Follow the glossary standard for the glossary every workspace carries at its root. It is the single source for the entry shape, the ordering, and which terms the file carries. Read it with `aitk standards glossary`.
13
13
  - Report it rather than proceeding silently when that file does not resolve. It ships with the plugin and this rule ships with the CLI, so a project that installed governance alone does not have it.
@@ -4,6 +4,7 @@ paths:
4
4
  - '**/*.tsx'
5
5
  - '**/*.jsx'
6
6
  - '**/*.astro'
7
+ - '**/*.html'
7
8
  ---
8
9
 
9
10
  # UI copy standards
@@ -4,6 +4,8 @@ paths:
4
4
  - '**/*.tsx'
5
5
  - '**/*.jsx'
6
6
  - '**/*.astro'
7
+ - '**/*.html'
8
+ - '**/*.css'
7
9
  ---
8
10
 
9
11
  # Accessibility and keyboard standards
@@ -4,6 +4,7 @@ paths:
4
4
  - '**/*.tsx'
5
5
  - '**/*.jsx'
6
6
  - '**/*.astro'
7
+ - '**/*.html'
7
8
  ---
8
9
 
9
10
  # Form standards
@@ -4,6 +4,8 @@ paths:
4
4
  - '**/*.tsx'
5
5
  - '**/*.jsx'
6
6
  - '**/*.astro'
7
+ - '**/*.html'
8
+ - '**/*.css'
7
9
  ---
8
10
 
9
11
  # UX completeness standards
@@ -1,24 +1,32 @@
1
1
  ---
2
- description: Require a route capture against a running preview after a page or route surface changes
2
+ description: Require a capture against a running preview after a rendered surface changes
3
3
  paths:
4
4
  - '**/routes/**/*.{tsx,jsx,vue,svelte,astro}'
5
5
  - '**/pages/**/*.{tsx,jsx,vue,svelte,astro}'
6
6
  - '**/app/**/page.{tsx,jsx}'
7
+ - '**/*.html'
7
8
  ---
8
9
 
9
10
  # Surface capture standards
10
11
 
12
+ ## What a surface is
13
+
14
+ - A surface is anything the project renders for a person to look at. A framework route is one. A page the project generates is another, whoever generates it.
15
+ - Judge a generated page by the same rule as a route. Reading its markup reports nothing about how it composes.
16
+
11
17
  ## When to capture
12
18
 
13
- - Run `bun run screenshot` after changing what a route renders.
19
+ - Run the project's capture command after changing what a surface renders. Report it rather than proceeding silently when the project has none.
14
20
  - Capture against a running preview server. Do not capture against a dev server.
15
- - Capture every theme the route ships. Do not capture the default theme alone.
21
+ - Capture every theme the surface ships. Do not capture the default theme alone.
22
+ - Capture again after fixing a defect a capture found. A repair inside a shared stylesheet can cancel a rule written earlier in the same file.
16
23
 
17
24
  ## What a capture covers
18
25
 
19
26
  - Capture the full page at the viewport its case declares. Do not capture a component in isolation.
20
- - Add a case to the capture record when adding a route.
21
- - Remove a route's case in the change that removes the route.
27
+ - Drive a state a screenshot cannot reach. An initial render reports nothing about a menu that opens, an answer that is chosen, or a rail that tracks scrolling.
28
+ - Add a case to the capture record when adding a surface.
29
+ - Remove a surface's case in the change that removes the surface.
22
30
 
23
31
  ## Sharing a capture
24
32
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "3.45.0",
4
+ "version": "3.47.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
package/src/cli.ts CHANGED
@@ -15,6 +15,7 @@ import { register as docs } from '@/commands/docs'
15
15
  import { register as design } from '@/commands/design'
16
16
  import { register as slides } from '@/commands/slides'
17
17
  import { register as capture } from '@/commands/capture'
18
+ import { register as serve } from '@/commands/serve'
18
19
  import { register as demo } from '@/commands/demo'
19
20
  import { register as inventory } from '@/commands/inventory'
20
21
  import { register as feedback } from '@/commands/feedback'
@@ -61,6 +62,7 @@ function showHelp(): void {
61
62
  `${GREY}│${NC} design [cmd] ${GREY}# Design system commands (render)${NC}`,
62
63
  `${GREY}│${NC} slides [cmd] ${GREY}# Slide deck commands (render, list)${NC}`,
63
64
  `${GREY}│${NC} capture [source] ${GREY}# Render HTML capture sources to PNG${NC}`,
65
+ `${GREY}│${NC} serve [dir] ${GREY}# Serve a directory over localhost and print the preview link${NC}`,
64
66
  `${GREY}│${NC} demo [cmd] ${GREY}# Record a running app (compile, run)${NC}`,
65
67
  `${GREY}│${NC} inventory [subj] ${GREY}# Report one computed property across every route${NC}`,
66
68
  `${GREY}│${NC} feedback ${GREY}# Write toolkit feedback from stdin to .claude/review/feedback/${NC}`,
@@ -108,6 +110,7 @@ function showHelp(): void {
108
110
  `${GREY}│${NC} aitk slides render`,
109
111
  `${GREY}│${NC} aitk slides list --json`,
110
112
  `${GREY}│${NC} aitk capture assets/install.html`,
113
+ `${GREY}│${NC} aitk serve .claude/teach`,
111
114
  `${GREY}│${NC} aitk inventory focus --json`,
112
115
  `${GREY}│${NC} pbpaste | aitk feedback`,
113
116
  `${GREY}│${NC} aitk transcripts https://youtu.be/VIDEO_ID`,
@@ -160,6 +163,7 @@ docs(program)
160
163
  design(program)
161
164
  slides(program)
162
165
  capture(program)
166
+ serve(program)
163
167
  demo(program)
164
168
  inventory(program)
165
169
  feedback(program)
@@ -0,0 +1,159 @@
1
+ import type { Command } from 'commander'
2
+ import { relative } from 'node:path'
3
+ import { DEFAULT_PORT, type ServeOutcome, startServer } from '@/serve/static'
4
+ import { intro, logError, logInfo, logStep, logWarn, outro } from '@/ui'
5
+
6
+ const DEFAULT_DIR = '.'
7
+
8
+ interface ServeCommandOptions {
9
+ readonly entry?: string
10
+ readonly json?: boolean
11
+ readonly port?: string
12
+ }
13
+
14
+ export function register(program: Command): void {
15
+ program
16
+ .command('serve')
17
+ .description('Serve a directory over localhost and print the preview link')
18
+ .argument('[dir]', 'Directory to serve', DEFAULT_DIR)
19
+ .helpOption('-h, --help', 'Show this help message')
20
+ .option('--port <number>', `Port to try first, default ${DEFAULT_PORT}`)
21
+ .option('--entry <path>', 'Page the printed link opens, default index.html')
22
+ .option('--json', 'Emit a machine-readable record on stdout')
23
+ .addHelpText(
24
+ 'after',
25
+ [
26
+ '',
27
+ 'Exit codes:',
28
+ ' 0 the server stopped after running',
29
+ ' 1 refused, with the reason on stderr or in the JSON record',
30
+ '',
31
+ 'The server binds 127.0.0.1 and nothing else, and it sends no cache',
32
+ 'headers, so an edited stylesheet is never served stale. A port in use',
33
+ 'is not a failure: the next free one is taken and the link says which.',
34
+ '',
35
+ 'It runs until interrupted. A session wanting the link without waiting',
36
+ 'starts it in the background and reads the record off stdout.',
37
+ '',
38
+ 'Examples:',
39
+ ' aitk serve .claude/teach',
40
+ ' aitk serve .claude/teach --entry 03-fde-system-design/index.html',
41
+ ' aitk serve dist --port 4000 --json',
42
+ '',
43
+ ].join('\n'),
44
+ )
45
+ .action(async (dir: string, opts: ServeCommandOptions) => {
46
+ process.exitCode = await runServe(dir, opts)
47
+ })
48
+ }
49
+
50
+ async function runServe(
51
+ dir: string,
52
+ opts: ServeCommandOptions,
53
+ ): Promise<number> {
54
+ const emitJson = opts.json ?? false
55
+
56
+ const port = parsePort(opts.port)
57
+ if (port === undefined) {
58
+ return report(
59
+ { ok: false, reason: 'no-port', detail: `${opts.port} is not a port` },
60
+ emitJson,
61
+ )
62
+ }
63
+
64
+ const outcome = startServer(dir, { port, entry: opts.entry })
65
+ const code = report(outcome, emitJson)
66
+ if (!outcome.ok) return code
67
+
68
+ await waitForInterrupt(outcome.stop)
69
+ return 0
70
+ }
71
+
72
+ /**
73
+ * The relative path where it stays inside the working directory, and the
74
+ * absolute one where it climbs out. A run from a linked worktree serving the
75
+ * main root reports `../../teach`, which names the directory without giving a
76
+ * reader anything they can open.
77
+ */
78
+ function displayPath(root: string): string {
79
+ const near = relative(process.cwd(), root)
80
+ if (near === '') return '.'
81
+ return near.startsWith('..') ? root : near
82
+ }
83
+
84
+ /**
85
+ * Undefined for a value that is not a port, distinct from an absent flag,
86
+ * which takes the default. A `Number` of a typo is `NaN`, and passing that on
87
+ * asks the runtime to bind a port nobody named.
88
+ */
89
+ function parsePort(raw: string | undefined): number | undefined {
90
+ if (raw === undefined) return DEFAULT_PORT
91
+ const value = Number(raw)
92
+ if (!Number.isInteger(value) || value < 0 || value > 65535) return undefined
93
+ return value
94
+ }
95
+
96
+ function report(outcome: ServeOutcome, emitJson: boolean): number {
97
+ if (!outcome.ok) {
98
+ if (emitJson) {
99
+ process.stdout.write(
100
+ `${JSON.stringify({ ok: false, reason: outcome.reason, detail: outcome.detail })}\n`,
101
+ )
102
+ return 1
103
+ }
104
+ intro('aitk serve')
105
+ logError(outcome.detail)
106
+ outro()
107
+ return 1
108
+ }
109
+
110
+ if (emitJson) {
111
+ process.stdout.write(
112
+ `${JSON.stringify({
113
+ ok: true,
114
+ root: outcome.root,
115
+ host: outcome.host,
116
+ port: outcome.port,
117
+ entry: outcome.entry,
118
+ url: outcome.url,
119
+ entryExists: outcome.entryExists,
120
+ })}\n`,
121
+ )
122
+ return 0
123
+ }
124
+
125
+ intro('aitk serve')
126
+ logStep('Serving')
127
+ logInfo(displayPath(outcome.root))
128
+ logStep('Open')
129
+ logInfo(outcome.url)
130
+
131
+ if (!outcome.entryExists) {
132
+ logStep('No entry page')
133
+ logWarn(
134
+ `${outcome.entry} is not in that directory, so the link opens a 404`,
135
+ )
136
+ }
137
+
138
+ logStep('Stop')
139
+ logInfo('Ctrl-C')
140
+ return 0
141
+ }
142
+
143
+ /**
144
+ * Resolves when the process is asked to stop. The frame is closed here rather
145
+ * than in the reporter, because the run is the serving rather than the start,
146
+ * and closing at start would print the frame's end while the server ran on.
147
+ */
148
+ function waitForInterrupt(stop: () => Promise<void>): Promise<void> {
149
+ return new Promise((settle) => {
150
+ const finish = () => {
151
+ void stop().then(() => {
152
+ outro()
153
+ settle()
154
+ })
155
+ }
156
+ process.once('SIGINT', finish)
157
+ process.once('SIGTERM', finish)
158
+ })
159
+ }