@litfamily/litopencode 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README-Ko-KR.md +8 -7
- package/README.md +8 -7
- package/dist/activation-prompt-utils.d.ts +1 -1
- package/dist/activation-routing.d.ts +1 -1
- package/dist/activation-routing.js +1 -8
- package/dist/activation-workflow-prompts.d.ts +0 -1
- package/dist/activation-workflow-prompts.js +0 -8
- package/dist/activation.d.ts +1 -1
- package/dist/activation.js +1 -1
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +1 -31
- package/dist/cli/loop.js +0 -19
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +0 -2
- package/dist/cli/types.d.ts +0 -4
- package/dist/cli.js +0 -14
- package/dist/commands.d.ts +1 -10
- package/dist/commands.js +1 -11
- package/dist/features.d.ts +1 -22
- package/dist/features.js +0 -43
- package/dist/hooks.js +0 -13
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -5
- package/dist/skills.d.ts +1 -8
- package/dist/skills.js +0 -13
- package/docs/assets/cover-motion.webp +0 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/privacy.md +2 -2
- package/docs/reference.md +5 -48
- package/package.json +2 -1
- package/skills/frontend-ui-ux/references/complete-contract.md +1 -1
- package/skills/lit-plan/SKILL.md +1 -1
- package/skills/managed-skill-manifest.json +2 -9
- package/skills/visual-qa/references/complete-contract.md +1 -1
- package/tools/check-payload-substance.mjs +74 -27
- package/tools/payload-substance-parity.json +10 -25
- package/tools/version-manifests.json +2 -2
- package/dist/cli/skill-loop.d.ts +0 -3
- package/dist/cli/skill-loop.js +0 -649
- package/dist/skill-loop/apply.d.ts +0 -19
- package/dist/skill-loop/apply.js +0 -483
- package/dist/skill-loop/config.d.ts +0 -50
- package/dist/skill-loop/config.js +0 -215
- package/dist/skill-loop/curator.d.ts +0 -18
- package/dist/skill-loop/curator.js +0 -232
- package/dist/skill-loop/ledger.d.ts +0 -39
- package/dist/skill-loop/ledger.js +0 -344
- package/dist/skill-loop/proposals.d.ts +0 -48
- package/dist/skill-loop/proposals.js +0 -290
- package/dist/skill-loop/storage.d.ts +0 -72
- package/dist/skill-loop/storage.js +0 -817
- package/dist/skill-loop/time.d.ts +0 -2
- package/dist/skill-loop/time.js +0 -23
- package/dist/skill-loop/transaction.d.ts +0 -62
- package/dist/skill-loop/transaction.js +0 -836
- package/dist/skill-loop/usage.d.ts +0 -31
- package/dist/skill-loop/usage.js +0 -146
- package/dist/skill-observer.d.ts +0 -22
- package/dist/skill-observer.js +0 -1154
- package/skills/skill-observer/SKILL.md +0 -148
- package/skills/skill-observer/references/review-contract.md +0 -95
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { parseStrictJson } from "../strict-json.js";
|
|
3
|
-
import { defaultOpenCodeConfigRoot } from "../state.js";
|
|
4
|
-
import { assertSkillLoopLockCapability, canonicalPhysicalPath, pinPhysicalDirectory, pinPhysicalDirectoryIfPresent, readSafeRuntimeFile, removeSafeRuntimeFile, withSkillLoopMutationLock, writeSafeRuntimeFile } from "./storage.js";
|
|
5
|
-
import { isStrictRfc3339 } from "./time.js";
|
|
6
|
-
export const defaultSkillLoopConfig = Object.freeze({
|
|
7
|
-
schemaVersion: 1,
|
|
8
|
-
autoApply: false,
|
|
9
|
-
nudgeInterval: 20,
|
|
10
|
-
curatorIntervalDays: 7,
|
|
11
|
-
minIdleHours: 2,
|
|
12
|
-
staleAfterDays: 30,
|
|
13
|
-
archiveAfterDays: 90
|
|
14
|
-
});
|
|
15
|
-
function isRecord(value) {
|
|
16
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
17
|
-
}
|
|
18
|
-
function positiveInteger(value, label) {
|
|
19
|
-
if (!Number.isInteger(value) || Number(value) <= 0)
|
|
20
|
-
throw new Error(`SKILL_LOOP_CONFIG_INVALID: ${label}`);
|
|
21
|
-
return Number(value);
|
|
22
|
-
}
|
|
23
|
-
export function skillLoopPaths(projectRoot = process.cwd(), configRoot = defaultOpenCodeConfigRoot()) {
|
|
24
|
-
const project = path.resolve(projectRoot);
|
|
25
|
-
const runtimeRoot = path.join(project, ".litopencode");
|
|
26
|
-
const config = path.resolve(configRoot);
|
|
27
|
-
return {
|
|
28
|
-
projectRoot: project,
|
|
29
|
-
runtimeRoot,
|
|
30
|
-
configRoot: config,
|
|
31
|
-
skillsRoot: path.join(config, "skills"),
|
|
32
|
-
configFile: path.join(runtimeRoot, "skill-loop.json"),
|
|
33
|
-
proposalsDir: path.join(runtimeRoot, "skill-proposals"),
|
|
34
|
-
usageFile: path.join(runtimeRoot, "skill-usage.json"),
|
|
35
|
-
ledgerFile: path.join(runtimeRoot, "skill-ledger.jsonl"),
|
|
36
|
-
blobsDir: path.join(runtimeRoot, "skill-ledger-blobs"),
|
|
37
|
-
stateFile: path.join(runtimeRoot, "skill-loop-state.json"),
|
|
38
|
-
pendingReviewFile: path.join(runtimeRoot, "pending-review.json"),
|
|
39
|
-
backupsDir: path.join(runtimeRoot, "skill-backups"),
|
|
40
|
-
archiveDir: path.join(runtimeRoot, "skill-archive"),
|
|
41
|
-
transactionFile: path.join(runtimeRoot, "skill-loop-transaction.json"),
|
|
42
|
-
observationTransactionFile: path.join(runtimeRoot, "skill-loop-observation-transaction.json")
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
export async function resolveSkillLoopPaths(projectRoot, configRoot) {
|
|
46
|
-
const project = await pinPhysicalDirectory(projectRoot);
|
|
47
|
-
const config = await pinPhysicalDirectoryIfPresent(configRoot);
|
|
48
|
-
return skillLoopPaths(project.physical, config?.physical ?? await canonicalPhysicalPath(configRoot));
|
|
49
|
-
}
|
|
50
|
-
export async function readSkillLoopConfig(projectRoot) {
|
|
51
|
-
const file = skillLoopPaths(projectRoot).configFile;
|
|
52
|
-
const bytes = await readSafeRuntimeFile(projectRoot, file, { maxBytes: 64 * 1024 });
|
|
53
|
-
if (bytes === undefined)
|
|
54
|
-
return defaultSkillLoopConfig;
|
|
55
|
-
const raw = bytes.toString("utf8");
|
|
56
|
-
const parsed = parseStrictJson(raw);
|
|
57
|
-
if (!isRecord(parsed))
|
|
58
|
-
throw new Error("SKILL_LOOP_CONFIG_INVALID: object required");
|
|
59
|
-
const allowed = new Set(Object.keys(defaultSkillLoopConfig));
|
|
60
|
-
if (Object.keys(parsed).some((key) => !allowed.has(key)))
|
|
61
|
-
throw new Error("SKILL_LOOP_CONFIG_INVALID: unknown field");
|
|
62
|
-
if (parsed.schemaVersion !== 1 || parsed.autoApply !== false) {
|
|
63
|
-
throw new Error("SKILL_LOOP_CONFIG_INVALID: autoApply must be false");
|
|
64
|
-
}
|
|
65
|
-
const result = {
|
|
66
|
-
schemaVersion: 1,
|
|
67
|
-
autoApply: false,
|
|
68
|
-
nudgeInterval: positiveInteger(parsed.nudgeInterval, "nudgeInterval"),
|
|
69
|
-
curatorIntervalDays: positiveInteger(parsed.curatorIntervalDays, "curatorIntervalDays"),
|
|
70
|
-
minIdleHours: positiveInteger(parsed.minIdleHours, "minIdleHours"),
|
|
71
|
-
staleAfterDays: positiveInteger(parsed.staleAfterDays, "staleAfterDays"),
|
|
72
|
-
archiveAfterDays: positiveInteger(parsed.archiveAfterDays, "archiveAfterDays")
|
|
73
|
-
};
|
|
74
|
-
if (result.archiveAfterDays < result.staleAfterDays) {
|
|
75
|
-
throw new Error("SKILL_LOOP_CONFIG_INVALID: archive precedes stale");
|
|
76
|
-
}
|
|
77
|
-
return Object.freeze(result);
|
|
78
|
-
}
|
|
79
|
-
export async function writeJsonAtomically(projectRoot, file, value, capability) {
|
|
80
|
-
if (capability === undefined) {
|
|
81
|
-
return withSkillLoopMutationLock(projectRoot, (locked) => writeJsonAtomically(projectRoot, file, value, locked));
|
|
82
|
-
}
|
|
83
|
-
assertSkillLoopLockCapability(capability, projectRoot);
|
|
84
|
-
await writeSafeRuntimeFile(projectRoot, file, Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8"));
|
|
85
|
-
}
|
|
86
|
-
function parseIterationState(value) {
|
|
87
|
-
if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.sessions)) {
|
|
88
|
-
throw new Error("SKILL_LOOP_STATE_INVALID");
|
|
89
|
-
}
|
|
90
|
-
const sessions = value.sessions.map((entry) => {
|
|
91
|
-
if (!isRecord(entry) || typeof entry.sessionRef !== "string" || entry.sessionRef === "" ||
|
|
92
|
-
!Number.isInteger(entry.count) || Number(entry.count) < 0 || typeof entry.noticePending !== "boolean") {
|
|
93
|
-
throw new Error("SKILL_LOOP_STATE_INVALID");
|
|
94
|
-
}
|
|
95
|
-
return { sessionRef: entry.sessionRef, count: Number(entry.count), noticePending: entry.noticePending };
|
|
96
|
-
});
|
|
97
|
-
return { schemaVersion: 1, sessions };
|
|
98
|
-
}
|
|
99
|
-
async function readIterationState(projectRoot, file) {
|
|
100
|
-
const bytes = await readSafeRuntimeFile(projectRoot, file, { maxBytes: 128 * 1024 });
|
|
101
|
-
return bytes === undefined ? { schemaVersion: 1, sessions: [] } : parseIterationState(parseStrictJson(bytes.toString("utf8")));
|
|
102
|
-
}
|
|
103
|
-
function parsePendingReviewState(value) {
|
|
104
|
-
if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.sessions))
|
|
105
|
-
throw new Error("SKILL_LOOP_STATE_INVALID");
|
|
106
|
-
const sessions = value.sessions.map((entry) => {
|
|
107
|
-
if (!isRecord(entry) || Object.keys(entry).some((key) => key !== "sessionRef" && key !== "iterationCount" && key !== "queuedAt") ||
|
|
108
|
-
typeof entry.sessionRef !== "string" || entry.sessionRef === "" || !Number.isInteger(entry.iterationCount) || Number(entry.iterationCount) < 1 ||
|
|
109
|
-
!isStrictRfc3339(entry.queuedAt))
|
|
110
|
-
throw new Error("SKILL_LOOP_STATE_INVALID");
|
|
111
|
-
return { sessionRef: entry.sessionRef, iterationCount: Number(entry.iterationCount), queuedAt: entry.queuedAt };
|
|
112
|
-
});
|
|
113
|
-
return Object.freeze({ schemaVersion: 1, sessions: Object.freeze(sessions) });
|
|
114
|
-
}
|
|
115
|
-
async function readPendingReviewStateRaw(projectRoot) {
|
|
116
|
-
const paths = skillLoopPaths(projectRoot);
|
|
117
|
-
const bytes = await readSafeRuntimeFile(projectRoot, paths.pendingReviewFile, { maxBytes: 128 * 1024 });
|
|
118
|
-
return bytes === undefined ? { schemaVersion: 1, sessions: [] } : parsePendingReviewState(parseStrictJson(bytes.toString("utf8")));
|
|
119
|
-
}
|
|
120
|
-
function parseObservationTransaction(value) {
|
|
121
|
-
if (!isRecord(value) || Object.keys(value).length !== 3 || value.schema !== "litfamily.skill-observation-transaction/v1" ||
|
|
122
|
-
!Object.hasOwn(value, "state") || !Object.hasOwn(value, "pending"))
|
|
123
|
-
throw new Error("SKILL_LOOP_STATE_INVALID");
|
|
124
|
-
return Object.freeze({
|
|
125
|
-
schema: "litfamily.skill-observation-transaction/v1",
|
|
126
|
-
state: parseIterationState(value.state),
|
|
127
|
-
pending: parsePendingReviewState(value.pending)
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
async function recoverObservationTransaction(projectRoot, capability) {
|
|
131
|
-
assertSkillLoopLockCapability(capability, projectRoot);
|
|
132
|
-
const paths = skillLoopPaths(projectRoot);
|
|
133
|
-
const bytes = await readSafeRuntimeFile(projectRoot, paths.observationTransactionFile, { maxBytes: 384 * 1024 });
|
|
134
|
-
if (bytes === undefined)
|
|
135
|
-
return false;
|
|
136
|
-
const transaction = parseObservationTransaction(parseStrictJson(bytes.toString("utf8")));
|
|
137
|
-
await writeJsonAtomically(projectRoot, paths.stateFile, transaction.state, capability);
|
|
138
|
-
if (transaction.pending.sessions.length === 0)
|
|
139
|
-
await removeSafeRuntimeFile(projectRoot, paths.pendingReviewFile);
|
|
140
|
-
else
|
|
141
|
-
await writeJsonAtomically(projectRoot, paths.pendingReviewFile, transaction.pending, capability);
|
|
142
|
-
await removeSafeRuntimeFile(projectRoot, paths.observationTransactionFile);
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
export async function readPendingReviewState(projectRoot, options = {}) {
|
|
146
|
-
if (options.capability !== undefined) {
|
|
147
|
-
await recoverObservationTransaction(projectRoot, options.capability);
|
|
148
|
-
return readPendingReviewStateRaw(projectRoot);
|
|
149
|
-
}
|
|
150
|
-
const paths = skillLoopPaths(projectRoot);
|
|
151
|
-
if (await readSafeRuntimeFile(projectRoot, paths.observationTransactionFile, { maxBytes: 384 * 1024 }) === undefined) {
|
|
152
|
-
return readPendingReviewStateRaw(projectRoot);
|
|
153
|
-
}
|
|
154
|
-
return withSkillLoopMutationLock(projectRoot, async (capability) => {
|
|
155
|
-
await recoverObservationTransaction(projectRoot, capability);
|
|
156
|
-
return readPendingReviewStateRaw(projectRoot);
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
export async function consumePendingReview(projectRoot, reviewedSessionRefs) {
|
|
160
|
-
await withSkillLoopMutationLock(projectRoot, async (capability) => {
|
|
161
|
-
const paths = skillLoopPaths(projectRoot);
|
|
162
|
-
const current = await readPendingReviewState(projectRoot, { capability });
|
|
163
|
-
const reviewed = new Set(reviewedSessionRefs);
|
|
164
|
-
const sessions = current.sessions.filter((entry) => !reviewed.has(entry.sessionRef));
|
|
165
|
-
if (sessions.length === 0)
|
|
166
|
-
await removeSafeRuntimeFile(projectRoot, paths.pendingReviewFile);
|
|
167
|
-
else
|
|
168
|
-
await writeJsonAtomically(projectRoot, paths.pendingReviewFile, { schemaVersion: 1, sessions }, capability);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
export async function observeToolIteration(projectRoot, sessionRef, now = new Date().toISOString()) {
|
|
172
|
-
if (!isStrictRfc3339(now))
|
|
173
|
-
throw new Error("SKILL_LOOP_STATE_INVALID");
|
|
174
|
-
return withSkillLoopMutationLock(projectRoot, async (capability) => {
|
|
175
|
-
const paths = skillLoopPaths(projectRoot);
|
|
176
|
-
await recoverObservationTransaction(projectRoot, capability);
|
|
177
|
-
const config = await readSkillLoopConfig(projectRoot);
|
|
178
|
-
const state = await readIterationState(projectRoot, paths.stateFile);
|
|
179
|
-
const current = state.sessions.find((entry) => entry.sessionRef === sessionRef) ?? {
|
|
180
|
-
sessionRef,
|
|
181
|
-
count: 0,
|
|
182
|
-
noticePending: false
|
|
183
|
-
};
|
|
184
|
-
const surfaceNotice = current.noticePending;
|
|
185
|
-
const count = current.count + 1;
|
|
186
|
-
const reviewQueued = count % config.nudgeInterval === 0;
|
|
187
|
-
const next = {
|
|
188
|
-
sessionRef,
|
|
189
|
-
count,
|
|
190
|
-
noticePending: reviewQueued || (current.noticePending && !surfaceNotice)
|
|
191
|
-
};
|
|
192
|
-
const sessions = [next, ...state.sessions.filter((entry) => entry.sessionRef !== sessionRef)].slice(0, 64);
|
|
193
|
-
const pending = await readPendingReviewStateRaw(projectRoot);
|
|
194
|
-
const pendingSessions = reviewQueued
|
|
195
|
-
? [
|
|
196
|
-
{ sessionRef, iterationCount: count, queuedAt: now },
|
|
197
|
-
...pending.sessions.filter((entry) => entry.sessionRef !== sessionRef)
|
|
198
|
-
].slice(0, 64)
|
|
199
|
-
: pending.sessions;
|
|
200
|
-
const nextState = { schemaVersion: 1, sessions };
|
|
201
|
-
const nextPending = { schemaVersion: 1, sessions: pendingSessions };
|
|
202
|
-
await writeJsonAtomically(projectRoot, paths.observationTransactionFile, {
|
|
203
|
-
schema: "litfamily.skill-observation-transaction/v1",
|
|
204
|
-
state: nextState,
|
|
205
|
-
pending: nextPending
|
|
206
|
-
}, capability);
|
|
207
|
-
await writeJsonAtomically(projectRoot, paths.stateFile, nextState, capability);
|
|
208
|
-
if (nextPending.sessions.length === 0)
|
|
209
|
-
await removeSafeRuntimeFile(projectRoot, paths.pendingReviewFile);
|
|
210
|
-
else
|
|
211
|
-
await writeJsonAtomically(projectRoot, paths.pendingReviewFile, nextPending, capability);
|
|
212
|
-
await removeSafeRuntimeFile(projectRoot, paths.observationTransactionFile);
|
|
213
|
-
return { surfaceNotice, reviewQueued };
|
|
214
|
-
});
|
|
215
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { readSkillLoopConfig } from "./config.ts";
|
|
2
|
-
import { readUsageSidecar } from "./usage.ts";
|
|
3
|
-
type CuratorTransition = {
|
|
4
|
-
readonly skillId: string;
|
|
5
|
-
readonly from: "active" | "stale";
|
|
6
|
-
readonly to: "stale" | "archived";
|
|
7
|
-
readonly archivePath?: string;
|
|
8
|
-
readonly ledgerEntryId?: string;
|
|
9
|
-
};
|
|
10
|
-
export declare function curatorStatus(projectRoot: string): Promise<{
|
|
11
|
-
readonly config: Awaited<ReturnType<typeof readSkillLoopConfig>>;
|
|
12
|
-
readonly usage: Awaited<ReturnType<typeof readUsageSidecar>>;
|
|
13
|
-
}>;
|
|
14
|
-
export declare function runCurator(projectRoot: string, configRoot?: string, now?: string): Promise<{
|
|
15
|
-
readonly backupPath?: string;
|
|
16
|
-
readonly transitions: readonly CuratorTransition[];
|
|
17
|
-
}>;
|
|
18
|
-
export {};
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { assertPathComponentsSafe, inventoryTree, lstatIfPresent } from "../cli/native-skill-tree.js";
|
|
5
|
-
import { assertMutableSkillIdentity, hasAgentOwnedSkillMarker } from "./apply.js";
|
|
6
|
-
import { readSkillLoopConfig, skillLoopPaths } from "./config.js";
|
|
7
|
-
import { appendSkillLedgerBatch, assertProposalLedgerIntegrity, readSkillLedger, readSnapshotFile, snapshotSkill } from "./ledger.js";
|
|
8
|
-
import { listProposals } from "./proposals.js";
|
|
9
|
-
import { anchoredRenameInSkillsRoot, assertSafeRuntimeFileForWrite, assertSkillLoopRootPins, ensureSafeRuntimeDirectory, syncDirectory, withSkillLoopMutationLock, withSkillRootsMutationLock } from "./storage.js";
|
|
10
|
-
import { beginSkillTransaction, completeSkillTransaction, createSkillTransactionId, materializeTransactionSnapshot, recoverSkillTransaction } from "./transaction.js";
|
|
11
|
-
import { isStrictRfc3339 } from "./time.js";
|
|
12
|
-
import { readUsageSidecar, writeUsageSidecar } from "./usage.js";
|
|
13
|
-
function lastActivity(entry) {
|
|
14
|
-
return Math.max(...[entry.created_at, entry.last_used_at, entry.last_viewed_at, entry.last_patched_at]
|
|
15
|
-
.filter((value) => value !== null)
|
|
16
|
-
.map(Date.parse));
|
|
17
|
-
}
|
|
18
|
-
async function isAgentOwned(skillRoot) {
|
|
19
|
-
const stat = await lstatIfPresent(skillRoot);
|
|
20
|
-
if (stat === null || stat.isSymbolicLink() || !stat.isDirectory())
|
|
21
|
-
return false;
|
|
22
|
-
const inventory = await inventoryTree(skillRoot);
|
|
23
|
-
if (inventory.unsafe.length > 0)
|
|
24
|
-
throw new Error("TARGET_PATH_UNSAFE");
|
|
25
|
-
try {
|
|
26
|
-
const text = await fs.readFile(path.join(skillRoot, "SKILL.md"), "utf8");
|
|
27
|
-
return hasAgentOwnedSkillMarker(text);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
async function snapshotAgentOwnedSkills(projectRoot, skillsRoot) {
|
|
34
|
-
const skills = [];
|
|
35
|
-
let entries = [];
|
|
36
|
-
try {
|
|
37
|
-
entries = await fs.readdir(skillsRoot);
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
41
|
-
return skills;
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
for (const skillId of entries.sort()) {
|
|
45
|
-
const source = path.join(skillsRoot, skillId);
|
|
46
|
-
if (await isAgentOwned(source))
|
|
47
|
-
skills.push({ skillId, snapshot: await snapshotSkill(projectRoot, source) });
|
|
48
|
-
}
|
|
49
|
-
return Object.freeze(skills);
|
|
50
|
-
}
|
|
51
|
-
async function backupAgentOwnedSkills(projectRoot, transactionId, backup) {
|
|
52
|
-
const backupPath = backup.path;
|
|
53
|
-
await ensureSafeRuntimeDirectory(projectRoot, path.dirname(backupPath));
|
|
54
|
-
if (await lstatIfPresent(backupPath) !== null || await lstatIfPresent(backup.stagePath) !== null)
|
|
55
|
-
throw new Error("CURATOR_BACKUP_EXISTS");
|
|
56
|
-
await fs.mkdir(backup.stagePath, { mode: 0o700 });
|
|
57
|
-
const marker = path.join(backup.stagePath, ".litopencode-curator-backup.json");
|
|
58
|
-
await fs.writeFile(marker, `${JSON.stringify({
|
|
59
|
-
schema: "litfamily.curator-backup/v1",
|
|
60
|
-
transactionId,
|
|
61
|
-
markerToken: backup.markerToken,
|
|
62
|
-
backupPath
|
|
63
|
-
})}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
64
|
-
for (const skill of backup.skills) {
|
|
65
|
-
for (const item of skill.snapshot) {
|
|
66
|
-
const bytes = await readSnapshotFile(projectRoot, skill.snapshot, item.file);
|
|
67
|
-
if (bytes === undefined)
|
|
68
|
-
throw new Error("LEDGER_BLOB_CORRUPT");
|
|
69
|
-
const destination = path.join(backup.stagePath, skill.skillId, ...item.file.split("/"));
|
|
70
|
-
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
71
|
-
await fs.writeFile(destination, bytes, { mode: 0o600, flag: "wx" });
|
|
72
|
-
}
|
|
73
|
-
if (JSON.stringify(await snapshotSkill(projectRoot, path.join(backup.stagePath, skill.skillId))) !== JSON.stringify(skill.snapshot)) {
|
|
74
|
-
throw new Error("CURATOR_BACKUP_INVALID");
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
await syncDirectory(backup.stagePath);
|
|
78
|
-
await fs.rename(backup.stagePath, backupPath);
|
|
79
|
-
await syncDirectory(path.dirname(backupPath));
|
|
80
|
-
}
|
|
81
|
-
export async function curatorStatus(projectRoot) {
|
|
82
|
-
return { config: await readSkillLoopConfig(projectRoot), usage: await readUsageSidecar(projectRoot) };
|
|
83
|
-
}
|
|
84
|
-
export async function runCurator(projectRoot, configRoot, now = new Date().toISOString()) {
|
|
85
|
-
if (!isStrictRfc3339(now))
|
|
86
|
-
throw new Error("CURATOR_TIME_INVALID");
|
|
87
|
-
const requestedConfigRoot = skillLoopPaths(projectRoot, configRoot).configRoot;
|
|
88
|
-
return withSkillLoopMutationLock(projectRoot, async () => withSkillRootsMutationLock(projectRoot, requestedConfigRoot, (capability, roots) => runCuratorLocked(roots.project.physical, roots.config.physical, now, capability)));
|
|
89
|
-
}
|
|
90
|
-
async function runCuratorLocked(projectRoot, configRoot, now, capability) {
|
|
91
|
-
const nowMs = Date.parse(now);
|
|
92
|
-
const paths = skillLoopPaths(projectRoot, configRoot);
|
|
93
|
-
const request = { kind: "curator", requestId: "curator-run" };
|
|
94
|
-
await recoverSkillTransaction(projectRoot, paths.configRoot, request, assertMutableSkillIdentity, async (targetSkill) => (await readUsageSidecar(projectRoot, now)).skills[targetSkill]?.pinned === true, capability);
|
|
95
|
-
await assertSkillLoopRootPins(capability);
|
|
96
|
-
try {
|
|
97
|
-
await assertPathComponentsSafe(paths.configRoot, paths.skillsRoot, "Agent-owned skills root");
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
throw new Error("TARGET_PATH_UNSAFE");
|
|
101
|
-
}
|
|
102
|
-
const ledger = await readSkillLedger(projectRoot);
|
|
103
|
-
const proposals = await listProposals(projectRoot, configRoot);
|
|
104
|
-
assertProposalLedgerIntegrity(proposals, ledger);
|
|
105
|
-
await assertSafeRuntimeFileForWrite(projectRoot, paths.ledgerFile);
|
|
106
|
-
await assertSafeRuntimeFileForWrite(projectRoot, paths.usageFile);
|
|
107
|
-
const config = await readSkillLoopConfig(projectRoot);
|
|
108
|
-
const usage = await readUsageSidecar(projectRoot, now);
|
|
109
|
-
const candidates = [];
|
|
110
|
-
for (const [skillId, entry] of Object.entries(usage.skills)) {
|
|
111
|
-
if (entry.pinned || entry.state === "archived")
|
|
112
|
-
continue;
|
|
113
|
-
assertMutableSkillIdentity(skillId);
|
|
114
|
-
const idleDays = (nowMs - lastActivity(entry)) / 86_400_000;
|
|
115
|
-
if (entry.state === "stale" && idleDays >= config.archiveAfterDays)
|
|
116
|
-
candidates.push({ skillId, entry, from: "stale", to: "archived" });
|
|
117
|
-
else if (entry.state === "active" && idleDays >= config.staleAfterDays)
|
|
118
|
-
candidates.push({ skillId, entry, from: "active", to: "stale" });
|
|
119
|
-
}
|
|
120
|
-
if (candidates.length === 0)
|
|
121
|
-
return { transitions: [] };
|
|
122
|
-
const stamp = now.replaceAll(":", "-");
|
|
123
|
-
const backupPath = path.join(paths.backupsDir, stamp);
|
|
124
|
-
const nextSkills = { ...usage.skills };
|
|
125
|
-
const planned = [];
|
|
126
|
-
for (const candidate of candidates) {
|
|
127
|
-
const skillRoot = path.join(paths.skillsRoot, candidate.skillId);
|
|
128
|
-
if (!await isAgentOwned(skillRoot))
|
|
129
|
-
continue;
|
|
130
|
-
if (candidate.to === "stale") {
|
|
131
|
-
nextSkills[candidate.skillId] = { ...candidate.entry, state: "stale" };
|
|
132
|
-
planned.push({ kind: "stale", transition: { skillId: candidate.skillId, from: candidate.from, to: "stale" } });
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
const before = await snapshotSkill(projectRoot, skillRoot);
|
|
136
|
-
const archivePath = path.join(paths.archiveDir, stamp, candidate.skillId);
|
|
137
|
-
await ensureSafeRuntimeDirectory(projectRoot, path.dirname(archivePath));
|
|
138
|
-
if (await lstatIfPresent(archivePath) !== null)
|
|
139
|
-
throw new Error("TARGET_ALREADY_EXISTS");
|
|
140
|
-
nextSkills[candidate.skillId] = { ...candidate.entry, state: "archived", archived_at: now };
|
|
141
|
-
planned.push({
|
|
142
|
-
kind: "archive",
|
|
143
|
-
transition: { skillId: candidate.skillId, from: candidate.from, to: "archived", archivePath },
|
|
144
|
-
skillRoot,
|
|
145
|
-
archivePath,
|
|
146
|
-
before
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
const archives = planned.filter((entry) => entry.kind === "archive");
|
|
150
|
-
if (await lstatIfPresent(backupPath) !== null)
|
|
151
|
-
throw new Error("CURATOR_BACKUP_EXISTS");
|
|
152
|
-
await assertSkillLoopRootPins(capability);
|
|
153
|
-
const markerToken = randomUUID();
|
|
154
|
-
const backup = Object.freeze({
|
|
155
|
-
path: backupPath,
|
|
156
|
-
stagePath: path.join(paths.backupsDir, `.${stamp}.${markerToken}.stage`),
|
|
157
|
-
markerToken,
|
|
158
|
-
skills: await snapshotAgentOwnedSkills(projectRoot, paths.skillsRoot)
|
|
159
|
-
});
|
|
160
|
-
await assertSkillLoopRootPins(capability);
|
|
161
|
-
let ledgers = [];
|
|
162
|
-
const transactionId = createSkillTransactionId();
|
|
163
|
-
const archiveStagePath = (archive) => path.join(path.dirname(archive.archivePath), `.${archive.transition.skillId}.${transactionId}.stage`);
|
|
164
|
-
for (const archive of archives) {
|
|
165
|
-
if (await lstatIfPresent(archive.archivePath) !== null || await lstatIfPresent(archiveStagePath(archive)) !== null)
|
|
166
|
-
throw new Error("TARGET_ALREADY_EXISTS");
|
|
167
|
-
}
|
|
168
|
-
if (await lstatIfPresent(backupPath) !== null || await lstatIfPresent(backup.stagePath) !== null)
|
|
169
|
-
throw new Error("CURATOR_BACKUP_EXISTS");
|
|
170
|
-
await beginSkillTransaction(projectRoot, paths.configRoot, {
|
|
171
|
-
id: transactionId,
|
|
172
|
-
request,
|
|
173
|
-
proposalId: null,
|
|
174
|
-
action: "curator",
|
|
175
|
-
targets: archives.map((archive) => ({
|
|
176
|
-
targetSkill: archive.transition.skillId,
|
|
177
|
-
proposalId: null,
|
|
178
|
-
action: "curator-archive",
|
|
179
|
-
before: archive.before,
|
|
180
|
-
after: [],
|
|
181
|
-
archivePath: archive.archivePath,
|
|
182
|
-
archiveStagePath: archiveStagePath(archive)
|
|
183
|
-
})),
|
|
184
|
-
includeLedger: true,
|
|
185
|
-
includeUsage: true,
|
|
186
|
-
backup,
|
|
187
|
-
capability
|
|
188
|
-
});
|
|
189
|
-
try {
|
|
190
|
-
await assertSkillLoopRootPins(capability);
|
|
191
|
-
await backupAgentOwnedSkills(projectRoot, transactionId, backup);
|
|
192
|
-
await assertSkillLoopRootPins(capability);
|
|
193
|
-
for (const archive of archives) {
|
|
194
|
-
await assertSkillLoopRootPins(capability);
|
|
195
|
-
await materializeTransactionSnapshot(projectRoot, archiveStagePath(archive), archive.archivePath, archive.before);
|
|
196
|
-
await anchoredRenameInSkillsRoot(capability, archive.transition.skillId, `.${archive.transition.skillId}.${transactionId}.backup`);
|
|
197
|
-
await assertSkillLoopRootPins(capability);
|
|
198
|
-
}
|
|
199
|
-
await writeUsageSidecar(projectRoot, { ...usage, updatedAt: now, skills: nextSkills }, { capability });
|
|
200
|
-
ledgers = await appendSkillLedgerBatch(projectRoot, archives.map((archive) => ({
|
|
201
|
-
proposalId: null,
|
|
202
|
-
actor: "curator",
|
|
203
|
-
operation: "curator-transition",
|
|
204
|
-
targetSkill: archive.transition.skillId,
|
|
205
|
-
targetRoot: paths.skillsRoot,
|
|
206
|
-
before: archive.before,
|
|
207
|
-
after: [],
|
|
208
|
-
reason: "deterministic archive threshold"
|
|
209
|
-
})), now, { capability });
|
|
210
|
-
await completeSkillTransaction(projectRoot, paths.configRoot, request, capability);
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
try {
|
|
214
|
-
await recoverSkillTransaction(projectRoot, paths.configRoot, request, assertMutableSkillIdentity, async (targetSkill) => (await readUsageSidecar(projectRoot, now)).skills[targetSkill]?.pinned === true, capability);
|
|
215
|
-
}
|
|
216
|
-
catch (recoveryError) {
|
|
217
|
-
throw new Error("SKILL_TRANSACTION_STATE_UNKNOWN", { cause: { original: error, recoveryError } });
|
|
218
|
-
}
|
|
219
|
-
throw error;
|
|
220
|
-
}
|
|
221
|
-
let ledgerIndex = 0;
|
|
222
|
-
const transitions = planned.map((entry) => {
|
|
223
|
-
if (entry.kind === "stale")
|
|
224
|
-
return entry.transition;
|
|
225
|
-
const ledger = ledgers[ledgerIndex];
|
|
226
|
-
ledgerIndex += 1;
|
|
227
|
-
if (ledger === undefined)
|
|
228
|
-
throw new Error("SKILL_LEDGER_INVALID");
|
|
229
|
-
return { ...entry.transition, ledgerEntryId: ledger.id };
|
|
230
|
-
});
|
|
231
|
-
return { backupPath, transitions };
|
|
232
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type SkillProposal } from "./proposals.ts";
|
|
2
|
-
import { type SkillLoopLockCapability } from "./storage.ts";
|
|
3
|
-
export type SkillSnapshotItem = {
|
|
4
|
-
readonly file: string;
|
|
5
|
-
readonly blob: {
|
|
6
|
-
readonly algorithm: "sha256";
|
|
7
|
-
readonly digest: string;
|
|
8
|
-
readonly size: number;
|
|
9
|
-
};
|
|
10
|
-
};
|
|
11
|
-
export type SkillLedgerOperation = "apply" | "reject" | "rollback" | "curator-transition";
|
|
12
|
-
export type SkillLedgerEntry = {
|
|
13
|
-
readonly schema: "litfamily.skill-ledger/v1";
|
|
14
|
-
readonly id: string;
|
|
15
|
-
readonly createdAt: string;
|
|
16
|
-
readonly proposalId: string | null;
|
|
17
|
-
readonly actor: "user" | "curator";
|
|
18
|
-
readonly operation: SkillLedgerOperation;
|
|
19
|
-
readonly targetSkill: string;
|
|
20
|
-
readonly targetRoot: string;
|
|
21
|
-
readonly before: readonly SkillSnapshotItem[];
|
|
22
|
-
readonly after: readonly SkillSnapshotItem[];
|
|
23
|
-
readonly restoresLedgerEntryId?: string;
|
|
24
|
-
readonly reason?: string;
|
|
25
|
-
};
|
|
26
|
-
export declare function snapshotSkill(projectRoot: string, skillRoot: string): Promise<readonly SkillSnapshotItem[]>;
|
|
27
|
-
export declare function readSnapshotFile(projectRoot: string, snapshot: readonly SkillSnapshotItem[], relativeFile: string): Promise<Buffer | undefined>;
|
|
28
|
-
export declare function assertSnapshotMatches(projectRoot: string, skillRoot: string, expected: readonly SkillSnapshotItem[]): Promise<void>;
|
|
29
|
-
export declare function restoreSnapshot(projectRoot: string, skillRoot: string, snapshot: readonly SkillSnapshotItem[]): Promise<void>;
|
|
30
|
-
export declare function parseSkillSnapshot(value: unknown): readonly SkillSnapshotItem[];
|
|
31
|
-
export declare function readSkillLedger(projectRoot: string): Promise<readonly SkillLedgerEntry[]>;
|
|
32
|
-
export declare function assertProposalLedgerIntegrity(proposals: readonly SkillProposal[], entries: readonly SkillLedgerEntry[]): void;
|
|
33
|
-
export declare function appendSkillLedger(projectRoot: string, entry: Omit<SkillLedgerEntry, "schema" | "id" | "createdAt">, now?: string, options?: {
|
|
34
|
-
readonly capability?: SkillLoopLockCapability;
|
|
35
|
-
}): Promise<SkillLedgerEntry>;
|
|
36
|
-
export declare function appendSkillLedgerBatch(projectRoot: string, entries: readonly Omit<SkillLedgerEntry, "schema" | "id" | "createdAt">[], now?: string, options?: {
|
|
37
|
-
readonly capability?: SkillLoopLockCapability;
|
|
38
|
-
}): Promise<readonly SkillLedgerEntry[]>;
|
|
39
|
-
export declare function findSkillLedgerEntry(projectRoot: string, id: string): Promise<SkillLedgerEntry>;
|