@pellux/goodvibes-agent 0.1.9 → 0.1.11

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 (97) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/docs/getting-started.md +1 -1
  4. package/docs/release-and-publishing.md +2 -2
  5. package/package.json +4 -1
  6. package/src/cli/agent-knowledge-command.ts +46 -20
  7. package/src/cli/help.ts +15 -2
  8. package/src/cli/management-commands.ts +3 -3
  9. package/src/cli/management.ts +7 -1
  10. package/src/cli/parser.ts +3 -0
  11. package/src/cli/service-posture.ts +6 -6
  12. package/src/cli/status.ts +9 -9
  13. package/src/cli/surface-command.ts +3 -3
  14. package/src/cli/types.ts +2 -0
  15. package/src/input/commands/cloudflare-runtime.ts +20 -5
  16. package/src/input/commands/confirmation.ts +24 -0
  17. package/src/input/commands/discovery-runtime.ts +16 -7
  18. package/src/input/commands/eval.ts +27 -14
  19. package/src/input/commands/experience-runtime.ts +66 -27
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/hooks-runtime.ts +79 -20
  22. package/src/input/commands/incident-runtime.ts +17 -6
  23. package/src/input/commands/integration-runtime.ts +93 -50
  24. package/src/input/commands/knowledge.ts +38 -12
  25. package/src/input/commands/local-auth-runtime.ts +36 -13
  26. package/src/input/commands/local-provider-runtime.ts +22 -11
  27. package/src/input/commands/local-runtime.ts +21 -11
  28. package/src/input/commands/local-setup.ts +35 -16
  29. package/src/input/commands/managed-runtime.ts +51 -20
  30. package/src/input/commands/marketplace-runtime.ts +31 -16
  31. package/src/input/commands/mcp-runtime.ts +65 -34
  32. package/src/input/commands/memory-product-runtime.ts +72 -35
  33. package/src/input/commands/memory.ts +9 -9
  34. package/src/input/commands/notify-runtime.ts +27 -8
  35. package/src/input/commands/operator-runtime.ts +85 -17
  36. package/src/input/commands/planning-runtime.ts +14 -2
  37. package/src/input/commands/platform-access-runtime.ts +88 -45
  38. package/src/input/commands/platform-services-runtime.ts +51 -25
  39. package/src/input/commands/product-runtime.ts +54 -27
  40. package/src/input/commands/profile-sync-runtime.ts +17 -6
  41. package/src/input/commands/recall-bundle.ts +38 -17
  42. package/src/input/commands/recall-query.ts +15 -4
  43. package/src/input/commands/recall-review.ts +9 -3
  44. package/src/input/commands/remote-runtime-setup.ts +45 -18
  45. package/src/input/commands/remote-runtime.ts +25 -9
  46. package/src/input/commands/replay-runtime.ts +9 -2
  47. package/src/input/commands/services-runtime.ts +21 -10
  48. package/src/input/commands/session-content.ts +53 -51
  49. package/src/input/commands/session-workflow.ts +10 -4
  50. package/src/input/commands/session.ts +1 -1
  51. package/src/input/commands/settings-sync-runtime.ts +40 -17
  52. package/src/input/commands/share-runtime.ts +12 -4
  53. package/src/input/commands/shell-core.ts +3 -3
  54. package/src/input/commands/subscription-runtime.ts +35 -20
  55. package/src/input/commands/teleport-runtime.ts +16 -5
  56. package/src/input/commands/work-plan-runtime.ts +23 -12
  57. package/src/input/handler-content-actions.ts +11 -62
  58. package/src/input/handler-interactions.ts +1 -1
  59. package/src/input/handler-onboarding-cloudflare.ts +48 -117
  60. package/src/input/handler.ts +1 -0
  61. package/src/input/keybindings.ts +1 -1
  62. package/src/input/mcp-workspace.ts +25 -49
  63. package/src/input/onboarding/onboarding-runtime-status.ts +8 -8
  64. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -53
  65. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +12 -12
  66. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +2 -7
  67. package/src/input/onboarding/onboarding-wizard-constants.ts +7 -7
  68. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +4 -4
  69. package/src/input/onboarding/onboarding-wizard-steps.ts +13 -13
  70. package/src/input/profile-picker-modal.ts +13 -31
  71. package/src/input/session-picker-modal.ts +4 -30
  72. package/src/input/settings-modal-agent-policy.ts +18 -0
  73. package/src/input/settings-modal-subscriptions.ts +3 -3
  74. package/src/input/settings-modal-types.ts +17 -0
  75. package/src/input/settings-modal.ts +30 -29
  76. package/src/main.ts +3 -26
  77. package/src/panels/incident-review-panel.ts +1 -1
  78. package/src/panels/local-auth-panel.ts +4 -4
  79. package/src/panels/provider-account-snapshot.ts +1 -1
  80. package/src/panels/provider-health-domains.ts +2 -2
  81. package/src/panels/settings-sync-panel.ts +2 -2
  82. package/src/panels/subscription-panel.ts +7 -7
  83. package/src/renderer/block-actions.ts +1 -1
  84. package/src/renderer/help-overlay.ts +2 -2
  85. package/src/renderer/mcp-workspace.ts +12 -12
  86. package/src/renderer/process-modal.ts +17 -8
  87. package/src/renderer/profile-picker-modal.ts +3 -11
  88. package/src/renderer/session-picker-modal.ts +2 -10
  89. package/src/renderer/settings-modal.ts +12 -8
  90. package/src/renderer/ui-factory.ts +4 -32
  91. package/src/runtime/bootstrap-shell.ts +0 -13
  92. package/src/runtime/bootstrap.ts +0 -10
  93. package/src/runtime/onboarding/derivation.ts +6 -6
  94. package/src/verification/live-verifier.ts +148 -13
  95. package/src/version.ts +10 -3
  96. package/src/input/commands/quit-shared.ts +0 -162
  97. package/src/renderer/git-status.ts +0 -89
