@osovv/vv-opencode 0.12.0 → 0.14.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 (46) hide show
  1. package/README.md +38 -34
  2. package/dist/commands/agent.js +23 -87
  3. package/dist/commands/agent.js.map +1 -1
  4. package/dist/commands/config-validate.d.ts +4 -24
  5. package/dist/commands/config-validate.js +52 -242
  6. package/dist/commands/config-validate.js.map +1 -1
  7. package/dist/commands/doctor.js +10 -9
  8. package/dist/commands/doctor.js.map +1 -1
  9. package/dist/commands/guardian.js +6 -13
  10. package/dist/commands/guardian.js.map +1 -1
  11. package/dist/commands/init.js +11 -15
  12. package/dist/commands/init.js.map +1 -1
  13. package/dist/commands/install.d.ts +1 -16
  14. package/dist/commands/install.js +8 -44
  15. package/dist/commands/install.js.map +1 -1
  16. package/dist/commands/status.js +9 -9
  17. package/dist/commands/status.js.map +1 -1
  18. package/dist/commands/sync.d.ts +1 -1
  19. package/dist/commands/sync.js +8 -10
  20. package/dist/commands/sync.js.map +1 -1
  21. package/dist/lib/opencode.d.ts +19 -52
  22. package/dist/lib/opencode.js +86 -336
  23. package/dist/lib/opencode.js.map +1 -1
  24. package/dist/lib/package.d.ts +2 -0
  25. package/dist/lib/package.js +14 -10
  26. package/dist/lib/package.js.map +1 -1
  27. package/dist/lib/vvoc-config.d.ts +209 -0
  28. package/dist/lib/vvoc-config.js +642 -0
  29. package/dist/lib/vvoc-config.js.map +1 -0
  30. package/dist/lib/vvoc-paths.d.ts +2 -0
  31. package/dist/lib/vvoc-paths.js +9 -3
  32. package/dist/lib/vvoc-paths.js.map +1 -1
  33. package/dist/plugins/guardian/index.js +24 -206
  34. package/dist/plugins/guardian/index.js.map +1 -1
  35. package/dist/plugins/memory/index.js +1 -1
  36. package/dist/plugins/memory/index.js.map +1 -1
  37. package/dist/plugins/memory-store.d.ts +2 -9
  38. package/dist/plugins/memory-store.js +20 -127
  39. package/dist/plugins/memory-store.js.map +1 -1
  40. package/dist/plugins/secrets-redaction/config.d.ts +3 -20
  41. package/dist/plugins/secrets-redaction/config.js +33 -134
  42. package/dist/plugins/secrets-redaction/config.js.map +1 -1
  43. package/dist/plugins/secrets-redaction/engine.js +20 -60
  44. package/dist/plugins/secrets-redaction/engine.js.map +1 -1
  45. package/package.json +3 -1
  46. package/schemas/vvoc/v1.json +94 -0
@@ -1,9 +1,9 @@
1
1
  // FILE: src/lib/opencode.ts
2
- // VERSION: 0.5.0
2
+ // VERSION: 0.6.0
3
3
  // START_MODULE_CONTRACT
4
- // PURPOSE: Manage OpenCode config mutation, provider patching, and vvoc-owned config files.
5
- // SCOPE: Scope-aware path resolution, pinned plugin writes, provider baseURL patching, managed OpenCode agent registration/model overrides, managed agent prompt sync, Guardian/Memory config rendering and sync, and installation inspection.
6
- // DEPENDS: [jsonc-parser, node:fs/promises, node:path, src/lib/managed-agents.ts, src/lib/package.ts, src/lib/vvoc-paths.ts, src/plugins/memory-store.ts]
4
+ // PURPOSE: Manage OpenCode config mutation, provider patching, and the canonical vvoc.json config file.
5
+ // SCOPE: Scope-aware path resolution, pinned plugin writes, provider baseURL patching, managed OpenCode agent registration/model overrides, managed agent prompt sync, canonical vvoc config rendering and sync, and installation inspection.
6
+ // DEPENDS: [jsonc-parser, node:fs/promises, node:path, src/lib/managed-agents.ts, src/lib/package.ts, src/lib/vvoc-config.ts, src/lib/vvoc-paths.ts]
7
7
  // LINKS: [M-CLI-CONFIG]
