@kisev/skills-opencode 2.1.0 → 2.2.1

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 (47) hide show
  1. package/README.md +6 -5
  2. package/README.ru.md +5 -5
  3. package/dist/assets/agents/mapper.md +10 -1
  4. package/dist/assets/agents/worker.md +12 -1
  5. package/dist/assets/commands/agents-md.md +1 -1
  6. package/dist/assets/commands/askme.md +1 -1
  7. package/dist/assets/commands/ast-grep.md +1 -1
  8. package/dist/assets/commands/code-explain.md +1 -1
  9. package/dist/assets/commands/code-review.md +1 -1
  10. package/dist/assets/commands/commit-msg.md +1 -1
  11. package/dist/assets/commands/docs-prepare.md +1 -1
  12. package/dist/assets/commands/docs-review.md +1 -1
  13. package/dist/assets/commands/doit.md +1 -1
  14. package/dist/assets/commands/goal.md +1 -1
  15. package/dist/assets/commands/humanize.md +1 -1
  16. package/dist/assets/commands/lsp-report.md +1 -1
  17. package/dist/assets/commands/mattermost.md +1 -1
  18. package/dist/assets/commands/mr-prepare.md +1 -1
  19. package/dist/assets/commands/release-prepare.md +1 -1
  20. package/dist/assets/commands/release-review.md +1 -1
  21. package/dist/assets/commands/rtk.md +1 -1
  22. package/dist/assets/commands/skill-improve.md +1 -1
  23. package/dist/assets/commands/slides-prompts-prepare.md +1 -1
  24. package/dist/assets/commands/spec-manage.md +1 -1
  25. package/dist/assets/commands/stopit.md +1 -1
  26. package/dist/assets/commands/summary.md +1 -1
  27. package/dist/assets/commands/task-prepare.md +1 -1
  28. package/dist/assets/commands/task-review.md +1 -1
  29. package/dist/assets/commands/task-triage.md +1 -1
  30. package/dist/assets/commands/team-retro.md +1 -1
  31. package/dist/assets/commands/team-roadmap.md +1 -1
  32. package/dist/assets/commands/team-sprint-close.md +1 -1
  33. package/dist/assets/commands/team-sprint-start.md +1 -1
  34. package/dist/catalog.d.ts +1 -1
  35. package/dist/catalog.js +1 -1
  36. package/dist/cli.js +101 -23
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +1 -1
  39. package/dist/installer.js +246 -45
  40. package/dist/plugins/rtk.js +35 -13
  41. package/dist/plugins/rules-injector.d.ts +1 -1
  42. package/dist/plugins/rules-injector.js +61 -26
  43. package/dist/plugins/zed-bell.js +4 -2
  44. package/dist/registry.js +68 -14
  45. package/dist/routing.js +34 -2
  46. package/dist/runtime/state.js +6 -2
  47. package/package.json +1 -1
package/dist/installer.js CHANGED
@@ -44,7 +44,8 @@ export function normalizeSelection(value = {}) {
44
44
  commands,
45
45
  agents: agents.sort(),
46
46
  plugins: plugins.sort(),
47
- core_activation: Boolean(value.core_activation ?? (commands.some((name) => PACKAGE_COMMANDS.includes(name)) || agents.length)),
47
+ core_activation: Boolean(value.core_activation ??
48
+ (commands.some((name) => PACKAGE_COMMANDS.includes(name)) || agents.length)),
48
49
  };
49
50
  }
50
51
  export class InstallerError extends LifecycleError {
@@ -112,27 +113,57 @@ function parseManifest(raw, path) {
112
113
  package_version: manifest.version,
113
114
  version: manifest.version,
114
115
  scope: "global",
115
- commands: Object.keys(files).filter((path) => path.startsWith("commands/")).map((path) => path.slice(9, -3)).sort(),
116
+ commands: Object.keys(files)
117
+ .filter((path) => path.startsWith("commands/"))
118
+ .map((path) => path.slice(9, -3))
119
+ .sort(),
116
120
  agents: [...FIXED_AGENT_ROLES].filter((role) => `agents/${role}.md` in files),
117
- plugins: Object.keys(files).filter((path) => path.startsWith("plugins/")).map((path) => path.slice(8, -3)).filter((name) => SELECTABLE_PLUGINS.includes(name)),
121
+ plugins: Object.keys(files)
122
+ .filter((path) => path.startsWith("plugins/"))
123
+ .map((path) => path.slice(8, -3))
124
+ .filter((name) => SELECTABLE_PLUGINS.includes(name)),
118
125
  core_activation: true,
119
126
  files,
120
127
  };
121
128
  }
