@kisev/agentomatic 11.0.2 → 12.0.0-dev.113.g1bd7385234c6

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 +12 -7
  63. package/dist/assets/commands/skill-improve.md +0 -10
  64. package/dist/assets/commands/stopit.md +0 -10
package/dist/installer.js CHANGED
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { buildAgentProfilePlan, listAgentProfiles, validateBuiltAgentProfilePlan, } from "./agent-profiles.js";
7
- import { applyTransaction, archiveRoot, deploymentRoot, destination, LifecycleError, lifecycleRoot, migrateLegacyDeploymentNamespace, migrateLegacyNamespaces, readRegular, recoverTransaction, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
7
+ import { applyTransaction, archiveRoot, deploymentRoot, destination, LifecycleError, lifecycleRoot, migrateLegacyDeploymentNamespace, migrateLegacyNamespaces, readRegular, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
8
8
  import { CATALOG } from "./catalog.js";
9
9
  import { FIXED_AGENT_ROLES } from "./agent-profiles.js";
10
10
  import { readPackageVersion } from "./package-metadata.js";
@@ -15,8 +15,11 @@ const LEGACY_MANIFEST_NAME = ".skills-opencode-manifest.json";
15
15
  const LEGACY_PACKAGE_NAME = "@kisev/skills-opencode";
16
16
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
17
  const assetsRoot = resolve(packageRoot, "dist", "assets");
18
+ function migrationInventory() {
19
+ return JSON.parse(readFileSync(resolve(assetsRoot, "migration-inventory.json"), "utf8"));
20
+ }
18
21
  function retiredAssetPaths() {
19
- const inventory = JSON.parse(readFileSync(resolve(assetsRoot, "migration-inventory.json"), "utf8"));
22
+ const inventory = migrationInventory();
20
23
  return new Set([
21
24
  ...Object.keys(inventory.retired_command_hashes ?? {}),
22
25
  ...Object.keys(inventory.retired_plugin_hashes ?? {}),
@@ -33,6 +36,49 @@ export function defaultSelection() {
33
36
  core_activation: true,
34
37
  };
35
38
  }
39
+ const RENAME_CHAIN_LIMIT = 8;
40
+ function migrateSavedCommand(renamed, name) {
41
+ let current = name;
42
+ for (let depth = 0; depth < RENAME_CHAIN_LIMIT; depth += 1) {
43
+ const replacement = renamed[current];
44
+ if (replacement === undefined)
45
+ return current;
46
+ current = replacement;
47
+ }
48
+ return current;
49
+ }
50
+ export function savedSelection(manifest) {
51
+ const renamed = migrationInventory().renamed ?? {};
52
+ const requested = (manifest.commands ?? defaultSelection().commands).map((name) => migrateSavedCommand(renamed, name));
53
+ const commands = requested.filter((name) => SKILL_COMMANDS.includes(name));
54
+ const agents = (manifest.agents ?? FIXED_AGENT_ROLES).filter((role) => FIXED_AGENT_ROLES.includes(role));
55
+ const plugins = (manifest.plugins ?? ["rtk"]).filter((plugin) => SELECTABLE_PLUGINS.includes(plugin));
56
+ if (requested.join("\0") !== commands.join("\0"))
57
+ process.stderr.write(`Saved selection migrated to the current catalog: ${requested.join(", ")} -> ${commands.join(", ") || "none"}\n`);
58
+ return { commands, agents, plugins };
59
+ }
60
+ export async function installedSelection(scope, cwd = process.cwd(), home = homedir(), overrides = {}) {
61
+ const { manifest } = await currentManifest(deploymentRoot(scope, cwd, home));
62
+ return manifest
63
+ ? normalizeSelection({
64
+ ...savedSelection(manifest),
65
+ core_activation: manifest.core_activation,
66
+ ...overrides,
67
+ })
68
+ : null;
69
+ }
70
+ export async function coreSelectionMutation(root, enabled) {
71
+ const owned = await currentManifest(root);
72
+ if (!owned.manifest || !owned.raw || owned.manifest.core_activation === enabled)
73
+ return undefined;
74
+ return {
75
+ path: MANIFEST_NAME,
76
+ operation: "write",
77
+ content: Buffer.from(`${stable({ ...owned.manifest, core_activation: enabled })}\n`),
78
+ mode: 0o600,
79
+ expected: { sha256: sha256(owned.raw) },
80
+ };
81
+ }
36
82
  export function normalizeSelection(value = {}) {
37
83
  const allCommands = new Set(SKILL_COMMANDS);
38
84
  const commands = [...new Set(value.commands ?? defaultSelection().commands)].sort();
@@ -325,15 +371,13 @@ function asLegacy(manifest) {
325
371
  ? manifest
326
372
  : undefined;
327
373
  }
328
- async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection, provisionDependency = true) {
374
+ async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection, provisionDependency = true, profileChanges = []) {
329
375
  const root = deploymentRoot(scope, cwd, home);
330
376
  const owned = await currentManifest(root);
331
377
  const selection = normalizeSelection(requestedSelection ??
332
378
  (owned.manifest
333
379
  ? {
334
- commands: owned.manifest.commands,
335
- agents: owned.manifest.agents,
336
- plugins: owned.manifest.plugins,
380
+ ...savedSelection(owned.manifest),
337
381
  core_activation: owned.manifest.core_activation,
338
382
  }
339
383
  : undefined));
@@ -346,7 +390,7 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
346
390
  manifestSha256: sha256(owned.raw),
347
391
  }
348
392
  : undefined;
349
- const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord, selection.agents);
393
+ const profiles = await buildAgentProfilePlan({ action, changes: profileChanges }, scope, cwd, home, legacyRecord, selection.agents);
350
394
  const dependency = action === "install" && provisionDependency && selection.core_activation
351
395
  ? planDependency(scope, cwd, home)
352
396
  : undefined;
@@ -628,24 +672,23 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
628
672
  item.path.startsWith("plugins/")),
629
673
  };
630
674
  return {
631
- plan: { ...base },
675
+ plan: {
676
+ ...base,
677
+ digest: sha256(stable({ base, mutations, manifest: owned.raw, profiles: profiles.plan.digest })),
678
+ },
632
679
  mutations,
633
680
  expectedManifest: manifestContent,
634
681
  profiles,
635
682
  };
636
683
  }