8
8
  // ROLE: RUNTIME
9
9
  // MAP_MODE: EXPORTS
@@ -15,16 +15,16 @@
15
15
  // OPENCODE_SCHEMA_URL - OpenCode config schema URL.
16
16
  // Scope - Supported installation scopes for vvoc config writes.
17
17
  // ResolvedPaths - Scope-aware path bundle for OpenCode and vvoc config locations.
18
- // GuardianConfigOverrides - Guardian config override shape parsed from managed JSONC.
19
18
  // WriteResult - Result shape returned by managed config write operations.
20
19
  // InstallationInspection - Current OpenCode and vvoc installation status snapshot.
21
20
  // resolvePaths - Resolves OpenCode and vvoc config paths for global/project scopes.
22
21
  // ensurePackageConfigText - Ensures OpenCode config contains the pinned vvoc plugin specifier.
23
22
  // ensureProviderBaseUrlConfigText - Ensures OpenCode config contains the requested provider options.baseURL override.
24
23
  // ensureManagedAgentRegistrationsConfigText - Ensures OpenCode config contains the vvoc-managed OpenCode agent registrations.
25
- // parseGuardianConfigText - Parses Guardian config JSONC into typed overrides.
26
- // renderGuardianConfig - Renders managed Guardian config JSONC.
24
+ // readVvocConfig - Loads the canonical vvoc.json document when present.
27
25
  // ensurePackageInstalled - Writes the pinned vvoc plugin specifier into OpenCode config.
26
+ // installVvocConfig - Creates or refreshes the canonical vvoc.json document.
27
+ // syncVvocConfig - Rewrites the canonical vvoc.json document while preserving valid current values.
28
28
  // writeProviderBaseUrl - Writes a provider options.baseURL override into OpenCode config.
29
29
  // syncManagedAgentRegistrations - Syncs the canonical vvoc-managed OpenCode agent registrations into OpenCode config.
30
30
  // installManagedAgentPrompts - Creates managed vvoc prompt files for the bundled Guardian and managed OpenCode agents when missing.
@@ -33,38 +33,27 @@
33
33
  // writeOpenCodeAgentModel - Writes or removes a model override for any OpenCode agent in config.
34
34
  // readManagedAgentModels - Reads model overrides for the bundled vvoc-managed OpenCode agents from OpenCode config.
35
35
  // writeManagedAgentModel - Writes or removes a bundled vvoc-managed OpenCode agent model override in OpenCode config.
36
- // installGuardianConfig - Creates or preserves managed Guardian config.
37
- // syncGuardianConfig - Rewrites managed Guardian config while preserving current values.
38
- // writeGuardianConfig - Writes explicit Guardian overrides to managed config.
39
- // installMemoryConfig - Creates or preserves managed Memory config.
40
- // syncMemoryConfig - Rewrites managed Memory config while preserving current values.
36
+ // writeGuardianConfig - Writes the guardian section into the canonical vvoc.json document.
37
+ // writeMemoryConfig - Writes the memory section into the canonical vvoc.json document.
41
38
  // inspectInstallation - Reads current OpenCode/vvoc installation state for status and doctor commands.
42
39
  // describeWriteResult - Formats config write outcomes for CLI output.
43
40
  // END_MODULE_MAP
44
41
  //
45
42
  // START_CHANGE_SUMMARY
46
- // LAST_CHANGE: [v0.5.0 - Removed the legacy enhance command path and kept enhancer as a managed primary agent only.]
43
+ // LAST_CHANGE: [v0.6.0 - Replaced per-feature vvoc config files with a single canonical vvoc.json document.]
47
44
  // END_CHANGE_SUMMARY
48
45
  import { applyEdits, format, modify, parse } from "jsonc-parser";
49
46
  import { mkdir, readFile, writeFile } from "node:fs/promises";
50
47
  import { dirname, join, relative } from "node:path";
51
48
  import { MANAGED_AGENT_PROMPT_NAMES, MANAGED_OPENCODE_AGENTS, getManagedAgentPromptPath, getManagedOpenCodeAgentDefinition, loadManagedAgentPromptTemplate, } from "./managed-agents.js";
