phronesis 1.0.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 (142) hide show
  1. package/README.md +66 -0
  2. package/package.json +34 -0
  3. package/src/act.js +61 -0
  4. package/src/active-context.js +56 -0
  5. package/src/adapter.js +386 -0
  6. package/src/cli.js +1893 -0
  7. package/src/codex-hooks-schema.js +92 -0
  8. package/src/compile.js +513 -0
  9. package/src/doctor.js +303 -0
  10. package/src/due.js +271 -0
  11. package/src/eval.js +467 -0
  12. package/src/event.js +212 -0
  13. package/src/export.js +136 -0
  14. package/src/guard.js +89 -0
  15. package/src/hooks.js +230 -0
  16. package/src/ingest.js +965 -0
  17. package/src/init.js +150 -0
  18. package/src/layout.js +394 -0
  19. package/src/links.js +83 -0
  20. package/src/lint.js +58 -0
  21. package/src/profile.js +116 -0
  22. package/src/prompts.js +26 -0
  23. package/src/registry.js +190 -0
  24. package/src/scaffold.js +134 -0
  25. package/src/search.js +139 -0
  26. package/src/skill-discovery.js +117 -0
  27. package/src/skills-bump.js +885 -0
  28. package/src/skills.js +355 -0
  29. package/src/subscription.js +5 -0
  30. package/src/validate.js +152 -0
  31. package/templates/claude/settings.json +86 -0
  32. package/templates/codex/INDEX.md +68 -0
  33. package/templates/codex/seed/ai-practice/compile-dont-just-capture.md +42 -0
  34. package/templates/codex/seed/ai-practice/design-for-the-distribution.md +42 -0
  35. package/templates/codex/seed/ai-practice/from-spec-author-to-outcome-curator.md +43 -0
  36. package/templates/codex/seed/ai-practice/recall-builds-trust-when-calibrated.md +70 -0
  37. package/templates/codex/seed/ai-practice/the-eval-is-the-spec.md +42 -0
  38. package/templates/codex/seed/ai-practice/the-gold-dust-leaves-no-trace.md +72 -0
  39. package/templates/codex/seed/ai-practice/the-model-is-not-the-moat.md +40 -0
  40. package/templates/codex/seed/ai-practice/when-generation-is-cheap-judgment-is-the-work.md +39 -0
  41. package/templates/codex/seed/judgment/contextual-stewardship-is-the-senior-skill.md +50 -0
  42. package/templates/codex/seed/judgment/decision-quality-is-distinct-from-outcome-quality.md +39 -0
  43. package/templates/codex/seed/judgment/embrace-reality-ego-and-blind-spots.md +38 -0
  44. package/templates/codex/seed/judgment/groups-seek-consensus-individuals-seek-truth.md +44 -0
  45. package/templates/codex/seed/judgment/heresies-are-where-the-edge-lives.md +42 -0
  46. package/templates/codex/seed/judgment/judgment-compounds-through-honest-reflection.md +38 -0
  47. package/templates/codex/seed/judgment/phronesis-is-judgment-in-the-particular.md +67 -0
  48. package/templates/codex/seed/judgment/show-me-the-apparatus.md +40 -0
  49. package/templates/codex/seed/judgment/small-judgment-gaps-compound-under-leverage.md +37 -0
  50. package/templates/codex/seed/judgment/taste-is-compressed-judgment.md +38 -0
  51. package/templates/codex/seed/judgment/trust-taste-only-where-feedback-is-valid.md +42 -0
  52. package/templates/codex/seed/judgment/weigh-the-reasoning-not-the-recommendation.md +43 -0
  53. package/templates/codex/seed/leadership/a-managers-output-is-the-teams-output.md +35 -0
  54. package/templates/codex/seed/leadership/most-team-problems-are-structural.md +40 -0
  55. package/templates/codex/seed/leadership/next-play.md +36 -0
  56. package/templates/codex/seed/leadership/psychological-safety.md +39 -0
  57. package/templates/codex/seed/leadership/right-size-your-teams.md +38 -0
  58. package/templates/codex/seed/leadership/spend-time-on-high-leverage-activities.md +36 -0
  59. package/templates/codex/seed/leadership/talent-density-is-the-master-variable.md +39 -0
  60. package/templates/codex/seed/leadership/under-pressure-calm-is-contagious.md +36 -0
  61. package/templates/codex/seed/leadership/your-best-alternative-is-your-leverage.md +37 -0
  62. package/templates/codex/seed/pm/a-problem-well-stated-is-half-solved.md +39 -0
  63. package/templates/codex/seed/pm/a-real-strategy-makes-planning-trivial.md +40 -0
  64. package/templates/codex/seed/pm/barrels-and-ammunition.md +39 -0
  65. package/templates/codex/seed/pm/develop-taste-in-your-beliefs.md +38 -0
  66. package/templates/codex/seed/pm/hire-for-progress.md +44 -0
  67. package/templates/codex/seed/pm/jobs-are-stable-solutions-churn.md +39 -0
  68. package/templates/codex/seed/pm/level-by-the-ambiguity-someone-can-own.md +39 -0
  69. package/templates/codex/seed/pm/listen-before-you-prescribe.md +38 -0
  70. package/templates/codex/seed/pm/live-in-the-future-and-build-whats-missing.md +38 -0
  71. package/templates/codex/seed/pm/match-effort-to-leverage.md +37 -0
  72. package/templates/codex/seed/pm/not-my-fault-but-my-problem.md +40 -0
  73. package/templates/codex/seed/pm/pick-the-job-then-refuse-everything-outside-it.md +37 -0
  74. package/templates/codex/seed/pm/retention-sets-the-ceiling.md +38 -0
  75. package/templates/codex/seed/pm/scale-with-leaders-not-process.md +38 -0
  76. package/templates/codex/seed/pm/solution-discovery-over-problem-validation.md +38 -0
  77. package/templates/codex/seed/pm/tenfold-better-on-a-painful-problem.md +40 -0
  78. package/templates/codex-surface/README.md +73 -0
  79. package/templates/codex-surface/hooks/_resolve.sh +36 -0
  80. package/templates/codex-surface/hooks/recall-guard.sh +30 -0
  81. package/templates/codex-surface/hooks/session-start.sh +18 -0
  82. package/templates/codex-surface/hooks/session-sweep-precompact.sh +20 -0
  83. package/templates/codex-surface/hooks/session-sweep-subagent.sh +23 -0
  84. package/templates/codex-surface/hooks/session-sweep.sh +19 -0
  85. package/templates/codex-surface/hooks.json +69 -0
  86. package/templates/domains/pm/AGENTS.md +111 -0
  87. package/templates/domains/pm/MEMORY.md +7 -0
  88. package/templates/domains/pm/USER.md +8 -0
  89. package/templates/phronesis-hooks/compile-active-context.sh +38 -0
  90. package/templates/phronesis-hooks/recall-guard.sh +103 -0
  91. package/templates/phronesis-hooks/session-start.sh +90 -0
  92. package/templates/phronesis-hooks/session-sweep.sh +140 -0
  93. package/templates/phronesis-hooks/skill-lifecycle.sh +37 -0
  94. package/templates/skills/compile/SKILL.md +279 -0
  95. package/templates/skills/compile/evals/rubric.md +75 -0
  96. package/templates/skills/decision-log/SKILL.md +156 -0
  97. package/templates/skills/decision-log/evals/rubric.md +97 -0
  98. package/templates/skills/extract-gold-dust/SKILL.md +311 -0
  99. package/templates/skills/extract-gold-dust/evals/rubric.md +52 -0
  100. package/templates/skills/ingest/SKILL.md +204 -0
  101. package/templates/skills/ingest/evals/rubric.md +117 -0
  102. package/templates/skills/lint/SKILL.md +246 -0
  103. package/templates/skills/lint/evals/rubric.md +139 -0
  104. package/templates/skills/meeting-prep/SKILL.md +195 -0
  105. package/templates/skills/meeting-prep/evals/rubric.md +93 -0
  106. package/templates/skills/onboard/SKILL.md +258 -0
  107. package/templates/skills/onboard/evals/rubric.md +60 -0
  108. package/templates/skills/prd-draft/SKILL.md +193 -0
  109. package/templates/skills/prd-draft/evals/rubric.md +38 -0
  110. package/templates/skills/prep-extraction/SKILL.md +217 -0
  111. package/templates/skills/prep-extraction/evals/rubric.md +45 -0
  112. package/templates/skills/recall/SKILL.md +155 -0
  113. package/templates/skills/recall/evals/rubric.md +37 -0
  114. package/templates/skills/research/SKILL.md +247 -0
  115. package/templates/skills/research/evals/rubric.md +139 -0
  116. package/templates/skills/stakeholder-update/SKILL.md +163 -0
  117. package/templates/skills/stakeholder-update/evals/rubric.md +111 -0
  118. package/vendor/core/package.json +25 -0
  119. package/vendor/core/src/action-registry.js +459 -0
  120. package/vendor/core/src/action-writers.js +201 -0
  121. package/vendor/core/src/actions.js +2140 -0
  122. package/vendor/core/src/active-context.js +167 -0
  123. package/vendor/core/src/artifacts.js +301 -0
  124. package/vendor/core/src/content-digest.js +21 -0
  125. package/vendor/core/src/contract.js +245 -0
  126. package/vendor/core/src/event-log.js +338 -0
  127. package/vendor/core/src/guard.js +398 -0
  128. package/vendor/core/src/hook-executor.js +527 -0
  129. package/vendor/core/src/index.js +180 -0
  130. package/vendor/core/src/ingest.js +286 -0
  131. package/vendor/core/src/install-root.js +42 -0
  132. package/vendor/core/src/layout.js +13 -0
  133. package/vendor/core/src/link-index.js +198 -0
  134. package/vendor/core/src/lint.js +368 -0
  135. package/vendor/core/src/schedule.js +535 -0
  136. package/vendor/core/src/search.js +534 -0
  137. package/vendor/core/src/session-hooks.js +129 -0
  138. package/vendor/core/src/skill-lifecycle.js +479 -0
  139. package/vendor/core/src/skill-registry.js +524 -0
  140. package/vendor/core/src/subscription.js +84 -0
  141. package/vendor/core/src/validate-object.js +62 -0
  142. package/vendor/core/src/workspace-scan.js +125 -0
