@kisev/skills-opencode 2.0.3 → 2.0.4
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 +17 -8
- package/README.ru.md +16 -8
- package/dist/agent-profiles.d.ts +4 -1
- package/dist/agent-profiles.js +13 -4
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/cli-output.js +15 -4
- package/dist/cli.js +1 -1
- package/dist/installer.d.ts +4 -1
- package/dist/installer.js +12 -4
- package/dist/lifecycle.d.ts +11 -1
- package/dist/lifecycle.js +58 -20
- package/dist/reconcile.d.ts +6 -2
- package/dist/reconcile.js +24 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[Русский](README.ru.md)
|
|
4
4
|
|
|
5
|
-
`@kisev/skills-opencode@2.0.
|
|
5
|
+
`@kisev/skills-opencode@2.0.4` is the optional OpenCode-specific layer. Portable
|
|
6
6
|
skills have a separate lifecycle and must be installed independently through the
|
|
7
7
|
[root portable flow](../../README.md).
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ Install in the repository's npm project and run the CLI from that project root:
|
|
|
30
30
|
|
|
31
31
|
```shell
|
|
32
32
|
cd /path/to/project
|
|
33
|
-
npm install --save-exact @kisev/skills-opencode@2.0.
|
|
33
|
+
npm install --save-exact @kisev/skills-opencode@2.0.4
|
|
34
34
|
npm exec -- skills-opencode install --scope project --dry-run
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -45,7 +45,7 @@ Use `~/.config/opencode` as the persistent npm project:
|
|
|
45
45
|
mkdir -p "$HOME/.config/opencode"
|
|
46
46
|
cd "$HOME/.config/opencode"
|
|
47
47
|
test -f package.json || npm init --yes
|
|
48
|
-
npm install --save-exact @kisev/skills-opencode@2.0.
|
|
48
|
+
npm install --save-exact @kisev/skills-opencode@2.0.4
|
|
49
49
|
npm exec -- skills-opencode install --scope global --dry-run
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -100,8 +100,12 @@ under `~/.config/opencode`. Restart OpenCode after activation or asset changes.
|
|
|
100
100
|
## Preview and Confirm
|
|
101
101
|
|
|
102
102
|
Every mutation begins with `--dry-run`. The preview reports operations,
|
|
103
|
-
conflicts, restart requirements, receipt expiry,
|
|
104
|
-
confirmation command.
|
|
103
|
+
conflicts, restart requirements, receipt expiry, separate plan and confirmation
|
|
104
|
+
digests, and the exact confirmation command. If reconcile reports modified
|
|
105
|
+
managed files or ownership conflicts, it is blocked: no receipt or Apply command
|
|
106
|
+
is issued. Install or upgrade the current package first, apply its exact
|
|
107
|
+
installer confirmation, then repeat reconcile; resolve ownership conflicts
|
|
108
|
+
manually.
|
|
105
109
|
|
|
106
110
|
```shell
|
|
107
111
|
npm exec -- skills-opencode install --scope project --dry-run
|
|
@@ -110,8 +114,13 @@ npm exec -- skills-opencode install --scope project --dry-run
|
|
|
110
114
|
The mandatory OpenCode flow is: persistent npm install, `install --dry-run`, the
|
|
111
115
|
exact confirmation command printed by that preview, add the package to the
|
|
112
116
|
user-owned `plugin` entry, and restart OpenCode. Use the persistent npm project
|
|
113
|
-
at `~/.config/opencode` for global commands.
|
|
114
|
-
|
|
117
|
+
at `~/.config/opencode` for global commands. Install or upgrade the package and
|
|
118
|
+
apply its installer plan before every reconcile.
|
|
119
|
+
|
|
120
|
+
The preview has a deterministic `plan_digest` and a unique
|
|
121
|
+
`confirmation_digest`. A later dry-run in the same scope supersedes any older
|
|
122
|
+
unconsumed preview, including previews from another package or agent operation;
|
|
123
|
+
the older confirmation is rejected.
|
|
115
124
|
|
|
116
125
|
Run the command printed by the preview, including all selection flags. Receipts
|
|
117
126
|
are private, valid for 10 minutes, single-use, and bound to the action, scope,
|
|
@@ -140,7 +149,7 @@ version, preview and confirm `install` with the same scope and desired selection
|
|
|
140
149
|
then restart OpenCode:
|
|
141
150
|
|
|
142
151
|
```shell
|
|
143
|
-
npm install --save-exact @kisev/skills-opencode@2.0.
|
|
152
|
+
npm install --save-exact @kisev/skills-opencode@2.0.4
|
|
144
153
|
npm exec -- skills-opencode install --scope project --dry-run
|
|
145
154
|
```
|
|
146
155
|
|
package/README.ru.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | [Русский](README.ru.md)
|
|
4
4
|
|
|
5
|
-
`@kisev/skills-opencode@2.0.
|
|
5
|
+
`@kisev/skills-opencode@2.0.4` - optional OpenCode-specific слой. У portable
|
|
6
6
|
skills отдельный lifecycle: их нужно установить независимо через
|
|
7
7
|
[корневую инструкцию](../../README.ru.md).
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ assets или portable skills и не меняют OpenCode configuration.
|
|
|
30
30
|
|
|
31
31
|
```shell
|
|
32
32
|
cd /path/to/project
|
|
33
|
-
npm install --save-exact @kisev/skills-opencode@2.0.
|
|
33
|
+
npm install --save-exact @kisev/skills-opencode@2.0.4
|
|
34
34
|
npm exec -- skills-opencode install --scope project --dry-run
|
|
35
35
|
```
|
|
36
36
|
|
|
@@ -45,7 +45,7 @@ Package остаётся в project `node_modules`, confirmed assets разме
|
|
|
45
45
|
mkdir -p "$HOME/.config/opencode"
|
|
46
46
|
cd "$HOME/.config/opencode"
|
|
47
47
|
test -f package.json || npm init --yes
|
|
48
|
-
npm install --save-exact @kisev/skills-opencode@2.0.
|
|
48
|
+
npm install --save-exact @kisev/skills-opencode@2.0.4
|
|
49
49
|
npm exec -- skills-opencode install --scope global --dry-run
|
|
50
50
|
```
|
|
51
51
|
|
|
@@ -101,8 +101,12 @@ OpenCode.
|
|
|
101
101
|
## Preview и confirm
|
|
102
102
|
|
|
103
103
|
Каждая mutation начинается с `--dry-run`. Preview показывает operations,
|
|
104
|
-
conflicts, необходимость restart, срок действия receipt,
|
|
105
|
-
confirmation command.
|
|
104
|
+
conflicts, необходимость restart, срок действия receipt, отдельные plan и
|
|
105
|
+
confirmation digests и точную confirmation command. Если reconcile показывает
|
|
106
|
+
modified managed files или ownership conflicts, он блокируется: receipt и Apply
|
|
107
|
+
command не создаются. Сначала установите или обновите текущий package,
|
|
108
|
+
примените его exact installer confirmation, затем повторите reconcile; ownership
|
|
109
|
+
conflicts нужно разрешить вручную.
|
|
106
110
|
|
|
107
111
|
```shell
|
|
108
112
|
npm exec -- skills-opencode install --scope project --dry-run
|
|
@@ -111,8 +115,12 @@ npm exec -- skills-opencode install --scope project --dry-run
|
|
|
111
115
|
Обязательный OpenCode flow: persistent npm install, `install --dry-run`, exact
|
|
112
116
|
confirmation command из preview, добавление package в user-owned `plugin` и
|
|
113
117
|
перезапуск OpenCode. Для global-команд используйте persistent npm project в
|
|
114
|
-
`~/.config/opencode`.
|
|
115
|
-
|
|
118
|
+
`~/.config/opencode`. Перед каждым reconcile установите или обновите package и
|
|
119
|
+
примените его installer plan.
|
|
120
|
+
|
|
121
|
+
Preview имеет deterministic `plan_digest` и unique `confirmation_digest`. Новый
|
|
122
|
+
dry-run в том же scope supersede-ит любой старый unconsumed preview, включая
|
|
123
|
+
preview другой package или agent operation; старая confirmation отклоняется.
|
|
116
124
|
|
|
117
125
|
Выполните команду из preview со всеми selection flags. Receipts приватны,
|
|
118
126
|
действуют 10 минут, применяются один раз и связаны с action, scope, root и
|
|
@@ -141,7 +149,7 @@ incomplete probe failure.
|
|
|
141
149
|
перезапустите OpenCode:
|
|
142
150
|
|
|
143
151
|
```shell
|
|
144
|
-
npm install --save-exact @kisev/skills-opencode@2.0.
|
|
152
|
+
npm install --save-exact @kisev/skills-opencode@2.0.4
|
|
145
153
|
npm exec -- skills-opencode install --scope project --dry-run
|
|
146
154
|
```
|
|
147
155
|
|
package/dist/agent-profiles.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LifecycleError, type FileMutation, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
1
|
+
import { LifecycleError, type SupersededPlan, type FileMutation, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
2
2
|
export { type Scope as AgentProfileScope } from "./lifecycle.js";
|
|
3
3
|
export declare const FIXED_AGENT_ROLES: readonly ["manager", "architect", "mapper", "worker", "review", "critic"];
|
|
4
4
|
export type FixedAgentRole = (typeof FIXED_AGENT_ROLES)[number];
|
|
@@ -70,6 +70,9 @@ export type AgentProfilePlan = {
|
|
|
70
70
|
root: string;
|
|
71
71
|
operations: AgentProfileOperation[];
|
|
72
72
|
critic_pool: string[];
|
|
73
|
+
plan_digest: string;
|
|
74
|
+
confirmation_digest?: string;
|
|
75
|
+
superseded_plan?: SupersededPlan;
|
|
73
76
|
digest: string;
|
|
74
77
|
receipt_expires_at?: string;
|
|
75
78
|
requires_restart: boolean;
|
package/dist/agent-profiles.js
CHANGED
|
@@ -520,9 +520,11 @@ export async function buildAgentProfilePlan(request, scope, cwd = process.cwd(),
|
|
|
520
520
|
critic_pool: desired?.critic_pool ?? finalManifest?.critic_pool ?? [],
|
|
521
521
|
requires_restart: mutations.some((item) => item.path.startsWith("agents/")),
|
|
522
522
|
};
|
|
523
|
+
const planDigest = planDigestBase(base, inventoryDigest, request);
|
|
523
524
|
const plan = {
|
|
524
525
|
...base,
|
|
525
|
-
|
|
526
|
+
plan_digest: planDigest,
|
|
527
|
+
digest: planDigest,
|
|
526
528
|
};
|
|
527
529
|
return {
|
|
528
530
|
plan,
|
|
@@ -639,8 +641,15 @@ export async function previewAgentProfileChange(request, scope, cwd = process.cw
|
|
|
639
641
|
if (await recoverTransaction(deploymentRoot(scope, cwd, home), stateRoot))
|
|
640
642
|
throw new AgentProfileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
641
643
|
const built = await buildAgentProfilePlan(request, scope, cwd, home);
|
|
642
|
-
const receipt = await saveReceipt(stateRoot, `agent:${request.action}`, scope, built.plan.root, { request,
|
|
643
|
-
return {
|
|
644
|
+
const receipt = await saveReceipt(stateRoot, `agent:${request.action}`, scope, built.plan.root, { request, plan_digest: built.plan.digest }, Date.now(), built.plan.digest);
|
|
645
|
+
return {
|
|
646
|
+
...built.plan,
|
|
647
|
+
plan_digest: built.plan.digest,
|
|
648
|
+
confirmation_digest: receipt.digest,
|
|
649
|
+
digest: receipt.digest,
|
|
650
|
+
receipt_expires_at: receipt.expires_at,
|
|
651
|
+
...(receipt.superseded_plan ? { superseded_plan: receipt.superseded_plan } : {}),
|
|
652
|
+
};
|
|
644
653
|
});
|
|
645
654
|
}
|
|
646
655
|
export async function applyAgentProfileChange(request, scope, confirmationDigest, cwd = process.cwd(), home = homedir(), options = {}) {
|
|
@@ -658,7 +667,7 @@ export async function applyAgentProfileChange(request, scope, confirmationDigest
|
|
|
658
667
|
if (stable(receipt.request) !== stable(request))
|
|
659
668
|
throw new AgentProfileError("confirmation_unknown", "Saved confirmation belongs to a different request");
|
|
660
669
|
const built = await buildAgentProfilePlan(request, scope, cwd, home);
|
|
661
|
-
if (built.plan.digest !== receipt.
|
|
670
|
+
if (built.plan.digest !== receipt.plan_digest)
|
|
662
671
|
throw new AgentProfileError("stale_plan", "Agent inventory changed after preview");
|
|
663
672
|
if (built.plan.operations.some((item) => item.operation === "conflict" && item.reason.includes("collision")))
|
|
664
673
|
throw new AgentProfileError("collision", "Exact-name user-owned collision blocks apply");
|
package/dist/catalog.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export declare const CATALOG: {
|
|
|
4
4
|
readonly agents: readonly ["manager", "architect", "mapper", "worker", "review", "critic"];
|
|
5
5
|
readonly package_commands: readonly ["capabilities", "doctor", "reconcile", "agent-profiles"];
|
|
6
6
|
readonly tools: readonly ["capabilities", "route", "doctor", "agent_profiles", "reconcile"];
|
|
7
|
-
readonly version: "2.0.
|
|
7
|
+
readonly version: "2.0.4";
|
|
8
8
|
};
|
package/dist/catalog.js
CHANGED
|
@@ -34,5 +34,5 @@ export const CATALOG = {
|
|
|
34
34
|
agents: ["manager", "architect", "mapper", "worker", "review", "critic"],
|
|
35
35
|
package_commands: ["capabilities", "doctor", "reconcile", "agent-profiles"],
|
|
36
36
|
tools: ["capabilities", "route", "doctor", "agent_profiles", "reconcile"],
|
|
37
|
-
version: "2.0.
|
|
37
|
+
version: "2.0.4",
|
|
38
38
|
};
|
package/dist/cli-output.js
CHANGED
|
@@ -119,9 +119,16 @@ export function renderPlan(plan, options) {
|
|
|
119
119
|
? `Restart required: ${plan.requires_restart ? "yes" : "no"}`
|
|
120
120
|
: `Restart after apply: ${plan.requires_restart ? "yes" : "no"}`);
|
|
121
121
|
if (!options.applied) {
|
|
122
|
+
if (plan.superseded_plan)
|
|
123
|
+
lines.push("", "Superseded plan:", ` kind: ${terminalSafe(plan.superseded_plan.kind)}`, ` confirmation: ${terminalSafe(plan.superseded_plan.confirmation_digest)}`, ` created: ${terminalSafe(plan.superseded_plan.created_at)}`, ` expires: ${terminalSafe(plan.superseded_plan.expires_at)}`);
|
|
122
124
|
if (plan.receipt_expires_at)
|
|
123
125
|
lines.push(`Confirmation expires: ${plan.receipt_expires_at}`);
|
|
124
|
-
|
|
126
|
+
if ("plan_digest" in plan)
|
|
127
|
+
lines.push(`Plan digest: ${plan.plan_digest}`);
|
|
128
|
+
if (plan.confirmation_digest ?? plan.digest) {
|
|
129
|
+
lines.push(`Confirmation digest: ${plan.confirmation_digest ?? plan.digest}`);
|
|
130
|
+
lines.push(`Digest: ${plan.confirmation_digest ?? plan.digest}`);
|
|
131
|
+
}
|
|
125
132
|
if (options.confirmationCommand)
|
|
126
133
|
lines.push("", "Apply:", ` ${options.confirmationCommand}`);
|
|
127
134
|
}
|
|
@@ -183,9 +190,6 @@ export function renderReconcile(plan, options) {
|
|
|
183
190
|
if (plan.modified_managed.length)
|
|
184
191
|
lines.push("", "Modified managed:", ...plan.modified_managed.map((entry) => ` ${terminalSafe(entry.path)} (${terminalSafe(entry.reason)})`));
|
|
185
192
|
if (!options.applied) {
|
|
186
|
-
lines.push("", `Digest: ${plan.digest}`);
|
|
187
|
-
if (plan.receipt_expires_at)
|
|
188
|
-
lines.push(`Confirmation expires: ${plan.receipt_expires_at}`);
|
|
189
193
|
const blocked = plan.modified_managed.length > 0 || plan.conflicts.length > 0;
|
|
190
194
|
if (blocked) {
|
|
191
195
|
lines.push("", "Blocked:");
|
|
@@ -195,6 +199,13 @@ export function renderReconcile(plan, options) {
|
|
|
195
199
|
lines.push(" Manually resolve every ownership conflict listed above before reconciling.");
|
|
196
200
|
}
|
|
197
201
|
else if (options.confirmationCommand) {
|
|
202
|
+
if (plan.superseded_plan)
|
|
203
|
+
lines.push("", "Superseded plan:", ` kind: ${terminalSafe(plan.superseded_plan.kind)}`, ` confirmation: ${terminalSafe(plan.superseded_plan.confirmation_digest)}`, ` created: ${terminalSafe(plan.superseded_plan.created_at)}`, ` expires: ${terminalSafe(plan.superseded_plan.expires_at)}`);
|
|
204
|
+
lines.push("", `Plan digest: ${plan.plan_digest}`);
|
|
205
|
+
if (plan.receipt_expires_at)
|
|
206
|
+
lines.push(`Confirmation expires: ${plan.receipt_expires_at}`);
|
|
207
|
+
if (plan.confirmation_digest ?? plan.digest)
|
|
208
|
+
lines.push(`Confirmation digest: ${plan.confirmation_digest ?? plan.digest}`, `Digest: ${plan.confirmation_digest ?? plan.digest}`);
|
|
198
209
|
lines.push("", "Apply:", ` ${options.confirmationCommand}`);
|
|
199
210
|
}
|
|
200
211
|
}
|
package/dist/cli.js
CHANGED
|
@@ -353,7 +353,7 @@ async function run(arguments_) {
|
|
|
353
353
|
if (options.json)
|
|
354
354
|
process.stdout.write(`${JSON.stringify({ status: "ok", applied: false, plan }, null, 2)}\n`);
|
|
355
355
|
else
|
|
356
|
-
process.stdout.write(renderReconcile(plan, { applied: false, confirmationCommand: shellCommand(["reconcile", "--scope", options.scope, "--confirm", plan.digest]) }));
|
|
356
|
+
process.stdout.write(renderReconcile(plan, { applied: false, confirmationCommand: plan.confirmable ? shellCommand(["reconcile", "--scope", options.scope, "--confirm", plan.confirmation_digest ?? plan.digest]) : undefined }));
|
|
357
357
|
}
|
|
358
358
|
else {
|
|
359
359
|
const applied = await applyReconcile(options.scope, options.confirm);
|
package/dist/installer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LifecycleError, type FileMutation, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
1
|
+
import { LifecycleError, type SupersededPlan, type FileMutation, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
2
2
|
import { CATALOG } from "./catalog.js";
|
|
3
3
|
import { type FixedAgentRole } from "./agent-profiles.js";
|
|
4
4
|
export type { Scope } from "./lifecycle.js";
|
|
@@ -25,6 +25,9 @@ export type Plan = {
|
|
|
25
25
|
package_version: string;
|
|
26
26
|
selection: InstallerSelection;
|
|
27
27
|
operations: PlanItem[];
|
|
28
|
+
plan_digest: string;
|
|
29
|
+
confirmation_digest?: string;
|
|
30
|
+
superseded_plan?: SupersededPlan;
|
|
28
31
|
digest: string;
|
|
29
32
|
receipt_expires_at?: string;
|
|
30
33
|
requires_restart: boolean;
|
package/dist/installer.js
CHANGED
|
@@ -404,8 +404,9 @@ async function build(action, scope, cwd = process.cwd(), home = homedir(), reque
|
|
|
404
404
|
}
|
|
405
405
|
const sorted = operations.sort((left, right) => left.path.localeCompare(right.path) || left.operation.localeCompare(right.operation));
|
|
406
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/")) };
|
|
407
|
+
const planDigest = digest(base);
|
|
407
408
|
return {
|
|
408
|
-
plan: { ...base,
|
|
409
|
+
plan: { ...base, plan_digest: planDigest, digest: planDigest },
|
|
409
410
|
mutations,
|
|
410
411
|
expectedManifest: manifestContent,
|
|
411
412
|
profiles,
|
|
@@ -419,8 +420,15 @@ export async function preview(action, scope, cwd = process.cwd(), home = homedir
|
|
|
419
420
|
if (await recoverTransaction(root, stateRoot))
|
|
420
421
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
421
422
|
const built = await build(action, scope, cwd, home, selection);
|
|
422
|
-
const receipt = await saveReceipt(stateRoot, `installer:${action}`, scope, root, {
|
|
423
|
-
return {
|
|
423
|
+
const receipt = await saveReceipt(stateRoot, `installer:${action}`, scope, root, { plan_digest: built.plan.digest }, Date.now(), built.plan.digest);
|
|
424
|
+
return {
|
|
425
|
+
...built.plan,
|
|
426
|
+
plan_digest: built.plan.digest,
|
|
427
|
+
confirmation_digest: receipt.digest,
|
|
428
|
+
digest: receipt.digest,
|
|
429
|
+
receipt_expires_at: receipt.expires_at,
|
|
430
|
+
...(receipt.superseded_plan ? { superseded_plan: receipt.superseded_plan } : {}),
|
|
431
|
+
};
|
|
424
432
|
});
|
|
425
433
|
}
|
|
426
434
|
catch (error) {
|
|
@@ -440,7 +448,7 @@ export async function apply(action, scope, confirmationDigest, cwd = process.cwd
|
|
|
440
448
|
throw new InstallerError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
441
449
|
const receipt = (await consumeReceipt(stateRoot, { digest: confirmationDigest, kind: `installer:${action}`, scope, root }));
|
|
442
450
|
const built = await build(action, scope, cwd, home, selection);
|
|
443
|
-
if (built.plan.digest !== receipt.
|
|
451
|
+
if (built.plan.digest !== receipt.plan_digest)
|
|
444
452
|
throw new InstallerError("stale_plan", "Installer plan changed after preview");
|
|
445
453
|
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")))) {
|
|
446
454
|
throw new InstallerError("conflict", "Installer plan contains an exact-name ownership conflict");
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export type FileMutation = {
|
|
|
21
21
|
operation: "remove";
|
|
22
22
|
expected: FileExpectation;
|
|
23
23
|
};
|
|
24
|
+
export type SupersededPlan = {
|
|
25
|
+
kind: string;
|
|
26
|
+
confirmation_digest: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
expires_at: string;
|
|
29
|
+
};
|
|
24
30
|
export declare function stable(value: unknown): string;
|
|
25
31
|
export declare function sha256(value: Buffer | string): string;
|
|
26
32
|
export declare function digest(value: unknown): string;
|
|
@@ -36,9 +42,11 @@ export declare function deploymentRoot(scope: Scope, cwd?: string, home?: string
|
|
|
36
42
|
export declare function lifecycleRoot(scope: Scope, cwd?: string, home?: string): string;
|
|
37
43
|
export declare function archiveRoot(scope: Scope, cwd?: string, home?: string): string;
|
|
38
44
|
export declare function withLifecycleLock<T>(stateRoot: string, callback: () => Promise<T>): Promise<T>;
|
|
39
|
-
export declare function saveReceipt(stateRoot: string, kind: string, scope: Scope, root: string, payload: unknown, now?: number): Promise<{
|
|
45
|
+
export declare function saveReceipt(stateRoot: string, kind: string, scope: Scope, root: string, payload: unknown, now?: number, planDigest?: string): Promise<{
|
|
40
46
|
digest: string;
|
|
47
|
+
plan_digest: string;
|
|
41
48
|
expires_at: string;
|
|
49
|
+
superseded_plan?: SupersededPlan;
|
|
42
50
|
}>;
|
|
43
51
|
export declare function consumeReceipt(stateRoot: string, expected: {
|
|
44
52
|
digest: string;
|
|
@@ -46,6 +54,8 @@ export declare function consumeReceipt(stateRoot: string, expected: {
|
|
|
46
54
|
scope: Scope;
|
|
47
55
|
root: string;
|
|
48
56
|
}, now?: number): Promise<unknown>;
|
|
57
|
+
/** Invalidate an active receipt when a preview is blocked before a replacement exists. */
|
|
58
|
+
export declare function supersedeReceipt(stateRoot: string, now?: number): Promise<void>;
|
|
49
59
|
export declare function recoverTransaction(root: string, stateRoot: string): Promise<boolean>;
|
|
50
60
|
export type TransactionOptions = {
|
|
51
61
|
beforePublish?: (index: number) => void;
|
package/dist/lifecycle.js
CHANGED
|
@@ -240,37 +240,52 @@ export async function withLifecycleLock(stateRoot, callback) {
|
|
|
240
240
|
function receiptPath(stateRoot) {
|
|
241
241
|
return join(stateRoot, "receipt.json");
|
|
242
242
|
}
|
|
243
|
-
export async function saveReceipt(stateRoot, kind, scope, root, payload, now = Date.now()) {
|
|
244
|
-
const confirmationDigest = digest({ schema_version: 1, kind, scope, root, payload });
|
|
243
|
+
export async function saveReceipt(stateRoot, kind, scope, root, payload, now = Date.now(), planDigest = digest(payload)) {
|
|
245
244
|
const existingRaw = await readRegular(receiptPath(stateRoot));
|
|
245
|
+
let superseded;
|
|
246
246
|
if (existingRaw) {
|
|
247
247
|
const existing = parseReceipt(existingRaw);
|
|
248
|
-
if (!existing.consumed &&
|
|
249
|
-
|
|
250
|
-
existing.digest !== confirmationDigest) {
|
|
251
|
-
throw new LifecycleError("active_receipt", "An unconsumed agent or installer plan is still active");
|
|
252
|
-
}
|
|
253
|
-
if (!existing.consumed &&
|
|
254
|
-
Date.parse(existing.expires_at) >= now &&
|
|
255
|
-
existing.digest === confirmationDigest) {
|
|
256
|
-
return { digest: existing.digest, expires_at: existing.expires_at };
|
|
248
|
+
if (!existing.consumed && Date.parse(existing.expires_at) >= now) {
|
|
249
|
+
superseded = existing;
|
|
257
250
|
}
|
|
258
251
|
}
|
|
252
|
+
const confirmationDigest = sha256(randomBytes(32));
|
|
259
253
|
const receipt = {
|
|
260
254
|
schema_version: 1,
|
|
261
255
|
digest: confirmationDigest,
|
|
256
|
+
plan_digest: planDigest,
|
|
262
257
|
nonce: randomBytes(32).toString("base64url"),
|
|
258
|
+
created_at: new Date(now).toISOString(),
|
|
263
259
|
expires_at: new Date(now + RECEIPT_TTL_MS).toISOString(),
|
|
264
260
|
consumed: false,
|
|
265
261
|
kind,
|
|
266
262
|
scope,
|
|
267
263
|
root,
|
|
268
264
|
payload,
|
|
265
|
+
...(superseded
|
|
266
|
+
? {
|
|
267
|
+
superseded_digests: [superseded.digest, ...(superseded.superseded_digests ?? [])],
|
|
268
|
+
}
|
|
269
|
+
: {}),
|
|
269
270
|
integrity: "",
|
|
270
271
|
};
|
|
271
272
|
receipt.integrity = receiptIntegrity(receipt);
|
|
272
273
|
await writeAtomic(receiptPath(stateRoot), Buffer.from(`${stable(receipt)}\n`), 0o600);
|
|
273
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
digest: receipt.digest,
|
|
276
|
+
plan_digest: receipt.plan_digest,
|
|
277
|
+
expires_at: receipt.expires_at,
|
|
278
|
+
...(superseded
|
|
279
|
+
? {
|
|
280
|
+
superseded_plan: {
|
|
281
|
+
kind: superseded.kind,
|
|
282
|
+
confirmation_digest: superseded.digest.slice(0, 12),
|
|
283
|
+
created_at: superseded.created_at,
|
|
284
|
+
expires_at: superseded.expires_at,
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
: {}),
|
|
288
|
+
};
|
|
274
289
|
}
|
|
275
290
|
function parseReceipt(raw) {
|
|
276
291
|
let value;
|
|
@@ -285,23 +300,29 @@ function parseReceipt(raw) {
|
|
|
285
300
|
receipt.schema_version !== 1 ||
|
|
286
301
|
typeof receipt.digest !== "string" ||
|
|
287
302
|
!/^[a-f0-9]{64}$/.test(receipt.digest) ||
|
|
303
|
+
typeof receipt.plan_digest !== "string" ||
|
|
304
|
+
!/^[a-f0-9]{64}$/.test(receipt.plan_digest) ||
|
|
288
305
|
typeof receipt.nonce !== "string" ||
|
|
306
|
+
typeof receipt.created_at !== "string" ||
|
|
289
307
|
typeof receipt.expires_at !== "string" ||
|
|
290
308
|
typeof receipt.consumed !== "boolean" ||
|
|
309
|
+
(receipt.superseded !== undefined && typeof receipt.superseded !== "boolean") ||
|
|
291
310
|
typeof receipt.kind !== "string" ||
|
|
292
311
|
(receipt.scope !== "global" && receipt.scope !== "project") ||
|
|
293
312
|
typeof receipt.root !== "string" ||
|
|
313
|
+
(receipt.superseded_digests !== undefined &&
|
|
314
|
+
(!Array.isArray(receipt.superseded_digests) ||
|
|
315
|
+
!receipt.superseded_digests.every((value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value)))) ||
|
|
294
316
|
typeof receipt.integrity !== "string") {
|
|
295
317
|
throw new LifecycleError("invalid_receipt", "Receipt has an unsupported schema");
|
|
296
318
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
})) {
|
|
319
|
+
const payloadRecord = receipt.payload;
|
|
320
|
+
const payloadPlanDigest = payloadRecord &&
|
|
321
|
+
typeof payloadRecord === "object" &&
|
|
322
|
+
typeof payloadRecord.plan_digest === "string"
|
|
323
|
+
? payloadRecord.plan_digest
|
|
324
|
+
: digest(receipt.payload);
|
|
325
|
+
if (receipt.plan_digest !== payloadPlanDigest) {
|
|
305
326
|
throw new LifecycleError("invalid_receipt", "Receipt digest does not match its payload");
|
|
306
327
|
}
|
|
307
328
|
if (receipt.integrity !== receiptIntegrity(receipt))
|
|
@@ -323,8 +344,12 @@ export async function consumeReceipt(stateRoot, expected, now = Date.now()) {
|
|
|
323
344
|
receipt.kind !== expected.kind ||
|
|
324
345
|
receipt.scope !== expected.scope ||
|
|
325
346
|
receipt.root !== expected.root) {
|
|
347
|
+
if (receipt.superseded_digests?.includes(expected.digest))
|
|
348
|
+
throw new LifecycleError("superseded_plan", "Confirmation receipt was superseded by a newer plan");
|
|
326
349
|
throw new LifecycleError("confirmation_unknown", "Confirmation does not match the saved plan");
|
|
327
350
|
}
|
|
351
|
+
if (receipt.superseded)
|
|
352
|
+
throw new LifecycleError("superseded_plan", "Confirmation receipt was superseded by a newer plan");
|
|
328
353
|
if (receipt.consumed)
|
|
329
354
|
throw new LifecycleError("confirmation_consumed", "Confirmation receipt was already consumed");
|
|
330
355
|
if (Date.parse(receipt.expires_at) < now)
|
|
@@ -334,6 +359,19 @@ export async function consumeReceipt(stateRoot, expected, now = Date.now()) {
|
|
|
334
359
|
await writeAtomic(receiptPath(stateRoot), Buffer.from(`${stable(consumed)}\n`), 0o600);
|
|
335
360
|
return receipt.payload;
|
|
336
361
|
}
|
|
362
|
+
/** Invalidate an active receipt when a preview is blocked before a replacement exists. */
|
|
363
|
+
export async function supersedeReceipt(stateRoot, now = Date.now()) {
|
|
364
|
+
const path = receiptPath(stateRoot);
|
|
365
|
+
const raw = await readRegular(path);
|
|
366
|
+
if (!raw)
|
|
367
|
+
return;
|
|
368
|
+
const receipt = parseReceipt(raw);
|
|
369
|
+
if (receipt.consumed || receipt.superseded || Date.parse(receipt.expires_at) < now)
|
|
370
|
+
return;
|
|
371
|
+
const superseded = { ...receipt, consumed: true, superseded: true, integrity: "" };
|
|
372
|
+
superseded.integrity = receiptIntegrity(superseded);
|
|
373
|
+
await writeAtomic(path, Buffer.from(`${stable(superseded)}\n`), 0o600);
|
|
374
|
+
}
|
|
337
375
|
function journalPath(stateRoot) {
|
|
338
376
|
return join(stateRoot, "transaction-journal.json");
|
|
339
377
|
}
|
package/dist/reconcile.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LifecycleError, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
1
|
+
import { LifecycleError, type SupersededPlan, type Scope, type TransactionOptions } from "./lifecycle.js";
|
|
2
2
|
export type ReconcileStatus = "current" | "retired" | "archive-pending" | "renamed" | "modified-managed" | "user-owned" | "unknown" | "conflict" | "diagnostic-state-only";
|
|
3
3
|
export type ReconcileItem = {
|
|
4
4
|
path: string;
|
|
@@ -27,7 +27,11 @@ export type ReconcilePlan = {
|
|
|
27
27
|
operation: "remove" | "write";
|
|
28
28
|
sha256?: string;
|
|
29
29
|
}>;
|
|
30
|
-
|
|
30
|
+
plan_digest: string;
|
|
31
|
+
confirmation_digest?: string;
|
|
32
|
+
superseded_plan?: SupersededPlan;
|
|
33
|
+
confirmable: boolean;
|
|
34
|
+
digest?: string;
|
|
31
35
|
receipt_expires_at?: string;
|
|
32
36
|
};
|
|
33
37
|
export type ReconcileResult = {
|
package/dist/reconcile.js
CHANGED
|
@@ -3,7 +3,7 @@ import { lstat, readdir, readFile } from "node:fs/promises";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { applyTransaction, assertSafePath, consumeReceipt, destination, digest, LifecycleError, lifecycleRoot, recoverTransaction, saveReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
6
|
+
import { applyTransaction, assertSafePath, consumeReceipt, destination, digest, LifecycleError, lifecycleRoot, recoverTransaction, saveReceipt, supersedeReceipt, sha256, stable, withLifecycleLock, } from "./lifecycle.js";
|
|
7
7
|
import { archiveMutations } from "./installer.js";
|
|
8
8
|
const PACKAGE_NAME = "@kisev/skills-opencode";
|
|
9
9
|
const GENERIC_MANIFEST = ".skills-opencode-manifest.json";
|
|
@@ -437,7 +437,11 @@ async function build(scope, cwd = process.cwd(), home = homedir()) {
|
|
|
437
437
|
diagnostic_state_only: diagnostic(scope, cwd, home),
|
|
438
438
|
operations: operations.sort((left, right) => left.path.localeCompare(right.path)),
|
|
439
439
|
};
|
|
440
|
-
|
|
440
|
+
const planDigest = digest(base);
|
|
441
|
+
return {
|
|
442
|
+
plan: { ...base, plan_digest: planDigest, confirmable: true, digest: planDigest },
|
|
443
|
+
mutations,
|
|
444
|
+
};
|
|
441
445
|
}
|
|
442
446
|
export async function previewReconcile(scope, cwd = process.cwd(), home = homedir()) {
|
|
443
447
|
const stateRoot = lifecycleRoot(scope, cwd, home);
|
|
@@ -447,10 +451,24 @@ export async function previewReconcile(scope, cwd = process.cwd(), home = homedi
|
|
|
447
451
|
if (await recoverTransaction(root, stateRoot))
|
|
448
452
|
throw new ReconcileError("recovered_transaction", "Recovered an interrupted transaction; request a fresh plan");
|
|
449
453
|
const built = await build(scope, cwd, home);
|
|
454
|
+
const blocked = built.plan.modified_managed.length > 0 || built.plan.conflicts.length > 0;
|
|
455
|
+
if (blocked) {
|
|
456
|
+
await supersedeReceipt(stateRoot);
|
|
457
|
+
const { digest: _digest, ...withoutReceipt } = built.plan;
|
|
458
|
+
return { ...withoutReceipt, confirmable: false };
|
|
459
|
+
}
|
|
450
460
|
const receipt = await saveReceipt(stateRoot, "reconcile", scope, root, {
|
|
451
|
-
|
|
452
|
-
});
|
|
453
|
-
return {
|
|
461
|
+
plan_digest: built.plan.plan_digest,
|
|
462
|
+
}, Date.now(), built.plan.plan_digest);
|
|
463
|
+
return {
|
|
464
|
+
...built.plan,
|
|
465
|
+
plan_digest: built.plan.plan_digest,
|
|
466
|
+
confirmation_digest: receipt.digest,
|
|
467
|
+
digest: receipt.digest,
|
|
468
|
+
confirmable: true,
|
|
469
|
+
receipt_expires_at: receipt.expires_at,
|
|
470
|
+
...(receipt.superseded_plan ? { superseded_plan: receipt.superseded_plan } : {}),
|
|
471
|
+
};
|
|
454
472
|
});
|
|
455
473
|
}
|
|
456
474
|
catch (error) {
|
|
@@ -479,7 +497,7 @@ export async function applyReconcile(scope, confirmationDigest, cwd = process.cw
|
|
|
479
497
|
root,
|
|
480
498
|
}));
|
|
481
499
|
const built = await build(scope, cwd, home);
|
|
482
|
-
if (built.plan.
|
|
500
|
+
if (built.plan.plan_digest !== payload.plan_digest)
|
|
483
501
|
throw new ReconcileError("stale_plan", "Reconcile inventory changed after preview");
|
|
484
502
|
if (built.plan.conflicts.length || built.plan.modified_managed.length)
|
|
485
503
|
throw new ReconcileError("conflict", "Reconcile contains unsafe ownership conflicts");
|