@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.
Files changed (39) hide show
  1. package/claude/.claude-plugin/plugin.json +1 -1
  2. package/claude/skills/claude-docs/SKILL.md +3 -1
  3. package/claude/skills/claude-feature/SKILL.md +2 -0
  4. package/claude/skills/claude-memory-capture/SKILL.md +9 -2
  5. package/claude/skills/claude-memory-review/SKILL.md +5 -2
  6. package/claude/skills/claude-review/SKILL.md +2 -0
  7. package/claude/skills/claude-screencast/SKILL.md +2 -0
  8. package/claude/skills/claude-seed-sync/SKILL.md +2 -0
  9. package/claude/skills/claude-tasks/SKILL.md +3 -1
  10. package/claude/skills/claude-ui-test/SKILL.md +2 -0
  11. package/claude/skills/claude-ux-audit/SKILL.md +2 -0
  12. package/claude/skills/git-pr/SKILL.md +9 -3
  13. package/docs/agents/commands.md +5 -1
  14. package/docs/agents/context-audit-checks.md +9 -11
  15. package/docs/agents/context-audit.md +3 -1
  16. package/docs/agents/index.md +3 -2
  17. package/docs/agents/markdown-audit.md +70 -0
  18. package/docs/agents/tasks.md +51 -2
  19. package/docs/ai-workflow.md +1 -1
  20. package/package.json +1 -1
  21. package/src/cli.ts +4 -0
  22. package/src/commands/context.ts +8 -115
  23. package/src/commands/markdown.ts +383 -0
  24. package/src/commands/records.ts +1 -18
  25. package/src/commands/tasks.ts +314 -19
  26. package/src/context/audit.ts +34 -309
  27. package/src/context/citations.ts +2 -30
  28. package/src/git-files.ts +31 -0
  29. package/src/markdown/bans.ts +241 -0
  30. package/src/markdown/files.ts +92 -0
  31. package/src/markdown/scan.ts +183 -0
  32. package/src/markdown/structure.ts +408 -0
  33. package/src/records/validate.ts +1 -37
  34. package/src/tasks/archive.ts +34 -3
  35. package/src/tasks/record.ts +311 -0
  36. package/src/worktree.ts +23 -0
  37. package/standards/context.md +2 -7
  38. package/standards/markdown.md +10 -2
  39. package/tooling/claude/seeds/CLAUDE.md +1 -0
@@ -1,7 +1,14 @@
1
1
  import { relative } from 'node:path'
2
- import { $ } from 'bun'
3
2
  import type { Command } from 'commander'
4
3
  import { type ArchiveOutcome, archiveTask } from '@/tasks/archive'
