@litfamily/litcodex 1.0.0 → 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 (121) hide show
  1. package/README.md +18 -21
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +2 -38
  4. package/dist/config-migration/profile-files.d.ts +4 -0
  5. package/dist/config-migration/profile-files.js +22 -0
  6. package/dist/install/capability-preflight-error.js +5 -1
  7. package/dist/install/codex.d.ts +1 -0
  8. package/dist/install/generated-skill-payload-hashes.d.ts +1 -6
  9. package/dist/install/generated-skill-payload-hashes.js +1 -6
  10. package/dist/install/host-capabilities.d.ts +2 -0
  11. package/dist/install/host-capabilities.js +13 -1
  12. package/dist/install/index.js +3 -1
  13. package/dist/install/skill-catalog.d.ts +1 -1
  14. package/dist/install/skill-catalog.js +1 -6
  15. package/dist/install/skill-resource-hashes.d.ts +1 -1
  16. package/dist/install/skill-resource-hashes.js +1 -1
  17. package/dist/install/strict-config-probe.d.ts +2 -0
  18. package/dist/install/strict-config-probe.js +34 -5
  19. package/dist/install/uninstall.d.ts +2 -0
  20. package/dist/install/uninstall.js +34 -0
  21. package/marketplace/plugins/litcodex/.codex-plugin/plugin.json +1 -1
  22. package/marketplace/plugins/litcodex/components/auto-update/package.json +1 -1
  23. package/marketplace/plugins/litcodex/components/comment-checker/package.json +1 -1
  24. package/marketplace/plugins/litcodex/components/git-bash/package.json +1 -1
  25. package/marketplace/plugins/litcodex/components/lit-loop/dist/cli.js +3 -20
  26. package/marketplace/plugins/litcodex/components/lit-loop/dist/codex-hook.js +1 -8
  27. package/marketplace/plugins/litcodex/components/lit-loop/dist/hook-cli.d.ts +2 -1
  28. package/marketplace/plugins/litcodex/components/lit-loop/dist/hook-cli.js +4 -27
  29. package/marketplace/plugins/litcodex/components/lit-loop/dist/markers.d.ts +0 -2
  30. package/marketplace/plugins/litcodex/components/lit-loop/dist/markers.js +0 -2
  31. package/marketplace/plugins/litcodex/components/lit-loop/dist/modes.d.ts +1 -3
  32. package/marketplace/plugins/litcodex/components/lit-loop/dist/modes.js +1 -10
  33. package/marketplace/plugins/litcodex/components/lit-loop/package.json +1 -1
  34. package/marketplace/plugins/litcodex/components/lsp/package.json +1 -1
  35. package/marketplace/plugins/litcodex/components/rules/dist/static-injection.js +2 -2
  36. package/marketplace/plugins/litcodex/components/rules/package.json +1 -1
  37. package/marketplace/plugins/litcodex/components/start-work-continuation/package.json +1 -1
  38. package/marketplace/plugins/litcodex/components/telemetry/package.json +1 -1
  39. package/marketplace/plugins/litcodex/components/wikify-knowledge/package.json +1 -1
  40. package/marketplace/plugins/litcodex/skills/browser-drive/scripts/capability-probe.mjs +13 -5
  41. package/node_modules/@litcodex/lit-loop/README.md +1 -7
  42. package/node_modules/@litcodex/lit-loop/dist/cli.js +3 -20
  43. package/node_modules/@litcodex/lit-loop/dist/codex-hook.js +1 -8
  44. package/node_modules/@litcodex/lit-loop/dist/hook-cli.d.ts +2 -1
  45. package/node_modules/@litcodex/lit-loop/dist/hook-cli.js +4 -27
  46. package/node_modules/@litcodex/lit-loop/dist/markers.d.ts +0 -2
  47. package/node_modules/@litcodex/lit-loop/dist/markers.js +0 -2
  48. package/node_modules/@litcodex/lit-loop/dist/modes.d.ts +1 -3
  49. package/node_modules/@litcodex/lit-loop/dist/modes.js +1 -10
  50. package/node_modules/@litcodex/lit-loop/package.json +1 -1
  51. package/node_modules/@litcodex/lit-loop/skills/browser-drive/scripts/capability-probe.mjs +13 -5
  52. package/package.json +2 -2
  53. package/readme-assets/badge-version.svg +1 -1
  54. package/readme-assets/cover-motion.webp +0 -0
  55. package/marketplace/plugins/litcodex/components/lit-loop/directives/skill-observer.md +0 -105
  56. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/apply.d.ts +0 -16
  57. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/apply.js +0 -227
  58. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/config.d.ts +0 -13
  59. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/config.js +0 -12
  60. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/curator.d.ts +0 -14
  61. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/curator.js +0 -112
  62. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/hook.d.ts +0 -19
  63. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/hook.js +0 -435
  64. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/ownership.d.ts +0 -6
  65. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/ownership.js +0 -145
  66. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/paths.d.ts +0 -18
  67. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/paths.js +0 -43
  68. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/review.d.ts +0 -28
  69. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/review.js +0 -702
  70. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/store.d.ts +0 -32
  71. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/store.js +0 -667
  72. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/transaction.d.ts +0 -26
  73. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/transaction.js +0 -524
  74. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/types.d.ts +0 -81
  75. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/types.js +0 -246
  76. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/usage.d.ts +0 -6
  77. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/usage.js +0 -119
  78. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop-cli.d.ts +0 -8
  79. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop-cli.js +0 -111
  80. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-cli.d.ts +0 -3
  81. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-cli.js +0 -116
  82. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-route.d.ts +0 -6
  83. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-route.js +0 -12
  84. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer.d.ts +0 -18
  85. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer.js +0 -946
  86. package/marketplace/plugins/litcodex/skills/skill-observer/SKILL.md +0 -215
  87. package/marketplace/plugins/litcodex/skills/skill-observer/agents/openai.yaml +0 -6
  88. package/marketplace/plugins/litcodex/skills/skill-observer/references/review-contract.md +0 -77
  89. package/node_modules/@litcodex/lit-loop/directives/skill-observer.md +0 -105
  90. package/node_modules/@litcodex/lit-loop/dist/skill-loop/apply.d.ts +0 -16
  91. package/node_modules/@litcodex/lit-loop/dist/skill-loop/apply.js +0 -227
  92. package/node_modules/@litcodex/lit-loop/dist/skill-loop/config.d.ts +0 -13
  93. package/node_modules/@litcodex/lit-loop/dist/skill-loop/config.js +0 -12
  94. package/node_modules/@litcodex/lit-loop/dist/skill-loop/curator.d.ts +0 -14
  95. package/node_modules/@litcodex/lit-loop/dist/skill-loop/curator.js +0 -112
  96. package/node_modules/@litcodex/lit-loop/dist/skill-loop/hook.d.ts +0 -19
  97. package/node_modules/@litcodex/lit-loop/dist/skill-loop/hook.js +0 -435
  98. package/node_modules/@litcodex/lit-loop/dist/skill-loop/ownership.d.ts +0 -6
  99. package/node_modules/@litcodex/lit-loop/dist/skill-loop/ownership.js +0 -145
  100. package/node_modules/@litcodex/lit-loop/dist/skill-loop/paths.d.ts +0 -18
  101. package/node_modules/@litcodex/lit-loop/dist/skill-loop/paths.js +0 -43
  102. package/node_modules/@litcodex/lit-loop/dist/skill-loop/review.d.ts +0 -28
  103. package/node_modules/@litcodex/lit-loop/dist/skill-loop/review.js +0 -702
  104. package/node_modules/@litcodex/lit-loop/dist/skill-loop/store.d.ts +0 -32
  105. package/node_modules/@litcodex/lit-loop/dist/skill-loop/store.js +0 -667
  106. package/node_modules/@litcodex/lit-loop/dist/skill-loop/transaction.d.ts +0 -26
  107. package/node_modules/@litcodex/lit-loop/dist/skill-loop/transaction.js +0 -524
  108. package/node_modules/@litcodex/lit-loop/dist/skill-loop/types.d.ts +0 -81
  109. package/node_modules/@litcodex/lit-loop/dist/skill-loop/types.js +0 -246
  110. package/node_modules/@litcodex/lit-loop/dist/skill-loop/usage.d.ts +0 -6
  111. package/node_modules/@litcodex/lit-loop/dist/skill-loop/usage.js +0 -119
  112. package/node_modules/@litcodex/lit-loop/dist/skill-loop-cli.d.ts +0 -8
  113. package/node_modules/@litcodex/lit-loop/dist/skill-loop-cli.js +0 -111
  114. package/node_modules/@litcodex/lit-loop/dist/skill-observer-cli.d.ts +0 -3
  115. package/node_modules/@litcodex/lit-loop/dist/skill-observer-cli.js +0 -116
  116. package/node_modules/@litcodex/lit-loop/dist/skill-observer-route.d.ts +0 -6
  117. package/node_modules/@litcodex/lit-loop/dist/skill-observer-route.js +0 -12
  118. package/node_modules/@litcodex/lit-loop/dist/skill-observer.d.ts +0 -18
  119. package/node_modules/@litcodex/lit-loop/dist/skill-observer.js +0 -946
  120. package/node_modules/@litcodex/lit-loop/skills/skill-observer/SKILL.md +0 -215
  121. package/node_modules/@litcodex/lit-loop/skills/skill-observer/references/review-contract.md +0 -77