52
- import { parseMemoryConfigText, renderMemoryConfig, } from "../plugins/memory-store.js";
49
+ import { createDefaultVvocConfig, createGuardianConfig, createMemoryConfig, parseVvocConfigText, renderVvocConfig, } from "./vvoc-config.js";
53
50
  import { getPinnedPackageSpecifier, PACKAGE_NAME } from "./package.js";
54
- import { getConfigHome, getGlobalOpencodeDir, getGlobalVvocDir, getProjectVvocDir, getVvocAgentsDir, } from "./vvoc-paths.js";
51
+ import { getConfigHome, getGlobalOpencodeDir, getGlobalVvocConfigPath, getGlobalVvocDir, getProjectVvocDir, getVvocAgentsDir, } from "./vvoc-paths.js";
55
52
  export const CLI_NAME = "vvoc";
56
53
  export { PACKAGE_NAME };
57
54
  export const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
58
55
  const MANAGED_MARKER = "Managed by vvoc";
59
- const DEFAULT_GUARDIAN_TIMEOUT_MS = 90_000;
60
- const DEFAULT_GUARDIAN_APPROVAL_RISK_THRESHOLD = 80;
61
- const GUARDIAN_CONFIG_FILE_NAMES = ["guardian.jsonc", "guardian.json"];
62
- const MEMORY_CONFIG_FILE_NAMES = ["memory.jsonc", "memory.json"];
63
56
  const OPENCODE_CONFIG_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
