@kisev/skills-opencode 1.1.1 → 2.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 (157) hide show
  1. package/README.md +134 -108
  2. package/README.ru.md +242 -0
  3. package/dist/agent-profiles.d.ts +1 -1
  4. package/dist/agent-profiles.js +21 -10
  5. package/dist/assets/agents/architect.md +67 -0
  6. package/{assets → dist/assets}/agents/critic.md +17 -2
  7. package/dist/assets/agents/manager.md +36 -0
  8. package/{assets → dist/assets}/agents/mapper.md +2 -2
  9. package/dist/assets/agents/review.md +25 -0
  10. package/{assets → dist/assets}/agents/worker.md +7 -5
  11. package/dist/assets/commands/agent-profiles.md +9 -0
  12. package/dist/assets/commands/agents-md.md +10 -0
  13. package/dist/assets/commands/askme.md +10 -0
  14. package/dist/assets/commands/ast-grep.md +10 -0
  15. package/dist/assets/commands/capabilities.md +9 -0
  16. package/dist/assets/commands/code-explain.md +10 -0
  17. package/dist/assets/commands/code-review.md +10 -0
  18. package/dist/assets/commands/commit-msg.md +10 -0
  19. package/dist/assets/commands/docs-prepare.md +10 -0
  20. package/dist/assets/commands/docs-review.md +10 -0
  21. package/dist/assets/commands/doctor.md +9 -0
  22. package/dist/assets/commands/doit.md +10 -0
  23. package/dist/assets/commands/goal.md +10 -0
  24. package/dist/assets/commands/humanize.md +10 -0
  25. package/dist/assets/commands/lsp-report.md +10 -0
  26. package/dist/assets/commands/mattermost.md +10 -0
  27. package/dist/assets/commands/mr-prepare.md +10 -0
  28. package/dist/assets/commands/reconcile.md +9 -0
  29. package/dist/assets/commands/release-prepare.md +10 -0
  30. package/dist/assets/commands/release-review.md +10 -0
  31. package/dist/assets/commands/rtk.md +10 -0
  32. package/dist/assets/commands/skill-improve.md +10 -0
  33. package/dist/assets/commands/slides-prompts-prepare.md +10 -0
  34. package/dist/assets/commands/spec-manage.md +10 -0
  35. package/dist/assets/commands/stopit.md +10 -0
  36. package/dist/assets/commands/summary.md +10 -0
  37. package/dist/assets/commands/task-prepare.md +10 -0
  38. package/dist/assets/commands/task-review.md +10 -0
  39. package/dist/assets/commands/task-triage.md +10 -0
  40. package/dist/assets/commands/team-retro.md +10 -0
  41. package/dist/assets/commands/team-roadmap.md +10 -0
  42. package/dist/assets/commands/team-sprint-close.md +10 -0
  43. package/dist/assets/commands/team-sprint-start.md +10 -0
  44. package/dist/assets/lsp-catalog.json +30 -0
  45. package/dist/assets/migration-inventory.json +294 -0
  46. package/dist/catalog.d.ts +8 -0
  47. package/dist/catalog.js +38 -0
  48. package/dist/cli-output.d.ts +7 -0
  49. package/dist/cli-output.js +68 -1
  50. package/dist/cli.js +256 -59
  51. package/dist/contracts.d.ts +52 -0
  52. package/dist/contracts.js +98 -0
  53. package/dist/doctor.d.ts +44 -0
  54. package/dist/doctor.js +595 -0
  55. package/dist/generate-assets.js +17 -2
  56. package/dist/index.d.ts +84 -55
  57. package/dist/index.js +170 -37
  58. package/dist/installer.d.ts +33 -5
  59. package/dist/installer.js +252 -28
  60. package/dist/lifecycle.d.ts +3 -0
  61. package/dist/lifecycle.js +49 -17
  62. package/dist/plugins/rtk.js +4 -1
  63. package/dist/reconcile.d.ts +43 -0
  64. package/dist/reconcile.js +507 -0
  65. package/dist/registry.d.ts +7 -4
  66. package/dist/registry.js +28 -88
  67. package/dist/routing.d.ts +97 -3
  68. package/dist/routing.js +470 -18
  69. package/dist/runtime/state.d.ts +1 -0
  70. package/dist/runtime/state.js +12 -5
  71. package/dist/runtime/worktree.d.ts +33 -0
  72. package/dist/runtime/worktree.js +220 -0
  73. package/dist/terminal-wizard.d.ts +2 -0
  74. package/dist/terminal-wizard.js +125 -0
  75. package/package.json +10 -26
  76. package/assets/agents/architect.md +0 -33
  77. package/assets/agents/manager.md +0 -82
  78. package/assets/agents/review.md +0 -25
  79. package/assets/commands/agent-list.md +0 -9
  80. package/assets/commands/agent-model-set.md +0 -9
  81. package/assets/commands/askme.md +0 -10
  82. package/assets/commands/ast-grep-rewrite.md +0 -10
  83. package/assets/commands/ast-grep-search.md +0 -10
  84. package/assets/commands/attempt-cancel.md +0 -10
  85. package/assets/commands/attempt-list.md +0 -10
  86. package/assets/commands/attempt-result.md +0 -10
  87. package/assets/commands/attempt-show.md +0 -10
  88. package/assets/commands/capabilities.md +0 -9
  89. package/assets/commands/code-review.md +0 -10
  90. package/assets/commands/commit-msg.md +0 -10
  91. package/assets/commands/critic-add.md +0 -9
  92. package/assets/commands/critic-remove.md +0 -9
  93. package/assets/commands/docs-prepare.md +0 -10
  94. package/assets/commands/docs-review.md +0 -10
  95. package/assets/commands/doctor.md +0 -9
  96. package/assets/commands/doit.md +0 -10
  97. package/assets/commands/goal-list.md +0 -10
  98. package/assets/commands/goal-pause.md +0 -10
  99. package/assets/commands/goal-prepare.md +0 -10
  100. package/assets/commands/goal-remove.md +0 -10
  101. package/assets/commands/goal-show.md +0 -10
  102. package/assets/commands/goal-start.md +0 -10
  103. package/assets/commands/lsp-report.md +0 -10
  104. package/assets/commands/mattermost.md +0 -10
  105. package/assets/commands/mr-prepare.md +0 -10
  106. package/assets/commands/mr-review.md +0 -10
  107. package/assets/commands/multi-run-cancel.md +0 -10
  108. package/assets/commands/multi-run-compare.md +0 -10
  109. package/assets/commands/multi-run-fusion.md +0 -10
  110. package/assets/commands/multi-run-start.md +0 -10
  111. package/assets/commands/multi-run-status.md +0 -10
  112. package/assets/commands/overview.md +0 -10
  113. package/assets/commands/release-prepare.md +0 -10
  114. package/assets/commands/release-review.md +0 -10
  115. package/assets/commands/route.md +0 -9
  116. package/assets/commands/schedule-add.md +0 -10
  117. package/assets/commands/schedule-disable.md +0 -10
  118. package/assets/commands/schedule-enable.md +0 -10
  119. package/assets/commands/schedule-list.md +0 -10
  120. package/assets/commands/schedule-remove.md +0 -10
  121. package/assets/commands/schedule-status.md +0 -10
  122. package/assets/commands/skill-improver.md +0 -10
  123. package/assets/commands/spec-audit.md +0 -10
  124. package/assets/commands/spec-init.md +0 -10
  125. package/assets/commands/spec-onboard.md +0 -10
  126. package/assets/commands/spec-update.md +0 -10
  127. package/assets/commands/stopit.md +0 -10
  128. package/assets/commands/summary.md +0 -10
  129. package/assets/commands/task-prepare-batch.md +0 -10
  130. package/assets/commands/task-prepare.md +0 -10
  131. package/assets/commands/task-review.md +0 -10
  132. package/assets/commands/task-triage.md +0 -10
  133. package/assets/commands/team-planning.md +0 -10
  134. package/assets/commands/team-retro.md +0 -10
  135. package/assets/commands/team-roadmap.md +0 -10
  136. package/assets/commands/team-slides-prompts.md +0 -10
  137. package/assets/commands/team-sprint-close.md +0 -10
  138. package/assets/commands/team-sprint-status.md +0 -10
  139. package/assets/commands/walkthrough.md +0 -10
  140. package/assets/plugins/autonomy-policy.js +0 -3
  141. package/assets/plugins/background-attempts.js +0 -3
  142. package/assets/plugins/goal-loop.js +0 -3
  143. package/assets/plugins/schedule.js +0 -3
  144. package/assets/plugins/zed-clickable-paths.js +0 -3
  145. package/dist/plugins/autonomy-policy.d.ts +0 -21
  146. package/dist/plugins/autonomy-policy.js +0 -71
  147. package/dist/plugins/background-attempts.d.ts +0 -76
  148. package/dist/plugins/background-attempts.js +0 -113
  149. package/dist/plugins/goal-loop.d.ts +0 -51
  150. package/dist/plugins/goal-loop.js +0 -87
  151. package/dist/plugins/schedule.d.ts +0 -36
  152. package/dist/plugins/schedule.js +0 -100
  153. package/dist/plugins/zed-clickable-paths.d.ts +0 -11
  154. package/dist/plugins/zed-clickable-paths.js +0 -10
  155. /package/{assets → dist/assets}/plugins/rtk.js +0 -0
  156. /package/{assets → dist/assets}/plugins/rules-injector.js +0 -0
  157. /package/{assets → dist/assets}/plugins/zed-bell.js +0 -0
