@erclx/aitk 0.79.0 → 0.81.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/ci-workflow/REQUIREMENT.md +3 -1
- package/claude/skills/claude-address-review/SKILL.md +8 -3
- package/claude/skills/claude-address-review/references/rebase-conflicts.md +3 -1
- package/claude/skills/claude-autoship/SKILL.md +3 -1
- package/claude/skills/claude-diagram/SKILL.md +3 -1
- package/claude/skills/claude-docs/SKILL.md +17 -4
- package/claude/skills/claude-feature/SKILL.md +6 -2
- package/claude/skills/claude-intake/REQUIREMENT.md +3 -1
- package/claude/skills/claude-memory-review/SKILL.md +6 -2
- package/claude/skills/claude-memory-review/references/receipt-format.md +3 -1
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +6 -2
- package/claude/skills/claude-orchestrate/references/orchestrator-resume.md +3 -1
- package/claude/skills/claude-pr-review/SKILL.md +11 -3
- package/claude/skills/claude-seed-sync/SKILL.md +3 -1
- package/claude/skills/cli-script/REQUIREMENT.md +3 -1
- package/claude/skills/docs-sync/SKILL.md +3 -1
- package/claude/skills/git-followup/REQUIREMENT.md +3 -1
- package/claude/skills/git-followup/SKILL.md +10 -2
- package/claude/skills/git-ship/SKILL.md +3 -1
- package/claude/skills/migration-context/REQUIREMENT.md +3 -1
- package/claude/skills/project-commands/SKILL.md +3 -1
- package/claude/skills/session-resume/SKILL.md +3 -1
- package/claude/skills/setup-indexes/SKILL.md +3 -1
- package/claude/skills/setup-init/SKILL.md +3 -1
- package/claude/skills/toolkit-cli/SKILL.md +1 -1
- package/docs/agents/commands.md +29 -27
- package/docs/agents/comments.md +3 -1
- package/docs/agents/context-audit-checks.md +40 -6
- package/docs/agents/context-audit.md +2 -2
- package/docs/agents/index.md +1 -1
- package/docs/agents/indexes.md +3 -1
- package/docs/agents/install-and-sync.md +12 -6
- package/docs/agents/output-shape.md +3 -1
- package/docs/agents/records.md +45 -1
- package/docs/agents/sandbox.md +3 -1
- package/docs/ai-workflow.md +31 -8
- package/docs/operating-model.md +6 -3
- package/docs/target-projects.md +22 -6
- package/docs/visual-design-workflow.md +13 -3
- package/package.json +1 -1
- package/src/cli.ts +2 -1
- package/src/commands/context.ts +65 -3
- package/src/commands/records.ts +160 -2
- package/src/context/audit.ts +87 -2
- package/src/context/citations.ts +17 -0
- package/src/records/backup.ts +394 -0
- package/standards/context.md +1 -0
- package/standards/prose.md +3 -1
- package/standards/rule.md +3 -1
- package/standards/tasks.md +11 -3
- package/standards/versioning.md +3 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { $ } from 'bun'
|
|
4
|
+
import { gitEnv } from '@/git-env'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The folders a backup carries, relative to `.claude/`. They are the `# Claude`
|
|
8
|
+
* group in `.gitignore` minus `.claude/.tmp`, which is defined as deletable
|
|
9
|
+
* without loss, and `.claude/worktrees/`, whose contents belong to the
|
|
10
|
+
* enclosing repository already. The list is spelled out rather than read off
|
|
11
|
+
* that group so adding an ignore entry cannot silently enlarge the payload.
|
|
12
|
+
*
|
|
13
|
+
* `RECORD_KINDS` in `validate.ts` names four of these. The two lists differ on
|
|
14
|
+
* purpose: one is what a standard governs, this is what a disk loss would take.
|
|
15
|
+
*/
|
|
16
|
+
export const BACKED_FOLDERS = [
|
|
17
|
+
'groundwork',
|
|
18
|
+
'intake',
|
|
19
|
+
'memory',
|
|
20
|
+
'plans',
|
|
21
|
+
'plans-archive',
|
|
22
|
+
'review',
|
|
23
|
+
'task-archive',
|
|
24
|
+
'tasks',
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
/** Holds the records history beside the folders it tracks, ignored by the enclosing repository. */
|
|
28
|
+
const RECORDS_GIT_DIR = join('.claude', '.records.git')
|
|
29
|
+
|
|
30
|
+
const WORK_TREE = '.claude'
|
|
31
|
+
|
|
32
|
+
/** Both directions name the branch, so a machine whose `init.defaultBranch` differs still lands on it. */
|
|
33
|
+
const RECORDS_BRANCH = 'main'
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The records history is machine-written and nobody reads its authorship, so a
|
|
37
|
+
* fixed identity keeps `push` from failing inside a git hook on a machine where
|
|
38
|
+
* `user.email` was never configured.
|
|
39
|
+
*/
|
|
40
|
+
const COMMIT_IDENTITY = ['-c', 'user.name=aitk', '-c', 'user.email=aitk@local']
|
|
41
|
+
|
|
42
|
+
export const BACKUP_REFUSALS = [
|
|
43
|
+
'no-repository',
|
|
44
|
+
'no-remote',
|
|
45
|
+
'remote-unreadable',
|
|
46
|
+
'remote-shared',
|
|
47
|
+
'no-remote-records',
|
|
48
|
+
'local-changes',
|
|
49
|
+
'local-ahead',
|
|
50
|
+
'git-failed',
|
|
51
|
+
] as const
|
|
52
|
+
|
|
53
|
+
export type BackupRefusal = (typeof BACKUP_REFUSALS)[number]
|
|
54
|
+
|
|
55
|
+
export interface BackupRefused {
|
|
56
|
+
readonly ok: false
|
|
57
|
+
readonly reason: BackupRefusal
|
|
58
|
+
readonly message: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PushReport {
|
|
62
|
+
readonly ok: true
|
|
63
|
+
readonly root: string
|
|
64
|
+
readonly folders: readonly string[]
|
|
65
|
+
readonly changed: number
|
|
66
|
+
readonly commit?: string
|
|
67
|
+
readonly pushed: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface PullReport {
|
|
71
|
+
readonly ok: true
|
|
72
|
+
readonly root: string
|
|
73
|
+
readonly folders: readonly string[]
|
|
74
|
+
readonly commit: string
|
|
75
|
+
readonly files: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type PushOutcome = PushReport | BackupRefused
|
|
79
|
+
export type PullOutcome = PullReport | BackupRefused
|
|
80
|
+
|
|
81
|
+
interface GitResult {
|
|
82
|
+
readonly ok: boolean
|
|
83
|
+
readonly text: string
|
|
84
|
+
readonly stderr: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Runs one git command against the records history.
|
|
89
|
+
*
|
|
90
|
+
* Both flags go on every call. `git --git-dir=<path> init` writes
|
|
91
|
+
* `core.bare = true`, and an explicit `--work-tree` is what overrides it, so
|
|
92
|
+
* dropping the flag on a single call reads the enclosing project as the tree
|
|
93
|
+
* and stages everything in it.
|
|
94
|
+
*/
|
|
95
|
+
async function records(root: string, args: string[]): Promise<GitResult> {
|
|
96
|
+
const gitDir = join(root, RECORDS_GIT_DIR)
|
|
97
|
+
const workTree = join(root, WORK_TREE)
|
|
98
|
+
|
|
99
|
+
const result =
|
|
100
|
+
await $`git --git-dir=${gitDir} --work-tree=${workTree} ${args}`
|
|
101
|
+
.env(gitEnv())
|
|
102
|
+
.quiet()
|
|
103
|
+
.nothrow()
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
ok: result.exitCode === 0,
|
|
107
|
+
text: result.stdout.toString().trim(),
|
|
108
|
+
stderr: result.stderr.toString().trim(),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function refuse(reason: BackupRefusal, message: string): BackupRefused {
|
|
113
|
+
return { ok: false, reason, message }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function failed(action: string, result: GitResult): BackupRefused {
|
|
117
|
+
return refuse(
|
|
118
|
+
'git-failed',
|
|
119
|
+
`git ${action} failed against the records history: ${result.stderr || 'no output'}.`,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reduces a remote URL to `host/path`, so every spelling of one repository
|
|
125
|
+
* compares equal.
|
|
126
|
+
*
|
|
127
|
+
* Transport is what the reduction drops. `git@github.com:owner/repo.git` and
|
|
128
|
+
* `https://github.com/owner/repo` name the same repository, and comparing them
|
|
129
|
+
* as written passes a records origin that publishes the payload through the
|
|
130
|
+
* other protocol.
|
|
131
|
+
*/
|
|
132
|
+
function remoteIdentity(url: string): string {
|
|
133
|
+
return url
|
|
134
|
+
.trim()
|
|
135
|
+
.toLowerCase()
|
|
136
|
+
.replace(/^[a-z+]+:\/\//, '')
|
|
137
|
+
.replace(/^[^@/]+@/, '')
|
|
138
|
+
.replace(/^([^/:]+):/, '$1/')
|
|
139
|
+
.replace(/\/+$/, '')
|
|
140
|
+
.replace(/\.git$/, '')
|
|
141
|
+
.replace(/\/+$/, '')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Lists every remote of the enclosing project, or undefined when git cannot
|
|
146
|
+
* answer.
|
|
147
|
+
*
|
|
148
|
+
* The caller refuses on undefined rather than smoothing it into an empty list.
|
|
149
|
+
* An empty list clears the gate below for every URL, so a git that failed for
|
|
150
|
+
* any reason would publish the payload to whatever origin the records history
|
|
151
|
+
* happens to name. A project with no remotes answers `0` with an exit of zero,
|
|
152
|
+
* so the two states stay distinguishable.
|
|
153
|
+
*/
|
|
154
|
+
async function enclosingRemoteUrls(
|
|
155
|
+
root: string,
|
|
156
|
+
): Promise<string[] | undefined> {
|
|
157
|
+
const result = await $`git -C ${root} remote -v`
|
|
158
|
+
.env(gitEnv())
|
|
159
|
+
.quiet()
|
|
160
|
+
.nothrow()
|
|
161
|
+
if (result.exitCode !== 0) return undefined
|
|
162
|
+
|
|
163
|
+
return result.stdout
|
|
164
|
+
.toString()
|
|
165
|
+
.split('\n')
|
|
166
|
+
.filter(Boolean)
|
|
167
|
+
.map((line) => line.split(/\s+/)[1] ?? '')
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.map(remoteIdentity)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Clears the four gates both verbs share and returns the records remote URL.
|
|
174
|
+
*
|
|
175
|
+
* The last two are the ones the payload depends on. This repository is public,
|
|
176
|
+
* so a records branch on any of its remotes serves the memory pen and the
|
|
177
|
+
* groundwork trails to anyone who fetches all refs. Comparing the configured
|
|
178
|
+
* URL against every remote of the enclosing repository is what keeps a
|
|
179
|
+
* misconfigured `origin` from publishing them, and refusing when that list
|
|
180
|
+
* cannot be read is what keeps a failed comparison from reading as a pass.
|
|
181
|
+
*/
|
|
182
|
+
async function resolveRemote(root: string): Promise<string | BackupRefused> {
|
|
183
|
+
if (!existsSync(join(root, RECORDS_GIT_DIR))) {
|
|
184
|
+
return refuse(
|
|
185
|
+
'no-repository',
|
|
186
|
+
[
|
|
187
|
+
`No records history at ${RECORDS_GIT_DIR}. Create it once, against a private repository:`,
|
|
188
|
+
` git --git-dir=${join(root, RECORDS_GIT_DIR)} init`,
|
|
189
|
+
` git --git-dir=${join(root, RECORDS_GIT_DIR)} remote add origin <private-repo-url>`,
|
|
190
|
+
].join('\n'),
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const remote = await records(root, ['remote', 'get-url', 'origin'])
|
|
195
|
+
if (!remote.ok || remote.text.length === 0) {
|
|
196
|
+
return refuse(
|
|
197
|
+
'no-remote',
|
|
198
|
+
[
|
|
199
|
+
'The records history has no origin. Point it at a private repository:',
|
|
200
|
+
` git --git-dir=${join(root, RECORDS_GIT_DIR)} remote add origin <private-repo-url>`,
|
|
201
|
+
].join('\n'),
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const enclosing = await enclosingRemoteUrls(root)
|
|
206
|
+
if (!enclosing) {
|
|
207
|
+
return refuse(
|
|
208
|
+
'remote-unreadable',
|
|
209
|
+
`Cannot read the remotes of the project at ${root}, so the records origin cannot be checked against them. Records carry the memory pen and the groundwork trails, and an unchecked origin risks publishing them.`,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const url = remoteIdentity(remote.text)
|
|
214
|
+
if (enclosing.includes(url)) {
|
|
215
|
+
return refuse(
|
|
216
|
+
'remote-shared',
|
|
217
|
+
`The records origin ${remote.text} is a remote of this project. Records carry the memory pen and the groundwork trails, so they need a repository of their own.`,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return url
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The subset of the eight a pathspec can name: on disk, or already in the
|
|
226
|
+
* records index.
|
|
227
|
+
*
|
|
228
|
+
* A pathspec matching neither fails the whole `add`, which is why the subset
|
|
229
|
+
* exists. The index half is what covers a folder deleted in full. Reading disk
|
|
230
|
+
* alone drops it from the pathspec, so its deletion never stages, the remote
|
|
231
|
+
* keeps it forever, and a later `pull` restores it past the gate that refuses
|
|
232
|
+
* every other unpushed deletion.
|
|
233
|
+
*/
|
|
234
|
+
async function scopedFolders(root: string): Promise<string[]> {
|
|
235
|
+
const tracked = await records(root, ['ls-files'])
|
|
236
|
+
const indexed = new Set(
|
|
237
|
+
tracked.ok ? tracked.text.split('\n').filter(Boolean).map(topSegment) : [],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return BACKED_FOLDERS.filter(
|
|
241
|
+
(folder) =>
|
|
242
|
+
existsSync(join(root, WORK_TREE, folder)) || indexed.has(folder),
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function topSegment(path: string): string {
|
|
247
|
+
return path.split('/')[0]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** What a report names, which is the folders a reader can go and open. */
|
|
251
|
+
function presentFolders(root: string): string[] {
|
|
252
|
+
return BACKED_FOLDERS.filter((folder) =>
|
|
253
|
+
existsSync(join(root, WORK_TREE, folder)),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function countLines(text: string): number {
|
|
258
|
+
return text.split('\n').filter(Boolean).length
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Stages the backed folders, commits when any of them changed, and pushes.
|
|
263
|
+
*
|
|
264
|
+
* The push runs whether or not this call committed, because a previous run can
|
|
265
|
+
* have committed and then failed to reach the network. Skipping it would leave
|
|
266
|
+
* that commit on one disk, which is the state the whole verb exists to end.
|
|
267
|
+
*/
|
|
268
|
+
export async function pushRecords(root: string): Promise<PushOutcome> {
|
|
269
|
+
const remote = await resolveRemote(root)
|
|
270
|
+
if (typeof remote !== 'string') return remote
|
|
271
|
+
|
|
272
|
+
const scope = await scopedFolders(root)
|
|
273
|
+
|
|
274
|
+
if (scope.length > 0) {
|
|
275
|
+
// `-f` is what carries the payload: every backed folder is ignored by the
|
|
276
|
+
// enclosing repository, and the pathspecs are the whole list, so nothing
|
|
277
|
+
// outside them can enter the index however the ignore rules read.
|
|
278
|
+
const staged = await records(root, ['add', '-A', '-f', '--', ...scope])
|
|
279
|
+
if (!staged.ok) return failed('add', staged)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const diff = await records(root, [
|
|
283
|
+
'diff',
|
|
284
|
+
'--cached',
|
|
285
|
+
'--name-only',
|
|
286
|
+
'--',
|
|
287
|
+
...scope,
|
|
288
|
+
])
|
|
289
|
+
if (!diff.ok) return failed('diff', diff)
|
|
290
|
+
|
|
291
|
+
const changed = countLines(diff.text)
|
|
292
|
+
|
|
293
|
+
if (changed > 0) {
|
|
294
|
+
const stamp = new Date().toISOString().replace('T', ' ').slice(0, 16)
|
|
295
|
+
const commit = await records(root, [
|
|
296
|
+
...COMMIT_IDENTITY,
|
|
297
|
+
'commit',
|
|
298
|
+
'--quiet',
|
|
299
|
+
'-m',
|
|
300
|
+
`records: ${changed} changed at ${stamp}`,
|
|
301
|
+
])
|
|
302
|
+
if (!commit.ok) return failed('commit', commit)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const folders = presentFolders(root)
|
|
306
|
+
const head = await records(root, ['rev-parse', '--short', 'HEAD'])
|
|
307
|
+
if (!head.ok) {
|
|
308
|
+
return { ok: true, root, folders, changed, pushed: false }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const pushed = await records(root, [
|
|
312
|
+
'push',
|
|
313
|
+
'origin',
|
|
314
|
+
`HEAD:refs/heads/${RECORDS_BRANCH}`,
|
|
315
|
+
])
|
|
316
|
+
if (!pushed.ok) return failed('push', pushed)
|
|
317
|
+
|
|
318
|
+
return { ok: true, root, folders, changed, commit: head.text, pushed: true }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Fetches the records history and writes it into the backed folders.
|
|
323
|
+
*
|
|
324
|
+
* The two directions are not symmetric. A push only ever adds, while a pull
|
|
325
|
+
* onto a machine holding work that never reached the remote would discard it,
|
|
326
|
+
* so both gates below refuse rather than choosing a merge strategy. A person
|
|
327
|
+
* resolves by pushing first or by moving the local folders aside.
|
|
328
|
+
*/
|
|
329
|
+
export async function pullRecords(root: string): Promise<PullOutcome> {
|
|
330
|
+
const remote = await resolveRemote(root)
|
|
331
|
+
if (typeof remote !== 'string') return remote
|
|
332
|
+
|
|
333
|
+
const fetched = await records(root, [
|
|
334
|
+
'fetch',
|
|
335
|
+
'--quiet',
|
|
336
|
+
'origin',
|
|
337
|
+
`refs/heads/${RECORDS_BRANCH}`,
|
|
338
|
+
])
|
|
339
|
+
if (!fetched.ok) {
|
|
340
|
+
// A missing branch and an unreachable remote both fail the fetch, and only
|
|
341
|
+
// the first is an ordinary state a person resolves by pushing once.
|
|
342
|
+
if (fetched.stderr.includes("couldn't find remote ref")) {
|
|
343
|
+
return refuse(
|
|
344
|
+
'no-remote-records',
|
|
345
|
+
`The records origin carries no ${RECORDS_BRANCH} branch yet. Run aitk records push from the machine holding the records.`,
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
return failed('fetch', fetched)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const target = await records(root, ['rev-parse', 'FETCH_HEAD'])
|
|
352
|
+
if (!target.ok) return failed('rev-parse', target)
|
|
353
|
+
|
|
354
|
+
const scope = await scopedFolders(root)
|
|
355
|
+
|
|
356
|
+
if (scope.length > 0) {
|
|
357
|
+
const dirty = await records(root, ['status', '--porcelain', '--', ...scope])
|
|
358
|
+
if (!dirty.ok) return failed('status', dirty)
|
|
359
|
+
|
|
360
|
+
if (dirty.text.length > 0) {
|
|
361
|
+
return refuse(
|
|
362
|
+
'local-changes',
|
|
363
|
+
`${countLines(dirty.text)} local record(s) are not in the records history. Run aitk records push first, or move them aside.`,
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const head = await records(root, ['rev-parse', '--verify', '--quiet', 'HEAD'])
|
|
369
|
+
if (head.ok && head.text.length > 0) {
|
|
370
|
+
const ahead = await records(root, ['rev-list', `${target.text}..HEAD`])
|
|
371
|
+
if (!ahead.ok) return failed('rev-list', ahead)
|
|
372
|
+
|
|
373
|
+
if (ahead.text.length > 0) {
|
|
374
|
+
return refuse(
|
|
375
|
+
'local-ahead',
|
|
376
|
+
`${countLines(ahead.text)} local commit(s) have not reached the records origin. Run aitk records push first.`,
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const reset = await records(root, ['reset', '--hard', '--quiet', target.text])
|
|
382
|
+
if (!reset.ok) return failed('reset', reset)
|
|
383
|
+
|
|
384
|
+
const files = await records(root, ['ls-files'])
|
|
385
|
+
if (!files.ok) return failed('ls-files', files)
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
root,
|
|
390
|
+
folders: presentFolders(root),
|
|
391
|
+
commit: target.text.slice(0, 7),
|
|
392
|
+
files: countLines(files.text),
|
|
393
|
+
}
|
|
394
|
+
}
|
package/standards/context.md
CHANGED
|
@@ -83,6 +83,7 @@ Only the `development` entry carries this section. It is not a general-purpose h
|
|
|
83
83
|
- Decisions specific to the domain. Broader cross-domain decisions belong in `.claude/ARCHITECTURE.md`.
|
|
84
84
|
- Constraints, gotchas, things tried and rejected
|
|
85
85
|
- Domain-specific conventions that do not fit a `paths:`-scoped rule
|
|
86
|
+
- A reference to another entry, spelled as the path that entry sits at rather than as its bare filename. A bare name resolves against whichever folder the reader is already in, so a domain that splits into subfolders strands every inbound reference and the break surfaces nowhere. A reference to a seed, a standard, or a file the project owns elsewhere keeps the form its own surface uses.
|
|
86
87
|
|
|
87
88
|
## What does not go in
|
|
88
89
|
|
package/standards/prose.md
CHANGED
|
@@ -5,7 +5,9 @@ description: Voice, language, and frontmatter wording for reference markdown
|
|
|
5
5
|
|
|
6
6
|
# Prose reference
|
|
7
7
|
|
|
8
|
-
Applies to markdown reference docs, READMEs, and inline documentation in repos. It is the default voice for `.md` files and yields to any surface with its own voice, such as blogs, emails, changelogs, or commit messages. It also yields wherever another standard states the voice for the surface it governs, which is how a surface claims the exemption without this file having to name it.
|
|
8
|
+
Applies to markdown reference docs, READMEs, and inline documentation in repos. It is the default voice for `.md` files and yields to any surface with its own voice, such as blogs, emails, changelogs, or commit messages. It also yields wherever another standard states the voice for the surface it governs, which is how a surface claims the exemption without this file having to name it.
|
|
9
|
+
|
|
10
|
+
The yield covers voice alone. The language rules below stay in force on every surface, including the surfaces no automated check reaches, as do the mechanics in `markdown.md`.
|
|
9
11
|
|
|
10
12
|
## Scope
|
|
11
13
|
|
package/standards/rule.md
CHANGED
|
@@ -7,7 +7,9 @@ description: Rule frontmatter, body shape, and voice for .claude/rules files
|
|
|
7
7
|
|
|
8
8
|
## Overview
|
|
9
9
|
|
|
10
|
-
Rules give Claude Code coding constraints scoped to file paths. Claude Code discovers `.claude/rules/**/*.md` at session start.
|
|
10
|
+
Rules give Claude Code coding constraints scoped to file paths. Claude Code discovers `.claude/rules/**/*.md` at session start.
|
|
11
|
+
|
|
12
|
+
A rule with no `paths:` field always applies, at the same priority as `CLAUDE.md`. A rule with `paths:` applies when Claude reads a file matching the glob. Author one rule per topic so the scope stays precise.
|
|
11
13
|
|
|
12
14
|
## Scope
|
|
13
15
|
|
package/standards/tasks.md
CHANGED
|
@@ -5,7 +5,9 @@ description: Folder layout, filename convention, readiness groups, and content r
|
|
|
5
5
|
|
|
6
6
|
# Tasks reference
|
|
7
7
|
|
|
8
|
-
Applies to `.claude/tasks/`. Tracks what is being built and why, at the level of features and outcomes. One file per task.
|
|
8
|
+
Applies to `.claude/tasks/`. Tracks what is being built and why, at the level of features and outcomes. One file per task.
|
|
9
|
+
|
|
10
|
+
Update when a task starts, completes, or changes scope. When to open a task at all is project policy, not a shape rule, and lives in `CLAUDE.md`.
|
|
9
11
|
|
|
10
12
|
The folder is gitignored. Board state changes when work ships rather than when a branch is written, so committing it would put a claim about the future into the diff of an unrelated pull request. The git log records what shipped.
|
|
11
13
|
|
|
@@ -108,7 +110,9 @@ description: Record what a target installed and report the delta against the too
|
|
|
108
110
|
|
|
109
111
|
## File format
|
|
110
112
|
|
|
111
|
-
Two headings, `## Outcomes` and `## Findings`. Outcomes are future and checkable, findings are past and factual, and as flat bullets at the same indent they are visually identical. A heading separates them at no cost.
|
|
113
|
+
Two headings, `## Outcomes` and `## Findings`. Outcomes are future and checkable, findings are past and factual, and as flat bullets at the same indent they are visually identical. A heading separates them at no cost.
|
|
114
|
+
|
|
115
|
+
Add no third heading. Status stays inline on an outcome rather than becoming an "In progress" section.
|
|
112
116
|
|
|
113
117
|
Size the outcomes so one pull request closes all of them. A task whose outcomes span two pull requests ships the first half and leaves the rest open, with nothing recording which outcomes the merged work covered, so the board reads as in-progress work that no branch is carrying. Split the task before handing it off rather than after. This is what `## Archiving` below depends on, since a task closes whole or not at all.
|
|
114
118
|
|
|
@@ -156,7 +160,11 @@ A task with no origin is either lost context or work nobody decided to do. The i
|
|
|
156
160
|
|
|
157
161
|
Phase-label format and where labels may appear are governed by `standards/versioning.md`.
|
|
158
162
|
|
|
159
|
-
`Plan:` points at `../plans/feature-<slug>.md` while the task is open. Once the task ships and the plan is archived, it points at `../plans-archive/feature-<slug>.md`. Retarget both halves of the link rather than dropping it, so a completed task still leads to the reasoning behind it.
|
|
163
|
+
`Plan:` points at `../plans/feature-<slug>.md` while the task is open. Once the task ships and the plan is archived, it points at `../plans-archive/feature-<slug>.md`. Retarget both halves of the link rather than dropping it, so a completed task still leads to the reasoning behind it.
|
|
164
|
+
|
|
165
|
+
A project that archived plans before the folder moved out of `.claude/.tmp/` holds closed tasks pointing at `../.tmp/plans-archive/`, and both forms resolve against the files each names, so leave those pointers where they are. Nothing migrates them, and a task retargeted without its plan moving leads nowhere.
|
|
166
|
+
|
|
167
|
+
One plan per task. A plan cited by two tasks is a misfile rather than a shape to design for, which is why the sweep counts citations before archiving: the count is a guard against the misfile stranding a pointer, not support for the shape.
|
|
160
168
|
|
|
161
169
|
`Groundwork:` points at `../groundwork/<slug>/`, the folder `claude-groundwork` fills. It names the surface it points at the way `Plan:` does. Use this key alone. `Research record` and `Decision record` are earlier spellings of the same thing and both convert to it.
|
|
162
170
|
|
package/standards/versioning.md
CHANGED
|
@@ -66,4 +66,6 @@ The surface publishing the text is the last gate on it. Where no automated check
|
|
|
66
66
|
|
|
67
67
|
## Why
|
|
68
68
|
|
|
69
|
-
Phase labels keep planning conversations efficient. They make `git log`, PR titles, and the tag list unreadable when they leak in. A future reader cannot reconstruct what an internal label meant without the matching task file, which is gitignored.
|
|
69
|
+
Phase labels keep planning conversations efficient. They make `git log`, PR titles, and the tag list unreadable when they leak in. A future reader cannot reconstruct what an internal label meant without the matching task file, which is gitignored.
|
|
70
|
+
|
|
71
|
+
Semver tags carry meaning independent of conversation state and survive in git history. Keeping the two namespaces apart preserves both.
|