phronesis 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/src/act.js +62 -1
  3. package/src/adapter.js +366 -61
  4. package/src/cli.js +527 -32
  5. package/src/compile.js +141 -10
  6. package/src/doctor.js +63 -2
  7. package/src/hook.js +312 -0
  8. package/src/hooks-refresh.js +1363 -0
  9. package/src/hooks.js +48 -33
  10. package/src/init.js +37 -4
  11. package/src/launcher.js +2105 -0
  12. package/src/layout.js +51 -7
  13. package/src/profile.js +169 -0
  14. package/src/prompts.js +88 -0
  15. package/src/skills-bump.js +88 -20
  16. package/src/skills.js +103 -10
  17. package/templates/codex/INDEX.md +4 -2
  18. package/templates/codex/seed/ai-practice/the-workspace-is-part-of-the-thinking.md +96 -0
  19. package/templates/codex-surface/README.md +15 -16
  20. package/templates/codex-surface/hooks/_resolve.sh +25 -24
  21. package/templates/codex-surface/hooks/recall-guard.sh +5 -30
  22. package/templates/codex-surface/hooks/session-start.sh +5 -18
  23. package/templates/codex-surface/hooks/session-sweep-precompact.sh +5 -20
  24. package/templates/codex-surface/hooks/session-sweep-subagent.sh +5 -23
  25. package/templates/codex-surface/hooks/session-sweep.sh +5 -19
  26. package/templates/launcher/phronesis +95 -0
  27. package/templates/phronesis-hooks/compile-active-context.sh +12 -38
  28. package/templates/phronesis-hooks/recall-guard.sh +12 -103
  29. package/templates/phronesis-hooks/session-start.sh +10 -88
  30. package/templates/phronesis-hooks/session-sweep.sh +16 -139
  31. package/templates/phronesis-hooks/skill-lifecycle.sh +12 -37
  32. package/templates/skills/lint/SKILL.md +7 -2
  33. package/templates/skills/onboard/SKILL.md +47 -9
  34. package/templates/skills/onboard/evals/rubric.md +5 -3
  35. package/templates/skills/prd-draft/SKILL.md +34 -26
  36. package/templates/skills/prd-draft/evals/rubric.md +1 -1
  37. package/vendor/core/src/action-registry.js +6 -3
  38. package/vendor/core/src/actions.js +120 -16
  39. package/vendor/core/src/index.js +3 -0
  40. package/vendor/core/src/lint.js +22 -5
  41. package/vendor/core/src/skill-lifecycle.js +67 -7
package/src/cli.js CHANGED
@@ -4,7 +4,6 @@ import { createRequire } from 'node:module';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { resolve } from 'node:path';
6
6
  import { realpathSync } from 'node:fs';
7
- import { DEFAULT_DOMAIN } from './layout.js';
8
7
  import { COVERAGE_FLOOR_RANGE, MAX_K, QUALITY_FLOOR_RANGE } from './skills-bump.js';
9
8
 
10
9
  const nodeMajor = parseInt(process.versions.node.split('.')[0], 10);
