@taskset/cli 3.0.3 → 5.1.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 (58) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +8 -3
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +686 -19
  5. package/docs/_meta.ts +8 -0
  6. package/docs/cli-reference.md +461 -0
  7. package/docs/configuration.md +61 -0
  8. package/docs/document-types.md +100 -0
  9. package/docs/getting-started.md +99 -0
  10. package/docs/index.md +37 -0
  11. package/docs/maintainers/_meta.ts +7 -0
  12. package/docs/maintainers/architecture/_meta.ts +5 -0
  13. package/docs/maintainers/architecture/decisions/0001-documentation-platform.md +70 -0
  14. package/docs/maintainers/architecture/decisions/0002-code-architecture.md +31 -0
  15. package/docs/maintainers/architecture/decisions/0003-snapshot-policy.md +31 -0
  16. package/docs/maintainers/architecture/decisions/_meta.ts +5 -0
  17. package/docs/maintainers/architecture/overview.md +111 -0
  18. package/docs/maintainers/architecture/synchronization.md +56 -0
  19. package/docs/maintainers/development/_meta.ts +6 -0
  20. package/docs/maintainers/development/contributing.md +59 -0
  21. package/docs/maintainers/development/documentation.md +69 -0
  22. package/docs/maintainers/development/engineering.md +59 -0
  23. package/docs/maintainers/development/testing.md +89 -0
  24. package/docs/maintainers/index.md +22 -0
  25. package/docs/maintainers/product/_meta.ts +3 -0
  26. package/docs/maintainers/product/vision.md +76 -0
  27. package/docs/maintainers/technology.md +55 -0
  28. package/docs/task-files.md +174 -0
  29. package/package.json +10 -8
  30. package/skills/taskset/SKILL.md +227 -0
  31. package/skills/taskset/references/changesets-examples.md +97 -0
  32. package/skills/taskset/references/document-modeling-examples.md +63 -0
  33. package/skills/taskset/references/monorepo-task-modeling.md +125 -0
  34. package/skills/taskset/references/task-modeling-examples.md +249 -0
  35. package/skills/taskset-implement/SKILL.md +234 -0
  36. package/skills/taskset-implement/agents/openai.yaml +4 -0
  37. package/skills/taskset-implement/references/architecture/client-and-server.md +69 -0
  38. package/skills/taskset-implement/references/architecture/documentation-and-generated.md +70 -0
  39. package/skills/taskset-implement/references/architecture/ownership-and-dependencies.md +107 -0
  40. package/skills/taskset-implement/references/architecture/product-and-source.md +80 -0
  41. package/skills/taskset-implement/references/architecture/storage-and-snapshots.md +45 -0
  42. package/skills/taskset-implement/references/architecture.md +34 -0
  43. package/skills/taskset-implement/references/conventions/backend-and-tooling.md +33 -0
  44. package/skills/taskset-implement/references/conventions/design.md +38 -0
  45. package/skills/taskset-implement/references/conventions/interfaces-and-ui.md +50 -0
  46. package/skills/taskset-implement/references/conventions/naming-and-packages.md +64 -0
  47. package/skills/taskset-implement/references/conventions/task-files.md +89 -0
  48. package/skills/taskset-implement/references/conventions/tests-and-docs.md +55 -0
  49. package/skills/taskset-implement/references/conventions/typescript-and-exports.md +41 -0
  50. package/skills/taskset-implement/references/conventions.md +40 -0
  51. package/skills/taskset-implement/references/release.md +135 -0
  52. package/skills/taskset-implement/references/workflows/dependencies-and-docs-site.md +54 -0
  53. package/skills/taskset-implement/references/workflows/environment-and-pnpm.md +108 -0
  54. package/skills/taskset-implement/references/workflows/persisted-data-and-git.md +30 -0
  55. package/skills/taskset-implement/references/workflows/validation.md +34 -0
  56. package/skills/taskset-implement/references/workflows/vitest-and-test-strategy.md +82 -0
  57. package/skills/taskset-implement/references/workflows.md +33 -0
  58. package/src/cli.ts +851 -17
package/src/cli.ts CHANGED
@@ -1,6 +1,10 @@
1
+ import { readFile } from 'node:fs/promises'
1
2
  import path from 'node:path'
2
3
  import { parseArgs } from 'node:util'
