@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.
- package/CHANGELOG.md +5 -5
- package/README.md +11 -38
- package/README_ko-KR.md +11 -37
- package/docs/assets/cover-motion.webp +0 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/privacy.md +12 -17
- package/package.json +3 -1
- package/plugins/litclaude/.claude-plugin/plugin.json +1 -1
- package/plugins/litclaude/bin/litclaude-hook.js +20 -57
- package/plugins/litclaude/bin/litclaude-mcp.js +1 -1
- package/plugins/litclaude/hooks/hooks.json +4 -4
- package/plugins/litclaude/lib/canonical-skill-catalog.mjs +0 -1
- package/plugins/litclaude/lib/canonical-skill-resources.mjs +5 -27
- package/plugins/litclaude/lib/lit-plan-persistence.mjs +2 -1
- package/plugins/litclaude/lib/litgoal/paths.mjs +10 -8
- package/plugins/litclaude/lib/project-state-root.mjs +22 -0
- package/plugins/litclaude/lib/rules/session-state.mjs +2 -1
- package/plugins/litclaude/lib/start-work-cli.mjs +3 -3
- package/plugins/litclaude/lib/wikify-knowledge-cli.mjs +3 -1
- package/plugins/litclaude/lib/wikify-knowledge.mjs +6 -5
- package/plugins/litclaude/scripts/scaffold-plan.mjs +9 -3
- package/plugins/litclaude/skills/lit-team/scripts/team.mjs +4 -3
- package/scripts/prepublish-test-gate.mjs +26 -0
- package/plugins/litclaude/lib/skill-loop/apply.mjs +0 -391
- package/plugins/litclaude/lib/skill-loop/authority.mjs +0 -129
- package/plugins/litclaude/lib/skill-loop/cli.mjs +0 -67
- package/plugins/litclaude/lib/skill-loop/common.mjs +0 -280
- package/plugins/litclaude/lib/skill-loop/config.mjs +0 -91
- package/plugins/litclaude/lib/skill-loop/curator.mjs +0 -670
- package/plugins/litclaude/lib/skill-loop/hook-runtime.mjs +0 -188
- package/plugins/litclaude/lib/skill-loop/hook-state.mjs +0 -484
- package/plugins/litclaude/lib/skill-loop/ledger.mjs +0 -834
- package/plugins/litclaude/lib/skill-loop/mutation-transaction.mjs +0 -529
- package/plugins/litclaude/lib/skill-loop/proposal-reader.mjs +0 -26
- package/plugins/litclaude/lib/skill-loop/proposal-record.mjs +0 -104
- package/plugins/litclaude/lib/skill-loop/proposal-store.mjs +0 -187
- package/plugins/litclaude/lib/skill-loop/proposals.mjs +0 -26
- package/plugins/litclaude/lib/skill-loop/review-runner.mjs +0 -13
- package/plugins/litclaude/lib/skill-loop/review-transaction.mjs +0 -358
- package/plugins/litclaude/lib/skill-loop/review.mjs +0 -223
- package/plugins/litclaude/lib/skill-loop/usage.mjs +0 -218
- package/plugins/litclaude/lib/skill-observer.mjs +0 -693
- package/plugins/litclaude/skills/skill-observer/SKILL.md +0 -155
- package/plugins/litclaude/skills/skill-observer/references/review-contract.md +0 -77
- 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
|
-
}
|