122
- if (manifest.schema_version !== 2 || manifest.package !== PACKAGE_NAME || typeof manifest.package_version !== "string" || (manifest.scope !== "global" && manifest.scope !== "project") || !Array.isArray(manifest.commands) || !Array.isArray(manifest.agents) || !Array.isArray(manifest.plugins) || typeof manifest.core_activation !== "boolean" || !manifest.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) {
129
+ if (manifest.schema_version !== 2 ||
130
+ manifest.package !== PACKAGE_NAME ||
131
+ typeof manifest.package_version !== "string" ||
132
+ (manifest.scope !== "global" && manifest.scope !== "project") ||
133
+ !Array.isArray(manifest.commands) ||
134
+ !Array.isArray(manifest.agents) ||
135
+ !Array.isArray(manifest.plugins) ||
136
+ typeof manifest.core_activation !== "boolean" ||
137
+ !manifest.files ||
138
+ typeof manifest.files !== "object" ||
139
+ Array.isArray(manifest.files)) {
123
140
  throw new InstallerError("invalid_manifest", `Ownership manifest has an unexpected format: ${path}`);
124
141
  }
125
142
  const files = Object.fromEntries(Object.entries(manifest.files).map(([relativePath, record]) => {
126
143
  destination("/", relativePath);
127
- if (!record || typeof record !== "object" || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
144
+ if (!record ||
145
+ typeof record !== "object" ||
146
+ typeof record.sha256 !== "string" ||
147
+ !/^[a-f0-9]{64}$/.test(record.sha256)) {
128
148
  throw new InstallerError("invalid_manifest", `Ownership manifest has an invalid record: ${relativePath}`);
129
149
  }
130
150
  const existing = record;
131
- return [relativePath, {
151
+ return [
152
+ relativePath,
153
+ {
132
154
  sha256: existing.sha256,
133
155
  mode: Number.isInteger(existing.mode) ? existing.mode : 0o644,
134
- kind: ["command", "agent", "plugin", "state"].includes(String(existing.kind)) ? existing.kind : relativePath.startsWith("commands/") ? "command" : relativePath.startsWith("plugins/") ? "plugin" : relativePath.startsWith("agents/") ? "agent" : "state",
135
- }];
156
+ kind: ["command", "agent", "plugin", "state"].includes(String(existing.kind))
157
+ ? existing.kind
158
+ : relativePath.startsWith("commands/")
159
+ ? "command"
160
+ : relativePath.startsWith("plugins/")
161
+ ? "plugin"
162
+ : relativePath.startsWith("agents/")
163
+ ? "agent"
164
+ : "state",
165
+ },
166
+ ];
136
167
  }));
137
168
  return { ...manifest, files };
138
169
  }
@@ -150,14 +181,16 @@ export async function archiveMutations(candidates, scope, cwd, home, sourceVersi
150
181
  return undefined;
151
182
  throw new InstallerError("unsafe_path", "Archive root is inaccessible");
152
183
  });
153
- if (rootInfo && (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || (rootInfo.mode & 0o077) !== 0))
184
+ if (rootInfo &&
185
+ (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || (rootInfo.mode & 0o077) !== 0))
154
186
  throw new InstallerError("unsafe_path", "Archive root must be a private directory");
155
187
  const objectsInfo = await lstat(join(root, "objects")).catch((error) => {
156
188
  if (error.code === "ENOENT")
157
189
  return undefined;
158
190
  throw new InstallerError("unsafe_path", "Archive object store is inaccessible");
159
191
  });
160
- if (objectsInfo && (!objectsInfo.isDirectory() || objectsInfo.isSymbolicLink() || (objectsInfo.mode & 0o077) !== 0))
192
+ if (objectsInfo &&
193
+ (!objectsInfo.isDirectory() || objectsInfo.isSymbolicLink() || (objectsInfo.mode & 0o077) !== 0))
161
194
  throw new InstallerError("unsafe_path", "Archive object store must be private");
162
195
  const indexPath = destination(root, "index.json");
163
196
  const existingRaw = await readRegular(indexPath);
@@ -218,7 +251,14 @@ export async function archiveMutations(candidates, scope, cwd, home, sourceVersi
218
251
  }
219
252
  const next = Buffer.from(`${stable({ schema_version: 1, version: packageVersion(), entries: entries.sort((a, b) => String(a.original_path).localeCompare(String(b.original_path)) || String(a.digest).localeCompare(String(b.digest))) })}\n`);
