@kisev/agentomatic 11.0.2 → 12.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +32 -16
  2. package/README.ru.md +33 -16
  3. package/dist/agent-profiles.d.ts +8 -2
  4. package/dist/agent-profiles.js +131 -82
  5. package/dist/assets/agents/architect.md +4 -4
  6. package/dist/assets/agents/critic.md +10 -11
  7. package/dist/assets/agents/manager.md +11 -12
  8. package/dist/assets/agents/mapper.md +4 -4
  9. package/dist/assets/agents/review.md +11 -12
  10. package/dist/assets/agents/worker.md +29 -32
  11. package/dist/assets/commands/asd-ste100.md +10 -0
  12. package/dist/assets/commands/code-simplify.md +12 -0
  13. package/dist/assets/commands/eli5.md +10 -0
  14. package/dist/assets/commands/handoff.md +10 -0
  15. package/dist/assets/commands/rtk-stats.md +2 -2
  16. package/dist/assets/commands/skill-doctor.md +10 -0
  17. package/dist/assets/migration-inventory.json +17 -6
  18. package/dist/assets/plugins/code-simplify.js +3 -0
  19. package/dist/assets/plugins/rtk.js +1 -1
  20. package/dist/assets/plugins/rules-injector.js +1 -1
  21. package/dist/assets/plugins/zed-bell.js +1 -1
  22. package/dist/catalog.d.ts +2 -2
  23. package/dist/catalog.js +9 -3
  24. package/dist/cli-output.js +6 -6
  25. package/dist/cli.d.ts +1 -1
  26. package/dist/cli.js +1 -1257
  27. package/dist/command-cli.d.ts +1 -0
  28. package/dist/command-cli.js +1004 -0
  29. package/dist/config-setup.d.ts +14 -8
  30. package/dist/config-setup.js +197 -64
  31. package/dist/contracts.js +26 -0
  32. package/dist/doctor.d.ts +0 -1
  33. package/dist/doctor.js +42 -55
  34. package/dist/index.d.ts +13 -118
  35. package/dist/index.js +118 -125
  36. package/dist/installer.d.ts +25 -2
  37. package/dist/installer.js +68 -23
  38. package/dist/jsonc.d.ts +18 -2
  39. package/dist/jsonc.js +109 -5
  40. package/dist/opencode-config.d.ts +11 -0
  41. package/dist/opencode-config.js +237 -0
  42. package/dist/plugins/code-simplify.d.ts +25 -0
  43. package/dist/plugins/code-simplify.js +49 -0
  44. package/dist/plugins/events.d.ts +20 -0
  45. package/dist/plugins/events.js +48 -0
  46. package/dist/plugins/rtk.d.ts +9 -12
  47. package/dist/plugins/rtk.js +20 -5
  48. package/dist/plugins/rules-injector.d.ts +21 -11
  49. package/dist/plugins/rules-injector.js +43 -31
  50. package/dist/plugins/zed-bell.d.ts +8 -5
  51. package/dist/plugins/zed-bell.js +10 -2
  52. package/dist/reconcile.d.ts +4 -1
  53. package/dist/reconcile.js +12 -16
  54. package/dist/registry.d.ts +1 -0
  55. package/dist/registry.js +21 -5
  56. package/dist/routing.d.ts +5 -3
  57. package/dist/routing.js +57 -22
  58. package/dist/self-install.d.ts +7 -0
  59. package/dist/self-install.js +52 -1
  60. package/dist/terminal-wizard.d.ts +1 -1
  61. package/dist/terminal-wizard.js +2 -2
  62. package/package.json +11 -6
  63. package/dist/assets/commands/skill-improve.md +0 -10
  64. package/dist/assets/commands/stopit.md +0 -10
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { fileURLToPath } from "node:url";
7
- import { applyTransaction, deploymentRoot, destination, LifecycleError, lifecycleRoot, listDirectRegular, readRegular, recoverTransaction, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
7
+ import { applyTransaction, deploymentRoot, destination, LifecycleError, lifecycleRoot, listDirectRegular, readRegular, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
8
8
  import { readPackageVersion } from "./package-metadata.js";
9
9
  export const FIXED_AGENT_ROLES = [
10
10
  "manager",
@@ -51,6 +51,8 @@ function packageVersion() {
51
51
  function emptyConfig() {
52
52
  return {
53
53
  schema_version: 1,
54
+ selected_fixed: [],
55
+ selected_critics: [],
54
56
  fixed: Object.fromEntries(FIXED_AGENT_ROLES.map((role) => [role, {}])),
55
57
  additional_critics: {},
56
58
  };
@@ -100,7 +102,10 @@ function parseConfig(raw) {
100
102
  throw new AgentProfileError("invalid_configuration", "Agent profile configuration is not valid JSON");
101
103
  }
102
104
  const config = value;
103
- if (config.schema_version !== 1 ||
105
+ if (!config ||
106
+ typeof config !== "object" ||
107
+ Array.isArray(config) ||
108
+ config.schema_version !== 1 ||
104
109
  !config.fixed ||
105
110
  typeof config.fixed !== "object" ||
106
111
  Array.isArray(config.fixed) ||
@@ -109,17 +114,37 @@ function parseConfig(raw) {
109
114
  Array.isArray(config.additional_critics)) {
110
115
  throw new AgentProfileError("invalid_configuration", "Agent profile configuration has an unsupported schema");
111
116
  }
117
+ if (Object.keys(config).some((key) => ![
118
+ "schema_version",
119
+ "selected_fixed",
120
+ "selected_critics",
121
+ "fixed",
122
+ "additional_critics",
123
+ ].includes(key)))
124
+ throw new AgentProfileError("invalid_configuration", "Agent profile configuration contains unknown fields; preserve and inspect it before retrying");
112
125
  if (Object.keys(config.fixed).sort().join(",") !== [...FIXED_AGENT_ROLES].sort().join(","))
113
126
  throw new AgentProfileError("invalid_configuration", "Configuration must contain every fixed role exactly once");
114
127
  const fixed = Object.fromEntries(FIXED_AGENT_ROLES.map((role) => [role, parseSelection(config.fixed[role], false)]));
128
+ if (config.selected_fixed !== undefined &&
129
+ (!Array.isArray(config.selected_fixed) ||
130
+ config.selected_fixed.some((role) => !FIXED_AGENT_ROLES.includes(role)) ||
131
+ new Set(config.selected_fixed).size !== config.selected_fixed.length))
132
+ throw new AgentProfileError("invalid_configuration", "selected_fixed must contain unique fixed roles");
115
133
  const additional = {};
116
134
  for (const [name, selection] of Object.entries(config.additional_critics)) {
117
135
  if (!CRITIC_PATTERN.test(name))
118
136
  throw new AgentProfileError("invalid_configuration", `Unsafe additional critic name: ${name}`);
119
137
  additional[name] = parseSelection(selection, true);
120
138
  }
139
+ if (config.selected_critics !== undefined &&
140
+ (!Array.isArray(config.selected_critics) ||
141
+ config.selected_critics.some((name) => !CRITIC_PATTERN.test(name) || !(name in additional)) ||
142
+ new Set(config.selected_critics).size !== config.selected_critics.length))
143
+ throw new AgentProfileError("invalid_configuration", "selected_critics must reference unique saved critics");
121
144
  return {
122
145
  schema_version: 1,
146
+ selected_fixed: config.selected_fixed ?? [],
147
+ selected_critics: config.selected_critics ?? [],
123
148
  fixed,
124
149
  additional_critics: Object.fromEntries(Object.entries(additional).sort(([left], [right]) => left.localeCompare(right))),
125
150
  };
@@ -183,10 +208,17 @@ async function loadState(scope, root) {
183
208
  throw new AgentProfileError("unsafe_configuration", "Agent profile configuration must use mode 0600");
184
209
  if (manifestRaw && (await inspectFileMode(manifestPath)) !== 0o600)
185
210
  throw new AgentProfileError("unsafe_manifest", "Agent deployment manifest must use mode 0600");
211
+ const manifest = manifestRaw ? parseManifest(manifestRaw, scope) : undefined;
212
+ const config = configRaw ? parseConfig(configRaw) : emptyConfig();
213
+ // Recover the selected set from ownership when reading an existing pre-selection file.
214
+ if (configRaw && !("selected_fixed" in JSON.parse(configRaw.toString("utf8")))) {
215
+ config.selected_fixed = FIXED_AGENT_ROLES.filter((role) => Boolean(manifest?.profiles[role]));
216
+ config.selected_critics = Object.keys(config.additional_critics).filter((name) => Boolean(manifest?.profiles[name]));
217
+ }
186
218
  return {
187
- config: configRaw ? parseConfig(configRaw) : emptyConfig(),
219
+ config,
188
220
  configRaw,
189
- manifest: manifestRaw ? parseManifest(manifestRaw, scope) : undefined,
221
+ manifest,
190
222
  manifestRaw,
191
223
  };
192
224
  }
@@ -210,21 +242,21 @@ function withSelection(content, selection) {
210
242
  const mode = filtered.findIndex((line, index) => index < end && line.startsWith("mode:"));
211
243
  if (mode < 0)
212
244
  throw new AgentProfileError("asset_error", "Canonical agent has no mode");
213
- filtered.splice(mode + 1, 0, `model: ${selection.model}`, ...(selection.variant ? [`variant: ${selection.variant}`] : []));
245
+ filtered.splice(mode + 1, 0, `model: ${selection.model}${selection.variant ? `#${selection.variant}` : ""}`);
214
246
  return filtered.join("\n");
215
247
  }
216
248
  function replaceTaskAllowlist(content, allowed) {
217
249
  const lines = content.split("\n");
218
- const permission = lines.indexOf("permission:");
250
+ const permission = lines.indexOf("permissions:");
219
251
  if (permission < 0)
220
252
  throw new AgentProfileError("asset_error", "Canonical primary agent has no permission block");
221
- const task = lines.findIndex((line, index) => index > permission && line === " task:");
253
+ const task = lines.findIndex((line, index) => index > permission && line.startsWith(" - { action: subagent,"));
222
254
  if (task < 0)
223
255
  throw new AgentProfileError("asset_error", "Canonical primary agent has no task permission");
224
256
  let end = task + 1;
225
- while (end < lines.length && (lines[end].startsWith(" ") || lines[end] === ""))
257
+ while (end < lines.length && lines[end].startsWith(" - { action: subagent,"))
226
258
  end += 1;
227
- lines.splice(task, end - task, " task:", ' "*": deny', ...allowed.map((name) => ` ${name}: allow`));
259
+ lines.splice(task, end - task, ' - { action: subagent, resource: "*", effect: deny }', ...allowed.map((name) => ` - { action: subagent, resource: ${JSON.stringify(name)}, effect: allow }`));
228
260
  return lines.join("\n");
229
261
  }
230
262
  export function renderAgentProfile(name, config, canonical) {
@@ -234,9 +266,12 @@ export function renderAgentProfile(name, config, canonical) {
234
266
  if (!selection)
235
267
  throw new AgentProfileError("invalid_configuration", `No configuration exists for ${name}`);
236
268
  let rendered = withSelection(canonical[role].toString("utf8"), selection);
237
- const pool = ["critic", ...Object.keys(config.additional_critics)].sort();
269
+ const pool = desiredNames(config).filter((item) => item === "critic" || CRITIC_PATTERN.test(item));
238
270
  if (name === "manager")
239
- rendered = replaceTaskAllowlist(rendered, ["architect", "worker", "mapper", "review", ...pool]);
271
+ rendered = replaceTaskAllowlist(rendered, [
272
+ ...config.selected_fixed.filter((role) => ["architect", "worker", "mapper", "review"].includes(role)),
273
+ ...pool,
274
+ ]);
240
275
  if (name === "review")
241
276
  rendered = replaceTaskAllowlist(rendered, pool);
242
277
  return Buffer.from(rendered);
@@ -247,7 +282,7 @@ function selectionFor(config, name) {
247
282
  : config.additional_critics[name];
248
283
  }
249
284
  function desiredNames(config) {
250
- return [...FIXED_AGENT_ROLES, ...Object.keys(config.additional_critics)].sort();
285
+ return [...config.selected_fixed, ...config.selected_critics].sort();
251
286
  }
252
287
  function desiredManifest(scope, config, rendered, canonical, names = desiredNames(config)) {
253
288
  const profiles = {};
@@ -273,6 +308,11 @@ function desiredManifest(scope, config, rendered, canonical, names = desiredName
273
308
  }
274
309
  function changeConfig(current, request) {
275
310
  const config = JSON.parse(JSON.stringify(current));
311
+ if (request.changes) {
312
+ if (request.changes.some((change) => change.changes || !["model-set", "critic-add", "critic-remove"].includes(change.action)))
313
+ throw new AgentProfileError("invalid_input", "Profile drafts accept only model and critic changes");
314
+ return request.changes.reduce((draft, change) => changeConfig(draft, change), config);
315
+ }
276
316
  if (request.action === "model-set") {
277
317
  const name = validateAgentName(request.name ?? "");
278
318
  if (!NAME_PATTERN.test(name) && !(name in config.additional_critics))
@@ -297,6 +337,7 @@ function changeConfig(current, request) {
297
337
  if (variant)
298
338
  selection.variant = variant;
299
339
  config.additional_critics[name] = selection;
340
+ config.selected_critics.push(name);
300
341
  }
301
342
  else if (request.action === "critic-remove") {
302
343
  const name = request.name ?? "";
@@ -307,6 +348,7 @@ function changeConfig(current, request) {
307
348
  if (!(name in config.additional_critics))
308
349
  throw new AgentProfileError("unknown_profile", `Additional critic is not configured: ${name}`);
309
350
  delete config.additional_critics[name];
351
+ config.selected_critics = config.selected_critics.filter((item) => item !== name);
310
352
  }
311
353
  config.additional_critics = Object.fromEntries(Object.entries(config.additional_critics).sort(([left], [right]) => left.localeCompare(right)));
312
354
  return config;
@@ -335,11 +377,17 @@ export async function buildAgentProfilePlan(request, scope, cwd = process.cwd(),
335
377
  ]);
336
378
  const byFile = new Map(agentFiles.filter((item) => item.name.endsWith(".md")).map((item) => [item.name, item.content]));
337
379
  const config = changeConfig(state.config, request);
338
- const names = request.action === "uninstall"
339
- ? []
340
- : selectedAgents === undefined
341
- ? desiredNames(config)
342
- : [...selectedAgents, ...Object.keys(config.additional_critics)].sort();
380
+ if (request.action === "install" && selectedAgents === undefined && !state.configRaw)
381
+ config.selected_fixed = [...FIXED_AGENT_ROLES];
382
+ if (selectedAgents !== undefined)
383
+ config.selected_fixed = [...selectedAgents];
384
+ if (request.action === "install")
385
+ config.selected_critics = Object.keys(config.additional_critics).sort();
386
+ if (request.action === "uninstall") {
387
+ config.selected_fixed = [];
388
+ config.selected_critics = [];
389
+ }
390
+ const names = request.action === "uninstall" ? [] : desiredNames(config);
343
391
  const rendered = Object.fromEntries(names.map((name) => [name, renderAgentProfile(name, config, canonical)]));
344
392
  const desired = request.action === "uninstall"
345
393
  ? undefined
@@ -412,11 +460,13 @@ export async function buildAgentProfilePlan(request, scope, cwd = process.cwd(),
412
460
  (!content ||
413
461
  sha256(content) !== ownedHash ||
414
462
  agentFiles.find((item) => item.name === `${name}.md`)?.mode !== 0o600);
415
- if (drift && request.action !== "reconcile") {
463
+ if (drift &&
464
+ !(["install", "reconcile"].includes(request.action) &&
465
+ (!content || sha256(content) === ownedHash))) {
416
466
  operations.push({
417
467
  path,
418
468
  operation: "conflict",
419
- reason: "managed profile drift requires explicit reconcile",
469
+ reason: "managed profile drift is preserved; restore the recorded bytes before retrying",
420
470
  ...(content ? { sha256: sha256(content) } : {}),
421
471
  });
422
472
  continue;
@@ -455,8 +505,7 @@ export async function buildAgentProfilePlan(request, scope, cwd = process.cwd(),
455
505
  }
456
506
  }
457
507
  const configContent = Buffer.from(`${stable(config)}\n`);
458
- if (request.action !== "uninstall" &&
459
- names.length > 0 &&
508
+ if ((names.length > 0 || Boolean(state.configRaw) || request.action === "model-set") &&
460
509
  (!state.configRaw || !state.configRaw.equals(configContent))) {
461
510
  operations.push({
462
511
  path: CONFIG_PATH,
@@ -521,13 +570,14 @@ export async function buildAgentProfilePlan(request, scope, cwd = process.cwd(),
521
570
  };
522
571
  const plan = {
523
572
  ...base,
573
+ digest: sha256(stable({ mutations, config: state.configRaw, manifest: state.manifestRaw, base })),
524
574
  };
525
575
  return {
526
576
  plan,
527
577
  mutations,
528
578
  config,
529
579
  manifest: finalManifest,
530
- expectedConfig: names.length ? configContent : state.configRaw,
580
+ expectedConfig: names.length || state.configRaw || request.action === "model-set" ? configContent : undefined,
531
581
  expectedManifest: manifestContent,
532
582
  legacyTransferred,
533
583
  };
@@ -577,17 +627,25 @@ export async function listAgentProfiles(scope, cwd = process.cwd(), home = homed
577
627
  const records = [];
578
628
  const collisions = [];
579
629
  const drift = [];
580
- for (const name of configured) {
630
+ for (const name of [
631
+ ...new Set([
632
+ ...FIXED_AGENT_ROLES,
633
+ ...Object.keys(state.config.additional_critics),
634
+ ...configured,
635
+ ]),
636
+ ].sort()) {
581
637
  const file = byName.get(name);
582
638
  const owned = state.manifest?.profiles[name];
583
639
  let stateValue;
584
- if (!owned && file)
585
- stateValue = "collision";
640
+ if (!configured.includes(name) && !owned && !file)
641
+ stateValue = "not-installed";
642
+ else if (!owned && file)
643
+ stateValue = configured.includes(name) ? "collision" : "current";
586
644
  else if (!file)
587
645
  stateValue = owned ? "drift" : "missing";
588
646
  else if (!owned ||
589
647
  sha256(file.content) !== owned.rendered_sha256 ||
590
- !file.content.equals(desired[name]) ||
648
+ (configured.includes(name) && !file.content.equals(desired[name])) ||
591
649
  file.mode !== 0o600)
592
650
  stateValue = owned ? "drift" : "collision";
593
651
  else
@@ -599,7 +657,11 @@ export async function listAgentProfiles(scope, cwd = process.cwd(), home = homed
599
657
  const selection = selectionFor(state.config, name);
600
658
  records.push({
601
659
  name,
602
- ownership: NAME_PATTERN.test(name) ? "package-owned" : "managed",
660
+ ownership: !configured.includes(name) && file && !owned
661
+ ? "user-owned"
662
+ : NAME_PATTERN.test(name)
663
+ ? "package-owned"
664
+ : "managed",
603
665
  state: stateValue,
604
666
  ...("model" in selection
605
667
  ? { model: selection.model, ...(selection.variant ? { variant: selection.variant } : {}) }
@@ -621,9 +683,12 @@ export async function listAgentProfiles(scope, cwd = process.cwd(), home = homed
621
683
  root,
622
684
  package_version: packageVersion(),
623
685
  critic_pool: state.manifest?.critic_pool ??
624
- ["critic", ...Object.keys(state.config.additional_critics)].sort(),
686
+ configured.filter((name) => name === "critic" || CRITIC_PATTERN.test(name)),
625
687
  profiles: records.sort((left, right) => left.name.localeCompare(right.name)),
626
- user_owned: userOwned,
688
+ user_owned: records
689
+ .filter((record) => record.ownership === "user-owned")
690
+ .map((record) => record.name)
691
+ .sort(),
627
692
  collisions,
628
693
  drift,
629
694
  requires_restart: false,
@@ -632,25 +697,24 @@ export async function listAgentProfiles(scope, cwd = process.cwd(), home = homed
632
697
  }
633
698
  export async function previewAgentProfileChange(request, scope, cwd = process.cwd(), home = homedir()) {
634
699
  const stateRoot = lifecycleRoot(scope, cwd, home);
635
- return withLifecycleLock(stateRoot, async () => {
636
- if (await recoverTransaction(deploymentRoot(scope, cwd, home), stateRoot))
637
- throw new AgentProfileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
638
- return (await buildAgentProfilePlan(request, scope, cwd, home)).plan;
639
- });
700
+ if (await readRegular(resolve(stateRoot, "transaction-journal.json")))
701
+ throw new AgentProfileError("recovery_required", "Run maintenance recover before a fresh preview");
702
+ return (await buildAgentProfilePlan(request, scope, cwd, home)).plan;
640
703
  }
641
704
  export async function applyAgentProfileChange(request, scope, cwd = process.cwd(), home = homedir(), options = {}) {
642
705
  const stateRoot = lifecycleRoot(scope, cwd, home);
643
706
  const root = deploymentRoot(scope, cwd, home);
644
707
  return withLifecycleLock(stateRoot, async () => {
645
- if (await recoverTransaction(root, stateRoot))
646
- throw new AgentProfileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
708
+ if (await readRegular(resolve(stateRoot, "transaction-journal.json")))
709
+ throw new AgentProfileError("recovery_required", "Run maintenance recover before a fresh preview");
647
710
  const built = await buildAgentProfilePlan(request, scope, cwd, home);
711
+ if (options.expectedDigest && options.expectedDigest !== built.plan.digest)
712
+ throw new AgentProfileError("stale_plan", "Profiles changed; request a fresh preview");
648
713
  if (built.plan.operations.some((item) => item.operation === "conflict" && item.reason.includes("collision")))
649
714
  throw new AgentProfileError("collision", "Exact-name user-owned collision blocks apply");
650
715
  if (request.action !== "uninstall" &&
651
- request.action !== "reconcile" &&
652
716
  built.plan.operations.some((item) => item.operation === "conflict"))
653
- throw new AgentProfileError("drift", "Managed profile drift requires explicit reconcile");
717
+ throw new AgentProfileError("drift", "Managed profile drift is preserved; restore the recorded bytes before retrying");
654
718
  await applyTransaction(root, stateRoot, built.mutations, {
655
719
  ...options,
656
720
  validateFinal: async () => {
@@ -662,7 +726,7 @@ export async function applyAgentProfileChange(request, scope, cwd = process.cwd(
662
726
  final.drift.length ||
663
727
  final.profiles
664
728
  .filter((item) => item.ownership !== "user-owned")
665
- .some((item) => item.state !== "current"))) {
729
+ .some((item) => item.state !== "current" && item.state !== "not-installed"))) {
666
730
  throw new AgentProfileError("final_validation_failed", "Final agent inventory validation failed");
667
731
  }
668
732
  },
@@ -675,65 +739,50 @@ export async function applyAgentProfileChange(request, scope, cwd = process.cwd(
675
739
  };
676
740
  });
677
741
  }
742
+ async function modelCatalog() {
743
+ const query = new URLSearchParams({ "location[directory]": process.cwd() });
744
+ const { stdout } = await execFileAsync("opencode", ["api", "get", `/api/model?${query}`], {
745
+ timeout: 10_000,
746
+ encoding: "utf8",
747
+ maxBuffer: 10 * 1024 * 1024,
748
+ });
749
+ const response = JSON.parse(stdout);
750
+ if (!Array.isArray(response.data) ||
751
+ !response.data.every((entry) => entry &&
752
+ typeof entry.providerID === "string" &&
753
+ typeof entry.id === "string" &&
754
+ MODEL_PATTERN.test(`${entry.providerID}/${entry.id}`)))
755
+ throw new Error("invalid V2 model inventory");
756
+ return response.data;
757
+ }
678
758
  export async function availableModels() {
679
759
  try {
680
- const { stdout } = await execFileAsync("opencode", ["models"], {
681
- timeout: 10_000,
682
- encoding: "utf8",
683
- });
684
760
  const models = [
685
- ...new Set(stdout
686
- .split(/\r?\n/)
687
- .map((line) => line.trim())
688
- .filter((line) => MODEL_PATTERN.test(line))),
761
+ ...new Set((await modelCatalog()).map((entry) => `${entry.providerID}/${entry.id}`)),
689
762
  ].sort();
690
763
  if (!models.length)
691
764
  throw new Error("empty catalog");
692
765
  return models;
693
766
  }
694
767
  catch (error) {
695
- throw new AgentProfileError("catalog_unavailable", `Cached OpenCode model catalog is unavailable; provide an explicit provider/model: ${error instanceof Error ? error.message : String(error)}`);
768
+ throw new AgentProfileError("catalog_unavailable", `OpenCode V2 model snapshot is unavailable; provide an explicit provider/model: ${error instanceof Error ? error.message : String(error)}`);
696
769
  }
697
770
  }
698
771
  export async function availableModelVariants(model) {
699
772
  const selected = validateModel(model);
700
- const provider = selected.split("/", 1)[0];
701
773
  try {
702
- const { stdout } = await execFileAsync("opencode", ["models", provider, "--verbose"], {
703
- timeout: 10_000,
704
- encoding: "utf8",
705
- maxBuffer: 10 * 1024 * 1024,
706
- });
707
- const lines = stdout.split(/\r?\n/);
708
- for (let index = 0; index < lines.length; index += 1) {
709
- if (lines[index].trim() !== selected)
710
- continue;
711
- let document = "";
712
- for (index += 1; index < lines.length; index += 1) {
713
- document += `${lines[index]}\n`;
714
- try {
715
- const metadata = JSON.parse(document);
716
- if (metadata.variants === undefined)
717
- return [];
718
- if (!metadata.variants ||
719
- typeof metadata.variants !== "object" ||
720
- Array.isArray(metadata.variants))
721
- throw new Error("invalid variants metadata");
722
- const variants = Object.keys(metadata.variants);
723
- if (!variants.every((variant) => VARIANT_PATTERN.test(variant)))
724
- throw new Error("unsafe variant");
725
- return variants;
726
- }
727
- catch (error) {
728
- if (!(error instanceof SyntaxError))
729
- throw error;
730
- }
731
- }
732
- }
733
- throw new Error("selected model is absent");
774
+ const metadata = (await modelCatalog()).find((entry) => `${entry.providerID}/${entry.id}` === selected);
775
+ if (!metadata)
776
+ throw new Error("selected model is absent");
777
+ if (metadata.variants === undefined)
778
+ return [];
779
+ if (!Array.isArray(metadata.variants) ||
780
+ !metadata.variants.every((variant) => variant && typeof variant.id === "string" && VARIANT_PATTERN.test(variant.id)))
781
+ throw new Error("invalid variants metadata");
782
+ return metadata.variants.map((variant) => variant.id);
734
783
  }
735
784
  catch (error) {
736
- throw new AgentProfileError("catalog_unavailable", `Cached OpenCode model variants are unavailable: ${error instanceof Error ? error.message : String(error)}`);
785
+ throw new AgentProfileError("catalog_unavailable", `OpenCode V2 model variants are unavailable: ${error instanceof Error ? error.message : String(error)}`);
737
786
  }
738
787
  }
739
788
  export async function inspectFileMode(path) {
@@ -3,10 +3,10 @@ description: Analyzes architecture choices, compatibility, and operational risk.
3
3
  mode: subagent
4
4
  hidden: true
5
5
  steps: 16
6
- permission:
7
- edit: deny
8
- bash: deny
9
- task: deny
6
+ permissions:
7
+ - { action: edit, resource: "*", effect: deny }
8
+ - { action: shell, resource: "*", effect: deny }
9
+ - { action: subagent, resource: "*", effect: deny }
10
10
  ---
11
11
 
12
12
  # Architect
@@ -3,14 +3,13 @@ description: Independently reviews changes for correctness, security, and regres
3
3
  mode: subagent
4
4
  hidden: true
5
5
  steps: 12
6
- permission:
7
- edit: deny
8
- bash:
9
- "*": deny
10
- "git --no-optional-locks -c core.fsmonitor=false status --porcelain=v1 --untracked-files=all": allow
11
- "git -c diff.external= -c diff.trustExitCode=false diff --no-ext-diff --no-textconv --": allow
12
- "git -c diff.external= -c diff.trustExitCode=false diff --cached --no-ext-diff --no-textconv --": allow
13
- task: deny
6
+ permissions:
7
+ - { action: edit, resource: "*", effect: deny }
8
+ - { action: shell, resource: "*", effect: deny }
9
+ - { action: shell, resource: "git --no-optional-locks -c core.fsmonitor=false status --porcelain=v1 --untracked-files=all", effect: allow }
10
+ - { action: shell, resource: "git -c diff.external= -c diff.trustExitCode=false diff --no-ext-diff --no-textconv --", effect: allow }
11
+ - { action: shell, resource: "git -c diff.external= -c diff.trustExitCode=false diff --cached --no-ext-diff --no-textconv --", effect: allow }
12
+ - { action: subagent, resource: "*", effect: deny }
14
13
  ---
15
14
 
16
15
  # Critic
@@ -24,7 +23,7 @@ a non-code target. Stay read-only and never delegate.
24
23
 
25
24
  For execution-card verification, inspect the actual worktree diff, not a worker summary. Use only the supplied
26
25
  execution card and worker report as context, and verify the result against the
27
- card's exact write set, acceptance criteria, checks, and boundaries. For Bash
26
+ card's exact write set, acceptance criteria, checks, and boundaries. For shell
28
27
  inspection use exactly the three literal commands in this agent's allowlist:
29
28
 
30
29
  ```text
@@ -34,10 +33,10 @@ git -c diff.external= -c diff.trustExitCode=false diff --cached --no-ext-diff --
34
33
  ```
35
34
 
36
35
  Read untracked files named by status with native OpenCode `Read`, never with
37
- Bash. Return exactly one structured `critic_report` with matching `card_id` and
36
+ shell. Return exactly one structured `critic_report` with matching `card_id` and
38
37
  `revision`. Its only status values are `APPROVED` and `CHANGES_REQUIRED`;
39
38
  include `schema_version: 1`, verified findings, evidence, unrun checks, and risks. Do not edit files,
40
- run any other Bash command, delegate work, or propose direct worker remediation.
39
+ run any other shell command, delegate work, or propose direct worker remediation.
41
40
 
42
41
  ```json
43
42
  {
@@ -2,17 +2,16 @@
2
2
  description: Coordinates bounded OpenCode routing and execution. Russian triggers: менеджер, координация.
3
3
  mode: primary
4
4
  steps: 12
5
- permission:
6
- edit: deny
7
- bash: deny
8
- question: allow
9
- task:
10
- "*": deny
11
- architect: allow
12
- worker: allow
13
- mapper: allow
14
- review: allow
15
- critic: allow
5
+ permissions:
6
+ - { action: edit, resource: "*", effect: deny }
7
+ - { action: shell, resource: "*", effect: deny }
8
+ - { action: question, resource: "*", effect: allow }
9
+ - { action: subagent, resource: "*", effect: deny }
10
+ - { action: subagent, resource: "architect", effect: allow }
11
+ - { action: subagent, resource: "worker", effect: allow }
12
+ - { action: subagent, resource: "mapper", effect: allow }
13
+ - { action: subagent, resource: "review", effect: allow }
14
+ - { action: subagent, resource: "critic", effect: allow }
16
15
  ---
17
16
 
18
17
  # Manager
@@ -33,7 +32,7 @@ Never pass caller-supplied agents, capabilities, tools, models, or availability
33
32
  to routing. Unknown or user-owned profiles require an explicit trusted override.
34
33
 
35
34
  Forward the original task, evidence, exact card, confirmation references, and
36
- structured result unchanged. Validate machine contracts at each OpenCode Task
35
+ structured result unchanged. Validate machine contracts at each OpenCode subagent
37
36
  dispatch and result boundary. Resolve disagreements claim-by-claim against the
38
37
  supplied evidence and record the evidence reference; unresolved conflicts remain
39
38
  blocked and return to the manager. Never vote between agents or start another worker or
@@ -3,10 +3,10 @@ description: Maps relevant files, callers, tests, and established repository pat
3
3
  mode: subagent
4
4
  hidden: true
5
5
  steps: 12
6
- permission:
7
- edit: deny
8
- bash: deny
9
- task: deny
6
+ permissions:
7
+ - { action: edit, resource: "*", effect: deny }
8
+ - { action: shell, resource: "*", effect: deny }
9
+ - { action: subagent, resource: "*", effect: deny }
10
10
  ---
11
11
 
12
12
  # Mapper
@@ -1,16 +1,15 @@
1
1
  ---
2
2
  description: Reviews exact targets, coordinates independent critics, and applies fixes only on explicit request. Russian triggers: ревью, проверка.
3
3
  mode: all
4
- permission:
5
- edit: allow
6
- bash: allow
7
- task:
8
- "*": deny
9
- critic: allow
10
- webfetch: deny
11
- websearch: deny
12
- question: allow
13
- skill: allow
4
+ permissions:
5
+ - { action: edit, resource: "*", effect: allow }
6
+ - { action: shell, resource: "*", effect: allow }
7
+ - { action: subagent, resource: "*", effect: deny }
8
+ - { action: subagent, resource: "critic", effect: allow }
9
+ - { action: webfetch, resource: "*", effect: deny }
10
+ - { action: websearch, resource: "*", effect: deny }
11
+ - { action: question, resource: "*", effect: allow }
12
+ - { action: skill, resource: "*", effect: allow }
14
13
  ---
15
14
 
16
15
  # Reviewer
@@ -25,9 +24,9 @@ in force during review. Report changed files and checks after fixes.
25
24
 
26
25
  Use independent critics when the selected skill requires them or the user requests
27
26
  them. Select exactly
28
- one critic from this profile's exact task allowlist; never infer or expand the pool
27
+ one critic from this profile's exact subagent allowlist; never infer or expand the pool
29
28
  with a prefix wildcard or auto-fan-out. Send the critic its clean package and
30
- use `route` with the selected critic as the override before each Task call. Keep
29
+ use `route` with the selected critic as the override before each subagent call. Keep
31
30
  the child's session independent from the primary review and withhold your findings
32
31
  while sharing the agreed scope, evidence, and accepted limitations. Validate its
33
32
  versioned `review_report` before use. Report only verified findings,
@@ -3,38 +3,35 @@ description: Implements scoped changes and runs the relevant verification. Russi
3
3
  mode: subagent
4
4
  hidden: true
5
5
  steps: 12
6
- permission:
7
- edit: allow
8
- task: deny
9
- bash:
10
- "*": ask
11
- mypy: allow
12
- "mypy *": allow
13
- "ruff check*": allow
14
- "ruff format --check*": allow
15
- "task --list*": allow
16
- "task -l*": allow
17
- rg: deny
18
- "rg *": deny
19
- grep: deny
20
- "grep *": deny
21
- find: deny
22
- "find *": deny
23
- fd: deny
24
- "fd *": deny
25
- "git grep": deny
26
- "git grep *": deny
27
- "git -C * grep": deny
28
- "git -C * grep *": deny
29
- "git * grep": deny
30
- "git * grep *": deny
31
- glob: deny
32
- grep: deny
33
- list: deny
34
- webfetch: deny
35
- websearch: deny
36
- skill: deny
37
- lsp: deny
6
+ permissions:
7
+ - { action: edit, resource: "*", effect: allow }
8
+ - { action: subagent, resource: "*", effect: deny }
9
+ - { action: shell, resource: "*", effect: ask }
10
+ - { action: shell, resource: "mypy", effect: allow }
11
+ - { action: shell, resource: "mypy *", effect: allow }
12
+ - { action: shell, resource: "ruff check*", effect: allow }
13
+ - { action: shell, resource: "ruff format --check*", effect: allow }
14
+ - { action: shell, resource: "task --list*", effect: allow }
15
+ - { action: shell, resource: "task -l*", effect: allow }
16
+ - { action: shell, resource: "rg", effect: deny }
17
+ - { action: shell, resource: "rg *", effect: deny }
18
+ - { action: shell, resource: "grep", effect: deny }
19
+ - { action: shell, resource: "grep *", effect: deny }
20
+ - { action: shell, resource: "find", effect: deny }
21
+ - { action: shell, resource: "find *", effect: deny }
22
+ - { action: shell, resource: "fd", effect: deny }
23
+ - { action: shell, resource: "fd *", effect: deny }
24
+ - { action: shell, resource: "git grep", effect: deny }
25
+ - { action: shell, resource: "git grep *", effect: deny }
26
+ - { action: shell, resource: "git -C * grep", effect: deny }
27
+ - { action: shell, resource: "git -C * grep *", effect: deny }
28
+ - { action: shell, resource: "git * grep", effect: deny }
29
+ - { action: shell, resource: "git * grep *", effect: deny }
30
+ - { action: glob, resource: "*", effect: deny }
31
+ - { action: grep, resource: "*", effect: deny }
32
+ - { action: webfetch, resource: "*", effect: deny }
33
+ - { action: websearch, resource: "*", effect: deny }
34
+ - { action: skill, resource: "*", effect: deny }
38
35
  ---
39
36
 
40
37
  # Worker