@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,391 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import {
3
- existsSync,
4
- lstatSync,
5
- mkdirSync,
6
- readFileSync,
7
- renameSync,
8
- rmSync,
9
- } from "node:fs";
10
- import { dirname, join } from "node:path";
11
-
12
- import { resolveAgentTarget } from "./authority.mjs";
13
- import {
14
- assertNoSecretStrings,
15
- atomicWriteBytes,
16
- captureDirectoryIdentity,
17
- decodeSkillLoopUtf8,
18
- directoryIdentityMatches,
19
- ensureSafeDirectoryChain,
20
- parseTimestamp,
21
- safeIsoPath,
22
- withSkillLoopLock,
23
- } from "./common.mjs";
24
- import {
25
- appendLedgerEntryUnlocked,
26
- captureSkillSnapshot,
27
- captureStagedSnapshot,
28
- materializeSnapshot,
29
- proposalReceiptMatches,
30
- readLedger,
31
- SkillLoopLedgerError,
32
- } from "./ledger.mjs";
33
- import { readProposalUnlocked } from "./proposal-reader.mjs";
34
- import { proposalFilePath, readProposalRecord, transitionProposalRecordUnlocked } from "./proposal-store.mjs";
35
- import { recoverReviewTransactionUnlocked } from "./review-transaction.mjs";
36
- import {
37
- beginSkillMutationTransactionUnlocked,
38
- recoverSkillMutationTransactionUnlocked,
39
- } from "./mutation-transaction.mjs";
40
- import { prepareUsageUpdate, writeUsageUnlocked } from "./usage.mjs";
41
-
42
- export class SkillLoopApplyError extends Error {
43
- constructor(code, message = code) {
44
- super(`${code}: ${message}`);
45
- this.name = "SkillLoopApplyError";
46
- this.code = code;
47
- }
48
- }
49
-
50
- function ensureSkillRoot(root, create) {
51
- return ensureSafeDirectoryChain(root, [".claude", "skills"], {
52
- create,
53
- ErrorType: SkillLoopApplyError,
54
- code: "TARGET_PATH_UNSAFE",
55
- });
56
- }
57
-
58
- function assertTargetIdentity(root, proposal) {
59
- return resolveAgentTarget(root, proposal.targetRoot, proposal.targetSkill, {
60
- allowMissing: true,
61
- requireOwned: false,
62
- ErrorType: SkillLoopApplyError,
63
- });
64
- }
65
-
66
- function writeStageFile(stage, file, bytes) {
67
- const path = join(stage, ...file.split("/"));
68
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
69
- if (existsSync(path)) throw new SkillLoopApplyError("TARGET_FILE_EXISTS");
70
- atomicWriteBytes(path, Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes, "utf8"), SkillLoopApplyError, "TARGET_WRITE_FAILED");
71
- }
72
-
73
- function populateCreate(stage, proposal) {
74
- mkdirSync(stage, { mode: 0o700 });
75
- const sourceSkill = proposal.createSpec.files.find(({ file }) => file === "SKILL.md")?.content ?? "";
76
- const frontmatter = [
77
- "---",
78
- `name: ${proposal.targetSkill}`,
79
- "description: Agent-generated skill approved by the user.",
80
- "metadata:",
81
- " litclaudeAgentGenerated: \"true\"",
82
- "---",
83
- sourceSkill,
84
- ].join("\n");
85
- writeStageFile(stage, "SKILL.md", frontmatter.endsWith("\n") ? frontmatter : `${frontmatter}\n`);
86
- for (const file of proposal.createSpec.files) {
87
- if (file.file !== "SKILL.md") writeStageFile(stage, file.file, file.content);
88
- }
89
- }
90
-
91
- function buildStage(root, stage, before, proposal) {
92
- if (proposal.action === "create") {
93
- populateCreate(stage, proposal);
94
- return;
95
- }
96
- materializeSnapshot(root, stage, before);
97
- if (proposal.action === "patch") {
98
- const path = join(stage, ...proposal.patch.file.split("/"));
99
- const details = lstatSync(path, { throwIfNoEntry: false });
100
- if (!details?.isFile() || details.isSymbolicLink() || details.nlink !== 1) {
101
- throw new SkillLoopApplyError("TARGET_PATH_UNSAFE");
102
- }
103
- let source;
104
- try {
105
- source = decodeSkillLoopUtf8(readFileSync(path));
106
- } catch {
107
- throw new SkillLoopApplyError("TARGET_PATH_UNSAFE", "patch source is not strict UTF-8 text");
108
- }
109
- const matches = source.split(proposal.patch.oldString).length - 1;
110
- if (matches !== 1) throw new SkillLoopApplyError("PATCH_MATCH_NOT_UNIQUE");
111
- atomicWriteBytes(path, Buffer.from(source.replace(proposal.patch.oldString, proposal.patch.newString), "utf8"),
112
- SkillLoopApplyError, "TARGET_WRITE_FAILED");
113
- } else if (proposal.action === "add-reference") {
114
- for (const file of proposal.createSpec.files) writeStageFile(stage, file.file, file.content);
115
- }
116
- }
117
-
118
- function mapLedgerError(error) {
119
- if (!(error instanceof SkillLoopLedgerError)) return error;
120
- if (["SNAPSHOT_PATH_UNSAFE", "SNAPSHOT_TOO_LARGE"].includes(error.code)) {
121
- return new SkillLoopApplyError("TARGET_PATH_UNSAFE");
122
- }
123
- return new SkillLoopApplyError("LEDGER_WRITE_FAILED");
124
- }
125
-
126
- export function applyProposal(root, proposalId, {
127
- now = new Date().toISOString(),
128
- beforeCommit,
129
- afterIdentityCapture,
130
- beforeProposalCommit,
131
- afterCurrentDisplaced,
132
- afterLiveMutation,
133
- afterUsageCommit,
134
- afterLedgerCommit,
135
- afterProposalCommit,
136
- } = {}) {
137
- if (parseTimestamp(now) === null) throw new SkillLoopApplyError("APPLY_CLOCK_INVALID");
138
- return withSkillLoopLock(root, "transaction", () => {
139
- recoverReviewTransactionUnlocked(root);
140
- const mutationRecovery = recoverSkillMutationTransactionUnlocked(root);
141
- if (mutationRecovery.status === "finalized" && mutationRecovery.kind === "apply"
142
- && mutationRecovery.requestId === proposalId) {
143
- return {
144
- status: "applied",
145
- ledgerEntryId: mutationRecovery.ledgerEntry.id,
146
- path: mutationRecovery.path,
147
- archivePath: mutationRecovery.archivePath,
148
- recovered: true,
149
- };
150
- }
151
- const recoveredPreparedMutation = mutationRecovery.status === "rolled-back"
152
- && mutationRecovery.kind === "apply" && mutationRecovery.requestId === proposalId;
153
- let proposal = readProposalUnlocked(root, proposalId);
154
- if (proposal.status === "pending") {
155
- proposal = transitionProposalRecordUnlocked(root, proposal.id, "approved");
156
- } else if (proposal.status !== "approved") throw new SkillLoopApplyError("PROPOSAL_NOT_APPLICABLE");
157
-
158
- const { target, exists } = assertTargetIdentity(root, proposal);
159
- if (proposal.status === "approved") {
160
- let current;
161
- try {
162
- current = captureSkillSnapshot(root, proposal.targetRoot, proposal.targetSkill);
163
- } catch (error) {
164
- throw mapLedgerError(error);
165
- }
166
- const prior = readLedger(root).findLast((entry) => proposalReceiptMatches(proposal, entry, {
167
- operation: "apply",
168
- currentSnapshot: current,
169
- }));
170
- if (prior) {
171
- transitionProposalRecordUnlocked(root, proposal.id, "applied", prior.id);
172
- return { status: "applied", ledgerEntryId: prior.id, path: target, recovered: true };
173
- }
174
- }
175
- if (proposal.action === "create" ? exists : !exists) throw new SkillLoopApplyError("TARGET_NOT_AGENT_OWNED");
176
- if (exists) resolveAgentTarget(root, proposal.targetRoot, proposal.targetSkill, {
177
- allowMissing: false,
178
- requireOwned: true,
179
- ErrorType: SkillLoopApplyError,
180
- });
181
-
182
- let before;
183
- try {
184
- before = captureSkillSnapshot(root, proposal.targetRoot, proposal.targetSkill);
185
- } catch (error) {
186
- throw mapLedgerError(error);
187
- }
188
- const skills = ensureSkillRoot(root, true);
189
- const stage = join(skills, `.${proposal.targetSkill}.apply-${randomUUID()}`);
190
- const displaced = join(skills, `.${proposal.targetSkill}.apply-before-${randomUUID()}`);
191
- let archiveDestination = null;
192
- let movedCurrent = false;
193
- let installedStage = false;
194
- let archivedCurrent = false;
195
- let ledgerCommitted = false;
196
- let proposalCommitted = false;
197
- let usageCommitted = false;
198
- let usageUpdate = null;
199
- try {
200
- if (proposal.action !== "archive") buildStage(root, stage, before, proposal);
201
- beforeCommit?.();
202
- let liveBefore;
203
- try {
204
- liveBefore = captureSkillSnapshot(root, proposal.targetRoot, proposal.targetSkill);
205
- } catch (error) {
206
- throw mapLedgerError(error);
207
- }
208
- if (JSON.stringify(liveBefore) !== JSON.stringify(before)) throw new SkillLoopApplyError("APPLY_CAS_MISMATCH");
209
- const expectedIdentity = captureDirectoryIdentity(target, SkillLoopApplyError, "TARGET_PATH_UNSAFE", {
210
- allowMissing: !exists,
211
- });
212
- afterIdentityCapture?.();
213
- if (!directoryIdentityMatches(target, expectedIdentity)) throw new SkillLoopApplyError("APPLY_IDENTITY_MISMATCH");
214
- let after;
215
- try {
216
- after = proposal.action === "archive" ? [] : captureStagedSnapshot(root, stage);
217
- } catch (error) {
218
- throw mapLedgerError(error);
219
- }
220
- const stageIdentity = proposal.action === "archive"
221
- ? null
222
- : captureDirectoryIdentity(stage, SkillLoopApplyError, "TARGET_PATH_UNSAFE");
223
- if (proposal.action === "archive") {
224
- const archiveRoot = ensureSafeDirectoryChain(root, [".litclaude", "skill-archives", safeIsoPath(now)], {
225
- create: true,
226
- ErrorType: SkillLoopApplyError,
227
- code: "TARGET_PATH_UNSAFE",
228
- });
229
- archiveDestination = join(archiveRoot, proposal.targetSkill);
230
- if (existsSync(archiveDestination)) throw new SkillLoopApplyError("TARGET_IDENTITY_COLLISION");
231
- }
232
- const ledgerId = `ledger-${randomUUID()}`;
233
- const entry = {
234
- schema: "litfamily.skill-ledger/v1",
235
- id: ledgerId,
236
- createdAt: now,
237
- proposalId: proposal.id,
238
- actor: "user",
239
- operation: "apply",
240
- targetSkill: proposal.targetSkill,
241
- targetRoot: proposal.targetRoot,
242
- before,
243
- after,
244
- };
245
- const proposalAfter = { ...proposal, status: "applied", ledgerEntryId: ledgerId };
246
- usageUpdate = proposal.action === "patch"
247
- ? prepareUsageUpdate(root, proposal.targetSkill, "patch", { now, createdBy: "agent" })
248
- : null;
249
- beginSkillMutationTransactionUnlocked(root, {
250
- schema: "litclaude.skill-mutation-transaction/v1",
251
- id: ledgerId,
252
- kind: "apply",
253
- requestId: proposal.id,
254
- createdAt: now,
255
- targetRoot: proposal.targetRoot,
256
- targetSkill: proposal.targetSkill,
257
- action: proposal.action,
258
- stageName: stage.split(/[\\/]/u).at(-1),
259
- displacedName: displaced.split(/[\\/]/u).at(-1),
260
- archiveParts: proposal.action === "archive"
261
- ? ["skill-archives", safeIsoPath(now), proposal.targetSkill]
262
- : null,
263
- beforeIdentity: expectedIdentity,
264
- stageIdentity,
265
- before,
266
- after,
267
- ledgerEntry: entry,
268
- proposalBefore: proposal,
269
- proposalAfter,
270
- usageBefore: usageUpdate?.before ?? null,
271
- usageAfter: usageUpdate?.after ?? null,
272
- configBefore: null,
273
- configAfter: null,
274
- });
275
- if (expectedIdentity !== null) {
276
- renameSync(target, displaced);
277
- movedCurrent = true;
278
- if (!directoryIdentityMatches(displaced, expectedIdentity)) {
279
- renameSync(displaced, target);
280
- movedCurrent = false;
281
- throw new SkillLoopApplyError("APPLY_IDENTITY_MISMATCH");
282
- }
283
- } else if (!directoryIdentityMatches(target, null)) {
284
- throw new SkillLoopApplyError("APPLY_IDENTITY_MISMATCH");
285
- }
286
- afterCurrentDisplaced?.();
287
- if (proposal.action !== "archive") {
288
- renameSync(stage, target);
289
- installedStage = true;
290
- } else {
291
- renameSync(displaced, archiveDestination);
292
- movedCurrent = false;
293
- archivedCurrent = true;
294
- if (!directoryIdentityMatches(archiveDestination, expectedIdentity)) {
295
- renameSync(archiveDestination, displaced);
296
- archivedCurrent = false;
297
- movedCurrent = true;
298
- throw new SkillLoopApplyError("APPLY_IDENTITY_MISMATCH");
299
- }
300
- }
301
- afterLiveMutation?.();
302
- let liveAfter;
303
- try {
304
- liveAfter = captureSkillSnapshot(root, proposal.targetRoot, proposal.targetSkill);
305
- } catch (error) {
306
- throw mapLedgerError(error);
307
- }
308
- if (JSON.stringify(liveAfter) !== JSON.stringify(after)) throw new SkillLoopApplyError("APPLY_CAS_MISMATCH");
309
- if (usageUpdate) {
310
- writeUsageUnlocked(root, usageUpdate.after);
311
- usageCommitted = true;
312
- afterUsageCommit?.();
313
- }
314
- appendLedgerEntryUnlocked(root, entry);
315
- ledgerCommitted = true;
316
- afterLedgerCommit?.();
317
- beforeProposalCommit?.(proposalFilePath(root, proposal.id, false));
318
- transitionProposalRecordUnlocked(root, proposal.id, "applied", entry.id);
319
- proposalCommitted = true;
320
- afterProposalCommit?.();
321
- const finalized = recoverSkillMutationTransactionUnlocked(root);
322
- if (finalized.status !== "finalized") throw new SkillLoopApplyError("APPLY_STATE_UNKNOWN");
323
- return {
324
- status: "applied",
325
- ledgerEntryId: entry.id,
326
- path: target,
327
- archivePath: archiveDestination,
328
- ...(recoveredPreparedMutation ? { recovered: true } : {}),
329
- };
330
- } catch (error) {
331
- if (ledgerCommitted) {
332
- throw new SkillLoopApplyError(
333
- proposalCommitted ? "APPLY_COMMITTED_CLEANUP_PENDING" : "APPLY_COMMITTED_METADATA_PENDING",
334
- proposalCommitted
335
- ? "mutation, ledger, and proposal are committed; recoverable cleanup remains"
336
- : "mutation and ledger are committed; retry apply to repair proposal state",
337
- );
338
- }
339
- try {
340
- if (installedStage && existsSync(target)) rmSync(target, { recursive: true, force: true });
341
- if (movedCurrent && existsSync(displaced)) renameSync(displaced, target);
342
- if (archivedCurrent && archiveDestination && existsSync(archiveDestination)) renameSync(archiveDestination, target);
343
- if (usageCommitted && usageUpdate) writeUsageUnlocked(root, usageUpdate.before);
344
- } catch {
345
- throw new SkillLoopApplyError("APPLY_STATE_UNKNOWN");
346
- }
347
- if (error instanceof SkillLoopApplyError) throw error;
348
- throw mapLedgerError(error);
349
- } finally {
350
- rmSync(stage, { recursive: true, force: true });
351
- if (!movedCurrent) rmSync(displaced, { recursive: true, force: true });
352
- }
353
- }, SkillLoopApplyError, "APPLY_LOCK_FAILED");
354
- }
355
-
356
- export function rejectProposal(root, proposalId, reason = "", options = {}) {
357
- assertNoSecretStrings(reason, () => { throw new SkillLoopApplyError("PROPOSAL_SECRET_REFUSED"); });
358
- return withSkillLoopLock(root, "transaction", () => {
359
- recoverReviewTransactionUnlocked(root);
360
- recoverSkillMutationTransactionUnlocked(root);
361
- const proposal = readProposalRecord(root, proposalId);
362
- if (proposal.status === "pending") {
363
- const prior = readLedger(root).findLast((entry) => proposalReceiptMatches(proposal, entry, { operation: "reject" }));
364
- if (prior) {
365
- transitionProposalRecordUnlocked(root, proposal.id, "rejected");
366
- return { ...proposal, status: "rejected", ledgerEntryId: prior.id, recovered: true };
367
- }
368
- }
369
- if (proposal.status !== "pending") throw new SkillLoopApplyError("PROPOSAL_NOT_REJECTABLE");
370
- const ledgerEntry = appendLedgerEntryUnlocked(root, {
371
- schema: "litfamily.skill-ledger/v1",
372
- id: `ledger-${randomUUID()}`,
373
- createdAt: new Date().toISOString(),
374
- proposalId: proposal.id,
375
- actor: "user",
376
- operation: "reject",
377
- targetSkill: proposal.targetSkill,
378
- targetRoot: proposal.targetRoot,
379
- before: [],
380
- after: [],
381
- reason,
382
- });
383
- try {
384
- options.beforeProposalCommit?.(proposalFilePath(root, proposal.id, false));
385
- transitionProposalRecordUnlocked(root, proposal.id, "rejected");
386
- } catch {
387
- throw new SkillLoopApplyError("REJECT_COMMITTED_METADATA_PENDING");
388
- }
389
- return { ...proposal, status: "rejected", ledgerEntryId: ledgerEntry.id };
390
- }, SkillLoopApplyError, "APPLY_LOCK_FAILED");
391
- }
@@ -1,129 +0,0 @@
1
- import { lstatSync, opendirSync } from "node:fs";
2
- import { join } from "node:path";
3
-
4
- import { canonicalSkillIds } from "../canonical-skill-catalog.mjs";
5
- import { readRegularStable } from "../secure-path-read.mjs";
6
- import {
7
- decodeSkillLoopUtf8,
8
- ensureSafeDirectoryChain,
9
- portablePathIdentity,
10
- skillRootFor,
11
- skillRootsEquivalent,
12
- SKILL_ID,
13
- } from "./common.mjs";
14
-
15
- const protectedIds = new Set(canonicalSkillIds.map(portablePathIdentity));
16
-
17
- export function hasAgentOwnershipMarker(text) {
18
- if (typeof text !== "string") return false;
19
- const normalized = text.replace(/\r\n/gu, "\n");
20
- if (!normalized.startsWith("---\n")) return false;
21
- const allLines = normalized.split("\n");
22
- let close = -1;
23
- for (let index = 1; index < allLines.length; index += 1) {
24
- if (/^---[\t ]*$/u.test(allLines[index])) {
25
- close = index;
26
- break;
27
- }
28
- if (allLines[index].startsWith("---")) return false;
29
- }
30
- if (close < 0) return false;
31
- const lines = allLines.slice(1, close);
32
- let inMetadata = false;
33
- let metadataSeen = false;
34
- let markerSeen = false;
35
- const metadataKeys = new Set();
36
- for (const raw of lines) {
37
- if (/^metadata:\s*(?:#.*)?$/u.test(raw)) {
38
- if (metadataSeen) return false;
39
- metadataSeen = true;
40
- inMetadata = true;
41
- continue;
42
- }
43
- if (/^metadata\s*:/u.test(raw)) return false;
44
- if (!inMetadata) continue;
45
- if (raw.trim() === "" || /^ #/u.test(raw)) continue;
46
- if (!/^\s/u.test(raw)) {
47
- inMetadata = false;
48
- continue;
49
- }
50
- const child = /^ {2}([A-Za-z0-9_-]+):\s*(.*?)\s*$/u.exec(raw);
51
- if (!child || metadataKeys.has(child[1])) return false;
52
- metadataKeys.add(child[1]);
53
- if (child[1] === "litclaudeAgentGenerated") {
54
- const match = /^(["'])true\1\s*(?:#.*)?$/u.exec(child[2]);
55
- if (!match || markerSeen) return false;
56
- markerSeen = true;
57
- }
58
- }
59
- return markerSeen;
60
- }
61
-
62
- function readOwnedMarker(root, target, ErrorType, pathCode) {
63
- const path = join(target, "SKILL.md");
64
- const details = lstatSync(path, { throwIfNoEntry: false });
65
- if (!details?.isFile() || details.isSymbolicLink() || details.nlink !== 1) throw new ErrorType(pathCode);
66
- const stable = readRegularStable(root, path, undefined, { maxBytes: 1024 * 1024 });
67
- if (stable.failure || stable.stat.nlink !== 1) throw new ErrorType(pathCode);
68
- try {
69
- return hasAgentOwnershipMarker(decodeSkillLoopUtf8(stable.bytes));
70
- } catch {
71
- throw new ErrorType(pathCode, "SKILL.md is not strict UTF-8 text");
72
- }
73
- }
74
-
75
- export function resolveAgentTarget(root, targetRoot, targetSkill, {
76
- createRoot = false,
77
- allowMissing = false,
78
- requireOwned = true,
79
- ErrorType,
80
- notOwnedCode = "TARGET_NOT_AGENT_OWNED",
81
- pathCode = "TARGET_PATH_UNSAFE",
82
- collisionCode = "TARGET_IDENTITY_COLLISION",
83
- maxDirectoryEntries = null,
84
- workCapCode = pathCode,
85
- } = {}) {
86
- if (!ErrorType || !skillRootsEquivalent(root, targetRoot) || !SKILL_ID.test(targetSkill)
87
- || protectedIds.has(portablePathIdentity(targetSkill))) throw new ErrorType(notOwnedCode);
88
- const skills = ensureSafeDirectoryChain(root, [".claude", "skills"], {
89
- create: createRoot,
90
- ErrorType,
91
- code: pathCode,
92
- });
93
- const target = join(skillRootFor(root), targetSkill);
94
- if (skills === null) {
95
- if (allowMissing) return { skills: targetRoot, target, exists: false, owned: false };
96
- throw new ErrorType(notOwnedCode);
97
- }
98
- const requestedIdentity = portablePathIdentity(targetSkill);
99
- if (maxDirectoryEntries !== null
100
- && (!Number.isSafeInteger(maxDirectoryEntries) || maxDirectoryEntries < 1)) {
101
- throw new ErrorType(workCapCode);
102
- }
103
- let handle;
104
- try {
105
- handle = opendirSync(skills);
106
- let count = 0;
107
- let entry;
108
- while ((entry = handle.readSync()) !== null) {
109
- count += 1;
110
- if (maxDirectoryEntries !== null && count > maxDirectoryEntries) {
111
- throw new ErrorType(workCapCode);
112
- }
113
- if (portablePathIdentity(entry.name) === requestedIdentity && entry.name !== targetSkill) {
114
- throw new ErrorType(collisionCode);
115
- }
116
- }
117
- } finally {
118
- handle?.closeSync();
119
- }
120
- const details = lstatSync(target, { throwIfNoEntry: false });
121
- if (!details) {
122
- if (allowMissing) return { skills, target, exists: false, owned: false };
123
- throw new ErrorType(notOwnedCode);
124
- }
125
- if (!details.isDirectory() || details.isSymbolicLink()) throw new ErrorType(pathCode);
126
- const owned = readOwnedMarker(root, target, ErrorType, pathCode);
127
- if (requireOwned && !owned) throw new ErrorType(notOwnedCode);
128
- return { skills, target, exists: true, owned };
129
- }
@@ -1,67 +0,0 @@
1
- import { applyProposal, rejectProposal } from "./apply.mjs";
2
- import { readSkillLoopConfig } from "./config.mjs";
3
- import { runCurator } from "./curator.mjs";
4
- import { readLatestHookActivity } from "./hook-state.mjs";
5
- import { readLedger, rollbackLedgerEntry } from "./ledger.mjs";
6
- import { listProposals } from "./proposals.mjs";
7
- import { runReview } from "./review.mjs";
8
- import { readUsage } from "./usage.mjs";
9
-
10
- class SkillLoopCliError extends Error {
11
- constructor(code, message = code) {
12
- super(message);
13
- this.code = code;
14
- }
15
- }
16
-
17
- const usage = () => new SkillLoopCliError(
18
- "USAGE",
19
- "usage: skill-loop review|list|apply <id>|reject <id>|rollback <ledger-id>|curator status|curator run",
20
- );
21
- const requireId = (args) => {
22
- if (args.length !== 1 || typeof args[0] !== "string" || args[0] === "") throw usage();
23
- return args[0];
24
- };
25
-
26
- export function runSkillLoopCli(root, argv) {
27
- const [mode, ...args] = argv;
28
- if (mode === "list") {
29
- if (args.length !== 0) throw usage();
30
- return { schema: "litclaude.skill-proposal-list/v1", proposals: listProposals(root) };
31
- }
32
- if (mode === "review") {
33
- if (args.length !== 0) throw usage();
34
- return runReview(root);
35
- }
36
- if (mode === "apply") return applyProposal(root, requireId(args));
37
- if (mode === "reject") return rejectProposal(root, requireId(args));
38
- if (mode === "rollback") return rollbackLedgerEntry(root, requireId(args));
39
- if (mode === "curator") {
40
- if (args.length !== 1 || !["status", "run"].includes(args[0])) throw usage();
41
- if (args[0] === "run") {
42
- return runCurator(root, { lastActivityAt: readLatestHookActivity(root) });
43
- }
44
- const config = readSkillLoopConfig(root);
45
- const usageSidecar = readUsage(root);
46
- const proposals = listProposals(root);
47
- return {
48
- schema: "litclaude.skill-curator-status/v1",
49
- lastCuratorRunAt: config.lastCuratorRunAt ?? null,
50
- lastActivityAt: readLatestHookActivity(root),
51
- activeSkills: Object.values(usageSidecar.skills).filter(({ state }) => state === "active").length,
52
- staleSkills: Object.values(usageSidecar.skills).filter(({ state }) => state === "stale").length,
53
- archivedSkills: Object.values(usageSidecar.skills).filter(({ state }) => state === "archived").length,
54
- pendingProposals: proposals.filter(({ status }) => status === "pending").length,
55
- ledgerEntries: readLedger(root).length,
56
- };
57
- }
58
- throw usage();
59
- }
60
-
61
- export function cliErrorPayload(error) {
62
- return {
63
- status: "error",
64
- code: typeof error?.code === "string" ? error.code : "SKILL_LOOP_FAILED",
65
- message: typeof error?.message === "string" ? error.message : "skill-loop command failed",
66
- };
67
- }