sfora-cli 0.14.0 → 0.16.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 (53) hide show
  1. package/README.md +128 -0
  2. package/dist/SforaFs.js +68 -0
  3. package/dist/api-client.d.ts +23 -0
  4. package/dist/api-client.js +29 -3
  5. package/dist/block-commands.d.ts +1 -0
  6. package/dist/block-commands.js +1 -0
  7. package/dist/cli-args.d.ts +4 -0
  8. package/dist/cli-args.js +8 -0
  9. package/dist/cli.js +39 -33
  10. package/dist/config.d.ts +2 -0
  11. package/dist/config.js +15 -6
  12. package/dist/format/cardMarkdown.d.ts +1 -0
  13. package/dist/format/cardMarkdown.js +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.js +1 -0
  16. package/dist/local/workspace.d.ts +0 -16
  17. package/dist/local/workspace.js +34 -28
  18. package/dist/local-core/files.d.ts +23 -0
  19. package/dist/local-core/files.js +110 -0
  20. package/dist/local-core/index.d.ts +16 -0
  21. package/dist/local-core/index.js +15 -0
  22. package/dist/local-core/skill-adapters.d.ts +21 -0
  23. package/dist/local-core/skill-adapters.js +19 -0
  24. package/dist/local-core/skill-discovery.d.ts +22 -0
  25. package/dist/local-core/skill-discovery.js +79 -0
  26. package/dist/local-core/skill-domain.d.ts +74 -0
  27. package/dist/local-core/skill-executor.d.ts +23 -0
  28. package/dist/local-core/skill-executor.js +51 -0
  29. package/dist/local-core/skill-index.d.ts +54 -0
  30. package/dist/local-core/skill-index.js +115 -0
  31. package/dist/local-core/skill-local-executor.d.ts +18 -0
  32. package/dist/local-core/skill-local-executor.js +249 -0
  33. package/dist/local-core/skill-operations.d.ts +61 -0
  34. package/dist/local-core/skill-operations.js +268 -0
  35. package/dist/local-core/skill-review.d.ts +46 -0
  36. package/dist/local-core/skill-review.js +132 -0
  37. package/dist/local-core/skill-service.d.ts +96 -0
  38. package/dist/local-core/skill-service.js +157 -0
  39. package/dist/local-core/skill-store.d.ts +34 -0
  40. package/dist/local-core/skill-store.js +187 -0
  41. package/dist/local-core/skill-sync.d.ts +132 -0
  42. package/dist/local-core/skill-sync.js +111 -0
  43. package/dist/local-core/skills.d.ts +55 -0
  44. package/dist/local-core/skills.js +251 -0
  45. package/dist/skills-client.d.ts +32 -0
  46. package/dist/skills-client.js +88 -0
  47. package/dist/skills-command.d.ts +4 -0
  48. package/dist/skills-command.js +223 -0
  49. package/dist/version.d.ts +1 -0
  50. package/dist/version.js +2 -0
  51. package/package.json +21 -3
  52. package/dist/config.test.js +0 -94
  53. /package/dist/{config.test.d.ts → local-core/skill-domain.js} +0 -0
