@taskset/cli 5.1.0 → 6.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 (38) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +21 -22
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +212 -113
  5. package/docs/_meta.ts +5 -0
  6. package/docs/agent-closeout.md +69 -0
  7. package/docs/agents/_meta.ts +6 -0
  8. package/docs/agents/commands.md +94 -0
  9. package/docs/agents/index.md +113 -0
  10. package/docs/agents/llms.txt +35 -0
  11. package/docs/agents/query-recipes.md +76 -0
  12. package/docs/agents/workflows.md +60 -0
  13. package/docs/cli-reference.md +53 -34
  14. package/docs/configuration.md +53 -31
  15. package/docs/document-types.md +53 -24
  16. package/docs/getting-started.md +61 -49
  17. package/docs/index.md +37 -25
  18. package/docs/maintainers/architecture/decisions/0001-documentation-platform.md +21 -47
  19. package/docs/maintainers/development/contributing.md +2 -3
  20. package/docs/maintainers/development/documentation.md +32 -49
  21. package/docs/maintainers/index.md +1 -3
  22. package/docs/maintainers/product/vision.md +27 -33
  23. package/docs/memory-model.md +58 -0
  24. package/docs/security-compliance-tracking.md +107 -0
  25. package/docs/task-files.md +19 -13
  26. package/docs/taxonomy-cookbook.md +59 -0
  27. package/package.json +4 -4
  28. package/skills/taskset/SKILL.md +89 -57
  29. package/skills/taskset/references/document-modeling-examples.md +53 -2
  30. package/skills/taskset-implement/SKILL.md +26 -17
  31. package/skills/taskset-implement/references/architecture/documentation-and-generated.md +3 -1
  32. package/skills/taskset-implement/references/architecture/ownership-and-dependencies.md +2 -1
  33. package/skills/taskset-implement/references/architecture/product-and-source.md +22 -11
  34. package/skills/taskset-implement/references/architecture/storage-and-snapshots.md +5 -2
  35. package/skills/taskset-implement/references/conventions/naming-and-packages.md +1 -1
  36. package/skills/taskset-implement/references/conventions/task-files.md +9 -4
  37. package/skills/taskset-implement/references/workflows/environment-and-pnpm.md +4 -3
  38. package/src/cli.ts +174 -112