@@ -4,9 +4,9 @@
4
4
  * Implements the Evaluation Harness commands:
5
5
  *
6
6
  * /eval list — List all available eval suites
7
- * /eval run <suite> — Run a named suite (or 'all')
7
+ * /eval run <suite> --yes — Run a named suite (or 'all')
8
8
  * /eval compare <baseline-file> — Compare last run against a baseline file
9
- * /eval gate <suite> — Run suite and apply CI gate (exits 1 on regression)
9
+ * /eval gate <suite> --yes — Run suite and apply CI gate (exits 1 on regression)
10
10
  */
11
11
 
12
12
  import type { SlashCommand, CommandContext } from '../command-registry.ts';
@@ -18,6 +18,7 @@ import type { EvalRegistry } from '../../panels/eval-panel.ts';
18
18
  import { formatSuiteResult, formatGateResult } from '@/runtime/index.ts';
19
19
  import { requireShellPaths } from './runtime-services.ts';
20
20
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
21
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
21
22
 
22
23
  // ── Subcommand helpers ────────────────────────────────────────────────────────
23
24
 
@@ -29,7 +30,7 @@ function printSuiteList(context: CommandContext): void {
29
30
  context.print(` - ${s.id}: ${s.name}`);
30
31
  }
31
32
  }
32
- context.print('[eval] Usage: /eval run <suite> or /eval run all');
33
+ context.print('[eval] Usage: /eval run <suite> --yes or /eval run all --yes');
33
34
  }
34
35
 