package/src/cli.js ADDED
@@ -0,0 +1,1893 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'node:module';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { resolve } from 'node:path';
6
+ import { realpathSync } from 'node:fs';
7
+ import { DEFAULT_DOMAIN } from './layout.js';
8
+ import { COVERAGE_FLOOR_RANGE, MAX_K, QUALITY_FLOOR_RANGE } from './skills-bump.js';
9
+
10
+ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
11
+ if (nodeMajor < 18) {
12
+ console.error(`phronesis requires Node.js 18 or later. You're running ${process.versions.node}.`);
13
+ process.exit(1);
14
+ }
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const { version } = require('../package.json');
18
+
19
+ function parseArgs(argv = process.argv.slice(2)) {
20
+ const [cmd, ...rest] = argv;
21
+ if (!cmd || cmd === '--help' || cmd === '-h') return { command: 'help' };
22
+ if (cmd === '--version' || cmd === '-v') return { command: 'version' };
23
+ if (cmd === 'init') return parseInitArgs(rest);
24
+ if (cmd === 'export') return parseExportArgs(rest);
25
+ if (cmd === 'validate') return parseValidateArgs(rest);
26
+ if (cmd === 'links') return parseLinksArgs(rest);
27
+ if (cmd === 'event') return parseEventArgs(rest);
28
+ if (cmd === 'events') return parseEventsArgs(rest);
29
+ if (cmd === 'act') return parseActArgs(rest);
30
+ if (cmd === 'ingest') return parseIngestArgs(rest);
31
+ if (cmd === 'compile') return parseCompileArgs(rest);
32
+ if (cmd === 'active-context') return parseActiveContextArgs(rest);
33
+ if (cmd === 'registry') return parseRegistryArgs(rest);
34
+ if (cmd === 'skills') return parseSkillsArgs(rest);
35
+ if (cmd === 'hooks') return parseHooksArgs(rest);
36
+ if (cmd === 'adapter') return parseAdapterArgs(rest);
37
+ if (cmd === 'guard') return parseGuardArgs(rest);
38
+ if (cmd === 'search') return parseRetrievalArgs(rest, 'search');
39
+ if (cmd === 'query') return parseRetrievalArgs(rest, 'query');
40
+ if (cmd === 'lint') return parseLintArgs(rest);
41
+ if (cmd === 'doctor') return parseDoctorArgs(rest);
42
+ if (cmd === 'due') return parseDueArgs(rest);
43
+ if (cmd === 'eval') return parseEvalArgs(rest);
44
+ if (cmd === 'index') return parseIndexArgs(rest);
45
+ return { command: 'unknown', input: cmd };
46
+ }
47
+
48
+ function parseInitArgs(args) {
49
+ let dir;
50
+ let force = false;
51
+ // domain + the minimal-profile value flags (core-0039). Each accepts `--flag value` and `--flag=value`.
52
+ const VALUE_FLAGS = ['domain', 'name', 'role', 'company', 'working-style'];
53
+ const values = {};
54
+
55
+ for (let i = 0; i < args.length; i += 1) {
56
+ const a = args[i];
57
+ if (a === '--help' || a === '-h') return { command: 'init-help' };
58
+ if (a === '--force' || a === '-f') {
59
+ force = true;
60
+ continue;
61
+ }
62
+ let matched = false;
63
+ for (const f of VALUE_FLAGS) {
64
+ if (a === `--${f}`) {
65
+ const v = args[i + 1];
66
+ if (v === undefined || v.startsWith('--')) return { command: 'invalid', error: `Missing value for --${f}` };
67
+ values[f] = v;
68
+ i += 1;
69
+ matched = true;
70
+ break;
71
+ }
72
+ if (a.startsWith(`--${f}=`)) {
73
+ values[f] = a.slice(`--${f}=`.length);
74
+ matched = true;
75
+ break;
76
+ }
77
+ }
78
+ if (matched) continue;
79
+ if (a.startsWith('-')) {
80
+ return { command: 'invalid', error: `Unknown option for init: ${a}` };
81
+ }
82
+ if (dir) return { command: 'invalid', error: 'init accepts at most one <dir> argument.' };
83
+ dir = a;
84
+ }
85
+
86
+ return {
87
+ command: 'init',
88
+ dir,
89
+ force,
90
+ domain: values.domain,
91
+ name: values.name,
92
+ role: values.role,
93
+ company: values.company,
94
+ workingStyle: values['working-style'],
95
+ };
96
+ }
97
+
98
+ function parseExportArgs(args) {
99
+ let dir;
100
+ let out;
101
+
102
+ for (let i = 0; i < args.length; i += 1) {
103
+ const a = args[i];
104
+ if (a === '--help' || a === '-h') return { command: 'export-help' };
105
+ if (a === '--out' || a === '-o') {
106
+ out = args[i + 1];
107
+ i += 1;
108
+ continue;
109
+ }
110
+ if (a.startsWith('--out=')) {
111
+ out = a.slice('--out='.length);
112
+ continue;
113
+ }
114
+ if (a.startsWith('-')) {
115
+ return { command: 'invalid', error: `Unknown option for export: ${a}` };
116
+ }
117
+ if (dir) return { command: 'invalid', error: 'export accepts at most one <dir> argument.' };
118
+ dir = a;
119
+ }
120
+
121
+ return { command: 'export', dir, out };
122
+ }
123
+
124
+ function parseValidateArgs(args) {
125
+ let dir;
126
+ for (let i = 0; i < args.length; i += 1) {
127
+ const a = args[i];
128
+ if (a === '--help' || a === '-h') return { command: 'validate-help' };
129
+ if (a.startsWith('-')) {
130
+ return { command: 'invalid', error: `Unknown option for validate: ${a}` };
131
+ }
132
+ if (dir) return { command: 'invalid', error: 'validate accepts at most one <dir> argument.' };
133
+ dir = a;
134
+ }
135
+ return { command: 'validate', dir };
136
+ }
137
+
138
+ function parseLinksArgs(args) {
139
+ let object;
140
+ let dir;
141
+ let hops = 2;
142
+ let direction = 'both';
143
+ let json = false;
144
+
145
+ for (let i = 0; i < args.length; i += 1) {
146
+ const a = args[i];
147
+ if (a === '--help' || a === '-h') return { command: 'links-help' };
148
+ if (a === '--json') {
149
+ json = true;
150
+ continue;
151
+ }
152
+ if (a === '--dir') {
153
+ dir = args[i + 1];
154
+ i += 1;
155
+ continue;
156
+ }
157
+ if (a.startsWith('--dir=')) {
158
+ dir = a.slice('--dir='.length);
159
+ continue;
160
+ }
161
+ if (a === '--hops') {
162
+ hops = args[i + 1];
163
+ i += 1;
164
+ continue;
165
+ }
166
+ if (a.startsWith('--hops=')) {
167
+ hops = a.slice('--hops='.length);
168
+ continue;
169
+ }
170
+ if (a === '--direction') {
171
+ direction = args[i + 1];
172
+ i += 1;
173
+ continue;
174
+ }
175
+ if (a.startsWith('--direction=')) {
176
+ direction = a.slice('--direction='.length);
177
+ continue;
178
+ }
179
+ if (a.startsWith('-')) {
180
+ return { command: 'invalid', error: `Unknown option for links: ${a}` };
181
+ }
182
+ if (object) return { command: 'invalid', error: 'links accepts at most one <object> argument.' };
183
+ object = a;
184
+ }
185
+
186
+ const hopsNum = Number(hops);
187
+ if (!Number.isInteger(hopsNum) || hopsNum < 0) {
188
+ return { command: 'invalid', error: `links --hops must be a non-negative integer (got "${hops}").` };
189
+ }
190
+ if (!['out', 'in', 'both'].includes(direction)) {
191
+ return { command: 'invalid', error: `links --direction must be out | in | both (got "${direction}").` };
192
+ }
193
+
194
+ return { command: 'links', object, dir, hops: hopsNum, direction, json };
195
+ }
196
+
197
+ // Compact option parser shared by the event commands (`--name value` / `--name=value` for
198
+ // `values`, presence flags for `flags`, the rest positional). The older parsers above predate
199
+ // it and stay hand-rolled; new multi-option commands use this to avoid the per-flag boilerplate.
200
+ function parseOptions(args, { values = [], flags = [] } = {}) {
201
+ const valueSet = new Set(values);
202
+ const flagSet = new Set(flags);
203
+ const out = { values: {}, flags: {}, positionals: [] };
204
+ for (let i = 0; i < args.length; i += 1) {
205
+ const a = args[i];
206
+ if (a === '--help' || a === '-h') return { help: true };
207
+ if (a.startsWith('--')) {
208
+ const eq = a.indexOf('=');
209
+ const name = eq === -1 ? a.slice(2) : a.slice(2, eq);
210
+ if (flagSet.has(name)) {
211
+ out.flags[name] = true;
212
+ continue;
213
+ }
214
+ if (valueSet.has(name)) {
215
+ if (eq === -1) {
216
+ // A bare `--name` at the end, or one followed by another `--option`, is a missing
217
+ // value — not "consume the next token". Swallowing it silently mislabels the next
218
+ // flag as this one's value and surfaces a misleading downstream error.
219
+ const next = args[i + 1];
220
+ if (next === undefined || next.startsWith('--')) {
221
+ return { error: `Missing value for --${name}` };
222
+ }
223
+ out.values[name] = next;
224
+ i += 1;
225
+ } else {
226
+ out.values[name] = a.slice(eq + 1);
227
+ }
228
+ continue;
229
+ }
230
+ return { error: `Unknown option: ${a}` };
231
+ }
232
+ if (a.startsWith('-') && a !== '-') return { error: `Unknown option: ${a}` };
233
+ out.positionals.push(a);
234
+ }
235
+ return out;
236
+ }
237
+
238
+ function parseEventArgs(args) {
239
+ const [sub, ...rest] = args;
240
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'event-help' };
241
+ if (sub === 'emit') {
242
+ const p = parseOptions(rest, { values: ['session', 'payload', 'actor', 'surface', 'dir'] });
243
+ if (p.help) return { command: 'event-help' };
244
+ if (p.error) return { command: 'invalid', error: `${p.error} for event emit.` };
245
+ if (p.positionals.length > 1) {
246
+ return { command: 'invalid', error: 'event emit takes one <type> argument.' };
247
+ }
248
+ return {
249
+ command: 'event-emit',
250
+ type: p.positionals[0],
251
+ session: p.values.session,
252
+ payload: p.values.payload,
253
+ actor: p.values.actor,
254
+ surface: p.values.surface,
255
+ dir: p.values.dir,
256
+ };
257
+ }
258
+ if (sub === 'tail') {
259
+ const p = parseOptions(rest, { values: ['type', 'since', 'dir'], flags: ['follow'] });
260
+ if (p.help) return { command: 'event-help' };
261
+ if (p.error) return { command: 'invalid', error: `${p.error} for event tail.` };
262
+ if (p.positionals.length) {
263
+ return { command: 'invalid', error: 'event tail takes no positional arguments.' };
264
+ }
265
+ return {
266
+ command: 'event-tail',
267
+ type: p.values.type,
268
+ since: p.values.since,
269
+ dir: p.values.dir,
270
+ follow: Boolean(p.flags.follow),
271
+ };
272
+ }
273
+ return { command: 'invalid', error: `Unknown event subcommand: ${sub} (expected: emit | tail).` };
274
+ }
275
+
276
+ // `act` is plumbing, not porcelain (ontology.md:163): parse-only here — semantic action
277
+ // inputs travel exclusively inside --json (a `--title`-style flag is an unknown option
278
+ // and is rejected); flags carry execution context only. The approval signal is env
279
+ // (PHRONESIS_APPROVAL_SOURCE), set by an approving adapter — deliberately not a flag.
280
+ function parseActArgs(args) {
281
+ const p = parseOptions(args, { values: ['json', 'session', 'domain', 'actor', 'surface', 'dir', 'invocation-id'] });
282
+ if (p.help) return { command: 'act-help' };
283
+ if (p.error) return { command: 'invalid', error: `${p.error} for act.` };
284
+ if (p.positionals.length > 1) {
285
+ return { command: 'invalid', error: 'act takes one <type> argument (semantic inputs go inside --json).' };
286
+ }
287
+ const parsed = {
288
+ command: 'act',
289
+ type: p.positionals[0],
290
+ json: p.values.json,
291
+ session: p.values.session,
292
+ domain: p.values.domain,
293
+ actor: p.values.actor,
294
+ surface: p.values.surface,
295
+ dir: p.values.dir,
296
+ };
297
+ if (p.values['invocation-id'] !== undefined) parsed.invocationId = p.values['invocation-id'];
298
+ return parsed;
299
+ }
300
+
301
+ function parseIngestArgs(args) {
302
+ const p = parseOptions(args, {
303
+ values: ['domain', 'trust', 'lane', 'dir', 'source-connector', 'source-external-id', 'source-timestamp', 'title', 'slug'],
304
+ flags: ['paste', 'json'],
305
+ });
306
+ if (p.help) return { command: 'ingest-help' };
307
+ if (p.error) return { command: 'invalid', error: `${p.error} for ingest.` };
308
+ if (p.positionals.length > 1) {
309
+ return { command: 'invalid', error: 'ingest takes one <url|file|dir> argument, or --paste / - for stdin.' };
310
+ }
311
+ return {
312
+ command: 'ingest',
313
+ source: p.positionals[0],
314
+ paste: Boolean(p.flags.paste),
315
+ domain: p.values.domain,
316
+ trust: p.values.trust,
317
+ lane: p.values.lane || 'inbox',
318
+ dir: p.values.dir,
319
+ json: Boolean(p.flags.json),
320
+ sourceConnector: p.values['source-connector'],
321
+ sourceExternalId: p.values['source-external-id'],
322
+ sourceTimestamp: p.values['source-timestamp'],
323
+ title: p.values.title,
324
+ slug: p.values.slug,
325
+ };
326
+ }
327
+
328
+ // `compile accept|reject <draft>` — the operator review primitives (core-0017 H9). The
329
+ // draft path is the one positional; execution context (session/actor/surface/dir) and the
330
+ // approval signal (env, like act) ride flags/env, never the payload.
331
+ function parseCompileArgs(args) {
332
+ const [sub, ...rest] = args;
333
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'compile-help' };
334
+ // accept/reject are operator-driven by contract — no --actor (the events are always
335
+ // actor: operator; the approval signal rides env PHRONESIS_APPROVAL_SOURCE, like act).
336
+ if (sub === 'accept') {
337
+ const p = parseOptions(rest, { values: ['session', 'surface', 'dir'] });
338
+ if (p.help) return { command: 'compile-help' };
339
+ if (p.error) return { command: 'invalid', error: `${p.error} for compile accept.` };
340
+ if (p.positionals.length !== 1) {
341
+ return { command: 'invalid', error: 'compile accept takes one <draft_path> argument.' };
342
+ }
343
+ return {
344
+ command: 'compile-accept',
345
+ draftPath: p.positionals[0],
346
+ session: p.values.session,
347
+ surface: p.values.surface,
348
+ dir: p.values.dir,
349
+ };
350
+ }
351
+ if (sub === 'reject') {
352
+ const p = parseOptions(rest, { values: ['session', 'reason', 'surface', 'dir'] });
353
+ if (p.help) return { command: 'compile-help' };
354
+ if (p.error) return { command: 'invalid', error: `${p.error} for compile reject.` };
355
+ if (p.positionals.length !== 1) {
356
+ return { command: 'invalid', error: 'compile reject takes one <draft_path> argument.' };
357
+ }
358
+ return {
359
+ command: 'compile-reject',
360
+ draftPath: p.positionals[0],
361
+ reason: p.values.reason,
362
+ session: p.values.session,
363
+ surface: p.values.surface,
364
+ dir: p.values.dir,
365
+ };
366
+ }
367
+ if (sub === 'review') {
368
+ const p = parseOptions(rest, {
369
+ values: ['domain', 'session', 'surface', 'dir', 'rationale'],
370
+ flags: ['approve-all', 'resume', 'json'],
371
+ });
372
+ if (p.help) return { command: 'compile-help' };
373
+ if (p.error) return { command: 'invalid', error: `${p.error} for compile review.` };
374
+ if (p.positionals.length) {
375
+ return { command: 'invalid', error: 'compile review takes no positional arguments (use --domain, --approve-all, --resume).' };
376
+ }
377
+ return {
378
+ command: 'compile-review',
379
+ domain: p.values.domain,
380
+ session: p.values.session,
381
+ surface: p.values.surface,
382
+ dir: p.values.dir,
383
+ rationale: p.values.rationale,
384
+ approveAll: Boolean(p.flags['approve-all']),
385
+ resume: Boolean(p.flags.resume),
386
+ json: Boolean(p.flags.json),
387
+ };
388
+ }
389
+ return { command: 'invalid', error: `Unknown compile subcommand: ${sub} (expected: accept | reject | review).` };
390
+ }
391
+
392
+ // `active-context refresh --domain <slug>` — regenerate a domain's tracked projection
393
+ // (core-0017 H5). The post-compile hook shells out to it; operators rarely call it directly.
394
+ function parseActiveContextArgs(args) {
395
+ const [sub, ...rest] = args;
396
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'active-context-help' };
397
+ if (sub === 'refresh') {
398
+ const p = parseOptions(rest, { values: ['domain', 'dir'], flags: ['json'] });
399
+ if (p.help) return { command: 'active-context-help' };
400
+ if (p.error) return { command: 'invalid', error: `${p.error} for active-context refresh.` };
401
+ if (p.positionals.length) {
402
+ return { command: 'invalid', error: 'active-context refresh takes no positional arguments.' };
403
+ }
404
+ return {
405
+ command: 'active-context-refresh',
406
+ domain: p.values.domain,
407
+ dir: p.values.dir,
408
+ json: Boolean(p.flags.json),
409
+ };
410
+ }
411
+ return { command: 'invalid', error: `Unknown active-context subcommand: ${sub} (expected: refresh).` };
412
+ }
413
+
414
+ function parseRegistryArgs(args) {
415
+ const [sub, ...rest] = args;
416
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'registry-help' };
417
+ if (sub === 'status') {
418
+ const p = parseOptions(rest, { values: ['dir'], flags: ['json'] });
419
+ if (p.help) return { command: 'registry-help' };
420
+ if (p.error) return { command: 'invalid', error: `${p.error} for registry status.` };
421
+ if (p.positionals.length) {
422
+ return { command: 'invalid', error: 'registry status takes no positional arguments.' };
423
+ }
424
+ return { command: 'registry-status', dir: p.values.dir, json: Boolean(p.flags.json) };
425
+ }
426
+ return { command: 'invalid', error: `Unknown registry subcommand: ${sub} (expected: status).` };
427
+ }
428
+
429
+ function parseSkillsArgs(args) {
430
+ const [sub, ...rest] = args;
431
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'skills-help' };
432
+ if (sub === 'status') {
433
+ const p = parseOptions(rest, { values: ['dir'], flags: ['json'] });
434
+ if (p.help) return { command: 'skills-help' };
435
+ if (p.error) return { command: 'invalid', error: `${p.error} for skills status.` };
436
+ if (p.positionals.length) {
437
+ return { command: 'invalid', error: 'skills status takes no positional arguments.' };
438
+ }
439
+ return { command: 'skills-status', dir: p.values.dir, json: Boolean(p.flags.json) };
440
+ }
441
+ if (sub === 'bump') {
442
+ const p = parseOptions(rest, {
443
+ values: ['to', 'repo', 'k', 'coverage-floor', 'quality-floor', 'rationale', 'accepted-by', 'judge-model', 'goldens-dir', 'domain'],
444
+ flags: ['accept-regression', 'rebaseline', 'shape-change', 'json'],
445
+ });
446
+ if (p.help) return { command: 'skills-help' };
447
+ if (p.error) return { command: 'invalid', error: `${p.error} for skills bump.` };
448
+ if (p.positionals.length !== 1) {
449
+ return { command: 'invalid', error: 'skills bump takes exactly one skill name: phronesis skills bump <skill> --to <pin>.' };
450
+ }
451
+ if (!p.values.to) {
452
+ return { command: 'invalid', error: 'skills bump requires --to <model_id>@<date> (the proposed prompt_pin).' };
453
+ }
454
+ let k;
455
+ if (p.values.k !== undefined) {
456
+ k = Number(p.values.k);
457
+ if (!Number.isInteger(k) || k < 1) {
458
+ return { command: 'invalid', error: '--k must be a positive integer.' };
459
+ }
460
+ if (k > MAX_K) {
461
+ return { command: 'invalid', error: `--k must not exceed ${MAX_K} — each run is k × goldens model+judge calls; a larger value is almost always a typo.` };
462
+ }
463
+ }
464
+ const coverageFloor = p.values['coverage-floor'] !== undefined ? Number(p.values['coverage-floor']) : undefined;
465
+ const qualityFloor = p.values['quality-floor'] !== undefined ? Number(p.values['quality-floor']) : undefined;
466
+ if (coverageFloor !== undefined && (!Number.isFinite(coverageFloor) || coverageFloor < COVERAGE_FLOOR_RANGE[0] || coverageFloor > COVERAGE_FLOOR_RANGE[1])) {
467
+ return { command: 'invalid', error: `--coverage-floor must be a number within [${COVERAGE_FLOOR_RANGE.join(', ')}] (a warranted-capture hit-rate); a negative floor would disable the coverage axis.` };
468
+ }
469
+ if (qualityFloor !== undefined && (!Number.isFinite(qualityFloor) || qualityFloor < QUALITY_FLOOR_RANGE[0] || qualityFloor > QUALITY_FLOOR_RANGE[1])) {
470
+ return { command: 'invalid', error: `--quality-floor must be a number within [${QUALITY_FLOOR_RANGE.join(', ')}] (judge scores are integers 1-5); a floor outside the scale would disable the quality axis.` };
471
+ }
472
+ if ((p.flags['accept-regression'] || p.flags.rebaseline) && (p.values.rationale === undefined || String(p.values.rationale).trim() === '')) {
473
+ const flag = p.flags.rebaseline ? '--rebaseline' : '--accept-regression';
474
+ return { command: 'invalid', error: `${flag} requires a non-empty --rationale (the audit trail for the escape hatch).` };
475
+ }
476
+ return {
477
+ command: 'skills-bump',
478
+ skill: p.positionals[0],
479
+ to: p.values.to,
480
+ repo: p.values.repo,
481
+ k,
482
+ coverageFloor,
483
+ qualityFloor,
484
+ rationale: p.values.rationale,
485
+ acceptedBy: p.values['accepted-by'],
486
+ judgeModel: p.values['judge-model'],
487
+ goldensDir: p.values['goldens-dir'],
488
+ domain: p.values.domain,
489
+ acceptRegression: Boolean(p.flags['accept-regression']),
490
+ rebaseline: Boolean(p.flags.rebaseline),
491
+ shapeChange: Boolean(p.flags['shape-change']),
492
+ json: Boolean(p.flags.json),
493
+ };
494
+ }
495
+ return { command: 'invalid', error: `Unknown skills subcommand: ${sub} (expected: status, bump).` };
496
+ }
497
+
498
+ function parseEvalArgs(args) {
499
+ const [sub, ...rest] = args;
500
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'eval-help' };
501
+ if (sub === 'view') {
502
+ const p = parseOptions(rest, {
503
+ values: ['repo', 'skill', 'goldens-dir', 'port', 'host'],
504
+ flags: ['json', 'no-serve'],
505
+ });
506
+ if (p.help) return { command: 'eval-help' };
507
+ if (p.error) return { command: 'invalid', error: `${p.error} for eval view.` };
508
+ if (p.positionals.length) {
509
+ return { command: 'invalid', error: 'eval view takes no positional arguments (use --skill <name>).' };
510
+ }
511
+ let port;
512
+ if (p.values.port !== undefined) {
513
+ port = Number(p.values.port);
514
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
515
+ return { command: 'invalid', error: '--port must be an integer in 0..65535.' };
516
+ }
517
+ }
518
+ return {
519
+ command: 'eval-view',
520
+ repo: p.values.repo,
521
+ skill: p.values.skill,
522
+ goldensDir: p.values['goldens-dir'],
523
+ port,
524
+ host: p.values.host,
525
+ json: Boolean(p.flags.json),
526
+ serve: !p.flags['no-serve'],
527
+ };
528
+ }
529
+ return { command: 'invalid', error: `Unknown eval subcommand: ${sub} (expected: view).` };
530
+ }
531
+
532
+ function parseHooksArgs(args) {
533
+ const [sub, ...rest] = args;
534
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'hooks-help' };
535
+ if (sub === 'due-check') {
536
+ const p = parseOptions(rest, { values: ['session', 'dir', 'actor', 'surface'] });
537
+ if (p.help) return { command: 'hooks-help' };
538
+ if (p.error) return { command: 'invalid', error: `${p.error} for hooks due-check.` };
539
+ if (p.positionals.length) {
540
+ return { command: 'invalid', error: 'hooks due-check takes no positional arguments.' };
541
+ }
542
+ return {
543
+ command: 'hooks-due-check',
544
+ session: p.values.session,
545
+ dir: p.values.dir,
546
+ actor: p.values.actor,
547
+ surface: p.values.surface,
548
+ };
549
+ }
550
+ if (sub === 'sweep-status') {
551
+ const p = parseOptions(rest, { values: ['session', 'dir'], flags: ['json'] });
552
+ if (p.help) return { command: 'hooks-help' };
553
+ if (p.error) return { command: 'invalid', error: `${p.error} for hooks sweep-status.` };
554
+ if (p.positionals.length) {
555
+ return { command: 'invalid', error: 'hooks sweep-status takes no positional arguments.' };
556
+ }
557
+ return {
558
+ command: 'hooks-sweep-status',
559
+ session: p.values.session,
560
+ dir: p.values.dir,
561
+ json: Boolean(p.flags.json),
562
+ };
563
+ }
564
+ if (sub === 'skill-start') {
565
+ const p = parseOptions(rest, { values: ['dir', 'payload'] });
566
+ if (p.help) return { command: 'hooks-help' };
567
+ if (p.error) return { command: 'invalid', error: `${p.error} for hooks skill-start.` };
568
+ if (p.positionals.length) {
569
+ return { command: 'invalid', error: 'hooks skill-start takes no positional arguments.' };
570
+ }
571
+ return { command: 'hooks-skill-start', dir: p.values.dir, payload: p.values.payload };
572
+ }
573
+ if (sub === 'skill-complete') {
574
+ const p = parseOptions(rest, { values: ['session', 'dir', 'payload', 'boundary-event'], flags: ['json'] });
575
+ if (p.help) return { command: 'hooks-help' };
576
+ if (p.error) return { command: 'invalid', error: `${p.error} for hooks skill-complete.` };
577
+ if (p.positionals.length) {
578
+ return { command: 'invalid', error: 'hooks skill-complete takes no positional arguments.' };
579
+ }
580
+ return {
581
+ command: 'hooks-skill-complete',
582
+ session: p.values.session,
583
+ dir: p.values.dir,
584
+ payload: p.values.payload,
585
+ boundaryEvent: p.values['boundary-event'],
586
+ json: Boolean(p.flags.json),
587
+ };
588
+ }
589
+ if (sub === 'skill-abandon') {
590
+ const p = parseOptions(rest, { values: ['dir', 'payload'], flags: ['json'] });
591
+ if (p.help) return { command: 'hooks-help' };
592
+ if (p.error) return { command: 'invalid', error: `${p.error} for hooks skill-abandon.` };
593
+ if (p.positionals.length) {
594
+ return { command: 'invalid', error: 'hooks skill-abandon takes no positional arguments.' };
595
+ }
596
+ return {
597
+ command: 'hooks-skill-abandon',
598
+ dir: p.values.dir,
599
+ payload: p.values.payload,
600
+ json: Boolean(p.flags.json),
601
+ };
602
+ }
603
+ if (sub === 'skill-sweep-stale') {
604
+ const p = parseOptions(rest, { values: ['session', 'dir'], flags: ['json'] });
605
+ if (p.help) return { command: 'hooks-help' };
606
+ if (p.error) return { command: 'invalid', error: `${p.error} for hooks skill-sweep-stale.` };
607
+ if (p.positionals.length) {
608
+ return { command: 'invalid', error: 'hooks skill-sweep-stale takes no positional arguments.' };
609
+ }
610
+ return {
611
+ command: 'hooks-skill-sweep-stale',
612
+ session: p.values.session,
613
+ dir: p.values.dir,
614
+ json: Boolean(p.flags.json),
615
+ };
616
+ }
617
+ return {
618
+ command: 'invalid',
619
+ error:
620
+ `Unknown hooks subcommand: ${sub} ` +
621
+ '(expected: due-check | sweep-status | skill-start | skill-complete | skill-abandon | skill-sweep-stale).',
622
+ };
623
+ }
624
+
625
+ // `adapter test <surface>` — the per-surface adapter self-check (specs/tech-stack.md
626
+ // §"Adapter test command"; change 0028 / core-0034). V1 ships `adapter test codex`.
627
+ function parseAdapterArgs(args) {
628
+ const [sub, ...rest] = args;
629
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'adapter-help' };
630
+ if (sub === 'test') {
631
+ const p = parseOptions(rest, { values: ['dir', 'domain'], flags: ['json'] });
632
+ if (p.help) return { command: 'adapter-help' };
633
+ if (p.error) return { command: 'invalid', error: `${p.error} for adapter test.` };
634
+ if (p.positionals.length !== 1) {
635
+ return { command: 'invalid', error: 'adapter test takes one <surface> argument (V1: codex).' };
636
+ }
637
+ return {
638
+ command: 'adapter-test',
639
+ surface: p.positionals[0],
640
+ dir: p.values.dir,
641
+ domain: p.values.domain,
642
+ json: Boolean(p.flags.json),
643
+ };
644
+ }
645
+ return { command: 'invalid', error: `Unknown adapter subcommand: ${sub} (expected: test).` };
646
+ }
647
+
648
+ function parseGuardArgs(args) {
649
+ const p = parseOptions(args, { values: ['dir', 'session', 'surface', 'actor', 'domain'], flags: ['json'] });
650
+ if (p.help) return { command: 'guard-help' };
651
+ if (p.error) return { command: 'invalid', error: `${p.error} for guard.` };
652
+ if (p.positionals.length) {
653
+ return { command: 'invalid', error: 'guard takes no positional prompt argument; pass the prompt on stdin.' };
654
+ }
655
+ return {
656
+ command: 'guard',
657
+ dir: p.values.dir,
658
+ session: p.values.session,
659
+ surface: p.values.surface,
660
+ actor: p.values.actor,
661
+ domain: p.values.domain,
662
+ json: Boolean(p.flags.json),
663
+ };
664
+ }
665
+
666
+ // search/query share one parser (core-0014 / T5.2): one positional (the query string),
667
+ // scope flags (--domain | --all-domains — contradictory together), --limit, and for
668
+ // query --hops. Semantic defaults (limit 10, hops 1) live in @phronesis/core, not here —
669
+ // omitted flags stay undefined so the library is the single source of the defaults.
670
+ function parseRetrievalArgs(args, which) {
671
+ const values = ['dir', 'domain', 'limit'];
672
+ if (which === 'query') values.push('hops');
673
+ const p = parseOptions(args, { values, flags: ['json', 'all-domains', 'include-artifacts'] });
674
+ if (p.help) return { command: `${which}-help` };
675
+ if (p.error) return { command: 'invalid', error: `${p.error} for ${which}.` };
676
+ if (p.positionals.length > 1) {
677
+ return { command: 'invalid', error: `${which} takes one quoted <query> argument.` };
678
+ }
679
+ if (p.values.domain !== undefined && p.flags['all-domains']) {
680
+ return { command: 'invalid', error: `${which}: --domain and --all-domains contradict — pick one scope.` };
681
+ }
682
+ let limit;
683
+ if (p.values.limit !== undefined) {
684
+ limit = Number(p.values.limit);
685
+ if (!Number.isInteger(limit) || limit < 1) {
686
+ return { command: 'invalid', error: `${which} --limit must be a positive integer (got "${p.values.limit}").` };
687
+ }
688
+ }
689
+ let hops;
690
+ if (which === 'query' && p.values.hops !== undefined) {
691
+ hops = Number(p.values.hops);
692
+ if (!Number.isInteger(hops) || hops < 0) {
693
+ return { command: 'invalid', error: `query --hops must be a non-negative integer (got "${p.values.hops}").` };
694
+ }
695
+ }
696
+ const parsed = {
697
+ command: which,
698
+ query: p.positionals[0],
699
+ dir: p.values.dir,
700
+ domain: p.values.domain,
701
+ allDomains: Boolean(p.flags['all-domains']),
702
+ limit,
703
+ hops,
704
+ json: Boolean(p.flags.json),
705
+ };
706
+ if (p.flags['include-artifacts']) parsed.includeArtifacts = true;
707
+ return parsed;
708
+ }
709
+
710
+ function parseIndexArgs(args) {
711
+ const [sub, ...rest] = args;
712
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'index-help' };
713
+ if (sub === 'rebuild') {
714
+ const p = parseOptions(rest, { values: ['dir'], flags: ['json'] });
715
+ if (p.help) return { command: 'index-help' };
716
+ if (p.error) return { command: 'invalid', error: `${p.error} for index rebuild.` };
717
+ if (p.positionals.length) {
718
+ return { command: 'invalid', error: 'index rebuild takes no positional arguments.' };
719
+ }
720
+ return { command: 'index-rebuild', dir: p.values.dir, json: Boolean(p.flags.json) };
721
+ }
722
+ return { command: 'invalid', error: `Unknown index subcommand: ${sub} (expected: rebuild).` };
723
+ }
724
+
725
+ function parseLintArgs(args) {
726
+ const p = parseOptions(args, { values: ['dir', 'domain', 'stale-months'], flags: ['json', 'all-domains'] });
727
+ if (p.help) return { command: 'lint-help' };
728
+ if (p.error) return { command: 'invalid', error: `${p.error} for lint.` };
729
+ if (p.positionals.length) {
730
+ return { command: 'invalid', error: 'lint takes no positional arguments.' };
731
+ }
732
+ if (p.values.domain !== undefined && p.flags['all-domains']) {
733
+ return { command: 'invalid', error: 'lint: --domain and --all-domains contradict — pick one scope.' };
734
+ }
735
+ let staleMonths;
736
+ if (p.values['stale-months'] !== undefined) {
737
+ staleMonths = Number(p.values['stale-months']);
738
+ if (!Number.isInteger(staleMonths) || staleMonths < 1) {
739
+ return { command: 'invalid', error: `lint --stale-months must be a positive integer (got "${p.values['stale-months']}").` };
740
+ }
741
+ }
742
+ return {
743
+ command: 'lint',
744
+ dir: p.values.dir,
745
+ domain: p.values.domain,
746
+ allDomains: Boolean(p.flags['all-domains']),
747
+ staleMonths,
748
+ json: Boolean(p.flags.json),
749
+ };
750
+ }
751
+
752
+ function parseDoctorArgs(args) {
753
+ const p = parseOptions(args, { values: ['dir'], flags: ['json'] });
754
+ if (p.help) return { command: 'doctor-help' };
755
+ if (p.error) return { command: 'invalid', error: `${p.error} for doctor.` };
756
+ if (p.positionals.length) {
757
+ return { command: 'invalid', error: 'doctor takes no positional arguments (pass --dir <install>).' };
758
+ }
759
+ return { command: 'doctor', dir: p.values.dir, json: Boolean(p.flags.json) };
760
+ }
761
+
762
+ // `phronesis due [--json]` (pure read of the schedule registry), `phronesis due complete
763
+ // <schedule_id>` (the explicit completion floor), and `phronesis due run <schedule_id>`
764
+ // (operator-invoked deterministic executor). core-0051/core-0058 / change 0042.
765
+ function parseDueArgs(args) {
766
+ const [sub, ...rest] = args;
767
+ if (sub === 'complete' || sub === 'run') {
768
+ const p = parseOptions(rest, { values: ['dir'] });
769
+ if (p.help) return { command: 'due-help' };
770
+ if (p.error) return { command: 'invalid', error: `${p.error} for due ${sub}.` };
771
+ if (p.positionals.length !== 1) {
772
+ return { command: 'invalid', error: `due ${sub} takes exactly one <schedule_id> (e.g. \`phronesis due ${sub} lint-weekly\`).` };
773
+ }
774
+ return { command: sub === 'complete' ? 'due-complete' : 'due-run', scheduleId: p.positionals[0], dir: p.values.dir };
775
+ }
776
+ const p = parseOptions(args, { values: ['dir'], flags: ['json'] });
777
+ if (p.help) return { command: 'due-help' };
778
+ if (p.error) return { command: 'invalid', error: `${p.error} for due.` };
779
+ if (p.positionals.length) {
780
+ return { command: 'invalid', error: `Unknown due subcommand: ${p.positionals[0]} (expected: complete | run, or no subcommand for the overdue list).` };
781
+ }
782
+ return { command: 'due', dir: p.values.dir, json: Boolean(p.flags.json) };
783
+ }
784
+
785
+ function parseEventsArgs(args) {
786
+ const [sub, ...rest] = args;
787
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'events-help' };
788
+ if (sub === 'purge') {
789
+ const p = parseOptions(rest, { values: ['before', 'dir'], flags: ['archive'] });
790
+ if (p.help) return { command: 'events-help' };
791
+ if (p.error) return { command: 'invalid', error: `${p.error} for events purge.` };
792
+ if (p.positionals.length) {
793
+ return { command: 'invalid', error: 'events purge takes no positional arguments.' };
794
+ }
795
+ return { command: 'events-purge', before: p.values.before, archive: Boolean(p.flags.archive), dir: p.values.dir };
796
+ }
797
+ if (sub === 'rotate') {
798
+ const p = parseOptions(rest, { values: ['dir'] });
799
+ if (p.help) return { command: 'events-help' };
800
+ if (p.error) return { command: 'invalid', error: `${p.error} for events rotate.` };
801
+ if (p.positionals.length) {
802
+ return { command: 'invalid', error: 'events rotate takes no positional arguments.' };
803
+ }
804
+ return { command: 'events-rotate', dir: p.values.dir };
805
+ }
806
+ return {
807
+ command: 'invalid',
808
+ error: `Unknown events subcommand: ${sub} (expected: purge | rotate).`,
809
+ };
810
+ }
811
+
812
+ function showHelp() {
813
+ console.log(
814
+ `
815
+ phronesis v${version} — phronesis-grounded workspace architecture
816
+
817
+ Usage: phronesis <command>
818
+
819
+ Commands:
820
+ init [dir] Create a Phronesis installation (default dir: phronesis-installation)
821
+ export [dir] Tar an installation into a portable archive (default dir: current)
822
+ validate [dir] Check compiled + codex objects against the ontology contract
823
+ search <query> Stage 1 retrieval — BM25 over active-domain compiled/ + shared codex
824
+ query <query> Stage 1+2 retrieval — BM25 seeds expanded through typed links, sources cited
825
+ index <sub> Maintain the derived machine search index (rebuild)
826
+ lint Read-only deterministic workspace health report
827
+ doctor Read-only install-health overview (is my install wired?)
828
+ due [sub] Show overdue schedule entries; complete or run one
829
+ links <object> Walk typed links from an object — the N-hop connected subgraph
830
+ event <sub> Emit (emit) or read (tail) the typed event stream
831
+ events <sub> Maintain the event stream (purge, rotate)
832
+ registry <sub> Show the install's action-type + hook registry (status)
833
+ skills <sub> Show the install's skill registry (status)
834
+ eval <sub> Local eval data viewer (view) — goldens, scores, candidate output
835
+ hooks <sub> Session-boundary hooks: due-check, sweep-status
836
+ adapter <sub> Per-surface adapter self-check (test codex)
837
+ guard Prompt-submit recall guard (stdin-only)
838
+ ingest <source> Stage one URL, file, or stdin paste into domain raw/
839
+
840
+ Options:
841
+ --help, -h Show this help
842
+ --version, -v Show version number
843
+
844
+ Run 'phronesis <command> --help' for command options.
845
+ `.trim(),
846
+ );
847
+ }
848
+
849
+ function showInitHelp() {
850
+ console.log(
851
+ `
852
+ Usage: phronesis init [dir] [--force] [--domain <slug>]
853
+ [--name <name>] [--role <role>] [--company <co>] [--working-style <style>]
854
+
855
+ Creates a three-layer installation at [dir]:
856
+ codex/ shared principles across all domains (seed + personal)
857
+ domains/<slug>/ a domain workspace (raw + compiled)
858
+ skills/ .phronesis/
859
+
860
+ A minimal operator profile (name + role) fills USER.md and AGENTS.md so a fresh
861
+ install does not load [Name]/[Role] placeholders. On an interactive terminal, a
862
+ missing name/role is prompted; a non-interactive (scripted/CI) init never prompts
863
+ or blocks — it completes and leaves the placeholders.
864
+
865
+ Options:
866
+ --force, -f Replace a non-empty target directory
867
+ --domain <slug> Domain workspace to create (default: pm)
868
+ --name <name> Operator name (fills USER.md + AGENTS.md §1)
869
+ --role <role> Operator role
870
+ --company <co> Operator company (USER.md only; optional)
871
+ --working-style <s> Operator working style (USER.md only; optional)
872
+ `.trim(),
873
+ );
874
+ }
875
+
876
+ function showExportHelp() {
877
+ console.log(
878
+ `
879
+ Usage: phronesis export [dir] [--out <file>]
880
+
881
+ Tars a Phronesis installation into a portable gzip archive. The export is a
882
+ faithful snapshot — plain Markdown plus config, relative symlinks preserved —
883
+ that opens in Obsidian or VS Code with no Phronesis runtime present.
884
+
885
+ Arguments:
886
+ dir The installation to export (default: current directory)
887
+
888
+ Options:
889
+ --out, -o <file> Archive path (default: <dir>.tar.gz beside the installation)
890
+ `.trim(),
891
+ );
892
+ }
893
+
894
+ function showValidateHelp() {
895
+ console.log(
896
+ `
897
+ Usage: phronesis validate [dir]
898
+
899
+ Checks every typed object in domains/*/compiled/ and the installation codex/ against
900
+ the ontology frontmatter contract (specs/ontology.md): required fields, enums, dates,
901
+ link vocabulary, and link-target grammar + resolution. Raw sources are not validated.
902
+
903
+ Exits 0 when the workspace is valid; non-zero with a per-object error report otherwise.
904
+
905
+ Arguments:
906
+ dir The installation to validate (default: current directory)
907
+ `.trim(),
908
+ );
909
+ }
910
+
911
+ function showLinksHelp() {
912
+ console.log(
913
+ `
914
+ Usage: phronesis links <object> [--dir <install>] [--hops N] [--direction out|in|both] [--json]
915
+
916
+ Walks typed links (specs/ontology.md) from an object across domains/*/compiled/ and the
917
+ installation codex/, returning the N-hop connected subgraph — Stage 2 retrieval. A
918
+ substrate command: skills and the operator-facing query compose the same walk.
919
+
920
+ <object> may be an install-relative path, a bare object name, or a {type-dir}/{name} suffix.
921
+ Raw-source and not-yet-compiled (forward) targets appear as terminal leaves.
922
+
923
+ Arguments:
924
+ object The object to walk from
925
+
926
+ Options:
927
+ --dir <install> The installation to read (default: current directory)
928
+ --hops N How many hops to walk (default: 2)
929
+ --direction <d> Edge directions to follow: out | in | both (default: both)
930
+ --json Emit the subgraph as JSON
931
+ `.trim(),
932
+ );
933
+ }
934
+
935
+ function showEventHelp() {
936
+ console.log(
937
+ `
938
+ Usage:
939
+ phronesis event emit <type> --session <id> [--payload '<json>'] [--actor <a>] [--surface <s>] [--dir <install>]
940
+ phronesis event tail [--type <t>] [--since <iso-ts>] [--follow] [--dir <install>]
941
+
942
+ The universal emission + read path over the typed event stream (specs/event-log.md). Every
943
+ surface and agent shells out to 'event emit'. The installation root is discovered by walking
944
+ up from the current directory to the nearest .phronesis/registry.json; --dir overrides.
945
+
946
+ emit:
947
+ <type> A V1 event type or a connector.<name>.<event> extension. Reserved types
948
+ are refused: file.* (the V1.5 daemon's filesystem watch) and outcome.due
949
+ (the session-start due-check, 'phronesis hooks due-check').
950
+ --session <id> Session the event belongs to (required).
951
+ --payload '<json>' Event payload object (default {}). Message text / recall query are
952
+ dropped unless event_log.capture_message_text is enabled in config.json.
953
+ --actor <a> Defaults to $PHRONESIS_ACTOR, else 'agent'.
954
+ --surface <s> Defaults to 'cli'.
955
+
956
+ tail:
957
+ --type <t> Only show events of this type.
958
+ --since <iso-ts> Only show events at or after this ISO-8601 timestamp.
959
+ --follow Stream new events as they are appended.
960
+ `.trim(),
961
+ );
962
+ }
963
+
964
+ function showActHelp() {
965
+ console.log(
966
+ `
967
+ Usage: phronesis act <type> --json '<payload>' --session <id> [--domain <slug>] [--actor <a>] [--surface <s>] [--invocation-id <id>] [--dir <install>]
968
+
969
+ The action-type execution floor (specs/ontology.md §"Action type contract") — plumbing,
970
+ not porcelain: skills compose it; it is not a promoted operator verb. One shared core
971
+ runs every action: schema validation, permission gate, action_type.requested/committed
972
+ events, and a staged atomic write that must pass the ontology contract before it lands.
973
+
974
+ Registered types: log_decision, enrich_person, update_project_state,
975
+ track_commitment, record_outcome, promote_to_codex (handlers: core-0009/0010),
976
+ compile (the agentic quality-gate writer for insight + organization, core-0017 — review
977
+ its untrusted drafts with 'phronesis compile accept|reject'). The install's EFFECTIVE
978
+ table (operator overrides in .phronesis/registry.json action_types) is shown by
979
+ 'phronesis registry status'.
980
+
981
+ <type> The action type to run.
982
+ --json '<payload>' The action's SEMANTIC inputs (title, insight, links, …), schema-
983
+ validated. Execution context never goes here.
984
+ --session <id> Session the action belongs to (required).
985
+ --domain <slug> Domain workspace (default: the domains/<slug>/ the cwd is inside,
986
+ else "pm"). Must be registered in the installation.
987
+ --actor <a> Defaults to $PHRONESIS_ACTOR, else 'agent'.
988
+ --surface <s> Defaults to 'cli'.
989
+ --invocation-id <i> Attribution provenance for a surrounding skill invocation. It is
990
+ caller-controlled (not an authority); adapters and the eval harness
991
+ may supply it. PHRONESIS_INVOCATION_ID is the equivalent env input.
992
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json).
993
+
994
+ Approval-gated actions refuse without an approval signal: the env var
995
+ PHRONESIS_APPROVAL_SOURCE (e.g. operator:interactive), set by an approving adapter and
996
+ recorded verbatim as approval_source on action_type.committed.
997
+ `.trim(),
998
+ );
999
+ }
1000
+
1001
+ function showIngestHelp() {
1002
+ console.log(
1003
+ `
1004
+ Usage:
1005
+ phronesis ingest <url|file|dir> --domain <slug> [--lane <lane>] [--trust trusted|untrusted] [--json] [--dir <install>]
1006
+ phronesis ingest --paste --domain <slug> [--lane <lane>] [--dir <install>]
1007
+ phronesis ingest - --domain <slug> --title <title> [--slug <slug>] [--lane <lane>] [--dir <install>]
1008
+ phronesis ingest - --domain <slug> --lane <lane> --source-connector <name>
1009
+ --source-external-id <id> --source-timestamp <iso> --title <title> [--slug <slug>] [--json]
1010
+
1011
+ Stages one operator-pointed source, or every readable text file under a directory,
1012
+ into domains/<slug>/raw/<lane>/ with connector provenance frontmatter, then stops.
1013
+ V1 ingest writes no compiled/ or codex/ state, emits no events, and does not auto-run
1014
+ compile.
1015
+
1016
+ Arguments:
1017
+ <url|file|dir> http(s) URL to fetch, local file to read, or directory to bulk-stage.
1018
+ Use - for titled stdin.
1019
+
1020
+ Options:
1021
+ --domain <slug> Registered domain to write into (required)
1022
+ --lane <lane> Raw content lane (default: inbox; e.g. transcripts)
1023
+ --paste Read source text from stdin
1024
+ --trust <tier> untrusted by default. trusted is honored only for local files.
1025
+ --title <title> Required when source is -; used to derive the staged filename slug.
1026
+ --slug <slug> Optional exact filename slug override for titled stdin.
1027
+ --source-connector <name> Bridge provenance connector name.
1028
+ --source-external-id <id> Bridge stable external item id.
1029
+ --source-timestamp <iso> Bridge source timestamp.
1030
+ --json Emit the ingest report as JSON
1031
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1032
+ `.trim(),
1033
+ );
1034
+ }
1035
+
1036
+ function showCompileHelp() {
1037
+ console.log(
1038
+ `
1039
+ Usage:
1040
+ phronesis compile accept <draft_path> --session <id> [--dir <install>] [--surface <s>]
1041
+ phronesis compile reject <draft_path> --session <id> [--reason '<why>'] [--dir <install>] [--surface <s>]
1042
+ phronesis compile review --session <id> [--domain <slug>] [--approve-all --rationale '<why>'] [--resume] [--json]
1043
+
1044
+ The operator review primitives over a compile candidate (specs/security.md draft
1045
+ lifecycle; core-0017 H9). An untrusted compile stages a draft under .phronesis/drafts/ and
1046
+ emits compile.candidate — no canonical state changes until you accept it. Both events are
1047
+ operator-attributed by contract — there is no --actor flag.
1048
+
1049
+ accept: Validate the draft AS its canonical compiled/ shape, move it into compiled/
1050
+ (overwriting on an update, fresh-writing on a create), discard the draft, and
1051
+ emit action_type.requested → action_type.committed (approval_source recorded —
1052
+ acceptance IS the approval) → compile.accepted. The committed event refreshes the
1053
+ domain's active-context projection (the post-compile hook).
1054
+ reject: Discard the draft and emit compile.rejected { draft_path, reason }. No canonical
1055
+ state changes. A draft is transient (a temp), so discarding it is permitted.
1056
+ review: The batch-approval UX over the same per-draft accept move (T7.5). Lists pending
1057
+ drafts and their fail-closed classification; --approve-all promotes the
1058
+ provably-clean set as ONE logical approval — every compile.accepted shares one
1059
+ batch_id + the operator's one --rationale (change 0025). A draft is EXCLUDED from
1060
+ approve-all (left for individual review) when its LATEST compile.candidate is
1061
+ suspicious_payload-flagged; when it has NO compile.candidate event at all; when that
1062
+ latest candidate carries NO content_digest (not provably clean); or when the on-disk
1063
+ draft's sha-256 no longer matches that digest (the bytes were edited after the clean
1064
+ candidate — change 0027). A benign re-draft (several clean candidates, latest clean,
1065
+ digest matching the current bytes) stays eligible. A mid-batch crash is recoverable:
1066
+ --resume re-drives the batch from a durable .phronesis/ journal, reconciling every
1067
+ missing post-move boundary in order (committed -> projection -> compile.accepted).
1068
+
1069
+ <draft_path> (accept/reject) The .phronesis/drafts/{domain}/{type-dir}/{name}.md candidate.
1070
+ --session <id> Review session the events belong to (required).
1071
+ --reason '<why>' (reject) recorded on compile.rejected.
1072
+ --domain <slug> (review) Domain whose drafts to review (default: pm).
1073
+ --approve-all (review) Promote the provably-clean set as one batch.
1074
+ --rationale '<why>'(review) Required with --approve-all; recorded on every batch compile.accepted.
1075
+ --resume (review) Reconcile a crashed batch from the durable journal.
1076
+ --json (review) Emit the plan / result as JSON.
1077
+
1078
+ accept consumes the approval signal from env PHRONESIS_APPROVAL_SOURCE (the same channel
1079
+ as 'phronesis act'); when unset it records 'operator:compile-accept'.
1080
+ `.trim(),
1081
+ );
1082
+ }
1083
+
1084
+ function showActiveContextHelp() {
1085
+ console.log(
1086
+ `
1087
+ Usage: phronesis active-context refresh --domain <slug> [--dir <install>] [--json]
1088
+
1089
+ Regenerate a domain's tracked active-context projection,
1090
+ domains/<slug>/ACTIVE_CONTEXT.md (specs/compile-retrieve-loop.md §"Generated-state
1091
+ hygiene"; core-0017 H5) — a derived-but-tracked summary of the domain's compiled state,
1092
+ distinct from the gitignored machine index under .phronesis/index/. The post-compile hook
1093
+ shells out to this on every compile commit; 'phronesis validate' flags the projection
1094
+ stale when a compiled object is newer than its entry.
1095
+
1096
+ Options:
1097
+ --domain <slug> The domain whose projection to refresh (required; must be registered)
1098
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1099
+ --json Emit the refresh result as JSON
1100
+ `.trim(),
1101
+ );
1102
+ }
1103
+
1104
+ function showRegistryHelp() {
1105
+ console.log(
1106
+ `
1107
+ Usage: phronesis registry status [--dir <install>] [--json]
1108
+
1109
+ Shows the install's action-type registry (specs/ontology.md §"Action type contract";
1110
+ specs/trust-scopes.md Scope 1): where the table comes from (.phronesis/registry.json
1111
+ action_types vs built-in defaults), every type's effective permission and its relation
1112
+ to the code default, the registered target layout, and the payload schema.
1113
+
1114
+ Operator overrides live in .phronesis/registry.json within a safety lattice whose floor
1115
+ is compiled into @phronesis/core: tightening any permission is allowed, loosening a
1116
+ floor action (record_outcome, promote_to_codex) fails closed at load, layouts and
1117
+ schema loosenings are refused. On an invalid registry this prints the load errors and
1118
+ exits non-zero — the action layer is disabled until the registry is fixed.
1119
+
1120
+ Options:
1121
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1122
+ --json Emit the effective registry as JSON
1123
+ `.trim(),
1124
+ );
1125
+ }
1126
+
1127
+ function showSkillsHelp() {
1128
+ console.log(
1129
+ `
1130
+ Usage: phronesis skills status [--dir <install>] [--json]
1131
+ phronesis skills bump <skill> --to <model_id>@<date> [options]
1132
+
1133
+ status — Shows the install's skill registry (specs/skills.md §"Skill registry"): every
1134
+ skill declared in .phronesis/registry.json's skills section, loaded from
1135
+ skills/{name}/SKILL.md and validated against the frontmatter contract (skills.md
1136
+ §"Frontmatter contract") plus the runtime-free degrade contract (§"Runtime-free
1137
+ invocation", change 0013).
1138
+
1139
+ On an invalid registry or any invalid registered skill this prints the load errors and
1140
+ exits non-zero — the skills layer is disabled until it is fixed. skills/ dirs without a
1141
+ registry entry are listed as unregistered (they do not load).
1142
+
1143
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1144
+ --json Emit the resolved skill records as JSON
1145
+
1146
+ bump — The deployment-unit gate (specs/eval-discipline.md §"Prompt + model is the
1147
+ deployment unit"). Runs the candidate-execution substrate (core-0028) k times under the
1148
+ proposed prompt_pin, aggregates to a STRICT-min floor on BOTH axes (quality + coverage),
1149
+ refuses the comparison if the evaluation identity drifted, blocks an either-axis
1150
+ regression vs the previous accepted pin, increments the skill version, and writes the new
1151
+ pin to skills/<skill>/SKILL.md ONLY on pass — appending a baseline record to
1152
+ skills/<skill>/evals/PINS.md. A bare model bump that regresses is caught before it ships
1153
+ (the Voiceflow safeguard). bump is a dev/release op on git-tracked source: it runs against
1154
+ a repo root (skills/ + changes/ present), never an install.
1155
+
1156
+ --to <pin> Proposed prompt_pin: {model_id}@{date} (required)
1157
+ --repo <path> Repo root (default: cwd; must carry skills/ and changes/)
1158
+ --k <n> Reliability runs per golden (default: 5; floor, not mean)
1159
+ --shape-change The prompt edit changes output shape → MAJOR version bump
1160
+ (default: model-only swap → MINOR, skills.md:43)
1161
+ --accept-regression Proceed over a real, stable-identity regression; logs a structured
1162
+ override record to PINS.md (does NOT bypass identity drift or
1163
+ fabrication)
1164
+ --rebaseline Reset the comparison basis after evaluation-identity drift: re-seed
1165
+ the baseline under the new identity (absolute floor on both axes,
1166
+ no regression check) and write a rebaseline record
1167
+ --coverage-floor <f> Min per-run warranted-capture hit-rate (default: 1.0)
1168
+ --quality-floor <f> Min per-criterion score across k runs (default: 4)
1169
+ --rationale <text> Recorded on an override / rebaseline record
1170
+ --accepted-by <who> Recorded on the PINS.md record (default: operator)
1171
+ --judge-model <m> Judge model for the L2 scores (default: sonnet)
1172
+ --goldens-dir <path> Override the golden corpus dir (default:
1173
+ $PHRONESIS_INSTALL/corpus/skill-goldens/<skill>)
1174
+ --domain <name> Active domain the goldens run under (default: pm)
1175
+ --json Emit the gate decision as JSON
1176
+
1177
+ A fabrication in any run, a quality score below the rubric floor (≥4), or a missed
1178
+ absolute floor on a first-bump/--rebaseline seed is a hard block — never overridable by
1179
+ --accept-regression (which overrides only a relative dip that stays above the floor). A
1180
+ non-completing substrate run (timeout/error) blocks the gate rather than counting as a
1181
+ candidate. An unsigned corpus still runs the gate; the result carries gate_status:
1182
+ provisional (never skipped, never a false green). bump is single-flight: do not run two
1183
+ bumps of the same skill concurrently (it writes git-tracked source; serialization is the
1184
+ daemon's job, T4.1/V1.5).
1185
+ `.trim(),
1186
+ );
1187
+ }
1188
+
1189
+ function showEvalHelp() {
1190
+ console.log(
1191
+ `
1192
+ Usage: phronesis eval view [--skill <name>] [--repo <path>] [options]
1193
+
1194
+ The tiny LOCAL eval data viewer (specs/eval-discipline.md §"Custom data viewer"). Serves a
1195
+ localhost-only page rendering, per skill: the golden list; input -> expected ->
1196
+ model_default (from the goldens); L2 per-criterion means + below-floor (failed) criteria
1197
+ (from results/aggregate.json). The "actual" (candidate output) column appears only once
1198
+ core-0028's candidate runner has persisted output to results/candidates/ — it is never read
1199
+ from runs/*.json (judge scores only) and never invented.
1200
+
1201
+ It binds localhost ONLY, writes nothing into the repo, and exposes nothing over the network
1202
+ (the operator's goldens carry real names). The repo root (skills/ + changes/) locates the
1203
+ rubrics + gitignored candidate results; goldens resolve from the install
1204
+ ($PHRONESIS_INSTALL/corpus/skill-goldens/<skill>, change 0023). Refuses outside a repo root.
1205
+
1206
+ Options:
1207
+ --skill <name> Render only this skill (default: all skills with eval data)
1208
+ --repo <path> Repo root (default: cwd; must carry skills/ and changes/)
1209
+ --goldens-dir <path> Override the golden corpus dir (default:
1210
+ $PHRONESIS_INSTALL/corpus/skill-goldens/<skill>)
1211
+ --port <n> Localhost port (default: an ephemeral port)
1212
+ --host <addr> Bind address (default: 127.0.0.1; off-localhost is refused)
1213
+ --no-serve Print the rendered HTML to stdout instead of serving
1214
+ --json List the skills that would be rendered, as JSON
1215
+ `.trim(),
1216
+ );
1217
+ }
1218
+
1219
+ function showHooksHelp() {
1220
+ console.log(
1221
+ `
1222
+ Usage:
1223
+ phronesis hooks due-check --session <id> [--actor <a>] [--surface <s>] [--dir <install>]
1224
+ phronesis hooks sweep-status --session <id> [--json] [--dir <install>]
1225
+ phronesis hooks skill-start --payload '<hook-json>' [--dir <install>]
1226
+ phronesis hooks skill-complete --session <id> [--payload '<hook-json>'] [--boundary-event <Stop|SubagentStop>] [--json] [--dir <install>]
1227
+ phronesis hooks skill-abandon --payload '<hook-json>' [--json] [--dir <install>]
1228
+ phronesis hooks skill-sweep-stale --session <id> [--json] [--dir <install>]
1229
+
1230
+ Session-boundary hook support (specs/hooks.md §"Emission discipline"; change 0010 D4).
1231
+ The scaffolded Claude Code hooks (.phronesis/hooks/session-start.sh, session-sweep.sh,
1232
+ skill-lifecycle.sh) shell out to these; any Tier-1 surface adapter can do the same.
1233
+
1234
+ due-check: Computes due review_dates across every domain's decisions (review_date
1235
+ <= today, no outcome recorded), emits one outcome.due per due decision
1236
+ (the V1 authoritative emitter — the daemon's date-watch takes over at
1237
+ V1.5), and prints the due list with a record_outcome pointer. Run it at
1238
+ session start; the printout is the review surface.
1239
+
1240
+ sweep-status: Reports whether this session has emitted its capture-sweep marker — a
1241
+ session.end event with a sweep payload ({"sweep":{"candidates":N}};
1242
+ candidates 0 is the honest negative). Prints "swept" (exit 0) or
1243
+ "pending" (exit 1); the scaffolded Stop hook keys its block-once
1244
+ capture prompt on it.
1245
+
1246
+ skill-start / skill-complete / skill-abandon / skill-sweep-stale:
1247
+ Claude Code skill lifecycle adapter plumbing. skill-start consumes
1248
+ PreToolUse(Skill) or UserPromptExpansion hook JSON, mints an
1249
+ adapter-owned invocation_id, emits skill.invoked, and writes the
1250
+ transient .phronesis/invocations/ record that act reads. Complete emits
1251
+ skill.completed and clears it; abandon/stale-sweep clear without a
1252
+ synthetic completion.
1253
+ `.trim(),
1254
+ );
1255
+ }
1256
+
1257
+ function showAdapterHelp() {
1258
+ console.log(
1259
+ `
1260
+ Usage: phronesis adapter test <surface> [--dir <install>] [--domain <slug>] [--json]
1261
+
1262
+ The per-surface adapter self-check (specs/tech-stack.md §"Adapter test command";
1263
+ specs/event-log.md §"Adapter test"). V1 ships the Codex adapter test.
1264
+
1265
+ adapter test codex — asserts the deterministic mechanics of the project-local Codex
1266
+ wiring (change 0028 / core-0034): domains/<slug>/.codex/hooks.json wires each native
1267
+ event to its bare-path launcher, the launchers resolve the install root and hand off to
1268
+ the shared .phronesis/hooks/ scripts, and an unswept Stop yields {"decision":"block"}.
1269
+ It reports the compliance LEVEL — Level 1 (reinforcement: recall / due-check / sweep) and
1270
+ Level 2 (skill-lifecycle), which is PARTIAL on Codex (no UserPromptExpansion/StopFailure;
1271
+ no fabricated skill.invoked/completed) — and points at the operator-verified desktop-app
1272
+ checklist section in docs/surface-setup.md, the half a CLI cannot auto-assert.
1273
+ It is read-only: no canonical state is written and no events are emitted.
1274
+
1275
+ Open Codex AT the domain workspace dir (Codex loads .codex/ only from the opened
1276
+ directory, no parent-walk) and trust the project hooks (CLI /hooks, or the app's
1277
+ Settings → Coding → Hooks). Editing the config re-hashes the hooks → re-trust required.
1278
+
1279
+ Options:
1280
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1281
+ --domain <slug> Check one domain's .codex/ (default: every registered domain)
1282
+ --json Emit the compliance report as JSON
1283
+ `.trim(),
1284
+ );
1285
+ }
1286
+
1287
+ function showGuardHelp() {
1288
+ console.log(
1289
+ `
1290
+ Usage: phronesis guard --dir <install> --session <id> [--surface <s>] [--actor <a>] [--domain <slug>] [--json]
1291
+
1292
+ Pre-answer context relevance guard (specs/compile-retrieve-loop.md §"Context relevance
1293
+ guard"). The prompt is read from stdin only: raw text, or a JSON envelope with
1294
+ {"prompt":"...", "session_id":"..."} mirroring a surface UserPromptSubmit payload.
1295
+ There is no --prompt flag, so prompt text never rides argv.
1296
+
1297
+ When an owned-context advice/action prompt is detected, the guard emits
1298
+ recall.candidate, runs in-process @phronesis/core retrieval over the active domain plus
1299
+ codex/personal, emits recall.injected, and returns either a recall packet or an
1300
+ honest-negative packet. Not-fired prompts return {"fired":false} and emit no events.
1301
+
1302
+ Options:
1303
+ --dir <install> Installation root (required; cwd is not trusted)
1304
+ --session <id> Session id (required)
1305
+ --surface <s> Surface name (default: cli)
1306
+ --actor <a> Actor (default: $PHRONESIS_ACTOR, else agent)
1307
+ --domain <slug> Domain scope override
1308
+ --json Emit the machine-readable guard result
1309
+ `.trim(),
1310
+ );
1311
+ }
1312
+
1313
+ function showSearchHelp() {
1314
+ console.log(
1315
+ `
1316
+ Usage: phronesis search "<query>" [--domain <slug> | --all-domains] [--include-artifacts] [--limit N] [--json] [--dir <install>]
1317
+
1318
+ Stage 1 retrieval (specs/compile-retrieve-loop.md §"Two-stage retrieval"): in-process
1319
+ BM25 over the active domain's compiled/ objects plus the shared codex. Freshness is
1320
+ read-time — the derived machine index (.phronesis/index/) is verified against disk and
1321
+ repaired on every load, so results always reflect current state (no daemon needed).
1322
+
1323
+ Zero hits is an honest negative, not an error: empty_result: true, exit 0.
1324
+
1325
+ Arguments:
1326
+ query The search text (quote it)
1327
+
1328
+ Options:
1329
+ --domain <slug> Domain scope (default: the domains/<slug>/ the cwd is inside, else "${DEFAULT_DOMAIN}")
1330
+ --all-domains Search every domain (default scope is one domain + codex)
1331
+ --include-artifacts Explicitly include durable artifacts for this search (not indexed by default)
1332
+ --limit N Max results (default: 10)
1333
+ --json Emit the contract envelope as JSON
1334
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1335
+ `.trim(),
1336
+ );
1337
+ }
1338
+
1339
+ function showQueryHelp() {
1340
+ console.log(
1341
+ `
1342
+ Usage: phronesis query "<question>" [--domain <slug> | --all-domains] [--include-artifacts] [--limit N] [--hops N] [--json] [--dir <install>]
1343
+
1344
+ Two-stage retrieval (specs/compile-retrieve-loop.md): Stage 1 BM25 finds seed objects,
1345
+ Stage 2 walks each seed's typed links into its connected subgraph — the related
1346
+ decisions, the people involved, and the raw sources they cite (per-result "sources").
1347
+ Semantic rerank (Stage 3) is V1.5+/BYO; this surface stays the same.
1348
+
1349
+ Options:
1350
+ --domain <slug> Domain scope (default: the domains/<slug>/ the cwd is inside, else "${DEFAULT_DOMAIN}")
1351
+ --all-domains Query every domain (default scope is one domain + codex)
1352
+ --include-artifacts Explicitly include durable artifacts for this query (not indexed by default)
1353
+ --limit N Max seed results (default: 10)
1354
+ --hops N Link-walk depth from each seed (default: 1)
1355
+ --json Emit the contract envelope as JSON
1356
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1357
+ `.trim(),
1358
+ );
1359
+ }
1360
+
1361
+ function showLintHelp() {
1362
+ console.log(
1363
+ `
1364
+ Usage: phronesis lint [--domain <slug> | --all-domains] [--stale-months N] [--json] [--dir <install>]
1365
+
1366
+ Read-only deterministic workspace health report over the active domain's compiled/
1367
+ objects plus the shared codex. This is the mechanical foundation for the lint skill:
1368
+ orphans, supersedes/superseded-by gaps, stale forward refs, codex citation defects,
1369
+ seed-adoption hygiene, and relationships value drift.
1370
+
1371
+ It writes nothing: no compiled edits, no raw/degraded captures, no events, no drafts,
1372
+ and no machine-index repair.
1373
+
1374
+ Options:
1375
+ --domain <slug> Domain scope (default: the active domain, else "${DEFAULT_DOMAIN}")
1376
+ --all-domains Lint every registered domain
1377
+ --stale-months N Staleness threshold for forward refs and seed adoption (default: 6)
1378
+ --json Emit the machine-readable lint envelope
1379
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1380
+ `.trim(),
1381
+ );
1382
+ }
1383
+
1384
+ function showDoctorHelp() {
1385
+ console.log(
1386
+ `
1387
+ Usage: phronesis doctor [--dir <install>] [--json]
1388
+
1389
+ Read-only install-health overview — the dogfood "is my install wired correctly?"
1390
+ command. Composes existing diagnostics into a one-screen OK / warn / fail report:
1391
+ install root, registered domain(s), skill registry, hook wiring, Codex surface-adapter
1392
+ status, USER.md placeholder check, and pending-draft count.
1393
+
1394
+ It writes nothing to the install and emits no events (the install is byte-identical
1395
+ after a run). It reports; it does not fix.
1396
+
1397
+ Exit is non-zero only on a FAIL check (no/invalid/unreadable install, an invalid skill
1398
+ registry). WARN checks (incomplete hook wiring, a non-compliant Codex adapter, USER.md
1399
+ placeholders, pending drafts) are informational and never fail the command.
1400
+
1401
+ Options:
1402
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1403
+ --json Emit the machine-readable report: { root, checks, summary, exit }
1404
+ `.trim(),
1405
+ );
1406
+ }
1407
+
1408
+ function showDueHelp() {
1409
+ console.log(
1410
+ `
1411
+ Usage: phronesis due [--json] [--dir <install>]
1412
+ phronesis due complete <schedule_id> [--dir <install>]
1413
+ phronesis due run <schedule_id> [--dir <install>]
1414
+
1415
+ The V1 cadence primitive (change 0042). Recurring work is declared in the schedule
1416
+ registry (.phronesis/schedule.json) — one typed entry per recurrence (id / kind /
1417
+ target / cadence). \`phronesis due\` is a PURE READ: it computes overdue entries from the
1418
+ registry + the clock and prints them. It emits nothing and writes nothing — the
1419
+ \`schedule.due\` events are the session-start due-check's job (phronesis hooks due-check).
1420
+
1421
+ \`phronesis due complete <id>\` is the explicit completion floor: it advances the entry's
1422
+ last-completed date so it stops surfacing until its next cadence boundary. (A domain-
1423
+ agnostic \`kind: skill\` entry also completes automatically when its target skill runs; a
1424
+ domain-scoped one is confirmed with \`due complete\`, since a skill completion carries no domain.)
1425
+
1426
+ \`phronesis due run <id>\` is the operator-invoked one-key executor for deterministic
1427
+ \`cli_verb\` and \`connector_pull\` entries. \`kind: skill\` is refused; run the skill in the
1428
+ operator session, then \`due complete <id>\`. A successful run advances through the same
1429
+ audit-first completion primitive as \`due complete\`; a failing target leaves the schedule due.
1430
+
1431
+ Nothing here ever invokes a model — a schedule only surfaces or runs deterministic work under
1432
+ operator presence.
1433
+
1434
+ Options:
1435
+ --json Emit the overdue list as JSON: [{ schedule_id, target, kind, days_overdue }]
1436
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
1437
+ `.trim(),
1438
+ );
1439
+ }
1440
+
1441
+ function showIndexHelp() {
1442
+ console.log(
1443
+ `
1444
+ Usage: phronesis index rebuild [--json] [--dir <install>]
1445
+
1446
+ Regenerates the derived machine search index (.phronesis/index/) from domains/*/compiled/
1447
+ and the shared codex/ (change 0008: the machine index is derived, gitignored, and
1448
+ rebuildable — never source of truth). Losing it never loses data; search/query also
1449
+ self-repair the index on every load, so rebuild is the explicit full-regeneration verb.
1450
+ `.trim(),
1451
+ );
1452
+ }
1453
+
1454
+ function showEventsHelp() {
1455
+ console.log(
1456
+ `
1457
+ Usage:
1458
+ phronesis events purge --before YYYY-MM-DD [--archive] [--dir <install>]
1459
+ phronesis events rotate [--dir <install>]
1460
+
1461
+ Maintain the local event stream (specs/event-log.md). Automatic scheduling is the daemon's
1462
+ job (T4.1); these are the explicitly-invokable mechanisms. Both verbs ALSO apply the same
1463
+ boundary to the hook audit log (.phronesis/audit.log) — one retention story, one verb family
1464
+ (core-0027). The audit log is a single append file, so it is filtered line-by-line: rotated
1465
+ or purged lines past the boundary move to .phronesis/audit.log.archive (or drop on a plain
1466
+ purge).
1467
+
1468
+ Run these when no session is actively writing the install (the standing one-writer-per-
1469
+ installation model — see actions.js). The audit-log filter rewrites the single live file in
1470
+ place, so an audit line appended by a concurrent hook fire DURING the rewrite can be lost; the
1471
+ daemon serializes this at V1.5. The day-file event stream is unaffected (only inactive past
1472
+ days are touched).
1473
+
1474
+ purge: DELETE day files before --before (operator wipe / privacy). --archive moves them to
1475
+ .phronesis/events/archive/ instead of deleting. Audit lines before --before are
1476
+ dropped too (or archived with --archive).
1477
+ rotate: ARCHIVE day files older than event_log.retention_days (default 90) to archive/, and
1478
+ audit lines past the same window to audit.log.archive.
1479
+ `.trim(),
1480
+ );
1481
+ }
1482
+
1483
+ async function run(argv) {
1484
+ const parsed = parseArgs(argv);
1485
+
1486
+ switch (parsed.command) {
1487
+ case 'help':
1488
+ showHelp();
1489
+ break;
1490
+ case 'init-help':
1491
+ showInitHelp();
1492
+ break;
1493
+ case 'export-help':
1494
+ showExportHelp();
1495
+ break;
1496
+ case 'validate-help':
1497
+ showValidateHelp();
1498
+ break;
1499
+ case 'links-help':
1500
+ showLinksHelp();
1501
+ break;
1502
+ case 'event-help':
1503
+ showEventHelp();
1504
+ break;
1505
+ case 'events-help':
1506
+ showEventsHelp();
1507
+ break;
1508
+ case 'act-help':
1509
+ showActHelp();
1510
+ break;
1511
+ case 'ingest-help':
1512
+ showIngestHelp();
1513
+ break;
1514
+ case 'compile-help':
1515
+ showCompileHelp();
1516
+ break;
1517
+ case 'active-context-help':
1518
+ showActiveContextHelp();
1519
+ break;
1520
+ case 'registry-help':
1521
+ showRegistryHelp();
1522
+ break;
1523
+ case 'skills-help':
1524
+ showSkillsHelp();
1525
+ break;
1526
+ case 'eval-help':
1527
+ showEvalHelp();
1528
+ break;
1529
+ case 'hooks-help':
1530
+ showHooksHelp();
1531
+ break;
1532
+ case 'adapter-help':
1533
+ showAdapterHelp();
1534
+ break;
1535
+ case 'guard-help':
1536
+ showGuardHelp();
1537
+ break;
1538
+ case 'search-help':
1539
+ showSearchHelp();
1540
+ break;
1541
+ case 'query-help':
1542
+ showQueryHelp();
1543
+ break;
1544
+ case 'lint-help':
1545
+ showLintHelp();
1546
+ break;
1547
+ case 'doctor-help':
1548
+ showDoctorHelp();
1549
+ break;
1550
+ case 'due-help':
1551
+ showDueHelp();
1552
+ break;
1553
+ case 'index-help':
1554
+ showIndexHelp();
1555
+ break;
1556
+ case 'version':
1557
+ console.log(version);
1558
+ break;
1559
+ case 'init': {
1560
+ const { init } = await import('./init.js');
1561
+ await init({
1562
+ dir: parsed.dir,
1563
+ force: parsed.force,
1564
+ domain: parsed.domain,
1565
+ name: parsed.name,
1566
+ role: parsed.role,
1567
+ company: parsed.company,
1568
+ workingStyle: parsed.workingStyle,
1569
+ });
1570
+ break;
1571
+ }
1572
+ case 'export': {
1573
+ const { exportInstallation } = await import('./export.js');
1574
+ await exportInstallation({ dir: parsed.dir, out: parsed.out });
1575
+ break;
1576
+ }
1577
+ case 'validate': {
1578
+ const { validateInstallation } = await import('./validate.js');
1579
+ await validateInstallation({ dir: parsed.dir });
1580
+ break;
1581
+ }
1582
+ case 'links': {
1583
+ const { runLinks } = await import('./links.js');
1584
+ await runLinks({
1585
+ dir: parsed.dir,
1586
+ object: parsed.object,
1587
+ hops: parsed.hops,
1588
+ direction: parsed.direction,
1589
+ json: parsed.json,
1590
+ });
1591
+ break;
1592
+ }
1593
+ case 'event-emit': {
1594
+ const { runEventEmit } = await import('./event.js');
1595
+ await runEventEmit({
1596
+ type: parsed.type,
1597
+ session: parsed.session,
1598
+ payload: parsed.payload,
1599
+ actor: parsed.actor,
1600
+ surface: parsed.surface,
1601
+ dir: parsed.dir,
1602
+ invocationId: parsed.invocationId,
1603
+ });
1604
+ break;
1605
+ }
1606
+ case 'event-tail': {
1607
+ const { runEventTail } = await import('./event.js');
1608
+ await runEventTail({
1609
+ type: parsed.type,
1610
+ since: parsed.since,
1611
+ follow: parsed.follow,
1612
+ dir: parsed.dir,
1613
+ });
1614
+ break;
1615
+ }
1616
+ case 'events-purge': {
1617
+ const { runEventsPurge } = await import('./event.js');
1618
+ await runEventsPurge({ before: parsed.before, archive: parsed.archive, dir: parsed.dir });
1619
+ break;
1620
+ }
1621
+ case 'events-rotate': {
1622
+ const { runEventsRotate } = await import('./event.js');
1623
+ await runEventsRotate({ dir: parsed.dir });
1624
+ break;
1625
+ }
1626
+ case 'act': {
1627
+ const { runAct } = await import('./act.js');
1628
+ await runAct({
1629
+ type: parsed.type,
1630
+ json: parsed.json,
1631
+ session: parsed.session,
1632
+ domain: parsed.domain,
1633
+ actor: parsed.actor,
1634
+ surface: parsed.surface,
1635
+ dir: parsed.dir,
1636
+ invocationId: parsed.invocationId,
1637
+ });
1638
+ break;
1639
+ }
1640
+ case 'ingest': {
1641
+ const { runIngest } = await import('./ingest.js');
1642
+ await runIngest({
1643
+ source: parsed.source,
1644
+ paste: parsed.paste,
1645
+ trust: parsed.trust,
1646
+ lane: parsed.lane,
1647
+ domain: parsed.domain,
1648
+ dir: parsed.dir,
1649
+ json: parsed.json,
1650
+ sourceConnector: parsed.sourceConnector,
1651
+ sourceExternalId: parsed.sourceExternalId,
1652
+ sourceTimestamp: parsed.sourceTimestamp,
1653
+ title: parsed.title,
1654
+ slug: parsed.slug,
1655
+ });
1656
+ break;
1657
+ }
1658
+ case 'compile-accept': {
1659
+ const { runCompileAccept } = await import('./compile.js');
1660
+ await runCompileAccept({
1661
+ draftPath: parsed.draftPath,
1662
+ session: parsed.session,
1663
+ surface: parsed.surface,
1664
+ dir: parsed.dir,
1665
+ });
1666
+ break;
1667
+ }
1668
+ case 'compile-reject': {
1669
+ const { runCompileReject } = await import('./compile.js');
1670
+ await runCompileReject({
1671
+ draftPath: parsed.draftPath,
1672
+ reason: parsed.reason,
1673
+ session: parsed.session,
1674
+ surface: parsed.surface,
1675
+ dir: parsed.dir,
1676
+ });
1677
+ break;
1678
+ }
1679
+ case 'compile-review': {
1680
+ const { runCompileReview } = await import('./compile.js');
1681
+ await runCompileReview({
1682
+ domain: parsed.domain,
1683
+ session: parsed.session,
1684
+ surface: parsed.surface,
1685
+ dir: parsed.dir,
1686
+ rationale: parsed.rationale,
1687
+ approveAll: parsed.approveAll,
1688
+ resume: parsed.resume,
1689
+ json: parsed.json,
1690
+ });
1691
+ break;
1692
+ }
1693
+ case 'active-context-refresh': {
1694
+ const { runActiveContextRefresh } = await import('./active-context.js');
1695
+ await runActiveContextRefresh({ dir: parsed.dir, domain: parsed.domain, json: parsed.json });
1696
+ break;
1697
+ }
1698
+ case 'registry-status': {
1699
+ const { runRegistryStatus } = await import('./registry.js');
1700
+ await runRegistryStatus({ dir: parsed.dir, json: parsed.json });
1701
+ break;
1702
+ }
1703
+ case 'skills-status': {
1704
+ const { runSkillsStatus } = await import('./skills.js');
1705
+ await runSkillsStatus({ dir: parsed.dir, json: parsed.json });
1706
+ break;
1707
+ }
1708
+ case 'eval-view': {
1709
+ const { runEvalView } = await import('./eval.js');
1710
+ await runEvalView({
1711
+ repo: parsed.repo,
1712
+ skill: parsed.skill,
1713
+ goldensDir: parsed.goldensDir,
1714
+ port: parsed.port,
1715
+ host: parsed.host,
1716
+ json: parsed.json,
1717
+ serve: parsed.serve,
1718
+ });
1719
+ break;
1720
+ }
1721
+ case 'skills-bump': {
1722
+ const { runSkillsBump } = await import('./skills.js');
1723
+ await runSkillsBump({
1724
+ skill: parsed.skill,
1725
+ to: parsed.to,
1726
+ repo: parsed.repo,
1727
+ k: parsed.k,
1728
+ acceptRegression: parsed.acceptRegression,
1729
+ rebaseline: parsed.rebaseline,
1730
+ shapeChange: parsed.shapeChange,
1731
+ qualityFloor: parsed.qualityFloor,
1732
+ coverageFloor: parsed.coverageFloor,
1733
+ rationale: parsed.rationale,
1734
+ acceptedBy: parsed.acceptedBy,
1735
+ judgeModel: parsed.judgeModel,
1736
+ goldensDir: parsed.goldensDir,
1737
+ domain: parsed.domain,
1738
+ json: parsed.json,
1739
+ });
1740
+ break;
1741
+ }
1742
+ case 'hooks-due-check': {
1743
+ const { runHooksDueCheck } = await import('./hooks.js');
1744
+ await runHooksDueCheck({
1745
+ session: parsed.session,
1746
+ dir: parsed.dir,
1747
+ actor: parsed.actor,
1748
+ surface: parsed.surface,
1749
+ });
1750
+ break;
1751
+ }
1752
+ case 'hooks-sweep-status': {
1753
+ const { runHooksSweepStatus } = await import('./hooks.js');
1754
+ await runHooksSweepStatus({ session: parsed.session, dir: parsed.dir, json: parsed.json });
1755
+ break;
1756
+ }
1757
+ case 'hooks-skill-start': {
1758
+ const { runHooksSkillStart } = await import('./hooks.js');
1759
+ await runHooksSkillStart({ dir: parsed.dir, payload: parsed.payload });
1760
+ break;
1761
+ }
1762
+ case 'hooks-skill-complete': {
1763
+ const { runHooksSkillComplete } = await import('./hooks.js');
1764
+ await runHooksSkillComplete({
1765
+ session: parsed.session,
1766
+ dir: parsed.dir,
1767
+ payload: parsed.payload,
1768
+ boundaryEvent: parsed.boundaryEvent,
1769
+ json: parsed.json,
1770
+ });
1771
+ break;
1772
+ }
1773
+ case 'hooks-skill-abandon': {
1774
+ const { runHooksSkillAbandon } = await import('./hooks.js');
1775
+ await runHooksSkillAbandon({ dir: parsed.dir, payload: parsed.payload, json: parsed.json });
1776
+ break;
1777
+ }
1778
+ case 'hooks-skill-sweep-stale': {
1779
+ const { runHooksSkillSweepStale } = await import('./hooks.js');
1780
+ await runHooksSkillSweepStale({ session: parsed.session, dir: parsed.dir, json: parsed.json });
1781
+ break;
1782
+ }
1783
+ case 'adapter-test': {
1784
+ const { runAdapterTest } = await import('./adapter.js');
1785
+ await runAdapterTest({
1786
+ surface: parsed.surface,
1787
+ dir: parsed.dir,
1788
+ domain: parsed.domain,
1789
+ json: parsed.json,
1790
+ });
1791
+ break;
1792
+ }
1793
+ case 'guard': {
1794
+ const { runGuard } = await import('./guard.js');
1795
+ await runGuard({
1796
+ dir: parsed.dir,
1797
+ session: parsed.session,
1798
+ surface: parsed.surface,
1799
+ actor: parsed.actor,
1800
+ domain: parsed.domain,
1801
+ json: parsed.json,
1802
+ });
1803
+ break;
1804
+ }
1805
+ case 'search': {
1806
+ const { runSearch } = await import('./search.js');
1807
+ await runSearch({
1808
+ query: parsed.query,
1809
+ dir: parsed.dir,
1810
+ domain: parsed.domain,
1811
+ allDomains: parsed.allDomains,
1812
+ includeArtifacts: parsed.includeArtifacts,
1813
+ limit: parsed.limit,
1814
+ json: parsed.json,
1815
+ });
1816
+ break;
1817
+ }
1818
+ case 'query': {
1819
+ const { runQuery } = await import('./search.js');
1820
+ await runQuery({
1821
+ query: parsed.query,
1822
+ dir: parsed.dir,
1823
+ domain: parsed.domain,
1824
+ allDomains: parsed.allDomains,
1825
+ includeArtifacts: parsed.includeArtifacts,
1826
+ limit: parsed.limit,
1827
+ hops: parsed.hops,
1828
+ json: parsed.json,
1829
+ });
1830
+ break;
1831
+ }
1832
+ case 'lint': {
1833
+ const { runLint } = await import('./lint.js');
1834
+ await runLint({
1835
+ dir: parsed.dir,
1836
+ domain: parsed.domain,
1837
+ allDomains: parsed.allDomains,
1838
+ staleMonths: parsed.staleMonths,
1839
+ json: parsed.json,
1840
+ });
1841
+ break;
1842
+ }
1843
+ case 'doctor': {
1844
+ const { runDoctor } = await import('./doctor.js');
1845
+ await runDoctor({ dir: parsed.dir, json: parsed.json });
1846
+ break;
1847
+ }
1848
+ case 'due': {
1849
+ const { runDue } = await import('./due.js');
1850
+ await runDue({ dir: parsed.dir, json: parsed.json });
1851
+ break;
1852
+ }
1853
+ case 'due-complete': {
1854
+ const { runDueComplete } = await import('./due.js');
1855
+ await runDueComplete({ dir: parsed.dir, scheduleId: parsed.scheduleId });
1856
+ break;
1857
+ }
1858
+ case 'due-run': {
1859
+ const { runDueRun } = await import('./due.js');
1860
+ await runDueRun({ dir: parsed.dir, scheduleId: parsed.scheduleId });
1861
+ break;
1862
+ }
1863
+ case 'index-rebuild': {
1864
+ const { runIndexRebuild } = await import('./search.js');
1865
+ await runIndexRebuild({ dir: parsed.dir, json: parsed.json });
1866
+ break;
1867
+ }
1868
+ case 'invalid':
1869
+ console.error(parsed.error);
1870
+ console.log();
1871
+ showHelp();
1872
+ process.exitCode = 1;
1873
+ break;
1874
+ case 'unknown':
1875
+ console.error(`Unknown command: ${parsed.input}`);
1876
+ console.log();
1877
+ showHelp();
1878
+ process.exitCode = 1;
1879
+ break;
1880
+ }
1881
+ }
1882
+
1883
+ export { parseArgs, run, showHelp, showInitHelp };
1884
+
1885
+ if (process.argv[1]) {
1886
+ try {
1887
+ const arg1Real = realpathSync(resolve(process.argv[1]));
1888
+ const selfReal = fileURLToPath(import.meta.url);
1889
+ if (arg1Real === selfReal) run();
1890
+ } catch {
1891
+ // realpathSync can fail if the path doesn't exist; skip silently
1892
+ }
1893
+ }