@@ -1,667 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { closeSync, constants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
3
- import { dirname, join, relative, resolve } from "node:path";
4
- import { isWithin, skillLoopPaths, targetSkillDir } from "./paths.js";
5
- import { canonicalPathIdentity, SkillLoopError, validateProposal, validateRelativeFile, validateTargetSkillIdentity, } from "./types.js";
6
- function assertDirectory(path, label) {
7
- const stat = lstatSync(path);
8
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
9
- throw new SkillLoopError("SKILL_LOOP_PATH_UNSAFE", `${label} is not a safe directory`);
10
- }
11
- }
12
- function ensureDirectory(path, label) {
13
- mkdirSync(path, { recursive: true, mode: 0o700 });
14
- assertDirectory(path, label);
15
- }
16
- export function ensureWorkspaceState(paths) {
17
- assertDirectory(paths.workspaceRoot, "workspace root");
18
- ensureDirectory(paths.stateRoot, "skill-loop state root");
19
- }
20
- export function ensureAgentRoot(paths) {
21
- const home = resolve(paths.agentRoot, "..", "..");
22
- assertDirectory(home, "HOME");
23
- const agents = join(home, ".agents");
24
- ensureDirectory(agents, "agent directory");
25
- ensureDirectory(paths.agentRoot, "agent skill root");
26
- }
27
- function ensureAgentLockRoot(paths) {
28
- const home = resolve(paths.agentRoot, "..", "..");
29
- assertDirectory(home, "HOME");
30
- ensureDirectory(resolve(paths.agentRoot, ".."), "agent directory");
31
- }
32
- export function atomicWrite(path, bytes, mode = 0o600) {
33
- ensureDirectory(dirname(path), "write parent");
34
- if (existsSync(path) && lstatSync(path).isSymbolicLink()) {
35
- throw new SkillLoopError("SKILL_LOOP_PATH_UNSAFE", "the write target is a symbolic link");
36
- }
37
- const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
38
- let descriptor;
39
- try {
40
- descriptor = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, mode);
41
- writeFileSync(descriptor, bytes);
42
- fsyncSync(descriptor);
43
- closeSync(descriptor);
44
- descriptor = undefined;
45
- renameSync(temp, path);
46
- fsyncDirectory(dirname(path));
47
- }
48
- catch (error) {
49
- if (descriptor !== undefined)
50
- closeSync(descriptor);
51
- if (existsSync(temp))
52
- unlinkSync(temp);
53
- throw error instanceof SkillLoopError
54
- ? error
55
- : new SkillLoopError("SKILL_LOOP_WRITE_FAILED", "the skill-loop write failed");
56
- }
57
- }
58
- export function fsyncDirectory(path) {
59
- const descriptor = openSync(path, constants.O_RDONLY);
60
- try {
61
- fsyncSync(descriptor);
62
- }
63
- finally {
64
- closeSync(descriptor);
65
- }
66
- }
67
- function reclaimStaleLock(lockPath, syncRoot) {
68
- let before;
69
- try {
70
- before = lstatSync(lockPath);
71
- }
72
- catch (error) {
73
- if (error.code === "ENOENT")
74
- return;
75
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "the skill-loop lock cannot be inspected safely");
76
- }
77
- const uid = typeof process.getuid === "function" ? process.getuid() : before.uid;
78
- if (before.isSymbolicLink() ||
79
- !before.isFile() ||
80
- before.size > 32 ||
81
- (before.mode & 0o777) !== 0o600 ||
82
- (before.uid !== uid && before.uid !== 0) ||
83
- before.nlink !== 1) {
84
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "the skill-loop lock is unsafe");
85
- }
86
- let owner;
87
- try {
88
- owner = readFileSync(lockPath, "utf8");
89
- }
90
- catch {
91
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "the skill-loop lock cannot be read safely");
92
- }
93
- if (!/^[1-9]\d{0,9}\n$/u.test(owner)) {
94
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "the skill-loop lock owner is invalid");
95
- }
96
- try {
97
- process.kill(Number(owner.trim()), 0);
98
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "another skill-loop mutation owns the lock");
99
- }
100
- catch (error) {
101
- if (error instanceof SkillLoopError)
102
- throw error;
103
- if (error.code !== "ESRCH") {
104
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "the skill-loop lock owner cannot be proven stale");
105
- }
106
- }
107
- let after;
108
- try {
109
- after = lstatSync(lockPath);
110
- }
111
- catch {
112
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "the skill-loop lock changed during inspection");
113
- }
114
- if (after.isSymbolicLink() ||
115
- !after.isFile() ||
116
- after.dev !== before.dev ||
117
- after.ino !== before.ino ||
118
- after.uid !== before.uid ||
119
- after.mode !== before.mode ||
120
- after.nlink !== before.nlink ||
121
- after.size !== before.size ||
122
- after.mtimeMs !== before.mtimeMs ||
123
- after.ctimeMs !== before.ctimeMs ||
124
- after.birthtimeMs !== before.birthtimeMs) {
125
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "the skill-loop lock changed during inspection");
126
- }
127
- unlinkSync(lockPath);
128
- fsyncDirectory(syncRoot);
129
- }
130
- function withMutationLock(lockPath, syncRoot, prepare, work) {
131
- prepare();
132
- let descriptor;
133
- try {
134
- descriptor = openSync(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
135
- }
136
- catch {
137
- reclaimStaleLock(lockPath, syncRoot);
138
- try {
139
- descriptor = openSync(lockPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
140
- }
141
- catch {
142
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "another skill-loop mutation owns the lock");
143
- }
144
- }
145
- const pin = fstatSync(descriptor);
146
- const uid = typeof process.getuid === "function" ? process.getuid() : pin.uid;
147
- if (!pin.isFile() || pin.nlink !== 1 || (pin.mode & 0o777) !== 0o600 || (pin.uid !== uid && pin.uid !== 0)) {
148
- closeSync(descriptor);
149
- throw new SkillLoopError("SKILL_LOOP_LOCK_BUSY", "the skill-loop lock is unsafe");
150
- }
151
- let result;
152
- let workError;
153
- let releasePin = pin;
154
- try {
155
- writeFileSync(descriptor, `${process.pid}\n`);
156
- fsyncSync(descriptor);
157
- releasePin = fstatSync(descriptor);
158
- result = work();
159
- }
160
- catch (error) {
161
- workError = error;
162
- }
163
- try {
164
- closeSync(descriptor);
165
- const beforeRelease = lstatSync(lockPath);
166
- if (beforeRelease.isSymbolicLink() ||
167
- !beforeRelease.isFile() ||
168
- beforeRelease.dev !== releasePin.dev ||
169
- beforeRelease.ino !== releasePin.ino ||
170
- beforeRelease.uid !== releasePin.uid ||
171
- beforeRelease.mode !== releasePin.mode ||
172
- beforeRelease.nlink !== releasePin.nlink ||
173
- beforeRelease.ctimeMs !== releasePin.ctimeMs ||
174
- beforeRelease.birthtimeMs !== releasePin.birthtimeMs) {
175
- throw new SkillLoopError("SKILL_LOOP_LOCK_RELEASE_FAILED", "the skill-loop lock changed before release");
176
- }
177
- unlinkSync(lockPath);
178
- fsyncDirectory(syncRoot);
179
- }
180
- catch {
181
- if (workError === undefined) {
182
- throw new SkillLoopError("SKILL_LOOP_LOCK_RELEASE_FAILED", "the skill-loop lock could not be released");
183
- }
184
- }
185
- if (workError !== undefined)
186
- throw workError;
187
- return result;
188
- }
189
- export function withSkillLoopLock(paths, work) {
190
- return withMutationLock(paths.lockPath, paths.stateRoot, () => ensureWorkspaceState(paths), work);
191
- }
192
- export function withAgentMutationLock(paths, work) {
193
- return withMutationLock(paths.agentMutationLockPath, resolve(paths.agentRoot, ".."), () => ensureAgentLockRoot(paths), work);
194
- }
195
- export function assertAgentMutationLockHeld(paths) {
196
- let stat;
197
- let owner;
198
- try {
199
- stat = lstatSync(paths.agentMutationLockPath);
200
- owner = readFileSync(paths.agentMutationLockPath, "utf8");
201
- }
202
- catch {
203
- throw new SkillLoopError("SKILL_LOOP_LOCK_REQUIRED", "the user skill mutation lock is required");
204
- }
205
- const uid = typeof process.getuid === "function" ? process.getuid() : stat.uid;
206
- if (stat.isSymbolicLink() ||
207
- !stat.isFile() ||
208
- stat.nlink !== 1 ||
209
- (stat.mode & 0o777) !== 0o600 ||
210
- (stat.uid !== uid && stat.uid !== 0) ||
211
- owner !== `${process.pid}\n`) {
212
- throw new SkillLoopError("SKILL_LOOP_LOCK_REQUIRED", "the user skill mutation lock is not held safely");
213
- }
214
- }
215
- function proposalPath(paths, id) {
216
- if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(id)) {
217
- throw new SkillLoopError("PROPOSAL_ID_INVALID", "the proposal id is invalid");
218
- }
219
- const path = resolve(paths.proposalsDir, `${id}.json`);
220
- if (!isWithin(paths.proposalsDir, path))
221
- throw new SkillLoopError("PROPOSAL_ID_INVALID", "the proposal id is invalid");
222
- return path;
223
- }
224
- export function writeProposalRecord(paths, proposal) {
225
- const validated = validateProposal(proposal, paths.agentRoot);
226
- validateProposalLedgerBinding(paths, validated);
227
- atomicWrite(proposalPath(paths, validated.id), `${JSON.stringify(validated, null, 2)}\n`);
228
- }
229
- export function readProposalRecord(paths, id) {
230
- const path = proposalPath(paths, id);
231
- if (!existsSync(path) || lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile()) {
232
- throw new SkillLoopError("PROPOSAL_NOT_FOUND", "the proposal was not found");
233
- }
234
- try {
235
- const proposal = validateProposal(JSON.parse(readFileSync(path, "utf8")), paths.agentRoot);
236
- validateProposalLedgerBinding(paths, proposal);
237
- return proposal;
238
- }
239
- catch (error) {
240
- if (error instanceof SkillLoopError)
241
- throw error;
242
- throw new SkillLoopError("PROPOSAL_SCHEMA_INVALID", "the stored proposal is invalid");
243
- }
244
- }
245
- function validateProposalLedgerBinding(paths, proposal) {
246
- if (proposal.status === "pending" || proposal.status === "approved")
247
- return;
248
- const expectedOperation = proposal.status === "applied" ? "apply" : proposal.status === "rejected" ? "reject" : "rollback";
249
- const entry = readLedgerFromPaths(paths).find((candidate) => candidate.id === proposal.ledgerEntryId);
250
- if (entry === undefined ||
251
- entry.operation !== expectedOperation ||
252
- entry.proposalId !== proposal.id ||
253
- entry.targetSkill !== proposal.targetSkill ||
254
- resolve(entry.targetRoot) !== resolve(proposal.targetRoot)) {
255
- throw new SkillLoopError("TRANSACTION_REFERENCE_INVALID", "the terminal proposal ledger binding is invalid");
256
- }
257
- }
258
- export function storeProposal(workspaceRoot, value, options = {}) {
259
- const stored = storeProposalBatch(workspaceRoot, [value], options);
260
- const proposal = stored[0];
261
- if (proposal === undefined)
262
- throw new SkillLoopError("PROPOSAL_SCHEMA_INVALID", "the proposal batch was empty");
263
- return proposal;
264
- }
265
- export function storeProposalBatch(workspaceRoot, values, options = {}) {
266
- const paths = skillLoopPaths(workspaceRoot, options.env);
267
- if (values.length < 1 || values.length > 32) {
268
- throw new SkillLoopError("PROPOSAL_SCHEMA_INVALID", "a proposal batch must contain 1 to 32 records");
269
- }
270
- const proposals = values.map((value) => validateProposal(value, paths.agentRoot));
271
- if (proposals.some((proposal) => proposal.status !== "pending")) {
272
- throw new SkillLoopError("PROPOSAL_STATUS_INVALID", "new proposals must be pending");
273
- }
274
- if (new Set(proposals.map((proposal) => proposal.id)).size !== proposals.length) {
275
- throw new SkillLoopError("PROPOSAL_EXISTS", "the proposal batch contains duplicate ids");
276
- }
277
- return withSkillLoopLock(paths, () => {
278
- for (const proposal of proposals) {
279
- assertNoCanonicalSkillCollision(paths, proposal.targetSkill);
280
- assertTargetNotPinned(paths, proposal.targetSkill);
281
- }
282
- ensureDirectory(paths.proposalsDir, "proposal queue");
283
- for (const proposal of proposals) {
284
- if (existsSync(proposalPath(paths, proposal.id))) {
285
- throw new SkillLoopError("PROPOSAL_EXISTS", "a proposal id already exists");
286
- }
287
- }
288
- const stage = join(paths.proposalsDir, `.batch-${process.pid}-${randomUUID()}`);
289
- mkdirSync(stage, { recursive: false, mode: 0o700 });
290
- const moved = [];
291
- try {
292
- for (const proposal of proposals) {
293
- atomicWrite(join(stage, `${proposal.id}.json`), `${JSON.stringify(proposal, null, 2)}\n`);
294
- }
295
- for (const proposal of proposals) {
296
- const target = proposalPath(paths, proposal.id);
297
- renameSync(join(stage, `${proposal.id}.json`), target);
298
- moved.push(target);
299
- }
300
- }
301
- catch (error) {
302
- for (const target of moved) {
303
- if (existsSync(target))
304
- unlinkSync(target);
305
- }
306
- throw error;
307
- }
308
- finally {
309
- if (existsSync(stage))
310
- rmSync(stage, { recursive: true, force: true });
311
- }
312
- return proposals;
313
- });
314
- }
315
- export function assertNoCanonicalSkillCollision(paths, skillId) {
316
- validateTargetSkillIdentity(skillId);
317
- if (!existsSync(paths.agentRoot))
318
- return;
319
- assertDirectory(paths.agentRoot, "agent skill root");
320
- const identity = canonicalPathIdentity(skillId);
321
- for (const entry of readdirSync(paths.agentRoot, { withFileTypes: true })) {
322
- if (canonicalPathIdentity(entry.name) !== identity)
323
- continue;
324
- if (entry.name !== skillId || !entry.isDirectory()) {
325
- throw new SkillLoopError("TARGET_NOT_AGENT_OWNED", "an existing skill collides by canonical identity");
326
- }
327
- }
328
- }
329
- export function assertTargetNotPinned(paths, skillId) {
330
- const sidecar = readUsage(paths);
331
- if (sidecar === null)
332
- return;
333
- const skills = sidecar["skills"];
334
- if (typeof skills !== "object" || skills === null || Array.isArray(skills)) {
335
- throw new SkillLoopError("USAGE_INVALID", "the usage sidecar skills are invalid");
336
- }
337
- const entry = skills[skillId];
338
- if (entry === undefined)
339
- return;
340
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
341
- throw new SkillLoopError("USAGE_INVALID", "the target usage entry is invalid");
342
- }
343
- const pinned = entry["pinned"];
344
- if (typeof pinned !== "boolean") {
345
- throw new SkillLoopError("USAGE_INVALID", "the target usage pin is invalid");
346
- }
347
- if (pinned)
348
- throw new SkillLoopError("TARGET_PINNED", "the proposal target is pinned");
349
- }
350
- export function listProposals(workspaceRoot, options = {}) {
351
- const paths = skillLoopPaths(workspaceRoot, options.env);
352
- if (!existsSync(paths.proposalsDir))
353
- return [];
354
- assertDirectory(paths.proposalsDir, "proposal queue");
355
- return readdirSync(paths.proposalsDir, { withFileTypes: true })
356
- .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
357
- .map((entry) => readProposalRecord(paths, entry.name.slice(0, -5)))
358
- .sort((left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id));
359
- }
360
- function validateSnapshot(snapshot) {
361
- if (!Array.isArray(snapshot) || snapshot.length > 256)
362
- return false;
363
- const identities = new Set();
364
- for (const item of snapshot) {
365
- if (typeof item !== "object" || item === null || Array.isArray(item))
366
- return false;
367
- const record = item;
368
- if (Object.keys(record).some((key) => key !== "file" && key !== "blob"))
369
- return false;
370
- if (typeof record["file"] !== "string")
371
- return false;
372
- try {
373
- validateRelativeFile(record["file"], "__ledger_target__");
374
- }
375
- catch {
376
- return false;
377
- }
378
- const identity = canonicalPathIdentity(record["file"]);
379
- if (identities.has(identity))
380
- return false;
381
- identities.add(identity);
382
- const blob = record["blob"];
383
- if (typeof blob !== "object" || blob === null || Array.isArray(blob))
384
- return false;
385
- const blobRecord = blob;
386
- if (blobRecord["algorithm"] !== "sha256" ||
387
- typeof blobRecord["digest"] !== "string" ||
388
- !/^[a-f0-9]{64}$/u.test(blobRecord["digest"]) ||
389
- typeof blobRecord["size"] !== "number" ||
390
- !Number.isInteger(blobRecord["size"]) ||
391
- blobRecord["size"] < 0) {
392
- return false;
393
- }
394
- }
395
- return true;
396
- }
397
- export function validateLedgerEntry(value) {
398
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
399
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger entry is invalid");
400
- }
401
- const record = value;
402
- const allowed = new Set([
403
- "schema",
404
- "id",
405
- "createdAt",
406
- "proposalId",
407
- "actor",
408
- "operation",
409
- "targetSkill",
410
- "targetRoot",
411
- "before",
412
- "after",
413
- "restoresLedgerEntryId",
414
- "reason",
415
- ]);
416
- if (Object.keys(record).some((key) => !allowed.has(key))) {
417
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger entry has an unknown field");
418
- }
419
- const id = record["id"];
420
- const createdAt = record["createdAt"];
421
- const proposalId = record["proposalId"];
422
- const actor = record["actor"];
423
- const operation = record["operation"];
424
- const targetSkill = record["targetSkill"];
425
- const targetRoot = record["targetRoot"];
426
- const restores = record["restoresLedgerEntryId"];
427
- const reason = record["reason"];
428
- if (record["schema"] !== "litfamily.skill-ledger/v1" ||
429
- typeof id !== "string" ||
430
- !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(id) ||
431
- typeof createdAt !== "string" ||
432
- !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/u.test(createdAt) ||
433
- Number.isNaN(Date.parse(createdAt)) ||
434
- !["user", "curator"].includes(actor) ||
435
- !["apply", "reject", "rollback", "curator-transition"].includes(operation) ||
436
- typeof targetSkill !== "string" ||
437
- typeof targetRoot !== "string" ||
438
- targetRoot.length < 1 ||
439
- targetRoot.length > 2048 ||
440
- !(proposalId === null ||
441
- (typeof proposalId === "string" && proposalId.length >= 1 && proposalId.length <= 128)) ||
442
- !(reason === undefined || (typeof reason === "string" && reason.length <= 8192)) ||
443
- !validateSnapshot(record["before"]) ||
444
- !validateSnapshot(record["after"])) {
445
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger entry is invalid");
446
- }
447
- try {
448
- validateTargetSkillIdentity(targetSkill);
449
- }
450
- catch {
451
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger target identity is invalid");
452
- }
453
- if (operation === "rollback"
454
- ? actor !== "user" || typeof restores !== "string" || restores.length < 1 || restores.length > 128
455
- : restores !== undefined) {
456
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger rollback mapping is invalid");
457
- }
458
- if ((operation === "apply" || operation === "reject") && (actor !== "user" || typeof proposalId !== "string")) {
459
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger user decision mapping is invalid");
460
- }
461
- if (operation === "curator-transition" && (actor !== "curator" || proposalId !== null)) {
462
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger curator mapping is invalid");
463
- }
464
- return record;
465
- }
466
- export function appendLedgerEntry(paths, entry) {
467
- const validated = validateLedgerEntry(entry);
468
- ensureDirectory(dirname(paths.ledgerPath), "ledger parent");
469
- if (existsSync(paths.ledgerPath) && lstatSync(paths.ledgerPath).isSymbolicLink()) {
470
- throw new SkillLoopError("SKILL_LOOP_PATH_UNSAFE", "the skill ledger is a symbolic link");
471
- }
472
- const existing = existsSync(paths.ledgerPath) ? readFileSync(paths.ledgerPath) : Buffer.alloc(0);
473
- if (existing.byteLength > 0 && existing.at(-1) !== 0x0a) {
474
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger has an incomplete final record");
475
- }
476
- atomicWrite(paths.ledgerPath, Buffer.concat([existing, Buffer.from(`${JSON.stringify(validated)}\n`, "utf8")]));
477
- }
478
- function readLedgerFromPaths(paths) {
479
- if (!existsSync(paths.ledgerPath))
480
- return [];
481
- if (lstatSync(paths.ledgerPath).isSymbolicLink() || !lstatSync(paths.ledgerPath).isFile()) {
482
- throw new SkillLoopError("SKILL_LOOP_PATH_UNSAFE", "the skill ledger is unsafe");
483
- }
484
- const lines = readFileSync(paths.ledgerPath, "utf8")
485
- .split("\n")
486
- .filter((line) => line.trim().length > 0);
487
- const entries = lines.map((line) => {
488
- try {
489
- return validateLedgerEntry(JSON.parse(line));
490
- }
491
- catch (error) {
492
- if (error instanceof SkillLoopError)
493
- throw error;
494
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger contains invalid JSON");
495
- }
496
- });
497
- for (const entry of entries)
498
- validateSnapshotBlobs(paths, [...entry.before, ...entry.after]);
499
- if (new Set(entries.map((entry) => entry.id)).size !== entries.length) {
500
- throw new SkillLoopError("LEDGER_INVALID", "the skill ledger contains duplicate ids");
501
- }
502
- const prior = new Map();
503
- for (const entry of entries) {
504
- if (entry.operation === "rollback") {
505
- const source = entry.restoresLedgerEntryId === undefined ? undefined : prior.get(entry.restoresLedgerEntryId);
506
- if (source === undefined ||
507
- source.operation === "reject" ||
508
- source.targetSkill !== entry.targetSkill ||
509
- resolve(source.targetRoot) !== resolve(entry.targetRoot) ||
510
- !snapshotsEqual(entry.before, source.after) ||
511
- !snapshotsEqual(entry.after, source.before)) {
512
- throw new SkillLoopError("LEDGER_INVALID", "a rollback ledger reference is invalid");
513
- }
514
- }
515
- prior.set(entry.id, entry);
516
- }
517
- return entries;
518
- }
519
- export function readLedger(workspaceRoot, options = {}) {
520
- return readLedgerFromPaths(skillLoopPaths(workspaceRoot, options.env));
521
- }
522
- function blobPath(paths, digest) {
523
- return join(paths.blobsDir, digest.slice(0, 2), digest);
524
- }
525
- export function storeBlob(paths, bytes) {
526
- const digest = createHash("sha256").update(bytes).digest("hex");
527
- const path = blobPath(paths, digest);
528
- if (!existsSync(path))
529
- atomicWrite(path, bytes);
530
- const stored = readFileSync(path);
531
- if (!stored.equals(bytes))
532
- throw new SkillLoopError("BLOB_INTEGRITY_FAILED", "a stored skill blob mismatched");
533
- return { algorithm: "sha256", digest, size: bytes.byteLength };
534
- }
535
- export function readBlob(paths, item) {
536
- const path = blobPath(paths, item.blob.digest);
537
- if (!existsSync(path) || lstatSync(path).isSymbolicLink() || !lstatSync(path).isFile()) {
538
- throw new SkillLoopError("BLOB_MISSING", "a required skill blob is missing");
539
- }
540
- const bytes = readFileSync(path);
541
- const digest = createHash("sha256").update(bytes).digest("hex");
542
- if (digest !== item.blob.digest || bytes.byteLength !== item.blob.size) {
543
- throw new SkillLoopError("BLOB_INTEGRITY_FAILED", "a required skill blob failed integrity validation");
544
- }
545
- return bytes;
546
- }
547
- export function validateSnapshotBlobs(paths, snapshot) {
548
- for (const item of snapshot)
549
- readBlob(paths, item);
550
- }
551
- function collectFiles(root, directory = root, out = []) {
552
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
553
- const path = join(directory, entry.name);
554
- const stat = lstatSync(path);
555
- if (stat.isSymbolicLink())
556
- throw new SkillLoopError("TARGET_PATH_UNSAFE", "skill packages may not contain links");
557
- if (stat.isDirectory())
558
- collectFiles(root, path, out);
559
- else if (stat.isFile())
560
- out.push(path);
561
- else
562
- throw new SkillLoopError("TARGET_PATH_UNSAFE", "skill packages may contain regular files only");
563
- if (out.length > 256)
564
- throw new SkillLoopError("TARGET_TOO_LARGE", "the skill package contains too many files");
565
- }
566
- return out;
567
- }
568
- export function snapshotSkill(paths, skillId) {
569
- assertNoCanonicalSkillCollision(paths, skillId);
570
- const target = targetSkillDir(paths, skillId);
571
- if (!existsSync(target))
572
- return [];
573
- assertDirectory(target, "agent-owned skill target");
574
- return collectFiles(target)
575
- .map((path) => {
576
- const file = relative(target, path).replaceAll("\\", "/");
577
- validateRelativeFile(file, "__snapshot_target__");
578
- return { file, blob: storeBlob(paths, readFileSync(path)) };
579
- })
580
- .sort((left, right) => left.file.localeCompare(right.file));
581
- }
582
- export function snapshotDirectory(paths, directory) {
583
- if (!isWithin(paths.agentRoot, directory) || directory === paths.agentRoot) {
584
- throw new SkillLoopError("TARGET_PATH_UNSAFE", "the snapshot source escapes the agent root");
585
- }
586
- if (!existsSync(directory))
587
- return [];
588
- assertDirectory(directory, "snapshot source");
589
- return collectFiles(directory)
590
- .map((path) => {
591
- const file = relative(directory, path).replaceAll("\\", "/");
592
- validateRelativeFile(file, "__snapshot_target__");
593
- return { file, blob: storeBlob(paths, readFileSync(path)) };
594
- })
595
- .sort((left, right) => left.file.localeCompare(right.file));
596
- }
597
- export function snapshotsEqual(left, right) {
598
- return JSON.stringify(left) === JSON.stringify(right);
599
- }
600
- export function writeSnapshotToDirectory(paths, snapshot, directory) {
601
- if (!isWithin(paths.agentRoot, directory) || directory === paths.agentRoot) {
602
- throw new SkillLoopError("TARGET_PATH_UNSAFE", "the snapshot destination escapes the agent root");
603
- }
604
- mkdirSync(directory, { recursive: false, mode: 0o700 });
605
- for (const item of snapshot) {
606
- const target = resolve(directory, item.file);
607
- if (!isWithin(directory, target))
608
- throw new SkillLoopError("TARGET_PATH_UNSAFE", "a snapshot path escaped");
609
- ensureDirectory(dirname(target), "snapshot directory");
610
- atomicWrite(target, readBlob(paths, item));
611
- }
612
- }
613
- export function replaceSkillWithSnapshot(paths, skillId, snapshot) {
614
- ensureAgentRoot(paths);
615
- const target = targetSkillDir(paths, skillId);
616
- const nonce = `${process.pid}-${randomUUID()}`;
617
- const stage = join(paths.agentRoot, `.litcodex-stage-${nonce}`);
618
- const backup = join(paths.agentRoot, `.litcodex-backup-${nonce}`);
619
- if (snapshot.length > 0)
620
- writeSnapshotToDirectory(paths, snapshot, stage);
621
- let movedTarget = false;
622
- try {
623
- if (existsSync(target)) {
624
- renameSync(target, backup);
625
- movedTarget = true;
626
- }
627
- if (snapshot.length > 0)
628
- renameSync(stage, target);
629
- }
630
- catch (error) {
631
- if (existsSync(target) && movedTarget)
632
- rmSync(target, { recursive: true, force: true });
633
- if (movedTarget && existsSync(backup))
634
- renameSync(backup, target);
635
- if (existsSync(stage))
636
- rmSync(stage, { recursive: true, force: true });
637
- throw error instanceof SkillLoopError
638
- ? error
639
- : new SkillLoopError("TARGET_WRITE_FAILED", "the agent skill transaction failed");
640
- }
641
- if (movedTarget && existsSync(backup)) {
642
- try {
643
- rmSync(backup, { recursive: true, force: true });
644
- }
645
- catch {
646
- // The committed target is authoritative. A same-root backup is recoverable cleanup residue.
647
- }
648
- }
649
- fsyncDirectory(paths.agentRoot);
650
- }
651
- export function readUsage(paths) {
652
- if (!existsSync(paths.usagePath))
653
- return null;
654
- if (lstatSync(paths.usagePath).isSymbolicLink() || !lstatSync(paths.usagePath).isFile()) {
655
- throw new SkillLoopError("USAGE_INVALID", "the usage sidecar is unsafe");
656
- }
657
- try {
658
- return JSON.parse(readFileSync(paths.usagePath, "utf8"));
659
- }
660
- catch {
661
- throw new SkillLoopError("USAGE_INVALID", "the usage sidecar is invalid");
662
- }
663
- }
664
- export function writeUsage(paths, sidecar) {
665
- ensureAgentRoot(paths);
666
- atomicWrite(paths.usagePath, `${JSON.stringify(sidecar, null, 2)}\n`);
667
- }
@@ -1,26 +0,0 @@
1
- import type { SkillLedgerEntry, SkillProposal, SkillUsageSidecar } from "./types.js";
2
- export interface SkillTransactionCopy {
3
- readonly kind: "backup" | "archive";
4
- readonly relativePath: string;
5
- }
6
- export interface SkillTransactionJournal {
7
- readonly schema: "litcodex.skill-transaction/v1";
8
- readonly operation: "apply" | "rollback" | "curator-transition";
9
- readonly ledger: SkillLedgerEntry;
10
- readonly authorization: string;
11
- readonly proposalBefore?: SkillProposal;
12
- readonly proposalAfter?: SkillProposal;
13
- readonly usageBefore?: SkillUsageSidecar | null;
14
- readonly usageAfter?: SkillUsageSidecar;
15
- readonly copies: readonly SkillTransactionCopy[];
16
- }
17
- export type SkillTransactionDraft = Omit<SkillTransactionJournal, "authorization" | "proposalBefore" | "usageBefore">;
18
- export interface SkillTransactionExpectation {
19
- readonly operation: SkillTransactionJournal["operation"];
20
- readonly proposalId?: string;
21
- readonly restoresLedgerEntryId?: string;
22
- }
23
- export declare function readMutationJournal(workspaceRoot: string, env?: NodeJS.ProcessEnv): SkillTransactionJournal | null;
24
- export declare function writeMutationJournal(workspaceRoot: string, journal: SkillTransactionDraft, env?: NodeJS.ProcessEnv): SkillTransactionJournal;
25
- export declare function recoverMutationJournalWhileLocked(workspaceRoot: string, env: NodeJS.ProcessEnv | undefined, expected: SkillTransactionExpectation): SkillTransactionJournal | null;
26
- export declare function recoverMutationJournal(workspaceRoot: string, env: NodeJS.ProcessEnv | undefined, expected: SkillTransactionExpectation): SkillTransactionJournal | null;