4
+ import {
5
+ type CloseOutcome,
6
+ closeOutcomes,
7
+ type PullRequestOutcome,
8
+ type RecordRefused,
9
+ type RecordSelector,
10
+ recordPullRequest,
11
+ } from '@/tasks/record'
5
12
  import {
6
13
  type Finding,
7
14
  type ValidateOutcome,
@@ -18,6 +25,7 @@ import {
18
25
  outro,
19
26
  pipeOutput,
20
27
  } from '@/ui'
28
+ import { mainWorktreeRoot } from '@/worktree'
21
29
 
22
30
  /** Returned when the board carries a finding, which is the gating result. */
23
31
  const EXIT_FINDINGS = 2
@@ -33,22 +41,17 @@ interface ValidateCommandOptions {
33
41
  readonly root?: string
34
42
  }
35
43
 
36
- /**
37
- * The board is shared scratch at the main worktree root, and `git worktree
38
- * list` puts that root first. A pull inside a linked worktree fires the same
39
- * hook, so trusting the working directory would write a second board nothing
40
- * else reads.
41
- */
42
- async function mainWorktreeRoot(): Promise<string> {
43
- const result = await $`git worktree list --porcelain`.quiet().nothrow()
44
- if (result.exitCode !== 0) return process.cwd()
45
-
46
- const line = result.stdout
47
- .toString()
48
- .split('\n')
49
- .find((entry) => entry.startsWith('worktree '))
44
+ interface PullRequestCommandOptions {
45
+ readonly json?: boolean
46
+ readonly plan?: string
47
+ readonly root?: string
48
+ }
50
49
 
51
- return line ? line.slice('worktree '.length).trim() : process.cwd()
50
+ interface OutcomeCommandOptions {
51
+ readonly close?: readonly string[]
52
+ readonly json?: boolean
53
+ readonly plan?: string
54
+ readonly root?: string
52
55
  }
53
56
 
54
57
  export function register(program: Command): void {
@@ -121,6 +124,298 @@ export function register(program: Command): void {
121
124
  .action(async (opts: ValidateCommandOptions) => {
122
125
  process.exitCode = await runValidate(opts)
123
126
  })
127
+
128
+ tasks
129
+ .command('pull-request')
130
+ .description('Record a pull request number on the task a branch closes')
131
+ .argument('<number>', 'Pull request number, without the #')
132
+ .argument('[task]', 'Task filename stem, as in v28.1-trigger-escalation')
133
+ .helpOption('-h, --help', 'Show this help message')
134
+ .option('--plan <slug>', 'Select the task whose Plan line names this plan')
135
+ .option('--json', 'Emit a machine-readable record on stdout')
136
+ .option('--root <path>', 'Board root, defaulting to the main worktree')
137
+ .addHelpText(
138
+ 'after',
139
+ [
140
+ '',
141
+ 'Exit codes:',
142
+ ' 0 the line was added, corrected, or already correct',
143
+ ' 1 refused, with the reason on stderr or in the JSON record',
144
+ '',
145
+ 'It adds Pull request: #NNN under the origin lines the task carries,',
146
+ 'and corrects the number in place when the line exists. The board is',
147
+ 'shared scratch, so a linked worktree records against the same board.',
148
+ '',
149
+ 'Examples:',
150
+ ' aitk tasks pull-request 673 v28.1-trigger-escalation',
151
+ ' aitk tasks pull-request 673 --plan worktree-scratch-routing --json',
152
+ '',
153
+ ].join('\n'),
154
+ )
155
+ .action(
156
+ async (
157
+ number: string,
158
+ task: string | undefined,
159
+ opts: PullRequestCommandOptions,
160
+ ) => {
161
+ process.exitCode = await runPullRequest(number, task, opts)
162
+ },
163
+ )
164
+
165
+ tasks
166
+ .command('outcome')
167
+ .description('Mark outcomes closed on a task by their position')
168
+ .argument('[task]', 'Task filename stem, as in v28.1-trigger-escalation')
169
+ .helpOption('-h, --help', 'Show this help message')
170
+ .option(
171
+ '--close <position>',
172
+ 'Outcome to mark [x], 1-based, repeatable',
173
+ collectPosition,
174
+ [] as string[],
175
+ )
176
+ .option('--plan <slug>', 'Select the task whose Plan line names this plan')
177
+ .option('--json', 'Emit a machine-readable record on stdout')
178
+ .option('--root <path>', 'Board root, defaulting to the main worktree')
179
+ .addHelpText(
180
+ 'after',
181
+ [
182
+ '',
183
+ 'Exit codes:',
184
+ ' 0 every named outcome is closed',
185
+ ' 1 refused, with the reason on stderr or in the JSON record',
186
+ '',
187
+ 'Positions count every outcome checkbox in file order, starting at 1.',
188
+ 'An outcome already closed is reported rather than refused, so a rerun',
189
+ 'against the same positions is safe.',
190
+ '',
191
+ 'Examples:',
192
+ ' aitk tasks outcome v28.1-trigger-escalation --close 1 --close 3',
193
+ ' aitk tasks outcome --plan worktree-scratch-routing --close 2 --json',
194
+ '',
195
+ ].join('\n'),
196
+ )
197
+ .action(async (task: string | undefined, opts: OutcomeCommandOptions) => {
198
+ process.exitCode = await runOutcome(task, opts)
199
+ })
200
+ }
201
+
202
+ function collectPosition(value: string, previous: string[]): string[] {
203
+ return [...previous, value]
204
+ }
205
+
206
+ /**
207
+ * Both record verbs name a task the same two ways, and naming it neither way or
208
+ * both ways is the same refusal in each. Both are `bad-input` rather than
209
+ * `ambiguous` or `no-match`, since those two describe the board and these
210
+ * describe the command line that reached it.
211
+ */
212
+ function selectorFor(
213
+ task: string | undefined,
214
+ plan: string | undefined,
215
+ ): RecordSelector | RecordRefused {
216
+ if (task && plan) {
217
+ return {
218
+ ok: false,
219
+ reason: 'bad-input',
220
+ message: 'Name a task or a plan, not both.',
221
+ detail: [],
222
+ }
223
+ }
224
+
225
+ if (task) return { kind: 'stem', stem: task }
226
+ if (plan) return { kind: 'plan', plan }
227
+
228
+ return {
229
+ ok: false,
230
+ reason: 'bad-input',
231
+ message: 'No task named. Pass a filename stem or --plan <slug>.',
232
+ detail: [],
233
+ }
234
+ }
235
+
236
+ async function runPullRequest(
237
+ number: string,
238
+ task: string | undefined,
239
+ opts: PullRequestCommandOptions,
240
+ ): Promise<number> {
241
+ const emitJson = opts.json ?? false
242
+ const selector = selectorFor(task, opts.plan)
243
+
244
+ if ('ok' in selector) {
245
+ return reportRecord(
246
+ 'aitk tasks pull-request',
247
+ selector,
248
+ emitJson,
249
+ process.cwd(),
250
+ )
251
+ }
252
+
253
+ if (!/^\d+$/.test(number)) {
254
+ return reportRecord(
255
+ 'aitk tasks pull-request',
256
+ {
257
+ ok: false,
258
+ reason: 'bad-input',
259
+ message: `Not a pull request number: ${number}`,
260
+ detail: [],
261
+ },
262
+ emitJson,
263
+ process.cwd(),
264
+ )
265
+ }
266
+
267
+ const root = opts.root ?? (await mainWorktreeRoot())
268
+ const outcome = await recordPullRequest(root, selector, Number(number))
269
+
270
+ return reportPullRequest(outcome, emitJson, root)
271
+ }
272
+
273
+ async function runOutcome(
274
+ task: string | undefined,
275
+ opts: OutcomeCommandOptions,
276
+ ): Promise<number> {
277
+ const emitJson = opts.json ?? false
278
+ const selector = selectorFor(task, opts.plan)
279
+
280
+ if ('ok' in selector) {
281
+ return reportRecord('aitk tasks outcome', selector, emitJson, process.cwd())
282
+ }
283
+
284
+ const raw = opts.close ?? []
285
+
286
+ if (raw.length === 0) {
287
+ return reportRecord(
288
+ 'aitk tasks outcome',
289
+ {
290
+ ok: false,
291
+ reason: 'bad-input',
292
+ message: 'No outcome named. Pass --close <position>.',
293
+ detail: [],
294
+ },
295
+ emitJson,
296
+ process.cwd(),
297
+ )
298
+ }
299
+
300
+ const invalid = raw.filter((value) => !/^\d+$/.test(value))
301
+
302
+ if (invalid.length > 0) {
303
+ return reportRecord(
304
+ 'aitk tasks outcome',
305
+ {
306
+ ok: false,
307
+ reason: 'bad-input',
308
+ message: `Not an outcome position: ${invalid.join(', ')}`,
309
+ detail: invalid,
310
+ },
311
+ emitJson,
312
+ process.cwd(),
313
+ )
314
+ }
315
+
316
+ const root = opts.root ?? (await mainWorktreeRoot())
317
+ const outcome = await closeOutcomes(root, selector, raw.map(Number))
318
+
319
+ return reportOutcome(outcome, emitJson, root)
320
+ }
321
+
322
+ function reportRecord(
323
+ title: string,
324
+ refused: RecordRefused,
325
+ emitJson: boolean,
326
+ root: string,
327
+ ): number {
328
+ // The framed branch below already reaches stderr through logError, so the
329
+ // bare write is what keeps the JSON mode from reporting the reason on stdout
330
+ // alone.
331
+ if (emitJson) {
332
+ process.stderr.write(`${refused.message}\n`)
333
+ process.stdout.write(
334
+ `${JSON.stringify({
335
+ ok: false,
336
+ root,
337
+ reason: refused.reason,
338
+ message: refused.message,
339
+ detail: refused.detail,
340
+ })}\n`,
341
+ )
342
+ return 1
343
+ }
344
+
345
+ intro(title)
346
+ logStep('Refused')
347
+ logError(refused.message)
348
+ if (refused.detail.length > 0) pipeOutput(refused.detail.join('\n'))
349
+ outro()
350
+
351
+ return 1
352
+ }
353
+
354
+ function reportPullRequest(
355
+ outcome: PullRequestOutcome,
356
+ emitJson: boolean,
357
+ root: string,
358
+ ): number {
359
+ if (!outcome.ok) {
360
+ return reportRecord('aitk tasks pull-request', outcome, emitJson, root)
361
+ }
362
+
363
+ if (emitJson) {
364
+ process.stdout.write(
365
+ `${JSON.stringify({
366
+ ok: true,
367
+ root,
368
+ task: outcome.stem,
369
+ path: relative(root, outcome.path),
370
+ pullRequest: outcome.number,
371
+ action: outcome.action,
372
+ })}\n`,
373
+ )
374
+ return 0
375
+ }
376
+
377
+ intro('aitk tasks pull-request')
378
+ logStep(outcome.action === 'unchanged' ? 'Already recorded' : 'Recorded')
379
+ logInfo(`${outcome.stem} names pull request #${outcome.number}`)
380
+ if (outcome.action !== 'unchanged') logAdd(relative(root, outcome.path))
381
+ outro()
382
+
383
+ return 0
384
+ }
385
+
386
+ function reportOutcome(
387
+ outcome: CloseOutcome,
388
+ emitJson: boolean,
389
+ root: string,
390
+ ): number {
391
+ if (!outcome.ok) {
392
+ return reportRecord('aitk tasks outcome', outcome, emitJson, root)
393
+ }
394
+
395
+ if (emitJson) {
396
+ process.stdout.write(
397
+ `${JSON.stringify({
398
+ ok: true,
399
+ root,
400
+ task: outcome.stem,
401
+ path: relative(root, outcome.path),
402
+ closed: outcome.closed,
403
+ alreadyClosed: outcome.alreadyClosed,
404
+ })}\n`,
405
+ )
406
+ return 0
407
+ }
408
+
409
+ intro('aitk tasks outcome')
410
+ logStep(outcome.closed.length > 0 ? 'Closed' : 'Nothing to close')
411
+
412
+ for (const closed of outcome.closed) logAdd(closed)
413
+ for (const already of outcome.alreadyClosed) logInfo(`${already} (already)`)
414
+
415
+ if (outcome.closed.length > 0) logInfo(relative(root, outcome.path))
416
+ outro()
417
+
418
+ return 0
124
419
  }
125
420
 
126
421
  async function runValidate(opts: ValidateCommandOptions): Promise<number> {
@@ -196,7 +491,7 @@ async function runArchive(
196
491
  return report(
197
492
  {
198
493
  ok: false,
199
- reason: 'ambiguous',
494
+ reason: 'bad-input',
200
495
  message: 'Name a task or a pull request, not both.',
201
496
  detail: [],
202
497
  },
@@ -209,7 +504,7 @@ async function runArchive(
209
504
  return report(
210
505
  {
211
506
  ok: false,
212
- reason: 'no-match',
507
+ reason: 'bad-input',
213
508
  message:
214
509
  'No task named. Pass a filename stem or --pull-request <number>.',
215
510
  detail: [],
@@ -225,7 +520,7 @@ async function runArchive(
225
520
  return report(
226
521
  {
227
522
  ok: false,
228
- reason: 'no-match',
523
+ reason: 'bad-input',
229
524
  message: `Not a pull request number: ${opts.pullRequest}`,
230
525
  detail: [],
231
526
  },