package/src/cli.ts CHANGED
@@ -2,9 +2,11 @@ import { readFile } from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { parseArgs } from 'node:util'
4
4
  import {
5
+ ConcernClassSchema,
5
6
  DocumentIdSchema,
6
7
  DocumentStatusSchema,
7
8
  DocumentTimestampSchema,
9
+ LessonSeveritySchema,
8
10
  TaskIdSchema,
9
11
  TaskPrioritySchema,
10
12
  TaskRiskSchema,
@@ -31,6 +33,7 @@ import {
31
33
  discoverRepository,
32
34
  executeDocumentBatch,
33
35
  generateViews,
36
+ getProgramRollup,
34
37
  importDocument,
35
38
  initializeRepository,
36
39
  listDocuments,
@@ -60,7 +63,7 @@ import {
60
63
  import * as z from 'zod'
61
64
 
62
65
  const USAGE = `Usage:
63
- taskset init [--cwd <path>]
66
+ taskset init [--config] [--cwd <path>]
64
67
  taskset config [--json] [--cwd <path>]
65
68
  taskset doctor [--json] [--cwd <path>]
66
69
  taskset generate [--json] [--cwd <path>]
@@ -73,8 +76,9 @@ const USAGE = `Usage:
73
76
  taskset task status <task-id> <status> [--json] [--cwd <path>]
74
77
  taskset task delete <task-id> [--remove-dependencies] [--json] [--cwd <path>]
75
78
  taskset task migrate-ids [--json] [--cwd <path>]
79
+ taskset task program <parent-id> [--json] [--cwd <path>]
76
80
  taskset sync [--concurrency <count>] [--json] [--cwd <path>]
77
- taskset document create <story|flow|decision|adr|dr|research|runbook> --title <title> [metadata options]
81
+ taskset document create <story|flow|decision|adr|dr|research|runbook|lesson|antipattern|concern|audit> --title <title> [metadata options]
78
82
  taskset document import <markdown-path> [--type <type>] [--move]
79
83
  taskset document batch <manifest.json> [--concurrency <count>] [--json]
80
84
  taskset document list [type] [query options]
@@ -87,10 +91,12 @@ Metadata options:
87
91
  --status --priority --order --owner --team --estimate --effort --risk --due-date
88
92
  --label --assignee --reviewer --depends-on --related --duplicate
89
93
  --parent --file --directory --project --body
94
+ Lesson/concern options: --severity --related-skill --pack --class --cadence
90
95
  Repeat array options. Task and document updates clear arrays with --clear-dependencies,
91
96
  --clear-labels, --clear-assignees, --clear-reviewers, --clear-related,
92
- --clear-files, --clear-directories, or --clear-projects. Clear scalar
93
- relationships with --clear-parent or --clear-owner.
97
+ --clear-files, --clear-directories, --clear-projects, --clear-related-skills,
98
+ or --clear-packs. Clear scalar relationships with --clear-parent, --clear-owner,
99
+ --clear-severity, --clear-class, or --clear-cadence.
94
100
 
95
101
  List query options:
96
102
  --status --priority --label --owner --assignee --reviewer --team --risk
@@ -139,6 +145,11 @@ const CommonValuesSchema = z.strictObject({
139
145
  json: JsonSchema,
140
146
  })
141
147
 
148
+ const InitValuesSchema = z.strictObject({
149
+ cwd: CwdSchema,
150
+ config: z.boolean().optional(),
151
+ })
152
+
142
153
  const ConcurrencySchema = z.coerce.number().int().min(1).max(32).optional()
143
154
  const DocumentKindInputSchema = TrimmedStringSchema.transform((value, context) => {
144
155
  try {
@@ -173,6 +184,11 @@ const DocumentInputSchema = z.strictObject({
173
184
  parent: TaskIdSchema.optional(),
174
185
  directories: StringListSchema.optional(),
175
186
  projects: StringListSchema.optional(),
187
+ severity: LessonSeveritySchema.optional(),
188
+ relatedSkills: StringListSchema.optional(),
189
+ packs: StringListSchema.optional(),
190
+ class: ConcernClassSchema.optional(),
191
+ cadence: TrimmedStringSchema.optional(),
176
192
  body: z.string().optional(),
177
193
  })
178
194
  const DocumentUpdateSchema = z
@@ -197,6 +213,11 @@ const DocumentUpdateSchema = z
197
213
  parent: TaskIdSchema.nullable().optional(),
198
214
  directories: StringListSchema.optional(),
199
215
  projects: StringListSchema.optional(),
216
+ severity: LessonSeveritySchema.nullable().optional(),
217
+ relatedSkills: StringListSchema.optional(),
218
+ packs: StringListSchema.optional(),
219
+ class: ConcernClassSchema.nullable().optional(),
220
+ cadence: TrimmedStringSchema.nullable().optional(),
200
221
  body: z.string().optional(),
201
222
  })
202
223
  .refine((input) => Object.keys(input).length > 0, 'Document update requires at least one field')
@@ -253,6 +274,15 @@ const UPDATE_CLEAR_PAIRS = [
253
274
  ['project', 'clear-projects'],
254
275
  ] as const
255
276
 
277
+ const DOCUMENT_UPDATE_CLEAR_PAIRS = [
278
+ ...UPDATE_CLEAR_PAIRS,
279
+ ['severity', 'clear-severity'],
280
+ ['related-skill', 'clear-related-skills'],
281
+ ['pack', 'clear-packs'],
282
+ ['class', 'clear-class'],
283
+ ['cadence', 'clear-cadence'],
284
+ ] as const
285
+
256
286
  const DocumentCreateValuesSchema = z.strictObject({
257
287
  title: TrimmedStringSchema,
258
288
  status: DocumentStatusSchema.optional(),
@@ -274,6 +304,11 @@ const DocumentCreateValuesSchema = z.strictObject({
274
304
  parent: TaskIdSchema.optional(),
275
305
  directory: StringListSchema.optional(),
276
306
  project: StringListSchema.optional(),
307
+ severity: LessonSeveritySchema.optional(),
308
+ 'related-skill': StringListSchema.optional(),
309
+ pack: StringListSchema.optional(),
310
+ class: ConcernClassSchema.optional(),
311
+ cadence: TrimmedStringSchema.optional(),
277
312
  body: z.string().optional(),
278
313
  cwd: CwdSchema,
279
314
  json: JsonSchema,
@@ -301,6 +336,11 @@ const DocumentUpdateValuesSchema = z
301
336
  parent: TaskIdSchema.optional(),
302
337
  directory: StringListSchema.optional(),
303
338
  project: StringListSchema.optional(),
339
+ severity: LessonSeveritySchema.optional(),
340
+ 'related-skill': StringListSchema.optional(),
341
+ pack: StringListSchema.optional(),
342
+ class: ConcernClassSchema.optional(),
343
+ cadence: TrimmedStringSchema.optional(),
304
344
  body: z.string().optional(),
305
345
  type: TrimmedStringSchema.optional(),
306
346
  'clear-priority': z.boolean().optional(),
@@ -321,11 +361,16 @@ const DocumentUpdateValuesSchema = z
321
361
  'clear-parent': z.boolean().optional(),
322
362
  'clear-directories': z.boolean().optional(),
323
363
  'clear-projects': z.boolean().optional(),
364
+ 'clear-severity': z.boolean().optional(),
365
+ 'clear-related-skills': z.boolean().optional(),
366
+ 'clear-packs': z.boolean().optional(),
367
+ 'clear-class': z.boolean().optional(),
368
+ 'clear-cadence': z.boolean().optional(),
324
369
  cwd: CwdSchema,
325
370
  json: JsonSchema,
326
371
  })
327
372
  .superRefine((values, context) => {
328
- for (const [valueKey, clearKey] of UPDATE_CLEAR_PAIRS) {
373
+ for (const [valueKey, clearKey] of DOCUMENT_UPDATE_CLEAR_PAIRS) {
329
374
  if (values[valueKey] !== undefined && values[clearKey]) {
330
375
  context.addIssue({
331
376
  code: 'custom',
@@ -357,6 +402,8 @@ const DocumentListValuesSchema = z.strictObject({
357
402
  team: StringListSchema.optional(),
358
403
  risk: z.array(TaskRiskSchema).optional(),
359
404
  project: StringListSchema.optional(),
405
+ class: z.array(ConcernClassSchema).optional(),
406
+ severity: z.array(LessonSeveritySchema).optional(),
360
407
  'depends-on': TaskIdSchema.optional(),
361
408
  related: TaskIdSchema.optional(),
362
409
  duplicate: TaskIdSchema.optional(),
@@ -532,6 +579,15 @@ const metadataOptionDefinitions = {
532
579
  project: { type: 'string', multiple: true },
533
580
  } as const
534
581
 
582
+ const documentMetadataOptionDefinitions = {
583
+ ...metadataOptionDefinitions,
584
+ severity: { type: 'string' },
585
+ 'related-skill': { type: 'string', multiple: true },
586
+ pack: { type: 'string', multiple: true },
587
+ class: { type: 'string' },
588
+ cadence: { type: 'string' },
589
+ } as const
590
+
535
591
  const commonOptionDefinitions = {
536
592
  cwd: { type: 'string' },
537
593
  json: { type: 'boolean' },
@@ -752,101 +808,59 @@ function documentUpdateInputFromValues(
752
808
  repository: Awaited<ReturnType<typeof discoverRepository>>,
753
809
  values: z.infer<typeof DocumentUpdateValuesSchema>,
754
810
  ): 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 } : {}),
811
+ const input: Record<string, unknown> = {}
812
+ if (values.title !== undefined) input.title = values.title
813
+ if (values.status !== undefined) input.status = values.status
814
+ if (values['clear-priority']) input.priority = null
815
+ else if (values.priority !== undefined) input.priority = values.priority
816
+ if (values['clear-order']) input.order = null
817
+ else if (values.order !== undefined) input.order = values.order
818
+ if (values['clear-labels']) input.labels = []
819
+ else if (values.label !== undefined) input.labels = values.label
820
+ if (values['clear-dependencies']) input.dependsOn = []
821
+ else if (values['depends-on'] !== undefined) input.dependsOn = values['depends-on']
822
+ if (values['clear-files']) input.files = []
823
+ else if (values.file !== undefined) input.files = normalizeMutationPaths(repository, values.file)
824
+ if (values['clear-owner']) input.owner = null
825
+ else if (values.owner !== undefined) input.owner = values.owner
826
+ if (values['clear-assignees']) input.assignees = []
827
+ else if (values.assignee !== undefined) input.assignees = values.assignee
828
+ if (values['clear-reviewers']) input.reviewers = []
829
+ else if (values.reviewer !== undefined) input.reviewers = values.reviewer
830
+ if (values['clear-team']) input.team = null
831
+ else if (values.team !== undefined) input.team = values.team
832
+ if (values['clear-estimate']) input.estimate = null
833
+ else if (values.estimate !== undefined) input.estimate = values.estimate
834
+ if (values['clear-effort']) input.effort = null
835
+ else if (values.effort !== undefined) input.effort = values.effort
836
+ if (values['clear-risk']) input.risk = null
837
+ else if (values.risk !== undefined) input.risk = values.risk
838
+ if (values['clear-due-date']) input.dueDate = null
839
+ else if (values['due-date'] !== undefined) input.dueDate = values['due-date']
840
+ if (values['clear-related']) input.related = []
841
+ else if (values.related !== undefined) input.related = values.related
842
+ if (values['clear-duplicates']) input.duplicates = []
843
+ else if (values.duplicate !== undefined) input.duplicates = values.duplicate
844
+ if (values['clear-parent']) input.parent = null
845
+ else if (values.parent !== undefined) input.parent = values.parent
846
+ if (values['clear-directories']) input.directories = []
847
+ else if (values.directory !== undefined) {
848
+ input.directories = normalizeMutationPaths(repository, values.directory)
849
849
  }
850
+ if (values['clear-projects']) input.projects = []
851
+ else if (values.project !== undefined) input.projects = values.project
852
+ if (values['clear-severity']) input.severity = null
853
+ else if (values.severity !== undefined) input.severity = values.severity
854
+ if (values['clear-related-skills']) input.relatedSkills = []
855
+ else if (values['related-skill'] !== undefined) input.relatedSkills = values['related-skill']
856
+ if (values['clear-packs']) input.packs = []
857
+ else if (values.pack !== undefined) input.packs = values.pack
858
+ if (values['clear-class']) input.class = null
859
+ else if (values.class !== undefined) input.class = values.class
860
+ if (values['clear-cadence']) input.cadence = null
861
+ else if (values.cadence !== undefined) input.cadence = values.cadence
862
+ if (values.body !== undefined) input.body = values.body
863
+ return input as UpdateDocumentInput
850
864
  }
851
865
 
852
866
  export async function runCli(args: readonly string[], context: CliContext = {}): Promise<number> {
@@ -866,6 +880,24 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
866
880
  }
867
881
 
868
882
  if (command === 'init' || command === 'config' || command === 'doctor') {
883
+ if (command === 'init') {
884
+ const parsed = parseArgs({
885
+ args: commandArgs,
886
+ allowPositionals: false,
887
+ options: {
888
+ cwd: { type: 'string' },
889
+ config: { type: 'boolean' },
890
+ },
891
+ })
892
+ const values = parseSchema(InitValuesSchema, parsed.values, 'init options')
893
+ const commandCwd = resolveCommandCwd(cwd, values.cwd)
894
+ const repository = await initializeRepository(commandCwd, {
895
+ writeConfig: values.config === true,
896
+ })
897
+ stdout(`Initialized Taskset in ${repository.rootDirectory}\n`)
898
+ return 0
899
+ }
900
+
869
901
  const parsed = parseArgs({
870
902
  args: commandArgs,
871
903
  allowPositionals: false,
@@ -873,13 +905,6 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
873
905
  })
874
906
  const values = parseSchema(CommonValuesSchema, parsed.values, `${command} options`)
875
907
  const commandCwd = resolveCommandCwd(cwd, values.cwd)
876
-
877
- if (command === 'init') {
878
- const repository = await initializeRepository(commandCwd)
879
- stdout(`Initialized Taskset in ${repository.rootDirectory}\n`)
880
- return 0
881
- }
882
-
883
908
  const repository = await discoverRepository(commandCwd)
884
909
 
885
910
  if (command === 'config') {
@@ -889,6 +914,7 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
889
914
  {
890
915
  rootDirectory: repository.rootDirectory,
891
916
  configPath: repository.configPath,
917
+ hasConfig: repository.hasConfig,
892
918
  dataDirectory: repository.dataDirectory,
893
919
  config: repository.config,
894
920
  },
@@ -896,8 +922,10 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
896
922
  2,
897
923
  )}\n`,
898
924
  )
899
- } else {
925
+ } else if (repository.hasConfig) {
900
926
  stdout(`${repository.configPath}\n`)
927
+ } else {
928
+ stdout(`defaults (${repository.rootDirectory})\n`)
901
929
  }
902
930
  return 0
903
931
  }
@@ -907,7 +935,9 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
907
935
  if (values.json) {
908
936
  stdout(`${JSON.stringify(result, null, 2)}\n`)
909
937
  } else if (result.valid) {
910
- stdout(`Taskset repository is valid (${result.taskCount} tasks)\n`)
938
+ stdout(
939
+ `Taskset repository is valid (${result.taskCount} tasks, ${result.documentCount} documents)\n`,
940
+ )
911
941
  } else {
912
942
  for (const diagnostic of result.diagnostics) {
913
943
  stdout(
@@ -958,7 +988,7 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
958
988
  stdout(
959
989
  values.json
960
990
  ? `${JSON.stringify(result, null, 2)}\n`
961
- : `Synced ${result.migrations.length} migrations and generated views\n`,
991
+ : `Synced ${result.migrations.length + result.documentMigrations.length} migrations and generated views\n`,
962
992
  )
963
993
  return 0
964
994
  }
@@ -1082,7 +1112,7 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
1082
1112
  const parsed = parseArgs({
1083
1113
  args: subcommandArgs,
1084
1114
  allowPositionals: true,
1085
- options: { ...metadataOptionDefinitions, ...commonOptionDefinitions },
1115
+ options: { ...documentMetadataOptionDefinitions, ...commonOptionDefinitions },
1086
1116
  })
1087
1117
  const [rawType] = requirePositionals(parsed.positionals, 1, 'exactly one document type')
1088
1118
  const values = parseSchema(
@@ -1117,6 +1147,11 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
1117
1147
  ? { directories: normalizeMutationPaths(repository, values.directory) }
1118
1148
  : {}),
1119
1149
  ...(values.project ? { projects: values.project } : {}),
1150
+ ...(values.severity ? { severity: values.severity } : {}),
1151
+ ...(values['related-skill'] ? { relatedSkills: values['related-skill'] } : {}),
1152
+ ...(values.pack ? { packs: values.pack } : {}),
1153
+ ...(values.class ? { class: values.class } : {}),
1154
+ ...(values.cadence ? { cadence: values.cadence } : {}),
1120
1155
  ...(values.body !== undefined ? { body: values.body } : {}),
1121
1156
  },
1122
1157
  { onWarning },
@@ -1178,6 +1213,8 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
1178
1213
  team: { type: 'string', multiple: true },
1179
1214
  risk: { type: 'string', multiple: true },
1180
1215
  project: { type: 'string', multiple: true },
1216
+ class: { type: 'string', multiple: true },
1217
+ severity: { type: 'string', multiple: true },
1181
1218
  'depends-on': { type: 'string' },
1182
1219
  related: { type: 'string' },
1183
1220
  duplicate: { type: 'string' },
@@ -1220,6 +1257,8 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
1220
1257
  ...(values.team ? { teams: values.team } : {}),
1221
1258
  ...(values.risk ? { risks: values.risk } : {}),
1222
1259
  ...(values.project ? { projects: values.project } : {}),
1260
+ ...(values.class ? { classes: values.class } : {}),
1261
+ ...(values.severity ? { severities: values.severity } : {}),
1223
1262
  ...(values['depends-on'] ? { dependsOn: values['depends-on'] } : {}),
1224
1263
  ...(values.related ? { related: values.related } : {}),
1225
1264
  ...(values.duplicate ? { duplicate: values.duplicate } : {}),
@@ -1335,13 +1374,13 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
1335
1374
 
1336
1375
  if (subcommand === 'update') {
1337
1376
  const clearDefinitions = Object.fromEntries(
1338
- UPDATE_CLEAR_PAIRS.map(([, clear]) => [clear, { type: 'boolean' as const }]),
1377
+ DOCUMENT_UPDATE_CLEAR_PAIRS.map(([, clear]) => [clear, { type: 'boolean' as const }]),
1339
1378
  )
1340
1379
  const parsed = parseArgs({
1341
1380
  args: subcommandArgs,
1342
1381
  allowPositionals: true,
1343
1382
  options: {
1344
- ...metadataOptionDefinitions,
1383
+ ...documentMetadataOptionDefinitions,
1345
1384
  ...clearDefinitions,
1346
1385
  ...commonOptionDefinitions,
1347
1386
  type: { type: 'string' },
@@ -1463,6 +1502,29 @@ export async function runCli(args: readonly string[], context: CliContext = {}):
1463
1502
  return 0
1464
1503
  }
1465
1504
 
1505
+ if (subcommand === 'program') {
1506
+ const parsed = parseArgs({
1507
+ args: subcommandArgs,
1508
+ allowPositionals: true,
1509
+ options: commonOptionDefinitions,
1510
+ })
1511
+ const [parentId] = requirePositionals(parsed.positionals, 1, 'exactly one parent task ID')
1512
+ const values = parseSchema(CommonValuesSchema, parsed.values, 'task program options')
1513
+ const repository = await discoverRepository(resolveCommandCwd(cwd, values.cwd))
1514
+ const rollup = await getProgramRollup(
1515
+ repository,
1516
+ parseSchema(TaskIdSchema, parentId, 'parent task ID'),
1517
+ )
1518
+ if (values.json) {
1519
+ stdout(`${JSON.stringify(rollup, null, 2)}\n`)
1520
+ } else {
1521
+ stdout(
1522
+ `${rollup.parentId}\t${rollup.status}\tchildren=${rollup.children.total}\topen=${rollup.children.openIds.length}\tconcerns=${rollup.relatedOpenConcerns.length}\tcloseoutReady=${rollup.closeoutReady}\t${rollup.title}\n`,
1523
+ )
1524
+ }
1525
+ return 0
1526
+ }
1527
+
1466
1528
  if (subcommand === 'create') {
1467
1529
  const parsed = parseArgs({
1468
1530
  args: subcommandArgs,