637
- export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection, provisionDependency = true) {
638
- // Infrastructure normalization like stale-lock reclamation: idempotent,
639
- // preserves every legacy byte, and never touches user configuration.
640
- await migrateLegacyNamespaces(home);
684
+ export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection, provisionDependency = true, profileChanges = []) {
685
+ // Preview never migrates namespaces, creates locks, or recovers transactions.
641
686
  const stateRoot = lifecycleRoot(scope, cwd, home);
642
- const root = deploymentRoot(scope, cwd, home);
643
687
  try {
644
- return await withLifecycleLock(stateRoot, async () => {
645
- if (await recoverTransaction(root, stateRoot, [archiveRoot(scope, cwd, home)]))
646
- throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
647
- return (await build(action, scope, cwd, home, selection, provisionDependency)).plan;
648
- });
688
+ if (await readRegular(join(stateRoot, "transaction-journal.json")))
689
+ throw new InstallerError("recovery_required", "Run maintenance recover before a fresh preview");
690
+ return (await build(action, scope, cwd, home, selection, provisionDependency, profileChanges))
691
+ .plan;
649
692
  }
650
693
  catch (error) {
651
694
  if (error instanceof InstallerError)
@@ -655,7 +698,7 @@ export async function preview(action, scope, cwd = process.cwd(), home = homedir
655
698
  throw error;
656
699
  }
657
700
  }
658
- export async function apply(action, scope, cwd = process.cwd(), home = homedir(), options = {}, selection, provisionDependency = true) {
701
+ export async function apply(action, scope, cwd = process.cwd(), home = homedir(), options = {}, selection, provisionDependency = true, profileChanges = []) {
659
702
  const { dependencyRunner, ...transactionOptions } = options;
660
703
  const stateRoot = lifecycleRoot(scope, cwd, home);
661
704
  const root = deploymentRoot(scope, cwd, home);
@@ -663,9 +706,11 @@ export async function apply(action, scope, cwd = process.cwd(), home = homedir()
663
706
  try {
664
707
  return await withLifecycleLock(stateRoot, async () => {
665
708
  await migrateLegacyDeploymentNamespace(root);
666
- if (await recoverTransaction(root, stateRoot, [archiveRoot(scope, cwd, home)]))
667
- throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
668
- const built = await build(action, scope, cwd, home, selection, provisionDependency);
709
+ if (await readRegular(join(stateRoot, "transaction-journal.json")))
710
+ throw new InstallerError("recovery_required", "Run maintenance recover before a fresh preview");
711
+ const built = await build(action, scope, cwd, home, selection, provisionDependency, profileChanges);
712
+ if (options.expectedDigest && built.plan.digest !== options.expectedDigest)
713
+ throw new InstallerError("stale_plan", "Installation changed; request a fresh preview");
669
714
  if (built.plan.operations.some((item) => item.operation === "conflict" &&
670
715
  (item.reason === "unmanaged_file" ||
671
716
  item.reason === "v1.0.0_agent_ownership_mismatch" ||
@@ -689,7 +734,7 @@ export async function apply(action, scope, cwd = process.cwd(), home = homedir()
689
734
  inventory.drift.length ||
690
735
  inventory.profiles
691
736
  .filter((item) => item.ownership !== "user-owned")
692
- .some((item) => item.state !== "current"))) {
737
+ .some((item) => item.state !== "current" && item.state !== "not-installed"))) {
693
738
  throw new InstallerError("final_validation_failed", "Final installed agent inventory is invalid");
694
739
  }
695
740
  },
package/dist/jsonc.d.ts CHANGED
@@ -30,7 +30,23 @@ export type JsoncPath = Array<string | number>;
30
30
  export type JsoncEdit = {
31
31
  kind: "append-unique";
32
32
  path: JsoncPath;
33
- value: string;
33
+ value: unknown;
34
+ } | {
35
+ kind: "insert-before";
36
+ path: JsoncPath;
37
+ before: unknown;
38
+ value: unknown;
39
+ } | {
40
+ kind: "set-value";
41
+ path: JsoncPath;
42
+ value: unknown;
43
+ } | {
44
+ kind: "rename-key";
45
+ path: JsoncPath;
46
+ to: string;
47
+ } | {
48
+ kind: "remove-key";
49
+ path: JsoncPath;
34
50
  } | {
35
51
  kind: "replace-array-value";
36
52
  path: JsoncPath;
@@ -45,7 +61,7 @@ export type JsoncEdit = {
45
61
  path: JsoncPath;
46
62
  entries: Record<string, string>;
47
63
  };
48
- export type JsoncEditResult = "created" | "present" | "appended" | "replaced" | "widened";
64
+ export type JsoncEditResult = "created" | "present" | "appended" | "inserted" | "replaced" | "widened";
49
65
  export declare function nodeToValue(node: Node): unknown;
50
66
  export declare function parseJsonc(text: string): unknown;
51
67
  export declare function applyJsoncEdits(text: string, edits: readonly JsoncEdit[]): {
package/dist/jsonc.js CHANGED
@@ -143,6 +143,9 @@ function parseObject(parser) {
143
143
  parser.cursor += 1;
144
144
  trailingComma = true;
145
145
  }
146
+ else if (next && !(next.type === "punct" && next.raw === "}")) {
147
+ throw new JsoncError("invalid_jsonc", `Expected comma at offset ${next.start}`);
148
+ }
146
149
  }
147
150
  if (!closed)
148
151
  throw new JsoncError("invalid_jsonc", `Unterminated object at offset ${open.start}`);
@@ -180,6 +183,9 @@ function parseArray(parser) {
180
183
  parser.cursor += 1;
181
184
  trailingComma = true;
182
185
  }
186
+ else if (next && !(next.type === "punct" && next.raw === "]")) {
187
+ throw new JsoncError("invalid_jsonc", `Expected comma at offset ${next.start}`);
188
+ }
183
189
  }
184
190
  if (!closed)
185
191
  throw new JsoncError("invalid_jsonc", `Unterminated array at offset ${open.start}`);
@@ -200,7 +206,12 @@ export function nodeToValue(node) {
200
206
  return node.items.map(nodeToValue);
201
207
  const value = {};
202
208
  for (const key of node.propOrder)
203
- value[key] = nodeToValue(node.props.get(key));
209
+ Object.defineProperty(value, key, {
210
+ value: nodeToValue(node.props.get(key)),
211
+ writable: true,
212
+ configurable: true,
213
+ enumerable: true,
214
+ });
204
215
  return value;
205
216
  }
206
217
  function parseDocument(text) {
@@ -278,16 +289,83 @@ function requireObjectPath(text, root, path) {
278
289
  return { text: currentText, node };
279
290
  }
280
291
  function applyEdit(text, root, edit) {
292
+ if (edit.kind === "rename-key" || edit.kind === "remove-key") {
293
+ const key = edit.path.at(-1);
294
+ const parent = resolve(root, edit.path.slice(0, -1));
295
+ if (typeof key !== "string" || !parent || parent.kind !== "object")
296
+ throw new JsoncError("conflict", "Key edit requires an object parent");
297
+ const target = parent.props.get(key);
298
+ if (!target)
299
+ return { text, result: "present" };
300
+ const index = parent.propOrder.indexOf(key);
301
+ const previous = index ? parent.props.get(parent.propOrder[index - 1]) : undefined;
302
+ const tokens = tokenize(text).filter((token) => token.start >= (previous?.end ?? parent.start + 1) && token.end <= target.start);
303
+ const keyToken = tokens.find((token) => token.type === "string");
304
+ if (!keyToken)
305
+ throw new JsoncError("conflict", "Object key token is unavailable");
306
+ if (edit.kind === "rename-key") {
307
+ if (parent.props.has(edit.to))
308
+ throw new JsoncError("conflict", `Destination key already exists: ${edit.to}`);
309
+ return {
310
+ text: text.slice(0, keyToken.start) + JSON.stringify(edit.to) + text.slice(keyToken.end),
311
+ result: "replaced",
312
+ };
313
+ }
314
+ const following = tokenize(text).find((token) => token.start >= target.end && token.type !== "ws" && token.type !== "comment");
315
+ const start = following?.raw === "," || !previous ? keyToken.start : previous.end;
316
+ const end = following?.raw === "," ? following.end : target.end;
317
+ const comments = tokenize(text.slice(start, end))
318
+ .filter((token) => token.type === "comment")
319
+ .map((token) => `${token.raw}\n`)
320
+ .join("");
321
+ return { text: text.slice(0, start) + comments + text.slice(end), result: "replaced" };
322
+ }
323
+ if (edit.kind === "set-value") {
324
+ const target = resolve(root, edit.path);
325
+ if (!target)
326
+ return applyEdit(text, root, { ...edit, kind: "set-if-absent" });
327
+ if (JSON.stringify(nodeToValue(target)) === JSON.stringify(edit.value))
328
+ return { text, result: "present" };
329
+ const comments = tokenize(text.slice(target.start, target.end))
330
+ .filter((token) => token.type === "comment")
331
+ .map((token) => `${token.raw}\n${lineIndent(text, target.start)}`)
332
+ .join("");
333
+ return {
334
+ text: text.slice(0, target.start) +
335
+ comments +
336
+ serialize(edit.value, lineIndent(text, target.start)) +
337
+ text.slice(target.end),
338
+ result: "replaced",
339
+ };
340
+ }
281
341
  if (edit.kind === "append-unique") {
282
342
  const target = resolve(root, edit.path);
283
343
  if (!target || target.kind !== "array")
284
344
  throw new JsoncError("conflict", "append-unique target is missing or not an array");
285
- if (target.items.map(nodeToValue).includes(edit.value))
345
+ if (target.items
346
+ .map(nodeToValue)
347
+ .some((value) => JSON.stringify(value) === JSON.stringify(edit.value)))
286
348
  return { text, result: "present" };
287
349
  const insertion = target.empty ? JSON.stringify(edit.value) : `, ${JSON.stringify(edit.value)}`;
288
350
  const at = target.empty ? target.start + 1 : target.items.at(-1).end;
289
351
  return { text: text.slice(0, at) + insertion + text.slice(at), result: "appended" };
290
352
  }
353
+ if (edit.kind === "insert-before") {
354
+ const target = resolve(root, edit.path);
355
+ if (!target || target.kind !== "array")
356
+ throw new JsoncError("conflict", "insert-before target is missing or not an array");
357
+ const items = target.items.map(nodeToValue);
358
+ if (items.some((value) => JSON.stringify(value) === JSON.stringify(edit.value)))
359
+ return { text, result: "present" };
360
+ const anchor = items.findIndex((value) => JSON.stringify(value) === JSON.stringify(edit.before));
361
+ if (anchor === -1)
362
+ throw new JsoncError("conflict", "insert-before anchor item is missing from the array");
363
+ const node = target.items[anchor];
364
+ return {
365
+ text: text.slice(0, node.start) + `${JSON.stringify(edit.value)}, ` + text.slice(node.start),
366
+ result: "inserted",
367
+ };
368
+ }
291
369
  if (edit.kind === "replace-array-value") {
292
370
  const target = resolve(root, edit.path);
293
371
  if (!target || target.kind !== "array")
@@ -369,12 +447,38 @@ function at(value, path) {
369
447
  : undefined, value);
370
448
  }
371
449
  function verifyEdit(before, after, edit) {
450
+ if (edit.kind === "set-value") {
451
+ if (JSON.stringify(at(after, edit.path)) !== JSON.stringify(edit.value))
452
+ throw new JsoncError("merge_validation_failed", "set-value postcondition failed");
453
+ return;
454
+ }
455
+ if (edit.kind === "rename-key" || edit.kind === "remove-key") {
456
+ if (at(after, edit.path) !== undefined)
457
+ throw new JsoncError("merge_validation_failed", "Old key remains after key edit");
458
+ if (edit.kind === "rename-key" &&
459
+ JSON.stringify(at(before, edit.path)) !==
460
+ JSON.stringify(at(after, [...edit.path.slice(0, -1), edit.to])))
461
+ throw new JsoncError("merge_validation_failed", "Renamed key lost its value");
462
+ return;
463
+ }
372
464
  if (edit.kind === "append-unique") {
373
465
  const target = at(after, edit.path);
374
- if (!Array.isArray(target) || !target.includes(edit.value))
466
+ if (!Array.isArray(target) ||
467
+ !target.some((value) => JSON.stringify(value) === JSON.stringify(edit.value)))
375
468
  throw new JsoncError("merge_validation_failed", "append-unique postcondition failed");
376
469
  return;
377
470
  }
471
+ if (edit.kind === "insert-before") {
472
+ const target = at(after, edit.path);
473
+ if (!Array.isArray(target))
474
+ throw new JsoncError("merge_validation_failed", "insert-before postcondition failed");
475
+ const serialized = target.map((value) => JSON.stringify(value));
476
+ const value = serialized.indexOf(JSON.stringify(edit.value));
477
+ const anchor = serialized.indexOf(JSON.stringify(edit.before));
478
+ if (value === -1 || anchor === -1 || value > anchor)
479
+ throw new JsoncError("merge_validation_failed", "insert-before postcondition failed");
480
+ return;
481
+ }
378
482
  if (edit.kind === "replace-array-value") {
379
483
  const beforeTarget = at(before, edit.path);
380
484
  const afterTarget = at(after, edit.path);
@@ -419,12 +523,12 @@ function verifyEdit(before, after, edit) {
419
523
  throw new JsoncError("merge_validation_failed", "widened map lost the original scalar value");
420
524
  }
421
525
  export function applyJsoncEdits(text, edits) {
422
- const before = parseJsonc(text);
526
+ parseJsonc(text);
423
527
  let current = text;
424
528
  const results = [];
425
529
  for (const edit of edits) {
426
530
  const applied = applyEdit(current, parseDocument(current), edit);
427
- verifyEdit(before, parseJsonc(applied.text), edit);
531
+ verifyEdit(parseJsonc(current), parseJsonc(applied.text), edit);
428
532
  results.push(applied.result);
429
533
  current = applied.text;
430
534
  }
@@ -0,0 +1,11 @@
1
+ import { type JsoncEdit } from "./jsonc.js";
2
+ export type PermissionRule = {
3
+ action: string;
4
+ resource: string;
5
+ effect: "allow" | "ask" | "deny";
6
+ };
7
+ export declare function permissionEdits(config: Record<string, unknown>, additions: PermissionRule[]): JsoncEdit[];
8
+ export declare function matchesWildcard(pattern: string, value: string): boolean;
9
+ export declare function patternsOverlap(left: string, right: string): boolean;
10
+ export declare function corePluginRemovalEdits(config: Record<string, unknown>): JsoncEdit[];
11
+ export declare function corePluginEdits(config: Record<string, unknown>): JsoncEdit[];
@@ -0,0 +1,237 @@
1
+ import { JsoncError } from "./jsonc.js";
2
+ import { requirePackageVersion } from "./package-metadata.js";
3
+ const effects = new Set(["allow", "ask", "deny"]);
4
+ const aliases = {
5
+ bash: "shell",
6
+ task: "subagent",
7
+ write: "edit",
8
+ patch: "edit",
9
+ };
10
+ function object(value) {
11
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
12
+ }
13
+ function legacyPermissions(value) {
14
+ if (typeof value === "string" && effects.has(value))
15
+ return [{ action: "*", resource: "*", effect: value }];
16
+ if (!object(value))
17
+ throw new JsoncError("conflict", "Legacy permission must be an effect or a tool map");
18
+ return Object.entries(value).flatMap(([name, entry]) => {
19
+ if (name === "lsp" || name === "doom_loop")
20
+ throw new JsoncError("conflict", `Permission action has no V2 equivalent: ${name}`);
21
+ const action = aliases[name] ?? name;
22
+ const resources = typeof entry === "string" ? { "*": entry } : entry;
23
+ if (!object(resources))
24
+ throw new JsoncError("conflict", `Invalid permission map: ${name}`);
25
+ return Object.entries(resources).map(([resource, effect]) => {
26
+ if (typeof effect !== "string" || !effects.has(effect))
27
+ throw new JsoncError("conflict", `Invalid permission effect: ${name}`);
28
+ return { action, resource, effect: effect };
29
+ });
30
+ });
31
+ }
32
+ export function permissionEdits(config, additions) {
33
+ const edits = [];
34
+ let rules = [];
35
+ if ("permissions" in config) {
36
+ if (!Array.isArray(config.permissions) ||
37
+ !config.permissions.every((rule) => object(rule) &&
38
+ typeof rule.action === "string" &&
39
+ typeof rule.resource === "string" &&
40
+ typeof rule.effect === "string" &&
41
+ effects.has(rule.effect)))
42
+ throw new JsoncError("conflict", "V2 permissions must be an ordered array of action/resource/effect rules");
43
+ rules = config.permissions;
44
+ }
45
+ let tools = [];
46
+ if ("tools" in config) {
47
+ if (!object(config.tools) ||
48
+ !Object.values(config.tools).every((value) => typeof value === "boolean"))
49
+ throw new JsoncError("conflict", "Legacy tools must be a boolean map");
50
+ tools = Object.entries(config.tools).map(([name, enabled]) => ({
51
+ action: aliases[name] ?? name,
52
+ resource: "*",
53
+ effect: enabled ? "allow" : "deny",
54
+ }));
55
+ }
56
+ const legacyKeys = ["tools", "permission"].filter((key) => key in config);
57
+ if (legacyKeys.length) {
58
+ // V2 normalizes tools first, legacy permission next, and native rules last.
59
+ rules = [
60
+ ...tools,
61
+ ...("permission" in config ? legacyPermissions(config.permission) : []),
62
+ ...rules,
63
+ ];
64
+ const renamed = "permissions" in config ? undefined : legacyKeys[0];
65
+ for (const key of legacyKeys)
66
+ edits.push(key === renamed
67
+ ? { kind: "rename-key", path: [key], to: "permissions" }
68
+ : { kind: "remove-key", path: [key] });
69
+ edits.push({ kind: "set-value", path: ["permissions"], value: rules });
70
+ }
71
+ else if (!("permissions" in config))
72
+ edits.push({ kind: "set-if-absent", path: ["permissions"], value: [] });
73
+ // Rules that existed before this preset run. V2 resolves by the last
74
+ // matching rule, so a new allow that can overlap a pre-existing deny is
75
+ // inserted before that deny instead of being appended after it: the user's
76
+ // prohibition keeps priority for every resource it matches. An allow that
77
+ // overlaps a deny added earlier in the same batch is a deliberate exception
78
+ // to that deny (the OpenCode "specific rule follows the broad rule" order)
79
+ // and is appended, and denies are always appended because a later deny can
80
+ // only strengthen the result. Batch membership is tracked by object
81
+ // identity: insertions shift array positions, so a length boundary would
82
+ // misclassify shifted pre-existing rules as batch additions.
83
+ const preexisting = rules;
84
+ const batchRules = new Set();
85
+ for (const rule of additions) {
86
+ const exact = rules.reduce((last, existing, index) => existing.action === rule.action && existing.resource === rule.resource ? index : last, -1);
87
+ if (exact >= 0) {
88
+ if (rules[exact].effect !== rule.effect ||
89
+ rules
90
+ .slice(exact + 1)
91
+ .some((later) => matchesWildcard(later.action, rule.action) &&
92
+ matchesWildcard(later.resource, rule.resource) &&
93
+ later.effect !== rule.effect))
94
+ throw new JsoncError("conflict", `Existing permission conflicts with preset: ${rule.action} ${rule.resource}`);
95
+ continue;
96
+ }
97
+ const overlapsDeny = (candidate) => candidate.effect === "deny" && rulesOverlap(candidate, rule);
98
+ const anchor = rule.effect === "allow" && ![...batchRules].some((added) => overlapsDeny(added))
99
+ ? preexisting.find((existing) => overlapsDeny(existing))
100
+ : undefined;
101
+ if (anchor !== undefined) {
102
+ edits.push({ kind: "insert-before", path: ["permissions"], before: anchor, value: rule });
103
+ const position = rules.indexOf(anchor);
104
+ rules = [...rules.slice(0, position), rule, ...rules.slice(position)];
105
+ }
106
+ else {
107
+ edits.push({ kind: "append-unique", path: ["permissions"], value: rule });
108
+ rules = [...rules, rule];
109
+ }
110
+ batchRules.add(rule);
111
+ }
112
+ return edits;
113
+ }
114
+ export function matchesWildcard(pattern, value) {
115
+ return new RegExp(`^${pattern
116
+ .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
117
+ .replace(/\\\*/g, ".*")
118
+ .replace(/\\\?/g, ".")}$`).test(value);
119
+ }
120
+ function patternUnits(pattern) {
121
+ return [...pattern].map((character) => character === "*"
122
+ ? { kind: "star" }
123
+ : character === "?"
124
+ ? { kind: "any" }
125
+ : { kind: "char", char: character });
126
+ }
127
+ export function patternsOverlap(left, right) {
128
+ const a = patternUnits(left);
129
+ const b = patternUnits(right);
130
+ const memo = new Map();
131
+ const overlap = (i, j) => {
132
+ if (i === a.length && j === b.length)
133
+ return true;
134
+ // A trailing wildcard run can still match the empty string, so exhaustion
135
+ // is compatible only with an all-star remainder on the other side.
136
+ if (i === a.length)
137
+ return b.slice(j).every((unit) => unit.kind === "star");
138
+ if (j === b.length)
139
+ return a.slice(i).every((unit) => unit.kind === "star");
140
+ const key = `${i}:${j}`;
141
+ const cached = memo.get(key);
142
+ if (cached !== undefined)
143
+ return cached;
144
+ const unit = a[i];
145
+ const other = b[j];
146
+ let result;
147
+ if (unit.kind === "star" || other.kind === "star")
148
+ // A star consumes nothing from the other pattern or absorbs one unit of it.
149
+ result = overlap(i + 1, j) || overlap(i, j + 1);
150
+ else if (unit.kind === "any" || other.kind === "any")
151
+ result = overlap(i + 1, j + 1);
152
+ else
153
+ result = unit.char === other.char && overlap(i + 1, j + 1);
154
+ memo.set(key, result);
155
+ return result;
156
+ };
157
+ return overlap(0, 0);
158
+ }
159
+ function rulesOverlap(existing, candidate) {
160
+ return (patternsOverlap(existing.action, candidate.action) &&
161
+ patternsOverlap(existing.resource, candidate.resource));
162
+ }
163
+ function plugins(value, legacy) {
164
+ if (!Array.isArray(value))
165
+ throw new JsoncError("conflict", "Plugin configuration must be an array");
166
+ return value.map((entry) => {
167
+ if (typeof entry === "string")
168
+ return entry === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry;
169
+ if (legacy &&
170
+ Array.isArray(entry) &&
171
+ entry.length === 2 &&
172
+ typeof entry[0] === "string" &&
173
+ object(entry[1]))
174
+ return {
175
+ package: entry[0] === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry[0],
176
+ options: entry[1],
177
+ };
178
+ if (object(entry) && typeof entry.package === "string")
179
+ return {
180
+ ...entry,
181
+ package: entry.package === "@kisev/skills-opencode" ? "@kisev/agentomatic" : entry.package,
182
+ };
183
+ throw new JsoncError("conflict", "Invalid plugin entry; use a package string or V2 package/options object");
184
+ });
185
+ }
186
+ const OUR_PACKAGE = /^@kisev\/(?:agentomatic|skills-opencode)(?:@|$)/;
187
+ function ourEntry(entry) {
188
+ if (typeof entry === "string")
189
+ return OUR_PACKAGE.test(entry);
190
+ return object(entry) && typeof entry.package === "string" && OUR_PACKAGE.test(entry.package);
191
+ }
192
+ export function corePluginRemovalEdits(config) {
193
+ return ["plugin", "plugins"].flatMap((key) => {
194
+ if (!(key in config))
195
+ return [];
196
+ plugins(config[key], key === "plugin");
197
+ const entries = config[key];
198
+ const remaining = entries.filter((entry) => !ourEntry(Array.isArray(entry) ? entry[0] : entry));
199
+ return remaining.length === entries.length
200
+ ? []
201
+ : [{ kind: "set-value", path: [key], value: remaining }];
202
+ });
203
+ }
204
+ export function corePluginEdits(config) {
205
+ const edits = [
206
+ { kind: "set-if-absent", path: ["$schema"], value: "https://opencode.ai/config.json" },
207
+ ];
208
+ // V2 resolves a bare package name through the registry "latest" dist-tag, which
209
+ // can differ from the installed build; pin the exact running version instead.
210
+ const pinned = `@kisev/agentomatic@${requirePackageVersion()}`;
211
+ const native = "plugins" in config ? plugins(config.plugins, false) : undefined;
212
+ const legacy = "plugin" in config ? plugins(config.plugin, true) : undefined;
213
+ if (native && legacy) {
214
+ if (JSON.stringify(native) !== JSON.stringify(legacy))
215
+ throw new JsoncError("conflict", "Conflicting plugin and plugins sections; migrate explicitly before config setup");
216
+ edits.push({ kind: "remove-key", path: ["plugin"] });
217
+ }
218
+ else if (legacy)
219
+ edits.push({ kind: "rename-key", path: ["plugin"], to: "plugins" });
220
+ const normalized = [];
221
+ let pinnedPresent = false;
222
+ for (const entry of native ?? legacy ?? []) {
223
+ if (!ourEntry(entry)) {
224
+ normalized.push(entry);
225
+ continue;
226
+ }
227
+ if (pinnedPresent)
228
+ continue;
229
+ normalized.push(object(entry) ? { ...entry, package: pinned } : pinned);
230
+ pinnedPresent = true;
231
+ }
232
+ const entries = normalized;
233
+ if (!pinnedPresent)
234
+ entries.push(pinned);
235
+ edits.push({ kind: "set-value", path: ["plugins"], value: entries });
236
+ return edits;
237
+ }
@@ -0,0 +1,25 @@
1
+ import type { Plugin } from "@opencode/plugin";
2
+ export type CodeSimplifyLevel = "off" | "lite" | "full" | "ultra";
3
+ export declare const CODE_SIMPLIFY_ROLES: readonly ["manager", "architect", "mapper", "worker", "review", "critic"];
4
+ export type CodeSimplifyRole = (typeof CODE_SIMPLIFY_ROLES)[number];
5
+ export type CodeSimplifyOptions = {
6
+ level?: CodeSimplifyLevel;
7
+ /** Agent roles that receive the injection; every role when absent. */
8
+ scope?: readonly CodeSimplifyRole[];
9
+ };
10
+ export declare function codeSimplify(options?: CodeSimplifyOptions): {
11
+ context?: undefined;
12
+ } | {
13
+ context: (input: {
14
+ agent?: string;
15
+ system: Array<{
16
+ type: "text";
17
+ text: string;
18
+ }>;
19
+ }) => Promise<void>;
20
+ };
21
+ declare const _default: {
22
+ id: string;
23
+ setup(ctx: Plugin.Context): Promise<void>;
24
+ };
25
+ export default _default;