@erclx/aitk 0.63.1 → 0.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-docs/SKILL.md +3 -1
- package/claude/skills/claude-feature/SKILL.md +2 -0
- package/claude/skills/claude-memory-capture/SKILL.md +9 -2
- package/claude/skills/claude-memory-review/SKILL.md +5 -2
- package/claude/skills/claude-review/SKILL.md +2 -0
- package/claude/skills/claude-screencast/SKILL.md +2 -0
- package/claude/skills/claude-seed-sync/SKILL.md +2 -0
- package/claude/skills/claude-tasks/SKILL.md +3 -1
- package/claude/skills/claude-ui-test/SKILL.md +2 -0
- package/claude/skills/claude-ux-audit/SKILL.md +2 -0
- package/claude/skills/git-pr/SKILL.md +9 -3
- package/docs/agents/commands.md +5 -1
- package/docs/agents/context-audit-checks.md +9 -11
- package/docs/agents/context-audit.md +3 -1
- package/docs/agents/index.md +3 -2
- package/docs/agents/markdown-audit.md +70 -0
- package/docs/agents/tasks.md +51 -2
- package/docs/ai-workflow.md +1 -1
- package/package.json +1 -1
- package/src/cli.ts +4 -0
- package/src/commands/context.ts +8 -115
- package/src/commands/markdown.ts +383 -0
- package/src/commands/records.ts +1 -18
- package/src/commands/tasks.ts +314 -19
- package/src/context/audit.ts +34 -309
- package/src/context/citations.ts +2 -30
- package/src/git-files.ts +31 -0
- package/src/markdown/bans.ts +241 -0
- package/src/markdown/files.ts +92 -0
- package/src/markdown/scan.ts +183 -0
- package/src/markdown/structure.ts +408 -0
- package/src/records/validate.ts +1 -37
- package/src/tasks/archive.ts +34 -3
- package/src/tasks/record.ts +311 -0
- package/src/worktree.ts +23 -0
- package/standards/context.md +2 -7
- package/standards/markdown.md +10 -2
- package/tooling/claude/seeds/CLAUDE.md +1 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { basename, join, relative } from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
fenceMask,
|
|
6
|
+
listTaskStems,
|
|
7
|
+
OUTCOME_PATTERN,
|
|
8
|
+
readPlanTarget,
|
|
9
|
+
tasksDir,
|
|
10
|
+
} from '@/tasks/archive'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lines a task carries above its outcomes naming where the work came from. The
|
|
14
|
+
* `Pull request:` line joins them, so the last one present is the anchor.
|
|
15
|
+
*/
|
|
16
|
+
const ORIGIN_PREFIXES = ['Plan:', 'Groundwork:', 'Intake:', 'Issue:'] as const
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `bad-input` is separated from the board-state reasons on purpose. `git-pr`
|
|
20
|
+
* swallows `no-board`, `no-match`, and `ambiguous`, because each is a case where
|
|
21
|
+
* a guessed write would archive the wrong task. A caller that passed a
|
|
22
|
+
* malformed argument has a defect nobody would otherwise hear about, so it
|
|
23
|
+
* reports rather than joining the swallowed set.
|
|
24
|
+
*/
|
|
25
|
+
export const RECORD_REFUSALS = [
|
|
26
|
+
'no-board',
|
|
27
|
+
'no-match',
|
|
28
|
+
'ambiguous',
|
|
29
|
+
'no-outcomes',
|
|
30
|
+
'out-of-range',
|
|
31
|
+
'bad-input',
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
export type RecordRefusal = (typeof RECORD_REFUSALS)[number]
|
|
35
|
+
|
|
36
|
+
export interface RecordRefused {
|
|
37
|
+
readonly ok: false
|
|
38
|
+
readonly reason: RecordRefusal
|
|
39
|
+
readonly message: string
|
|
40
|
+
readonly detail: readonly string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type RecordSelector =
|
|
44
|
+
| { readonly kind: 'stem'; readonly stem: string }
|
|
45
|
+
| { readonly kind: 'plan'; readonly plan: string }
|
|
46
|
+
|
|
47
|
+
export type LineAction = 'added' | 'corrected' | 'unchanged'
|
|
48
|
+
|
|
49
|
+
export interface PullRequestRecorded {
|
|
50
|
+
readonly ok: true
|
|
51
|
+
readonly stem: string
|
|
52
|
+
readonly path: string
|
|
53
|
+
readonly number: number
|
|
54
|
+
readonly action: LineAction
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type PullRequestOutcome = PullRequestRecorded | RecordRefused
|
|
58
|
+
|
|
59
|
+
export interface OutcomesClosed {
|
|
60
|
+
readonly ok: true
|
|
61
|
+
readonly stem: string
|
|
62
|
+
readonly path: string
|
|
63
|
+
readonly closed: readonly string[]
|
|
64
|
+
readonly alreadyClosed: readonly string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type CloseOutcome = OutcomesClosed | RecordRefused
|
|
68
|
+
|
|
69
|
+
function refuse(
|
|
70
|
+
reason: RecordRefusal,
|
|
71
|
+
message: string,
|
|
72
|
+
detail: readonly string[] = [],
|
|
73
|
+
): RecordRefused {
|
|
74
|
+
return { ok: false, reason, message, detail }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reduces a plan reference to the token both spellings share. A task's `Plan:`
|
|
79
|
+
* line carries a path, a caller carries the branch slug, and the two differ by
|
|
80
|
+
* the folder, the extension, and the `feature-` prefix the filename adds.
|
|
81
|
+
*/
|
|
82
|
+
function planKey(reference: string): string {
|
|
83
|
+
const stem = basename(reference).replace(/\.md$/, '')
|
|
84
|
+
return stem.startsWith('feature-') ? stem.slice('feature-'.length) : stem
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Places the `Pull request:` line under the origin lines the task already
|
|
89
|
+
* carries, and corrects the number in place when the line exists. A task with
|
|
90
|
+
* no origin line takes it under the H1, which is the only other anchor the
|
|
91
|
+
* board format guarantees.
|
|
92
|
+
*/
|
|
93
|
+
export function writePullRequestLine(
|
|
94
|
+
text: string,
|
|
95
|
+
number: number,
|
|
96
|
+
): { readonly text: string; readonly action: LineAction } {
|
|
97
|
+
const line = `Pull request: #${number}`
|
|
98
|
+
const lines = text.split('\n')
|
|
99
|
+
const existing = lines.findIndex((entry) => entry.startsWith('Pull request:'))
|
|
100
|
+
|
|
101
|
+
if (existing !== -1) {
|
|
102
|
+
if (lines[existing] === line) return { text, action: 'unchanged' }
|
|
103
|
+
lines[existing] = line
|
|
104
|
+
return { text: lines.join('\n'), action: 'corrected' }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const origin = lastOriginLine(lines)
|
|
108
|
+
if (origin !== undefined) {
|
|
109
|
+
lines.splice(origin + 1, 0, line)
|
|
110
|
+
return { text: lines.join('\n'), action: 'added' }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const heading = lines.findIndex((entry) => entry.startsWith('# '))
|
|
114
|
+
if (heading === -1) return { text: `${line}\n${text}`, action: 'added' }
|
|
115
|
+
|
|
116
|
+
lines.splice(heading + 1, 0, '', line)
|
|
117
|
+
return { text: lines.join('\n'), action: 'added' }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function lastOriginLine(lines: readonly string[]): number | undefined {
|
|
121
|
+
let found: number | undefined
|
|
122
|
+
|
|
123
|
+
for (const [index, line] of lines.entries()) {
|
|
124
|
+
if (ORIGIN_PREFIXES.some((prefix) => line.startsWith(prefix))) found = index
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return found
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Marks outcomes closed by their 1-based position in the task's outcome list,
|
|
132
|
+
* which is the order the board format writes them. A caller reads the file
|
|
133
|
+
* before deciding, so a position is what it already holds, while a text match
|
|
134
|
+
* would need the wording handed back exactly.
|
|
135
|
+
*/
|
|
136
|
+
export function closeOutcomeLines(
|
|
137
|
+
text: string,
|
|
138
|
+
positions: readonly number[],
|
|
139
|
+
): {
|
|
140
|
+
readonly text: string
|
|
141
|
+
readonly closed: readonly string[]
|
|
142
|
+
readonly alreadyClosed: readonly string[]
|
|
143
|
+
readonly total: number
|
|
144
|
+
} {
|
|
145
|
+
const wanted = new Set(positions)
|
|
146
|
+
const closed: string[] = []
|
|
147
|
+
const alreadyClosed: string[] = []
|
|
148
|
+
const lines = text.split('\n')
|
|
149
|
+
const fenced = fenceMask(lines)
|
|
150
|
+
let seen = 0
|
|
151
|
+
|
|
152
|
+
const rewritten = lines.map((line, index) => {
|
|
153
|
+
if (fenced[index]) return line
|
|
154
|
+
|
|
155
|
+
const match = OUTCOME_PATTERN.exec(line)
|
|
156
|
+
if (!match) return line
|
|
157
|
+
|
|
158
|
+
seen += 1
|
|
159
|
+
if (!wanted.has(seen)) return line
|
|
160
|
+
|
|
161
|
+
const [, box, body] = match
|
|
162
|
+
if (box !== ' ') {
|
|
163
|
+
alreadyClosed.push(body.trim())
|
|
164
|
+
return line
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
closed.push(body.trim())
|
|
168
|
+
return body ? `- [x] ${body}` : '- [x]'
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return { text: rewritten.join('\n'), closed, alreadyClosed, total: seen }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function matchByPlan(
|
|
175
|
+
dir: string,
|
|
176
|
+
stems: readonly string[],
|
|
177
|
+
plan: string,
|
|
178
|
+
): Promise<string[]> {
|
|
179
|
+
const key = planKey(plan)
|
|
180
|
+
|
|
181
|
+
const read = await Promise.all(
|
|
182
|
+
stems.map(async (stem) => ({
|
|
183
|
+
stem,
|
|
184
|
+
target: readPlanTarget(await readFile(join(dir, `${stem}.md`), 'utf8')),
|
|
185
|
+
})),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return read
|
|
189
|
+
.filter(
|
|
190
|
+
(entry) => entry.target !== undefined && planKey(entry.target) === key,
|
|
191
|
+
)
|
|
192
|
+
.map(({ stem }) => stem)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function resolveStem(
|
|
196
|
+
dir: string,
|
|
197
|
+
selector: RecordSelector,
|
|
198
|
+
): Promise<string | RecordRefused> {
|
|
199
|
+
const stems = await listTaskStems(dir)
|
|
200
|
+
|
|
201
|
+
if (selector.kind === 'stem') {
|
|
202
|
+
if (!stems.includes(selector.stem)) {
|
|
203
|
+
return refuse(
|
|
204
|
+
'no-match',
|
|
205
|
+
`No task named ${selector.stem} on the board.`,
|
|
206
|
+
stems,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
return selector.stem
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const matched = await matchByPlan(dir, stems, selector.plan)
|
|
213
|
+
|
|
214
|
+
if (matched.length === 0) {
|
|
215
|
+
return refuse('no-match', `No task names plan ${selector.plan}.`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (matched.length > 1) {
|
|
219
|
+
return refuse(
|
|
220
|
+
'ambiguous',
|
|
221
|
+
`${matched.length} tasks name plan ${selector.plan}. One task, one plan.`,
|
|
222
|
+
matched,
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return matched[0]
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function openTask(
|
|
230
|
+
root: string,
|
|
231
|
+
selector: RecordSelector,
|
|
232
|
+
): Promise<{ readonly stem: string; readonly path: string } | RecordRefused> {
|
|
233
|
+
const dir = tasksDir(root)
|
|
234
|
+
|
|
235
|
+
if (!existsSync(dir)) {
|
|
236
|
+
return refuse('no-board', `No task board at ${relative(root, dir)}.`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const resolved = await resolveStem(dir, selector)
|
|
240
|
+
if (typeof resolved !== 'string') return resolved
|
|
241
|
+
|
|
242
|
+
return { stem: resolved, path: join(dir, `${resolved}.md`) }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Records a pull request number on the task the branch closes. `git-pr` runs
|
|
247
|
+
* this from a linked worktree, where an in-place edit through the file-editing
|
|
248
|
+
* tools is refused and a shell stream editor is banned, so the write has to
|
|
249
|
+
* resolve the board root in-process.
|
|
250
|
+
*/
|
|
251
|
+
export async function recordPullRequest(
|
|
252
|
+
root: string,
|
|
253
|
+
selector: RecordSelector,
|
|
254
|
+
number: number,
|
|
255
|
+
): Promise<PullRequestOutcome> {
|
|
256
|
+
const opened = await openTask(root, selector)
|
|
257
|
+
if ('ok' in opened) return opened
|
|
258
|
+
|
|
259
|
+
const { stem, path } = opened
|
|
260
|
+
const { text, action } = writePullRequestLine(
|
|
261
|
+
await readFile(path, 'utf8'),
|
|
262
|
+
number,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if (action !== 'unchanged') await writeFile(path, text)
|
|
266
|
+
|
|
267
|
+
return { ok: true, stem, path, number, action }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Marks the named outcomes `[x]` in place. `claude-docs` runs this against a
|
|
272
|
+
* board it can read and cannot edit from a linked worktree, and the positions
|
|
273
|
+
* come from the read it already made.
|
|
274
|
+
*/
|
|
275
|
+
export async function closeOutcomes(
|
|
276
|
+
root: string,
|
|
277
|
+
selector: RecordSelector,
|
|
278
|
+
positions: readonly number[],
|
|
279
|
+
): Promise<CloseOutcome> {
|
|
280
|
+
const opened = await openTask(root, selector)
|
|
281
|
+
if ('ok' in opened) return opened
|
|
282
|
+
|
|
283
|
+
const { stem, path } = opened
|
|
284
|
+
const result = closeOutcomeLines(await readFile(path, 'utf8'), positions)
|
|
285
|
+
|
|
286
|
+
if (result.total === 0) {
|
|
287
|
+
return refuse('no-outcomes', `${stem} carries no outcomes to close.`)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const beyond = positions.filter(
|
|
291
|
+
(position) => position < 1 || position > result.total,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
if (beyond.length > 0) {
|
|
295
|
+
return refuse(
|
|
296
|
+
'out-of-range',
|
|
297
|
+
`${stem} carries ${result.total} outcome(s), so ${beyond.join(', ')} names nothing.`,
|
|
298
|
+
beyond.map(String),
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (result.closed.length > 0) await writeFile(path, result.text)
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
ok: true,
|
|
306
|
+
stem,
|
|
307
|
+
path,
|
|
308
|
+
closed: result.closed,
|
|
309
|
+
alreadyClosed: result.alreadyClosed,
|
|
310
|
+
}
|
|
311
|
+
}
|
package/src/worktree.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the root every shared-scratch folder lives under. `git worktree
|
|
5
|
+
* list` puts the main worktree first, and trusting the working directory
|
|
6
|
+
* instead would write a second board nothing else reads, or validate a linked
|
|
7
|
+
* worktree's empty folder and report it clean.
|
|
8
|
+
*
|
|
9
|
+
* A caller reaching this from a linked worktree is the case it exists for: the
|
|
10
|
+
* file-editing tools refuse a main-root path there, so a verb resolving the
|
|
11
|
+
* root in-process is the route a skill body has.
|
|
12
|
+
*/
|
|
13
|
+
export async function mainWorktreeRoot(): Promise<string> {
|
|
14
|
+
const result = await $`git worktree list --porcelain`.quiet().nothrow()
|
|
15
|
+
if (result.exitCode !== 0) return process.cwd()
|
|
16
|
+
|
|
17
|
+
const line = result.stdout
|
|
18
|
+
.toString()
|
|
19
|
+
.split('\n')
|
|
20
|
+
.find((entry) => entry.startsWith('worktree '))
|
|
21
|
+
|
|
22
|
+
return line ? line.slice('worktree '.length).trim() : process.cwd()
|
|
23
|
+
}
|
package/standards/context.md
CHANGED
|
@@ -100,13 +100,8 @@ Only the `development` entry carries this section. It is not a general-purpose h
|
|
|
100
100
|
## Length
|
|
101
101
|
|
|
102
102
|
- Aim for one entry per domain. There is no hard cap. Length is a symptom, not the defect.
|
|
103
|
-
- Past roughly 150 rendered lines, check three things before adding more: whether the entry still covers a single domain, whether it has filled with content `ls` or `--help` reproduces, and whether it has accumulated the history of its own changes. Fix whichever is true rather than trimming to hit a number.
|
|
104
|
-
-
|
|
105
|
-
- Both checkpoints count rendered lines, so wrap each source line at 80 columns and sum the heights. Source lines undercount an entry authored one line per bullet, where a block of fifteen paragraph-bullets occupies fifteen lines and renders past sixty. Counting the two checkpoints in different units would put a file measured one way beside a run measured another.
|
|
106
|
-
- Exempt a block whose lines are all list items at one level averaging under roughly 130 characters. A flat list of short peers is already navigable, and a subheading dropped into it splits a set that belongs together. Bullet count says nothing on its own, since a catalog of one-liners and a stack of paragraphs reach the same count and read nothing alike, so weight is what decides. Mixing prose with the list, or nesting levels inside it, ends the exemption at any weight.
|
|
107
|
-
- Exempt a block whose lines are all table rows, at any length. The peer list above is exempt because it is already navigable, and a table because the remedy does not exist: a subheading dropped inside one splits the table rather than the run, so no edit short of rewriting it as a list clears the checkpoint. Prose either side of the table ends the exemption, since that block has a seam and a heading breaks it there.
|
|
108
|
-
- Past roughly 400 characters in one top-level bullet, counting the lines that continue it and excluding any bullet nested under it, check whether the incident that motivated the decision sits beside the decision itself. Keep the current design and the alternative that lost, and move the incident to the change that introduced it, the issue that tracked it, or the research record behind it. The number is a checkpoint like the two above, and a bullet reading well past it means the number is wrong rather than the rule.
|
|
109
|
-
- Collapse a stack of bullets narrating one subsystem into a single `###` subsection carrying one narrative. Splitting a heavy bullet into three light ones satisfies the checkpoint above and leaves the reader no better off, and subdividing a block does not lighten the bullets inside it, so the two rules answer different defects.
|
|
103
|
+
- Past roughly 150 rendered lines, check three things before adding more: whether the entry still covers a single domain, whether it has filled with content `ls` or `--help` reproduces, and whether it has accumulated the history of its own changes. Fix whichever is true rather than trimming to hit a number. Rendered lines count as `markdown.md` defines them.
|
|
104
|
+
- Where a bullet sits past the weight checkpoint `markdown.md` states, the overflow to move is the incident that motivated the decision, which specializes that rule's instruction to send the overflow to prose. Keep the current design and the alternative that lost, and send the incident to the change that introduced it, the issue that tracked it, or the research record behind it.
|
|
110
105
|
- Never cut a `## Decisions` or `## Gotchas` entry to shorten a file. Cut a `## Layout` or `## CLI` section instead.
|
|
111
106
|
- Retire a decision or gotcha once its subject is gone, rewriting the bullet to state the current design rather than leaving the narration of what it replaced beside it. A rejected alternative is not a retired one, so what was tried and why it lost stays whatever its age. The rule above protects content whose subject is live, and this one releases content whose subject is not.
|
|
112
107
|
- Rewrite a decision a later one replaced rather than appending the replacement beside it. The subject is still live, so the rule above does not reach it, and two bullets on one subject leave a reader to work out which of them is current. State the design that stands and keep the superseded reasoning only where it is the alternative that lost.
|
package/standards/markdown.md
CHANGED
|
@@ -9,7 +9,7 @@ Applies to markdown reference docs, READMEs, and inline documentation in repos.
|
|
|
9
9
|
|
|
10
10
|
## Scope
|
|
11
11
|
|
|
12
|
-
Governs the markdown mechanics of every markdown file: headings, paragraph and list structure, code spans and fences, punctuation, emphasis, and file references. It is an attribute standard rather than a document-type one, so it applies over documents whose shape another standard sets, and it carries no template because mechanics are written across every document and have
|
|
12
|
+
Governs the markdown mechanics of every markdown file: headings, paragraph and list structure, code spans and fences, punctuation, emphasis, and file references. It is an attribute standard rather than a document-type one, so it applies over documents whose shape another standard sets, and it carries no template because mechanics are written across every document and have no shape of their own.
|
|
13
13
|
|
|
14
14
|
Does not govern:
|
|
15
15
|
|
|
@@ -23,12 +23,20 @@ Does not govern:
|
|
|
23
23
|
- H1 for document title, H2 for main sections, H3 for subsections
|
|
24
24
|
- Use sentence case for all headings (H1, H2, H3)
|
|
25
25
|
- Proper nouns and product names retain their casing in headings
|
|
26
|
+
- Past roughly 40 rendered lines with no heading of any level breaking them, add a subheading at the seam. Measure the longest such run rather than everything under one `##`, and exclude fenced code blocks. The number is a checkpoint, not a cap.
|
|
27
|
+
- Count rendered lines rather than source lines, wrapping each source line at 80 columns and summing the heights. Source lines undercount a file authored one line per bullet, where a block of fifteen paragraph-bullets occupies fifteen lines and renders past sixty. A checkpoint another standard states counts the same unit, so a file measured one way never sits beside a run measured another.
|
|
28
|
+
- Exempt a block whose lines are all list items at one level averaging under roughly 130 characters. A flat list of short peers is already navigable, and a subheading dropped into it splits a set that belongs together. Bullet count says nothing on its own, since a catalog of one-liners and a stack of paragraphs reach the same count and read nothing alike, so weight is what decides.
|
|
29
|
+
- Mixing prose with that list, or nesting levels inside it, ends the exemption at any weight.
|
|
30
|
+
- Exempt a block whose lines are all table rows, at any length. The peer list above is exempt because it is already navigable, and a table because the remedy does not exist: a subheading dropped inside one splits the table rather than the run, so no edit short of rewriting it as a list clears the checkpoint.
|
|
31
|
+
- Prose either side of the table ends that exemption, since the block has a seam and a heading breaks it there.
|
|
26
32
|
|
|
27
33
|
## Paragraphs and lists
|
|
28
34
|
|
|
29
35
|
- Use prose by default. Reserve bullets for discrete, unrelated items.
|
|
30
36
|
- Keep paragraphs to four sentences or fewer. Split longer blocks at the next logical boundary.
|
|
31
|
-
-
|
|
37
|
+
- Past roughly 400 characters in one paragraph, folding in the lines that wrap it, split at the next logical boundary as well. The sentence cap alone is satisfied by writing fewer and longer sentences, and measured across this corpus 344 paragraphs sit inside four sentences and past this number, so a count on its own passes every one of them.
|
|
38
|
+
- Keep bullets tight. Past roughly 400 characters in one top-level bullet, counting the lines that continue it and excluding any bullet nested under it, the overflow belongs in prose. The number is a checkpoint rather than a cap, and a bullet reading well past it means the number is wrong rather than the rule.
|
|
39
|
+
- Collapse a stack of bullets narrating one subsystem into a single `###` subsection carrying one narrative. Splitting a heavy bullet into three light ones satisfies the checkpoint above and leaves the reader no better off, and subdividing a block does not lighten the bullets inside it, so the two rules answer different defects.
|
|
32
40
|
- Use dashes (`-`) not asterisks (`*`) for bulleted lists
|
|
33
41
|
- Do not end single-sentence or fragment bullets with a period. Use periods when a bullet has two or more sentences.
|
|
34
42
|
- For key path lists, use colon format: `- \`src/\`: description`. Never use an em dash.
|
|
@@ -87,3 +87,4 @@
|
|
|
87
87
|
- From a linked worktree, every `Edit` or `Write` to a tracked file (source, docs) must use a path starting with `pwd`.
|
|
88
88
|
- From a linked worktree, `Edit` and `Write` are refused for every main-root path, session scratch included. The refusal names session isolation and points at the worktree copy, which is a second gitignored file no later session reads, so never take that redirect.
|
|
89
89
|
- `Read` resolves against the main root normally from a linked worktree. A main-root write reaches it only through `Bash`, as one plain command rather than a compound one, which is refused for complexity.
|
|
90
|
+
- Route a main-root write by what it does to the file. Creating a whole file goes out as one plain `Bash` command carrying a heredoc. Changing a line inside a file that already exists goes through an `aitk` verb, which resolves the main root in-process, because the shell route for that case is the stream editor this file bans.
|