package/dist/installer.js CHANGED
@@ -1,14 +1,52 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { lstat, readdir } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
- import { dirname, resolve } from "node:path";
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, consumeReceipt, deploymentRoot, destination, digest, LifecycleError, lifecycleRoot, readRegular, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
7
+ import { applyTransaction, archiveRoot, consumeReceipt, deploymentRoot, destination, digest, LifecycleError, lifecycleRoot, readRegular, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
8
+ import { CATALOG } from "./catalog.js";
9
+ import { FIXED_AGENT_ROLES } from "./agent-profiles.js";
8
10
  const PACKAGE_NAME = "@kisev/skills-opencode";
9
11
  const MANIFEST_NAME = ".skills-opencode-manifest.json";
10
12
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
11
- const assetsRoot = resolve(packageRoot, "assets");
13
+ const assetsRoot = resolve(packageRoot, "dist", "assets");
14
+ function retiredAssetPaths() {
15
+ const inventory = JSON.parse(readFileSync(resolve(assetsRoot, "migration-inventory.json"), "utf8"));
16
+ return new Set([
17
+ ...Object.keys(inventory.retired_command_hashes ?? {}),
18
+ ...Object.keys(inventory.retired_plugin_hashes ?? {}),
19
+ ]);
20
+ }
21
+ export const SELECTABLE_PLUGINS = [...CATALOG.plugins];
22
+ export const PACKAGE_COMMANDS = [...CATALOG.package_commands];
23
+ export const SKILL_COMMANDS = [...CATALOG.skills];
24
+ export function defaultSelection() {
25
+ return {
26
+ commands: [...SKILL_COMMANDS, ...PACKAGE_COMMANDS].sort(),
27
+ agents: [...FIXED_AGENT_ROLES],
28
+ plugins: [],
29
+ core_activation: true,
30
+ };
31
+ }
32
+ export function normalizeSelection(value = {}) {
33
+ const allCommands = new Set([...SKILL_COMMANDS, ...PACKAGE_COMMANDS]);
34
+ const commands = [...new Set(value.commands ?? defaultSelection().commands)].sort();
35
+ const agents = [...new Set(value.agents ?? FIXED_AGENT_ROLES)];
36
+ const plugins = [...new Set(value.plugins ?? [])];
37
+ if (commands.some((name) => !allCommands.has(name)))
38
+ throw new InstallerError("invalid_selection", "Unknown command selection");
39
+ if (agents.some((name) => !FIXED_AGENT_ROLES.includes(name)))
40
+ throw new InstallerError("invalid_selection", "Unknown agent selection");
41
+ if (plugins.some((name) => !SELECTABLE_PLUGINS.includes(name)))
42
+ throw new InstallerError("invalid_selection", "Unknown plugin selection");
43
+ return {
44
+ commands,
45
+ agents: agents.sort(),
46
+ plugins: plugins.sort(),
47
+ core_activation: Boolean(value.core_activation ?? (commands.some((name) => PACKAGE_COMMANDS.includes(name)) || agents.length)),
48
+ };
49
+ }
12
50
  export class InstallerError extends LifecycleError {
13
51
  }
14
52
  function packageVersion() {
@@ -17,14 +55,19 @@ function packageVersion() {
17
55
  throw new InstallerError("invalid_package", "Package version is unavailable");
18
56
  return metadata.version;
19
57
  }
20
- async function assets() {
58
+ async function assets(selection) {
21
59
  const result = [];
22
60
  for (const category of ["commands", "plugins"]) {
23
61
  const directory = resolve(assetsRoot, category);
24
62
  for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
25
63
  const extension = category === "plugins" ? ".js" : ".md";
64
+ const name = entry.name.slice(0, -extension.length);
26
65
  if (!entry.isFile() || entry.isSymbolicLink() || !entry.name.endsWith(extension))
27
66
  throw new InstallerError("asset_error", `Asset is not a regular ${extension} file: ${entry.name}`);
67
+ if (category === "plugins" && !selection.plugins.includes(name))
68
+ continue;
69
+ if (category === "commands" && !selection.commands.includes(name))
70
+ continue;
28
71
  const relativePath = `${category}/${entry.name}`;
29
72
  const content = await readRegular(destination(assetsRoot, relativePath));
30
73
  if (!content)
@@ -43,23 +86,143 @@ function parseManifest(raw, path) {
43
86
  throw new InstallerError("invalid_manifest", `Ownership manifest is not valid JSON: ${path}`);
44
87
  }
45
88
  const manifest = value;
46
- if (manifest.schema_version !== 1 || manifest.package !== PACKAGE_NAME || typeof manifest.version !== "string" || !manifest.files || typeof manifest.files !== "object" || Array.isArray(manifest.files)) {
89
+ if (manifest.schema_version === 1 &&
90
+ manifest.package === PACKAGE_NAME &&
91
+ typeof manifest.version === "string" &&
92
+ manifest.files &&
93
+ typeof manifest.files === "object" &&
94
+ !Array.isArray(manifest.files)) {
95
+ const files = Object.fromEntries(Object.entries(manifest.files).map(([relativePath, record]) => [
96
+ relativePath,
97
+ {
98
+ sha256: record.sha256,
99
+ mode: 0o644,
100
+ kind: relativePath.startsWith("commands/")
101
+ ? "command"
102
+ : relativePath.startsWith("plugins/")
103
+ ? "plugin"
104
+ : relativePath.startsWith("agents/")
105
+ ? "agent"
106
+ : "state",
107
+ },
108
+ ]));
109
+ return {
110
+ schema_version: 1,
111
+ package: PACKAGE_NAME,
112
+ package_version: manifest.version,
113
+ version: manifest.version,
114
+ scope: "global",
115
+ commands: Object.keys(files).filter((path) => path.startsWith("commands/")).map((path) => path.slice(9, -3)).sort(),
116
+ 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)),
118
+ core_activation: true,
119
+ files,
120
+ };
121
+ }
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)) {
47
123
  throw new InstallerError("invalid_manifest", `Ownership manifest has an unexpected format: ${path}`);
48
124
  }
49
- for (const [relativePath, record] of Object.entries(manifest.files)) {
125
+ const files = Object.fromEntries(Object.entries(manifest.files).map(([relativePath, record]) => {
50
126
  destination("/", relativePath);
51
127
  if (!record || typeof record !== "object" || typeof record.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(record.sha256)) {
52
128
  throw new InstallerError("invalid_manifest", `Ownership manifest has an invalid record: ${relativePath}`);
53
129
  }
54
- }
55
- return manifest;
130
+ const existing = record;
131
+ return [relativePath, {
132
+ sha256: existing.sha256,
133
+ 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
+ }];
136
+ }));
137
+ return { ...manifest, files };
56
138
  }
57
139
  async function currentManifest(root) {
58
140
  const path = destination(root, MANIFEST_NAME);
59
141
  const raw = await readRegular(path);
60
142
  return raw ? { manifest: parseManifest(raw, path), raw } : {};
61
143
  }
62
- async function validateGenericDeployment(root, action, expectedAssets, plannedOperations, expectedManifest) {
144
+ export async function archiveMutations(candidates, scope, cwd, home, sourceVersion) {
145
+ if (!candidates.length)
146
+ return [];
147
+ const root = archiveRoot(scope, cwd, home);
148
+ const rootInfo = await lstat(root).catch((error) => {
149
+ if (error.code === "ENOENT")
150
+ return undefined;
151
+ throw new InstallerError("unsafe_path", "Archive root is inaccessible");
152
+ });
153
+ if (rootInfo && (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || (rootInfo.mode & 0o077) !== 0))
154
+ throw new InstallerError("unsafe_path", "Archive root must be a private directory");
155
+ const objectsInfo = await lstat(join(root, "objects")).catch((error) => {
156
+ if (error.code === "ENOENT")
157
+ return undefined;
158
+ throw new InstallerError("unsafe_path", "Archive object store is inaccessible");
159
+ });
160
+ if (objectsInfo && (!objectsInfo.isDirectory() || objectsInfo.isSymbolicLink() || (objectsInfo.mode & 0o077) !== 0))
161
+ throw new InstallerError("unsafe_path", "Archive object store must be private");
162
+ const indexPath = destination(root, "index.json");
163
+ const existingRaw = await readRegular(indexPath);
164
+ if (existingRaw) {
165
+ const indexInfo = await lstat(indexPath);
166
+ if ((indexInfo.mode & 0o777) !== 0o600)
167
+ throw new InstallerError("unsafe_path", "Archive index must be private");
168
+ }
169
+ let entries = [];
170
+ if (existingRaw) {
171
+ try {
172
+ const parsed = JSON.parse(existingRaw.toString("utf8"));
173
+ if (parsed.schema_version !== 1 || !Array.isArray(parsed.entries))
174
+ throw new Error("invalid archive index");
175
+ entries = parsed.entries.filter((entry) => Boolean(entry && typeof entry === "object" && !Array.isArray(entry)));
176
+ }
177
+ catch {
178
+ throw new InstallerError("invalid_archive", "Archive index is not valid content-addressed state");
179
+ }
180
+ }
181
+ const known = new Set(entries.map((entry) => `${String(entry.original_path)}:${String(entry.digest)}`));
182
+ const mutations = [];
183
+ const now = new Date(0).toISOString();
184
+ for (const candidate of candidates) {
185
+ const digestValue = sha256(candidate.content);
186
+ const key = `${candidate.path}:${digestValue}`;
187
+ if (known.has(key))
188
+ continue;
189
+ const objectPath = `objects/${digestValue}`;
190
+ const object = await readRegular(destination(root, objectPath));
191
+ if (!object) {
192
+ mutations.push({
193
+ root,
194
+ path: objectPath,
195
+ operation: "write",
196
+ content: candidate.content,
197
+ mode: candidate.record.mode,
198
+ expected: { absent: true },
199
+ });
200
+ }
201
+ else if (sha256(object) !== digestValue) {
202
+ throw new InstallerError("archive_collision", `Archive object collision: ${digestValue}`);
203
+ }
204
+ entries.push({
205
+ schema_version: 1,
206
+ digest: digestValue,
207
+ original_path: candidate.path,
208
+ scope,
209
+ kind: candidate.kind,
210
+ reason: candidate.reason,
211
+ package_version: packageVersion(),
212
+ source_version: sourceVersion,
213
+ original_hash: candidate.record.sha256,
214
+ mode: candidate.record.mode,
215
+ timestamp: now,
216
+ });
217
+ known.add(key);
218
+ }
219
+ 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
+ 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 } });
222
+ }
223
+ return mutations;
224
+ }
225
+ async function validateGenericDeployment(root, action, expectedAssets, plannedOperations, expectedManifest, retiredPaths) {
63
226
  const owned = await currentManifest(root);
64
227
  if ((expectedManifest && (!owned.raw || !owned.raw.equals(expectedManifest))) ||
65
228
  (!expectedManifest && owned.raw))
@@ -73,7 +236,10 @@ async function validateGenericDeployment(root, action, expectedAssets, plannedOp
73
236
  throw new InstallerError("final_validation_failed", "Generic ownership manifest is not private");
74
237
  const expected = new Map(expectedAssets.map((asset) => [asset.relativePath, asset]));
75
238
  if (action === "install" &&
76
- Object.keys(owned.manifest.files).sort().join(",") !== [...expected.keys()].sort().join(",")) {
239
+ Object.keys(owned.manifest.files)
240
+ .filter((path) => !retiredPaths.has(path))
241
+ .sort()
242
+ .join(",") !== [...expected.keys()].sort().join(",")) {
77
243
  throw new InstallerError("final_validation_failed", "Generic ownership inventory is incomplete");
78
244
  }
79
245
  for (const [relativePath, record] of Object.entries(owned.manifest.files)) {
@@ -89,28 +255,45 @@ async function validateGenericDeployment(root, action, expectedAssets, plannedOp
89
255
  if (!content || (sha256(content) !== record.sha256 && !preservedDrift))
90
256
  throw new InstallerError("final_validation_failed", `Generic managed file failed final validation: ${relativePath}`);
91
257
  const asset = expected.get(relativePath);
92
- if (action === "install" && (!asset || asset.sha256 !== record.sha256))
258
+ if (action === "install" &&
259
+ ((!asset && !retiredPaths.has(relativePath)) || (asset && asset.sha256 !== record.sha256)))
93
260
  throw new InstallerError("final_validation_failed", `Generic manifest does not match package asset: ${relativePath}`);
94
- if (action === "install" && ((await lstat(target)).mode & 0o777) !== asset.mode)
261
+ if (action === "install" && asset && ((await lstat(target)).mode & 0o777) !== asset.mode)
95
262
  throw new InstallerError("final_validation_failed", `Generic managed file has an unexpected mode: ${relativePath}`);
96
263
  }
97
264
  }
98
265
  function asLegacy(manifest) {
99
266
  return manifest?.version === "1.0.0" ? manifest : undefined;
100
267
  }
101
- async function build(action, scope, cwd = process.cwd(), home = homedir()) {
268
+ async function build(action, scope, cwd = process.cwd(), home = homedir(), requestedSelection) {
102
269
  const root = deploymentRoot(scope, cwd, home);
103
- const [owned, bundled] = await Promise.all([currentManifest(root), assets()]);
270
+ const owned = await currentManifest(root);
271
+ const selection = normalizeSelection(requestedSelection ??
272
+ (owned.manifest
273
+ ? {
274
+ commands: owned.manifest.commands,
275
+ agents: owned.manifest.agents,
276
+ plugins: owned.manifest.plugins,
277
+ core_activation: owned.manifest.core_activation,
278
+ }
279
+ : undefined));
280
+ const bundled = await assets(selection);
281
+ const retiredPaths = retiredAssetPaths();
104
282
  const legacyRecord = owned.manifest && owned.raw && asLegacy(owned.manifest)
105
283
  ? { manifest: asLegacy(owned.manifest), manifestPath: destination(root, MANIFEST_NAME), manifestSha256: sha256(owned.raw) }
106
284
  : undefined;
107
- const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord);
285
+ const profiles = await buildAgentProfilePlan({ action }, scope, cwd, home, legacyRecord, selection.agents);
108
286
  const operations = profiles.plan.operations.map((item) => ({ path: item.path, operation: item.operation, reason: item.reason }));
109
287
  const mutations = [...profiles.mutations];
288
+ const archiveCandidates = [];
110
289
  const desiredFiles = {};
111
290
  if (action === "install") {
112
291
  for (const asset of bundled) {
113
- desiredFiles[asset.relativePath] = { sha256: asset.sha256 };
292
+ desiredFiles[asset.relativePath] = {
293
+ sha256: asset.sha256,
294
+ mode: asset.mode,
295
+ kind: asset.relativePath.startsWith("commands/") ? "command" : "plugin",
296
+ };
114
297
  const current = await readRegular(destination(root, asset.relativePath));
115
298
  const record = owned.manifest?.files[asset.relativePath];
116
299
  if (!current) {
@@ -136,15 +319,30 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
136
319
  for (const [relativePath, record] of Object.entries(owned.manifest?.files ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
137
320
  if (active.has(relativePath) || profiles.legacyTransferred.includes(relativePath))
138
321
  continue;
322
+ const current = await readRegular(destination(root, relativePath));
323
+ if (retiredPaths.has(relativePath)) {
324
+ if (!current) {
325
+ operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
326
+ }
327
+ else if (sha256(current) !== record.sha256) {
328
+ operations.push({ path: relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
329
+ }
330
+ 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 } });
334
+ }
335
+ continue;
336
+ }
139
337
  if (relativePath.startsWith("agents/")) {
140
338
  operations.push({ path: relativePath, operation: "conflict", reason: "v1.0.0_agent_ownership_mismatch" });
141
339
  continue;
142
340
  }
143
- const current = await readRegular(destination(root, relativePath));
144
341
  if (!current)
145
342
  operations.push({ path: relativePath, operation: "missing" });
146
343
  else if (sha256(current) === record.sha256) {
147
- operations.push({ path: relativePath, operation: "remove", sha256: 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 });
148
346
  mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
149
347
  }
150
348
  else
@@ -154,10 +352,24 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
154
352
  else {
155
353
  for (const [relativePath, record] of Object.entries(owned.manifest?.files ?? {}).sort(([left], [right]) => left.localeCompare(right))) {
156
354
  const current = await readRegular(destination(root, relativePath));
157
- if (!current)
355
+ if (retiredPaths.has(relativePath)) {
356
+ if (!current) {
357
+ operations.push({ path: relativePath, operation: "missing", sha256: record.sha256 });
358
+ }
359
+ else if (sha256(current) !== record.sha256) {
360
+ operations.push({ path: relativePath, operation: "conflict", reason: "managed_file_changed", sha256: sha256(current) });
361
+ }
362
+ 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 } });
366
+ }
367
+ }
368
+ else if (!current)
158
369
  operations.push({ path: relativePath, operation: "missing" });
159
370
  else if (sha256(current) === record.sha256) {
160
- operations.push({ path: relativePath, operation: "remove", sha256: 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 });
161
373
  mutations.push({ path: relativePath, operation: "remove", expected: { sha256: record.sha256 } });
162
374
  }
163
375
  else {
@@ -166,8 +378,20 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
166
378
  }
167
379
  }
168
380
  }
381
+ mutations.push(...await archiveMutations(archiveCandidates, scope, cwd, home, owned.manifest?.package_version ?? "1.0.0"));
169
382
  const nextManifest = action === "install" || Object.keys(desiredFiles).length
170
- ? { schema_version: 1, package: PACKAGE_NAME, version: packageVersion(), files: desiredFiles }
383
+ ? {
384
+ schema_version: 2,
385
+ package: PACKAGE_NAME,
386
+ package_version: packageVersion(),
387
+ version: packageVersion(),
388
+ scope,
389
+ commands: selection.commands,
390
+ agents: selection.agents,
391
+ plugins: selection.plugins,
392
+ core_activation: selection.core_activation,
393
+ files: desiredFiles,
394
+ }
171
395
  : undefined;
172
396
  const manifestContent = nextManifest ? Buffer.from(`${stable(nextManifest)}\n`) : undefined;
173
397
  if (manifestContent && (!owned.raw || !owned.raw.equals(manifestContent))) {
@@ -179,7 +403,7 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
179
403
  mutations.push({ path: MANIFEST_NAME, operation: "remove", expected: { sha256: sha256(owned.raw) } });
180
404
  }
181
405
  const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
182
- const base = { schema_version: 1, action, scope, root, package_version: packageVersion(), operations: sorted, requires_restart: (action === "install" && owned.manifest?.version !== packageVersion()) || profiles.plan.requires_restart || mutations.some((item) => item.path.startsWith("agents/") || item.path.startsWith("commands/") || item.path.startsWith("plugins/")) };
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/")) };
183
407
  return {
184
408
  plan: { ...base, digest: digest(base) },
185
409
  mutations,
@@ -187,14 +411,14 @@ async function build(action, scope, cwd = process.cwd(), home = homedir()) {
187
411
  profiles,
188
412
  };
189
413
  }
190
- export async function preview(action, scope, cwd = process.cwd(), home = homedir()) {
414
+ export async function preview(action, scope, cwd = process.cwd(), home = homedir(), selection) {
191
415
  const stateRoot = lifecycleRoot(scope, cwd, home);
192
416
  const root = deploymentRoot(scope, cwd, home);
193
417
  try {
194
418
  return await withLifecycleLock(stateRoot, async () => {
195
419
  if (await recoverTransaction(root, stateRoot))
196
420
  throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
197
- const built = await build(action, scope, cwd, home);
421
+ const built = await build(action, scope, cwd, home, selection);
198
422
  const receipt = await saveReceipt(stateRoot, `installer:${action}`, scope, root, { digest: built.plan.digest });
199
423
  return { ...built.plan, digest: receipt.digest, receipt_expires_at: receipt.expires_at };
200
424
  });
@@ -207,7 +431,7 @@ export async function preview(action, scope, cwd = process.cwd(), home = homedir
207
431
  throw error;
208
432
  }
209
433
  }
210
- export async function apply(action, scope, confirmationDigest, cwd = process.cwd(), home = homedir(), options = {}) {
434
+ export async function apply(action, scope, confirmationDigest, cwd = process.cwd(), home = homedir(), options = {}, selection) {
211
435
  const stateRoot = lifecycleRoot(scope, cwd, home);
212
436
  const root = deploymentRoot(scope, cwd, home);
213
437
  try {
@@ -215,7 +439,7 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
215
439
  if (await recoverTransaction(root, stateRoot))
216
440
  throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
217
441
  const receipt = (await consumeReceipt(stateRoot, { digest: confirmationDigest, kind: `installer:${action}`, scope, root }));
218
- const built = await build(action, scope, cwd, home);
442
+ const built = await build(action, scope, cwd, home, selection);
219
443
  if (built.plan.digest !== receipt.digest)
220
444
  throw new InstallerError("stale_plan", "Installer plan changed after preview");
221
445
  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")))) {
@@ -228,11 +452,11 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
228
452
  validateFinal: async () => {
229
453
  await options.validateFinal?.();
230
454
  await validateBuiltAgentProfilePlan(built.profiles);
231
- await validateGenericDeployment(root, action, await assets(), built.plan.operations, built.expectedManifest);
455
+ await validateGenericDeployment(root, action, await assets(built.plan.selection), built.plan.operations, built.expectedManifest, retiredAssetPaths());
232
456
  if (action !== "install")
233
457
  return;
234
458
  const inventory = await listAgentProfiles(scope, cwd, home);
235
- if (inventory.collisions.length || inventory.drift.length || inventory.profiles.filter((item) => item.ownership !== "user-owned").some((item) => item.state !== "current")) {
459
+ 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"))) {
236
460
  throw new InstallerError("final_validation_failed", "Final installed agent inventory is invalid");
237
461
  }
238
462
  },
@@ -10,12 +10,14 @@ export type FileExpectation = {
10
10
  };
11
11
  export type FileMutation = {
12
12
  path: string;
13
+ root?: string;
13
14
  operation: "write";
14
15
  content: Buffer;
15
16
  mode: number;
16
17
  expected: FileExpectation;
17
18
  } | {
18
19
  path: string;
20
+ root?: string;
19
21
  operation: "remove";
20
22
  expected: FileExpectation;
21
23
  };
@@ -32,6 +34,7 @@ export declare function readRegular(path: string): Promise<Buffer | undefined>;
32
34
  export declare function writeAtomic(path: string, content: Buffer, mode: number): Promise<void>;
33
35
  export declare function deploymentRoot(scope: Scope, cwd?: string, home?: string): string;
34
36
  export declare function lifecycleRoot(scope: Scope, cwd?: string, home?: string): string;
37
+ export declare function archiveRoot(scope: Scope, cwd?: string, home?: string): string;
35
38
  export declare function withLifecycleLock<T>(stateRoot: string, callback: () => Promise<T>): Promise<T>;
36
39
  export declare function saveReceipt(stateRoot: string, kind: string, scope: Scope, root: string, payload: unknown, now?: number): Promise<{
37
40
  digest: string;
package/dist/lifecycle.js CHANGED
@@ -151,6 +151,12 @@ export function lifecycleRoot(scope, cwd = process.cwd(), home = homedir()) {
151
151
  const suffix = scope === "global" ? "global" : join("project", sha256(resolve(cwd)));
152
152
  return join(base, "opencode", "skills-opencode", suffix);
153
153
  }
154
+ export function archiveRoot(scope, cwd = process.cwd(), home = homedir()) {
155
+ const base = process.env.XDG_DATA_HOME && home === homedir()
156
+ ? resolve(process.env.XDG_DATA_HOME)
157
+ : resolve(home, ".local", "share");
158
+ return join(base, "opencode", "skills-opencode", "archive", scope);
159
+ }
154
160
  async function processAlive(pid) {
155
161
  if (!Number.isSafeInteger(pid) || pid <= 0)
156
162
  return false;
@@ -331,16 +337,25 @@ export async function consumeReceipt(stateRoot, expected, now = Date.now()) {
331
337
  function journalPath(stateRoot) {
332
338
  return join(stateRoot, "transaction-journal.json");
333
339
  }
340
+ function mutationRoot(root, mutation) {
341
+ return resolve(mutation.root ?? root);
342
+ }
334
343
  async function snapshot(root, mutation) {
335
- const target = destination(root, mutation.path);
344
+ const targetRoot = mutationRoot(root, mutation);
345
+ const target = destination(targetRoot, mutation.path);
336
346
  const content = await readRegular(target);
337
347
  if (!content)
338
- return { path: mutation.path, content: null, mode: null };
348
+ return { path: mutation.path, root: targetRoot, content: null, mode: null };
339
349
  const metadata = await stat(target);
340
- return { path: mutation.path, content: content.toString("base64"), mode: metadata.mode & 0o777 };
350
+ return {
351
+ path: mutation.path,
352
+ root: targetRoot,
353
+ content: content.toString("base64"),
354
+ mode: metadata.mode & 0o777,
355
+ };
341
356
  }
342
357
  async function validateExpectation(root, mutation) {
343
- const current = await readRegular(destination(root, mutation.path));
358
+ const current = await readRegular(destination(mutationRoot(root, mutation), mutation.path));
344
359
  if (mutation.expected.absent) {
345
360
  if (current)
346
361
  throw new LifecycleError("stale_plan", `Expected an absent target: ${mutation.path}`);
@@ -358,7 +373,7 @@ async function restore(root, journal) {
358
373
  for (let index = journal.snapshots.length - 1; index >= 0; index -= 1) {
359
374
  const item = journal.snapshots[index];
360
375
  const operation = journal.operations[index];
361
- const target = destination(root, item.path);
376
+ const target = destination(item.root, item.path);
362
377
  const current = await readRegular(target);
363
378
  const currentHash = current ? sha256(current) : undefined;
364
379
  const currentMode = current ? (await lstat(target)).mode & 0o777 : undefined;
@@ -436,8 +451,11 @@ function parseJournal(raw, expectedRoot) {
436
451
  journal.applying >= journal.operations.length))) {
437
452
  throw new LifecycleError("invalid_journal", "Transaction journal has an unsupported schema");
438
453
  }
439
- for (const item of journal.snapshots)
454
+ for (const item of journal.snapshots) {
440
455
  assertSafeRelative(item.path);
456
+ if (item.root !== undefined && !isAbsolute(item.root))
457
+ throw new LifecycleError("invalid_journal", "Transaction snapshot root must be absolute");
458
+ }
441
459
  for (const item of journal.snapshots) {
442
460
  if ((item.content !== null &&
443
461
  (typeof item.content !== "string" ||
@@ -449,6 +467,8 @@ function parseJournal(raw, expectedRoot) {
449
467
  }
450
468
  for (const item of journal.operations) {
451
469
  assertSafeRelative(item.path);
470
+ if (item.root !== undefined && !isAbsolute(item.root))
471
+ throw new LifecycleError("invalid_journal", "Transaction operation root must be absolute");
452
472
  if (item.operation !== "write" && item.operation !== "remove")
453
473
  throw new LifecycleError("invalid_journal", "Transaction journal contains an invalid operation");
454
474
  if (item.operation === "write" &&
@@ -462,14 +482,18 @@ function parseJournal(raw, expectedRoot) {
462
482
  }
463
483
  if (!journal.created_directories.every((item) => item &&
464
484
  typeof item.path === "string" &&
465
- inside(resolve(expectedRoot), resolve(item.path)) &&
485
+ (inside(resolve(expectedRoot), resolve(item.path)) ||
486
+ (journal.operations ?? []).some((operation) => operation.root && inside(resolve(operation.root), resolve(item.path)))) &&
466
487
  Number.isSafeInteger(item.device) &&
467
488
  item.device >= 0 &&
468
489
  Number.isSafeInteger(item.inode) &&
469
490
  item.inode > 0)) {
470
491
  throw new LifecycleError("invalid_journal", "Transaction journal contains an unsafe created directory");
471
492
  }
472
- return journal;
493
+ return {
494
+ ...journal,
495
+ snapshots: journal.snapshots.map((item) => ({ ...item, root: item.root ?? expectedRoot })),
496
+ };
473
497
  }
474
498
  export async function recoverTransaction(root, stateRoot) {
475
499
  const raw = await readRegular(journalPath(stateRoot));
@@ -481,12 +505,13 @@ export async function recoverTransaction(root, stateRoot) {
481
505
  return true;
482
506
  }
483
507
  async function ensureTransactionDirectories(root, mutation, stateRoot, journal) {
484
- await ensureDirectory(dirname(resolve(root)), 0o700);
485
- const relativeParent = relative(resolve(root), dirname(destination(root, mutation.path)));
508
+ const targetRoot = mutationRoot(root, mutation);
509
+ await ensureDirectory(dirname(targetRoot), 0o700);
510
+ const relativeParent = relative(targetRoot, dirname(destination(targetRoot, mutation.path)));
486
511
  const parts = relativeParent.split(sep).filter(Boolean);
487
512
  const directories = [
488
- resolve(root),
489
- ...parts.map((_, index) => resolve(root, ...parts.slice(0, index + 1))),
513
+ targetRoot,
514
+ ...parts.map((_, index) => resolve(targetRoot, ...parts.slice(0, index + 1))),
490
515
  ];
491
516
  for (const directory of directories) {
492
517
  const existing = await lstatSafe(directory);
@@ -521,9 +546,10 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
521
546
  const unique = new Set();
522
547
  for (const mutation of mutations) {
523
548
  assertSafeRelative(mutation.path);
524
- if (unique.has(mutation.path))
549
+ const key = `${mutationRoot(root, mutation)}:${mutation.path}`;
550
+ if (unique.has(key))
525
551
  throw new LifecycleError("invalid_plan", `Duplicate transaction target: ${mutation.path}`);
526
- unique.add(mutation.path);
552
+ unique.add(key);
527
553
  await validateExpectation(root, mutation);
528
554
  }
529
555
  const snapshots = await Promise.all(mutations.map((mutation) => snapshot(root, mutation)));
@@ -533,12 +559,18 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
533
559
  operations: mutations.map((mutation) => mutation.operation === "write"
534
560
  ? {
535
561
  path: mutation.path,
562
+ ...(mutation.root ? { root: resolve(mutation.root) } : {}),
536
563
  operation: mutation.operation,
537
564
  content: mutation.content.toString("base64"),
538
565
  mode: mutation.mode,
539
566
  expected: mutation.expected,
540
567
  }
541
- : { path: mutation.path, operation: mutation.operation, expected: mutation.expected }),
568
+ : {
569
+ path: mutation.path,
570
+ ...(mutation.root ? { root: resolve(mutation.root) } : {}),
571
+ operation: mutation.operation,
572
+ expected: mutation.expected,
573
+ }),
542
574
  snapshots,
543
575
  created_directories: [],
544
576
  published: 0,
@@ -553,7 +585,7 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
553
585
  if (mutation.operation === "write")
554
586
  await ensureTransactionDirectories(root, mutation, stateRoot, journal);
555
587
  await validateExpectation(root, mutation);
556
- const target = destination(root, mutation.path);
588
+ const target = destination(mutationRoot(root, mutation), mutation.path);
557
589
  if (mutation.operation === "write")
558
590
  await writeAtomic(target, mutation.content, mutation.mode);
559
591
  else
@@ -568,7 +600,7 @@ export async function applyTransaction(root, stateRoot, mutations, options = {})
568
600
  throw new LifecycleError("test_failure", "Injected transaction failure");
569
601
  }
570
602
  for (const mutation of mutations) {
571
- const target = destination(root, mutation.path);
603
+ const target = destination(mutationRoot(root, mutation), mutation.path);
572
604
  const current = await readRegular(target);
573
605
  if ((mutation.operation === "remove" && current) ||
574
606
  (mutation.operation === "write" &&
@@ -8,6 +8,8 @@ const FILTERS = [
8
8
  function filter(command) {
9
9
  if (/[|;$&\n()`]/.test(command))
10
10
  return undefined;
11
+ if (/\b(--json|--format(?:=|\s)|diagnos|security|deploy|migrat|delete|remove)\b/i.test(command))
12
+ return undefined;
11
13
  const values = command.trim().split(/\s+/);
12
14
  return FILTERS.find((item) => item.prefix.every((part, index) => values[index] === part))?.filter;
13
15
  }
@@ -40,7 +42,8 @@ export async function rtk(options = {}) {
40
42
  const selected = typeof input.args?.command === "string" ? filter(input.args.command) : undefined;
41
43
  const compressed = selected ? await run(selected, output.output) : undefined;
42
44
  const result = compressed && compressed.length < output.output.length ? compressed : truncate(output.output);
43
- output.output = `${result}\n[rtk: compressed ${output.output.length} chars using ${compressed ? `rtk/${selected}` : "head+tail"}]`;
45
+ const originalSize = output.output.length;
46
+ output.output = `${result}\n[rtk: compressed method=${compressed ? `rtk/${selected}` : "head+tail"}; sizes=${originalSize}->${result.length}; evidence_complete=false; loss=possible]`;
44
47
  }
45
48
  catch { /* RTK is fail-open: preserve original tool output on plugin failure. */ }
46
49
  } };