@@ -21,6 +20,8 @@ function parseArgs(argv = process.argv.slice(2)) {
21
20
  if (!cmd || cmd === '--help' || cmd === '-h') return { command: 'help' };
22
21
  if (cmd === '--version' || cmd === '-v') return { command: 'version' };
23
22
  if (cmd === 'init') return parseInitArgs(rest);
23
+ if (cmd === 'launcher') return parseLauncherArgs(rest);
24
+ if (cmd === 'profile') return parseProfileArgs(rest);
24
25
  if (cmd === 'export') return parseExportArgs(rest);
25
26
  if (cmd === 'validate') return parseValidateArgs(rest);
26
27
  if (cmd === 'links') return parseLinksArgs(rest);
@@ -32,6 +33,7 @@ function parseArgs(argv = process.argv.slice(2)) {
32
33
  if (cmd === 'active-context') return parseActiveContextArgs(rest);
33
34
  if (cmd === 'registry') return parseRegistryArgs(rest);
34
35
  if (cmd === 'skills') return parseSkillsArgs(rest);
36
+ if (cmd === 'hook') return parseHookArgs(rest);
35
37
  if (cmd === 'hooks') return parseHooksArgs(rest);
36
38
  if (cmd === 'adapter') return parseAdapterArgs(rest);
37
39
  if (cmd === 'guard') return parseGuardArgs(rest);
@@ -48,6 +50,7 @@ function parseArgs(argv = process.argv.slice(2)) {
48
50
  function parseInitArgs(args) {
49
51
  let dir;
50
52
  let force = false;
53
+ let persist;
51
54
  // domain + the minimal-profile value flags (core-0039). Each accepts `--flag value` and `--flag=value`.
52
55
  const VALUE_FLAGS = ['domain', 'name', 'role', 'company', 'working-style'];
53
56
  const values = {};
@@ -59,6 +62,14 @@ function parseInitArgs(args) {
59
62
  force = true;
60
63
  continue;
61
64
  }
65
+ if (a === '--persist' || a === '--no-persist') {
66
+ const requested = a === '--persist';
67
+ if (persist !== undefined && persist !== requested) {
68
+ return { command: 'invalid', error: 'init: --persist and --no-persist are mutually exclusive.' };
69
+ }
70
+ persist = requested;
71
+ continue;
72
+ }
62
73
  let matched = false;
63
74
  for (const f of VALUE_FLAGS) {
64
75
  if (a === `--${f}`) {
@@ -92,6 +103,168 @@ function parseInitArgs(args) {
92
103
  role: values.role,
93
104
  company: values.company,
94
105
  workingStyle: values['working-style'],
106
+ persist,
107
+ };
108
+ }
109
+
110
+ function parseLauncherArgs(args) {
111
+ const [sub, ...rest] = args;
112
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'launcher-help' };
113
+ if (sub === 'install') {
114
+ const parsed = parseOptions(rest, {
115
+ values: ['backend', 'app-target', 'app-runtime', 'app-version', 'expect-owner', 'expect-version'],
116
+ flags: ['force-launcher', 'if-owned', 'no-path-offer', 'expect-absent'],
117
+ });
118
+ if (parsed.help) return { command: 'launcher-help' };
119
+ if (parsed.error) return { command: 'invalid', error: `${parsed.error} for launcher install.` };
120
+ if (parsed.positionals.length) {
121
+ return { command: 'invalid', error: 'launcher install takes no positional arguments.' };
122
+ }
123
+ const backend = parsed.values.backend || 'self-copy';
124
+ if (!['app', 'self-copy'].includes(backend)) {
125
+ return { command: 'invalid', error: `launcher install: --backend must be app | self-copy (got ${JSON.stringify(backend)}).` };
126
+ }
127
+ const appValues = ['app-target', 'app-runtime', 'app-version'];
128
+ if (backend === 'app') {
129
+ const missing = ['app-target', 'app-runtime'].filter((name) => !parsed.values[name]);
130
+ if (missing.length) {
131
+ return { command: 'invalid', error: `launcher install --backend app requires ${missing.map((name) => `--${name}`).join(', ')}.` };
132
+ }
133
+ } else if (appValues.some((name) => parsed.values[name] !== undefined) || parsed.flags['if-owned']) {
134
+ return { command: 'invalid', error: 'launcher install: app target/runtime/version and --if-owned require --backend app.' };
135
+ }
136
+ const expectOwner = parsed.values['expect-owner'];
137
+ const expectVersion = parsed.values['expect-version'];
138
+ const expectAbsent = Boolean(parsed.flags['expect-absent']);
139
+ if (parsed.flags['if-owned'] && (expectAbsent || expectOwner !== undefined || expectVersion !== undefined)) {
140
+ return { command: 'invalid', error: 'launcher install: --if-owned cannot be combined with --expect-absent/--expect-owner/--expect-version.' };
141
+ }
142
+ if (expectAbsent && (expectOwner !== undefined || expectVersion !== undefined)) {
143
+ return { command: 'invalid', error: 'launcher install: --expect-absent cannot be combined with --expect-owner/--expect-version.' };
144
+ }
145
+ if ((expectOwner === undefined) !== (expectVersion === undefined)) {
146
+ return { command: 'invalid', error: 'launcher install: --expect-owner and --expect-version must be passed together.' };
147
+ }
148
+ if (expectOwner !== undefined && !['app', 'self-copy'].includes(expectOwner)) {
149
+ return { command: 'invalid', error: `launcher install: --expect-owner must be app | self-copy (got ${JSON.stringify(expectOwner)}).` };
150
+ }
151
+ return {
152
+ command: 'launcher-install',
153
+ forceLauncher: Boolean(parsed.flags['force-launcher']),
154
+ backend,
155
+ appTarget: parsed.values['app-target'],
156
+ appRuntime: parsed.values['app-runtime'],
157
+ appVersion: parsed.values['app-version'],
158
+ ifOwned: Boolean(parsed.flags['if-owned']),
159
+ offerPath: !parsed.flags['no-path-offer'],
160
+ expectedPrior: expectAbsent
161
+ ? { absent: true }
162
+ : expectOwner === undefined
163
+ ? null
164
+ : { owner: expectOwner, version: expectVersion },
165
+ };
166
+ }
167
+ if (sub === 'remove') {
168
+ const parsed = parseOptions(rest, { values: ['backend'], flags: ['force-launcher'] });
169
+ if (parsed.help) return { command: 'launcher-help' };
170
+ if (parsed.error) return { command: 'invalid', error: `${parsed.error} for launcher remove.` };
171
+ if (parsed.positionals.length) {
172
+ return { command: 'invalid', error: 'launcher remove takes no positional arguments.' };
173
+ }
174
+ const backend = parsed.values.backend || 'self-copy';
175
+ if (!['app', 'self-copy'].includes(backend)) {
176
+ return { command: 'invalid', error: `launcher remove: --backend must be app | self-copy (got ${JSON.stringify(backend)}).` };
177
+ }
178
+ return {
179
+ command: 'launcher-remove',
180
+ forceLauncher: Boolean(parsed.flags['force-launcher']),
181
+ backend,
182
+ };
183
+ }
184
+ if (sub === 'path') {
185
+ const parsed = parseOptions(rest, { flags: ['append', 'append-consented-by-surface', 'json'] });
186
+ if (parsed.help) return { command: 'launcher-help' };
187
+ if (parsed.error) return { command: 'invalid', error: `${parsed.error} for launcher path.` };
188
+ if (parsed.positionals.length) {
189
+ return { command: 'invalid', error: 'launcher path takes no positional arguments.' };
190
+ }
191
+ if (parsed.flags['append-consented-by-surface'] && !parsed.flags.append) {
192
+ return { command: 'invalid', error: 'launcher path: --append-consented-by-surface requires --append.' };
193
+ }
194
+ return {
195
+ command: 'launcher-path',
196
+ append: Boolean(parsed.flags.append),
197
+ surfaceConsented: Boolean(parsed.flags['append-consented-by-surface']),
198
+ json: Boolean(parsed.flags.json),
199
+ };
200
+ }
201
+ if (sub === 'status') {
202
+ const parsed = parseOptions(rest, { flags: ['json'] });
203
+ if (parsed.help) return { command: 'launcher-help' };
204
+ if (parsed.error) return { command: 'invalid', error: `${parsed.error} for launcher status.` };
205
+ if (parsed.positionals.length) {
206
+ return { command: 'invalid', error: 'launcher status takes no positional arguments.' };
207
+ }
208
+ return { command: 'launcher-status', json: Boolean(parsed.flags.json) };
209
+ }
210
+ return { command: 'invalid', error: `Unknown launcher subcommand: ${sub} (expected: install | remove | path | status).` };
211
+ }
212
+
213
+ // `phronesis profile set` (core-0062 D1) — the non-destructive repair verb for the stamped
214
+ // operator profile. Same value-flag grammar as init (--flag value / --flag=value); no --force.
215
+ function parseProfileArgs(args) {
216
+ const [sub, ...rest] = args;
217
+ if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'profile-help' };
218
+ if (sub !== 'set') return { command: 'invalid', error: `Unknown profile subcommand: ${sub} (expected: set).` };
219
+
220
+ let dir;
221
+ const VALUE_FLAGS = ['domain', 'name', 'role', 'company', 'working-style'];
222
+ const values = {};
223
+ for (let i = 0; i < rest.length; i += 1) {
224
+ const a = rest[i];
225
+ if (a === '--help' || a === '-h') return { command: 'profile-help' };
226
+ let matched = false;
227
+ for (const f of VALUE_FLAGS) {
228
+ if (a === `--${f}`) {
229
+ const v = rest[i + 1];
230
+ if (v === undefined || v.startsWith('--')) return { command: 'invalid', error: `Missing value for --${f}` };
231
+ values[f] = v;
232
+ i += 1;
233
+ matched = true;
234
+ break;
235
+ }
236
+ if (a.startsWith(`--${f}=`)) {
237
+ values[f] = a.slice(`--${f}=`.length);
238
+ matched = true;
239
+ break;
240
+ }
241
+ }
242
+ if (matched) continue;
243
+ if (a === '--dir') {
244
+ const v = rest[i + 1];
245
+ if (v === undefined || v.startsWith('--')) return { command: 'invalid', error: 'Missing value for --dir' };
246
+ dir = v;
247
+ i += 1;
248
+ continue;
249
+ }
250
+ if (a.startsWith('--dir=')) {
251
+ dir = a.slice('--dir='.length);
252
+ continue;
253
+ }
254
+ if (a.startsWith('-')) return { command: 'invalid', error: `Unknown option for profile set: ${a}` };
255
+ return { command: 'invalid', error: 'profile set takes no positional arguments (use --name, --role, --company, --working-style).' };
256
+ }
257
+ if (values.name === undefined && values.role === undefined && values.company === undefined && values['working-style'] === undefined) {
258
+ return { command: 'invalid', error: 'profile set: pass at least one of --name, --role, --company, --working-style.' };
259
+ }
260
+ return {
261
+ command: 'profile-set',
262
+ dir,
263
+ domain: values.domain,
264
+ name: values.name,
265
+ role: values.role,
266
+ company: values.company,
267
+ workingStyle: values['working-style'],
95
268
  };
96
269
  }
97
270
 
@@ -197,8 +370,9 @@ function parseLinksArgs(args) {
197
370
  // Compact option parser shared by the event commands (`--name value` / `--name=value` for
198
371
  // `values`, presence flags for `flags`, the rest positional). The older parsers above predate
199
372
  // it and stay hand-rolled; new multi-option commands use this to avoid the per-flag boilerplate.
200
- function parseOptions(args, { values = [], flags = [] } = {}) {
373
+ function parseOptions(args, { values = [], multiValues = [], flags = [] } = {}) {
201
374
  const valueSet = new Set(values);
375
+ const multiValueSet = new Set(multiValues);
202
376
  const flagSet = new Set(flags);
203
377
  const out = { values: {}, flags: {}, positionals: [] };
204
378
  for (let i = 0; i < args.length; i += 1) {
@@ -208,10 +382,11 @@ function parseOptions(args, { values = [], flags = [] } = {}) {
208
382
  const eq = a.indexOf('=');
209
383
  const name = eq === -1 ? a.slice(2) : a.slice(2, eq);
210
384
  if (flagSet.has(name)) {
385
+ if (eq !== -1) return { error: `Presence flag --${name} does not accept a value` };
211
386
  out.flags[name] = true;
212
387
  continue;
213
388
  }
214
- if (valueSet.has(name)) {
389
+ if (valueSet.has(name) || multiValueSet.has(name)) {
215
390
  if (eq === -1) {
216
391
  // A bare `--name` at the end, or one followed by another `--option`, is a missing
217
392
  // value — not "consume the next token". Swallowing it silently mislabels the next
@@ -220,10 +395,17 @@ function parseOptions(args, { values = [], flags = [] } = {}) {
220
395
  if (next === undefined || next.startsWith('--')) {
221
396
  return { error: `Missing value for --${name}` };
222
397
  }
223
- out.values[name] = next;
398
+ if (multiValueSet.has(name)) {
399
+ if (!Array.isArray(out.values[name])) out.values[name] = [];
400
+ out.values[name].push(next);
401
+ } else out.values[name] = next;
224
402
  i += 1;
225
403
  } else {
226
- out.values[name] = a.slice(eq + 1);
404
+ const value = a.slice(eq + 1);
405
+ if (multiValueSet.has(name)) {
406
+ if (!Array.isArray(out.values[name])) out.values[name] = [];
407
+ out.values[name].push(value);
408
+ } else out.values[name] = value;
227
409
  }
228
410
  continue;
229
411
  }
@@ -278,12 +460,26 @@ function parseEventArgs(args) {
278
460
  // and is rejected); flags carry execution context only. The approval signal is env
279
461
  // (PHRONESIS_APPROVAL_SOURCE), set by an approving adapter — deliberately not a flag.
280
462
  function parseActArgs(args) {
281
- const p = parseOptions(args, { values: ['json', 'session', 'domain', 'actor', 'surface', 'dir', 'invocation-id'] });
463
+ const p = parseOptions(args, {
464
+ values: ['json', 'session', 'domain', 'actor', 'surface', 'dir', 'invocation-id'],
465
+ flags: ['schema'],
466
+ });
282
467
  if (p.help) return { command: 'act-help' };
283
468
  if (p.error) return { command: 'invalid', error: `${p.error} for act.` };
284
469
  if (p.positionals.length > 1) {
285
470
  return { command: 'invalid', error: 'act takes one <type> argument (semantic inputs go inside --json).' };
286
471
  }
472
+ // `--schema` is a read-only introspection of the effective payload schema — it does not
473
+ // run the action, so it is mutually exclusive with `--json` (describe vs execute).
474
+ if (p.flags.schema) {
475
+ if (p.values.json !== undefined) {
476
+ return {
477
+ command: 'invalid',
478
+ error: 'act --schema and --json are mutually exclusive: --schema prints the payload contract, --json executes against it.',
479
+ };
480
+ }
481
+ return { command: 'act-schema', type: p.positionals[0], dir: p.values.dir };
482
+ }
287
483
  const parsed = {
288
484
  command: 'act',
289
485
  type: p.positionals[0],
@@ -334,7 +530,7 @@ function parseCompileArgs(args) {
334
530
  // accept/reject are operator-driven by contract — no --actor (the events are always
335
531
  // actor: operator; the approval signal rides env PHRONESIS_APPROVAL_SOURCE, like act).
336
532
  if (sub === 'accept') {
337
- const p = parseOptions(rest, { values: ['session', 'surface', 'dir'] });
533
+ const p = parseOptions(rest, { values: ['session', 'surface', 'dir', 'reviewed-digest'] });
338
534
  if (p.help) return { command: 'compile-help' };
339
535
  if (p.error) return { command: 'invalid', error: `${p.error} for compile accept.` };
340
536
  if (p.positionals.length !== 1) {
@@ -346,6 +542,9 @@ function parseCompileArgs(args) {
346
542
  session: p.values.session,
347
543
  surface: p.values.surface,
348
544
  dir: p.values.dir,
545
+ // --reviewed-digest <sha>: refuse in-child if the draft's current bytes differ from the bytes the
546
+ // operator reviewed (closes the accept byte-race at the true promotion boundary).
547
+ reviewedDigest: p.values['reviewed-digest'],
349
548
  };
350
549
  }
351
550
  if (sub === 'reject') {
@@ -366,7 +565,7 @@ function parseCompileArgs(args) {
366
565
  }
367
566
  if (sub === 'review') {
368
567
  const p = parseOptions(rest, {
369
- values: ['domain', 'session', 'surface', 'dir', 'rationale'],
568
+ values: ['domain', 'session', 'surface', 'dir', 'rationale', 'approve-only'],
370
569
  flags: ['approve-all', 'resume', 'json'],
371
570
  });
372
571
  if (p.help) return { command: 'compile-help' };
@@ -382,6 +581,8 @@ function parseCompileArgs(args) {
382
581
  dir: p.values.dir,
383
582
  rationale: p.values.rationale,
384
583
  approveAll: Boolean(p.flags['approve-all']),
584
+ // --approve-only <comma-list>: restrict approve-all to a caller-reviewed subset (else all eligible).
585
+ approveOnly: p.values['approve-only'],
385
586
  resume: Boolean(p.flags.resume),
386
587
  json: Boolean(p.flags.json),
387
588
  };
@@ -441,7 +642,7 @@ function parseSkillsArgs(args) {
441
642
  if (sub === 'bump') {
442
643
  const p = parseOptions(rest, {
443
644
  values: ['to', 'repo', 'k', 'coverage-floor', 'quality-floor', 'rationale', 'accepted-by', 'judge-model', 'goldens-dir', 'domain'],
444
- flags: ['accept-regression', 'rebaseline', 'shape-change', 'json'],
645
+ flags: ['accept-regression', 'rebaseline', 'verify-current', 'shape-change', 'json'],
445
646
  });
446
647
  if (p.help) return { command: 'skills-help' };
447
648
  if (p.error) return { command: 'invalid', error: `${p.error} for skills bump.` };
@@ -488,6 +689,7 @@ function parseSkillsArgs(args) {
488
689
  domain: p.values.domain,
489
690
  acceptRegression: Boolean(p.flags['accept-regression']),
490
691
  rebaseline: Boolean(p.flags.rebaseline),
692
+ verifyCurrent: Boolean(p.flags['verify-current']),
491
693
  shapeChange: Boolean(p.flags['shape-change']),
492
694
  json: Boolean(p.flags.json),
493
695
  };
@@ -532,6 +734,31 @@ function parseEvalArgs(args) {
532
734
  function parseHooksArgs(args) {
533
735
  const [sub, ...rest] = args;
534
736
  if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'hooks-help' };
737
+ if (sub === 'refresh') {
738
+ const p = parseOptions(rest, {
739
+ values: ['dir'],
740
+ multiValues: ['apply'],
741
+ flags: ['apply-all', 'persist', 'no-persist'],
742
+ });
743
+ if (p.help) return { command: 'hooks-help' };
744
+ if (p.error) return { command: 'invalid', error: `${p.error} for hooks refresh.` };
745
+ if (p.positionals.length) {
746
+ return { command: 'invalid', error: 'hooks refresh takes no positional arguments.' };
747
+ }
748
+ if (p.flags.persist && p.flags['no-persist']) {
749
+ return { command: 'invalid', error: 'hooks refresh: --persist and --no-persist are mutually exclusive.' };
750
+ }
751
+ if (p.flags['apply-all'] && p.values.apply?.length) {
752
+ return { command: 'invalid', error: 'hooks refresh: --apply and --apply-all are mutually exclusive.' };
753
+ }
754
+ return {
755
+ command: 'hooks-refresh',
756
+ dir: p.values.dir,
757
+ apply: p.values.apply || [],
758
+ applyAll: Boolean(p.flags['apply-all']),
759
+ persist: p.flags.persist ? true : p.flags['no-persist'] ? false : undefined,
760
+ };
761
+ }
535
762
  if (sub === 'due-check') {
536
763
  const p = parseOptions(rest, { values: ['session', 'dir', 'actor', 'surface'] });
537
764
  if (p.help) return { command: 'hooks-help' };
@@ -618,12 +845,31 @@ function parseHooksArgs(args) {
618
845
  command: 'invalid',
619
846
  error:
620
847
  `Unknown hooks subcommand: ${sub} ` +
621
- '(expected: due-check | sweep-status | skill-start | skill-complete | skill-abandon | skill-sweep-stale).',
848
+ '(expected: refresh | due-check | sweep-status | skill-start | skill-complete | skill-abandon | skill-sweep-stale).',
849
+ };
850
+ }
851
+
852
+ function parseHookArgs(args) {
853
+ const p = parseOptions(args, { values: ['surface', 'mode'] });
854
+ if (p.help) return { command: 'hook-help' };
855
+ if (p.error) return { command: 'invalid', error: `${p.error} for hook.` };
856
+ if (p.positionals.length !== 1) {
857
+ return { command: 'invalid', error: 'hook takes exactly one <event> argument.' };
858
+ }
859
+ if (!p.values.surface) {
860
+ return { command: 'invalid', error: 'hook requires --surface <surface>.' };
861
+ }
862
+ return {
863
+ command: 'hook',
864
+ event: p.positionals[0],
865
+ surface: p.values.surface,
866
+ mode: p.values.mode,
622
867
  };
623
868
  }
624
869
 
625
870
  // `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`.
871
+ // §"Adapter test command"; change 0028 / core-0034; change 0055). V1 ships `adapter test codex`
872
+ // and `adapter test desktop`.
627
873
  function parseAdapterArgs(args) {
628
874
  const [sub, ...rest] = args;
629
875
  if (sub === undefined || sub === '--help' || sub === '-h') return { command: 'adapter-help' };
@@ -632,7 +878,7 @@ function parseAdapterArgs(args) {
632
878
  if (p.help) return { command: 'adapter-help' };
633
879
  if (p.error) return { command: 'invalid', error: `${p.error} for adapter test.` };
634
880
  if (p.positionals.length !== 1) {
635
- return { command: 'invalid', error: 'adapter test takes one <surface> argument (V1: codex).' };
881
+ return { command: 'invalid', error: 'adapter test takes one <surface> argument (V1: codex, desktop).' };
636
882
  }
637
883
  return {
638
884
  command: 'adapter-test',
@@ -818,6 +1064,7 @@ Usage: phronesis <command>
818
1064
 
819
1065
  Commands:
820
1066
  init [dir] Create a Phronesis installation (default dir: phronesis-installation)
1067
+ launcher <sub> Install or remove the managed command dispatcher
821
1068
  export [dir] Tar an installation into a portable archive (default dir: current)
822
1069
  validate [dir] Check compiled + codex objects against the ontology contract
823
1070
  search <query> Stage 1 retrieval — BM25 over active-domain compiled/ + shared codex
@@ -832,7 +1079,8 @@ Commands:
832
1079
  registry <sub> Show the install's action-type + hook registry (status)
833
1080
  skills <sub> Show the install's skill registry (status)
834
1081
  eval <sub> Local eval data viewer (view) — goldens, scores, candidate output
835
- hooks <sub> Session-boundary hooks: due-check, sweep-status
1082
+ hook <event> Surface hook dispatcher (stdin JSON → surface-shaped stdout)
1083
+ hooks <sub> Refresh installed hook wiring; session-boundary hook support
836
1084
  adapter <sub> Per-surface adapter self-check (test codex)
837
1085
  guard Prompt-submit recall guard (stdin-only)
838
1086
  ingest <source> Stage one URL, file, or stdin paste into domain raw/
@@ -851,6 +1099,7 @@ function showInitHelp() {
851
1099
  `
852
1100
  Usage: phronesis init [dir] [--force] [--domain <slug>]
853
1101
  [--name <name>] [--role <role>] [--company <co>] [--working-style <style>]
1102
+ [--persist | --no-persist]
854
1103
 
855
1104
  Creates a three-layer installation at [dir]:
856
1105
  codex/ shared principles across all domains (seed + personal)
@@ -862,13 +1111,86 @@ install does not load [Name]/[Role] placeholders. On an interactive terminal, a
862
1111
  missing name/role is prompted; a non-interactive (scripted/CI) init never prompts
863
1112
  or blocks — it completes and leaves the placeholders.
864
1113
 
1114
+ On an interactive terminal, init asks once whether to install the managed
1115
+ \`phronesis\` command for automatic hooks. --persist opts in for scripts;
1116
+ --no-persist suppresses the interactive persistence prompt.
1117
+
865
1118
  Options:
866
1119
  --force, -f Replace a non-empty target directory
867
1120
  --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)
1121
+ --name <name> Operator name, max 48 chars (fills USER.md + AGENTS.md §1)
1122
+ --role <role> Operator role, max 48 chars
1123
+ --company <co> Operator company, max 64 chars (USER.md only; optional)
1124
+ --working-style <s> Operator working style, max 80 chars (USER.md only; optional)
1125
+ --persist Install the managed command after workspace creation
1126
+ --no-persist Do not install or prompt to install the managed command
1127
+ `.trim(),
1128
+ );
1129
+ }
1130
+
1131
+ function showLauncherHelp() {
1132
+ console.log(
1133
+ `
1134
+ Usage: phronesis launcher install [--force-launcher] [--no-path-offer]
1135
+ phronesis launcher install --backend app --app-target <absolute cli.js>
1136
+ --app-runtime <absolute Electron> [--app-version <asserted-version>]
1137
+ [--if-owned] [--force-launcher] [--no-path-offer]
1138
+ [--expect-absent | --expect-owner <backend> --expect-version <version>]
1139
+ phronesis launcher remove [--backend app|self-copy] [--force-launcher]
1140
+ phronesis launcher path [--append] [--append-consented-by-surface] [--json]
1141
+ phronesis launcher status [--json]
1142
+
1143
+ Installs or removes the managed command at ~/.phronesis/bin/phronesis. Install
1144
+ defaults to a self-copy generation. App mode records an installed app's bundled
1145
+ CLI and Electron runtime without copying either one. Both modes commit by atomically
1146
+ renaming launcher.env. Remove is backend-owner-checked and removes the manifest
1147
+ before the dispatcher. Cross-backend replacement requires --force-launcher. The path
1148
+ command reports the exact shell-profile edit; --append prompts on a TTY and never
1149
+ writes off-TTY without the documented consenting-surface integration flag.
1150
+
1151
+ Options:
1152
+ --force-launcher Replace/remove a foreign owner, invalid or unreadable manifest, or unowned
1153
+ dispatcher; also permits a downgrade. Ownership by another uid is never overridden.
1154
+ --backend Launcher owner: app or self-copy (default: self-copy).
1155
+ --app-target Absolute bundled cli.js path (app installs only).
1156
+ --app-runtime Absolute Electron executable path (app installs only).
1157
+ --app-version Optional assertion; must match the target package.json version.
1158
+ --if-owned Re-stamp only when a valid app-owned manifest already exists; cannot be
1159
+ combined with the --expect-* disclosure-binding options.
1160
+ --expect-absent Refuse if the launcher slot changed since an absent-slot disclosure.
1161
+ --expect-owner Expected prior app|self-copy owner; requires --expect-version.
1162
+ --expect-version Expected prior version; requires --expect-owner.
1163
+ --no-path-offer Commit the launcher without the terminal PATH offer.
1164
+ --append On a TTY, disclose and confirm before appending; off-TTY, print only.
1165
+ --append-consented-by-surface
1166
+ Off-TTY only: append after the calling surface has shown the exact file
1167
+ and line and received operator confirmation, then print both as a record.
1168
+ On a TTY this still discloses and prompts. Integration surfaces only;
1169
+ scripts must not pass this flag.
1170
+ --json Print launcher path/status data as JSON.
1171
+ `.trim(),
1172
+ );
1173
+ }
1174
+
1175
+ function showProfileHelp() {
1176
+ console.log(
1177
+ `
1178
+ Usage: phronesis profile set [--name <name>] [--role <role>]
1179
+ [--company <co>] [--working-style <style>] [--domain <slug>] [--dir <install>]
1180
+
1181
+ Updates the stamped operator profile in place — the non-destructive repair verb for an
1182
+ install that still carries [Name]/[Role] placeholders (a scripted init leaves them) or a
1183
+ profile that has changed. Touches only the stamped field lines in USER.md and the always-loaded
1184
+ AGENTS.md §1 operator line; never rewrites surrounding operator prose, and never needs --force.
1185
+ If a field line cannot be located uniquely, it fails closed and points you at the file to edit.
1186
+
1187
+ Options:
1188
+ --name <name> Operator name, max 48 chars (updates USER.md + AGENTS.md §1)
1189
+ --role <role> Operator role, max 48 chars
1190
+ --company <co> Operator company, max 64 chars (USER.md only)
1191
+ --working-style <s> Operator working style, max 80 chars (USER.md only)
1192
+ --domain <slug> Domain whose profile to update (default: the sole registered domain)
1193
+ --dir <install> Installation root (default: walk up to .phronesis/registry.json)
872
1194
  `.trim(),
873
1195
  );
874
1196
  }
@@ -965,12 +1287,17 @@ function showActHelp() {
965
1287
  console.log(
966
1288
  `
967
1289
  Usage: phronesis act <type> --json '<payload>' --session <id> [--domain <slug>] [--actor <a>] [--surface <s>] [--invocation-id <id>] [--dir <install>]
1290
+ phronesis act <type> --schema [--dir <install>]
968
1291
 
969
1292
  The action-type execution floor (specs/ontology.md §"Action type contract") — plumbing,
970
1293
  not porcelain: skills compose it; it is not a promoted operator verb. One shared core
971
1294
  runs every action: schema validation, permission gate, action_type.requested/committed
972
1295
  events, and a staged atomic write that must pass the ontology contract before it lands.
973
1296
 
1297
+ Use --schema to PRINT a type's effective payload schema (fields, kinds, required markers,
1298
+ and enum values) instead of running it — a read-only introspection of the same registry
1299
+ table --json is validated against. --schema and --json are mutually exclusive.
1300
+
974
1301
  Registered types: log_decision, enrich_person, update_project_state,
975
1302
  track_commitment, record_outcome, promote_to_codex (handlers: core-0009/0010),
976
1303
  compile (the agentic quality-gate writer for insight + organization, core-0017 — review
@@ -983,13 +1310,15 @@ table (operator overrides in .phronesis/registry.json action_types) is shown by
983
1310
  validated. Execution context never goes here.
984
1311
  --session <id> Session the action belongs to (required).
985
1312
  --domain <slug> Domain workspace (default: the domains/<slug>/ the cwd is inside,
986
- else "pm"). Must be registered in the installation.
1313
+ else the sole registered domain; several registered pass one).
987
1314
  --actor <a> Defaults to $PHRONESIS_ACTOR, else 'agent'.
988
1315
  --surface <s> Defaults to 'cli'.
989
1316
  --invocation-id <i> Attribution provenance for a surrounding skill invocation. It is
990
1317
  caller-controlled (not an authority); adapters and the eval harness
991
1318
  may supply it. PHRONESIS_INVOCATION_ID is the equivalent env input.
992
1319
  --dir <install> Installation root (default: walk up to .phronesis/registry.json).
1320
+ --schema Print <type>'s effective payload schema and exit (read-only; no
1321
+ write, no event). Mutually exclusive with --json.
993
1322
 
994
1323
  Approval-gated actions refuse without an approval signal: the env var
995
1324
  PHRONESIS_APPROVAL_SOURCE (e.g. operator:interactive), set by an approving adapter and
@@ -1039,7 +1368,7 @@ function showCompileHelp() {
1039
1368
  Usage:
1040
1369
  phronesis compile accept <draft_path> --session <id> [--dir <install>] [--surface <s>]
1041
1370
  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]
1371
+ phronesis compile review --session <id> [--domain <slug>] [--approve-all --rationale '<why>'] [--approve-only '<p1,p2>'] [--resume] [--json]
1043
1372
 
1044
1373
  The operator review primitives over a compile candidate (specs/security.md draft
1045
1374
  lifecycle; core-0017 H9). An untrusted compile stages a draft under .phronesis/drafts/ and
@@ -1069,8 +1398,11 @@ review: The batch-approval UX over the same per-draft accept move (T7.5). Lists
1069
1398
  <draft_path> (accept/reject) The .phronesis/drafts/{domain}/{type-dir}/{name}.md candidate.
1070
1399
  --session <id> Review session the events belong to (required).
1071
1400
  --reason '<why>' (reject) recorded on compile.rejected.
1072
- --domain <slug> (review) Domain whose drafts to review (default: pm).
1401
+ --domain <slug> (review) Domain whose drafts to review (default: the sole registered domain; several registered → pass one).
1073
1402
  --approve-all (review) Promote the provably-clean set as one batch.
1403
+ --approve-only '<p1,p2>' (review) Restrict --approve-all to a caller-reviewed SUBSET (drafts the
1404
+ operator confirmed); an eligible-but-unreviewed draft is not promoted. Absent →
1405
+ all eligible. A subset filter only — digest/suspicious eligibility is unchanged.
1074
1406
  --rationale '<why>'(review) Required with --approve-all; recorded on every batch compile.accepted.
1075
1407
  --resume (review) Reconcile a crashed batch from the durable journal.
1076
1408
  --json (review) Emit the plan / result as JSON.
@@ -1164,6 +1496,8 @@ a repo root (skills/ + changes/ present), never an install.
1164
1496
  --rebaseline Reset the comparison basis after evaluation-identity drift: re-seed
1165
1497
  the baseline under the new identity (absolute floor on both axes,
1166
1498
  no regression check) and write a rebaseline record
1499
+ --verify-current Certify the already-deployed --to pin and seed/refresh PINS.md
1500
+ without changing the skill version or prompt_pin
1167
1501
  --coverage-floor <f> Min per-run warranted-capture hit-rate (default: 1.0)
1168
1502
  --quality-floor <f> Min per-criterion score across k runs (default: 4)
1169
1503
  --rationale <text> Recorded on an override / rebaseline record
@@ -1220,6 +1554,8 @@ function showHooksHelp() {
1220
1554
  console.log(
1221
1555
  `
1222
1556
  Usage:
1557
+ phronesis hooks refresh [--dir <install>] [--apply <path> ... | --apply-all]
1558
+ [--persist | --no-persist]
1223
1559
  phronesis hooks due-check --session <id> [--actor <a>] [--surface <s>] [--dir <install>]
1224
1560
  phronesis hooks sweep-status --session <id> [--json] [--dir <install>]
1225
1561
  phronesis hooks skill-start --payload '<hook-json>' [--dir <install>]
@@ -1227,6 +1563,20 @@ Usage:
1227
1563
  phronesis hooks skill-abandon --payload '<hook-json>' [--json] [--dir <install>]
1228
1564
  phronesis hooks skill-sweep-stale --session <id> [--json] [--dir <install>]
1229
1565
 
1566
+ refresh: Reports every init-derived hook-manifest path as current / differs /
1567
+ missing, plus unknown siblings as extra-unknown (never touched). The
1568
+ default is report-only off-TTY. Writes require per-path --apply or
1569
+ --apply-all; blanket approval covers only safely attributable stamped
1570
+ updates. Unstamped or operator-modified paths require targeted per-path
1571
+ approval. Restamps re-check reviewed bytes at the final atomic/no-follow
1572
+ write boundary and update only .phronesis/hook-stamp.json, never
1573
+ the broader upgrade baseline. .claude/settings.json receives a bounded
1574
+ Phronesis-hook-entry merge that preserves operator settings and hooks.
1575
+ If the launcher is absent or self-copy-owned but stale, --persist
1576
+ installs/repairs it off-TTY; --no-persist suppresses the TTY offer.
1577
+ App-owned or ownership-unreadable launchers are reported and never
1578
+ force-replaced by this hook migration.
1579
+
1230
1580
  Session-boundary hook support (specs/hooks.md §"Emission discipline"; change 0010 D4).
1231
1581
  The scaffolded Claude Code hooks (.phronesis/hooks/session-start.sh, session-sweep.sh,
1232
1582
  skill-lifecycle.sh) shell out to these; any Tier-1 surface adapter can do the same.
@@ -1254,18 +1604,50 @@ skill-start / skill-complete / skill-abandon / skill-sweep-stale:
1254
1604
  );
1255
1605
  }
1256
1606
 
1607
+ function showHookHelp() {
1608
+ console.log(
1609
+ `
1610
+ Usage: phronesis hook <event> --surface <surface> [--mode <start|abandon>]
1611
+
1612
+ The resolved executable interface used by emitted surface hooks (change 0065). It reads
1613
+ the surface event JSON from stdin, runs the deterministic hook logic in this CLI process,
1614
+ and emits the surface's required stdout shape. Hook scripts resolve one managed-first
1615
+ executable and contain no JSON parser or nested runtime invocation.
1616
+
1617
+ Events:
1618
+ recall-guard
1619
+ session-start
1620
+ session-sweep
1621
+ session-sweep-subagent
1622
+ session-sweep-precompact
1623
+ skill-lifecycle (--mode start|abandon)
1624
+ compile-active-context
1625
+
1626
+ Options:
1627
+ --surface <surface> Surface attribution/output contract (claude-code or codex)
1628
+ --mode <mode> skill-lifecycle variant: start or abandon
1629
+ `.trim(),
1630
+ );
1631
+ }
1632
+
1257
1633
  function showAdapterHelp() {
1258
1634
  console.log(
1259
1635
  `
1260
1636
  Usage: phronesis adapter test <surface> [--dir <install>] [--domain <slug>] [--json]
1261
1637
 
1262
1638
  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.
1639
+ specs/event-log.md §"Adapter test"). V1 ships the Codex and Craft Phronesis Desktop adapter tests.
1640
+
1641
+ adapter test desktop (change 0055) — auto-asserts the first-party in-process reinforcement +
1642
+ lifecycle CONTRACT via @phronesis/core against a throwaway synthetic install: three guard fixtures
1643
+ (seeded-state injection / no-cue / honest-negative), due-check surfacing, sweep block-once, and no
1644
+ fabricated skill.invoked/completed on the error path. The live app UI half stays operator-verified.
1264
1645
 
1265
1646
  adapter test codex — asserts the deterministic mechanics of the project-local Codex
1266
1647
  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"}.
1648
+ event to its bare-path launcher, the launchers resolve the install root and bind stdin
1649
+ directly to phronesis hook <event> --surface codex, and an unswept Stop yields
1650
+ {"decision":"block"}.
1269
1651
  It reports the compliance LEVEL — Level 1 (reinforcement: recall / due-check / sweep) and
1270
1652
  Level 2 (skill-lifecycle), which is PARTIAL on Codex (no UserPromptExpansion/StopFailure;
1271
1653
  no fabricated skill.invoked/completed) — and points at the operator-verified desktop-app
@@ -1274,7 +1656,10 @@ It is read-only: no canonical state is written and no events are emitted.
1274
1656
 
1275
1657
  Open Codex AT the domain workspace dir (Codex loads .codex/ only from the opened
1276
1658
  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.
1659
+ Settings → Coding → Hooks). Empirically verified 2026-07-18 on Codex
1660
+ v0.145.0-alpha.18, per-hook trust keys on the hooks.json registration, not script
1661
+ bytes: launcher/_resolve.sh edits did not invalidate trust; a changed config should
1662
+ show the review banner. This is version-scoped and may change in later Codex versions.
1278
1663
 
1279
1664
  Options:
1280
1665
  --dir <install> Installation root (default: walk up to .phronesis/registry.json)
@@ -1326,7 +1711,7 @@ Arguments:
1326
1711
  query The search text (quote it)
1327
1712
 
1328
1713
  Options:
1329
- --domain <slug> Domain scope (default: the domains/<slug>/ the cwd is inside, else "${DEFAULT_DOMAIN}")
1714
+ --domain <slug> Domain scope (default: the domains/<slug>/ the cwd is inside, else the sole registered domain)
1330
1715
  --all-domains Search every domain (default scope is one domain + codex)
1331
1716
  --include-artifacts Explicitly include durable artifacts for this search (not indexed by default)
1332
1717
  --limit N Max results (default: 10)
@@ -1347,7 +1732,7 @@ decisions, the people involved, and the raw sources they cite (per-result "sourc
1347
1732
  Semantic rerank (Stage 3) is V1.5+/BYO; this surface stays the same.
1348
1733
 
1349
1734
  Options:
1350
- --domain <slug> Domain scope (default: the domains/<slug>/ the cwd is inside, else "${DEFAULT_DOMAIN}")
1735
+ --domain <slug> Domain scope (default: the domains/<slug>/ the cwd is inside, else the sole registered domain)
1351
1736
  --all-domains Query every domain (default scope is one domain + codex)
1352
1737
  --include-artifacts Explicitly include durable artifacts for this query (not indexed by default)
1353
1738
  --limit N Max seed results (default: 10)
@@ -1372,7 +1757,7 @@ It writes nothing: no compiled edits, no raw/degraded captures, no events, no dr
1372
1757
  and no machine-index repair.
1373
1758
 
1374
1759
  Options:
1375
- --domain <slug> Domain scope (default: the active domain, else "${DEFAULT_DOMAIN}")
1760
+ --domain <slug> Domain scope (default: the active domain, else the sole registered domain)
1376
1761
  --all-domains Lint every registered domain
1377
1762
  --stale-months N Staleness threshold for forward refs and seed adoption (default: 6)
1378
1763
  --json Emit the machine-readable lint envelope
@@ -1388,15 +1773,16 @@ Usage: phronesis doctor [--dir <install>] [--json]
1388
1773
 
1389
1774
  Read-only install-health overview — the dogfood "is my install wired correctly?"
1390
1775
  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.
1776
+ install root, managed launcher/manifest integrity, registered domain(s), skill registry,
1777
+ hook wiring, Codex surface-adapter status, USER.md placeholder check, and pending-draft count.
1393
1778
 
1394
1779
  It writes nothing to the install and emits no events (the install is byte-identical
1395
1780
  after a run). It reports; it does not fix.
1396
1781
 
1397
1782
  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.
1783
+ registry). WARN checks (missing or non-compliant launcher state, incomplete hook wiring,
1784
+ a non-compliant Codex adapter, USER.md placeholders, pending drafts) are informational
1785
+ and never fail the command.
1400
1786
 
1401
1787
  Options:
1402
1788
  --dir <install> Installation root (default: walk up to .phronesis/registry.json)
@@ -1490,6 +1876,12 @@ async function run(argv) {
1490
1876
  case 'init-help':
1491
1877
  showInitHelp();
1492
1878
  break;
1879
+ case 'launcher-help':
1880
+ showLauncherHelp();
1881
+ break;
1882
+ case 'profile-help':
1883
+ showProfileHelp();
1884
+ break;
1493
1885
  case 'export-help':
1494
1886
  showExportHelp();
1495
1887
  break;
@@ -1529,6 +1921,9 @@ async function run(argv) {
1529
1921
  case 'hooks-help':
1530
1922
  showHooksHelp();
1531
1923
  break;
1924
+ case 'hook-help':
1925
+ showHookHelp();
1926
+ break;
1532
1927
  case 'adapter-help':
1533
1928
  showAdapterHelp();
1534
1929
  break;
@@ -1566,6 +1961,78 @@ async function run(argv) {
1566
1961
  role: parsed.role,
1567
1962
  company: parsed.company,
1568
1963
  workingStyle: parsed.workingStyle,
1964
+ persist: parsed.persist,
1965
+ });
1966
+ break;
1967
+ }
1968
+ case 'launcher-install': {
1969
+ const [{ runLauncherInstall }, { confirmPathAppend }] = await Promise.all([
1970
+ import('./launcher.js'),
1971
+ import('./prompts.js'),
1972
+ ]);
1973
+ try {
1974
+ await runLauncherInstall({
1975
+ forceLauncher: parsed.forceLauncher,
1976
+ backend: parsed.backend,
1977
+ appTarget: parsed.appTarget,
1978
+ appRuntime: parsed.appRuntime,
1979
+ appVersion: parsed.appVersion,
1980
+ ifOwned: parsed.ifOwned,
1981
+ offerPath: parsed.offerPath,
1982
+ expectedPrior: parsed.expectedPrior,
1983
+ promptPath: confirmPathAppend,
1984
+ });
1985
+ } catch (error) {
1986
+ console.error(`Launcher install failed: ${error.message}`);
1987
+ process.exitCode = 1;
1988
+ }
1989
+ break;
1990
+ }
1991
+ case 'launcher-remove': {
1992
+ const { runLauncherRemove } = await import('./launcher.js');
1993
+ try {
1994
+ await runLauncherRemove({ forceLauncher: parsed.forceLauncher, backend: parsed.backend });
1995
+ } catch (error) {
1996
+ console.error(`Launcher remove failed: ${error.message}`);
1997
+ process.exitCode = 1;
1998
+ }
1999
+ break;
2000
+ }
2001
+ case 'launcher-path': {
2002
+ const [{ runLauncherPath }, { confirmPathAppend }] = await Promise.all([
2003
+ import('./launcher.js'),
2004
+ import('./prompts.js'),
2005
+ ]);
2006
+ try {
2007
+ await runLauncherPath({
2008
+ append: parsed.append,
2009
+ surfaceConsented: parsed.surfaceConsented,
2010
+ json: parsed.json,
2011
+ prompt: confirmPathAppend,
2012
+ });
2013
+ } catch (error) {
2014
+ console.error(`Launcher PATH update failed: ${error.message}`);
2015
+ process.exitCode = 1;
2016
+ }
2017
+ break;
2018
+ }
2019
+ case 'launcher-status': {
2020
+ const { launcherStatus } = await import('./launcher.js');
2021
+ const status = launcherStatus();
2022
+ if (parsed.json) console.log(JSON.stringify(status));
2023
+ else if (!status.exists) console.log('Managed `phronesis` command is not installed.');
2024
+ else console.log(`Managed command owner=${status.owner ?? 'unrecognized'}, version=${status.version ?? 'unknown'}.`);
2025
+ break;
2026
+ }
2027
+ case 'profile-set': {
2028
+ const { runProfileSet } = await import('./profile.js');
2029
+ await runProfileSet({
2030
+ dir: parsed.dir,
2031
+ domain: parsed.domain,
2032
+ name: parsed.name,
2033
+ role: parsed.role,
2034
+ company: parsed.company,
2035
+ workingStyle: parsed.workingStyle,
1569
2036
  });
1570
2037
  break;
1571
2038
  }
@@ -1637,6 +2104,11 @@ async function run(argv) {
1637
2104
  });
1638
2105
  break;
1639
2106
  }
2107
+ case 'act-schema': {
2108
+ const { runActSchema } = await import('./act.js');
2109
+ await runActSchema({ type: parsed.type, dir: parsed.dir });
2110
+ break;
2111
+ }
1640
2112
  case 'ingest': {
1641
2113
  const { runIngest } = await import('./ingest.js');
1642
2114
  await runIngest({
@@ -1662,6 +2134,7 @@ async function run(argv) {
1662
2134
  session: parsed.session,
1663
2135
  surface: parsed.surface,
1664
2136
  dir: parsed.dir,
2137
+ reviewedDigest: parsed.reviewedDigest,
1665
2138
  });
1666
2139
  break;
1667
2140
  }
@@ -1685,6 +2158,7 @@ async function run(argv) {
1685
2158
  dir: parsed.dir,
1686
2159
  rationale: parsed.rationale,
1687
2160
  approveAll: parsed.approveAll,
2161
+ approveOnly: parsed.approveOnly,
1688
2162
  resume: parsed.resume,
1689
2163
  json: parsed.json,
1690
2164
  });
@@ -1727,6 +2201,7 @@ async function run(argv) {
1727
2201
  k: parsed.k,
1728
2202
  acceptRegression: parsed.acceptRegression,
1729
2203
  rebaseline: parsed.rebaseline,
2204
+ verifyCurrent: parsed.verifyCurrent,
1730
2205
  shapeChange: parsed.shapeChange,
1731
2206
  qualityFloor: parsed.qualityFloor,
1732
2207
  coverageFloor: parsed.coverageFloor,
@@ -1749,6 +2224,21 @@ async function run(argv) {
1749
2224
  });
1750
2225
  break;
1751
2226
  }
2227
+ case 'hooks-refresh': {
2228
+ const { runHooksRefresh } = await import('./hooks-refresh.js');
2229
+ try {
2230
+ await runHooksRefresh({
2231
+ dir: parsed.dir,
2232
+ apply: parsed.apply,
2233
+ applyAll: parsed.applyAll,
2234
+ persist: parsed.persist,
2235
+ });
2236
+ } catch (error) {
2237
+ console.error(`hooks refresh failed: ${error.message}`);
2238
+ process.exitCode = 1;
2239
+ }
2240
+ break;
2241
+ }
1752
2242
  case 'hooks-sweep-status': {
1753
2243
  const { runHooksSweepStatus } = await import('./hooks.js');
1754
2244
  await runHooksSweepStatus({ session: parsed.session, dir: parsed.dir, json: parsed.json });
@@ -1780,6 +2270,11 @@ async function run(argv) {
1780
2270
  await runHooksSkillSweepStale({ session: parsed.session, dir: parsed.dir, json: parsed.json });
1781
2271
  break;
1782
2272
  }
2273
+ case 'hook': {
2274
+ const { runHook } = await import('./hook.js');
2275
+ await runHook({ event: parsed.event, surface: parsed.surface, mode: parsed.mode });
2276
+ break;
2277
+ }
1783
2278
  case 'adapter-test': {
1784
2279
  const { runAdapterTest } = await import('./adapter.js');
1785
2280
  await runAdapterTest({
@@ -1880,7 +2375,7 @@ async function run(argv) {
1880
2375
  }
1881
2376
  }
1882
2377
 
1883
- export { parseArgs, run, showHelp, showInitHelp };
2378
+ export { parseArgs, run, showHelp, showInitHelp, showLauncherHelp, showHooksHelp };
1884
2379
 
1885
2380
  if (process.argv[1]) {
1886
2381
  try {