3
4
  import {
5
+ DocumentIdSchema,
6
+ DocumentStatusSchema,
7
+ DocumentTimestampSchema,
4
8
  TaskIdSchema,
5
9
  TaskPrioritySchema,
6
10
  TaskRiskSchema,
@@ -8,28 +12,49 @@ import {
8
12
  TaskTimestampSchema,
9
13
  } from '@taskset/contracts'
10
14
  import {
15
+ buildDocumentGraph,
11
16
  buildTaskIndex,
17
+ createDocument,
12
18
  createSnapshot,
13
19
  createTask,
20
+ type DerivedDocumentRelationships,
14
21
  type DerivedTaskRelationships,
22
+ DOCUMENT_SORT_DIRECTIONS,
23
+ DOCUMENT_SORT_KEYS,
24
+ type DocumentBatchOperation,
25
+ type DocumentQuery,
26
+ DocumentQuerySchema,
27
+ type DocumentRecord,
28
+ deleteDocument,
15
29
  deleteTask,
16
30
  diagnoseRepository,
17
31
  discoverRepository,
32
+ executeDocumentBatch,
18
33
  generateViews,
34
+ importDocument,
19
35
  initializeRepository,
36
+ listDocuments,
20
37
  listSnapshots,
38
+ migrateTaskIds,
39
+ normalizeDocumentKind,
21
40
  normalizeRepositoryPath,
41
+ queryDocuments,
22
42
  queryTasks,
23
43
  RepositoryPathError,
44
+ readDocument,
24
45
  readTask,
25
46
  restoreSnapshot,
47
+ serializeDocumentFile,
26
48
  serializeTaskFile,
49
+ syncRepository,
27
50
  TASK_SORT_DIRECTIONS,
28
51
  TASK_SORT_KEYS,
29
52
  type TaskQuery,
30
53
  TaskQuerySchema,
31
54
  type TaskRecord,
55
+ type UpdateDocumentInput,
32
56
  type UpdateTaskInput,
57
+ updateDocument,
33
58
  updateTask,
34
59
  } from '@taskset/core'
35
60
  import * as z from 'zod'
@@ -47,12 +72,22 @@ const USAGE = `Usage:
47
72
  taskset task update <task-id> [metadata options]
48
73
  taskset task status <task-id> <status> [--json] [--cwd <path>]
49
74
  taskset task delete <task-id> [--remove-dependencies] [--json] [--cwd <path>]
75
+ taskset task migrate-ids [--json] [--cwd <path>]
76
+ taskset sync [--concurrency <count>] [--json] [--cwd <path>]
77
+ taskset document create <story|flow|decision|adr|dr|research|runbook> --title <title> [metadata options]
78
+ taskset document import <markdown-path> [--type <type>] [--move]
79
+ taskset document batch <manifest.json> [--concurrency <count>] [--json]
80
+ taskset document list [type] [query options]
81
+ taskset document show <document-id> [--type <type>] [--include-derived] [--json]
82
+ taskset document update <document-id> [metadata options] [--type <type>]
83
+ taskset document status <document-id> <status> [--type <type>] [--json]
84
+ taskset document delete <document-id> [--type <type>] [--remove-dependencies] [--json]
50
85
 
51
86
  Metadata options:
52
87
  --status --priority --order --owner --team --estimate --effort --risk --due-date
53
88
  --label --assignee --reviewer --depends-on --related --duplicate
54
89
  --parent --file --directory --project --body
55
- Repeat array options. Task updates clear arrays with --clear-dependencies,
90
+ Repeat array options. Task and document updates clear arrays with --clear-dependencies,
56
91
  --clear-labels, --clear-assignees, --clear-reviewers, --clear-related,
57
92
  --clear-files, --clear-directories, or --clear-projects. Clear scalar
58
93
  relationships with --clear-parent or --clear-owner.
@@ -104,31 +139,98 @@ const CommonValuesSchema = z.strictObject({
104
139
  json: JsonSchema,
105
140
  })
106
141
 
107
- const CreateValuesSchema = z.strictObject({
142
+ const ConcurrencySchema = z.coerce.number().int().min(1).max(32).optional()
143
+ const DocumentKindInputSchema = TrimmedStringSchema.transform((value, context) => {
144
+ try {
145
+ return normalizeDocumentKind(value)
146
+ } catch (error) {
147
+ context.addIssue({
148
+ code: 'custom',
149
+ message: error instanceof Error ? error.message : 'Invalid document type',
150
+ })
151
+ return z.NEVER
152
+ }
153
+ })
154
+ const DocumentInputSchema = z.strictObject({
155
+ type: DocumentKindInputSchema,
108
156
  title: TrimmedStringSchema,
109
- status: TaskStatusSchema.optional(),
157
+ status: DocumentStatusSchema.optional(),
110
158
  priority: TaskPrioritySchema.optional(),
111
- order: z.coerce.number().finite().nonnegative().optional(),
112
- label: StringListSchema.optional(),
113
- 'depends-on': TaskIdListSchema.optional(),
114
- file: StringListSchema.optional(),
159
+ order: z.number().finite().nonnegative().optional(),
160
+ labels: StringListSchema.optional(),
161
+ dependsOn: TaskIdListSchema.optional(),
162
+ files: StringListSchema.optional(),
115
163
  owner: TrimmedStringSchema.optional(),
116
- assignee: StringListSchema.optional(),
117
- reviewer: StringListSchema.optional(),
164
+ assignees: StringListSchema.optional(),
165
+ reviewers: StringListSchema.optional(),
118
166
  team: TrimmedStringSchema.optional(),
119
- estimate: z.coerce.number().int().nonnegative().optional(),
120
- effort: z.coerce.number().finite().nonnegative().optional(),
167
+ estimate: z.number().int().nonnegative().optional(),
168
+ effort: z.number().finite().nonnegative().optional(),
121
169
  risk: TaskRiskSchema.optional(),
122
- 'due-date': TaskTimestampSchema.optional(),
170
+ dueDate: DocumentTimestampSchema.optional(),
123
171
  related: TaskIdListSchema.optional(),
124
- duplicate: TaskIdListSchema.optional(),
172
+ duplicates: TaskIdListSchema.optional(),
125
173
  parent: TaskIdSchema.optional(),
126
- directory: StringListSchema.optional(),
127
- project: StringListSchema.optional(),
174
+ directories: StringListSchema.optional(),
175
+ projects: StringListSchema.optional(),
128
176
  body: z.string().optional(),
129
- cwd: CwdSchema,
130
- json: JsonSchema,
131
177
  })
178
+ const DocumentUpdateSchema = z
179
+ .strictObject({
180
+ title: TrimmedStringSchema.optional(),
181
+ status: DocumentStatusSchema.optional(),
182
+ priority: TaskPrioritySchema.nullable().optional(),
183
+ order: z.number().finite().nonnegative().nullable().optional(),
184
+ labels: StringListSchema.optional(),
185
+ dependsOn: TaskIdListSchema.optional(),
186
+ files: StringListSchema.optional(),
187
+ owner: TrimmedStringSchema.nullable().optional(),
188
+ assignees: StringListSchema.optional(),
189
+ reviewers: StringListSchema.optional(),
190
+ team: TrimmedStringSchema.nullable().optional(),
191
+ estimate: z.number().int().nonnegative().nullable().optional(),
192
+ effort: z.number().finite().nonnegative().nullable().optional(),
193
+ risk: TaskRiskSchema.nullable().optional(),
194
+ dueDate: DocumentTimestampSchema.nullable().optional(),
195
+ related: TaskIdListSchema.optional(),
196
+ duplicates: TaskIdListSchema.optional(),
197
+ parent: TaskIdSchema.nullable().optional(),
198
+ directories: StringListSchema.optional(),
199
+ projects: StringListSchema.optional(),
200
+ body: z.string().optional(),
201
+ })
202
+ .refine((input) => Object.keys(input).length > 0, 'Document update requires at least one field')
203
+ const DocumentBatchSchema = z
204
+ .array(
205
+ z.discriminatedUnion('action', [
206
+ z.strictObject({ action: z.literal('create'), input: DocumentInputSchema }),
207
+ z.strictObject({
208
+ action: z.literal('import'),
209
+ sourcePath: TrimmedStringSchema,
210
+ options: z
211
+ .strictObject({
212
+ type: DocumentKindInputSchema.optional(),
213
+ title: TrimmedStringSchema.optional(),
214
+ move: z.boolean().optional(),
215
+ })
216
+ .optional(),
217
+ }),
218
+ z.strictObject({
219
+ action: z.literal('update'),
220
+ id: TrimmedStringSchema,
221
+ type: DocumentKindInputSchema.optional(),
222
+ input: DocumentUpdateSchema,
223
+ }),
224
+ z.strictObject({
225
+ action: z.literal('export'),
226
+ id: TrimmedStringSchema,
227
+ type: DocumentKindInputSchema.optional(),
228
+ targetPath: TrimmedStringSchema,
229
+ overwrite: z.boolean().optional(),
230
+ }),
231
+ ]),
232
+ )
233
+ .min(1)
132
234
 
133
235
  const UPDATE_CLEAR_PAIRS = [
134
236
  ['priority', 'clear-priority'],
@@ -151,6 +253,161 @@ const UPDATE_CLEAR_PAIRS = [
151
253
  ['project', 'clear-projects'],
152
254
  ] as const
153
255
 
256
+ const DocumentCreateValuesSchema = z.strictObject({
257
+ title: TrimmedStringSchema,
258
+ status: DocumentStatusSchema.optional(),
259
+ priority: TaskPrioritySchema.optional(),
260
+ order: z.coerce.number().finite().nonnegative().optional(),
261
+ label: StringListSchema.optional(),
262
+ 'depends-on': TaskIdListSchema.optional(),
263
+ file: StringListSchema.optional(),
264
+ owner: TrimmedStringSchema.optional(),
265
+ assignee: StringListSchema.optional(),
266
+ reviewer: StringListSchema.optional(),
267
+ team: TrimmedStringSchema.optional(),
268
+ estimate: z.coerce.number().int().nonnegative().optional(),
269
+ effort: z.coerce.number().finite().nonnegative().optional(),
270
+ risk: TaskRiskSchema.optional(),
271
+ 'due-date': DocumentTimestampSchema.optional(),
272
+ related: TaskIdListSchema.optional(),
273
+ duplicate: TaskIdListSchema.optional(),
274
+ parent: TaskIdSchema.optional(),
275
+ directory: StringListSchema.optional(),
276
+ project: StringListSchema.optional(),
277
+ body: z.string().optional(),
278
+ cwd: CwdSchema,
279
+ json: JsonSchema,
280
+ })
281
+
282
+ const DocumentUpdateValuesSchema = z
283
+ .strictObject({
284
+ title: TrimmedStringSchema.optional(),
285
+ status: DocumentStatusSchema.optional(),
286
+ priority: TaskPrioritySchema.optional(),
287
+ order: z.coerce.number().finite().nonnegative().optional(),
288
+ label: StringListSchema.optional(),
289
+ 'depends-on': TaskIdListSchema.optional(),
290
+ file: StringListSchema.optional(),
291
+ owner: TrimmedStringSchema.optional(),
292
+ assignee: StringListSchema.optional(),
293
+ reviewer: StringListSchema.optional(),
294
+ team: TrimmedStringSchema.optional(),
295
+ estimate: z.coerce.number().int().nonnegative().optional(),
296
+ effort: z.coerce.number().finite().nonnegative().optional(),
297
+ risk: TaskRiskSchema.optional(),
298
+ 'due-date': DocumentTimestampSchema.optional(),
299
+ related: TaskIdListSchema.optional(),
300
+ duplicate: TaskIdListSchema.optional(),
301
+ parent: TaskIdSchema.optional(),
302
+ directory: StringListSchema.optional(),
303
+ project: StringListSchema.optional(),
304
+ body: z.string().optional(),
305
+ type: TrimmedStringSchema.optional(),
306
+ 'clear-priority': z.boolean().optional(),
307
+ 'clear-order': z.boolean().optional(),
308
+ 'clear-labels': z.boolean().optional(),
309
+ 'clear-dependencies': z.boolean().optional(),
310
+ 'clear-files': z.boolean().optional(),
311
+ 'clear-owner': z.boolean().optional(),
312
+ 'clear-assignees': z.boolean().optional(),
313
+ 'clear-reviewers': z.boolean().optional(),
314
+ 'clear-team': z.boolean().optional(),
315
+ 'clear-estimate': z.boolean().optional(),
316
+ 'clear-effort': z.boolean().optional(),
317
+ 'clear-risk': z.boolean().optional(),
318
+ 'clear-due-date': z.boolean().optional(),
319
+ 'clear-related': z.boolean().optional(),
320
+ 'clear-duplicates': z.boolean().optional(),
321
+ 'clear-parent': z.boolean().optional(),
322
+ 'clear-directories': z.boolean().optional(),
323
+ 'clear-projects': z.boolean().optional(),
324
+ cwd: CwdSchema,
325
+ json: JsonSchema,
326
+ })
327
+ .superRefine((values, context) => {
328
+ for (const [valueKey, clearKey] of UPDATE_CLEAR_PAIRS) {
329
+ if (values[valueKey] !== undefined && values[clearKey]) {
330
+ context.addIssue({
331
+ code: 'custom',
332
+ path: [clearKey],
333
+ message: `Cannot set and clear ${valueKey} in the same update`,
334
+ })
335
+ }
336
+ }
337
+
338
+ const nonControlKeys = Object.keys(values).filter(
339
+ (key) => key !== 'cwd' && key !== 'json' && key !== 'type',
340
+ )
341
+
342
+ if (nonControlKeys.length === 0) {
343
+ context.addIssue({
344
+ code: 'custom',
345
+ message: 'Document update requires at least one field option',
346
+ })
347
+ }
348
+ })
349
+
350
+ const DocumentListValuesSchema = z.strictObject({
351
+ status: z.array(DocumentStatusSchema).optional(),
352
+ priority: z.array(TaskPrioritySchema).optional(),
353
+ label: StringListSchema.optional(),
354
+ owner: StringListSchema.optional(),
355
+ assignee: StringListSchema.optional(),
356
+ reviewer: StringListSchema.optional(),
357
+ team: StringListSchema.optional(),
358
+ risk: z.array(TaskRiskSchema).optional(),
359
+ project: StringListSchema.optional(),
360
+ 'depends-on': TaskIdSchema.optional(),
361
+ related: TaskIdSchema.optional(),
362
+ duplicate: TaskIdSchema.optional(),
363
+ parent: TaskIdSchema.optional(),
364
+ file: StringListSchema.optional(),
365
+ directory: StringListSchema.optional(),
366
+ 'estimate-min': z.coerce.number().finite().nonnegative().optional(),
367
+ 'estimate-max': z.coerce.number().finite().nonnegative().optional(),
368
+ 'effort-min': z.coerce.number().finite().nonnegative().optional(),
369
+ 'effort-max': z.coerce.number().finite().nonnegative().optional(),
370
+ 'due-before': DocumentTimestampSchema.optional(),
371
+ 'due-after': DocumentTimestampSchema.optional(),
372
+ 'created-before': DocumentTimestampSchema.optional(),
373
+ 'created-after': DocumentTimestampSchema.optional(),
374
+ 'updated-before': DocumentTimestampSchema.optional(),
375
+ 'updated-after': DocumentTimestampSchema.optional(),
376
+ search: z.string().optional(),
377
+ sort: z.enum(DOCUMENT_SORT_KEYS).optional(),
378
+ direction: z.enum(DOCUMENT_SORT_DIRECTIONS).optional(),
379
+ impact: z.boolean().optional(),
380
+ 'include-derived': z.boolean().optional(),
381
+ cwd: CwdSchema,
382
+ json: JsonSchema,
383
+ })
384
+
385
+ const CreateValuesSchema = z.strictObject({
386
+ title: TrimmedStringSchema,
387
+ status: TaskStatusSchema.optional(),
388
+ priority: TaskPrioritySchema.optional(),
389
+ order: z.coerce.number().finite().nonnegative().optional(),
390
+ label: StringListSchema.optional(),
391
+ 'depends-on': TaskIdListSchema.optional(),
392
+ file: StringListSchema.optional(),
393
+ owner: TrimmedStringSchema.optional(),
394
+ assignee: StringListSchema.optional(),
395
+ reviewer: StringListSchema.optional(),
396
+ team: TrimmedStringSchema.optional(),
397
+ estimate: z.coerce.number().int().nonnegative().optional(),
398
+ effort: z.coerce.number().finite().nonnegative().optional(),
399
+ risk: TaskRiskSchema.optional(),
400
+ 'due-date': TaskTimestampSchema.optional(),
401
+ related: TaskIdListSchema.optional(),
402
+ duplicate: TaskIdListSchema.optional(),
403
+ parent: TaskIdSchema.optional(),
404
+ directory: StringListSchema.optional(),
405
+ project: StringListSchema.optional(),
406
+ body: z.string().optional(),
407
+ cwd: CwdSchema,
408
+ json: JsonSchema,
409
+ })
410
+
154
411
  const UpdateValuesSchema = z
155
412
  .strictObject({
156
413
  title: TrimmedStringSchema.optional(),
@@ -342,6 +599,17 @@ function taskRecordJson(
342
599
  }
343
600
  }
344
601
 
602
+ function documentRecordJson(
603
+ record: DocumentRecord,
604
+ derived?: DerivedDocumentRelationships,
605
+ ): Record<string, unknown> {
606
+ return {
607
+ relativePath: record.relativePath,
608
+ ...record.document.metadata,
609
+ ...(derived ? { derived } : {}),
610
+ }
611
+ }
612
+
345
613
  function writeTaskRecord(
346
614
  record: TaskRecord,
347
615
  json: boolean | undefined,
@@ -354,6 +622,18 @@ function writeTaskRecord(
354
622
  }
355
623
  }
356
624
 
625
+ function writeDocumentRecord(
626
+ record: DocumentRecord,
627
+ json: boolean | undefined,
628
+ stdout: (value: string) => void,
629
+ ): void {
630
+ if (json) {
631
+ stdout(`${JSON.stringify(documentRecordJson(record), null, 2)}\n`)
632
+ } else {
633
+ stdout(`${record.document.metadata.id}\n`)
634
+ }
635
+ }
636
+
357
637
  function warningWriter(
358
638
  stderr: (value: string) => void,
359
639
  ): (warning: { readonly message: string }) => void {
@@ -468,6 +748,107 @@ function updateInputFromValues(
468
748
  }
469
749
  }
470
750
 
751
+ function documentUpdateInputFromValues(
752
+ repository: Awaited<ReturnType<typeof discoverRepository>>,
753
+ values: z.infer<typeof DocumentUpdateValuesSchema>,
754
+ ): UpdateDocumentInput {
755
+ return {
756
+ ...(values.title !== undefined ? { title: values.title } : {}),
757
+ ...(values.status !== undefined ? { status: values.status } : {}),
758
+ ...(values['clear-priority']
759
+ ? { priority: null }
760
+ : values.priority !== undefined
761
+ ? { priority: values.priority }
762
+ : {}),
763
+ ...(values['clear-order']
764
+ ? { order: null }
765
+ : values.order !== undefined
766
+ ? { order: values.order }
767
+ : {}),
768
+ ...(values['clear-labels']
769
+ ? { labels: [] }
770
+ : values.label !== undefined
771
+ ? { labels: values.label }
772
+ : {}),
773
+ ...(values['clear-dependencies']
774
+ ? { dependsOn: [] }
775
+ : values['depends-on'] !== undefined
776
+ ? { dependsOn: values['depends-on'] }
777
+ : {}),
778
+ ...(values['clear-files']
779
+ ? { files: [] }
780
+ : values.file !== undefined
781
+ ? { files: normalizeMutationPaths(repository, values.file) }
782
+ : {}),
783
+ ...(values['clear-owner']
784
+ ? { owner: null }
785
+ : values.owner !== undefined
786
+ ? { owner: values.owner }
787
+ : {}),
788
+ ...(values['clear-assignees']
789
+ ? { assignees: [] }
790
+ : values.assignee !== undefined
791
+ ? { assignees: values.assignee }
792
+ : {}),
793
+ ...(values['clear-reviewers']
794
+ ? { reviewers: [] }
795
+ : values.reviewer !== undefined
796
+ ? { reviewers: values.reviewer }
797
+ : {}),
798
+ ...(values['clear-team']
799
+ ? { team: null }
800
+ : values.team !== undefined
801
+ ? { team: values.team }
802
+ : {}),
803
+ ...(values['clear-estimate']
804
+ ? { estimate: null }
805
+ : values.estimate !== undefined
806
+ ? { estimate: values.estimate }
807
+ : {}),
808
+ ...(values['clear-effort']
809
+ ? { effort: null }
810
+ : values.effort !== undefined
811
+ ? { effort: values.effort }
812
+ : {}),
813
+ ...(values['clear-risk']
814
+ ? { risk: null }
815
+ : values.risk !== undefined
816
+ ? { risk: values.risk }
817
+ : {}),
818
+ ...(values['clear-due-date']
819
+ ? { dueDate: null }
820
+ : values['due-date'] !== undefined
821
+ ? { dueDate: values['due-date'] }
822
+ : {}),
823
+ ...(values['clear-related']
824
+ ? { related: [] }
825
+ : values.related !== undefined
826
+ ? { related: values.related }
827
+ : {}),
828
+ ...(values['clear-duplicates']
829
+ ? { duplicates: [] }
830
+ : values.duplicate !== undefined
831
+ ? { duplicates: values.duplicate }
832
+ : {}),
833
+ ...(values['clear-parent']
834
+ ? { parent: null }
835
+ : values.parent !== undefined
836
+ ? { parent: values.parent }
837
+ : {}),
838
+ ...(values['clear-directories']
839
+ ? { directories: [] }
840
+ : values.directory !== undefined
841
+ ? { directories: normalizeMutationPaths(repository, values.directory) }
842
+ : {}),
843
+ ...(values['clear-projects']
844
+ ? { projects: [] }
845
+ : values.project !== undefined
846
+ ? { projects: values.project }
847
+ : {}),
848
+ ...(values.body !== undefined ? { body: values.body } : {}),
849
+ }
850
+ }
851
+
471
852
  export async function runCli(args: readonly string[], context: CliContext = {}): Promise<number> {
472
853
  const cwd = path.resolve(context.cwd ?? process.cwd())
473
854
  const stdout = context.stdout ?? ((value: string) => process.stdout.write(value))
@@ -555,6 +936,33 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
555
936
  return 0
556
937
  }
557
938
 
939
+ if (command === 'sync') {
940
+ const parsed = parseArgs({
941
+ args: commandArgs,
942
+ allowPositionals: false,
943
+ options: { ...commonOptionDefinitions, concurrency: { type: 'string' } },
944
+ })
945
+ const values = parseSchema(
946
+ z.strictObject({ ...CommonValuesSchema.shape, concurrency: ConcurrencySchema }),
947
+ parsed.values,
948
+ 'sync options',
949
+ )
950
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
951
+ const result = await syncRepository(repository, {
952
+ concurrency: values.concurrency,
953
+ onProgress: (progress) =>
954
+ stderr(
955
+ `sync ${progress.phase}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`,
956
+ ),
957
+ })
958
+ stdout(
959
+ values.json
960
+ ? `${JSON.stringify(result, null, 2)}\n`
961
+ : `Synced ${result.migrations.length} migrations and generated views\n`,
962
+ )
963
+ return 0
964
+ }
965
+
558
966
  if (command === 'snapshot') {
559
967
  if (subcommand === 'create' || subcommand === 'list') {
560
968
  const parsed = parseArgs({
@@ -625,10 +1033,436 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
625
1033
  throw new CliUsageError(`Unknown snapshot command "${subcommand ?? ''}"`)
626
1034
  }
627
1035
 
1036
+ if (command === 'document' || command === 'doc') {
1037
+ if (subcommand === 'batch') {
1038
+ const parsed = parseArgs({
1039
+ args: subcommandArgs,
1040
+ allowPositionals: true,
1041
+ options: { ...commonOptionDefinitions, concurrency: { type: 'string' } },
1042
+ })
1043
+ const [manifestPath] = requirePositionals(
1044
+ parsed.positionals,
1045
+ 1,
1046
+ 'exactly one batch manifest path',
1047
+ )
1048
+ const values = parseSchema(
1049
+ z.strictObject({ ...CommonValuesSchema.shape, concurrency: ConcurrencySchema }),
1050
+ parsed.values,
1051
+ 'document batch options',
1052
+ )
1053
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1054
+ const validatedManifestPath = parseSchema(
1055
+ z.string(),
1056
+ manifestPath,
1057
+ 'document batch manifest path',
1058
+ )
1059
+ const raw = JSON.parse(
1060
+ await readFile(path.resolve(repository.rootDirectory, validatedManifestPath), 'utf8'),
1061
+ ) as unknown
1062
+ const operations = parseSchema(
1063
+ DocumentBatchSchema,
1064
+ raw,
1065
+ 'document batch manifest',
1066
+ ) as readonly DocumentBatchOperation[]
1067
+ const results = await executeDocumentBatch(repository, operations, {
1068
+ concurrency: values.concurrency,
1069
+ onProgress: (progress) =>
1070
+ stderr(
1071
+ `document ${progress.action}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`,
1072
+ ),
1073
+ })
1074
+ stdout(
1075
+ values.json
1076
+ ? `${JSON.stringify(results, null, 2)}\n`
1077
+ : `${results.map((result) => result.relativePath).join('\n')}\n`,
1078
+ )
1079
+ return 0
1080
+ }
1081
+ if (subcommand === 'create') {
1082
+ const parsed = parseArgs({
1083
+ args: subcommandArgs,
1084
+ allowPositionals: true,
1085
+ options: { ...metadataOptionDefinitions, ...commonOptionDefinitions },
1086
+ })
1087
+ const [rawType] = requirePositionals(parsed.positionals, 1, 'exactly one document type')
1088
+ const values = parseSchema(
1089
+ DocumentCreateValuesSchema,
1090
+ parsed.values,
1091
+ 'document create options',
1092
+ )
1093
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1094
+ const record = await createDocument(
1095
+ repository,
1096
+ {
1097
+ type: normalizeDocumentKind(parseSchema(z.string(), rawType, 'document type')),
1098
+ title: values.title,
1099
+ ...(values.status ? { status: values.status } : {}),
1100
+ ...(values.priority ? { priority: values.priority } : {}),
1101
+ ...(values.order !== undefined ? { order: values.order } : {}),
1102
+ ...(values.label ? { labels: values.label } : {}),
1103
+ ...(values['depends-on'] ? { dependsOn: values['depends-on'] } : {}),
1104
+ ...(values.file ? { files: normalizeMutationPaths(repository, values.file) } : {}),
1105
+ ...(values.owner ? { owner: values.owner } : {}),
1106
+ ...(values.assignee ? { assignees: values.assignee } : {}),
1107
+ ...(values.reviewer ? { reviewers: values.reviewer } : {}),
1108
+ ...(values.team ? { team: values.team } : {}),
1109
+ ...(values.estimate !== undefined ? { estimate: values.estimate } : {}),
1110
+ ...(values.effort !== undefined ? { effort: values.effort } : {}),
1111
+ ...(values.risk ? { risk: values.risk } : {}),
1112
+ ...(values['due-date'] ? { dueDate: values['due-date'] } : {}),
1113
+ ...(values.related ? { related: values.related } : {}),
1114
+ ...(values.duplicate ? { duplicates: values.duplicate } : {}),
1115
+ ...(values.parent ? { parent: values.parent } : {}),
1116
+ ...(values.directory
1117
+ ? { directories: normalizeMutationPaths(repository, values.directory) }
1118
+ : {}),
1119
+ ...(values.project ? { projects: values.project } : {}),
1120
+ ...(values.body !== undefined ? { body: values.body } : {}),
1121
+ },
1122
+ { onWarning },
1123
+ )
1124
+ writeDocumentRecord(record, values.json, stdout)
1125
+ return 0
1126
+ }
1127
+
1128
+ if (subcommand === 'import') {
1129
+ const parsed = parseArgs({
1130
+ args: subcommandArgs,
1131
+ allowPositionals: true,
1132
+ options: {
1133
+ ...commonOptionDefinitions,
1134
+ type: { type: 'string' },
1135
+ title: { type: 'string' },
1136
+ move: { type: 'boolean' },
1137
+ },
1138
+ })
1139
+ const [sourcePath] = requirePositionals(parsed.positionals, 1, 'exactly one Markdown path')
1140
+ const values = parseSchema(
1141
+ z.strictObject({
1142
+ type: TrimmedStringSchema.optional(),
1143
+ title: TrimmedStringSchema.optional(),
1144
+ move: z.boolean().optional(),
1145
+ cwd: CwdSchema,
1146
+ json: JsonSchema,
1147
+ }),
1148
+ parsed.values,
1149
+ 'document import options',
1150
+ )
1151
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1152
+ const record = await importDocument(
1153
+ repository,
1154
+ parseSchema(z.string(), sourcePath, 'document import path'),
1155
+ {
1156
+ ...(values.type ? { type: normalizeDocumentKind(values.type) } : {}),
1157
+ ...(values.title ? { title: values.title } : {}),
1158
+ move: values.move,
1159
+ },
1160
+ )
1161
+ stdout(values.json ? `${JSON.stringify(record, null, 2)}\n` : `${record.relativePath}\n`)
1162
+ return 0
1163
+ }
1164
+
1165
+ if (subcommand === 'list') {
1166
+ const parsed = parseArgs({
1167
+ args: subcommandArgs,
1168
+ allowPositionals: true,
1169
+ options: {
1170
+ cwd: { type: 'string' },
1171
+ json: { type: 'boolean' },
1172
+ status: { type: 'string', multiple: true },
1173
+ priority: { type: 'string', multiple: true },
1174
+ label: { type: 'string', multiple: true },
1175
+ owner: { type: 'string', multiple: true },
1176
+ assignee: { type: 'string', multiple: true },
1177
+ reviewer: { type: 'string', multiple: true },
1178
+ team: { type: 'string', multiple: true },
1179
+ risk: { type: 'string', multiple: true },
1180
+ project: { type: 'string', multiple: true },
1181
+ 'depends-on': { type: 'string' },
1182
+ related: { type: 'string' },
1183
+ duplicate: { type: 'string' },
1184
+ parent: { type: 'string' },
1185
+ file: { type: 'string', multiple: true },
1186
+ directory: { type: 'string', multiple: true },
1187
+ 'estimate-min': { type: 'string' },
1188
+ 'estimate-max': { type: 'string' },
1189
+ 'effort-min': { type: 'string' },
1190
+ 'effort-max': { type: 'string' },
1191
+ 'due-before': { type: 'string' },
1192
+ 'due-after': { type: 'string' },
1193
+ 'created-before': { type: 'string' },
1194
+ 'created-after': { type: 'string' },
1195
+ 'updated-before': { type: 'string' },
1196
+ 'updated-after': { type: 'string' },
1197
+ search: { type: 'string' },
1198
+ sort: { type: 'string' },
1199
+ direction: { type: 'string' },
1200
+ impact: { type: 'boolean' },
1201
+ 'include-derived': { type: 'boolean' },
1202
+ },
1203
+ })
1204
+ if (parsed.positionals.length > 1)
1205
+ throw new CliUsageError('Expected at most one document type')
1206
+ const values = parseSchema(DocumentListValuesSchema, parsed.values, 'document list options')
1207
+ const typeFilter = parsed.positionals[0]
1208
+ ? normalizeDocumentKind(parsed.positionals[0])
1209
+ : undefined
1210
+ const query = parseSchema(
1211
+ DocumentQuerySchema,
1212
+ {
1213
+ ...(typeFilter ? { types: [typeFilter] } : {}),
1214
+ ...(values.status ? { statuses: values.status } : {}),
1215
+ ...(values.priority ? { priorities: values.priority } : {}),
1216
+ ...(values.label ? { labels: values.label } : {}),
1217
+ ...(values.owner ? { owners: values.owner } : {}),
1218
+ ...(values.assignee ? { assignees: values.assignee } : {}),
1219
+ ...(values.reviewer ? { reviewers: values.reviewer } : {}),
1220
+ ...(values.team ? { teams: values.team } : {}),
1221
+ ...(values.risk ? { risks: values.risk } : {}),
1222
+ ...(values.project ? { projects: values.project } : {}),
1223
+ ...(values['depends-on'] ? { dependsOn: values['depends-on'] } : {}),
1224
+ ...(values.related ? { related: values.related } : {}),
1225
+ ...(values.duplicate ? { duplicate: values.duplicate } : {}),
1226
+ ...(values.parent ? { parent: values.parent } : {}),
1227
+ ...(values.file ? { files: values.file } : {}),
1228
+ ...(values.directory ? { directories: values.directory } : {}),
1229
+ ...(values['estimate-min'] !== undefined
1230
+ ? { estimateMin: values['estimate-min'] }
1231
+ : {}),
1232
+ ...(values['estimate-max'] !== undefined
1233
+ ? { estimateMax: values['estimate-max'] }
1234
+ : {}),
1235
+ ...(values['effort-min'] !== undefined ? { effortMin: values['effort-min'] } : {}),
1236
+ ...(values['effort-max'] !== undefined ? { effortMax: values['effort-max'] } : {}),
1237
+ ...(values['due-before'] ? { dueBefore: values['due-before'] } : {}),
1238
+ ...(values['due-after'] ? { dueAfter: values['due-after'] } : {}),
1239
+ ...(values['created-before'] ? { createdBefore: values['created-before'] } : {}),
1240
+ ...(values['created-after'] ? { createdAfter: values['created-after'] } : {}),
1241
+ ...(values['updated-before'] ? { updatedBefore: values['updated-before'] } : {}),
1242
+ ...(values['updated-after'] ? { updatedAfter: values['updated-after'] } : {}),
1243
+ ...(values.search !== undefined ? { text: values.search } : {}),
1244
+ ...(values.sort ? { sortBy: values.sort } : {}),
1245
+ ...(values.direction ? { direction: values.direction } : {}),
1246
+ ...(values.impact ? { impact: true } : {}),
1247
+ } satisfies DocumentQuery,
1248
+ 'document list query',
1249
+ )
1250
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1251
+ const result = await queryDocuments(repository, query)
1252
+ const derivedGraph = values['include-derived']
1253
+ ? buildDocumentGraph(await listDocuments(repository))
1254
+ : undefined
1255
+ const serialize = (record: DocumentRecord) =>
1256
+ documentRecordJson(record, derivedGraph?.derive(record.document.metadata.id))
1257
+
1258
+ if (values.json) {
1259
+ stdout(
1260
+ `${JSON.stringify(
1261
+ values.impact
1262
+ ? {
1263
+ direct: result.direct.map(serialize),
1264
+ impacted: result.impacted.map(serialize),
1265
+ }
1266
+ : result.direct.map(serialize),
1267
+ null,
1268
+ 2,
1269
+ )}\n`,
1270
+ )
1271
+ } else {
1272
+ for (const record of result.direct) {
1273
+ stdout(
1274
+ `${values.impact ? 'direct\t' : ''}${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
1275
+ )
1276
+ }
1277
+ for (const record of result.impacted) {
1278
+ stdout(
1279
+ `impact\t${record.document.metadata.id}\t${record.document.metadata.type}\t${record.document.metadata.status}\t${record.document.metadata.title}\n`,
1280
+ )
1281
+ }
1282
+ }
1283
+ return 0
1284
+ }
1285
+
1286
+ if (subcommand === 'show') {
1287
+ const parsed = parseArgs({
1288
+ args: subcommandArgs,
1289
+ allowPositionals: true,
1290
+ options: {
1291
+ ...commonOptionDefinitions,
1292
+ type: { type: 'string' },
1293
+ 'include-derived': { type: 'boolean' },
1294
+ },
1295
+ })
1296
+ const [id] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
1297
+ const values = parseSchema(
1298
+ z.strictObject({
1299
+ ...CommonValuesSchema.shape,
1300
+ type: TrimmedStringSchema.optional(),
1301
+ 'include-derived': z.boolean().optional(),
1302
+ }),
1303
+ parsed.values,
1304
+ 'document show options',
1305
+ )
1306
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1307
+ const validatedId = parseSchema(DocumentIdSchema, id, 'document ID')
1308
+ const record = await readDocument(
1309
+ repository,
1310
+ validatedId,
1311
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1312
+ )
1313
+
1314
+ if (values.json) {
1315
+ const derived = values['include-derived']
1316
+ ? buildDocumentGraph(await listDocuments(repository)).derive(validatedId)
1317
+ : undefined
1318
+ stdout(
1319
+ `${JSON.stringify(
1320
+ {
1321
+ relativePath: record.relativePath,
1322
+ metadata: record.document.metadata,
1323
+ body: record.document.body,
1324
+ ...(derived ? { derived } : {}),
1325
+ },
1326
+ null,
1327
+ 2,
1328
+ )}\n`,
1329
+ )
1330
+ } else {
1331
+ stdout(serializeDocumentFile(record.document))
1332
+ }
1333
+ return 0
1334
+ }
1335
+
1336
+ if (subcommand === 'update') {
1337
+ const clearDefinitions = Object.fromEntries(
1338
+ UPDATE_CLEAR_PAIRS.map(([, clear]) => [clear, { type: 'boolean' as const }]),
1339
+ )
1340
+ const parsed = parseArgs({
1341
+ args: subcommandArgs,
1342
+ allowPositionals: true,
1343
+ options: {
1344
+ ...metadataOptionDefinitions,
1345
+ ...clearDefinitions,
1346
+ ...commonOptionDefinitions,
1347
+ type: { type: 'string' },
1348
+ },
1349
+ })
1350
+ const [documentId] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
1351
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1352
+ const values = parseSchema(
1353
+ DocumentUpdateValuesSchema,
1354
+ parsed.values,
1355
+ 'document update options',
1356
+ )
1357
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1358
+ const record = await updateDocument(
1359
+ repository,
1360
+ validatedDocumentId,
1361
+ documentUpdateInputFromValues(repository, values),
1362
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1363
+ { onWarning },
1364
+ )
1365
+ writeDocumentRecord(record, values.json, stdout)
1366
+ return 0
1367
+ }
1368
+
1369
+ if (subcommand === 'status') {
1370
+ const parsed = parseArgs({
1371
+ args: subcommandArgs,
1372
+ allowPositionals: true,
1373
+ options: { ...commonOptionDefinitions, type: { type: 'string' } },
1374
+ })
1375
+ const [documentId, status] = requirePositionals(
1376
+ parsed.positionals,
1377
+ 2,
1378
+ 'a document ID and status',
1379
+ )
1380
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1381
+ const validatedStatus = parseSchema(DocumentStatusSchema, status, 'document status')
1382
+ const values = parseSchema(
1383
+ z.strictObject({ ...CommonValuesSchema.shape, type: TrimmedStringSchema.optional() }),
1384
+ parsed.values,
1385
+ 'document status options',
1386
+ )
1387
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1388
+ const record = await updateDocument(
1389
+ repository,
1390
+ validatedDocumentId,
1391
+ { status: validatedStatus },
1392
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1393
+ { onWarning },
1394
+ )
1395
+ writeDocumentRecord(record, values.json, stdout)
1396
+ return 0
1397
+ }
1398
+
1399
+ if (subcommand === 'delete') {
1400
+ const parsed = parseArgs({
1401
+ args: subcommandArgs,
1402
+ allowPositionals: true,
1403
+ options: {
1404
+ ...commonOptionDefinitions,
1405
+ type: { type: 'string' },
1406
+ 'remove-dependencies': { type: 'boolean' },
1407
+ },
1408
+ })
1409
+ const [documentId] = requirePositionals(parsed.positionals, 1, 'exactly one document ID')
1410
+ const validatedDocumentId = parseSchema(DocumentIdSchema, documentId, 'document ID')
1411
+ const values = parseSchema(
1412
+ z.strictObject({
1413
+ cwd: CwdSchema,
1414
+ json: JsonSchema,
1415
+ type: TrimmedStringSchema.optional(),
1416
+ 'remove-dependencies': z.boolean().optional(),
1417
+ }),
1418
+ parsed.values,
1419
+ 'document delete options',
1420
+ )
1421
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1422
+ const record = await deleteDocument(
1423
+ repository,
1424
+ validatedDocumentId,
1425
+ values.type ? normalizeDocumentKind(values.type) : undefined,
1426
+ {
1427
+ removeDependencies: values['remove-dependencies'],
1428
+ onWarning,
1429
+ },
1430
+ )
1431
+
1432
+ if (values.json) {
1433
+ stdout(`${JSON.stringify({ deleted: true, ...documentRecordJson(record) }, null, 2)}\n`)
1434
+ } else {
1435
+ stdout(`${validatedDocumentId}\n`)
1436
+ }
1437
+ return 0
1438
+ }
1439
+
1440
+ throw new CliUsageError(`Unknown document command "${subcommand ?? ''}"`)
1441
+ }
1442
+
628
1443
  if (command !== 'task') {
629
1444
  throw new CliUsageError(`Unknown command "${command}"`)
630
1445
  }
631
1446
 
1447
+ if (subcommand === 'migrate-ids') {
1448
+ const parsed = parseArgs({
1449
+ args: subcommandArgs,
1450
+ allowPositionals: false,
1451
+ options: commonOptionDefinitions,
1452
+ })
1453
+ const values = parseSchema(CommonValuesSchema, parsed.values, 'task migrate-ids options')
1454
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1455
+ const migrations = await migrateTaskIds(repository, {
1456
+ onProgress: (progress) =>
1457
+ stderr(
1458
+ `migration ${progress.phase}: ${progress.completed}/${progress.total} (${progress.percent}%)\n`,
1459
+ ),
1460
+ })
1461
+ if (values.json) stdout(`${JSON.stringify(migrations, null, 2)}\n`)
1462
+ else for (const migration of migrations) stdout(`${migration.from}\t${migration.to}\n`)
1463
+ return 0
1464
+ }
1465
+
632
1466
  if (subcommand === 'create') {
633
1467
  const parsed = parseArgs({
634
1468
  args: subcommandArgs,