runlist 0.0.0 → 0.94.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.
Files changed (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1112 -6
  3. package/assets/app/app.css +283 -0
  4. package/assets/app/app.mjs +381 -0
  5. package/assets/app/block-editor.mjs +182 -0
  6. package/assets/app/block-model.mjs +52 -0
  7. package/assets/app/document-create.mjs +86 -0
  8. package/assets/app/document-lifecycle.mjs +36 -0
  9. package/assets/app/document-stages.mjs +22 -0
  10. package/assets/app/document-views.mjs +33 -0
  11. package/assets/app/document-yardstick.mjs +258 -0
  12. package/assets/app/editor-navigation.mjs +51 -0
  13. package/assets/app/filing-navigation.mjs +138 -0
  14. package/assets/app/git-changes.mjs +80 -0
  15. package/assets/app/git-commit.mjs +46 -0
  16. package/assets/app/index.html +70 -0
  17. package/assets/app/library-navigation.mjs +93 -0
  18. package/assets/app/outline.mjs +18 -0
  19. package/assets/app/quick-navigation.mjs +52 -0
  20. package/assets/app/record-navigation.mjs +78 -0
  21. package/assets/app/recovery-center.mjs +53 -0
  22. package/assets/app/semantic-search.mjs +14 -0
  23. package/assets/app/settings.mjs +15 -0
  24. package/assets/app/shared.mjs +104 -0
  25. package/assets/app/stage-source.mjs +75 -0
  26. package/assets/app/template-editor.mjs +47 -0
  27. package/assets/app/theme.mjs +25 -0
  28. package/assets/app/transport.mjs +43 -0
  29. package/assets/contracts/source-editing-v1.md +232 -0
  30. package/assets/opencode/plugin.js +162 -0
  31. package/assets/schemas/runlist-record-v1.schema.json +912 -0
  32. package/bin/dotmd.mjs +5 -0
  33. package/bin/runlist.mjs +2714 -0
  34. package/package.json +53 -6
  35. package/plugins/runlist-codex/.codex-plugin/plugin.json +18 -0
  36. package/plugins/runlist-codex/bin/runlist-hook +86 -0
  37. package/plugins/runlist-codex/hooks/hooks.json +16 -0
  38. package/plugins/runlist-codex/skills/runlist/SKILL.md +25 -0
  39. package/runlist.config.example.mjs +370 -0
  40. package/scripts/postinstall.mjs +56 -0
  41. package/src/agent-context.mjs +201 -0
  42. package/src/ai.mjs +30 -0
  43. package/src/app-create.mjs +169 -0
  44. package/src/app-filing.mjs +139 -0
  45. package/src/app-git-commits.mjs +48 -0
  46. package/src/app-git.mjs +180 -0
  47. package/src/app-library.mjs +152 -0
  48. package/src/app-lifecycle-worker.mjs +12 -0
  49. package/src/app-lifecycle.mjs +79 -0
  50. package/src/app-records.mjs +134 -0
  51. package/src/app-search.mjs +80 -0
  52. package/src/app-semantic-search.mjs +105 -0
  53. package/src/app-service.mjs +208 -0
  54. package/src/app-yardstick.mjs +269 -0
  55. package/src/app.mjs +102 -0
  56. package/src/atomic-mutation.mjs +1736 -0
  57. package/src/baton.mjs +435 -0
  58. package/src/body-link.mjs +48 -0
  59. package/src/bulk-tag.mjs +174 -0
  60. package/src/check-collapse.mjs +75 -0
  61. package/src/claude-commands.mjs +75 -0
  62. package/src/cli-editor.mjs +8 -0
  63. package/src/code-refs.mjs +545 -0
  64. package/src/codex-integration.mjs +81 -0
  65. package/src/color.mjs +14 -0
  66. package/src/commands.mjs +374 -0
  67. package/src/completions.mjs +93 -0
  68. package/src/config-edit.mjs +606 -0
  69. package/src/config.mjs +705 -0
  70. package/src/decision-action.mjs +58 -0
  71. package/src/decision.mjs +220 -0
  72. package/src/decisions.mjs +787 -0
  73. package/src/deps.mjs +317 -0
  74. package/src/desktop-install.mjs +158 -0
  75. package/src/desktop-runtime.mjs +4 -0
  76. package/src/desktop.mjs +25 -0
  77. package/src/diff.mjs +101 -0
  78. package/src/doctor.mjs +591 -0
  79. package/src/durable-rename.mjs +55 -0
  80. package/src/errors-read.mjs +44 -0
  81. package/src/export.mjs +496 -0
  82. package/src/extractors.mjs +101 -0
  83. package/src/filing.mjs +255 -0
  84. package/src/fix-membership.mjs +134 -0
  85. package/src/fix-refs.mjs +174 -0
  86. package/src/flags.mjs +318 -0
  87. package/src/frontmatter-fix.mjs +195 -0
  88. package/src/frontmatter.mjs +252 -0
  89. package/src/git-commit-filters.mjs +32 -0
  90. package/src/git-commit-protocol.mjs +198 -0
  91. package/src/git-commit-signing.mjs +75 -0
  92. package/src/git-commit-store.mjs +26 -0
  93. package/src/git-commit-support.mjs +16 -0
  94. package/src/git-commit-worker.mjs +32 -0
  95. package/src/git.mjs +909 -0
  96. package/src/glossary-check.mjs +35 -0
  97. package/src/glossary.mjs +243 -0
  98. package/src/graph.mjs +298 -0
  99. package/src/guard.mjs +488 -0
  100. package/src/health.mjs +212 -0
  101. package/src/hints.mjs +129 -0
  102. package/src/host-integration.mjs +339 -0
  103. package/src/hub-membership.mjs +284 -0
  104. package/src/hub.mjs +296 -0
  105. package/src/hud.mjs +436 -0
  106. package/src/index-file.mjs +163 -0
  107. package/src/index.mjs +478 -0
  108. package/src/init.mjs +512 -0
  109. package/src/install.mjs +222 -0
  110. package/src/journal-read.mjs +116 -0
  111. package/src/journal.mjs +472 -0
  112. package/src/lifecycle.mjs +1444 -0
  113. package/src/lint.mjs +386 -0
  114. package/src/managed-path.mjs +192 -0
  115. package/src/markdown-code-spans.mjs +58 -0
  116. package/src/migrate-prompts.mjs +171 -0
  117. package/src/migrate-template.mjs +170 -0
  118. package/src/migrate.mjs +90 -0
  119. package/src/misuse-read.mjs +61 -0
  120. package/src/model-request.mjs +33 -0
  121. package/src/model.mjs +523 -0
  122. package/src/modules.mjs +355 -0
  123. package/src/naming.mjs +55 -0
  124. package/src/native-action.mjs +59 -0
  125. package/src/native-record.mjs +244 -0
  126. package/src/new.mjs +1321 -0
  127. package/src/output-identity.mjs +111 -0
  128. package/src/parent-plan.mjs +58 -0
  129. package/src/parse-cache.mjs +99 -0
  130. package/src/path-containment.mjs +15 -0
  131. package/src/pickup-card.mjs +293 -0
  132. package/src/pickup.mjs +671 -0
  133. package/src/prompt.mjs +25 -0
  134. package/src/prompts.mjs +585 -0
  135. package/src/public-inspection.mjs +19 -0
  136. package/src/query.mjs +977 -0
  137. package/src/record-action.mjs +11 -0
  138. package/src/record-create.mjs +82 -0
  139. package/src/reference-planner.mjs +388 -0
  140. package/src/reference-values.mjs +11 -0
  141. package/src/rename.mjs +107 -0
  142. package/src/render.mjs +678 -0
  143. package/src/roadmap.mjs +184 -0
  144. package/src/runlist.mjs +944 -0
  145. package/src/section.mjs +225 -0
  146. package/src/semantic-search-client.mjs +58 -0
  147. package/src/ship.mjs +166 -0
  148. package/src/show.mjs +101 -0
  149. package/src/skill-drift.mjs +98 -0
  150. package/src/source-editor.mjs +491 -0
  151. package/src/stages.mjs +73 -0
  152. package/src/state-migration.mjs +123 -0
  153. package/src/stats.mjs +200 -0
  154. package/src/status-metadata.mjs +87 -0
  155. package/src/statuses.mjs +721 -0
  156. package/src/summary.mjs +74 -0
  157. package/src/surfaces.mjs +28 -0
  158. package/src/sync-status.mjs +316 -0
  159. package/src/template-store.mjs +62 -0
  160. package/src/update.mjs +338 -0
  161. package/src/use.mjs +58 -0
  162. package/src/util.mjs +337 -0
  163. package/src/validate.mjs +795 -0
  164. package/src/watch.mjs +56 -0
  165. package/src/xref.mjs +406 -0
  166. package/src/yardstick-command.mjs +178 -0
  167. package/src/yardstick.mjs +112 -0
@@ -0,0 +1,374 @@
1
+ // Canonical command grammar. Execution starts at bin/runlist.mjs; this schema owns
2
+ // names, aliases, visibility, options, positional arity, help groups, and policy.
3
+ const none = Object.freeze({ mutation: 'none', pathPolicy: 'read-only' });
4
+ const mutates = (pathPolicy) => Object.freeze({ mutation: 'conditional', pathPolicy });
5
+
6
+ const flag = (...names) => Object.freeze({ names: Object.freeze(names), arity: 0 });
7
+ const value = (...names) => Object.freeze({ names: Object.freeze(names), arity: 1 });
8
+ const optionalValue = (...names) => Object.freeze({ names: Object.freeze(names), arity: 'optional' });
9
+ const positionals = (min = 0, max = min) => Object.freeze({ min, max });
10
+ const form = (syntax, { subcommands = [], args = positionals(), options = [], passthrough = false, dashPositionalsAfter = Infinity } = {}) => Object.freeze({
11
+ syntax,
12
+ subcommands: Object.freeze(subcommands),
13
+ positionals: args,
14
+ options: Object.freeze(options),
15
+ passthrough,
16
+ dashPositionalsAfter,
17
+ });
18
+
19
+ export const GLOBAL_OPTIONS = Object.freeze([
20
+ value('--config'), value('--root'), value('--type'),
21
+ flag('--dry-run', '-n'), flag('--verbose'), flag('--help', '-h'), flag('--version', '-v'),
22
+ ]);
23
+
24
+ const QUERY_OPTIONS = Object.freeze([
25
+ value('--type'), value('--status'), value('--keyword'), flag('--body'), value('--owner'),
26
+ value('--surface'), value('--module'), value('--domain'), value('--audience'),
27
+ value('--execution-mode'), value('--updated-since'), value('--limit'), value('--sort'),
28
+ value('--group', '--group-by'), value('--stage'), flag('--all'), flag('--include-archived'), flag('--exclude-archived'),
29
+ flag('--stale'), flag('--has-next-step'), flag('--has-blockers'), flag('--checklist-open'),
30
+ flag('--json'), flag('--git'), flag('--summarize'), value('--summarize-limit'), value('--model'),
31
+ ]);
32
+ const LIFECYCLE_OPTIONS = Object.freeze([
33
+ value('--note'), flag('--no-index'), flag('--show-files'), flag('--force'),
34
+ ]);
35
+ const STATUS_PROPERTY_OPTIONS = Object.freeze([
36
+ value('--type'), value('--like'), flag('--yes', '-y'), flag('--ignore-lifecycle-override'), flag('--json'),
37
+ value('--context'), value('--staleDays'),
38
+ flag('--requiresModule', '--no-requiresModule'), flag('--terminal', '--no-terminal'),
39
+ flag('--archive', '--no-archive'), flag('--skipStale', '--no-skipStale'),
40
+ flag('--skipWarnings', '--no-skipWarnings'), flag('--quiet', '--no-quiet'),
41
+ ]);
42
+
43
+ const command = (name, policy, group, forms, extra = {}) => Object.freeze({
44
+ name,
45
+ policy,
46
+ group,
47
+ forms: Object.freeze(forms),
48
+ aliases: Object.freeze(extra.aliases ?? []),
49
+ visibility: extra.visibility ?? 'public',
50
+ });
51
+
52
+ const definitions = [
53
+ command('help', none, 'setup', [form('[topic]', { args: positionals(0, 1) })]),
54
+ command('completions', none, 'setup', [form('<bash|zsh>', { args: positionals(1, 1) })]),
55
+ command('init', mutates('repository setup paths and config files'), 'setup', [form('', { options: [flag('--force')] })]),
56
+ command('watch', mutates('proxy: child command policy applies'), 'setup', [form('[command...]', { args: positionals(0, Infinity), passthrough: true })]),
57
+ command('desktop', mutates('explicit desktop installation; otherwise launch only'), 'setup', [
58
+ form('[--app <application>]', {options:[value('--app')]}),
59
+ form('install [--from <installer.dmg>]', {subcommands:['install'],options:[value('--from')]}),
60
+ ]),
61
+ command('app', mutates('authenticated revision-checked document edits and private editor state'), 'setup', [form('[document] [--port <port>]', { args: positionals(0, 1), options: [value('--port')] })]),
62
+
63
+ command('list', none, 'read', [form('', { options: [flag('--json'), flag('--verbose')] })]),
64
+ command('json', none, 'read', [form('')]),
65
+ command('coverage', none, 'read', [form('', { options: [flag('--json')] })]),
66
+ command('yardstick', mutates('explicit owner assessments and existing archive workflow; show is read-only'), 'mutate', [
67
+ form('show [plan]', { subcommands: ['show'], args: positionals(0, 1), options: [flag('--json')] }),
68
+ form('mark <plan> serves', { subcommands: ['mark'], args: positionals(2, 2), options: [value('--reason'), flag('--json'), flag('--no-index')] }),
69
+ form('close <plan> --reason <text>', { subcommands: ['close'], args: positionals(1, 1), options: [value('--reason'), flag('--json'), flag('--no-index')] }),
70
+ form('fold <plan> --into <plan> --reason <text>', { subcommands: ['fold'], args: positionals(1, 1), options: [value('--into'), value('--reason'), flag('--json'), flag('--no-index')] }),
71
+ form('clear <plan>', { subcommands: ['clear'], args: positionals(1, 1), options: [flag('--json'), flag('--no-index')] }),
72
+ ]),
73
+ command('stats', none, 'read', [form('', { options: [flag('--json')] })]),
74
+ command('graph', none, 'read', [form('', { options: [flag('--dot'), flag('--json'), value('--status'), value('--module'), value('--surface')] })]),
75
+ command('deps', none, 'read', [form('[file]', { args: positionals(0, 1), options: [flag('--json'), value('--depth')] })]),
76
+ command('briefing', none, 'read', [form('', { options: [flag('--json')] })]),
77
+ command('context', none, 'read', [form('', { options: [flag('--json'), flag('--compact'), flag('--summarize'), value('--model'), value('--sections'), value('--max-bytes')] })]),
78
+ command('agent-context', none, 'read', [form('', { options: [flag('--json'), value('--sections'), value('--max-bytes')] })]),
79
+ command('hud', none, 'read', [form('', { options: [flag('--json'), flag('--subagent'), flag('--prompt-submit')] })]),
80
+ command('focus', none, 'read', [form('[status]', { args: positionals(0, 1), options: [flag('--json')] })]),
81
+ command('query', none, 'read', [form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS })]),
82
+ command('grep', none, 'read', [form('<term> [terms...]', { args: positionals(1, Infinity), options: QUERY_OPTIONS })]),
83
+ command('plans', none, 'read', [
84
+ form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS }),
85
+ form('status [terms...]', { subcommands: ['status'], args: positionals(0, Infinity), options: QUERY_OPTIONS }),
86
+ ]),
87
+ command('runlists', none, 'read', [form('', { options: [flag('--json'), value('--limit'), value('--sort')] })]),
88
+ command('roadmaps', none, 'read', [form('', { options: [flag('--json')] })]),
89
+ command('diff', none, 'read', [form('[file]', { args: positionals(0, 1), options: [flag('--stat'), value('--since'), flag('--summarize'), value('--model')] })]),
90
+ command('summary', none, 'read', [form('<file>', { args: positionals(1, 1), options: [value('--model'), value('--max-tokens'), flag('--json')] })]),
91
+ command('unblocks', none, 'read', [form('<file>', { args: positionals(1, 1), options: [flag('--json')] })]),
92
+ command('health', none, 'read', [form('', { options: [flag('--json')] })]),
93
+ command('flags', none, 'read', [form('', { options: [flag('--all'), flag('--json')] })]),
94
+ command('flag', mutates('the flags log under the state directory'), 'mutate', [
95
+ form('add <file[:line]> <text...>', { subcommands: ['add'], args: positionals(2, Infinity), options: [value('--severity'), value('--by')] }),
96
+ form('accept <id>', { subcommands: ['accept'], args: positionals(1, 1), options: [value('--note'), value('--by')] }),
97
+ form('reject <id>', { subcommands: ['reject'], args: positionals(1, 1), options: [value('--note'), value('--by')] }),
98
+ form('resolve <id>', { subcommands: ['resolve'], args: positionals(1, 1), options: [value('--note'), value('--by')] }),
99
+ form('show <id>', { subcommands: ['show'], args: positionals(1, 1) }),
100
+ form('sync <check-name> [findings]', { subcommands: ['sync'], args: positionals(1, 2), dashPositionalsAfter: 1 }),
101
+ ]),
102
+ command('glossary', none, 'read', [form('[term]', { args: positionals(0, 1), options: [flag('--list'), flag('--json')] })]),
103
+ command('model', mutates('~/.runlist/model.json and the local model server'), 'read', [
104
+ form('', { options: [flag('--json')] }),
105
+ form('status', { subcommands: ['status'], options: [flag('--json')] }),
106
+ form('start', { subcommands: ['start'], options: [flag('--force')] }),
107
+ form('stop', { subcommands: ['stop'], options: [flag('--all')] }),
108
+ form('measure [name...]', { subcommands: ['measure'], args: positionals(0, Infinity) }),
109
+ form('use <name>', { subcommands: ['use'], args: positionals(1, 1) }),
110
+ form('cap <gb>', { subcommands: ['cap'], args: positionals(1, 1) }),
111
+ ]),
112
+ command('show', none, 'read', [form('<file...>', { args: positionals(1, Infinity), options: [flag('--json')] })]),
113
+ command('xref', mutates('the flags log under the state directory with --check --flag; otherwise read-only'), 'read', [
114
+ form('<file...> | --check [--flag]', { args: positionals(0, Infinity), options: [flag('--json'), flag('--check'), flag('--flag')] }),
115
+ ]),
116
+ command('record', mutates('native flag or decision through the shared domain writer'), 'mutate', [form('<file> <action>', { args: positionals(2, 2), options: [value('--note'), value('--option'), value('--expected-revision'), flag('--json')] })]),
117
+ command('decision', mutates('selected decision source through the source editor'), 'mutate', [form('<file> <id>', { args: positionals(2, 2), options: [value('--disposition'), value('--note'), value('--choice'), value('--expected-revision'), flag('--json')] })]),
118
+ command('decisions', none, 'read', [form('[doc-or-id]', { args: positionals(0, 1), options: [flag('--all'), flag('--json'), flag('--check')] })]),
119
+ command('modules', none, 'read', [form('', { options: [value('--sort'), value('--limit'), flag('--all'), flag('--json')] })]),
120
+ command('module', none, 'read', [form('<name>', { args: positionals(1, 1), options: [value('--sort'), flag('--json')] })]),
121
+ command('surfaces', none, 'read', [form('', { options: [flag('--json')] })]),
122
+ command('journal', none, 'read', [form('', { options: [value('--tail'), flag('--errors'), value('--session'), value('--since'), flag('--by-command'), flag('--help-topics'), flag('--json')] })]),
123
+ command('errors', none, 'read', [form('', { options: [flag('--json'), flag('--by-family'), value('--limit'), value('--tail'), value('--repo')] })]),
124
+ command('misuse', none, 'read', [form('', { options: [flag('--json'), value('--tail'), flag('--by-rule'), value('--repo')] })]),
125
+
126
+ command('roadmap', mutates('managed source when `next`; otherwise read-only'), 'workflow', [
127
+ form('[hub]', { args: positionals(0, 1), options: [flag('--json')] }),
128
+ form('next [hub]', { subcommands: ['next'], args: positionals(0, 1), options: [flag('--json'), flag('--full'), flag('--no-index')] }),
129
+ ]),
130
+ command('prompts', mutates('managed sources/destinations by subcommand'), 'workflow', [
131
+ form('', { options: [flag('--json'), value('--status'), flag('--include-archived'), value('--sort'), value('--limit'), flag('--all')] }),
132
+ form('list', { subcommands: ['list', 'status'], options: [flag('--json'), value('--status'), flag('--include-archived'), value('--sort'), value('--limit'), flag('--all')] }),
133
+ form('next', { subcommands: ['next'], options: [flag('--no-claim')] }),
134
+ form('use [file]', { subcommands: ['use', 'resume'], args: positionals(0, 1), options: [flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] }),
135
+ form('show [file...]', { subcommands: ['show', 'peek'], args: positionals(0, Infinity), options: [flag('--json'), flag('--all'), value('--limit')] }),
136
+ form('archive <file>', { subcommands: ['archive'], args: positionals(1, 1), options: [flag('--no-index'), flag('--show-files')] }),
137
+ form('new <slug> [body...]', { subcommands: ['new'], args: positionals(1, Infinity), options: [value('--body', '--message'), value('--title'), value('--status')] }),
138
+ form('hold <file>', { subcommands: ['hold', 'shelve'], args: positionals(1, 1) }),
139
+ form('unhold <file>', { subcommands: ['unhold', 'unshelve'], args: positionals(1, 1) }),
140
+ ], { aliases: ['prompt'] }),
141
+ command('use', mutates('managed source when starting/consuming; docs remain read-only'), 'workflow', [form('[file]', { args: positionals(0, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] })]),
142
+ command('next', mutates('managed prompt source and same-root archive destination'), 'workflow', [form('', { options: [flag('--json'), flag('--no-index'), flag('--show-files'), flag('--force'), flag('--no-claim')] })]),
143
+ command('baton', mutates('managed plan/prompt sources and managed prompt destination'), 'workflow', [form('[plan|slug] <@<file>|->', { args: positionals(0, 2), options: [value('--status'), value('--note'), value('--body', '--message'), flag('--force'), flag('--replace'), flag('--json')] })]),
144
+ command('runlist', mutates('managed hubs/children and managed scaffold destinations'), 'workflow', [
145
+ form('<hub>', { args: positionals(1, 1), options: [flag('--json')] }),
146
+ form('next <hub>', { subcommands: ['next'], args: positionals(1, 1), options: [flag('--json'), flag('--full'), flag('--no-index'), flag('--show-files')] }),
147
+ form('add <hub> <child...>', { subcommands: ['add'], args: positionals(2, Infinity), options: [flag('--json')] }),
148
+ form('remove <hub> <child...>', { subcommands: ['remove'], args: positionals(2, Infinity), options: [flag('--json'), flag('--clear-parent')] }),
149
+ form('reorder <hub> <child...>', { subcommands: ['reorder'], args: positionals(2, Infinity), options: [flag('--json'), value('--before'), value('--after')] }),
150
+ ]),
151
+
152
+ command('export', mutates('external output path intentionally unrestricted'), 'mutate', [form('[file]', { args: positionals(0, 1), options: [value('--format'), value('--output'), value('--status'), value('--module'), value('--root'), value('--type')] })]),
153
+ command('guard', mutates('global misuse log; no managed document writes'), 'mutate', [form('')]),
154
+ command('update', mutates('global CLI and plugin installation state'), 'mutate', [form('', { options: [flag('--check'), flag('--cli-only'), flag('--plugin-only')] })]),
155
+ command('install', mutates('agent-host plugin directory outside the repo'), 'mutate', [
156
+ form('[host]', { args: positionals(0, 1), options: [flag('--remove'), flag('--force'), flag('--json'), value('--path')] }),
157
+ ]),
158
+ command('status', mutates('managed source and same-root destination'), 'mutate', [form('<file> [status]', { args: positionals(1, 2), options: LIFECYCLE_OPTIONS })]),
159
+ command('set', mutates('managed source and same-root destination'), 'mutate', [form('<status> [file]', { args: positionals(1, 2), options: LIFECYCLE_OPTIONS })]),
160
+ command('ship', mutates('repository release/index paths and global release tooling'), 'mutate', [form('[patch|minor|major]', { args: positionals(0, 1) })]),
161
+ command('archive', mutates('managed source and same-root destination'), 'mutate', [form('<file>', { args: positionals(1, 1), options: [...LIFECYCLE_OPTIONS, flag('--closeout-template'), flag('--fix-refs'), flag('--strings')] })]),
162
+ command('bulk', mutates('managed source sweep; archive destinations preserve roots'), 'mutate', [
163
+ form('archive <files...>', { subcommands: ['archive'], args: positionals(1, Infinity), options: [flag('--json'), flag('--no-index'), flag('--show-files')] }),
164
+ form('tag [files...]', { subcommands: ['tag'], args: positionals(0, Infinity), options: [value('--type'), value('--status'), flag('--json')] }),
165
+ ]),
166
+ command('bulk-tag', mutates('managed source sweep'), 'mutate', [form('[files...]', { args: positionals(0, Infinity), options: [value('--type'), value('--status'), flag('--json')] })]),
167
+ command('touch', mutates('managed source or managed source sweep'), 'mutate', [form('[file...]', { args: positionals(0, Infinity), options: [flag('--git')] })]),
168
+ command('new', mutates('managed document destination; external body input unrestricted'), 'mutate', [form('[type] <name> [body...]', {
169
+ args: positionals(0, Infinity),
170
+ options: [value('--status'), value('--title'), value('--runlist'), flag('--coordination'), flag('--roadmap'), flag('--lite', '--minimal'), flag('--audit', '--findings'), value('--body', '--message'), value('--root'), flag('--show-files'), flag('--list-templates', '--list-types'), flag('--render-preview'), value('--question'), value('--answers'), value('--disposition')],
171
+ dashPositionalsAfter: 1,
172
+ })]),
173
+ command('lint', mutates('managed source sweep with --fix; otherwise read-only'), 'mutate', [form('', { options: [flag('--fix')] })]),
174
+ command('rename', mutates('managed source, same-root destination, and rewrite sweep'), 'mutate', [form('<old> [new]', { args: positionals(1, 2), options: [flag('--show-files'), flag('--fix-refs'), flag('--strings')] })]),
175
+ command('migrate', mutates('managed source sweep'), 'mutate', [form('<field> <old> <new> [files...]', { args: positionals(3, Infinity), options: [flag('--show-files')] })]),
176
+ command('fix-refs', mutates('managed source sweep'), 'mutate', [form('', { options: [flag('--show-files')] })]),
177
+ command('refs', mutates('configured code roots with --fix; otherwise read-only'), 'mutate', [
178
+ form('repair', { subcommands: ['repair'], options: [flag('--fix'), flag('--strings')] }),
179
+ form('<old> <new>', { args: positionals(2, 2), options: [flag('--fix'), flag('--strings')] }),
180
+ ]),
181
+ command('fix-membership', mutates('managed source sweep'), 'mutate', [form('[hubs...]', { args: positionals(0, Infinity), options: [flag('--json')] })]),
182
+ command('sync-status', mutates('managed source sweep'), 'mutate', [form('[hubs...]', { args: positionals(0, Infinity), options: [flag('--adopt'), flag('--json')] })]),
183
+ command('doctor', mutates('managed sweeps, repo index, and maintenance config paths by mode'), 'mutate', [form('[path]', { args: positionals(0, 1), options: [flag('--apply', '--yes'), flag('--statuses'), optionalValue('--migrate-template'), flag('--migrate-prompts'), flag('--frontmatter-fix'), flag('--project'), flag('--transactions'), flag('--claims'), flag('--session'), value('--older-than'), flag('--json'), flag('--include-archived')] })]),
184
+ command('statuses', mutates('project config path; document scan is read-only'), 'mutate', [
185
+ form('list', { subcommands: ['list'], options: [value('--type'), flag('--json')] }),
186
+ form('add <name>', { subcommands: ['add'], args: positionals(1, 1), options: STATUS_PROPERTY_OPTIONS }),
187
+ form('set <name>', { subcommands: ['set'], args: positionals(1, 1), options: STATUS_PROPERTY_OPTIONS }),
188
+ form('remove <name>', { subcommands: ['remove'], args: positionals(1, 1), options: STATUS_PROPERTY_OPTIONS }),
189
+ form('migrate <type>', { subcommands: ['migrate'], args: positionals(1, 1), options: [flag('--yes', '-y'), flag('--json'), flag('--ignore-lifecycle-override')] }),
190
+ form('', { options: [value('--type'), flag('--json')] }),
191
+ ]),
192
+ command('check', mutates('managed fix sweeps and repo-generated index; otherwise validation'), 'mutate', [form('[paths...]', { args: positionals(0, Infinity), options: [flag('--fix'), flag('--errors-only'), flag('--no-collapse'), flag('--json'), flag('--verbose'), value('--min-docs'), flag('--flag')] })]),
193
+ command('index', mutates('repo-generated index destination; --print is read-only'), 'mutate', [form('', { options: [flag('--print')] })]),
194
+
195
+ command('self-check', none, 'internal', [form('', { options: [flag('--json')] })], { visibility: 'internal' }),
196
+ command('pickup', none, 'removed', [form('[args...]', { args: positionals(0, Infinity) })], { visibility: 'removed' }),
197
+ command('unpickup', none, 'removed', [form('[args...]', { args: positionals(0, Infinity) })], { visibility: 'removed' }),
198
+ command('release', none, 'removed', [form('[args...]', { args: positionals(0, Infinity) })], { visibility: 'removed' }),
199
+ command('finish', none, 'removed', [form('[args...]', { args: positionals(0, Infinity) })], { visibility: 'removed' }),
200
+ command('handoff', none, 'removed', [form('[args...]', { args: positionals(0, Infinity) })], { visibility: 'removed' }),
201
+
202
+ command('stale', none, 'preset', [form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS })]),
203
+ command('actionable', none, 'preset', [form('[terms...]', { args: positionals(0, Infinity), options: QUERY_OPTIONS })]),
204
+ ];
205
+
206
+ function schemaErrors(schema) {
207
+ const errors = [];
208
+ const names = new Set();
209
+ const aliases = new Set();
210
+ for (const entry of schema) {
211
+ if (!entry.name || names.has(entry.name)) errors.push(`duplicate or empty command name: ${entry.name}`);
212
+ names.add(entry.name);
213
+ if (!entry.group) errors.push(`${entry.name}: missing help group`);
214
+ if (!entry.forms.length) errors.push(`${entry.name}: missing forms`);
215
+ for (const alias of entry.aliases) {
216
+ if (!alias || names.has(alias) || aliases.has(alias)) errors.push(`${entry.name}: duplicate alias ${alias}`);
217
+ aliases.add(alias);
218
+ }
219
+ const formKeys = new Set();
220
+ for (const commandForm of entry.forms) {
221
+ const key = commandForm.subcommands.join('|') || '<default>';
222
+ if (formKeys.has(key)) errors.push(`${entry.name}: duplicate form ${key}`);
223
+ formKeys.add(key);
224
+ const { min, max } = commandForm.positionals;
225
+ if (!Number.isInteger(min) || min < 0 || !(max === Infinity || Number.isInteger(max)) || max < min) {
226
+ errors.push(`${entry.name} ${key}: malformed positional arity`);
227
+ }
228
+ const optionNames = new Set();
229
+ for (const option of commandForm.options) {
230
+ if (![0, 1, 'optional'].includes(option.arity) || !option.names.length) errors.push(`${entry.name} ${key}: malformed option`);
231
+ for (const optionName of option.names) {
232
+ if (!optionName.startsWith('-') || optionNames.has(optionName)) errors.push(`${entry.name} ${key}: duplicate or malformed option ${optionName}`);
233
+ optionNames.add(optionName);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ for (const alias of aliases) if (names.has(alias)) errors.push(`alias collides with command: ${alias}`);
239
+ return errors;
240
+ }
241
+
242
+ const errors = schemaErrors(definitions);
243
+ if (errors.length) throw new Error(`Invalid command schema:\n${errors.join('\n')}`);
244
+
245
+ export const COMMAND_SCHEMA = Object.freeze(Object.fromEntries(definitions.map(entry => [entry.name, entry])));
246
+ const ALIASES = Object.freeze(Object.fromEntries(definitions.flatMap(entry => entry.aliases.map(alias => [alias, entry.name]))));
247
+
248
+ export const COMMAND_POLICIES = Object.freeze(Object.fromEntries(definitions.map(entry => [entry.name, entry.policy])));
249
+ export const KNOWN_COMMANDS = Object.freeze(definitions.map(entry => entry.name));
250
+ export const PUBLIC_COMMANDS = Object.freeze(definitions.filter(entry => entry.visibility === 'public').map(entry => entry.name));
251
+ export const COMPLETION_COMMANDS = Object.freeze(definitions
252
+ .filter(entry => entry.visibility === 'public')
253
+ .flatMap(entry => [entry.name, ...entry.aliases]));
254
+ export const MUTATION_CAPABLE_COMMANDS = Object.freeze(new Set(
255
+ definitions.filter(entry => entry.policy.mutation !== 'none').map(entry => entry.name),
256
+ ));
257
+
258
+ export function canonicalCommand(name) {
259
+ return ALIASES[name] ?? name;
260
+ }
261
+
262
+ export function commandDefinition(name) {
263
+ return COMMAND_SCHEMA[canonicalCommand(name)] ?? null;
264
+ }
265
+
266
+ export function commandPolicy(name) {
267
+ return commandDefinition(name)?.policy ?? null;
268
+ }
269
+
270
+ export function commandOwnsOption(name, optionName) {
271
+ const definition = commandDefinition(name);
272
+ return Boolean(definition?.forms.some(commandForm => commandForm.options.some(option => option.names.includes(optionName))));
273
+ }
274
+
275
+ export function commandCompletionWords(name) {
276
+ const definition = commandDefinition(name);
277
+ if (!definition) return [];
278
+ const words = new Set();
279
+ for (const commandForm of definition.forms) {
280
+ for (const subcommand of commandForm.subcommands) words.add(subcommand);
281
+ for (const option of commandForm.options) for (const optionName of option.names) words.add(optionName);
282
+ }
283
+ return [...words];
284
+ }
285
+
286
+ export function commandUsage(name) {
287
+ const definition = commandDefinition(name);
288
+ if (!definition) return null;
289
+ return definition.forms.map(commandForm => `runlist ${definition.name}${commandForm.syntax ? ` ${commandForm.syntax}` : ''}`).join('\n');
290
+ }
291
+
292
+ function optionMap(forms) {
293
+ const map = new Map();
294
+ for (const commandForm of forms) {
295
+ for (const option of commandForm.options) {
296
+ for (const optionName of option.names) map.set(optionName, option);
297
+ }
298
+ }
299
+ return map;
300
+ }
301
+
302
+ function scanArgs(name, argv, options, passthrough = false, dashPositionalsAfter = Infinity) {
303
+ const positional = [];
304
+ for (let i = 0; i < argv.length; i += 1) {
305
+ const arg = argv[i];
306
+ if (arg === '-') { positional.push(arg); continue; }
307
+ if (!arg.startsWith('-')) { positional.push(arg); continue; }
308
+ const option = options.get(arg);
309
+ if (!option) {
310
+ if (passthrough) continue;
311
+ if (positional.length >= dashPositionalsAfter) { positional.push(arg); continue; }
312
+ throw new Error(`Unknown flag for \`runlist ${name}\`: ${arg}`);
313
+ }
314
+ if (option.arity === 1) {
315
+ const next = argv[i + 1];
316
+ if (next === undefined || options.has(next)) {
317
+ throw new Error(`Missing value for \`${arg}\` in \`runlist ${name}\`.`);
318
+ }
319
+ i += 1;
320
+ } else if (option.arity === 'optional' && argv[i + 1] !== undefined && !argv[i + 1].startsWith('-')) {
321
+ i += 1;
322
+ }
323
+ }
324
+ return positional;
325
+ }
326
+
327
+ export function normalizeCommandArgs(name, argv) {
328
+ const canonical = canonicalCommand(name);
329
+ if (canonical !== 'roadmap') return [...argv];
330
+ const positionalIndexes = [];
331
+ const options = optionMap(commandDefinition(canonical).forms);
332
+ for (let i = 0; i < argv.length; i += 1) {
333
+ const option = options.get(argv[i]);
334
+ if (option) {
335
+ if (option.arity === 1) i += 1;
336
+ else if (option.arity === 'optional' && argv[i + 1] !== undefined && !argv[i + 1].startsWith('-')) i += 1;
337
+ continue;
338
+ }
339
+ if (!argv[i].startsWith('-')) positionalIndexes.push(i);
340
+ }
341
+ if (positionalIndexes.length === 2 && argv[positionalIndexes[1]] === 'next' && argv[positionalIndexes[0]] !== 'next') {
342
+ const normalized = [...argv];
343
+ const hub = normalized[positionalIndexes[0]];
344
+ normalized[positionalIndexes[0]] = 'next';
345
+ normalized[positionalIndexes[1]] = hub;
346
+ return normalized;
347
+ }
348
+ return [...argv];
349
+ }
350
+
351
+ export function validateCommandArgs(name, argv, { preset = false } = {}) {
352
+ const definition = preset ? COMMAND_SCHEMA.query : commandDefinition(name);
353
+ if (!definition) return argv;
354
+ const canonical = definition.name;
355
+ const normalized = normalizeCommandArgs(canonical, argv);
356
+ const allOptions = optionMap(definition.forms);
357
+ const allPassthrough = definition.forms.some(commandForm => commandForm.passthrough);
358
+ const dashPositionalsAfter = Math.min(...definition.forms.map(commandForm => commandForm.dashPositionalsAfter));
359
+ const preliminary = scanArgs(canonical, normalized, allOptions, allPassthrough, dashPositionalsAfter);
360
+ const selected = definition.forms.find(commandForm => commandForm.subcommands.includes(preliminary[0]))
361
+ ?? definition.forms.find(commandForm => commandForm.subcommands.length === 0);
362
+ if (!selected) throw new Error(`Unknown subcommand for \`runlist ${canonical}\`: ${preliminary[0] ?? '(missing)'}`);
363
+ const selectedOptions = optionMap([selected]);
364
+ const positional = scanArgs(canonical, normalized, selectedOptions, selected.passthrough, selected.dashPositionalsAfter);
365
+ const args = selected.subcommands.includes(positional[0]) ? positional.slice(1) : positional;
366
+ if (args.length < selected.positionals.min || args.length > selected.positionals.max) {
367
+ throw new Error(`Usage: ${commandUsage(canonical)}`);
368
+ }
369
+ return normalized;
370
+ }
371
+
372
+ export function validateCommandSchema(schema = definitions) {
373
+ return schemaErrors(schema);
374
+ }
@@ -0,0 +1,93 @@
1
+ import { die } from './util.mjs';
2
+ import { COMPLETION_COMMANDS, GLOBAL_OPTIONS, PUBLIC_COMMANDS, commandCompletionWords } from './commands.mjs';
3
+
4
+ const GLOBAL_FLAGS = GLOBAL_OPTIONS.flatMap(option => option.names);
5
+ const COMMAND_WORDS = Object.freeze(Object.fromEntries(
6
+ PUBLIC_COMMANDS.map(command => [command, commandCompletionWords(command)]),
7
+ ));
8
+
9
+ function bashCompletion() {
10
+ return `# runlist bash completion
11
+ # Add to ~/.bashrc: eval "$(runlist completions bash)"
12
+ _runlist() {
13
+ local cur cmd expect_value
14
+ cur="\${COMP_WORDS[COMP_CWORD]}"
15
+ cmd=""
16
+ expect_value=0
17
+
18
+ # Find the command while skipping pre-command global flags and their values.
19
+ for ((i=1; i < COMP_CWORD; i++)); do
20
+ if (( expect_value )); then expect_value=0; continue; fi
21
+ case "\${COMP_WORDS[i]}" in
22
+ --config|--root|--type) expect_value=1 ;;
23
+ --dry-run|-n|--verbose|--help|-h|--version|-v) ;;
24
+ -*) ;;
25
+ *) cmd="\${COMP_WORDS[i]}"; break ;;
26
+ esac
27
+ done
28
+
29
+ if [[ -z "$cmd" ]]; then
30
+ COMPREPLY=( $(compgen -W "${COMPLETION_COMMANDS.join(' ')} ${GLOBAL_FLAGS.join(' ')}" -- "$cur") )
31
+ return
32
+ fi
33
+
34
+ case "$cmd" in
35
+ ${Object.entries(COMMAND_WORDS).map(([command, words]) =>
36
+ ` ${command}) COMPREPLY=( $(compgen -W "${words.join(' ')} ${GLOBAL_FLAGS.join(' ')}" -- "$cur") ) ;;`
37
+ ).join('\n')}
38
+ prompt) COMPREPLY=( $(compgen -W "${commandCompletionWords('prompts').join(' ')} ${GLOBAL_FLAGS.join(' ')}" -- "$cur") ) ;;
39
+ *) COMPREPLY=( $(compgen -W "${GLOBAL_FLAGS.join(' ')}" -- "$cur") ) ;;
40
+ esac
41
+ }
42
+ complete -F _runlist runlist rl dotmd`;
43
+ }
44
+
45
+ function zshCompletion() {
46
+ return `# runlist zsh completion
47
+ # Add to ~/.zshrc: eval "$(runlist completions zsh)"
48
+ _runlist() {
49
+ local -a commands global_flags
50
+ commands=(
51
+ ${COMPLETION_COMMANDS.map(command => ` '${command}'`).join('\n')}
52
+ )
53
+ global_flags=(
54
+ ${GLOBAL_FLAGS.map(option => ` '${option}'`).join('\n')}
55
+ )
56
+
57
+ local cmd=''
58
+ local expect_value=0
59
+ for ((i=2; i < CURRENT; i++)); do
60
+ if (( expect_value )); then expect_value=0; continue; fi
61
+ case "\${words[i]}" in
62
+ --config|--root|--type) expect_value=1 ;;
63
+ --dry-run|-n|--verbose|--help|-h|--version|-v) ;;
64
+ -*) ;;
65
+ *) cmd="\${words[i]}"; break ;;
66
+ esac
67
+ done
68
+
69
+ if [[ -z "$cmd" ]]; then
70
+ _describe 'command' commands
71
+ _describe 'flag' global_flags
72
+ return
73
+ fi
74
+
75
+ case "$cmd" in
76
+ ${Object.entries(COMMAND_WORDS).map(([command, words]) =>
77
+ ` ${command}) _values 'arguments' ${words.map(word => `'${word}'`).join(' ')} ;;`
78
+ ).join('\n')}
79
+ prompt) _values 'arguments' ${commandCompletionWords('prompts').map(word => `'${word}'`).join(' ')} ;;
80
+ esac
81
+
82
+ _describe 'flag' global_flags
83
+ }
84
+ compdef _runlist runlist rl dotmd`;
85
+ }
86
+
87
+ export function runCompletions(argv) {
88
+ const shell = argv[0];
89
+ if (!shell) die('Usage: runlist completions <bash|zsh>');
90
+ if (shell === 'bash') process.stdout.write(bashCompletion() + '\n');
91
+ else if (shell === 'zsh') process.stdout.write(zshCompletion() + '\n');
92
+ else die(`Unsupported shell: ${shell}\nSupported: bash, zsh`);
93
+ }