220
253
  if (!existingRaw || !existingRaw.equals(next)) {
221
- mutations.push({ root, path: "index.json", operation: "write", content: next, mode: 0o600, expected: existingRaw ? { sha256: sha256(existingRaw) } : { absent: true } });
254
+ mutations.push({
255
+ root,
256
+ path: "index.json",
257
+ operation: "write",
258
+ content: next,
259
+ mode: 0o600,
260
+ expected: existingRaw ? { sha256: sha256(existingRaw) } : { absent: true },
261
+ });
222
262
  }
223
263
  return mutations;
224
264
  }
@@ -263,7 +303,9 @@ async function validateGenericDeployment(root, action, expectedAssets, plannedOp
263
303
  }
264
304
  }
265
305
  function asLegacy(manifest) {
266
- return manifest?.version === "1.0.0" ? manifest : undefined;
306
+ return manifest?.version === "1.0.0"
307
+ ? manifest
308
+ : undefined;
267
309
  }
268
310
  async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection) {
269
311
  const root = deploymentRoot(scope, cwd, home);
@@ -280,10 +322,18 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
280
322
  const bundled = await assets(selection);
281
323
  const retiredPaths = retiredAssetPaths();
282
324
  const legacyRecord = owned.manifest && owned.raw && asLegacy(owned.manifest)
283
- ? { manifest: asLegacy(owned.manifest), manifestPath: destination(root, MANIFEST_NAME), manifestSha256: sha256(owned.raw) }
325
+ ? {
326
+ manifest: asLegacy(owned.manifest),
327
+ manifestPath: destination(root, MANIFEST_NAME),
328
+ manifestSha256: sha256(owned.raw),
329
+ }
284
330
  : undefined;
285
331
  const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord, selection.agents);
286
- const operations = profiles.plan.operations.map((item) => ({ path: item.path, operation: item.operation, reason: item.reason }));
332
+ const operations = profiles.plan.operations.map((item) => ({
333
+ path: item.path,
334
+ operation: item.operation,
335
+ reason: item.reason,
336
+ }));
287
337
  const mutations = [...profiles.mutations];
288
338
  const archiveCandidates = [];
289
339
  const desiredFiles = {};
@@ -298,13 +348,29 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
298
348
  const record = owned.manifest?.files[asset.relativePath];
299
349
  if (!current) {
300
350
  operations.push({ path: asset.relativePath, operation: "create", sha256: asset.sha256 });
301
- mutations.push({ path: asset.relativePath, operation: "write", content: asset.content, mode: asset.mode, expected: { absent: true } });
351
+ mutations.push({
352
+ path: asset.relativePath,
353
+ operation: "write",
354
+ content: asset.content,
355
+ mode: asset.mode,
356
+ expected: { absent: true },
357
+ });
302
358
  }
303
359
  else if (!record) {
304
- operations.push({ path: asset.relativePath, operation: "conflict", reason: "unmanaged_file", sha256: sha256(current) });
360
+ operations.push({
361
+ path: asset.relativePath,
362
+ operation: "conflict",
363
+ reason: "unmanaged_file",
364
+ sha256: sha256(current),
365
+ });
305
366
  }
306
367
  else if (sha256(current) !== record.sha256) {
307
- operations.push({ path: asset.relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
368
+ operations.push({
369
+ path: asset.relativePath,
370
+ operation: "conflict",
371
+ reason: "managed_file_changed",
372
+ sha256: sha256(current),
373
+ });
308
374
  }
309
375
  else if (current.equals(asset.content) &&
310
376
  ((await lstat(destination(root, asset.relativePath))).mode & 0o777) === asset.mode) {
@@ -312,7 +378,13 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
312
378
  }
313
379
  else {
314
380
  operations.push({ path: asset.relativePath, operation: "update", sha256: asset.sha256 });
315
- mutations.push({ path: asset.relativePath, operation: "write", content: asset.content, mode: asset.mode, expected: { sha256: record.sha256 } });
381
+ mutations.push({
382
+ path: asset.relativePath,
383
+ operation: "write",
384
+ content: asset.content,
385
+ mode: asset.mode,
386
+ expected: { sha256: record.sha256 },
387
+ });
316
388
  }
317
389
  }
318
390
  const active = new Set(bundled.map((asset) => asset.relativePath));
@@ -325,28 +397,72 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
325
397
  operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
326
398
  }
