@litfamily/litopencode 1.0.1 → 1.0.2

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 (62) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README-Ko-KR.md +8 -7
  3. package/README.md +8 -7
  4. package/dist/activation-prompt-utils.d.ts +1 -1
  5. package/dist/activation-routing.d.ts +1 -1
  6. package/dist/activation-routing.js +1 -8
  7. package/dist/activation-workflow-prompts.d.ts +0 -1
  8. package/dist/activation-workflow-prompts.js +0 -8
  9. package/dist/activation.d.ts +1 -1
  10. package/dist/activation.js +1 -1
  11. package/dist/cli/args.d.ts +1 -1
  12. package/dist/cli/args.js +1 -31
  13. package/dist/cli/loop.js +0 -19
  14. package/dist/cli/managed-skill-assets.d.ts +1 -1
  15. package/dist/cli/managed-skill-assets.js +0 -2
  16. package/dist/cli/types.d.ts +0 -4
  17. package/dist/cli.js +0 -14
  18. package/dist/commands.d.ts +1 -10
  19. package/dist/commands.js +1 -11
  20. package/dist/features.d.ts +1 -22
  21. package/dist/features.js +0 -43
  22. package/dist/hooks.js +0 -13
  23. package/dist/index.d.ts +0 -5
  24. package/dist/index.js +0 -5
  25. package/dist/skills.d.ts +1 -8
  26. package/dist/skills.js +0 -13
  27. package/docs/assets/cover-motion.webp +0 -0
  28. package/docs/assets/readme/badge-version.svg +1 -1
  29. package/docs/privacy.md +2 -2
  30. package/docs/reference.md +5 -48
  31. package/package.json +2 -1
  32. package/skills/frontend-ui-ux/references/complete-contract.md +1 -1
  33. package/skills/lit-plan/SKILL.md +1 -1
  34. package/skills/managed-skill-manifest.json +2 -9
  35. package/skills/visual-qa/references/complete-contract.md +1 -1
  36. package/tools/check-payload-substance.mjs +74 -27
  37. package/tools/payload-substance-parity.json +10 -25
  38. package/tools/version-manifests.json +2 -2
  39. package/dist/cli/skill-loop.d.ts +0 -3
  40. package/dist/cli/skill-loop.js +0 -649
  41. package/dist/skill-loop/apply.d.ts +0 -19
  42. package/dist/skill-loop/apply.js +0 -483
  43. package/dist/skill-loop/config.d.ts +0 -50
  44. package/dist/skill-loop/config.js +0 -215
  45. package/dist/skill-loop/curator.d.ts +0 -18
  46. package/dist/skill-loop/curator.js +0 -232
  47. package/dist/skill-loop/ledger.d.ts +0 -39
  48. package/dist/skill-loop/ledger.js +0 -344
  49. package/dist/skill-loop/proposals.d.ts +0 -48
  50. package/dist/skill-loop/proposals.js +0 -290
  51. package/dist/skill-loop/storage.d.ts +0 -72
  52. package/dist/skill-loop/storage.js +0 -817
  53. package/dist/skill-loop/time.d.ts +0 -2
  54. package/dist/skill-loop/time.js +0 -23
  55. package/dist/skill-loop/transaction.d.ts +0 -62
  56. package/dist/skill-loop/transaction.js +0 -836
  57. package/dist/skill-loop/usage.d.ts +0 -31
  58. package/dist/skill-loop/usage.js +0 -146
  59. package/dist/skill-observer.d.ts +0 -22
  60. package/dist/skill-observer.js +0 -1154
  61. package/skills/skill-observer/SKILL.md +0 -148
  62. package/skills/skill-observer/references/review-contract.md +0 -95
