@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
package/src/commands/tasks.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
523
|
+
reason: 'bad-input',
|
|
229
524
|
message: `Not a pull request number: ${opts.pullRequest}`,
|
|
230
525
|
detail: [],
|
|
231
526
|
},
|