327
399
  else if (sha256(current) !== record.sha256) {
328
- operations.push({ path: relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
400
+ operations.push({
401
+ path: relativePath,
402
+ operation: "conflict",
403
+ reason: "managed_file_changed",
404
+ sha256: sha256(current),
405
+ });
329
406
  }
330
407
  else {
331
- operations.push({ path: relativePath, operation: "archive-pending", reason: "archive lifecycle is pending", sha256: record.sha256 });
332
- archiveCandidates.push({ path: relativePath, record, content: current, reason: "retired managed asset", kind: record.kind });
333
- mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
408
+ operations.push({
409
+ path: relativePath,
410
+ operation: "archive-pending",
411
+ reason: "archive lifecycle is pending",
412
+ sha256: record.sha256,
413
+ });
414
+ archiveCandidates.push({
415
+ path: relativePath,
416
+ record,
417
+ content: current,
418
+ reason: "retired managed asset",
419
+ kind: record.kind,
420
+ });
421
+ mutations.push({
422
+ path: relativePath,
423
+ operation: "remove",
424
+ expected: { sha256: record.sha256 },
425
+ });
334
426
  }
335
427
  continue;
336
428
  }
337
429
  if (relativePath.startsWith("agents/")) {
338
- operations.push({ path: relativePath, operation: "conflict", reason: "v1.0.0_agent_ownership_mismatch" });
430
+ operations.push({
431
+ path: relativePath,
432
+ operation: "conflict",
433
+ reason: "v1.0.0_agent_ownership_mismatch",
434
+ });
339
435
  continue;
340
436
  }
341
437
  if (!current)
342
438
  operations.push({ path: relativePath, operation: "missing" });
343
439
  else if (sha256(current) === record.sha256) {
344
- operations.push({ path: relativePath, operation: "archive-pending", reason: "stale managed asset is archived", sha256: record.sha256 });
345
- archiveCandidates.push({ path: relativePath, record, content: current, reason: "stale managed asset", kind: record.kind });
346
- mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
440
+ operations.push({
441
+ path: relativePath,
442
+ operation: "archive-pending",
443
+ reason: "stale managed asset is archived",
444
+ sha256: record.sha256,
445
+ });
446
+ archiveCandidates.push({
447
+ path: relativePath,
448
+ record,
449
+ content: current,
450
+ reason: "stale managed asset",
451
+ kind: record.kind,
452
+ });
453
+ mutations.push({
454
+ path: relativePath,
455
+ operation: "remove",
456
+ expected: { sha256: record.sha256 },
457
+ });
347
458
  }
348
459
  else
349
- operations.push({ path: relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
460
+ operations.push({
461
+ path: relativePath,
462
+ operation: "conflict",
463
+ reason: "managed_file_changed",
464
+ sha256: sha256(current),
465
+ });
350
466
  }
351
467
  }
352
468
  else {
@@ -357,28 +473,68 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
357
473
  operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
358
474
  }
359
475
  else if (sha256(current) !== record.sha256) {
360
- operations.push({ path: relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
476
+ operations.push({
477
+ path: relativePath,
478
+ operation: "conflict",
479
+ reason: "managed_file_changed",
480
+ sha256: sha256(current),
481
+ });
361
482
  }
362
483
  else {
363
- operations.push({ path: relativePath, operation: "archive-pending", reason: "archive lifecycle is pending", sha256: record.sha256 });
364
- archiveCandidates.push({ path: relativePath, record, content: current, reason: "retired managed asset", kind: record.kind });
365
- mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
484
+ operations.push({
485
+ path: relativePath,
486
+ operation: "archive-pending",
487
+ reason: "archive lifecycle is pending",
488
+ sha256: record.sha256,
489
+ });
490
+ archiveCandidates.push({
491
+ path: relativePath,
492
+ record,
493
+ content: current,
494
+ reason: "retired managed asset",
495
+ kind: record.kind,
496
+ });
497
+ mutations.push({
498
+ path: relativePath,
499
+ operation: "remove",
500
+ expected: { sha256: record.sha256 },
501
+ });
366
502
  }
367
503
  }
368
504
  else if (!current)
369
505
  operations.push({ path: relativePath, operation: "missing" });
370
506
  else if (sha256(current) === record.sha256) {
371
- operations.push({ path: relativePath, operation: "archive-pending", reason: "stale managed asset is archived", sha256: record.sha256 });
372
- archiveCandidates.push({ path: relativePath, record, content: current, reason: "stale managed asset", kind: record.kind });
373
- mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
507
+ operations.push({
508
+ path: relativePath,
509
+ operation: "archive-pending",
510
+ reason: "stale managed asset is archived",
511
+ sha256: record.sha256,
512
+ });
513
+ archiveCandidates.push({
514
+ path: relativePath,
515
+ record,
516
+ content: current,
517
+ reason: "stale managed asset",
518
+ kind: record.kind,
519
+ });
520
+ mutations.push({
521
+ path: relativePath,
522
+ operation: "remove",
523
+ expected: { sha256: record.sha256 },
524
+ });
374
525
  }
375
526
  else {
376
- operations.push({ path: relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
527
+ operations.push({
528
+ path: relativePath,
529
+ operation: "conflict",
530
+ reason: "managed_file_changed",
531
+ sha256: sha256(current),
532
+ });
377
533
  desiredFiles[relativePath] = record;
378
534
  }
379
535
  }
380
536
  }
381
- mutations.push(...await archiveMutations(archiveCandidates, scope, cwd, home, owned.manifest?.package_version ?? "1.0.0"));
537
+ mutations.push(...(await archiveMutations(archiveCandidates, scope, cwd, home, owned.manifest?.package_version ?? "1.0.0")));
382
538
  const nextManifest = action === "install" || Object.keys(desiredFiles).length
383
539
  ? {
384
540
  schema_version: 2,
@@ -395,15 +551,46 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
395
551
  : undefined;
396
552
  const manifestContent = nextManifest ? Buffer.from(`${stable(nextManifest)}\n`) : undefined;
397
553
  if (manifestContent && (!owned.raw || !owned.raw.equals(manifestContent))) {
398
- operations.push({ path: MANIFEST_NAME, operation: owned.raw ? "update" : "create", reason: "generic installer ownership" });
399
- mutations.push({ path: MANIFEST_NAME, operation: "write", content: manifestContent, mode: 0o600, expected: owned.raw ? { sha256: sha256(owned.raw) } : { absent: true } });
554
+ operations.push({
555
+ path: MANIFEST_NAME,
556
+ operation: owned.raw ? "update" : "create",
557
+ reason: "generic installer ownership",
558
+ });
559
+ mutations.push({
560
+ path: MANIFEST_NAME,
561
+ operation: "write",
562
+ content: manifestContent,
563
+ mode: 0o600,
564
+ expected: owned.raw ? { sha256: sha256(owned.raw) } : { absent: true },
565
+ });
400
566
  }
401
567
  else if (!manifestContent && owned.raw) {
402
- operations.push({ path: MANIFEST_NAME, operation: "remove", reason: "generic assets uninstalled" });
403
- mutations.push({ path: MANIFEST_NAME, operation: "remove", expected: { sha256: sha256(owned.raw) } });
568
+ operations.push({
569
+ path: MANIFEST_NAME,
570
+ operation: "remove",
571
+ reason: "generic assets uninstalled",
572
+ });
573
+ mutations.push({
574
+ path: MANIFEST_NAME,
575
+ operation: "remove",
576
+ expected: { sha256: sha256(owned.raw) },
577
+ });
404
578
  }
405
579
  const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
406
- const base = { schema_version: 2, action, scope, root, package_version: packageVersion(), selection, operations: sorted, requires_restart: (action === "install" && owned.manifest?.package_version !== packageVersion()) || profiles.plan.requires_restart || mutations.some((item) => item.path.startsWith("agents/") || item.path.startsWith("commands/") || item.path.startsWith("plugins/")) };
580
+ const base = {
581
+ schema_version: 2,
582
+ action,
583
+ scope,
584
+ root,
585
+ package_version: packageVersion(),
586
+ selection,
587
+ operations: sorted,
588
+ requires_restart: (action === "install" && owned.manifest?.package_version !== packageVersion()) ||
589
+ profiles.plan.requires_restart ||
590
+ mutations.some((item) => item.path.startsWith("agents/") ||
591
+ item.path.startsWith("commands/") ||
592
+ item.path.startsWith("plugins/")),
593
+ };
407
594
  const planDigest = digest(base);
408
595
  return {
409
596
  plan: { ...base, plan_digest: planDigest, digest: planDigest },
@@ -446,16 +633,25 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
446
633
  return await withLifecycleLock(stateRoot, async () => {
447
634
  if (await recoverTransaction(root, stateRoot))
448
635
  throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
449
- const receipt = (await consumeReceipt(stateRoot, { digest: confirmationDigest, kind: `installer:${action}`, scope, root }));
636
+ const receipt = (await consumeReceipt(stateRoot, {
637
+ digest: confirmationDigest,
638
+ kind: `installer:${action}`,
639
+ scope,
640
+ root,
641
+ }));
450
642
  const built = await build(action, scope, cwd, home, selection);
451
643
  const savedPlanDigest = receipt.plan_digest ??
452
644
  receipt.digest;
453
645
  if (built.plan.digest !== savedPlanDigest)
454
646
  throw new InstallerError("stale_plan", "Installer plan changed after preview");
455
- if (built.plan.operations.some((item) => item.operation === "conflict" && (item.reason === "unmanaged_file" || item.reason === "v1.0.0_agent_ownership_mismatch" || item.reason?.includes("collision")))) {
647
+ if (built.plan.operations.some((item) => item.operation === "conflict" &&
648
+ (item.reason === "unmanaged_file" ||
649
+ item.reason === "v1.0.0_agent_ownership_mismatch" ||
650
+ item.reason?.includes("collision")))) {
456
651
  throw new InstallerError("conflict", "Installer plan contains an exact-name ownership conflict");
457
652
  }
458
- if (action === "install" && built.plan.operations.some((item) => item.operation === "conflict"))
653
+ if (action === "install" &&
654
+ built.plan.operations.some((item) => item.operation === "conflict"))
459
655
  throw new InstallerError("conflict", "Installer plan contains managed drift");
460
656
  await applyTransaction(root, stateRoot, built.mutations, {
461
657
  ...options,
@@ -466,7 +662,12 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
466
662
  if (action !== "install")
467
663
  return;
468
664
  const inventory = await listAgentProfiles(scope, cwd, home);
469
- if (built.plan.selection.agents.length > 0 && (inventory.collisions.length || inventory.drift.length || inventory.profiles.filter((item) => item.ownership !== "user-owned").some((item) => item.state !== "current"))) {
665
+ if (built.plan.selection.agents.length > 0 &&
666
+ (inventory.collisions.length ||
667
+ inventory.drift.length ||
668
+ inventory.profiles
669
+ .filter((item) => item.ownership !== "user-owned")
670
+ .some((item) => item.state !== "current"))) {
470
671
  throw new InstallerError("final_validation_failed", "Final installed agent inventory is invalid");
471
672
  }
472
673
  },
@@ -1,9 +1,13 @@
1
1
  import { spawn } from "node:child_process";
2
2
  const THRESHOLD = 8_000;
3
3
  const FILTERS = [
4
- { prefix: ["git", "log"], filter: "git-log" }, { prefix: ["git", "diff"], filter: "git-diff" },
5
- { prefix: ["git", "status"], filter: "git-status" }, { prefix: ["rg"], filter: "grep" },
6
- { prefix: ["grep"], filter: "grep" }, { prefix: ["pytest"], filter: "pytest" }, { prefix: ["tsc"], filter: "tsc" },
4
+ { prefix: ["git", "log"], filter: "git-log" },
5
+ { prefix: ["git", "diff"], filter: "git-diff" },
6
+ { prefix: ["git", "status"], filter: "git-status" },
7
+ { prefix: ["rg"], filter: "grep" },
8
+ { prefix: ["grep"], filter: "grep" },
9
+ { prefix: ["pytest"], filter: "pytest" },
10
+ { prefix: ["tsc"], filter: "tsc" },
7
11
  ];
8
12
  function filter(command) {
9
13
  if (/[|;$&\n()`]/.test(command))
@@ -16,14 +20,21 @@ function filter(command) {
16
20
  function truncate(value) {
17
21
  const points = Array.from(value);
18
22
  const size = 3_200;
19
- return points.length <= size * 2 ? value : `${points.slice(0, size).join("")}\n…\n${points.slice(-size).join("")}`;
23
+ return points.length <= size * 2
24
+ ? value
25
+ : `${points.slice(0, size).join("")}\n…\n${points.slice(-size).join("")}`;
20
26
  }
21
27
  function external(binary, selected, input) {
22
28
  return new Promise((done) => {
23
- const child = spawn(binary, ["pipe", "--filter", selected], { stdio: ["pipe", "pipe", "ignore"], timeout: 5_000 });
29
+ const child = spawn(binary, ["pipe", "--filter", selected], {
30
+ stdio: ["pipe", "pipe", "ignore"],
31
+ timeout: 5_000,
32
+ });
24
33
  let output = "";
25
34
  child.stdout.setEncoding("utf8");
26
- child.stdout.on("data", (chunk) => { output += chunk; });
35
+ child.stdout.on("data", (chunk) => {
36
+ output += chunk;
37
+ });
27
38
  child.once("error", () => done(undefined));
28
39
  child.once("close", (code) => done(code === 0 ? output : undefined));
29
40
  child.stdin.end(input, "utf8");
@@ -32,20 +43,31 @@ function external(binary, selected, input) {
32
43
  export async function rtk(options = {}) {
33
44
  if (options.enabled === false)
34
45
  return {};
35
- const run = options.run ?? ((selected, input) => external(options.bin ?? "rtk", selected, input));
36
- return { "tool.execute.after": async (input, output) => {
46
+ const run = options.run ??
47
+ ((selected, input) => external(options.bin ?? "rtk", selected, input));
48
+ return {
49
+ "tool.execute.after": async (input, output) => {
37
50
  try {
38
- if (input.tool === "edit" && typeof output.output === "string" && /oldString (not found|found multiple times|and newString must be different)/i.test(output.output))
51
+ if (input.tool === "edit" &&
52
+ typeof output.output === "string" &&
53
+ /oldString (not found|found multiple times|and newString must be different)/i.test(output.output))
39
54
  output.output = `${output.output}\nSTOP. Read the file before retrying Edit.`;
40
- if (input.tool !== "bash" || typeof output.output !== "string" || output.output.length < (options.threshold ?? THRESHOLD))
55
+ if (input.tool !== "bash" ||
56
+ typeof output.output !== "string" ||
57
+ output.output.length < (options.threshold ?? THRESHOLD))
41
58
  return;
42
59
  const selected = typeof input.args?.command === "string" ? filter(input.args.command) : undefined;
43
60
  const compressed = selected ? await run(selected, output.output) : undefined;
44
- const result = compressed && compressed.length < output.output.length ? compressed : truncate(output.output);
61
+ const result = compressed && compressed.length < output.output.length
62
+ ? compressed
63
+ : truncate(output.output);
45
64
  const originalSize = output.output.length;
46
65
  output.output = `${result}\n[rtk: compressed method=${compressed ? `rtk/${selected}` : "head+tail"}; sizes=${originalSize}->${result.length}; evidence_complete=false; loss=possible]`;
47
66
  }
48
- catch { /* RTK is fail-open: preserve original tool output on plugin failure. */ }
49
- } };
67
+ catch {
68
+ /* RTK is fail-open: preserve original tool output on plugin failure. */
69
+ }
70
+ },
71
+ };
50
72
  }
51
73
  export default rtk;
@@ -9,7 +9,7 @@ export declare function rulesInjector(options?: RulesInjectorOptions): Promise<{
9
9
  "experimental.chat.system.transform"?: undefined;
10
10
  } | {
11
11
  "tool.execute.after": (input: unknown, result: unknown) => Promise<void>;
12
- event: ({ event }: {
12
+ event: ({ event, }: {
13
13
  event?: {
14
14
  type?: string;
15
15
  properties?: Record<string, unknown>;
@@ -2,19 +2,33 @@ import { lstat, readFile, realpath } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, resolve } from "node:path";
4
4
  const NAME = "AGENTS.md";
5
- function sessionID(value) { const item = value; for (const key of ["sessionID", "sessionId", "session_id"])
6
- if (typeof item?.[key] === "string" && item[key])
7
- return item[key]; return undefined; }
8
- function pathOf(input, output) { for (const value of [input, output]) {
9
- const args = value?.args;
10
- for (const key of ["filePath", "file_path", "path"])
11
- if (typeof args?.[key] === "string")
12
- return args[key];
13
- } return undefined; }
14
- function output(value, text) { const item = value; if (typeof item?.output === "string")
15
- item.output = `${item.output}\n${text}`; }
16
- function marker(rule) { return `[rules-injector source=${rule.path} revision=${rule.revision}]`; }
17
- function block(rule) { return `${marker(rule)}\n${rule.content}\n[/rules-injector source=${rule.path}]`; }
5
+ function sessionID(value) {
6
+ const item = value;
7
+ for (const key of ["sessionID", "sessionId", "session_id"])
8
+ if (typeof item?.[key] === "string" && item[key])
9
+ return item[key];
10
+ return undefined;
11
+ }
12
+ function pathOf(input, output) {
13
+ for (const value of [input, output]) {
14
+ const args = value?.args;
15
+ for (const key of ["filePath", "file_path", "path"])
16
+ if (typeof args?.[key] === "string")
17
+ return args[key];
18
+ }
19
+ return undefined;
20
+ }
21
+ function output(value, text) {
22
+ const item = value;
23
+ if (typeof item?.output === "string")
24
+ item.output = `${item.output}\n${text}`;
25
+ }
26
+ function marker(rule) {
27
+ return `[rules-injector source=${rule.path} revision=${rule.revision}]`;
28
+ }
29
+ function block(rule) {
30
+ return `${marker(rule)}\n${rule.content}\n[/rules-injector source=${rule.path}]`;
31
+ }
18
32
  export async function rulesInjector(options = {}) {
19
33
  if (options.enabled === false)
20
34
  return {};
@@ -22,10 +36,14 @@ export async function rulesInjector(options = {}) {
22
36
  const cwd = resolve(options.cwd ?? process.cwd());
23
37
  const global = resolve(process.env.XDG_CONFIG_HOME ?? resolve(homedir(), ".config"), "opencode");
24
38
  const sessions = new Map();
25
- const state = (identifier) => { let item = sessions.get(identifier); if (!item) {
26
- item = { loaded: new Map(), injected: new Set(), used: 0, replay: false };
27
- sessions.set(identifier, item);
28
- } return item; };
39
+ const state = (identifier) => {
40
+ let item = sessions.get(identifier);
41
+ if (!item) {
42
+ item = { loaded: new Map(), injected: new Set(), used: 0, replay: false };
43
+ sessions.set(identifier, item);
44
+ }
45
+ return item;
46
+ };
29
47
  const inject = async (input, result) => {
30
48
  const call = input;
31
49
  if (call?.tool !== "read" && call?.tool !== "edit")
@@ -42,7 +60,9 @@ export async function rulesInjector(options = {}) {
42
60
  const candidate = resolve(current, NAME);
43
61
  try {
44
62
  const info = await lstat(candidate);
45
- if (info.isFile() && candidate !== resolve(native, NAME) && candidate !== resolve(global, NAME))
63
+ if (info.isFile() &&
64
+ candidate !== resolve(native, NAME) &&
65
+ candidate !== resolve(global, NAME))
46
66
  paths.push(await realpath(candidate));
47
67
  }
48
68
  catch (error) {
@@ -63,7 +83,11 @@ export async function rulesInjector(options = {}) {
63
83
  const info = await lstat(path);
64
84
  if (!info.isFile())
65
85
  continue;
66
- const rule = { path, revision: `${Math.trunc(info.mtimeMs)}:${info.size}`, content: await readFile(path, "utf8") };
86
+ const rule = {
87
+ path,
88
+ revision: `${Math.trunc(info.mtimeMs)}:${info.size}`,
89
+ content: await readFile(path, "utf8"),
90
+ };
67
91
  const text = block(rule);
68
92
  if (currentState.used + text.length > budget) {
69
93
  additions.push(`[rules-injector warning] context budget exhausted; skipped ${path}`);
@@ -85,12 +109,23 @@ export async function rulesInjector(options = {}) {
85
109
  output(result, `[rules-injector warning] cannot read AGENTS.md: ${String(error)}`);
86
110
  }
87
111
  };
88
- return { "tool.execute.after": inject, event: async ({ event }) => { if (event?.type?.includes("compaction")) {
89
- const identifier = sessionID(event.properties);
90
- if (identifier)
91
- state(identifier).replay = true;
92
- } }, "experimental.chat.system.transform": async (input, result) => { const identifier = sessionID(input); if (!identifier || !state(identifier).replay || !Array.isArray(result.system))
93
- return; for (const rule of state(identifier).loaded.values())
94
- result.system.push(block(rule)); state(identifier).replay = false; } };
112
+ return {
113
+ "tool.execute.after": inject,
114
+ event: async ({ event, }) => {
115
+ if (event?.type?.includes("compaction")) {
116
+ const identifier = sessionID(event.properties);
117
+ if (identifier)
118
+ state(identifier).replay = true;
119
+ }
120
+ },
121
+ "experimental.chat.system.transform": async (input, result) => {
122
+ const identifier = sessionID(input);
123
+ if (!identifier || !state(identifier).replay || !Array.isArray(result.system))
124
+ return;
125
+ for (const rule of state(identifier).loaded.values())
126
+ result.system.push(block(rule));
127
+ state(identifier).replay = false;
128
+ },
129
+ };
95
130
  }
96
131
  export default rulesInjector;