35
36
  function getRegistry(context: CommandContext): EvalRegistry | undefined {
@@ -45,7 +46,8 @@ function handleList(_args: string[], context: CommandContext): void {
45
46
  // ── /eval run ────────────────────────────────────────────────────────────────
46
47
 
47
48
  async function handleRun(args: string[], context: CommandContext): Promise<void> {
48
- const suiteName = args[0] ?? 'all';
49
+ const { rest, yes } = stripYesFlag(args);
50
+ const suiteName = rest[0] ?? 'all';
49
51
  const registry = getRegistry(context);
50
52
 
51
53
  const suitesToRun =
@@ -59,6 +61,10 @@ async function handleRun(args: string[], context: CommandContext): Promise<void>
59
61
  context.print(`[eval] Unknown suite: "${suiteName}". Run /eval list to see available suites.`);
60
62
  return;
61
63
  }
64
+ if (!yes) {
65
+ requireYesFlag(context, `run eval suite ${suiteName}`, '/eval run <suite|all> --yes');
66
+ return;
67
+ }
62
68
 
63
69
  const runner = new EvalRunner();
64
70
  registry?.setRunning(true);
@@ -90,14 +96,14 @@ async function handleCompare(args: string[], context: CommandContext): Promise<v
90
96
  const suiteResults = registry?.getSuiteResults() ?? [];
91
97
 
92
98
  if (suiteResults.length === 0) {
93
- context.print('[eval] No suite results to compare. Run /eval run <suite> first.');
99
+ context.print('[eval] No suite results to compare. Run /eval run <suite> --yes first.');
94
100
  return;
95
101
  }
96
102
 
97
103
  const baseline = await loadBaseline(baselineFile, projectRoot);
98
104
  if (!baseline) {
99
105
  context.print(`[eval] Baseline file not found: ${baselineFile}`);
100
- context.print('[eval] Tip: run /eval gate <suite> to create a baseline.');
106
+ context.print('[eval] Tip: run /eval gate <suite> [baseline-file] --save-baseline --yes to create a baseline.');
101
107
  return;
102
108
  }
103
109
 
@@ -109,13 +115,15 @@ async function handleCompare(args: string[], context: CommandContext): Promise<v
109
115
  // ── /eval gate ────────────────────────────────────────────────────────────────
110
116
 
111
117
  async function handleGate(args: string[], context: CommandContext): Promise<void> {
112
- const suiteName = args[0];
113
- const baselineFile = args[1] ?? '.goodvibes/eval/baseline.json';
114
- const saveFlag = args.includes('--save-baseline');
118
+ const { rest, yes } = stripYesFlag(args);
119
+ const positional = rest.filter((arg) => arg !== '--save-baseline');
120
+ const suiteName = positional[0];
121
+ const baselineFile = positional[1] ?? '.goodvibes/eval/baseline.json';
122
+ const saveFlag = rest.includes('--save-baseline');
115
123
  const projectRoot = requireShellPaths(context).workingDirectory;
116
124
 
117
125
  if (!suiteName) {
118
- context.print('[eval] Usage: /eval gate <suite> [baseline-file] [--save-baseline]');
126
+ context.print('[eval] Usage: /eval gate <suite> [baseline-file] [--save-baseline] --yes');
119
127
  return;
120
128
  }
121
129
 
@@ -124,6 +132,10 @@ async function handleGate(args: string[], context: CommandContext): Promise<void
124
132
  context.print(`[eval] Unknown suite: "${suiteName}". Run /eval list to see available suites.`);
125
133
  return;
126
134
  }
135
+ if (!yes) {
136
+ requireYesFlag(context, `run eval gate ${suiteName}`, '/eval gate <suite> [baseline-file] [--save-baseline] --yes');
137
+ return;
138
+ }
127
139
 
128
140
  const registry = getRegistry(context);
129
141
  const runner = new EvalRunner();
@@ -141,7 +153,7 @@ async function handleGate(args: string[], context: CommandContext): Promise<void
141
153
  context.print(formatGateResult(gate));
142
154
 
143
155
  if (saveFlag || !baseline) {
144
- const label = args[0] ?? 'latest';
156
+ const label = suiteName ?? 'latest';
145
157
  const newBaseline = captureBaseline(label, [fresh]);
146
158
  try {
147
159
  await writeBaseline(baselineFile, newBaseline, projectRoot);
@@ -164,7 +176,7 @@ export const evalCommand: SlashCommand = {
164
176
  name: 'eval',
165
177
  description: 'Evaluation harness: run benchmark suites, compare baselines, and gate regressions.',
166
178
  usage: '<subcommand> [args]',
167
- argsHint: 'list|run <suite>|compare <baseline>|gate <suite>',
179
+ argsHint: 'list|run <suite> --yes|compare <baseline>|gate <suite> --yes',
168
180
  handler: async (args: string[], context: CommandContext): Promise<void> => {
169
181
  const [sub, ...rest] = args;
170
182
 
@@ -191,9 +203,10 @@ export const evalCommand: SlashCommand = {
191
203
  const usage = [
192
204
  'Usage: /eval <subcommand>',
193
205
  ' list — List all available eval suites',
194
- ' run <suite|all> — Run a named suite (or all suites)',
206
+ ' run <suite|all> --yes — Run a named suite (or all suites)',
195
207
  ' compare [baseline-file] — Compare last results against baseline',
196
- ' gate <suite> [baseline-file] Run suite and apply regression gate',
208
+ ' gate <suite> [baseline-file] --yes',
209
+ ' — Run suite and apply regression gate',
197
210
  ' --save-baseline — Save fresh run as new baseline',
198
211
  ].join('\n');
199
212
  context.print(usage);
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import type { CommandRegistry } from '../command-registry.ts';
4
4
  import { requirePanelManager, requireShellPaths } from './runtime-services.ts';
5
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
5
6
 
6
7
  interface VoiceBundle {
7
8
  readonly version: 1;
@@ -23,21 +24,27 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
23
24
  registry.register({
24
25
  name: 'remote-setup',
25
26
  description: 'Dedicated front-door for remote setup review and portable setup bundles',
26
- usage: '[review|export <path>]',
27
+ usage: '[review|export <path> --yes]',
27
28
  async handler(args, ctx) {
28
- const sub = (args[0] ?? 'review').toLowerCase();
29
+ const parsed = stripYesFlag(args);
30
+ const commandArgs = [...parsed.rest];
31
+ const sub = (commandArgs[0] ?? 'review').toLowerCase();
29
32
  if (ctx.executeCommand) {
30
33
  if (sub === 'review') {
31
34
  await ctx.executeCommand('remote', ['setup']);
32
35
  return;
33
36
  }
34
37
  if (sub === 'export') {
35
- const pathArg = args[1];
38
+ const pathArg = commandArgs[1];
36
39
  if (!pathArg) {
37
- ctx.print('Usage: /remote-setup export <path>');
40
+ ctx.print('Usage: /remote-setup export <path> --yes');
38
41
  return;
39
42
  }
40
- await ctx.executeCommand('remote', ['setup', 'export', pathArg]);
43
+ if (!parsed.yes) {
44
+ requireYesFlag(ctx, `export remote setup bundle to ${pathArg}`, '/remote-setup export <path> --yes');
45
+ return;
46
+ }
47
+ await ctx.executeCommand('remote', ['setup', 'export', pathArg, '--yes']);
41
48
  return;
42
49
  }
43
50
  }
@@ -48,21 +55,27 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
48
55
  registry.register({
49
56
  name: 'remote-env',
50
57
  description: 'Dedicated front-door for remote environment snippets and portable env exports',
51
- usage: '[review|export <path>]',
58
+ usage: '[review|export <path> --yes]',
52
59
  async handler(args, ctx) {
53
- const sub = (args[0] ?? 'review').toLowerCase();
60
+ const parsed = stripYesFlag(args);
61
+ const commandArgs = [...parsed.rest];
62
+ const sub = (commandArgs[0] ?? 'review').toLowerCase();
54
63
  if (ctx.executeCommand) {
55
64
  if (sub === 'review') {
56
65
  await ctx.executeCommand('remote', ['env']);
57
66
  return;
58
67
  }
59
68
  if (sub === 'export') {
60
- const pathArg = args[1];
69
+ const pathArg = commandArgs[1];
61
70
  if (!pathArg) {
62
- ctx.print('Usage: /remote-env export <path>');
71
+ ctx.print('Usage: /remote-env export <path> --yes');
72
+ return;
73
+ }
74
+ if (!parsed.yes) {
75
+ requireYesFlag(ctx, `export remote environment snippet to ${pathArg}`, '/remote-env export <path> --yes');
63
76
  return;
64
77
  }
65
- await ctx.executeCommand('remote', ['env', 'export', pathArg]);
78
+ await ctx.executeCommand('remote', ['env', 'export', pathArg, '--yes']);
66
79
  return;
67
80
  }
68
81
  }
@@ -73,21 +86,27 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
73
86
  registry.register({
74
87
  name: 'tunnel',
75
88
  description: 'Dedicated front-door for remote tunnel review and export flows',
76
- usage: '[review|export <path>]',
89
+ usage: '[review|export <path> --yes]',
77
90
  async handler(args, ctx) {
78
- const sub = (args[0] ?? 'review').toLowerCase();
91
+ const parsed = stripYesFlag(args);
92
+ const commandArgs = [...parsed.rest];
93
+ const sub = (commandArgs[0] ?? 'review').toLowerCase();
79
94
  if (ctx.executeCommand) {
80
95
  if (sub === 'review') {
81
96
  await ctx.executeCommand('remote', ['tunnel', 'review']);
82
97
  return;
83
98
  }
84
99
  if (sub === 'export') {
85
- const pathArg = args[1];
100
+ const pathArg = commandArgs[1];
86
101
  if (!pathArg) {
87
- ctx.print('Usage: /tunnel export <path>');
102
+ ctx.print('Usage: /tunnel export <path> --yes');
88
103
  return;
89
104
  }
90
- await ctx.executeCommand('remote', ['tunnel', 'export', pathArg]);
105
+ if (!parsed.yes) {
106
+ requireYesFlag(ctx, `export remote tunnel review to ${pathArg}`, '/tunnel export <path> --yes');
107
+ return;
108
+ }
109
+ await ctx.executeCommand('remote', ['tunnel', 'export', pathArg, '--yes']);
91
110
  return;
92
111
  }
93
112
  }
@@ -98,19 +117,29 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
98
117
  registry.register({
99
118
  name: 'bootstrap',
100
119
  description: 'Dedicated front-door for remote bootstrap bundle export and inspection',
101
- usage: '[export <path>|inspect <path>]',
120
+ usage: '[export <path> --yes|inspect <path>]',
102
121
  async handler(args, ctx) {
103
- const sub = (args[0] ?? '').toLowerCase();
104
- const pathArg = args[1];
122
+ const parsed = stripYesFlag(args);
123
+ const commandArgs = [...parsed.rest];
124
+ const sub = (commandArgs[0] ?? '').toLowerCase();
125
+ const pathArg = commandArgs[1];
105
126
  if (!ctx.executeCommand) {
106
127
  ctx.print('Bootstrap controls are not available in this runtime.');
107
128
  return;
108
129
  }
109
- if ((sub === 'export' || sub === 'inspect') && pathArg) {
130
+ if (sub === 'export' && pathArg) {
131
+ if (!parsed.yes) {
132
+ requireYesFlag(ctx, `export remote bootstrap bundle to ${pathArg}`, '/bootstrap export <path> --yes');
133
+ return;
134
+ }
135
+ await ctx.executeCommand('remote', ['bootstrap', sub, pathArg, '--yes']);
136
+ return;
137
+ }
138
+ if (sub === 'inspect' && pathArg) {
110
139
  await ctx.executeCommand('remote', ['bootstrap', sub, pathArg]);
111
140
  return;
112
141
  }
113
- ctx.print('Usage: /bootstrap [export <path>|inspect <path>]');
142
+ ctx.print('Usage: /bootstrap [export <path> --yes|inspect <path>]');
114
143
  },
115
144
  });
116
145
 
@@ -165,7 +194,7 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
165
194
  ['shell', 'Shell execution approval with side-effect and credential review.'],
166
195
  ['file', 'File mutation approval with config/notebook differentiation.'],
167
196
  ['network', 'Network access approval with host/scope review.'],
168
- ['delegate', 'Agent spawn/delegation approval with recursion ceilings.'],
197
+ ['delegate', 'Explicit GoodVibes TUI build delegation approval; local Agent spawn is blocked.'],
169
198
  ['mcp', 'MCP trust escalation approval with host/path review.'],
170
199
  ['remote', 'Remote dispatch approval with trust/artifact review.'],
171
200
  ['hook', 'Hook execution approval with deny/mutate authority review.'],
@@ -223,10 +252,12 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
223
252
  registry.register({
224
253
  name: 'voice',
225
254
  description: 'Review voice posture and package portable voice-surface metadata',
226
- usage: '[review|enable|disable|bundle export <path>|bundle inspect <path>]',
255
+ usage: '[review|enable --yes|disable --yes|bundle export <path> --yes|bundle inspect <path>]',
227
256
  handler(args, ctx) {
257
+ const parsed = stripYesFlag(args);
258
+ const commandArgs = [...parsed.rest];
228
259
  const shellPaths = requireShellPaths(ctx);
229
- const sub = (args[0] ?? 'review').toLowerCase();
260
+ const sub = (commandArgs[0] ?? 'review').toLowerCase();
230
261
  if (sub === 'review') {
231
262
  const enabled = Boolean(ctx.platform.configManager.get('ui.voiceEnabled') ?? false);
232
263
  ctx.print([
@@ -238,20 +269,28 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
238
269
  return;
239
270
  }
240
271
  if (sub === 'enable' || sub === 'disable') {
272
+ if (!parsed.yes) {
273
+ requireYesFlag(ctx, `${sub} voice surface`, `/voice ${sub} --yes`);
274
+ return;
275
+ }
241
276
  const next = sub === 'enable';
242
277
  ctx.platform.configManager.setDynamic('ui.voiceEnabled', next);
243
278
  ctx.print(`Voice surface ${next ? 'enabled' : 'disabled'} for this runtime.`);
244
279
  return;
245
280
  }
246
281
  if (sub === 'bundle') {
247
- const mode = args[1];
248
- const pathArg = args[2];
282
+ const mode = commandArgs[1];
283
+ const pathArg = commandArgs[2];
249
284
  if ((mode === 'export' || mode === 'inspect') && !pathArg) {
250
- ctx.print(`Usage: /voice bundle ${mode} <path>`);
285
+ ctx.print(`Usage: /voice bundle ${mode} <path>${mode === 'export' ? ' --yes' : ''}`);
251
286
  return;
252
287
  }
253
288
  const targetPath = shellPaths.resolveWorkspacePath(pathArg!);
254
289
  if (mode === 'export') {
290
+ if (!parsed.yes) {
291
+ requireYesFlag(ctx, `export voice bundle to ${pathArg}`, '/voice bundle export <path> --yes');
292
+ return;
293
+ }
255
294
  const bundle: VoiceBundle = {
256
295
  version: 1,
257
296
  exportedAt: Date.now(),
@@ -272,7 +311,7 @@ export function registerExperienceRuntimeCommands(registry: CommandRegistry): vo
272
311
  return;
273
312
  }
274
313
  }
275
- ctx.print('Usage: /voice [review|enable|disable|bundle export <path>|bundle inspect <path>]');
314
+ ctx.print('Usage: /voice [review|enable --yes|disable --yes|bundle export <path> --yes|bundle inspect <path>]');
276
315
  },
277
316
  });
278
317
  }
@@ -299,7 +299,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
299
299
  lines.push(' domain: auth');
300
300
  lines.push(...(
301
301
  auth.bootstrapCredentialPresent
302
- ? [' /auth local review', ' /auth local rotate-password admin <password>', ' /auth local clear-bootstrap-file']
302
+ ? [' /auth local review', ' /auth local rotate-password admin <password> --yes', ' /auth local clear-bootstrap-file --yes']
303
303
  : [' /auth local review']
304
304
  ));
305
305
  lines.push(' verify: /health auth');
@@ -1,34 +1,69 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import type { CommandRegistry } from '../command-registry.ts';
2
3
  import { requireHookApi } from './runtime-services.ts';
4
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
5
+
6
+ function isRecord(value: unknown): value is Record<string, unknown> {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ }
9
+
10
+ function containsAgentHookType(value: unknown): boolean {
11
+ if (Array.isArray(value)) return value.some((entry) => containsAgentHookType(entry));
12
+ if (!isRecord(value)) return false;
13
+ if (value.type === 'agent') return true;
14
+ return Object.values(value).some((entry) => containsAgentHookType(entry));
15
+ }
16
+
17
+ function fileContainsAgentHookType(path: string): boolean {
18
+ try {
19
+ return containsAgentHookType(JSON.parse(readFileSync(path, 'utf-8')) as unknown);
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
3
24
 
4
25
  export function registerHooksRuntimeCommands(registry: CommandRegistry): void {
5
26
  registry.register({
6
27
  name: 'hooks',
7
28
  aliases: [],
8
29
  description: 'Inspect, author, simulate, and reload managed hook workflows',
9
- usage: '[contracts [filter] | reload | scaffold <name> <match> <type> | chain <name> <event1,event2,...> | remove <name> | enable <name> | disable <name> | simulate <eventPath> | inspect <path> | import <path> [merge|replace] | export [path]]',
30
+ usage: '[contracts [filter] | reload --yes | scaffold <name> <match> <type> --yes | chain <name> <event1,event2,...> --yes | remove <name> --yes | enable <name> --yes | disable <name> --yes | simulate <eventPath> | inspect <path> | import <path> [merge|replace] --yes | export [path] --yes]',
10
31
  argsHint: '[subcommand]',
11
32
  async handler(args, ctx) {
33
+ const parsed = stripYesFlag(args);
34
+ const commandArgs = [...parsed.rest];
12
35
  const hookApi = requireHookApi(ctx);
13
36
  const workbench = hookApi.workbench;
14
- if (args.length === 0 && ctx.openHooksPanel) {
37
+ if (commandArgs.length === 0 && ctx.openHooksPanel) {
15
38
  ctx.openHooksPanel();
16
39
  return;
17
40
  }
18
41
 
19
- const subcommand = (args[0] ?? 'contracts').toLowerCase();
42
+ const subcommand = (commandArgs[0] ?? 'contracts').toLowerCase();
20
43
  if (subcommand === 'reload') {
44
+ if (!parsed.yes) {
45
+ requireYesFlag(ctx, 'reload managed hook workflows', '/hooks reload --yes');
46
+ return;
47
+ }
21
48
  await workbench.reload();
22
49
  ctx.print(`Reloaded managed hooks from ${workbench.getFilePath()}`);
23
50
  return;
24
51
  }
25
52
  if (subcommand === 'scaffold') {
26
- const [name, match, type] = args.slice(1);
53
+ const [name, match, type] = commandArgs.slice(1);
27
54
  if (!name || !match || !type) {
28
- ctx.print('Usage: /hooks scaffold <name> <match> <command|prompt|agent|http|ts>');
55
+ ctx.print('Usage: /hooks scaffold <name> <match> <command|prompt|http|ts> --yes');
56
+ return;
57
+ }
58
+ if (!parsed.yes) {
59
+ requireYesFlag(ctx, `scaffold managed hook ${name}`, '/hooks scaffold <name> <match> <command|prompt|http|ts> --yes');
60
+ return;
61
+ }
62
+ if (type === 'agent') {
63
+ ctx.print('Blocked: GoodVibes Agent does not author local agent-spawning hooks. Use command, prompt, http, or ts hooks, or delegate explicit build work to GoodVibes TUI.');
29
64
  return;
30
65
  }
31
- if (!['command', 'prompt', 'agent', 'http', 'ts'].includes(type)) {
66
+ if (!['command', 'prompt', 'http', 'ts'].includes(type)) {
32
67
  ctx.print(`Unknown hook type: ${type}`);
33
68
  return;
34
69
  }
@@ -37,10 +72,14 @@ export function registerHooksRuntimeCommands(registry: CommandRegistry): void {
37
72
  return;
38
73
  }
39
74
  if (subcommand === 'chain') {
40
- const name = args[1];
41
- const matches = args[2]?.split(',').map((entry) => entry.trim()).filter(Boolean) ?? [];
75
+ const name = commandArgs[1];
76
+ const matches = commandArgs[2]?.split(',').map((entry) => entry.trim()).filter(Boolean) ?? [];
42
77
  if (!name || matches.length === 0) {
43
- ctx.print('Usage: /hooks chain <name> <event1,event2,...>');
78
+ ctx.print('Usage: /hooks chain <name> <event1,event2,...> --yes');
79
+ return;
80
+ }
81
+ if (!parsed.yes) {
82
+ requireYesFlag(ctx, `scaffold managed hook chain ${name}`, '/hooks chain <name> <event1,event2,...> --yes');
44
83
  return;
45
84
  }
46
85
  const chain = await workbench.scaffoldChain(name, matches);
@@ -48,9 +87,13 @@ export function registerHooksRuntimeCommands(registry: CommandRegistry): void {
48
87
  return;
49
88
  }
50
89
  if (subcommand === 'remove') {
51
- const name = args[1];
90
+ const name = commandArgs[1];
52
91
  if (!name) {
53
- ctx.print('Usage: /hooks remove <name>');
92
+ ctx.print('Usage: /hooks remove <name> --yes');
93
+ return;
94
+ }
95
+ if (!parsed.yes) {
96
+ requireYesFlag(ctx, `remove managed hook workflow ${name}`, '/hooks remove <name> --yes');
54
97
  return;
55
98
  }
56
99
  const removed = await workbench.remove(name);
@@ -62,9 +105,13 @@ export function registerHooksRuntimeCommands(registry: CommandRegistry): void {
62
105
  return;
63
106
  }
64
107
  if (subcommand === 'enable' || subcommand === 'disable') {
65
- const name = args[1];
108
+ const name = commandArgs[1];
66
109
  if (!name) {
67
- ctx.print(`Usage: /hooks ${subcommand} <name>`);
110
+ ctx.print(`Usage: /hooks ${subcommand} <name> --yes`);
111
+ return;
112
+ }
113
+ if (!parsed.yes) {
114
+ requireYesFlag(ctx, `${subcommand} managed hook ${name}`, `/hooks ${subcommand} <name> --yes`);
68
115
  return;
69
116
  }
70
117
  const changed = await workbench.toggle(name, subcommand === 'enable');
@@ -76,7 +123,7 @@ export function registerHooksRuntimeCommands(registry: CommandRegistry): void {
76
123
  return;
77
124
  }
78
125
  if (subcommand === 'simulate') {
79
- const eventPath = args[1];
126
+ const eventPath = commandArgs[1];
80
127
  if (!eventPath) {
81
128
  ctx.print('Usage: /hooks simulate <eventPath>');
82
129
  return;
@@ -92,12 +139,16 @@ export function registerHooksRuntimeCommands(registry: CommandRegistry): void {
92
139
  return;
93
140
  }
94
141
  if (subcommand === 'export') {
95
- const path = await workbench.export(args[1] ?? workbench.getFilePath());
142
+ if (!parsed.yes) {
143
+ requireYesFlag(ctx, 'export managed hook workflows', '/hooks export [path] --yes');
144
+ return;
145
+ }
146
+ const path = await workbench.export(commandArgs[1] ?? workbench.getFilePath());
96
147
  ctx.print(`Exported managed hooks to ${path}`);
97
148
  return;
98
149
  }
99
150
  if (subcommand === 'inspect') {
100
- const path = args[1];
151
+ const path = commandArgs[1];
101
152
  if (!path) {
102
153
  ctx.print('Usage: /hooks inspect <path>');
103
154
  return;
@@ -112,10 +163,18 @@ export function registerHooksRuntimeCommands(registry: CommandRegistry): void {
112
163
  return;
113
164
  }
114
165
  if (subcommand === 'import') {
115
- const path = args[1];
116
- const strategy = args[2] === 'replace' ? 'replace' : 'merge';
166
+ const path = commandArgs[1];
167
+ const strategy = commandArgs[2] === 'replace' ? 'replace' : 'merge';
117
168
  if (!path) {
118
- ctx.print('Usage: /hooks import <path> [merge|replace]');
169
+ ctx.print('Usage: /hooks import <path> [merge|replace] --yes');
170
+ return;
171
+ }
172
+ if (!parsed.yes) {
173
+ requireYesFlag(ctx, `import managed hook workflows from ${path}`, '/hooks import <path> [merge|replace] --yes');
174
+ return;
175
+ }
176
+ if (fileContainsAgentHookType(path)) {
177
+ ctx.print('Blocked: hook bundle contains type=agent entries. GoodVibes Agent does not import local agent-spawning hooks.');
119
178
  return;
120
179
  }
121
180
  await workbench.import(path, strategy);
@@ -123,7 +182,7 @@ export function registerHooksRuntimeCommands(registry: CommandRegistry): void {
123
182
  return;
124
183
  }
125
184
 
126
- const filter = (subcommand === 'contracts' ? args.slice(1) : args).join(' ').trim().toLowerCase();
185
+ const filter = (subcommand === 'contracts' ? commandArgs.slice(1) : commandArgs).join(' ').trim().toLowerCase();
127
186
  const contracts = hookApi.contracts(filter);
128
187
 
129
188
  if (contracts.length === 0) {
@@ -4,16 +4,19 @@ import type { CommandRegistry } from '../command-registry.ts';
4
4
  import { buildIncidentMemoryAddOptions } from '@pellux/goodvibes-sdk/platform/state';
5
5
  import { requireShellPaths } from './runtime-services.ts';
6
6
  import { getMemoryApi } from './recall-query.ts';
7
+ import { requireYesFlag, stripYesFlag } from './confirmation.ts';
7
8
 
8
9
  export function registerIncidentRuntimeCommands(registry: CommandRegistry): void {
9
10
  registry.register({
10
11
  name: 'incident',
11
12
  aliases: [],
12
13
  description: 'Open, export, and capture incident review bundles',
13
- usage: '[open | latest | show <id|latest> | export <id|latest> <path> | capture <id|latest>]',
14
+ usage: '[open | latest | show <id|latest> | export <id|latest> <path> --yes | capture <id|latest> --yes]',
14
15
  async handler(args, ctx) {
16
+ const parsed = stripYesFlag(args);
17
+ const commandArgs = [...parsed.rest];
15
18
  const shellPaths = requireShellPaths(ctx);
16
- const subcommand = (args[0] ?? 'open').toLowerCase();
19
+ const subcommand = (commandArgs[0] ?? 'open').toLowerCase();
17
20
  const forensicRegistry = ctx.extensions.forensicsRegistry;
18
21
  if (subcommand === 'open') {
19
22
  if (ctx.openIncidentPanel) {
@@ -27,7 +30,7 @@ export function registerIncidentRuntimeCommands(registry: CommandRegistry): void
27
30
  ctx.print('Forensics registry is not available in this runtime.');
28
31
  return;
29
32
  }
30
- const requestedId = args[1];
33
+ const requestedId = commandArgs[1];
31
34
  const report = !requestedId || requestedId === 'latest'
32
35
  ? forensicRegistry.latest()
33
36
  : forensicRegistry.getById(requestedId);
@@ -53,9 +56,13 @@ export function registerIncidentRuntimeCommands(registry: CommandRegistry): void
53
56
  return;
54
57
  }
55
58
  if (subcommand === 'export') {
56
- const pathArg = args[2];
59
+ const pathArg = commandArgs[2];
57
60
  if (!requestedId || !pathArg) {
58
- ctx.print('Usage: /incident export <id|latest> <path>');
61
+ ctx.print('Usage: /incident export <id|latest> <path> --yes');
62
+ return;
63
+ }
64
+ if (!parsed.yes) {
65
+ requireYesFlag(ctx, `export incident bundle ${requestedId}`, '/incident export <id|latest> <path> --yes');
59
66
  return;
60
67
  }
61
68
  if (!report) {
@@ -74,6 +81,10 @@ export function registerIncidentRuntimeCommands(registry: CommandRegistry): void
74
81
  return;
75
82
  }
76
83
  if (subcommand === 'capture') {
84
+ if (!parsed.yes) {
85
+ requireYesFlag(ctx, `capture incident ${requestedId ?? 'latest'} into durable memory`, '/incident capture <id|latest> --yes');
86
+ return;
87
+ }
77
88
  const memory = getMemoryApi(ctx);
78
89
  if (!memory) return;
79
90
  if (!report) {
@@ -89,7 +100,7 @@ export function registerIncidentRuntimeCommands(registry: CommandRegistry): void
89
100
  ctx.print(`Captured incident ${report.id} into durable memory as ${record.id}`);
90
101
  return;
91
102
  }
92
- ctx.print('Usage: /incident [open | latest | show <id|latest> | export <id|latest> <path> | capture <id|latest>]');
103
+ ctx.print('Usage: /incident [open | latest | show <id|latest> | export <id|latest> <path> --yes | capture <id|latest> --yes]');
93
104
  },
94
105
  });
95
106
  }