@litfamily/litclaude 1.0.6 → 1.0.7

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 (45) hide show
  1. package/CHANGELOG.md +5 -5
  2. package/README.md +11 -38
  3. package/README_ko-KR.md +11 -37
  4. package/docs/assets/cover-motion.webp +0 -0
  5. package/docs/assets/readme/badge-version.svg +1 -1
  6. package/docs/privacy.md +12 -17
  7. package/package.json +3 -1
  8. package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
  9. package/plugins/litclaude/bin/litclaude-hook.js +20 -57
  10. package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
  11. package/plugins/litclaude/hooks/hooks.json +4 -4
  12. package/plugins/litclaude/lib/canonical-skill-catalog.mjs +0 -1
  13. package/plugins/litclaude/lib/canonical-skill-resources.mjs +5 -27
  14. package/plugins/litclaude/lib/lit-plan-persistence.mjs +2 -1
  15. package/plugins/litclaude/lib/litgoal/paths.mjs +10 -8
  16. package/plugins/litclaude/lib/project-state-root.mjs +22 -0
  17. package/plugins/litclaude/lib/rules/session-state.mjs +2 -1
  18. package/plugins/litclaude/lib/start-work-cli.mjs +3 -3
  19. package/plugins/litclaude/lib/wikify-knowledge-cli.mjs +3 -1
  20. package/plugins/litclaude/lib/wikify-knowledge.mjs +6 -5
  21. package/plugins/litclaude/scripts/scaffold-plan.mjs +9 -3
  22. package/plugins/litclaude/skills/lit-team/scripts/team.mjs +4 -3
  23. package/scripts/prepublish-test-gate.mjs +26 -0
  24. package/plugins/litclaude/lib/skill-loop/apply.mjs +0 -391
  25. package/plugins/litclaude/lib/skill-loop/authority.mjs +0 -129
  26. package/plugins/litclaude/lib/skill-loop/cli.mjs +0 -67
  27. package/plugins/litclaude/lib/skill-loop/common.mjs +0 -280
  28. package/plugins/litclaude/lib/skill-loop/config.mjs +0 -91
  29. package/plugins/litclaude/lib/skill-loop/curator.mjs +0 -670
  30. package/plugins/litclaude/lib/skill-loop/hook-runtime.mjs +0 -188
  31. package/plugins/litclaude/lib/skill-loop/hook-state.mjs +0 -484
  32. package/plugins/litclaude/lib/skill-loop/ledger.mjs +0 -834
  33. package/plugins/litclaude/lib/skill-loop/mutation-transaction.mjs +0 -529
  34. package/plugins/litclaude/lib/skill-loop/proposal-reader.mjs +0 -26
  35. package/plugins/litclaude/lib/skill-loop/proposal-record.mjs +0 -104
  36. package/plugins/litclaude/lib/skill-loop/proposal-store.mjs +0 -187
  37. package/plugins/litclaude/lib/skill-loop/proposals.mjs +0 -26
  38. package/plugins/litclaude/lib/skill-loop/review-runner.mjs +0 -13
  39. package/plugins/litclaude/lib/skill-loop/review-transaction.mjs +0 -358
  40. package/plugins/litclaude/lib/skill-loop/review.mjs +0 -223
  41. package/plugins/litclaude/lib/skill-loop/usage.mjs +0 -218
  42. package/plugins/litclaude/lib/skill-observer.mjs +0 -693
  43. package/plugins/litclaude/skills/skill-observer/SKILL.md +0 -155
  44. package/plugins/litclaude/skills/skill-observer/references/review-contract.md +0 -77
  45. package/plugins/litclaude/skills/skill-observer/scripts/skill-loop.mjs +0 -10
