@kisev/skills-opencode 1.2.0 → 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.
- package/README.md +134 -112
- package/README.ru.md +242 -0
- package/dist/agent-profiles.d.ts +1 -1
- package/dist/agent-profiles.js +18 -9
- package/dist/assets/agents/architect.md +67 -0
- package/{assets → dist/assets}/agents/critic.md +17 -2
- package/dist/assets/agents/manager.md +36 -0
- package/{assets → dist/assets}/agents/mapper.md +2 -2
- package/dist/assets/agents/review.md +25 -0
- package/{assets → dist/assets}/agents/worker.md +7 -5
- package/dist/assets/commands/agent-profiles.md +9 -0
- package/dist/assets/commands/agents-md.md +10 -0
- package/dist/assets/commands/askme.md +10 -0
- package/dist/assets/commands/ast-grep.md +10 -0
- package/dist/assets/commands/capabilities.md +9 -0
- package/dist/assets/commands/code-explain.md +10 -0
- package/dist/assets/commands/code-review.md +10 -0
- package/dist/assets/commands/commit-msg.md +10 -0
- package/dist/assets/commands/docs-prepare.md +10 -0
- package/dist/assets/commands/docs-review.md +10 -0
- package/dist/assets/commands/doctor.md +9 -0
- package/dist/assets/commands/doit.md +10 -0
- package/dist/assets/commands/goal.md +10 -0
- package/dist/assets/commands/humanize.md +10 -0
- package/dist/assets/commands/lsp-report.md +10 -0
- package/dist/assets/commands/mattermost.md +10 -0
- package/dist/assets/commands/mr-prepare.md +10 -0
- package/dist/assets/commands/reconcile.md +9 -0
- package/dist/assets/commands/release-prepare.md +10 -0
- package/dist/assets/commands/release-review.md +10 -0
- package/dist/assets/commands/rtk.md +10 -0
- package/dist/assets/commands/skill-improve.md +10 -0
- package/dist/assets/commands/slides-prompts-prepare.md +10 -0
- package/dist/assets/commands/spec-manage.md +10 -0
- package/dist/assets/commands/stopit.md +10 -0
- package/dist/assets/commands/summary.md +10 -0
- package/dist/assets/commands/task-prepare.md +10 -0
- package/dist/assets/commands/task-review.md +10 -0
- package/dist/assets/commands/task-triage.md +10 -0
- package/dist/assets/commands/team-retro.md +10 -0
- package/dist/assets/commands/team-roadmap.md +10 -0
- package/dist/assets/commands/team-sprint-close.md +10 -0
- package/dist/assets/commands/team-sprint-start.md +10 -0
- package/dist/assets/lsp-catalog.json +30 -0
- package/dist/assets/migration-inventory.json +294 -0
- package/dist/catalog.d.ts +8 -0
- package/dist/catalog.js +38 -0
- package/dist/cli-output.d.ts +7 -0
- package/dist/cli-output.js +68 -1
- package/dist/cli.js +136 -10
- package/dist/contracts.d.ts +52 -0
- package/dist/contracts.js +98 -0
- package/dist/doctor.d.ts +44 -0
- package/dist/doctor.js +595 -0
- package/dist/generate-assets.js +17 -2
- package/dist/index.d.ts +80 -56
- package/dist/index.js +162 -37
- package/dist/installer.d.ts +33 -5
- package/dist/installer.js +252 -28
- package/dist/lifecycle.d.ts +3 -0
- package/dist/lifecycle.js +49 -17
- package/dist/plugins/rtk.js +4 -1
- package/dist/reconcile.d.ts +43 -0
- package/dist/reconcile.js +507 -0
- package/dist/registry.d.ts +7 -4
- package/dist/registry.js +28 -88
- package/dist/routing.d.ts +39 -2
- package/dist/routing.js +339 -27
- package/dist/runtime/worktree.d.ts +33 -0
- package/dist/runtime/worktree.js +220 -0
- package/package.json +10 -26
- package/assets/agents/architect.md +0 -33
- package/assets/agents/manager.md +0 -82
- package/assets/agents/review.md +0 -25
- package/assets/commands/agent-list.md +0 -9
- package/assets/commands/agent-model-set.md +0 -9
- package/assets/commands/askme.md +0 -10
- package/assets/commands/ast-grep-rewrite.md +0 -10
- package/assets/commands/ast-grep-search.md +0 -10
- package/assets/commands/attempt-cancel.md +0 -10
- package/assets/commands/attempt-list.md +0 -10
- package/assets/commands/attempt-result.md +0 -10
- package/assets/commands/attempt-show.md +0 -10
- package/assets/commands/capabilities.md +0 -9
- package/assets/commands/code-review.md +0 -10
- package/assets/commands/commit-msg.md +0 -10
- package/assets/commands/critic-add.md +0 -9
- package/assets/commands/critic-remove.md +0 -9
- package/assets/commands/docs-prepare.md +0 -10
- package/assets/commands/docs-review.md +0 -10
- package/assets/commands/doctor.md +0 -9
- package/assets/commands/doit.md +0 -10
- package/assets/commands/goal-list.md +0 -10
- package/assets/commands/goal-pause.md +0 -10
- package/assets/commands/goal-prepare.md +0 -10
- package/assets/commands/goal-remove.md +0 -10
- package/assets/commands/goal-show.md +0 -10
- package/assets/commands/goal-start.md +0 -10
- package/assets/commands/lsp-report.md +0 -10
- package/assets/commands/mattermost.md +0 -10
- package/assets/commands/mr-prepare.md +0 -10
- package/assets/commands/mr-review.md +0 -10
- package/assets/commands/multi-run-cancel.md +0 -10
- package/assets/commands/multi-run-compare.md +0 -10
- package/assets/commands/multi-run-fusion.md +0 -10
- package/assets/commands/multi-run-start.md +0 -10
- package/assets/commands/multi-run-status.md +0 -10
- package/assets/commands/overview.md +0 -10
- package/assets/commands/release-prepare.md +0 -10
- package/assets/commands/release-review.md +0 -10
- package/assets/commands/route.md +0 -9
- package/assets/commands/schedule-add.md +0 -10
- package/assets/commands/schedule-disable.md +0 -10
- package/assets/commands/schedule-enable.md +0 -10
- package/assets/commands/schedule-list.md +0 -10
- package/assets/commands/schedule-remove.md +0 -10
- package/assets/commands/schedule-status.md +0 -10
- package/assets/commands/skill-improver.md +0 -10
- package/assets/commands/spec-audit.md +0 -10
- package/assets/commands/spec-init.md +0 -10
- package/assets/commands/spec-onboard.md +0 -10
- package/assets/commands/spec-update.md +0 -10
- package/assets/commands/stopit.md +0 -10
- package/assets/commands/summary.md +0 -10
- package/assets/commands/task-prepare-batch.md +0 -10
- package/assets/commands/task-prepare.md +0 -10
- package/assets/commands/task-review.md +0 -10
- package/assets/commands/task-triage.md +0 -10
- package/assets/commands/team-planning.md +0 -10
- package/assets/commands/team-retro.md +0 -10
- package/assets/commands/team-roadmap.md +0 -10
- package/assets/commands/team-slides-prompts.md +0 -10
- package/assets/commands/team-sprint-close.md +0 -10
- package/assets/commands/team-sprint-status.md +0 -10
- package/assets/commands/walkthrough.md +0 -10
- package/assets/plugins/autonomy-policy.js +0 -3
- package/assets/plugins/background-attempts.js +0 -3
- package/assets/plugins/goal-loop.js +0 -3
- package/assets/plugins/schedule.js +0 -3
- package/assets/plugins/zed-clickable-paths.js +0 -3
- package/dist/plugins/autonomy-policy.d.ts +0 -21
- package/dist/plugins/autonomy-policy.js +0 -71
- package/dist/plugins/background-attempts.d.ts +0 -76
- package/dist/plugins/background-attempts.js +0 -124
- package/dist/plugins/goal-loop.d.ts +0 -62
- package/dist/plugins/goal-loop.js +0 -99
- package/dist/plugins/schedule.d.ts +0 -36
- package/dist/plugins/schedule.js +0 -100
- package/dist/plugins/zed-clickable-paths.d.ts +0 -11
- package/dist/plugins/zed-clickable-paths.js +0 -10
- /package/{assets → dist/assets}/plugins/rtk.js +0 -0
- /package/{assets → dist/assets}/plugins/rules-injector.js +0 -0
- /package/{assets → dist/assets}/plugins/zed-bell.js +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { LifecycleError, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
2
|
+
export type ReconcileStatus = "current" | "retired" | "archive-pending" | "renamed" | "modified-managed" | "user-owned" | "unknown" | "conflict" | "diagnostic-state-only";
|
|
3
|
+
export type ReconcileItem = {
|
|
4
|
+
path: string;
|
|
5
|
+
status: ReconcileStatus;
|
|
6
|
+
reason: string;
|
|
7
|
+
sha256?: string;
|
|
8
|
+
replacement?: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type ReconcilePlan = {
|
|
11
|
+
schema_version: 1;
|
|
12
|
+
domain: "reconcile";
|
|
13
|
+
scope: Scope;
|
|
14
|
+
root: string;
|
|
15
|
+
inventory_version: string;
|
|
16
|
+
current: ReconcileItem[];
|
|
17
|
+
retired: ReconcileItem[];
|
|
18
|
+
"archive-pending": ReconcileItem[];
|
|
19
|
+
renamed: ReconcileItem[];
|
|
20
|
+
modified_managed: ReconcileItem[];
|
|
21
|
+
user_owned: ReconcileItem[];
|
|
22
|
+
unknown: ReconcileItem[];
|
|
23
|
+
conflicts: ReconcileItem[];
|
|
24
|
+
diagnostic_state_only: ReconcileItem[];
|
|
25
|
+
operations: Array<{
|
|
26
|
+
path: string;
|
|
27
|
+
operation: "remove" | "write";
|
|
28
|
+
sha256?: string;
|
|
29
|
+
}>;
|
|
30
|
+
digest: string;
|
|
31
|
+
receipt_expires_at?: string;
|
|
32
|
+
};
|
|
33
|
+
export type ReconcileResult = {
|
|
34
|
+
status: "ok";
|
|
35
|
+
applied: true;
|
|
36
|
+
plan: ReconcilePlan;
|
|
37
|
+
};
|
|
38
|
+
export declare class ReconcileError extends LifecycleError {
|
|
39
|
+
}
|
|
40
|
+
export declare function previewReconcile(scope: Scope, cwd?: string, home?: string): Promise<ReconcilePlan>;
|
|
41
|
+
/** Build the ownership classification without receipts, locks, or recovery. */
|
|
42
|
+
export declare function inspectReconcile(scope: Scope, cwd?: string, home?: string): Promise<ReconcilePlan>;
|
|
43
|
+
export declare function applyReconcile(scope: Scope, confirmationDigest: string, cwd?: string, home?: string, options?: TransactionOptions): Promise<ReconcileResult>;
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { applyTransaction, assertSafePath, consumeReceipt, destination, digest, LifecycleError, lifecycleRoot, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
7
|
+
import { archiveMutations } from "./installer.js";
|
|
8
|
+
const PACKAGE_NAME = "@kisev/skills-opencode";
|
|
9
|
+
const GENERIC_MANIFEST = ".skills-opencode-manifest.json";
|
|
10
|
+
const SEMANTIC_MANIFEST = ".skills-opencode/agent-profiles.manifest.json";
|
|
11
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
const assetsRoot = resolve(packageRoot, "dist", "assets");
|
|
13
|
+
const inventoryPath = resolve(assetsRoot, "migration-inventory.json");
|
|
14
|
+
export class ReconcileError extends LifecycleError {
|
|
15
|
+
}
|
|
16
|
+
function loadInventory() {
|
|
17
|
+
const value = JSON.parse(readFileSync(inventoryPath, "utf8"));
|
|
18
|
+
if (value.schema_version !== 2 ||
|
|
19
|
+
typeof value.inventory_version !== "string" ||
|
|
20
|
+
!Array.isArray(value.active_portable_skills) ||
|
|
21
|
+
!Array.isArray(value.records)) {
|
|
22
|
+
throw new ReconcileError("invalid_inventory", "Migration inventory has an unsupported schema");
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function scopeRoot(scope, cwd, home) {
|
|
27
|
+
return scope === "global" ? resolve(home) : resolve(cwd);
|
|
28
|
+
}
|
|
29
|
+
function portableRoot(scope, cwd, home) {
|
|
30
|
+
return resolve(scopeRoot(scope, cwd, home), ".agents", "skills");
|
|
31
|
+
}
|
|
32
|
+
function relativePath(root, target) {
|
|
33
|
+
const value = relative(resolve(root), resolve(target));
|
|
34
|
+
if (!value || value.startsWith(`..${sep}`) || value === "..")
|
|
35
|
+
throw new ReconcileError("unsafe_path", `Path is outside reconcile root: ${target}`);
|
|
36
|
+
return value.split(sep).join("/");
|
|
37
|
+
}
|
|
38
|
+
function safeTarget(root, path) {
|
|
39
|
+
try {
|
|
40
|
+
return destination(root, path);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof LifecycleError)
|
|
44
|
+
throw new ReconcileError(error.code, error.message);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function metadata(path) {
|
|
49
|
+
try {
|
|
50
|
+
return await lstat(path);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code === "ENOENT")
|
|
54
|
+
return undefined;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function regular(path) {
|
|
59
|
+
try {
|
|
60
|
+
await assertSafePath(path);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof LifecycleError && error.code === "unsafe_path")
|
|
64
|
+
return { unsafe: true };
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const info = await metadata(path);
|
|
68
|
+
if (!info)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (info.isSymbolicLink() || !info.isFile() || info.nlink !== 1)
|
|
71
|
+
return { unsafe: true };
|
|
72
|
+
return { content: await readFile(path), mode: Number(info.mode) & 0o777 };
|
|
73
|
+
}
|
|
74
|
+
async function filesUnder(root) {
|
|
75
|
+
const info = await metadata(root);
|
|
76
|
+
if (!info)
|
|
77
|
+
return [];
|
|
78
|
+
if (info.isSymbolicLink() || !info.isDirectory())
|
|
79
|
+
return [{ relative: "", absolute: root, unsafe: true }];
|
|
80
|
+
const result = [];
|
|
81
|
+
async function visit(directory) {
|
|
82
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
83
|
+
const absolute = join(directory, entry.name);
|
|
84
|
+
const child = await metadata(absolute);
|
|
85
|
+
if (!child || child.isSymbolicLink()) {
|
|
86
|
+
result.push({ relative: relativePath(root, absolute), absolute, unsafe: true });
|
|
87
|
+
}
|
|
88
|
+
else if (child.isDirectory()) {
|
|
89
|
+
await visit(absolute);
|
|
90
|
+
}
|
|
91
|
+
else if (child.isFile() && child.nlink === 1) {
|
|
92
|
+
result.push({
|
|
93
|
+
relative: relativePath(root, absolute),
|
|
94
|
+
absolute,
|
|
95
|
+
content: await readFile(absolute),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
result.push({ relative: relativePath(root, absolute), absolute, unsafe: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
await visit(root);
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function item(path, status, reason, content, replacement) {
|
|
107
|
+
return {
|
|
108
|
+
path,
|
|
109
|
+
status,
|
|
110
|
+
reason,
|
|
111
|
+
...(content ? { sha256: sha256(content) } : {}),
|
|
112
|
+
...(replacement !== undefined ? { replacement } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function add(target, value) {
|
|
116
|
+
target[value.status].push(value);
|
|
117
|
+
}
|
|
118
|
+
async function packageAssets() {
|
|
119
|
+
const result = new Map();
|
|
120
|
+
for (const category of ["commands", "plugins"]) {
|
|
121
|
+
const directory = resolve(assetsRoot, category);
|
|
122
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
123
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
124
|
+
throw new ReconcileError("unsafe_path", `Package asset is not regular: ${entry.name}`);
|
|
125
|
+
result.set(`${category}/${entry.name}`, await readFile(join(directory, entry.name)));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
function parseGenericManifest(raw) {
|
|
131
|
+
let value;
|
|
132
|
+
try {
|
|
133
|
+
value = JSON.parse(raw.toString("utf8"));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new ReconcileError("invalid_manifest", "Installation manifest is not valid JSON");
|
|
137
|
+
}
|
|
138
|
+
const manifest = value;
|
|
139
|
+
if ((manifest.schema_version !== 1 && manifest.schema_version !== 2) ||
|
|
140
|
+
manifest.package !== PACKAGE_NAME ||
|
|
141
|
+
!manifest.files ||
|
|
142
|
+
typeof manifest.files !== "object" ||
|
|
143
|
+
Array.isArray(manifest.files))
|
|
144
|
+
throw new ReconcileError("invalid_manifest", "Installation manifest has an unsupported identity");
|
|
145
|
+
const files = {};
|
|
146
|
+
for (const [path, record] of Object.entries(manifest.files)) {
|
|
147
|
+
if (!/^[a-f0-9]{64}$/.test(String(record?.sha256)))
|
|
148
|
+
throw new ReconcileError("invalid_manifest", `Installation manifest has invalid hash: ${path}`);
|
|
149
|
+
files[path] = { sha256: record.sha256 };
|
|
150
|
+
}
|
|
151
|
+
return files;
|
|
152
|
+
}
|
|
153
|
+
function retiredRecords(inventory, scope) {
|
|
154
|
+
const commandRecords = Object.entries(inventory.retired_command_hashes ?? {}).map(([installed_path, sha256]) => ({
|
|
155
|
+
id: `package-command:${installed_path}`,
|
|
156
|
+
kind: "package-command",
|
|
157
|
+
historical_path: `packages/opencode/assets/${installed_path}`,
|
|
158
|
+
installed_path,
|
|
159
|
+
introduced: { ref: "5f09d504758b0e99ad9c0306df796411fbcf0f4a", version: "1.2.0" },
|
|
160
|
+
retired: { ref: "5f09d504758b0e99ad9c0306df796411fbcf0f4a", version: "2.0.0" },
|
|
161
|
+
replacement: null,
|
|
162
|
+
scopes: ["project", "global"],
|
|
163
|
+
sha256,
|
|
164
|
+
}));
|
|
165
|
+
return [...inventory.records, ...commandRecords].filter((record) => record.retired && record.scopes.includes(scope));
|
|
166
|
+
}
|
|
167
|
+
function diagnostic(scope, cwd, home) {
|
|
168
|
+
const state = lifecycleRoot(scope, cwd, home);
|
|
169
|
+
return [
|
|
170
|
+
item(`${state}/goal`, "diagnostic-state-only", "runtime state is not inspected or migrated"),
|
|
171
|
+
item(`${state}/multi-run`, "diagnostic-state-only", "runtime state is not inspected or migrated"),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
async function build(scope, cwd = process.cwd(), home = homedir()) {
|
|
175
|
+
const inventory = loadInventory();
|
|
176
|
+
const root = scopeRoot(scope, cwd, home);
|
|
177
|
+
const deployment = resolve(root, scope === "global" ? ".config/opencode" : ".opencode");
|
|
178
|
+
const portable = portableRoot(scope, cwd, home);
|
|
179
|
+
const currentAssets = await packageAssets();
|
|
180
|
+
const retired = retiredRecords(inventory, scope);
|
|
181
|
+
const groups = {
|
|
182
|
+
current: [],
|
|
183
|
+
retired: [],
|
|
184
|
+
"archive-pending": [],
|
|
185
|
+
renamed: [],
|
|
186
|
+
"modified-managed": [],
|
|
187
|
+
"user-owned": [],
|
|
188
|
+
unknown: [],
|
|
189
|
+
conflict: [],
|
|
190
|
+
"diagnostic-state-only": [],
|
|
191
|
+
};
|
|
192
|
+
const mutations = [];
|
|
193
|
+
const archiveCandidates = [];
|
|
194
|
+
const managed = new Map();
|
|
195
|
+
const manifestPath = join(deployment, GENERIC_MANIFEST);
|
|
196
|
+
const manifestValue = await regular(manifestPath);
|
|
197
|
+
let manifestRaw;
|
|
198
|
+
let manifestFiles = {};
|
|
199
|
+
if (manifestValue && "unsafe" in manifestValue) {
|
|
200
|
+
add(groups, item(relativePath(root, manifestPath), "conflict", "installation manifest is a symlink or non-regular file"));
|
|
201
|
+
}
|
|
202
|
+
else if (manifestValue) {
|
|
203
|
+
manifestRaw = manifestValue.content;
|
|
204
|
+
try {
|
|
205
|
+
manifestFiles = parseGenericManifest(manifestRaw);
|
|
206
|
+
for (const [path, record] of Object.entries(manifestFiles))
|
|
207
|
+
managed.set(path, record);
|
|
208
|
+
add(groups, item(relativePath(root, manifestPath), "current", "package ownership manifest is valid", manifestRaw));
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
add(groups, item(relativePath(root, manifestPath), "conflict", error instanceof Error ? error.message : "installation manifest is invalid", manifestRaw));
|
|
212
|
+
manifestRaw = undefined;
|
|
213
|
+
manifestFiles = {};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const retiredByPath = new Map(retired
|
|
217
|
+
.filter((record) => record.kind !== "portable-skill" && record.kind !== "installation-metadata")
|
|
218
|
+
.map((record) => [record.installed_path, record]));
|
|
219
|
+
for (const [path, record] of retiredByPath) {
|
|
220
|
+
const target = safeTarget(deployment, path);
|
|
221
|
+
const value = await regular(target);
|
|
222
|
+
const manifestRecord = manifestFiles[path];
|
|
223
|
+
if (value && "unsafe" in value) {
|
|
224
|
+
add(groups, item(relativePath(root, target), "conflict", "retired asset is a symlink or non-regular file"));
|
|
225
|
+
}
|
|
226
|
+
else if (value &&
|
|
227
|
+
record.sha256 === sha256(value.content) &&
|
|
228
|
+
(!manifestRecord || manifestRecord.sha256 === sha256(value.content))) {
|
|
229
|
+
add(groups, item(relativePath(root, target), "archive-pending", record.replacement
|
|
230
|
+
? "exact historical SHA-256 proves renamed ownership; archive lifecycle is pending"
|
|
231
|
+
: "exact historical SHA-256 proves retired public ownership; archive lifecycle is pending", value.content, record.replacement));
|
|
232
|
+
const kind = record.kind === "package-command"
|
|
233
|
+
? "command"
|
|
234
|
+
: record.kind === "package-plugin"
|
|
235
|
+
? "plugin"
|
|
236
|
+
: "state";
|
|
237
|
+
archiveCandidates.push({
|
|
238
|
+
path: relativePath(root, target),
|
|
239
|
+
record: { sha256: record.sha256, mode: 0o644, kind },
|
|
240
|
+
content: value.content,
|
|
241
|
+
reason: "retired inventory asset",
|
|
242
|
+
kind,
|
|
243
|
+
});
|
|
244
|
+
mutations.push({
|
|
245
|
+
path: relativePath(root, target),
|
|
246
|
+
operation: "remove",
|
|
247
|
+
expected: { sha256: sha256(value.content) },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
else if (!value && manifestRecord && record.sha256 === manifestRecord.sha256) {
|
|
251
|
+
add(groups, item(relativePath(root, target), "archive-pending", "stale exact ownership record; archive lifecycle is pending", undefined, record.replacement));
|
|
252
|
+
}
|
|
253
|
+
else if (value) {
|
|
254
|
+
add(groups, item(relativePath(root, target), "conflict", "retired asset is modified or ownership is ambiguous", value.content));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const [path, content] of currentAssets) {
|
|
258
|
+
const target = safeTarget(deployment, path);
|
|
259
|
+
const value = await regular(target);
|
|
260
|
+
const manifestRecord = manifestFiles[path] ?? managed.get(path);
|
|
261
|
+
if (!value)
|
|
262
|
+
continue;
|
|
263
|
+
if ("unsafe" in value) {
|
|
264
|
+
add(groups, item(relativePath(root, target), "conflict", "managed asset is a symlink or non-regular file"));
|
|
265
|
+
}
|
|
266
|
+
else if (manifestRecord &&
|
|
267
|
+
manifestRecord.sha256 === sha256(value.content) &&
|
|
268
|
+
sha256(content) === sha256(value.content)) {
|
|
269
|
+
add(groups, item(relativePath(root, target), "current", "current canonical package asset", value.content));
|
|
270
|
+
}
|
|
271
|
+
else if (manifestRecord) {
|
|
272
|
+
add(groups, item(relativePath(root, target), "modified-managed", "managed asset differs from its recorded SHA-256", value.content));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
add(groups, item(relativePath(root, target), "user-owned", "asset exists without package ownership evidence", value.content));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const path of Object.keys(manifestFiles).sort()) {
|
|
279
|
+
if (currentAssets.has(path) || retiredByPath.has(path))
|
|
280
|
+
continue;
|
|
281
|
+
let target;
|
|
282
|
+
try {
|
|
283
|
+
target = safeTarget(deployment, path);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
add(groups, item(path, "conflict", error instanceof Error ? error.message : "manifest path is unsafe"));
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const value = await regular(target);
|
|
290
|
+
if (value && "unsafe" in value)
|
|
291
|
+
add(groups, item(relativePath(root, target), "conflict", "manifest target is a symlink or non-regular file"));
|
|
292
|
+
else if (value)
|
|
293
|
+
add(groups, item(relativePath(root, target), "unknown", "manifest entry is outside the current public inventory", value.content));
|
|
294
|
+
else
|
|
295
|
+
add(groups, item(relativePath(root, target), "unknown", "manifest entry has no current asset"));
|
|
296
|
+
}
|
|
297
|
+
for (const category of ["commands", "plugins"]) {
|
|
298
|
+
const directory = resolve(deployment, category);
|
|
299
|
+
const entries = await metadata(directory);
|
|
300
|
+
if (!entries)
|
|
301
|
+
continue;
|
|
302
|
+
if (entries.isSymbolicLink() || !entries.isDirectory()) {
|
|
303
|
+
add(groups, item(relativePath(root, directory), "conflict", "asset directory is a symlink or non-directory"));
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
for (const entry of await readdir(directory)) {
|
|
307
|
+
const path = `${category}/${entry}`;
|
|
308
|
+
if (currentAssets.has(path) || retiredByPath.has(path) || manifestFiles[path])
|
|
309
|
+
continue;
|
|
310
|
+
const value = await regular(join(directory, entry));
|
|
311
|
+
if (value && "unsafe" in value)
|
|
312
|
+
add(groups, item(relativePath(root, join(directory, entry)), "conflict", "unmanaged asset is a symlink or non-regular file"));
|
|
313
|
+
else if (value)
|
|
314
|
+
add(groups, item(relativePath(root, join(directory, entry)), "user-owned", "asset has no public package ownership evidence", value.content));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const semanticPath = safeTarget(deployment, SEMANTIC_MANIFEST);
|
|
318
|
+
const semanticValue = await regular(semanticPath);
|
|
319
|
+
const agentHashes = new Map();
|
|
320
|
+
if (semanticValue && "unsafe" in semanticValue) {
|
|
321
|
+
add(groups, item(relativePath(root, semanticPath), "conflict", "agent ownership manifest is a symlink or non-regular file"));
|
|
322
|
+
}
|
|
323
|
+
else if (semanticValue) {
|
|
324
|
+
try {
|
|
325
|
+
const parsed = JSON.parse(semanticValue.content.toString("utf8"));
|
|
326
|
+
for (const [name, record] of Object.entries(parsed.profiles ?? {})) {
|
|
327
|
+
if (typeof record.rendered_sha256 === "string")
|
|
328
|
+
agentHashes.set(`${name}.md`, record.rendered_sha256);
|
|
329
|
+
}
|
|
330
|
+
add(groups, item(relativePath(root, semanticPath), "current", "agent ownership manifest is valid", semanticValue.content));
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
add(groups, item(relativePath(root, semanticPath), "conflict", "agent ownership manifest is not valid JSON"));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const agentDirectory = resolve(deployment, "agents");
|
|
337
|
+
const agentInfo = await metadata(agentDirectory);
|
|
338
|
+
if (agentInfo?.isSymbolicLink() || (agentInfo && !agentInfo.isDirectory())) {
|
|
339
|
+
add(groups, item(relativePath(root, agentDirectory), "conflict", "agent directory is a symlink or non-directory"));
|
|
340
|
+
}
|
|
341
|
+
else if (agentInfo?.isDirectory()) {
|
|
342
|
+
for (const entry of await readdir(agentDirectory)) {
|
|
343
|
+
const target = join(agentDirectory, entry);
|
|
344
|
+
const value = await regular(target);
|
|
345
|
+
if (value && "unsafe" in value)
|
|
346
|
+
add(groups, item(relativePath(root, target), "conflict", "agent is a symlink or non-regular file"));
|
|
347
|
+
else if (!value)
|
|
348
|
+
add(groups, item(relativePath(root, target), "unknown", "agent ownership record has no file"));
|
|
349
|
+
else if (agentHashes.get(entry) === sha256(value.content))
|
|
350
|
+
add(groups, item(relativePath(root, target), "current", "semantic agent manifest proves package ownership", value.content));
|
|
351
|
+
else if (agentHashes.has(entry))
|
|
352
|
+
add(groups, item(relativePath(root, target), "modified-managed", "semantic agent manifest hash differs", value.content));
|
|
353
|
+
else
|
|
354
|
+
add(groups, item(relativePath(root, target), "user-owned", "agent has no semantic ownership record", value.content));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
for (const record of retired.filter((value) => value.kind === "portable-skill")) {
|
|
358
|
+
const directory = resolve(portable, record.installed_path.replace(/^skills\//, ""));
|
|
359
|
+
const files = await filesUnder(directory);
|
|
360
|
+
const expected = record.files ?? {};
|
|
361
|
+
const safe = files.length === Object.keys(expected).length &&
|
|
362
|
+
files.every((file) => !file.unsafe && expected[file.relative] === sha256(file.content));
|
|
363
|
+
if (files.some((file) => file.unsafe)) {
|
|
364
|
+
add(groups, item(relativePath(root, directory), "conflict", "retired skill contains a symlink or unsafe file"));
|
|
365
|
+
}
|
|
366
|
+
else if (safe) {
|
|
367
|
+
for (const file of files) {
|
|
368
|
+
const target = relativePath(root, file.absolute);
|
|
369
|
+
add(groups, item(target, "archive-pending", "exact historical SHA-256 proves retired portable ownership; archive lifecycle is pending", file.content));
|
|
370
|
+
archiveCandidates.push({
|
|
371
|
+
path: relativePath(root, file.absolute),
|
|
372
|
+
record: { sha256: sha256(file.content), mode: 0o644, kind: "state" },
|
|
373
|
+
content: file.content,
|
|
374
|
+
reason: "retired portable skill",
|
|
375
|
+
kind: "state",
|
|
376
|
+
});
|
|
377
|
+
mutations.push({
|
|
378
|
+
path: relativePath(root, file.absolute),
|
|
379
|
+
operation: "remove",
|
|
380
|
+
expected: { sha256: sha256(file.content) },
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (files.length) {
|
|
385
|
+
for (const file of files)
|
|
386
|
+
add(groups, item(relativePath(root, file.absolute), "conflict", "retired skill is modified or ownership is ambiguous", file.content));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (archiveCandidates.length && manifestRaw) {
|
|
390
|
+
for (const candidate of archiveCandidates) {
|
|
391
|
+
delete manifestFiles[candidate.path];
|
|
392
|
+
delete manifestFiles[candidate.path.replace(/^\.opencode\//, "")];
|
|
393
|
+
}
|
|
394
|
+
const value = JSON.parse(manifestRaw.toString("utf8"));
|
|
395
|
+
const next = Buffer.from(`${stable({ ...value, files: manifestFiles })}\n`);
|
|
396
|
+
mutations.push({
|
|
397
|
+
path: relativePath(root, manifestPath),
|
|
398
|
+
operation: "write",
|
|
399
|
+
content: next,
|
|
400
|
+
mode: 0o600,
|
|
401
|
+
expected: { sha256: sha256(manifestRaw) },
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
mutations.push(...(await archiveMutations(archiveCandidates, scope, cwd, home, inventory.inventory_version)));
|
|
405
|
+
const portableInfo = await metadata(portable);
|
|
406
|
+
if (portableInfo?.isSymbolicLink() || (portableInfo && !portableInfo.isDirectory())) {
|
|
407
|
+
add(groups, item(relativePath(root, portable), "conflict", "portable skill root is a symlink or non-directory"));
|
|
408
|
+
}
|
|
409
|
+
for (const entry of portableInfo?.isDirectory() ? await readdir(portable) : []) {
|
|
410
|
+
const directory = resolve(portable, entry);
|
|
411
|
+
if (retired.some((record) => record.kind === "portable-skill" && record.installed_path === `skills/${entry}`))
|
|
412
|
+
continue;
|
|
413
|
+
const path = relativePath(root, directory);
|
|
414
|
+
add(groups, item(path, inventory.active_portable_skills.includes(entry) ? "current" : "unknown", inventory.active_portable_skills.includes(entry)
|
|
415
|
+
? "current portable skill source"
|
|
416
|
+
: "portable skill is outside the canonical inventory"));
|
|
417
|
+
}
|
|
418
|
+
const operations = mutations.map((mutation) => ({
|
|
419
|
+
path: mutation.path,
|
|
420
|
+
operation: mutation.operation,
|
|
421
|
+
...(mutation.operation === "write" ? { sha256: sha256(mutation.content) } : {}),
|
|
422
|
+
}));
|
|
423
|
+
const base = {
|
|
424
|
+
schema_version: 1,
|
|
425
|
+
domain: "reconcile",
|
|
426
|
+
scope,
|
|
427
|
+
root,
|
|
428
|
+
inventory_version: inventory.inventory_version,
|
|
429
|
+
current: groups.current.sort((left, right) => left.path.localeCompare(right.path)),
|
|
430
|
+
retired: groups.retired.sort((left, right) => left.path.localeCompare(right.path)),
|
|
431
|
+
"archive-pending": groups["archive-pending"].sort((left, right) => left.path.localeCompare(right.path)),
|
|
432
|
+
renamed: groups.renamed.sort((left, right) => left.path.localeCompare(right.path)),
|
|
433
|
+
modified_managed: groups["modified-managed"].sort((left, right) => left.path.localeCompare(right.path)),
|
|
434
|
+
user_owned: groups["user-owned"].sort((left, right) => left.path.localeCompare(right.path)),
|
|
435
|
+
unknown: groups.unknown.sort((left, right) => left.path.localeCompare(right.path)),
|
|
436
|
+
conflicts: groups.conflict.sort((left, right) => left.path.localeCompare(right.path)),
|
|
437
|
+
diagnostic_state_only: diagnostic(scope, cwd, home),
|
|
438
|
+
operations: operations.sort((left, right) => left.path.localeCompare(right.path)),
|
|
439
|
+
};
|
|
440
|
+
return { plan: { ...base, digest: digest(base) }, mutations };
|
|
441
|
+
}
|
|
442
|
+
export async function previewReconcile(scope, cwd = process.cwd(), home = homedir()) {
|
|
443
|
+
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
444
|
+
const root = scopeRoot(scope, cwd, home);
|
|
445
|
+
try {
|
|
446
|
+
return await withLifecycleLock(stateRoot, async () => {
|
|
447
|
+
if (await recoverTransaction(root, stateRoot))
|
|
448
|
+
throw new ReconcileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
449
|
+
const built = await build(scope, cwd, home);
|
|
450
|
+
const receipt = await saveReceipt(stateRoot, "reconcile", scope, root, {
|
|
451
|
+
digest: built.plan.digest,
|
|
452
|
+
});
|
|
453
|
+
return { ...built.plan, digest: receipt.digest, receipt_expires_at: receipt.expires_at };
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
if (error instanceof ReconcileError)
|
|
458
|
+
throw error;
|
|
459
|
+
if (error instanceof LifecycleError)
|
|
460
|
+
throw new ReconcileError(error.code, error.message);
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/** Build the ownership classification without receipts, locks, or recovery. */
|
|
465
|
+
export async function inspectReconcile(scope, cwd = process.cwd(), home = homedir()) {
|
|
466
|
+
return (await build(scope, cwd, home)).plan;
|
|
467
|
+
}
|
|
468
|
+
export async function applyReconcile(scope, confirmationDigest, cwd = process.cwd(), home = homedir(), options = {}) {
|
|
469
|
+
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
470
|
+
const root = scopeRoot(scope, cwd, home);
|
|
471
|
+
try {
|
|
472
|
+
return await withLifecycleLock(stateRoot, async () => {
|
|
473
|
+
if (await recoverTransaction(root, stateRoot))
|
|
474
|
+
throw new ReconcileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
475
|
+
const payload = (await consumeReceipt(stateRoot, {
|
|
476
|
+
digest: confirmationDigest,
|
|
477
|
+
kind: "reconcile",
|
|
478
|
+
scope,
|
|
479
|
+
root,
|
|
480
|
+
}));
|
|
481
|
+
const built = await build(scope, cwd, home);
|
|
482
|
+
if (built.plan.digest !== payload.digest)
|
|
483
|
+
throw new ReconcileError("stale_plan", "Reconcile inventory changed after preview");
|
|
484
|
+
if (built.plan.conflicts.length || built.plan.modified_managed.length)
|
|
485
|
+
throw new ReconcileError("conflict", "Reconcile contains unsafe ownership conflicts");
|
|
486
|
+
await applyTransaction(root, stateRoot, built.mutations, {
|
|
487
|
+
...options,
|
|
488
|
+
validateFinal: async () => {
|
|
489
|
+
await options.validateFinal?.();
|
|
490
|
+
const final = await build(scope, cwd, home);
|
|
491
|
+
if (final.plan.retired.length ||
|
|
492
|
+
final.plan["archive-pending"].length ||
|
|
493
|
+
final.plan.operations.length)
|
|
494
|
+
throw new ReconcileError("final_validation_failed", "Retired assets remain after reconcile");
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
return { status: "ok", applied: true, plan: { ...built.plan, digest: confirmationDigest } };
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
catch (error) {
|
|
501
|
+
if (error instanceof ReconcileError)
|
|
502
|
+
throw error;
|
|
503
|
+
if (error instanceof LifecycleError)
|
|
504
|
+
throw new ReconcileError(error.code, error.message);
|
|
505
|
+
throw error;
|
|
506
|
+
}
|
|
507
|
+
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export type CommandRegistration = {
|
|
2
2
|
name: string;
|
|
3
3
|
skill?: string;
|
|
4
|
-
packageTool?: "capabilities" | "
|
|
5
|
-
packageAction?: "list" | "model_set" | "critic_add" | "critic_remove";
|
|
4
|
+
packageTool?: "capabilities" | "doctor" | "reconcile" | "agent_profiles";
|
|
6
5
|
description: string;
|
|
7
|
-
mode?: string;
|
|
8
6
|
};
|
|
9
|
-
export declare const COMMAND_REGISTRY:
|
|
7
|
+
export declare const COMMAND_REGISTRY: {
|
|
8
|
+
name: string;
|
|
9
|
+
skill?: string;
|
|
10
|
+
packageTool?: "capabilities" | "doctor" | "reconcile" | "agent_profiles";
|
|
11
|
+
description: string;
|
|
12
|
+
}[];
|
|
10
13
|
export declare function renderCommand(command: CommandRegistration): string;
|