@@ -1,483 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { assertPathComponentsSafe, inventoryTree, lstatIfPresent } from "../cli/native-skill-tree.js";
4
- import { litOpenCodeRuntimeSkills, litOpenCodeStaticOnlySkills } from "../skills.js";
5
- import { hasLitOpenCodeGeneratedFileMarker } from "../user-facing-markdown.js";
6
- import { resolveSkillLoopPaths, skillLoopPaths } from "./config.js";
7
- import { appendSkillLedger, assertProposalLedgerIntegrity, assertSnapshotMatches, readSkillLedger, readSnapshotFile, restoreSnapshot, snapshotSkill } from "./ledger.js";
8
- import { canonicalRelativeIdentity, listProposals, SkillProposalError, updateProposal } from "./proposals.js";
9
- import { anchoredAssertSkillPathAbsent, anchoredCopyIntoSkillsRoot, anchoredRenameInSkillsRoot, anchoredVerifySkillTree, assertSafeRuntimeFileForWrite, assertSkillLoopRootPins, ensureSafeRuntimeDirectory, withSkillLoopMutationLock, withSkillRootsMutationLock } from "./storage.js";
10
- import { beginSkillTransaction, completeSkillTransaction, createSkillTransactionId, materializeTransactionSnapshot, recoverSkillTransaction } from "./transaction.js";
11
- import { bumpSkillUsage, readUsageSidecar, setSkillUsageLifecycle } from "./usage.js";
12
- export const litOpenCodeAgentGeneratedMarker = 'litopencodeAgentGenerated: "true"';
13
- export class SkillApplyError extends Error {
14
- code;
15
- constructor(code, message = code) {
16
- super(message);
17
- this.name = "SkillApplyError";
18
- this.code = code;
19
- }
20
- }
21
- const shippedSkillIds = new Set([
22
- ...litOpenCodeRuntimeSkills.map((skill) => skill.id),
23
- ...litOpenCodeStaticOnlySkills.map((skill) => skill.id)
24
- ]);
25
- const shippedSkillIdentities = new Set([...shippedSkillIds].map(canonicalRelativeIdentity));
26
- export function assertMutableSkillIdentity(targetSkill) {
27
- if (shippedSkillIdentities.has(canonicalRelativeIdentity(targetSkill))) {
28
- throw new SkillApplyError("TARGET_NOT_AGENT_OWNED");
29
- }
30
- }
31
- export function hasAgentOwnedSkillMarker(text) {
32
- if (!text.startsWith("---\n"))
33
- return false;
34
- const end = text.indexOf("\n---\n", 4);
35
- if (end < 0)
36
- return false;
37
- const lines = text.slice(4, end).split("\n");
38
- const metadataIndexes = lines.flatMap((line, index) => /^metadata:\s*(?:#.*)?$/u.test(line) ? [index] : []);
39
- if (metadataIndexes.length !== 1)
40
- return false;
41
- const children = [];
42
- for (const line of lines.slice(metadataIndexes[0] + 1)) {
43
- if (/^\s*(?:#.*)?$/u.test(line))
44
- continue;
45
- const indentation = /^ */u.exec(line)?.[0].length ?? 0;
46
- if (indentation === 0 || line.includes("\t"))
47
- break;
48
- children.push({ indent: indentation, line: line.slice(indentation) });
49
- }
50
- const directIndent = Math.min(...children.map((child) => child.indent));
51
- const markers = children.filter((child) => child.indent === directIndent &&
52
- /^litopencodeAgentGenerated:\s*(?:"true"|'true')\s*(?:#.*)?$/u.test(child.line));
53
- return Number.isFinite(directIndent) && markers.length === 1;
54
- }
55
- function addAgentMarker(text, targetSkill) {
56
- if (hasAgentOwnedSkillMarker(text))
57
- return text;
58
- if (!text.startsWith("---\n")) {
59
- return `---\nname: ${targetSkill}\ndescription: Agent-owned skill.\nmetadata:\n ${litOpenCodeAgentGeneratedMarker}\n---\n\n${text}`;
60
- }
61
- const end = text.indexOf("\n---\n", 4);
62
- if (end < 0)
63
- throw new SkillApplyError("PROPOSAL_CONTENT_INVALID");
64
- const frontmatter = text.slice(4, end);
65
- const marked = /^metadata:\s*$/mu.test(frontmatter)
66
- ? frontmatter.replace(/^metadata:\s*$/mu, `metadata:\n ${litOpenCodeAgentGeneratedMarker}`)
67
- : `${frontmatter}\nmetadata:\n ${litOpenCodeAgentGeneratedMarker}`;
68
- return `---\n${marked}\n---\n${text.slice(end + "\n---\n".length)}`;
69
- }
70
- async function assertAgentOwnedTarget(skillRoot, proposal) {
71
- assertMutableSkillIdentity(proposal.targetSkill);
72
- let siblingNames = [];
73
- try {
74
- siblingNames = await fs.readdir(path.dirname(skillRoot));
75
- }
76
- catch (error) {
77
- if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
78
- throw error;
79
- }
80
- const targetIdentity = canonicalRelativeIdentity(proposal.targetSkill);
81
- if (siblingNames.some((name) => name !== proposal.targetSkill && canonicalRelativeIdentity(name) === targetIdentity)) {
82
- throw new SkillApplyError("TARGET_ALREADY_EXISTS");
83
- }
84
- const stat = await lstatIfPresent(skillRoot);
85
- if (stat === null) {
86
- if (proposal.action !== "create")
87
- throw new SkillApplyError("TARGET_NOT_AGENT_OWNED");
88
- return false;
89
- }
90
- await assertExistingAgentOwnedSkill(skillRoot);
91
- if (proposal.action === "create")
92
- throw new SkillApplyError("TARGET_ALREADY_EXISTS");
93
- return true;
94
- }
95
- async function assertExistingAgentOwnedSkill(skillRoot) {
96
- const stat = await lstatIfPresent(skillRoot);
97
- if (stat === null)
98
- throw new SkillApplyError("TARGET_NOT_AGENT_OWNED");
99
- if (stat.isSymbolicLink() || !stat.isDirectory())
100
- throw new SkillApplyError("TARGET_PATH_UNSAFE");
101
- const inventory = await inventoryTree(skillRoot);
102
- if (inventory.unsafe.length > 0)
103
- throw new SkillApplyError("TARGET_PATH_UNSAFE");
104
- const entry = path.join(skillRoot, "SKILL.md");
105
- const entryStat = await lstatIfPresent(entry);
106
- if (entryStat === null || entryStat.isSymbolicLink() || !entryStat.isFile())
107
- throw new SkillApplyError("TARGET_NOT_AGENT_OWNED");
108
- const content = await fs.readFile(entry, "utf8");
109
- if (hasLitOpenCodeGeneratedFileMarker(content) || !hasAgentOwnedSkillMarker(content))
110
- throw new SkillApplyError("TARGET_NOT_AGENT_OWNED");
111
- }
112
- async function assertAgentOwnedSnapshot(projectRoot, snapshot) {
113
- if (snapshot.length === 0)
114
- return;
115
- const entry = await readSnapshotFile(projectRoot, snapshot, "SKILL.md");
116
- if (entry === undefined)
117
- throw new SkillApplyError("TARGET_NOT_AGENT_OWNED");
118
- const content = entry.toString("utf8");
119
- if (hasLitOpenCodeGeneratedFileMarker(content) || !hasAgentOwnedSkillMarker(content))
120
- throw new SkillApplyError("TARGET_NOT_AGENT_OWNED");
121
- }
122
- async function buildStage(skillRoot, proposal, exists, stage) {
123
- const parent = path.dirname(skillRoot);
124
- await fs.mkdir(parent, { recursive: true });
125
- if (exists)
126
- await fs.cp(skillRoot, stage, { recursive: true, errorOnExist: true, force: false });
127
- else
128
- await fs.mkdir(stage, { mode: 0o700 });
129
- try {
130
- if (proposal.action === "patch") {
131
- const patch = proposal.patch;
132
- if (patch === undefined)
133
- throw new SkillApplyError("PROPOSAL_SCHEMA_INVALID");
134
- const file = path.join(stage, ...patch.file.split("/"));
135
- const content = await fs.readFile(file, "utf8").catch(() => { throw new SkillApplyError("PATCH_TARGET_MISSING"); });
136
- const occurrences = content.split(patch.oldString).length - 1;
137
- if (occurrences !== 1)
138
- throw new SkillApplyError("PATCH_MATCH_NOT_UNIQUE");
139
- await fs.writeFile(file, content.replace(patch.oldString, patch.newString), "utf8");
140
- }
141
- else if (proposal.action === "create" || proposal.action === "add-reference") {
142
- const spec = proposal.createSpec;
143
- if (spec === undefined)
144
- throw new SkillApplyError("PROPOSAL_SCHEMA_INVALID");
145
- for (const item of spec.files) {
146
- const file = path.join(stage, ...item.file.split("/"));
147
- if (proposal.action === "add-reference" && await lstatIfPresent(file) !== null)
148
- throw new SkillApplyError("TARGET_ALREADY_EXISTS");
149
- await fs.mkdir(path.dirname(file), { recursive: true });
150
- const content = item.file === "SKILL.md" ? addAgentMarker(item.content, proposal.targetSkill) : item.content;
151
- await fs.writeFile(file, content, { encoding: "utf8", mode: 0o600, flag: proposal.action === "add-reference" ? "wx" : "w" });
152
- }
153
- }
154
- const entry = await fs.readFile(path.join(stage, "SKILL.md"), "utf8").catch(() => { throw new SkillApplyError("PROPOSAL_CONTENT_INVALID"); });
155
- if (!hasAgentOwnedSkillMarker(entry))
156
- throw new SkillApplyError("PROPOSAL_CONTENT_INVALID");
157
- return stage;
158
- }
159
- catch (error) {
160
- await fs.rm(stage, { recursive: true, force: true });
161
- throw error;
162
- }
163
- }
164
- async function swapStage(capability, targetSkill, stage, backup, currentExists) {
165
- if (!currentExists) {
166
- await anchoredRenameInSkillsRoot(capability, stage, targetSkill);
167
- return;
168
- }
169
- await anchoredRenameInSkillsRoot(capability, targetSkill, backup);
170
- try {
171
- await anchoredRenameInSkillsRoot(capability, stage, targetSkill);
172
- }
173
- catch (error) {
174
- await anchoredRenameInSkillsRoot(capability, backup, targetSkill);
175
- throw error;
176
- }
177
- }
178
- async function targetPinned(projectRoot, targetSkill) {
179
- return (await readUsageSidecar(projectRoot)).skills[targetSkill]?.pinned === true;
180
- }
181
- async function recoverRequest(projectRoot, configRoot, request, capability) {
182
- return recoverSkillTransaction(projectRoot, configRoot, request, assertMutableSkillIdentity, (targetSkill) => targetPinned(projectRoot, targetSkill), capability);
183
- }
184
- async function recoverAfterFailure(projectRoot, configRoot, request, capability, original) {
185
- try {
186
- await recoverRequest(projectRoot, configRoot, request, capability);
187
- }
188
- catch (recoveryError) {
189
- throw new Error("SKILL_TRANSACTION_STATE_UNKNOWN", { cause: { original, recoveryError } });
190
- }
191
- }
192
- function rollbackOrigin(reference, entries) {
193
- const byId = new Map(entries.map((entry) => [entry.id, entry]));
194
- const visited = new Set();
195
- let current = reference;
196
- let depth = 0;
197
- while (current.operation === "rollback") {
198
- if (current.restoresLedgerEntryId === undefined || visited.has(current.id))
199
- throw new Error("SKILL_LEDGER_INVALID");
200
- visited.add(current.id);
201
- const restored = byId.get(current.restoresLedgerEntryId);
202
- if (restored === undefined)
203
- throw new Error("SKILL_LEDGER_INVALID");
204
- current = restored;
205
- depth += 1;
206
- }
207
- return { origin: current, depth };
208
- }
209
- async function withUserSkillRootLock(projectRoot, configRoot, run) {
210
- try {
211
- return await withSkillRootsMutationLock(projectRoot, configRoot, run);
212
- }
213
- catch (error) {
214
- if (error instanceof Error && error.message === "SKILL_LOOP_PATH_UNSAFE")
215
- throw new SkillApplyError("TARGET_PATH_UNSAFE");
216
- throw error;
217
- }
218
- }
219
- async function applyProposalLocked(id, projectRoot, configRoot, capability) {
220
- const paths = skillLoopPaths(projectRoot, configRoot);
221
- const request = { kind: "apply", requestId: id };
222
- const recovery = await recoverRequest(projectRoot, paths.configRoot, request, capability);
223
- await assertSkillLoopRootPins(capability);
224
- const entries = await readSkillLedger(projectRoot);
225
- const proposals = await listProposals(projectRoot, configRoot);
226
- assertProposalLedgerIntegrity(proposals, entries);
227
- let proposal = proposals.find((candidate) => candidate.id === id);
228
- if (proposal === undefined)
229
- throw new SkillProposalError("PROPOSAL_NOT_FOUND");
230
- if (recovery === "committed" && proposal.status === "applied" && proposal.ledgerEntryId !== undefined) {
231
- return { proposal, ledgerEntryId: proposal.ledgerEntryId, appliedPath: path.join(paths.skillsRoot, proposal.targetSkill) };
232
- }
233
- if (proposal.status !== "pending" && proposal.status !== "approved")
234
- throw new SkillApplyError("PROPOSAL_STATUS_INVALID");
235
- const skillRoot = path.join(paths.skillsRoot, proposal.targetSkill);
236
- try {
237
- await assertPathComponentsSafe(paths.configRoot, skillRoot, `Agent-owned skill ${proposal.targetSkill}`);
238
- }
239
- catch {
240
- throw new SkillApplyError("TARGET_PATH_UNSAFE");
241
- }
242
- const exists = await assertAgentOwnedTarget(skillRoot, proposal);
243
- if (await targetPinned(projectRoot, proposal.targetSkill))
244
- throw new SkillApplyError("TARGET_PINNED");
245
- await assertSafeRuntimeFileForWrite(projectRoot, paths.ledgerFile);
246
- await assertSafeRuntimeFileForWrite(projectRoot, paths.usageFile);
247
- const before = await snapshotSkill(projectRoot, skillRoot);
248
- const transactionId = createSkillTransactionId();
249
- const stageName = `.${proposal.targetSkill}.${transactionId}.stage`;
250
- const backupName = `.${proposal.targetSkill}.${transactionId}.backup`;
251
- const draftStage = path.join(paths.runtimeRoot, "skill-transaction-staging", transactionId, proposal.targetSkill);
252
- let archive;
253
- let archiveStage;
254
- if (proposal.action === "archive") {
255
- archive = path.join(paths.archiveDir, new Date().toISOString().replaceAll(":", "-"), proposal.targetSkill);
256
- archiveStage = path.join(path.dirname(archive), `.${proposal.targetSkill}.${transactionId}.stage`);
257
- await ensureSafeRuntimeDirectory(projectRoot, path.dirname(archive));
258
- if (await lstatIfPresent(archive) !== null || await lstatIfPresent(archiveStage) !== null)
259
- throw new SkillApplyError("TARGET_ALREADY_EXISTS");
260
- }
261
- else {
262
- await ensureSafeRuntimeDirectory(projectRoot, path.dirname(draftStage));
263
- await buildStage(skillRoot, proposal, exists, draftStage);
264
- }
265
- const after = proposal.action === "archive" ? [] : await snapshotSkill(projectRoot, draftStage);
266
- if (archive !== undefined && (await lstatIfPresent(archive) !== null || await lstatIfPresent(archiveStage) !== null)) {
267
- throw new SkillApplyError("TARGET_ALREADY_EXISTS");
268
- }
269
- await beginSkillTransaction(projectRoot, paths.configRoot, {
270
- id: transactionId,
271
- request,
272
- proposalId: proposal.id,
273
- action: proposal.action,
274
- targets: [{ targetSkill: proposal.targetSkill, proposalId: proposal.id, action: proposal.action, before, after,
275
- ...(archive === undefined ? {} : { archivePath: archive, archiveStagePath: archiveStage }) }],
276
- includeLedger: true,
277
- includeUsage: true,
278
- includeProposal: true,
279
- capability
280
- });
281
- try {
282
- if (proposal.status === "pending")
283
- proposal = await updateProposal(proposal, { status: "approved" }, projectRoot, configRoot, { capability });
284
- await assertSkillLoopRootPins(capability);
285
- if (proposal.action === "archive") {
286
- await materializeTransactionSnapshot(projectRoot, archiveStage, archive, before);
287
- await anchoredRenameInSkillsRoot(capability, proposal.targetSkill, backupName);
288
- await anchoredAssertSkillPathAbsent(capability, proposal.targetSkill);
289
- }
290
- else {
291
- await anchoredCopyIntoSkillsRoot(capability, draftStage, stageName, after);
292
- await anchoredVerifySkillTree(capability, stageName, after);
293
- await swapStage(capability, proposal.targetSkill, stageName, backupName, exists);
294
- await anchoredVerifySkillTree(capability, proposal.targetSkill, after);
295
- }
296
- await assertSkillLoopRootPins(capability);
297
- await fs.rm(path.join(paths.runtimeRoot, "skill-transaction-staging", transactionId), { recursive: true, force: true });
298
- const ledger = await appendSkillLedger(projectRoot, {
299
- proposalId: proposal.id,
300
- actor: "user",
301
- operation: "apply",
302
- targetSkill: proposal.targetSkill,
303
- targetRoot: paths.skillsRoot,
304
- before,
305
- after,
306
- reason: proposal.rationale
307
- }, new Date().toISOString(), { capability });
308
- proposal = await updateProposal(proposal, { status: "applied", ledgerEntryId: ledger.id }, projectRoot, configRoot, { capability });
309
- if (proposal.action === "archive")
310
- await setSkillUsageLifecycle(projectRoot, proposal.targetSkill, "archived", ledger.createdAt, { capability });
311
- else
312
- await bumpSkillUsage(projectRoot, proposal.targetSkill, "patch", ledger.createdAt, { capability });
313
- await completeSkillTransaction(projectRoot, paths.configRoot, request, capability);
314
- return { proposal, ledgerEntryId: ledger.id, appliedPath: skillRoot };
315
- }
316
- catch (error) {
317
- await recoverAfterFailure(projectRoot, paths.configRoot, request, capability, error);
318
- await fs.rm(path.join(paths.runtimeRoot, "skill-transaction-staging", transactionId), { recursive: true, force: true }).catch(() => undefined);
319
- throw error;
320
- }
321
- }
322
- export async function applyProposal(id, projectRoot, configRoot) {
323
- const requestedConfigRoot = skillLoopPaths(projectRoot, configRoot).configRoot;
324
- return withSkillLoopMutationLock(projectRoot, async () => withUserSkillRootLock(projectRoot, requestedConfigRoot, (capability, roots) => applyProposalLocked(id, roots.project.physical, roots.config.physical, capability)));
325
- }
326
- async function rejectProposalLocked(id, projectRoot, configRoot, capability) {
327
- const paths = skillLoopPaths(projectRoot, configRoot);
328
- const request = { kind: "reject", requestId: id };
329
- const recovery = await recoverRequest(projectRoot, paths.configRoot, request, capability);
330
- const entries = await readSkillLedger(projectRoot);
331
- const proposals = await listProposals(projectRoot, configRoot);
332
- assertProposalLedgerIntegrity(proposals, entries);
333
- const proposal = proposals.find((candidate) => candidate.id === id);
334
- if (proposal === undefined)
335
- throw new SkillProposalError("PROPOSAL_NOT_FOUND");
336
- if (recovery === "committed" && proposal.status === "rejected")
337
- return proposal;
338
- if (proposal.status !== "pending")
339
- throw new SkillApplyError("PROPOSAL_STATUS_INVALID");
340
- await beginSkillTransaction(projectRoot, paths.configRoot, {
341
- id: createSkillTransactionId(), request, proposalId: proposal.id, action: "reject", targets: [], includeLedger: true, includeProposal: true, capability
342
- });
343
- try {
344
- await appendSkillLedger(projectRoot, {
345
- proposalId: proposal.id,
346
- actor: "user",
347
- operation: "reject",
348
- targetSkill: proposal.targetSkill,
349
- targetRoot: paths.skillsRoot,
350
- before: [],
351
- after: [],
352
- reason: proposal.rationale
353
- }, new Date().toISOString(), { capability });
354
- const rejected = await updateProposal(proposal, { status: "rejected" }, projectRoot, configRoot, { capability });
355
- await completeSkillTransaction(projectRoot, paths.configRoot, request, capability);
356
- return rejected;
357
- }
358
- catch (error) {
359
- await recoverAfterFailure(projectRoot, paths.configRoot, request, capability, error);
360
- throw error;
361
- }
362
- }
363
- export async function rejectProposal(id, projectRoot, configRoot) {
364
- const requestedConfigRoot = skillLoopPaths(projectRoot, configRoot).configRoot;
365
- return withSkillLoopMutationLock(projectRoot, async (capability) => {
366
- const paths = await resolveSkillLoopPaths(projectRoot, requestedConfigRoot);
367
- return rejectProposalLocked(id, paths.projectRoot, paths.configRoot, capability);
368
- });
369
- }
370
- async function rollbackLedgerEntryLocked(id, projectRoot, configRoot, capability) {
371
- const paths = skillLoopPaths(projectRoot, configRoot);
372
- const request = { kind: "rollback", requestId: id };
373
- const recovery = await recoverRequest(projectRoot, paths.configRoot, request, capability);
374
- await assertSkillLoopRootPins(capability);
375
- const entries = await readSkillLedger(projectRoot);
376
- const proposals = await listProposals(projectRoot, configRoot);
377
- assertProposalLedgerIntegrity(proposals, entries);
378
- if (recovery === "committed") {
379
- const committed = entries.find((candidate) => candidate.operation === "rollback" && candidate.restoresLedgerEntryId === id);
380
- if (committed !== undefined)
381
- return { ledgerEntryId: committed.id, restoredLedgerEntryId: id };
382
- }
383
- const reference = entries.find((candidate) => candidate.id === id);
384
- if (reference === undefined)
385
- throw new Error("LEDGER_ENTRY_NOT_FOUND");
386
- if (reference.operation !== "apply" && reference.operation !== "curator-transition" && reference.operation !== "rollback") {
387
- throw new SkillApplyError("LEDGER_ENTRY_NOT_ROLLBACK_ELIGIBLE");
388
- }
389
- if (entries.some((candidate) => candidate.operation === "rollback" && candidate.restoresLedgerEntryId === reference.id)) {
390
- throw new SkillApplyError("LEDGER_ENTRY_ALREADY_ROLLED_BACK");
391
- }
392
- if (path.resolve(reference.targetRoot) !== path.resolve(paths.skillsRoot))
393
- throw new SkillApplyError("PROPOSAL_TARGET_ROOT_MISMATCH");
394
- const referenceProposal = reference.proposalId === null ? undefined : proposals.find((candidate) => candidate.id === reference.proposalId);
395
- if (reference.proposalId !== null && (referenceProposal === undefined || referenceProposal.ledgerEntryId !== reference.id)) {
396
- throw new SkillProposalError("PROPOSAL_INTEGRITY_INVALID");
397
- }
398
- const skillRoot = path.join(paths.skillsRoot, reference.targetSkill);
399
- assertMutableSkillIdentity(reference.targetSkill);
400
- try {
401
- await assertPathComponentsSafe(paths.configRoot, skillRoot, `Agent-owned skill ${reference.targetSkill}`);
402
- }
403
- catch {
404
- throw new SkillApplyError("TARGET_PATH_UNSAFE");
405
- }
406
- await assertSnapshotMatches(projectRoot, skillRoot, reference.after);
407
- await assertAgentOwnedSnapshot(projectRoot, reference.before);
408
- await assertAgentOwnedSnapshot(projectRoot, reference.after);
409
- if (reference.after.length > 0)
410
- await assertExistingAgentOwnedSkill(skillRoot);
411
- if (await targetPinned(projectRoot, reference.targetSkill))
412
- throw new SkillApplyError("TARGET_PINNED");
413
- const before = await snapshotSkill(projectRoot, skillRoot);
414
- const transactionId = createSkillTransactionId();
415
- const stageName = `.${reference.targetSkill}.${transactionId}.stage`;
416
- const backupName = `.${reference.targetSkill}.${transactionId}.backup`;
417
- const draftStage = path.join(paths.runtimeRoot, "skill-transaction-staging", transactionId, reference.targetSkill);
418
- if (reference.before.length > 0) {
419
- await ensureSafeRuntimeDirectory(projectRoot, path.dirname(draftStage));
420
- await restoreSnapshot(projectRoot, draftStage, reference.before);
421
- }
422
- await beginSkillTransaction(projectRoot, paths.configRoot, {
423
- id: transactionId,
424
- request,
425
- proposalId: reference.proposalId,
426
- action: "rollback",
427
- targets: [{ targetSkill: reference.targetSkill, proposalId: reference.proposalId, action: "rollback", before, after: reference.before }],
428
- includeLedger: true,
429
- includeUsage: true,
430
- includeProposal: referenceProposal !== undefined,
431
- capability
432
- });
433
- try {
434
- if (reference.before.length === 0) {
435
- await anchoredRenameInSkillsRoot(capability, reference.targetSkill, backupName);
436
- await anchoredAssertSkillPathAbsent(capability, reference.targetSkill);
437
- }
438
- else {
439
- await anchoredCopyIntoSkillsRoot(capability, draftStage, stageName, reference.before);
440
- await anchoredVerifySkillTree(capability, stageName, reference.before);
441
- await swapStage(capability, reference.targetSkill, stageName, backupName, reference.after.length > 0);
442
- await anchoredVerifySkillTree(capability, reference.targetSkill, reference.before);
443
- }
444
- await assertSkillLoopRootPins(capability);
445
- await fs.rm(path.join(paths.runtimeRoot, "skill-transaction-staging", transactionId), { recursive: true, force: true });
446
- const after = await snapshotSkill(projectRoot, skillRoot);
447
- const ledger = await appendSkillLedger(projectRoot, {
448
- proposalId: reference.proposalId,
449
- actor: "user",
450
- operation: "rollback",
451
- targetSkill: reference.targetSkill,
452
- targetRoot: reference.targetRoot,
453
- before,
454
- after,
455
- restoresLedgerEntryId: reference.id
456
- }, new Date().toISOString(), { capability });
457
- if (referenceProposal !== undefined) {
458
- const { depth } = rollbackOrigin(reference, entries);
459
- const status = (depth + 1) % 2 === 0 ? "applied" : "rolled-back";
460
- await updateProposal(referenceProposal, { status, ledgerEntryId: ledger.id }, projectRoot, configRoot, { capability });
461
- }
462
- const { origin } = rollbackOrigin(reference, entries);
463
- await setSkillUsageLifecycle(projectRoot, reference.targetSkill, reference.before.length === 0 ? "archived" : origin.operation === "curator-transition" ? "stale" : "active", ledger.createdAt, { capability });
464
- await completeSkillTransaction(projectRoot, paths.configRoot, request, capability);
465
- return { ledgerEntryId: ledger.id, restoredLedgerEntryId: reference.id };
466
- }
467
- catch (error) {
468
- await recoverAfterFailure(projectRoot, paths.configRoot, request, capability, error);
469
- await fs.rm(path.join(paths.runtimeRoot, "skill-transaction-staging", transactionId), { recursive: true, force: true }).catch(() => undefined);
470
- throw error;
471
- }
472
- }
473
- export async function rollbackLedgerEntry(id, projectRoot, configRoot) {
474
- const requestedConfigRoot = skillLoopPaths(projectRoot, configRoot).configRoot;
475
- return withSkillLoopMutationLock(projectRoot, async () => withUserSkillRootLock(projectRoot, requestedConfigRoot, (capability, roots) => rollbackLedgerEntryLocked(id, roots.project.physical, roots.config.physical, capability)));
476
- }
477
- export function skillLoopErrorCode(error) {
478
- if (error instanceof SkillApplyError || error instanceof SkillProposalError)
479
- return error.code;
480
- if (error instanceof Error && /^[A-Z][A-Z0-9_]+$/u.test(error.message))
481
- return error.message;
482
- return "SKILL_LOOP_FAILED";
483
- }
@@ -1,50 +0,0 @@
1
- import { type SkillLoopLockCapability } from "./storage.ts";
2
- export type SkillLoopConfig = {
3
- readonly schemaVersion: 1;
4
- readonly autoApply: false;
5
- readonly nudgeInterval: number;
6
- readonly curatorIntervalDays: number;
7
- readonly minIdleHours: number;
8
- readonly staleAfterDays: number;
9
- readonly archiveAfterDays: number;
10
- };
11
- export type SkillLoopPaths = {
12
- readonly projectRoot: string;
13
- readonly runtimeRoot: string;
14
- readonly configRoot: string;
15
- readonly skillsRoot: string;
16
- readonly configFile: string;
17
- readonly proposalsDir: string;
18
- readonly usageFile: string;
19
- readonly ledgerFile: string;
20
- readonly blobsDir: string;
21
- readonly stateFile: string;
22
- readonly pendingReviewFile: string;
23
- readonly backupsDir: string;
24
- readonly archiveDir: string;
25
- readonly transactionFile: string;
26
- readonly observationTransactionFile: string;
27
- };
28
- export declare const defaultSkillLoopConfig: SkillLoopConfig;
29
- export declare function skillLoopPaths(projectRoot?: string, configRoot?: string): SkillLoopPaths;
30
- export declare function resolveSkillLoopPaths(projectRoot: string, configRoot: string): Promise<SkillLoopPaths>;
31
- export declare function readSkillLoopConfig(projectRoot: string): Promise<SkillLoopConfig>;
32
- export declare function writeJsonAtomically(projectRoot: string, file: string, value: unknown, capability?: SkillLoopLockCapability): Promise<void>;
33
- export type PendingReviewSession = {
34
- readonly sessionRef: string;
35
- readonly iterationCount: number;
36
- readonly queuedAt: string;
37
- };
38
- type PendingReviewState = {
39
- readonly schemaVersion: 1;
40
- readonly sessions: readonly PendingReviewSession[];
41
- };
42
- export declare function readPendingReviewState(projectRoot: string, options?: {
43
- readonly capability?: SkillLoopLockCapability;
44
- }): Promise<PendingReviewState>;
45
- export declare function consumePendingReview(projectRoot: string, reviewedSessionRefs: readonly string[]): Promise<void>;
46
- export declare function observeToolIteration(projectRoot: string, sessionRef: string, now?: string): Promise<{
47
- readonly surfaceNotice: boolean;
48
- readonly reviewQueued: boolean;
49
- }>;
50
- export {};