@erclx/aitk 0.63.0 → 0.63.2
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 +2 -0
- package/docs/agents/context-audit-checks.md +3 -3
- package/docs/agents/index.md +1 -1
- package/docs/agents/tasks.md +51 -2
- package/docs/ai-workflow.md +1 -1
- package/package.json +1 -1
- package/src/commands/context.ts +3 -14
- package/src/commands/records.ts +1 -18
- package/src/commands/tasks.ts +314 -19
- package/src/context/audit.ts +23 -21
- 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 +8 -1
- package/tooling/claude/seeds/CLAUDE.md +4 -1
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
|
},
|
package/src/context/audit.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { relative } from 'node:path'
|
|
|
3
3
|
import type { AuditedFolder } from '@/context/folders'
|
|
4
4
|
import { isStubSeed } from '@/seed-marker'
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Checkpoints quoted from the standard stating each. Neither is a cap.
|
|
8
|
+
*
|
|
9
|
+
* Entry length rests on one entry per domain, which is a domain fact, so
|
|
10
|
+
* `standards/context.md` keeps it. Run depth reads the same over any markdown
|
|
11
|
+
* file, so `standards/markdown.md` states it at the attribute tier.
|
|
12
|
+
*/
|
|
7
13
|
export const LENGTH_CHECKPOINT = 150
|
|
8
14
|
export const RUN_CHECKPOINT = 40
|
|
9
15
|
|
|
@@ -29,7 +35,7 @@ export const RENDER_WIDTH = 80
|
|
|
29
35
|
export const PEER_BULLET_CHECKPOINT = 130
|
|
30
36
|
|
|
31
37
|
/**
|
|
32
|
-
* Characters a bullet carries before
|
|
38
|
+
* Characters a top-level bullet carries before its overflow belongs in prose.
|
|
33
39
|
*
|
|
34
40
|
* Unlike the peer-list checkpoint above, this corpus has no gap behind the
|
|
35
41
|
* number. Bullet weight decays smoothly from a median near 170 with the
|
|
@@ -95,25 +101,21 @@ const PROVENANCE: readonly { kind: ProvenanceKind; pattern: RegExp }[] = [
|
|
|
95
101
|
*
|
|
96
102
|
* `standards/context.md` opens its scope by handing diagrams and wireframes to
|
|
97
103
|
* `diagrams.md` and `wireframes.md`, so a marker reported in either would cite
|
|
98
|
-
* a rule that entry's own standard routes elsewhere. The length
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
104
|
+
* a rule that entry's own standard routes elsewhere. The length and table
|
|
105
|
+
* checkpoints are quoted from the same standard and keep reaching every audited
|
|
106
|
+
* folder, because a threshold on how far a reader travels generalizes across
|
|
107
|
+
* entry types while a rule about what an entry may say does not.
|
|
102
108
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* diagram entry declares `## Decisions` or `## Gotchas`, so the scope of the
|
|
109
|
-
* rule is what narrows the finding rather than the shape of the number.
|
|
109
|
+
* What gates here is a rule only this standard states. Bullet weight does not,
|
|
110
|
+
* since `standards/markdown.md` owns that checkpoint across document types and
|
|
111
|
+
* its remedy sends the overflow to prose, which any entry type can act on.
|
|
112
|
+
* `standards/context.md` specializes that remedy for an entry carrying
|
|
113
|
+
* decisions, and specializing a rule narrows the advice rather than the measure.
|
|
110
114
|
*
|
|
111
|
-
* Restating the exclusion in the sibling standards was the alternative
|
|
112
|
-
* duplicates one knowledge item across three surfaces, which the
|
|
113
|
-
* instruction file forbids, and pointing is not available because the
|
|
114
|
-
* they would point at is the one disclaiming them.
|
|
115
|
-
* accumulate narration, the escalation is an attribute standard owning the rule
|
|
116
|
-
* across document types, not restoring this reach without an owner.
|
|
115
|
+
* Restating the exclusion in the sibling standards was the alternative for what
|
|
116
|
+
* gates here. It duplicates one knowledge item across three surfaces, which the
|
|
117
|
+
* root instruction file forbids, and pointing is not available because the
|
|
118
|
+
* surface they would point at is the one disclaiming them.
|
|
117
119
|
*/
|
|
118
120
|
export const PROVENANCE_FOLDER = 'context'
|
|
119
121
|
|
|
@@ -159,7 +161,7 @@ export interface EntryReport {
|
|
|
159
161
|
readonly catalogTables: readonly TableFinding[]
|
|
160
162
|
/** Empty for an entry no standard bans a change narrative in. */
|
|
161
163
|
readonly provenance: readonly ProvenanceFinding[]
|
|
162
|
-
/**
|
|
164
|
+
/** Measured in every audited folder, since an attribute standard states it. */
|
|
163
165
|
readonly heavyBullets: readonly BulletFinding[]
|
|
164
166
|
/**
|
|
165
167
|
* Required sections this entry declares, in the standard's order, and empty
|
|
@@ -549,7 +551,7 @@ export function measureEntry(
|
|
|
549
551
|
longestRunLine: run.line,
|
|
550
552
|
catalogTables: catalogTables(lines),
|
|
551
553
|
provenance: governsContent ? provenance(lines) : [],
|
|
552
|
-
heavyBullets:
|
|
554
|
+
heavyBullets: heavyBullets(lines),
|
|
553
555
|
sections: governsContent ? declaredSections(lines) : [],
|
|
554
556
|
stub: isStubSeed(source),
|
|
555
557
|
}
|
package/src/tasks/archive.ts
CHANGED
|
@@ -15,6 +15,12 @@ const PLANS_DIR = join('.claude', 'plans')
|
|
|
15
15
|
*/
|
|
16
16
|
export const RESERVED_STEMS = ['index', 'priority', 'session'] as const
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* `bad-input` describes the command line rather than the board, which is the
|
|
20
|
+
* split `record.ts` draws for the same reason. A caller naming two selectors
|
|
21
|
+
* answered as `ambiguous` sends a session to repair a task citation that is
|
|
22
|
+
* fine, so the three task verbs answer one mistake one way.
|
|
23
|
+
*/
|
|
18
24
|
export const ARCHIVE_REFUSALS = [
|
|
19
25
|
'no-board',
|
|
20
26
|
'no-match',
|
|
@@ -22,6 +28,7 @@ export const ARCHIVE_REFUSALS = [
|
|
|
22
28
|
'no-outcomes',
|
|
23
29
|
'open-outcomes',
|
|
24
30
|
'plan-unswept',
|
|
31
|
+
'bad-input',
|
|
25
32
|
] as const
|
|
26
33
|
|
|
27
34
|
export type ArchiveRefusal = (typeof ARCHIVE_REFUSALS)[number]
|
|
@@ -61,6 +68,26 @@ export function archiveDir(root: string): string {
|
|
|
61
68
|
return join(root, ARCHIVE_DIR)
|
|
62
69
|
}
|
|
63
70
|
|
|
71
|
+
export const OUTCOME_PATTERN = /^- \[([ xX])\] ?(.*)$/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Marks the lines sitting inside a fenced block, the fence delimiters included.
|
|
75
|
+
* A checkbox in a sample a task displays is not an outcome the task claims, and
|
|
76
|
+
* counting one shifts every position after it. Every reader of the outcome list
|
|
77
|
+
* shares this so none of them can disagree about what the list holds.
|
|
78
|
+
*/
|
|
79
|
+
export function fenceMask(lines: readonly string[]): boolean[] {
|
|
80
|
+
let inside = false
|
|
81
|
+
|
|
82
|
+
return lines.map((line) => {
|
|
83
|
+
if (/^\s*(?:```|~~~)/.test(line)) {
|
|
84
|
+
inside = !inside
|
|
85
|
+
return true
|
|
86
|
+
}
|
|
87
|
+
return inside
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
64
91
|
/**
|
|
65
92
|
* Splits a task's outcome list by checkbox state. The board format puts every
|
|
66
93
|
* outcome at the top level of `## Outcomes`, so an anchored match is enough and
|
|
@@ -69,9 +96,13 @@ export function archiveDir(root: string): string {
|
|
|
69
96
|
export function readOutcomes(text: string): TaskOutcomes {
|
|
70
97
|
const open: string[] = []
|
|
71
98
|
const closed: string[] = []
|
|
99
|
+
const lines = text.split('\n')
|
|
100
|
+
const fenced = fenceMask(lines)
|
|
101
|
+
|
|
102
|
+
for (const [index, line] of lines.entries()) {
|
|
103
|
+
if (fenced[index]) continue
|
|
72
104
|
|
|
73
|
-
|
|
74
|
-
const match = /^- \[([ xX])\] ?(.*)$/.exec(line)
|
|
105
|
+
const match = OUTCOME_PATTERN.exec(line)
|
|
75
106
|
if (!match) continue
|
|
76
107
|
|
|
77
108
|
const [, box, body] = match
|
|
@@ -157,7 +188,7 @@ function isLivePlan(target: string, dir: string, root: string): boolean {
|
|
|
157
188
|
)
|
|
158
189
|
}
|
|
159
190
|
|
|
160
|
-
async function listTaskStems(dir: string): Promise<string[]> {
|
|
191
|
+
export async function listTaskStems(dir: string): Promise<string[]> {
|
|
161
192
|
const entries = await readdir(dir)
|
|
162
193
|
const reserved: readonly string[] = RESERVED_STEMS
|
|
163
194
|
|