@@ -1,529 +0,0 @@
1
- import { lstatSync, renameSync, rmSync, unlinkSync } from "node:fs";
2
- import { randomUUID } from "node:crypto";
3
- import { dirname, join } from "node:path";
4
-
5
- import { validateProposal } from "./proposal-record.mjs";
6
- import { readProposalRecord, transitionProposalRecordUnlocked } from "./proposal-store.mjs";
7
- import {
8
- appendLedgerEntryUnlocked,
9
- captureSkillSnapshot,
10
- captureStagedSnapshot,
11
- readLedger,
12
- rollbackEntryMatchesReference,
13
- SESSION_START_LEDGER_WORK_LIMITS,
14
- validateLedgerEntry,
15
- verifySnapshotBlobs,
16
- } from "./ledger.mjs";
17
- import {
18
- atomicWriteJson,
19
- directoryIdentityMatches,
20
- ensureSafeDirectoryChain,
21
- isPlainObject,
22
- parseTimestamp,
23
- readStrictJsonFile,
24
- RECORD_ID,
25
- requireExactKeys,
26
- skillRootsEquivalent,
27
- SKILL_ID,
28
- } from "./common.mjs";
29
- import { readUsage, validateUsage, writeUsageUnlocked } from "./usage.mjs";
30
- import {
31
- readSkillLoopConfig,
32
- validateSkillLoopConfig,
33
- writeSkillLoopConfigUnlocked,
34
- } from "./config.mjs";
35
-
36
- export class SkillLoopMutationTransactionError extends Error {
37
- constructor(code, message = code) {
38
- super(`${code}: ${message}`);
39
- this.name = "SkillLoopMutationTransactionError";
40
- this.code = code;
41
- }
42
- }
43
-
44
- const MAX_JOURNAL_BYTES = 12 * 1024 * 1024;
45
- const fail = (code, message) => { throw new SkillLoopMutationTransactionError(code, message); };
46
- const snapshotsMatch = (left, right) => JSON.stringify(left) === JSON.stringify(right);
47
- const valuesMatch = (left, right) => JSON.stringify(left) === JSON.stringify(right);
48
-
49
- function expectedPatchUsage(journal) {
50
- if (journal.usageBefore === null || journal.usageAfter === null) return null;
51
- const expected = structuredClone(journal.usageBefore);
52
- const entry = expected.skills[journal.targetSkill] ?? {
53
- state: "active",
54
- use_count: 0,
55
- view_count: 0,
56
- patch_count: 0,
57
- created_at: journal.createdAt,
58
- created_by: "agent",
59
- last_used_at: null,
60
- last_viewed_at: null,
61
- last_patched_at: null,
62
- archived_at: null,
63
- pinned: false,
64
- };
65
- if (entry.state === "archived") return null;
66
- entry.patch_count += 1;
67
- entry.last_patched_at = journal.createdAt;
68
- expected.skills[journal.targetSkill] = entry;
69
- expected.updatedAt = journal.createdAt;
70
- return expected;
71
- }
72
-
73
- function expectedRollbackUsage(journal) {
74
- const transition = /^usage:(active|stale|archived)->(active|stale|archived)$/u.exec(
75
- journal.ledgerEntry.reason ?? "",
76
- );
77
- if (!transition) return journal.usageBefore === null && journal.usageAfter === null ? null : false;
78
- if (journal.usageBefore === null || journal.usageAfter === null
79
- || journal.usageBefore.skills[journal.targetSkill]?.state !== transition[1]) return false;
80
- const expected = structuredClone(journal.usageBefore);
81
- expected.skills[journal.targetSkill].state = transition[2];
82
- expected.skills[journal.targetSkill].archived_at = transition[2] === "archived" ? journal.createdAt : null;
83
- expected.updatedAt = journal.createdAt;
84
- return expected;
85
- }
86
-
87
- function expectedCuratorUsage(journal) {
88
- const transition = /^usage:(active|stale|archived)->(active|stale|archived)$/u.exec(
89
- journal.ledgerEntry.reason ?? "",
90
- );
91
- if (!transition || journal.usageBefore === null || journal.usageAfter === null
92
- || journal.usageBefore.skills[journal.targetSkill]?.state !== transition[1]) return false;
93
- const expected = structuredClone(journal.usageBefore);
94
- expected.skills[journal.targetSkill].state = transition[2];
95
- expected.skills[journal.targetSkill].archived_at = transition[2] === "archived" ? journal.createdAt : null;
96
- expected.updatedAt = journal.createdAt;
97
- return expected;
98
- }
99
-
100
- function stateDirectory(root, create) {
101
- return ensureSafeDirectoryChain(root, [".litclaude"], {
102
- create,
103
- ErrorType: SkillLoopMutationTransactionError,
104
- code: "MUTATION_TRANSACTION_UNSAFE",
105
- });
106
- }
107
-
108
- function journalPath(root, create) {
109
- const directory = stateDirectory(root, create);
110
- return directory === null ? null : join(directory, "skill-mutation-transaction.json");
111
- }
112
-
113
- function validIdentity(identity) {
114
- return identity === null || (isPlainObject(identity) && requireExactKeys(identity, ["dev", "ino", "birthtimeMs"])
115
- && Number.isSafeInteger(identity.dev) && identity.dev >= 0
116
- && Number.isSafeInteger(identity.ino) && identity.ino > 0
117
- && Number.isFinite(identity.birthtimeMs) && identity.birthtimeMs >= 0);
118
- }
119
-
120
- function validTemporaryName(name, skill, kind, suffix) {
121
- return typeof name === "string" && name.startsWith(`.${skill}.${kind}-${suffix}`)
122
- && /^\.[a-z0-9][a-z0-9-]*\.(?:apply|rollback)-(?:before-)?[a-f0-9-]+$/u.test(name);
123
- }
124
-
125
- function validateJournal(journal) {
126
- let bytes;
127
- try { bytes = Buffer.from(`${JSON.stringify(journal, null, 2)}\n`, "utf8"); } catch { fail("MUTATION_TRANSACTION_INVALID"); }
128
- if (bytes.length > MAX_JOURNAL_BYTES || !requireExactKeys(journal, [
129
- "schema", "id", "kind", "requestId", "createdAt", "targetRoot", "targetSkill", "action",
130
- "stageName", "displacedName", "archiveParts", "beforeIdentity", "stageIdentity",
131
- "before", "after", "ledgerEntry", "proposalBefore", "proposalAfter", "usageBefore", "usageAfter",
132
- "configBefore", "configAfter",
133
- ], ["abortRequested"]) || journal.schema !== "litclaude.skill-mutation-transaction/v1" || !RECORD_ID.test(journal.id)
134
- || !["apply", "rollback", "curator"].includes(journal.kind) || !RECORD_ID.test(journal.requestId)
135
- || parseTimestamp(journal.createdAt) === null || !SKILL_ID.test(journal.targetSkill)
136
- || typeof journal.targetRoot !== "string" || journal.targetRoot.length < 1 || journal.targetRoot.length > 2048
137
- || !["create", "patch", "add-reference", "archive", "rollback", "curator-stale", "curator-archive"].includes(journal.action)
138
- || (journal.kind === "curator"
139
- ? journal.stageName !== null || journal.displacedName !== null
140
- : !validTemporaryName(journal.stageName, journal.targetSkill, journal.kind, "")
141
- || !validTemporaryName(journal.displacedName, journal.targetSkill, journal.kind, "before-"))
142
- || !validIdentity(journal.beforeIdentity) || !validIdentity(journal.stageIdentity)
143
- || (Object.hasOwn(journal, "abortRequested")
144
- && (typeof journal.abortRequested !== "boolean"
145
- || journal.kind !== "curator"
146
- || !["curator-stale", "curator-archive"].includes(journal.action)))
147
- || !Array.isArray(journal.before) || !Array.isArray(journal.after)
148
- || (["archive", "curator-archive"].includes(journal.action)) !== Array.isArray(journal.archiveParts)
149
- || (!["archive", "curator-archive"].includes(journal.action) && journal.archiveParts !== null)
150
- || (journal.configBefore === null) !== (journal.configAfter === null)) {
151
- fail("MUTATION_TRANSACTION_INVALID");
152
- }
153
- if (journal.archiveParts !== null && (journal.archiveParts.length !== 3
154
- || journal.archiveParts[0] !== "skill-archives"
155
- || !/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/u.test(journal.archiveParts[1])
156
- || journal.archiveParts[2] !== journal.targetSkill)) fail("MUTATION_TRANSACTION_INVALID");
157
- try {
158
- validateLedgerEntry(journal.ledgerEntry);
159
- if (journal.ledgerEntry.id !== journal.id || journal.ledgerEntry.targetSkill !== journal.targetSkill
160
- || journal.ledgerEntry.targetRoot !== journal.targetRoot
161
- || journal.ledgerEntry.createdAt !== journal.createdAt
162
- || !valuesMatch(journal.ledgerEntry.before, journal.before)
163
- || !valuesMatch(journal.ledgerEntry.after, journal.after)) fail("MUTATION_TRANSACTION_INVALID");
164
- if ((journal.proposalBefore === null) !== (journal.proposalAfter === null)) fail("MUTATION_TRANSACTION_INVALID");
165
- if ((journal.proposalBefore === null) !== (journal.ledgerEntry.proposalId === null)) {
166
- fail("MUTATION_TRANSACTION_INVALID");
167
- }
168
- if (journal.proposalBefore !== null) {
169
- validateProposal(journal.proposalBefore);
170
- validateProposal(journal.proposalAfter);
171
- if (journal.proposalBefore.id !== journal.proposalAfter.id
172
- || journal.proposalBefore.id !== journal.ledgerEntry.proposalId
173
- || journal.proposalAfter.ledgerEntryId !== journal.id
174
- || journal.proposalBefore.targetSkill !== journal.targetSkill
175
- || journal.proposalBefore.targetRoot !== journal.targetRoot) fail("MUTATION_TRANSACTION_INVALID");
176
- }
177
- if ((journal.usageBefore === null) !== (journal.usageAfter === null)) fail("MUTATION_TRANSACTION_INVALID");
178
- if (journal.usageBefore !== null) {
179
- validateUsage(journal.usageBefore);
180
- validateUsage(journal.usageAfter);
181
- }
182
- if (journal.configBefore !== null) {
183
- validateSkillLoopConfig(journal.configBefore);
184
- validateSkillLoopConfig(journal.configAfter);
185
- }
186
- if (journal.kind === "apply") {
187
- const expectedProposal = journal.proposalBefore === null ? null : {
188
- ...journal.proposalBefore,
189
- status: "applied",
190
- ledgerEntryId: journal.id,
191
- };
192
- const expectedUsage = journal.action === "patch" ? expectedPatchUsage(journal) : null;
193
- if (journal.action === "rollback" || journal.ledgerEntry.operation !== "apply"
194
- || journal.ledgerEntry.actor !== "user" || journal.requestId !== journal.ledgerEntry.proposalId
195
- || journal.proposalBefore?.id !== journal.requestId || journal.proposalBefore?.status !== "approved"
196
- || journal.proposalBefore?.action !== journal.action || !valuesMatch(journal.proposalAfter, expectedProposal)
197
- || journal.configBefore !== null || journal.configAfter !== null
198
- || (journal.action === "archive") !== (journal.stageIdentity === null && journal.after.length === 0)
199
- || (expectedUsage === null) !== (journal.usageBefore === null)
200
- || (expectedUsage !== null && !valuesMatch(journal.usageAfter, expectedUsage))) {
201
- fail("MUTATION_TRANSACTION_INVALID");
202
- }
203
- } else if (journal.kind === "rollback") {
204
- const expectedProposal = journal.proposalBefore === null ? null : {
205
- ...journal.proposalBefore,
206
- status: "rolled-back",
207
- ledgerEntryId: journal.id,
208
- };
209
- const expectedUsage = expectedRollbackUsage(journal);
210
- if (journal.action !== "rollback" || journal.ledgerEntry.operation !== "rollback"
211
- || journal.ledgerEntry.actor !== "user" || journal.ledgerEntry.restoresLedgerEntryId !== journal.requestId
212
- || !["applied", "rolled-back"].includes(journal.proposalBefore?.status ?? "applied")
213
- || !valuesMatch(journal.proposalAfter, expectedProposal)
214
- || journal.configBefore !== null || journal.configAfter !== null
215
- || (journal.after.length === 0) !== (journal.stageIdentity === null)
216
- || expectedUsage === false
217
- || (expectedUsage !== null && !valuesMatch(journal.usageAfter, expectedUsage))) {
218
- fail("MUTATION_TRANSACTION_INVALID");
219
- }
220
- } else {
221
- const expectedUsage = expectedCuratorUsage(journal);
222
- const transition = /^usage:(active|stale|archived)->(active|stale|archived)$/u.exec(
223
- journal.ledgerEntry.reason ?? "",
224
- );
225
- const expectedConfig = journal.configBefore === null
226
- ? null
227
- : { ...journal.configBefore, lastCuratorRunAt: journal.createdAt };
228
- if (journal.ledgerEntry.operation !== "curator-transition" || journal.ledgerEntry.actor !== "curator"
229
- || journal.ledgerEntry.proposalId !== null || journal.requestId !== journal.id
230
- || journal.proposalBefore !== null || journal.proposalAfter !== null
231
- || journal.beforeIdentity === null || journal.before.length === 0
232
- || journal.stageIdentity !== null || journal.stageName !== null || journal.displacedName !== null
233
- || expectedUsage === false || !valuesMatch(journal.usageAfter, expectedUsage)
234
- || journal.configBefore === null || !valuesMatch(journal.configAfter, expectedConfig)
235
- || (journal.action === "curator-archive"
236
- ? transition?.[1] !== "stale" || transition?.[2] !== "archived" || journal.after.length !== 0
237
- : transition?.[1] !== "active" || transition?.[2] !== "stale"
238
- || !valuesMatch(journal.after, journal.before))) {
239
- fail("MUTATION_TRANSACTION_INVALID");
240
- }
241
- }
242
- } catch (error) {
243
- if (error instanceof SkillLoopMutationTransactionError) throw error;
244
- fail("MUTATION_TRANSACTION_INVALID");
245
- }
246
- return journal;
247
- }
248
-
249
- function readJournalUnlocked(root) {
250
- const path = journalPath(root, false);
251
- if (path === null || !lstatSync(path, { throwIfNoEntry: false })) return null;
252
- return validateJournal(readStrictJsonFile(path, SkillLoopMutationTransactionError,
253
- "MUTATION_TRANSACTION_UNSAFE", { maxBytes: MAX_JOURNAL_BYTES, parseCode: "MUTATION_TRANSACTION_INVALID" }));
254
- }
255
-
256
- export function beginSkillMutationTransactionUnlocked(root, journal) {
257
- if (readJournalUnlocked(root) !== null) fail("MUTATION_TRANSACTION_ACTIVE");
258
- validateJournal(journal);
259
- atomicWriteJson(journalPath(root, true), journal,
260
- SkillLoopMutationTransactionError, "MUTATION_TRANSACTION_UNSAFE");
261
- return journal;
262
- }
263
-
264
- export function requestCuratorMutationRollbackUnlocked(root, transactionId) {
265
- const journal = readJournalUnlocked(root);
266
- if (journal === null || journal.id !== transactionId || journal.kind !== "curator"
267
- || !["curator-stale", "curator-archive"].includes(journal.action)
268
- || journal.abortRequested === true) {
269
- fail("MUTATION_TRANSACTION_INVALID");
270
- }
271
- const aborting = { ...journal, abortRequested: true };
272
- validateJournal(aborting);
273
- atomicWriteJson(journalPath(root, true), aborting,
274
- SkillLoopMutationTransactionError, "MUTATION_TRANSACTION_UNSAFE");
275
- return recoverSkillMutationTransactionUnlocked(root);
276
- }
277
-
278
- function clearJournalUnlocked(root) {
279
- const path = journalPath(root, false);
280
- const details = path === null ? null : lstatSync(path, { throwIfNoEntry: false });
281
- if (!details) return;
282
- if (!details.isFile() || details.isSymbolicLink() || details.nlink !== 1) fail("MUTATION_TRANSACTION_UNSAFE");
283
- unlinkSync(path);
284
- }
285
-
286
- function directorySnapshot(root, path, targetRoot, skill, snapshotOptions) {
287
- const details = lstatSync(path, { throwIfNoEntry: false });
288
- if (!details) return null;
289
- if (!details.isDirectory() || details.isSymbolicLink()) fail("MUTATION_TRANSACTION_AMBIGUOUS");
290
- const skills = ensureSafeDirectoryChain(root, [".claude", "skills"], {
291
- create: false,
292
- ErrorType: SkillLoopMutationTransactionError,
293
- code: "MUTATION_TRANSACTION_UNSAFE",
294
- });
295
- if (skills !== null && path === join(skills, skill)) {
296
- return captureSkillSnapshot(root, targetRoot, skill, snapshotOptions);
297
- }
298
- return captureStagedSnapshot(root, path, snapshotOptions);
299
- }
300
-
301
- function assertExactDirectory(root, path, identity, snapshot, targetRoot, skill, snapshotOptions) {
302
- if (identity === null || !directoryIdentityMatches(path, identity)) fail("MUTATION_TRANSACTION_AMBIGUOUS");
303
- const actual = directorySnapshot(root, path, targetRoot, skill, snapshotOptions);
304
- if (actual === null || !snapshotsMatch(actual, snapshot)) fail("MUTATION_TRANSACTION_AMBIGUOUS");
305
- }
306
-
307
- function removeExactDirectory(root, path, identity, snapshot, targetRoot, skill, snapshotOptions) {
308
- assertExactDirectory(root, path, identity, snapshot, targetRoot, skill, snapshotOptions);
309
- const skills = ensureSafeDirectoryChain(root, [".claude", "skills"], {
310
- create: false,
311
- ErrorType: SkillLoopMutationTransactionError,
312
- code: "MUTATION_TRANSACTION_UNSAFE",
313
- });
314
- if (skills === null || dirname(path) !== skills) fail("MUTATION_TRANSACTION_AMBIGUOUS");
315
- const quarantine = join(skills, `.${skill}.rollback-${randomUUID()}`);
316
- try {
317
- renameSync(path, quarantine);
318
- if (!directoryIdentityMatches(quarantine, identity)) throw new Error("directory identity changed");
319
- const quarantinedSnapshot = captureStagedSnapshot(root, quarantine, snapshotOptions);
320
- if (!snapshotsMatch(quarantinedSnapshot, snapshot)) throw new Error("directory contents changed");
321
- rmSync(quarantine, { recursive: true, force: true });
322
- } catch (error) {
323
- try {
324
- if (!lstatSync(path, { throwIfNoEntry: false }) && lstatSync(quarantine, { throwIfNoEntry: false })) {
325
- renameSync(quarantine, path);
326
- }
327
- } catch { /* Leave uncertain cleanup as a visible residue. */ }
328
- if (error instanceof SkillLoopMutationTransactionError) throw error;
329
- fail("MUTATION_TRANSACTION_AMBIGUOUS");
330
- }
331
- }
332
-
333
- function ensureLedgerCommitted(root, journal) {
334
- const ledger = readLedger(root);
335
- const existing = ledger.find(({ id }) => id === journal.ledgerEntry.id);
336
- if (existing) {
337
- if (JSON.stringify(existing) !== JSON.stringify(journal.ledgerEntry)) fail("MUTATION_TRANSACTION_AMBIGUOUS");
338
- } else appendLedgerEntryUnlocked(root, journal.ledgerEntry);
339
- }
340
-
341
- function ensureUsageCommitted(root, journal) {
342
- if (journal.usageBefore === null) return;
343
- const current = readUsage(root);
344
- if (JSON.stringify(current) === JSON.stringify(journal.usageAfter)) return;
345
- if (JSON.stringify(current) !== JSON.stringify(journal.usageBefore)) fail("MUTATION_TRANSACTION_AMBIGUOUS");
346
- writeUsageUnlocked(root, journal.usageAfter);
347
- }
348
-
349
- function ensureProposalCommitted(root, journal) {
350
- if (journal.proposalBefore === null) return;
351
- const current = readProposalRecord(root, journal.proposalBefore.id);
352
- if (JSON.stringify(current) === JSON.stringify(journal.proposalAfter)) return;
353
- if (JSON.stringify(current) !== JSON.stringify(journal.proposalBefore)) fail("MUTATION_TRANSACTION_AMBIGUOUS");
354
- transitionProposalRecordUnlocked(root, current.id, journal.proposalAfter.status, journal.id);
355
- const written = readProposalRecord(root, current.id);
356
- if (JSON.stringify(written) !== JSON.stringify(journal.proposalAfter)) fail("MUTATION_TRANSACTION_AMBIGUOUS");
357
- }
358
-
359
- function ensureConfigCommitted(root, journal) {
360
- if (journal.configBefore === null) return;
361
- const current = readSkillLoopConfig(root);
362
- if (JSON.stringify(current) === JSON.stringify(journal.configAfter)) return;
363
- if (JSON.stringify(current) !== JSON.stringify(journal.configBefore)) fail("MUTATION_TRANSACTION_AMBIGUOUS");
364
- writeSkillLoopConfigUnlocked(root, journal.configAfter);
365
- }
366
-
367
- function cleanupDisplaced(root, journal, skills, snapshotOptions) {
368
- if (journal.displacedName === null) return;
369
- const displaced = join(skills, journal.displacedName);
370
- if (!lstatSync(displaced, { throwIfNoEntry: false })) return;
371
- removeExactDirectory(root, displaced, journal.beforeIdentity, journal.before,
372
- journal.targetRoot, journal.targetSkill, snapshotOptions);
373
- }
374
-
375
- function assertExactRollbackReference(root, journal) {
376
- if (journal.kind !== "rollback") return;
377
- try {
378
- const reference = readLedger(root).find(({ id }) => id === journal.requestId);
379
- if (!rollbackEntryMatchesReference(reference, journal.ledgerEntry)
380
- || !snapshotsMatch(journal.before, reference.after)
381
- || !snapshotsMatch(journal.after, reference.before)) {
382
- fail("MUTATION_TRANSACTION_INVALID");
383
- }
384
- const referenceTransition = /^usage:(active|stale|archived)->(active|stale|archived)$/u.exec(
385
- reference.reason ?? "",
386
- );
387
- if (referenceTransition) {
388
- if (journal.usageBefore === null || journal.usageAfter === null) fail("MUTATION_TRANSACTION_INVALID");
389
- } else if (journal.usageBefore !== null || journal.usageAfter !== null) {
390
- fail("MUTATION_TRANSACTION_INVALID");
391
- }
392
- } catch (error) {
393
- if (error instanceof SkillLoopMutationTransactionError) throw error;
394
- fail("MUTATION_TRANSACTION_INVALID");
395
- }
396
- }
397
-
398
- export function recoverSkillMutationTransactionUnlocked(root, {
399
- afterUsageCommit,
400
- afterLedgerCommit,
401
- afterConfigCommit,
402
- snapshotOptions,
403
- } = {}) {
404
- const journal = readJournalUnlocked(root);
405
- if (journal === null) return { status: "none" };
406
- if (!skillRootsEquivalent(root, journal.targetRoot)) fail("MUTATION_TRANSACTION_INVALID");
407
- assertExactRollbackReference(root, journal);
408
- verifySnapshotBlobs(root, journal.before);
409
- verifySnapshotBlobs(root, journal.after);
410
- const skills = ensureSafeDirectoryChain(root, [".claude", "skills"], {
411
- create: true,
412
- ErrorType: SkillLoopMutationTransactionError,
413
- code: "MUTATION_TRANSACTION_UNSAFE",
414
- });
415
- const target = join(skills, journal.targetSkill);
416
- const stage = journal.stageName === null ? null : join(skills, journal.stageName);
417
- const displaced = journal.displacedName === null ? null : join(skills, journal.displacedName);
418
- const archiveBase = journal.archiveParts === null ? null : ensureSafeDirectoryChain(
419
- root, [".litclaude", ...journal.archiveParts.slice(0, 2)], {
420
- create: false,
421
- ErrorType: SkillLoopMutationTransactionError,
422
- code: "MUTATION_TRANSACTION_UNSAFE",
423
- },
424
- );
425
- const archive = archiveBase === null ? null : join(archiveBase, journal.targetSkill);
426
- const targetDetails = lstatSync(target, { throwIfNoEntry: false });
427
- const stageDetails = stage === null ? null : lstatSync(stage, { throwIfNoEntry: false });
428
- const displacedDetails = displaced === null ? null : lstatSync(displaced, { throwIfNoEntry: false });
429
- const archiveDetails = archive === null ? null : lstatSync(archive, { throwIfNoEntry: false });
430
-
431
- if (journal.abortRequested === true) {
432
- if (journal.kind !== "curator"
433
- || !["curator-stale", "curator-archive"].includes(journal.action)
434
- || stageDetails || displacedDetails
435
- || !valuesMatch(readUsage(root), journal.usageBefore)
436
- || !valuesMatch(readSkillLoopConfig(root), journal.configBefore)
437
- || readLedger(root, { workLimits: SESSION_START_LEDGER_WORK_LIMITS }).some(({ id }) => id === journal.id)) {
438
- fail("MUTATION_TRANSACTION_AMBIGUOUS");
439
- }
440
- if (journal.action === "curator-stale") {
441
- if (targetDetails && directoryIdentityMatches(target, journal.beforeIdentity) && !archiveDetails) {
442
- clearJournalUnlocked(root);
443
- return { status: "rolled-back", kind: journal.kind, requestId: journal.requestId };
444
- }
445
- fail("MUTATION_TRANSACTION_AMBIGUOUS");
446
- }
447
- if (!targetDetails && archive !== null && directoryIdentityMatches(archive, journal.beforeIdentity)) {
448
- renameSync(archive, target);
449
- if (!directoryIdentityMatches(target, journal.beforeIdentity)) fail("MUTATION_TRANSACTION_AMBIGUOUS");
450
- clearJournalUnlocked(root);
451
- return { status: "rolled-back", kind: journal.kind, requestId: journal.requestId };
452
- }
453
- if (targetDetails && directoryIdentityMatches(target, journal.beforeIdentity) && !archiveDetails) {
454
- clearJournalUnlocked(root);
455
- return { status: "rolled-back", kind: journal.kind, requestId: journal.requestId };
456
- }
457
- fail("MUTATION_TRANSACTION_AMBIGUOUS");
458
- }
459
-
460
- const archiveAction = ["archive", "curator-archive"].includes(journal.action);
461
- const targetAfter = archiveAction
462
- ? !targetDetails && archive !== null && archiveDetails && directoryIdentityMatches(archive, journal.beforeIdentity)
463
- : journal.action === "curator-stale"
464
- ? targetDetails && directoryIdentityMatches(target, journal.beforeIdentity)
465
- && snapshotsMatch(directorySnapshot(
466
- root, target, journal.targetRoot, journal.targetSkill, snapshotOptions,
467
- ), journal.after)
468
- : journal.stageIdentity === null && journal.after.length === 0
469
- ? !targetDetails
470
- : targetDetails && directoryIdentityMatches(target, journal.stageIdentity)
471
- && snapshotsMatch(directorySnapshot(
472
- root, target, journal.targetRoot, journal.targetSkill, snapshotOptions,
473
- ), journal.after);
474
- if (targetAfter) {
475
- if (archiveAction && archive !== null) {
476
- assertExactDirectory(root, archive, journal.beforeIdentity, journal.before,
477
- journal.targetRoot, journal.targetSkill, snapshotOptions);
478
- }
479
- if (stageDetails) fail("MUTATION_TRANSACTION_AMBIGUOUS");
480
- if (displacedDetails) assertExactDirectory(root, displaced, journal.beforeIdentity, journal.before,
481
- journal.targetRoot, journal.targetSkill, snapshotOptions);
482
- ensureUsageCommitted(root, journal);
483
- afterUsageCommit?.();
484
- ensureLedgerCommitted(root, journal);
485
- afterLedgerCommit?.();
486
- ensureProposalCommitted(root, journal);
487
- ensureConfigCommitted(root, journal);
488
- afterConfigCommit?.();
489
- cleanupDisplaced(root, journal, skills, snapshotOptions);
490
- clearJournalUnlocked(root);
491
- return {
492
- status: "finalized",
493
- kind: journal.kind,
494
- requestId: journal.requestId,
495
- ledgerEntry: journal.ledgerEntry,
496
- path: target,
497
- archivePath: archive,
498
- transition: /^usage:(active|stale|archived)->(active|stale|archived)$/u.exec(
499
- journal.ledgerEntry.reason ?? "",
500
- )?.slice(1),
501
- };
502
- }
503
-
504
- const targetBefore = journal.beforeIdentity === null
505
- ? !targetDetails && journal.before.length === 0
506
- : targetDetails && directoryIdentityMatches(target, journal.beforeIdentity)
507
- && snapshotsMatch(directorySnapshot(
508
- root, target, journal.targetRoot, journal.targetSkill, snapshotOptions,
509
- ), journal.before);
510
- if (targetBefore && !displacedDetails && !archiveDetails) {
511
- if (stageDetails && stage !== null) removeExactDirectory(root, stage, journal.stageIdentity, journal.after,
512
- journal.targetRoot, journal.targetSkill, snapshotOptions);
513
- clearJournalUnlocked(root);
514
- return { status: "rolled-back", kind: journal.kind, requestId: journal.requestId };
515
- }
516
-
517
- if (!targetDetails && displacedDetails && displaced !== null && !archiveDetails) {
518
- assertExactDirectory(root, displaced, journal.beforeIdentity, journal.before,
519
- journal.targetRoot, journal.targetSkill, snapshotOptions);
520
- if (stageDetails && stage !== null) removeExactDirectory(root, stage, journal.stageIdentity, journal.after,
521
- journal.targetRoot, journal.targetSkill, snapshotOptions);
522
- renameSync(displaced, target);
523
- assertExactDirectory(root, target, journal.beforeIdentity, journal.before,
524
- journal.targetRoot, journal.targetSkill, snapshotOptions);
525
- clearJournalUnlocked(root);
526
- return { status: "rolled-back", kind: journal.kind, requestId: journal.requestId };
527
- }
528
- fail("MUTATION_TRANSACTION_AMBIGUOUS");
529
- }
@@ -1,26 +0,0 @@
1
- import { readLedger, validateProposalReceipt } from "./ledger.mjs";
2
- import { SkillLoopProposalError } from "./proposal-record.mjs";
3
- import { listProposalRecords, readProposalRecord } from "./proposal-store.mjs";
4
-
5
- function validateReceipt(root, proposal, ledgerIndex) {
6
- if (!["applied", "rolled-back"].includes(proposal.status)) return proposal;
7
- try {
8
- validateProposalReceipt(root, proposal, ledgerIndex);
9
- } catch {
10
- throw new SkillLoopProposalError("PROPOSAL_LEDGER_INVALID", "proposal receipt is missing or mismatched");
11
- }
12
- return proposal;
13
- }
14
-
15
- export function readProposalUnlocked(root, id) {
16
- return validateReceipt(root, readProposalRecord(root, id));
17
- }
18
-
19
- export function listProposalsUnlocked(root, { ledgerWorkLimits } = {}) {
20
- const proposals = listProposalRecords(root);
21
- const needsReceipt = proposals.some(({ status }) => ["applied", "rolled-back"].includes(status));
22
- const ledgerIndex = needsReceipt
23
- ? new Map(readLedger(root, { workLimits: ledgerWorkLimits }).map((entry) => [entry.id, entry]))
24
- : undefined;
25
- return proposals.map((proposal) => validateReceipt(root, proposal, ledgerIndex));
26
- }
@@ -1,104 +0,0 @@
1
- import {
2
- assertNoSecretStrings,
3
- parseTimestamp,
4
- portablePathIdentity,
5
- prettyJsonBytes,
6
- PROPOSAL_MAX_BYTES,
7
- RECORD_ID,
8
- requireExactKeys,
9
- SKILL_ID,
10
- } from "./common.mjs";
11
-
12
- export class SkillLoopProposalError extends Error {
13
- constructor(code, message = code) {
14
- super(`${code}: ${message}`);
15
- this.name = "SkillLoopProposalError";
16
- this.code = code;
17
- }
18
- }
19
-
20
- const required = [
21
- "schema", "id", "createdAt", "host", "sessionRef", "signal", "targetSkill", "targetRoot",
22
- "action", "rationale", "evidenceRefs", "status",
23
- ];
24
- const optional = ["patch", "createSpec", "ledgerEntryId"];
25
- const text = (value, min, max) => typeof value === "string" && value.length >= min && value.length <= max;
26
- const schemaError = () => new SkillLoopProposalError("PROPOSAL_SCHEMA_INVALID", "proposal does not match v1 schema");
27
-
28
- function validateRelativeFile(file, targetSkill) {
29
- const identity = portablePathIdentity(file);
30
- const targetIdentity = portablePathIdentity(targetSkill);
31
- if (identity === null || targetIdentity === null || identity === targetIdentity
32
- || identity.startsWith(`${targetIdentity}/`)) {
33
- throw new SkillLoopProposalError("PROPOSAL_PATH_INVALID", "proposal contains a non-portable relative path");
34
- }
35
- }
36
-
37
- function validatePatch(patch, targetSkill) {
38
- if (!requireExactKeys(patch, ["file", "oldString", "newString"])) throw schemaError();
39
- validateRelativeFile(patch.file, targetSkill);
40
- if (!text(patch.oldString, 1, 65536) || !text(patch.newString, 0, 65536)) throw schemaError();
41
- }
42
-
43
- function validateCreateSpec(createSpec, targetSkill) {
44
- if (!requireExactKeys(createSpec, ["files"]) || !Array.isArray(createSpec.files)
45
- || createSpec.files.length < 1 || createSpec.files.length > 64) throw schemaError();
46
- const byteIdentities = new Set();
47
- const canonicalIdentities = new Set();
48
- for (const item of createSpec.files) {
49
- if (!requireExactKeys(item, ["file", "content"]) || !text(item.content, 0, 65536)) throw schemaError();
50
- validateRelativeFile(item.file, targetSkill);
51
- const identity = portablePathIdentity(item.file);
52
- if (byteIdentities.has(item.file) || canonicalIdentities.has(identity)) {
53
- throw new SkillLoopProposalError("PROPOSAL_PATH_COLLISION", "proposal file identities collide");
54
- }
55
- byteIdentities.add(item.file);
56
- canonicalIdentities.add(identity);
57
- }
58
- }
59
-
60
- export function validateProposal(proposal) {
61
- let serialized;
62
- try {
63
- serialized = prettyJsonBytes(proposal);
64
- } catch {
65
- throw schemaError();
66
- }
67
- if (serialized.length > PROPOSAL_MAX_BYTES) {
68
- throw new SkillLoopProposalError("PROPOSAL_TOO_LARGE", "proposal exceeds 64 KiB");
69
- }
70
- if (!requireExactKeys(proposal, required, optional)
71
- || proposal.schema !== "litfamily.skill-proposal/v1"
72
- || !RECORD_ID.test(proposal.id)
73
- || parseTimestamp(proposal.createdAt) === null
74
- || proposal.host !== "litclaude"
75
- || !text(proposal.sessionRef, 1, 512)
76
- || !text(proposal.signal, 1, 128)
77
- || !SKILL_ID.test(proposal.targetSkill)
78
- || portablePathIdentity(proposal.targetSkill) === null
79
- || !text(proposal.targetRoot, 1, 2048)
80
- || !["patch", "create", "add-reference", "archive"].includes(proposal.action)
81
- || !text(proposal.rationale, 1, 8192)
82
- || !Array.isArray(proposal.evidenceRefs) || proposal.evidenceRefs.length > 32
83
- || proposal.evidenceRefs.some((value) => !text(value, 1, 2048))
84
- || new Set(proposal.evidenceRefs).size !== proposal.evidenceRefs.length
85
- || !["pending", "approved", "applied", "rejected", "rolled-back"].includes(proposal.status)) {
86
- throw schemaError();
87
- }
88
- const hasPatch = Object.hasOwn(proposal, "patch");
89
- const hasCreate = Object.hasOwn(proposal, "createSpec");
90
- if (proposal.action === "patch") {
91
- if (!hasPatch || hasCreate) throw schemaError();
92
- validatePatch(proposal.patch, proposal.targetSkill);
93
- } else if (["create", "add-reference"].includes(proposal.action)) {
94
- if (hasPatch || !hasCreate) throw schemaError();
95
- validateCreateSpec(proposal.createSpec, proposal.targetSkill);
96
- } else if (hasPatch || hasCreate) throw schemaError();
97
- const needsLedger = ["applied", "rolled-back"].includes(proposal.status);
98
- if (needsLedger !== Object.hasOwn(proposal, "ledgerEntryId")
99
- || (needsLedger && !text(proposal.ledgerEntryId, 1, 128))) throw schemaError();
100
- assertNoSecretStrings(proposal, () => {
101
- throw new SkillLoopProposalError("PROPOSAL_SECRET_REFUSED", "credential-shaped proposal content was refused");
102
- });
103
- return proposal;
104
- }