@@ -0,0 +1,115 @@
1
+ import { watch } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { pathWithin } from './skill-service.js';
4
+ /** Events are hints; periodic reconciliation repairs missed events and root changes. */
5
+ export class SkillIndex {
6
+ service;
7
+ changed;
8
+ reconciliationMs;
9
+ watchers = [];
10
+ timer;
11
+ interval;
12
+ pending = new Set();
13
+ full = false;
14
+ running = false;
15
+ active;
16
+ closed = false;
17
+ started;
18
+ warnings = [];
19
+ constructor(service, changed = () => { }, reconciliationMs = 60_000) {
20
+ this.service = service;
21
+ this.changed = changed;
22
+ this.reconciliationMs = reconciliationMs;
23
+ }
24
+ start() {
25
+ return this.started ??= (async () => {
26
+ this.full = true;
27
+ await this.flush();
28
+ if (!this.closed) {
29
+ this.interval = setInterval(() => this.invalidate(), this.reconciliationMs);
30
+ this.interval.unref();
31
+ }
32
+ })();
33
+ }
34
+ async inventory() { await this.start(); return { ...await this.service.cachedInventory(), warnings: this.warnings }; }
35
+ invalidate(path) {
36
+ if (this.closed)
37
+ return;
38
+ const storePath = 'path' in this.service.store ? String(this.service.store.path) : undefined;
39
+ if (path && storePath && (path === storePath || path.startsWith(`${storePath}-`)))
40
+ return;
41
+ if (path && this.pending.size < 1000)
42
+ this.pending.add(path);
43
+ else
44
+ this.full = true;
45
+ if (!this.timer)
46
+ this.timer = setTimeout(() => { this.timer = undefined; void this.flush(); }, 150);
47
+ }
48
+ async configure() {
49
+ for (const watcher of this.watchers)
50
+ watcher.close();
51
+ this.watchers = [];
52
+ const sources = await this.service.discoverySources();
53
+ for (const source of sources.slice(0, 128)) {
54
+ try {
55
+ const watcher = watch(source.path, { recursive: true, persistent: false }, (_event, name) => this.invalidate(name ? resolve(source.path, String(name)) : undefined));
56
+ watcher.on('error', () => this.invalidate());
57
+ this.watchers.push(watcher);
58
+ }
59
+ catch { /* Missing roots are retried by reconciliation, never deleted from the catalog. */ }
60
+ }
61
+ }
62
+ async refresh() {
63
+ await this.start();
64
+ if (this.active)
65
+ await this.active;
66
+ this.full = true;
67
+ await this.flush();
68
+ return { ...await this.service.cachedInventory(), warnings: this.warnings };
69
+ }
70
+ flush() {
71
+ if (this.active)
72
+ return this.active;
73
+ this.active = this.perform().finally(() => { this.active = undefined; });
74
+ return this.active;
75
+ }
76
+ async perform() {
77
+ if (this.running || this.closed)
78
+ return;
79
+ this.running = true;
80
+ const full = this.full;
81
+ this.full = false;
82
+ const paths = [...this.pending];
83
+ this.pending.clear();
84
+ let failed = false;
85
+ try {
86
+ const catalog = await this.service.store.read();
87
+ const ids = catalog.locations.filter(l => paths.some(p => pathWithin(l.path, p))).map(l => l.id);
88
+ const unknown = paths.some(p => !catalog.locations.some(l => pathWithin(l.path, p)));
89
+ if (full || unknown) {
90
+ const result = await this.service.inventory();
91
+ this.warnings = result.warnings;
92
+ await this.configure();
93
+ }
94
+ else if (ids.length)
95
+ await this.service.refreshLocations(ids);
96
+ if (!this.closed)
97
+ this.changed();
98
+ }
99
+ catch (error) {
100
+ failed = true;
101
+ this.warnings = [error.message];
102
+ this.full = true;
103
+ if (!this.closed)
104
+ this.changed();
105
+ }
106
+ finally {
107
+ this.running = false;
108
+ }
109
+ // Do not drop events received during inspection, including external edits during our own writes.
110
+ if ((this.pending.size || (this.full && !failed)) && !this.closed && !this.timer)
111
+ this.timer = setTimeout(() => { this.timer = undefined; void this.flush(); }, 150);
112
+ }
113
+ close() { this.closed = true; clearTimeout(this.timer); clearInterval(this.interval); for (const watcher of this.watchers)
114
+ watcher.close(); this.watchers = []; }
115
+ }
@@ -0,0 +1,18 @@
1
+ import { type PublishedSkillSnapshot } from './skill-sync.js';
2
+ import { SkillOperationJournal, type SkillTransferOperation } from './skill-operations.js';
3
+ import type { SkillStore } from './skill-store.js';
4
+ export interface SkillLocalPorts {
5
+ snapshot(project: string, name: string): Promise<PublishedSkillSnapshot>;
6
+ /** Fault/telemetry seam, after durable steps; production never infers success from it. */
7
+ checkpoint?: (phase: string) => Promise<void>;
8
+ }
9
+ /** Recoverable local installation. Old bytes remain in an operation-specific backup.
10
+ * Destination creation and every file publication are exclusive: no foreign file is overwritten. */
11
+ export declare class SkillLocalExecutor {
12
+ readonly store: SkillStore;
13
+ readonly journal: SkillOperationJournal;
14
+ readonly cloud: SkillLocalPorts;
15
+ constructor(store: SkillStore, journal: SkillOperationJournal, cloud: SkillLocalPorts);
16
+ apply(id: string, recover?: boolean): Promise<SkillTransferOperation>;
17
+ private run;
18
+ }
@@ -0,0 +1,249 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { constants } from 'node:fs';
3
+ import { mkdir, lstat, open, readdir, realpath, rename, rm, rmdir } from 'node:fs/promises';
4
+ import { basename, dirname, join, isAbsolute } from 'node:path';
5
+ import { atomicCreate, atomicWrite, sha256, withLocalLock } from './files.js';
6
+ import { inspectSkillTarget, readInstallation, readSkillBundle } from './skills.js';
7
+ import { assertSkillBindingSnapshot, planSkillInstallation, planSkillTransfer, validatePublishedSkillSnapshot } from './skill-sync.js';
8
+ import { operationDestination } from './skill-operations.js';
9
+ const exists = async (path) => lstat(path).catch(e => { if (e.code === 'ENOENT')
10
+ return null; throw e; });
11
+ const readRegular = async (path, limit) => {
12
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
13
+ try {
14
+ const info = await handle.stat();
15
+ if (!info.isFile() || info.size > limit)
16
+ throw new Error('Invalid operation artifact.');
17
+ return await handle.readFile();
18
+ }
19
+ finally {
20
+ await handle.close();
21
+ }
22
+ };
23
+ const receiptHash = (receipt) => receipt ? sha256(JSON.stringify(receipt)) : null;
24
+ async function directoryProof(path) {
25
+ const info = await lstat(path, { bigint: true });
26
+ if (!info.isDirectory() || info.isSymbolicLink())
27
+ throw new Error('Destination is no longer a directory.');
28
+ return { dev: String(info.dev), ino: String(info.ino) };
29
+ }
30
+ /** Recoverable local installation. Old bytes remain in an operation-specific backup.
31
+ * Destination creation and every file publication are exclusive: no foreign file is overwritten. */
32
+ export class SkillLocalExecutor {
33
+ store;
34
+ journal;
35
+ cloud;
36
+ constructor(store, journal, cloud) {
37
+ this.store = store;
38
+ this.journal = journal;
39
+ this.cloud = cloud;
40
+ }
41
+ async apply(id, recover = false) {
42
+ const op = await this.journal.claim(id, recover);
43
+ if (['succeeded', 'cancelled'].includes(op.status))
44
+ return op;
45
+ try {
46
+ const plan = op.plan;
47
+ if (plan.kind === 'skill-transfer-preview' && plan.direction !== 'pull')
48
+ throw new Error('Use the push executor for this plan.');
49
+ const destination = operationDestination(plan);
50
+ if (!isAbsolute(destination) || join(await realpath(dirname(destination)), basename(destination)) !== destination)
51
+ throw new Error('Destination parent changed since review.');
52
+ const lock = `${destination}.sfora-lock`;
53
+ if (recover && await exists(lock)) {
54
+ const info = await lstat(lock);
55
+ if (!info.isDirectory() || info.isSymbolicLink())
56
+ throw new Error('Unrecognized local lock; preserved.');
57
+ const entries = await readdir(lock);
58
+ if (entries.length !== 1 || entries[0] !== 'operation.json')
59
+ throw new Error('Local lock ownership is unknown; preserved for inspection.');
60
+ const owner = JSON.parse((await readRegular(join(lock, 'operation.json'), 1024)).toString());
61
+ if (owner.id !== op.id)
62
+ throw new Error('Local lock belongs to another operation.');
63
+ await rm(join(lock, 'operation.json'));
64
+ await rmdir(lock);
65
+ }
66
+ return await withLocalLock(destination, async () => {
67
+ await atomicCreate(join(lock, 'operation.json'), JSON.stringify({ id: op.id }));
68
+ return await this.run(op, destination);
69
+ });
70
+ }
71
+ catch (error) {
72
+ await this.journal.finish(id, 'needs-attention', error.message);
73
+ throw error;
74
+ }
75
+ }
76
+ async run(op, destination) {
77
+ const plan = op.plan;
78
+ if (plan.kind === 'skill-transfer-preview' && plan.direction !== 'pull')
79
+ throw new Error('Not a local plan.');
80
+ const artifactDirectory = join(dirname(this.journal.path), 'operation-artifacts', op.id);
81
+ const artifact = join(artifactDirectory, 'snapshot.json');
82
+ const backup = plan.kind === 'skill-transfer-preview' ? join(dirname(destination), `.${basename(destination)}.sfora-${op.id}.previous`) : undefined;
83
+ let local = op.local;
84
+ if (local && (local.destination !== destination || local.backup !== backup))
85
+ throw new Error('Operation checkpoint does not match its destination.');
86
+ let remote;
87
+ if (local) {
88
+ remote = JSON.parse((await readRegular(artifact, 30 * 1024 * 1024)).toString());
89
+ validatePublishedSkillSnapshot(remote);
90
+ }
91
+ else {
92
+ const locator = plan.kind === 'skill-install-preview' ? plan.locator : plan.binding.locator;
93
+ remote = await this.cloud.snapshot(locator.project, locator.name);
94
+ validatePublishedSkillSnapshot(remote);
95
+ }
96
+ if (remote.revision.hash !== plan.expected.remote.hash || remote.revision.cloudVersion !== plan.expected.remote.cloudVersion)
97
+ throw new Error('Published revision differs from the reviewed plan.');
98
+ if (plan.kind === 'skill-transfer-preview')
99
+ assertSkillBindingSnapshot(plan.binding, remote);
100
+ else if (JSON.stringify({ ...remote.remote, accountId: remote.accountId }) !== JSON.stringify(plan.source))
101
+ throw new Error('Installation source identity changed.');
102
+ const before = await this.store.read();
103
+ if (plan.kind === 'skill-transfer-preview') {
104
+ const binding = before.bindings.find(b => b.id === plan.binding.id), location = before.locations.find(l => l.id === plan.location.id);
105
+ if (!binding || !location || location.path !== destination || location.skillId !== binding.localSkillId
106
+ || JSON.stringify({ ...binding, baseline: plan.binding.baseline }) !== JSON.stringify(plan.binding)
107
+ || (!op.confirmedRevision && JSON.stringify(binding.baseline) !== JSON.stringify(plan.binding.baseline)))
108
+ throw new Error('Binding or location changed since review.');
109
+ }
110
+ else {
111
+ const location = before.locations.find(l => l.path === destination);
112
+ if (location && before.bindings.some(b => b.localSkillId === location.skillId && b.accountId === remote.accountId && b.remote.deployment === remote.remote.deployment && b.remote.organizationId === remote.remote.organizationId && b.remote.projectId === remote.remote.projectId && b.remote.skillId !== remote.remote.skillId))
113
+ throw new Error('Destination history belongs to another cloud identity. Choose a new destination.');
114
+ }
115
+ const checkpoint = async (next) => { await this.journal.checkpoint(op.id, next); local = next; await this.cloud.checkpoint?.(next.phase); };
116
+ if (!local) {
117
+ const target = await inspectSkillTarget(destination);
118
+ const current = plan.kind === 'skill-install-preview' ? planSkillInstallation(remote, destination, target)
119
+ : planSkillTransfer({ direction: 'pull', binding: plan.binding, location: plan.location, remote, local: { state: 'available', bundle: await readSkillBundle(destination) }, ownership: target.ownership });
120
+ if (JSON.stringify(current) !== JSON.stringify(plan))
121
+ throw new Error('Local or cloud state changed since review.');
122
+ await mkdir(artifactDirectory, { recursive: true, mode: 0o700 });
123
+ if (await exists(artifact)) {
124
+ const previous = JSON.parse((await readRegular(artifact, 30 * 1024 * 1024)).toString());
125
+ if (JSON.stringify(previous) !== JSON.stringify(remote))
126
+ throw new Error('Staged snapshot differs; preserve it and create a new plan.');
127
+ }
128
+ else
129
+ await atomicCreate(artifact, JSON.stringify(remote));
130
+ await checkpoint({ phase: 'prepared', destination, backup, receiptBefore: receiptHash(await readInstallation(destination)) });
131
+ }
132
+ if (local.phase === 'prepared') {
133
+ if (backup) {
134
+ if (await exists(backup)) {
135
+ if (await exists(destination))
136
+ throw new Error('Both backup and destination exist without an ownership checkpoint; preserved for inspection.');
137
+ }
138
+ else {
139
+ const target = await inspectSkillTarget(destination);
140
+ if (target.ownership !== 'managed' || target.hash !== plan.expected.localHash || receiptHash(target.receipt) !== local.receiptBefore)
141
+ throw new Error('Managed target changed before replacement.');
142
+ await rename(destination, backup);
143
+ await this.cloud.checkpoint?.('after-backup-rename');
144
+ }
145
+ if ((await readSkillBundle(backup, basename(destination))).hash !== plan.expected.localHash)
146
+ throw new Error('Backup contains external changes; preserved for inspection.');
147
+ }
148
+ else if (await exists(destination))
149
+ throw new Error('Installation destination became occupied.');
150
+ await checkpoint({ ...local, phase: 'backed-up' });
151
+ }
152
+ if (local.phase === 'backed-up') {
153
+ if (await exists(destination))
154
+ throw new Error('Destination exists without a recorded owner. Its contents were preserved; inspect before recovery.');
155
+ await mkdir(destination, { mode: 0o700 });
156
+ await checkpoint({ ...local, phase: 'owned', directory: await directoryProof(destination) });
157
+ }
158
+ const verifyOwner = async () => {
159
+ if (JSON.stringify(await directoryProof(destination)) !== JSON.stringify(local.directory))
160
+ throw new Error('Destination was replaced by another process.');
161
+ if (join(await realpath(dirname(destination)), basename(destination)) !== destination)
162
+ throw new Error('Destination parent changed.');
163
+ };
164
+ await verifyOwner();
165
+ if (local.phase === 'owned') {
166
+ for (const file of remote.bundle.files) {
167
+ await verifyOwner();
168
+ let parent = destination;
169
+ for (const part of file.path.split('/').slice(0, -1)) {
170
+ parent = join(parent, part);
171
+ await mkdir(parent).catch(e => { if (e.code !== 'EEXIST')
172
+ throw e; });
173
+ const info = await lstat(parent);
174
+ if (!info.isDirectory() || info.isSymbolicLink() || await realpath(parent) !== parent)
175
+ throw new Error('Skill subdirectory changed; refusing to follow a link.');
176
+ }
177
+ const path = join(destination, file.path);
178
+ try {
179
+ await atomicCreate(path, Buffer.from(file.contentBase64, 'base64'), file.executable ? 0o755 : 0o644);
180
+ }
181
+ catch (error) {
182
+ if (error.code !== 'EEXIST')
183
+ throw error;
184
+ const bytes = await readRegular(path, 5 * 1024 * 1024);
185
+ const info = await lstat(path);
186
+ if (sha256(bytes) !== file.sha256 || !!(info.mode & 0o111) !== file.executable)
187
+ throw new Error(`External changes in ${file.path}; preserved instead of overwritten.`);
188
+ }
189
+ await this.cloud.checkpoint?.(`file:${file.path}`);
190
+ }
191
+ await verifyOwner();
192
+ if ((await readSkillBundle(destination)).hash !== remote.bundle.hash)
193
+ throw new Error('Installed files include external changes; no baseline was recorded.');
194
+ const record = { schemaVersion: 1, path: destination, name: basename(destination), hash: remote.bundle.hash, installedAt: op.createdAt, source: `${remote.remote.deployment}/${remote.locator.project}/${remote.locator.name}`, sourceVersion: remote.revision.cloudVersion, operationId: op.id };
195
+ const receipt = await readInstallation(destination);
196
+ const receiptPath = join(dirname(destination), `.${basename(destination)}.sfora-install.json`);
197
+ if (JSON.stringify(receipt) !== JSON.stringify(record)) {
198
+ if (receiptHash(receipt) !== local.receiptBefore)
199
+ throw new Error('Installation ownership record changed.');
200
+ if (receipt)
201
+ await atomicWrite(receiptPath, JSON.stringify(record));
202
+ else
203
+ await atomicCreate(receiptPath, JSON.stringify(record));
204
+ }
205
+ await checkpoint({ ...local, phase: 'copied' });
206
+ }
207
+ await verifyOwner();
208
+ if ((await readSkillBundle(destination)).hash !== remote.bundle.hash)
209
+ throw new Error('Local files changed before confirmation; preserved.');
210
+ const finalReceipt = await readInstallation(destination);
211
+ if (finalReceipt?.operationId !== op.id || finalReceipt.hash !== remote.bundle.hash)
212
+ throw new Error('Installation ownership record changed before confirmation.');
213
+ await this.journal.confirm(op.id, remote.revision);
214
+ await this.store.transaction(c => {
215
+ if (plan.kind === 'skill-transfer-preview') {
216
+ const current = c.bindings.find(b => b.id === plan.binding.id);
217
+ if (!current || JSON.stringify({ ...current, baseline: plan.binding.baseline }) !== JSON.stringify(plan.binding))
218
+ throw new Error('Binding changed during installation.');
219
+ if (JSON.stringify(current.baseline) === JSON.stringify(plan.binding.baseline))
220
+ current.baseline = { ...remote.revision };
221
+ }
222
+ else {
223
+ let root = c.roots.find(r => r.path === destination);
224
+ if (!root) {
225
+ root = { id: randomUUID(), path: destination, label: basename(destination), depth: 0, scope: 'custom' };
226
+ c.roots.push(root);
227
+ }
228
+ let location = c.locations.find(l => l.path === destination);
229
+ if (!location) {
230
+ const identity = { id: randomUUID(), createdAt: Date.now() };
231
+ c.identities.push(identity);
232
+ location = { id: randomUUID(), skillId: identity.id, path: destination, createdAt: Date.now(), aliases: [] };
233
+ c.locations.push(location);
234
+ }
235
+ if (!location.aliases.some(a => a.sourceId === root.id))
236
+ location.aliases.push({ sourceId: root.id, path: destination, label: root.label, kind: 'custom' });
237
+ const binding = c.bindings.find(b => b.localSkillId === location.skillId && b.accountId === remote.accountId && b.remote.deployment === remote.remote.deployment && b.remote.organizationId === remote.remote.organizationId && b.remote.projectId === remote.remote.projectId);
238
+ if (binding && binding.remote.skillId !== remote.remote.skillId)
239
+ throw new Error('A different cloud identity claimed this destination.');
240
+ if (!binding)
241
+ c.bindings.push({ id: randomUUID(), localSkillId: location.skillId, remote: { ...remote.remote }, accountId: remote.accountId, locator: { ...remote.locator }, baseline: { ...remote.revision } });
242
+ else if (!op.confirmedRevision)
243
+ binding.baseline = { ...remote.revision };
244
+ }
245
+ });
246
+ // Retain the original directory and staged snapshot for explicit inspection.
247
+ return this.journal.finish(op.id, 'succeeded');
248
+ }
249
+ }
@@ -0,0 +1,61 @@
1
+ import { readSkillBundle } from './skills.js';
2
+ import { type PublishedSkillSnapshot, type SkillTransferPlan, type SkillInstallPlan } from './skill-sync.js';
3
+ import type { SkillStore } from './skill-store.js';
4
+ import type { SkillRevision } from './skill-domain.js';
5
+ export type SkillOperationPlan = SkillTransferPlan | SkillInstallPlan;
6
+ export interface LocalOperationCheckpoint {
7
+ phase: "prepared" | "backed-up" | "owned" | "copied";
8
+ destination: string;
9
+ backup?: string;
10
+ directory?: {
11
+ dev: string;
12
+ ino: string;
13
+ };
14
+ receiptBefore: string | null;
15
+ }
16
+ export declare const operationDirection: (plan: SkillOperationPlan) => "push" | "pull" | "install";
17
+ export declare const operationRemote: (plan: SkillOperationPlan) => import("./skill-domain.js").CloudSkillIdentity & {
18
+ projectId: string;
19
+ };
20
+ export declare const operationDestination: (plan: SkillOperationPlan) => string;
21
+ export interface SkillTransferOperation {
22
+ id: string;
23
+ createdAt: number;
24
+ updatedAt: number;
25
+ plan: SkillOperationPlan;
26
+ status: 'planned' | 'running' | 'needs-attention' | 'succeeded' | 'cancelled';
27
+ ownerPid?: number;
28
+ error?: string;
29
+ confirmedRevision?: SkillRevision;
30
+ local?: LocalOperationCheckpoint;
31
+ }
32
+ /** Separate versioned journal: intent commits before network I/O. A dead worker never retains a SQLite lock. */
33
+ export declare class SkillOperationJournal {
34
+ readonly path: string;
35
+ constructor(path: string);
36
+ private access;
37
+ private decode;
38
+ list(): Promise<SkillTransferOperation[]>;
39
+ create(plan: SkillOperationPlan): Promise<SkillTransferOperation>;
40
+ confirm(id: string, revision: SkillRevision): Promise<void>;
41
+ checkpoint(id: string, local: LocalOperationCheckpoint): Promise<void>;
42
+ claim(id: string, recover?: boolean): Promise<SkillTransferOperation>;
43
+ cancel(id: string): Promise<SkillTransferOperation>;
44
+ finish(id: string, status: 'succeeded' | 'needs-attention', error?: string): Promise<SkillTransferOperation>;
45
+ }
46
+ export interface SkillPushPorts {
47
+ snapshot(project: string, name: string): Promise<PublishedSkillSnapshot>;
48
+ saveDraft(project: string, bundle: Awaited<ReturnType<typeof readSkillBundle>>, version: number, draftRevision: number, skillId: string): Promise<{
49
+ draftRevision: number;
50
+ }>;
51
+ publish(project: string, name: string, version: number, draftRevision: number, skillId: string): Promise<unknown>;
52
+ read?: typeof readSkillBundle;
53
+ }
54
+ /** Idempotent by exact observed content/revision, never by names or a blind retry. */
55
+ export declare class SkillPushExecutor {
56
+ readonly store: SkillStore;
57
+ readonly journal: SkillOperationJournal;
58
+ readonly cloud: SkillPushPorts;
59
+ constructor(store: SkillStore, journal: SkillOperationJournal, cloud: SkillPushPorts);
60
+ apply(id: string, recover?: boolean): Promise<SkillTransferOperation>;
61
+ }