64
- const SECRETS_REDACTION_CONFIG_FILE_NAMES = [
65
- "secrets-redaction.config.json",
66
- "secrets-redaction.config.jsonc",
67
- ];
68
57
  const JSON_FORMAT = {
69
58
  insertSpaces: true,
70
59
  tabSize: 2,
@@ -73,30 +62,21 @@ const JSON_FORMAT = {
73
62
  // START_BLOCK_RESOLVE_CONFIG_PATHS
74
63
  export async function resolvePaths(options) {
75
64
  const configHome = getConfigHome(options.configDir);
65
+ const vvocBaseDir = getGlobalVvocDir(options.configDir);
76
66
  const opencodeBaseDir = options.scope === "global" ? getGlobalOpencodeDir(options.configDir) : options.cwd;
77
- const vvocBaseDir = options.scope === "global"
78
- ? getGlobalVvocDir(options.configDir)
79
- : getProjectVvocDir(options.cwd);
80
- const managedAgentsDirPath = getVvocAgentsDir(vvocBaseDir);
67
+ const managedAgentsBaseDir = options.scope === "global" ? vvocBaseDir : getProjectVvocDir(options.cwd);
68
+ const managedAgentsDirPath = getVvocAgentsDir(managedAgentsBaseDir);
81
69
  const opencodeSelection = await selectPrimaryPath(OPENCODE_CONFIG_FILE_NAMES.map((name) => join(opencodeBaseDir, name)));
82
- const guardianSelection = await selectPrimaryPath(GUARDIAN_CONFIG_FILE_NAMES.map((name) => join(vvocBaseDir, name)));
83
- const memorySelection = await selectPrimaryPath(MEMORY_CONFIG_FILE_NAMES.map((name) => join(vvocBaseDir, name)));
84
- const secretsRedactionSelection = await selectPrimaryPath(SECRETS_REDACTION_CONFIG_FILE_NAMES.map((name) => join(vvocBaseDir, name)));
85
70
  return {
86
71
  scope: options.scope,
87
72
  cwd: options.cwd,
88
73
  configHome,
89
74
  opencodeBaseDir,
90
75
  vvocBaseDir,
76
+ vvocConfigPath: getGlobalVvocConfigPath(options.configDir),
91
77
  managedAgentsDirPath,
92
78
  opencodeConfigPath: opencodeSelection.primary,
93
79
  opencodeAlternatePaths: opencodeSelection.alternates,
94
- guardianConfigPath: guardianSelection.primary,
95
- guardianAlternatePaths: guardianSelection.alternates,
96
- memoryConfigPath: memorySelection.primary,
97
- memoryConfigAlternates: memorySelection.alternates,
98
- secretsRedactionConfigPath: secretsRedactionSelection.primary,
99
- secretsRedactionConfigAlternates: secretsRedactionSelection.alternates,
100
80
  };
101
81
  }
102
82
  // END_BLOCK_RESOLVE_CONFIG_PATHS
@@ -304,42 +284,7 @@ export async function writeManagedAgentModel(paths, agentName, options) {
304
284
  };
305
285
  }
306
286
  // END_BLOCK_ENSURE_MANAGED_SUBAGENT_CONFIG
307
- export function parseGuardianConfigText(text, label) {
308
- return normalizeGuardianOverrides(parseObjectDocument(text, label), label);
309
- }
310
- // START_BLOCK_RENDER_GUARDIAN_CONFIG
311
- export function renderGuardianConfig(overrides = {}) {
312
- const timeoutMs = overrides.timeoutMs ?? DEFAULT_GUARDIAN_TIMEOUT_MS;
313
- const approvalRiskThreshold = overrides.approvalRiskThreshold ?? DEFAULT_GUARDIAN_APPROVAL_RISK_THRESHOLD;
314
- const reviewToastDurationMs = overrides.reviewToastDurationMs ?? timeoutMs;
315
- const lines = [
316
- "// Managed by vvoc.",
317
- "// `vvoc sync` rewrites files with this marker while preserving current values.",
318
- "// Remove this header if you want to manage the file manually.",
319
- "",
320
- "{",
321
- ];
322
- if (overrides.model) {
323
- lines.push(` "model": ${JSON.stringify(overrides.model)},`);
324
- }
325
- else {
326
- lines.push(' // "model": "anthropic/claude-sonnet-4-5",');
327
- }
328
- lines.push("");
329
- if (overrides.variant) {
330
- lines.push(` "variant": ${JSON.stringify(overrides.variant)},`);
331
- }
332
- else {
333
- lines.push(' // "variant": "high",');
334
- }
335
- lines.push("");
336
- lines.push(` "timeoutMs": ${timeoutMs},`);
337
- lines.push(` "approvalRiskThreshold": ${approvalRiskThreshold},`);
338
- lines.push(` "reviewToastDurationMs": ${reviewToastDurationMs}`);
339
- lines.push("}");
340
- return `${lines.join("\n")}\n`;
341
- }
342
- // END_BLOCK_RENDER_GUARDIAN_CONFIG
287
+ export { parseGuardianConfigText, renderGuardianConfig, } from "./vvoc-config.js";
343
288
  // START_BLOCK_INSTALL_PACKAGE_AND_GUARDIAN_CONFIG
344
289
  export async function ensurePackageInstalled(paths) {
345
290
  const currentText = await readOptionalText(paths.opencodeConfigPath);
@@ -397,177 +342,47 @@ export async function writeProviderBaseUrl(paths, providerID, baseURL) {
397
342
  };
398
343
  }
399
344
  // END_BLOCK_ENSURE_PROVIDER_BASE_URL_CONFIG
400
- export async function installGuardianConfig(paths, options) {
401
- const currentText = await readOptionalText(paths.guardianConfigPath);
402
- if (!currentText) {
403
- await writeText(paths.guardianConfigPath, renderGuardianConfig());
404
- return { action: "created", path: paths.guardianConfigPath };
405
- }
406
- if (!options.force) {
407
- if (!isManagedFile(currentText)) {
408
- return {
409
- action: "skipped",
410
- path: paths.guardianConfigPath,
411
- reason: "existing file is not managed by vvoc",
412
- };
413
- }
414
- return { action: "kept", path: paths.guardianConfigPath };
415
- }
416
- return syncGuardianConfig(paths, options);
417
- }
418
- export async function syncGuardianConfig(paths, options) {
419
- const currentText = await readOptionalText(paths.guardianConfigPath);
420
- if (!currentText) {
421
- await writeText(paths.guardianConfigPath, renderGuardianConfig());
422
- return { action: "created", path: paths.guardianConfigPath };
423
- }
424
- if (!options.force && !isManagedFile(currentText)) {
425
- return {
426
- action: "skipped",
427
- path: paths.guardianConfigPath,
428
- reason: "existing file is not managed by vvoc",
429
- };
430
- }
431
- const nextText = renderGuardianConfig(parseGuardianConfigText(currentText, paths.guardianConfigPath));
432
- if (currentText === nextText) {
433
- return { action: "kept", path: paths.guardianConfigPath };
434
- }
435
- await writeText(paths.guardianConfigPath, nextText);
436
- return { action: "updated", path: paths.guardianConfigPath };
437
- }
438
- export async function writeGuardianConfig(paths, overrides, options) {
439
- const currentText = await readOptionalText(paths.guardianConfigPath);
440
- if (currentText && !options.force && !isManagedFile(currentText)) {
441
- return {
442
- action: "skipped",
443
- path: paths.guardianConfigPath,
444
- reason: "existing file is not managed by vvoc",
445
- };
446
- }
447
- const nextText = renderGuardianConfig(overrides);
448
- if (currentText === nextText) {
449
- return { action: "kept", path: paths.guardianConfigPath };
450
- }
451
- await writeText(paths.guardianConfigPath, nextText);
452
- return {
453
- action: currentText ? "updated" : "created",
454
- path: paths.guardianConfigPath,
345
+ export async function readVvocConfig(paths) {
346
+ const currentText = await readOptionalText(paths.vvocConfigPath);
347
+ return currentText ? parseVvocConfigText(currentText, paths.vvocConfigPath) : undefined;
348
+ }
349
+ export async function installVvocConfig(paths) {
350
+ return syncVvocConfig(paths);
351
+ }
352
+ export async function syncVvocConfig(paths) {
353
+ const currentText = await readOptionalText(paths.vvocConfigPath);
354
+ const nextConfig = currentText
355
+ ? parseVvocConfigText(currentText, paths.vvocConfigPath)
356
+ : createDefaultVvocConfig();
357
+ return writeResolvedVvocConfig(paths.vvocConfigPath, currentText, nextConfig);
358
+ }
359
+ export async function writeGuardianConfig(paths, overrides, options = {}) {
360
+ const currentText = await readOptionalText(paths.vvocConfigPath);
361
+ const currentConfig = currentText
362
+ ? parseVvocConfigText(currentText, paths.vvocConfigPath)
363
+ : createDefaultVvocConfig();
364
+ const nextConfig = {
365
+ ...currentConfig,
366
+ guardian: options.merge
367
+ ? createGuardianConfig({ ...currentConfig.guardian, ...overrides })
368
+ : createGuardianConfig(overrides),
455
369
  };
456
- }
457
- // END_BLOCK_INSTALL_PACKAGE_AND_GUARDIAN_CONFIG
458
- // START_BLOCK_INSTALL_MEMORY_CONFIG
459
- export async function installMemoryConfig(paths, options) {
460
- const currentText = await readOptionalText(paths.memoryConfigPath);
461
- if (!currentText) {
462
- await writeText(paths.memoryConfigPath, renderMemoryConfig());
463
- return { action: "created", path: paths.memoryConfigPath };
464
- }
465
- if (!options.force) {
466
- if (!isManagedFile(currentText)) {
467
- return {
468
- action: "skipped",
469
- path: paths.memoryConfigPath,
470
- reason: "existing file is not managed by vvoc",
471
- };
472
- }
473
- return { action: "kept", path: paths.memoryConfigPath };
474
- }
475
- return syncMemoryConfig(paths, options);
476
- }
477
- export async function syncMemoryConfig(paths, options) {
478
- const currentText = await readOptionalText(paths.memoryConfigPath);
479
- if (!currentText) {
480
- await writeText(paths.memoryConfigPath, renderMemoryConfig());
481
- return { action: "created", path: paths.memoryConfigPath };
482
- }
483
- if (!options.force && !isManagedFile(currentText)) {
484
- return {
485
- action: "skipped",
486
- path: paths.memoryConfigPath,
487
- reason: "existing file is not managed by vvoc",
488
- };
489
- }
490
- const nextText = renderMemoryConfig(parseMemoryConfigText(currentText, paths.memoryConfigPath));
491
- if (currentText === nextText) {
492
- return { action: "kept", path: paths.memoryConfigPath };
493
- }
494
- await writeText(paths.memoryConfigPath, nextText);
495
- return { action: "updated", path: paths.memoryConfigPath };
370
+ return writeResolvedVvocConfig(paths.vvocConfigPath, currentText, nextConfig);
371
+ }
372
+ export async function writeMemoryConfig(paths, overrides, options = {}) {
373
+ const currentText = await readOptionalText(paths.vvocConfigPath);
374
+ const currentConfig = currentText
375
+ ? parseVvocConfigText(currentText, paths.vvocConfigPath)
376
+ : createDefaultVvocConfig();
377
+ const nextConfig = {
378
+ ...currentConfig,
379
+ memory: options.merge
380
+ ? createMemoryConfig({ ...currentConfig.memory, ...overrides })
381
+ : createMemoryConfig(overrides),
382
+ };
383
+ return writeResolvedVvocConfig(paths.vvocConfigPath, currentText, nextConfig);
496
384
  }
497
385
  // END_BLOCK_INSTALL_MEMORY_CONFIG
498
- // START_BLOCK_INSTALL_SECRETS_REDACTION_CONFIG
499
- export function renderSecretsRedactionConfig() {
500
- const lines = [
501
- "// Managed by vvoc.",
502
- "// `vvoc sync` rewrites files with this marker while preserving current values.",
503
- "// Remove this header if you want to manage the file manually.",
504
- "",
505
- "{",
506
- ' "enabled": true,',
507
- ' "secret": "${VVOC_SECRET}",',
508
- ' "ttlMs": 3600000,',
509
- ' "maxMappings": 10000,',
510
- ' "patterns": {',
511
- ' "keywords": [],',
512
- ' "regex": [],',
513
- ' "builtin": ["email", "uuid", "ipv4", "mac", "openai_key", "anthropic_key", "github_token", "aws_access_key", "stripe_key", "bearer_token", "bearer_dot", "syn_key", "hex_token"],',
514
- ' "exclude": []',
515
- " },",
516
- ' "debug": false',
517
- "}",
518
- ];
519
- return `${lines.join("\n")}\n`;
520
- }
521
- export function parseSecretsRedactionConfigText(text, _filePath) {
522
- const errors = [];
523
- const result = parse(text, errors, { allowTrailingComma: true });
524
- if (errors.length > 0) {
525
- throw new Error(`parse error at offset ${errors[0].offset}`);
526
- }
527
- if (typeof result !== "object" || result === null) {
528
- throw new Error("root must be an object");
529
- }
530
- return result;
531
- }
532
- export async function installSecretsRedactionConfig(paths, options) {
533
- const currentText = await readOptionalText(paths.secretsRedactionConfigPath);
534
- if (!currentText) {
535
- await writeText(paths.secretsRedactionConfigPath, renderSecretsRedactionConfig());
536
- return { action: "created", path: paths.secretsRedactionConfigPath };
537
- }
538
- if (!options.force) {
539
- if (!isManagedFile(currentText)) {
540
- return {
541
- action: "skipped",
542
- path: paths.secretsRedactionConfigPath,
543
- reason: "existing file is not managed by vvoc",
544
- };
545
- }
546
- return { action: "kept", path: paths.secretsRedactionConfigPath };
547
- }
548
- return syncSecretsRedactionConfig(paths, options);
549
- }
550
- export async function syncSecretsRedactionConfig(paths, options) {
551
- const currentText = await readOptionalText(paths.secretsRedactionConfigPath);
552
- if (!currentText) {
553
- await writeText(paths.secretsRedactionConfigPath, renderSecretsRedactionConfig());
554
- return { action: "created", path: paths.secretsRedactionConfigPath };
555
- }
556
- if (!options.force && !isManagedFile(currentText)) {
557
- return {
558
- action: "skipped",
559
- path: paths.secretsRedactionConfigPath,
560
- reason: "existing file is not managed by vvoc",
561
- };
562
- }
563
- const nextText = renderSecretsRedactionConfig();
564
- if (currentText === nextText) {
565
- return { action: "kept", path: paths.secretsRedactionConfigPath };
566
- }
567
- await writeText(paths.secretsRedactionConfigPath, nextText);
568
- return { action: "updated", path: paths.secretsRedactionConfigPath };
569
- }
570
- // END_BLOCK_INSTALL_SECRETS_REDACTION_CONFIG
571
386
  // START_BLOCK_INSPECT_INSTALLATION_STATE
572
387
  export async function inspectInstallation(paths) {
573
388
  const warnings = [];
@@ -575,15 +390,6 @@ export async function inspectInstallation(paths) {
575
390
  if (paths.opencodeAlternatePaths.length > 0) {
576
391
  warnings.push(`multiple OpenCode config files exist: ${[paths.opencodeConfigPath, ...paths.opencodeAlternatePaths].join(", ")}`);
577
392
  }
578
- if (paths.guardianAlternatePaths.length > 0) {
579
- warnings.push(`multiple Guardian config files exist: ${[paths.guardianConfigPath, ...paths.guardianAlternatePaths].join(", ")}`);
580
- }
581
- if (paths.memoryConfigAlternates.length > 0) {
582
- warnings.push(`multiple Memory config files exist: ${[paths.memoryConfigPath, ...paths.memoryConfigAlternates].join(", ")}`);
583
- }
584
- if (paths.secretsRedactionConfigAlternates.length > 0) {
585
- warnings.push(`multiple SecretsRedaction config files exist: ${[paths.secretsRedactionConfigPath, ...paths.secretsRedactionConfigAlternates].join(", ")}`);
586
- }
587
393
  const opencodeText = await readOptionalText(paths.opencodeConfigPath);
588
394
  let opencodeParseError;
589
395
  let plugins = [];
@@ -599,49 +405,24 @@ export async function inspectInstallation(paths) {
599
405
  problems.push(opencodeParseError);
600
406
  }
601
407
  }
602
- const guardianText = await readOptionalText(paths.guardianConfigPath);
603
- let guardianParseError;
604
- let guardianOverrides;
605
- const guardianManaged = guardianText ? isManagedFile(guardianText) : false;
606
- if (guardianText) {
408
+ const vvocText = await readOptionalText(paths.vvocConfigPath);
409
+ let vvocParseError;
410
+ let vvocConfig;
411
+ if (vvocText) {
607
412
  try {
608
- guardianOverrides = parseGuardianConfigText(guardianText, paths.guardianConfigPath);
413
+ vvocConfig = parseVvocConfigText(vvocText, paths.vvocConfigPath);
609
414
  }
610
415
  catch (error) {
611
- guardianParseError = error instanceof Error ? error.message : String(error);
612
- problems.push(guardianParseError);
613
- }
614
- }
615
- const memoryText = await readOptionalText(paths.memoryConfigPath);
616
- let memoryParseError;
617
- let memoryOverrides;
618
- const memoryManaged = memoryText ? isManagedFile(memoryText) : false;
619
- if (memoryText) {
620
- try {
621
- memoryOverrides = parseMemoryConfigText(memoryText, paths.memoryConfigPath);
622
- }
623
- catch (error) {
624
- memoryParseError = error instanceof Error ? error.message : String(error);
625
- problems.push(memoryParseError);
626
- }
627
- }
628
- const secretsRedactionText = await readOptionalText(paths.secretsRedactionConfigPath);
629
- let secretsRedactionParseError;
630
- const secretsRedactionManaged = secretsRedactionText
631
- ? isManagedFile(secretsRedactionText)
632
- : false;
633
- if (secretsRedactionText) {
634
- try {
635
- parseSecretsRedactionConfigText(secretsRedactionText, paths.secretsRedactionConfigPath);
636
- }
637
- catch (error) {
638
- secretsRedactionParseError = error instanceof Error ? error.message : String(error);
639
- problems.push(secretsRedactionParseError);
416
+ vvocParseError = error instanceof Error ? error.message : String(error);
417
+ problems.push(vvocParseError);
640
418
  }
641
419
  }
642
420
  if (!pluginConfigured) {
643
421
  problems.push(`${PACKAGE_NAME} is not configured in ${paths.opencodeConfigPath}`);
644
422
  }
423
+ if (!vvocText) {
424
+ problems.push(`vvoc config is missing at ${paths.vvocConfigPath}`);
425
+ }
645
426
  return {
646
427
  scope: paths.scope,
647
428
  opencode: {
@@ -652,28 +433,21 @@ export async function inspectInstallation(paths) {
652
433
  pluginConfigured,
653
434
  plugins,
654
435
  },
436
+ vvoc: {
437
+ path: paths.vvocConfigPath,
438
+ exists: Boolean(vvocText),
439
+ parseError: vvocParseError,
440
+ schema: vvocConfig?.$schema,
441
+ version: vvocConfig?.version,
442
+ },
655
443
  guardian: {
656
- path: paths.guardianConfigPath,
657
- exists: Boolean(guardianText),
658
- alternates: paths.guardianAlternatePaths,
659
- managed: guardianManaged,
660
- parseError: guardianParseError,
661
- overrides: guardianOverrides,
444
+ config: vvocConfig?.guardian,
662
445
  },
663
446
  memory: {
664
- path: paths.memoryConfigPath,
665
- exists: Boolean(memoryText),
666
- alternates: paths.memoryConfigAlternates,
667
- managed: memoryManaged,
668
- parseError: memoryParseError,
669
- overrides: memoryOverrides,
447
+ config: vvocConfig?.memory,
670
448
  },
671
449
  secretsRedaction: {
672
- path: paths.secretsRedactionConfigPath,
673
- exists: Boolean(secretsRedactionText),
674
- alternates: paths.secretsRedactionConfigAlternates,
675
- managed: secretsRedactionManaged,
676
- parseError: secretsRedactionParseError,
450
+ config: vvocConfig?.secretsRedaction,
677
451
  },
678
452
  warnings,
679
453
  problems,
@@ -868,29 +642,6 @@ function updateAgentEntryText(text, agentName, entry) {
868
642
  return ensureTrailingNewline(applyEdits(nextText, format(nextText, undefined, JSON_FORMAT)));
869
643
  }
870
644
  // END_BLOCK_MANAGED_AGENT_HELPERS
871
- function normalizeGuardianOverrides(raw, label) {
872
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
873
- throw new Error(`${label}: expected a top-level object`);
874
- }
875
- const record = raw;
876
- const overrides = {};
877
- if (Object.hasOwn(record, "model")) {
878
- overrides.model = readNonEmptyString(record.model, `${label}: model`);
879
- }
880
- if (Object.hasOwn(record, "variant")) {
881
- overrides.variant = readNonEmptyString(record.variant, `${label}: variant`);
882
- }
883
- if (Object.hasOwn(record, "timeoutMs")) {
884
- overrides.timeoutMs = readPositiveInteger(record.timeoutMs, `${label}: timeoutMs`);
885
- }
886
- if (Object.hasOwn(record, "approvalRiskThreshold")) {
887
- overrides.approvalRiskThreshold = readThreshold(record.approvalRiskThreshold, `${label}: approvalRiskThreshold`);
888
- }
889
- if (Object.hasOwn(record, "reviewToastDurationMs")) {
890
- overrides.reviewToastDurationMs = readPositiveInteger(record.reviewToastDurationMs, `${label}: reviewToastDurationMs`);
891
- }
892
- return overrides;
893
- }
894
645
  function readNonEmptyString(value, label) {
895
646
  if (typeof value !== "string" || !value.trim()) {
896
647
  throw new Error(`${label}: expected a non-empty string`);
@@ -907,18 +658,6 @@ function readOptionalObject(document, key, label) {
907
658
  }
908
659
  return value;
909
660
  }
910
- function readPositiveInteger(value, label) {
911
- if (typeof value === "number" && Number.isFinite(value) && value > 0) {
912
- return Math.round(value);
913
- }
914
- throw new Error(`${label}: expected a positive integer`);
915
- }
916
- function readThreshold(value, label) {
917
- if (typeof value === "number" && Number.isFinite(value)) {
918
- return Math.max(0, Math.min(100, Math.round(value)));
919
- }
920
- throw new Error(`${label}: expected a number between 0 and 100`);
921
- }
922
661
  // END_BLOCK_PARSE_AND_NORMALIZE_CONFIG_VALUES
923
662
  // START_BLOCK_FILESYSTEM_HELPERS
924
663
  function isManagedFile(text) {
@@ -934,6 +673,17 @@ function stripMarkdownFrontmatter(text) {
934
673
  function ensureTrailingNewline(text) {
935
674
  return text.endsWith("\n") ? text : `${text}\n`;
936
675
  }
676
+ async function writeResolvedVvocConfig(path, currentText, config) {
677
+ const nextText = renderVvocConfig(config);
678
+ if ((currentText ?? "") === nextText) {
679
+ return { action: "kept", path };
680
+ }
681
+ await writeText(path, nextText);
682
+ return {
683
+ action: currentText ? "updated" : "created",
684
+ path,
685
+ };
686
+ }
937
687
  async function readOptionalText(path) {
938
688
  try {
939
689
  return await readFile(path, "utf8");