@litfamily/litcodex 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +18 -21
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +2 -38
  4. package/dist/config-migration/profile-files.d.ts +4 -0
  5. package/dist/config-migration/profile-files.js +22 -0
  6. package/dist/install/capability-preflight-error.js +5 -1
  7. package/dist/install/codex.d.ts +1 -0
  8. package/dist/install/generated-skill-payload-hashes.d.ts +1 -6
  9. package/dist/install/generated-skill-payload-hashes.js +1 -6
  10. package/dist/install/host-capabilities.d.ts +2 -0
  11. package/dist/install/host-capabilities.js +13 -1
  12. package/dist/install/index.js +3 -1
  13. package/dist/install/skill-catalog.d.ts +1 -1
  14. package/dist/install/skill-catalog.js +1 -6
  15. package/dist/install/skill-resource-hashes.d.ts +1 -1
  16. package/dist/install/skill-resource-hashes.js +1 -1
  17. package/dist/install/strict-config-probe.d.ts +2 -0
  18. package/dist/install/strict-config-probe.js +34 -5
  19. package/dist/install/uninstall.d.ts +2 -0
  20. package/dist/install/uninstall.js +34 -0
  21. package/marketplace/plugins/litcodex/.codex-plugin/plugin.json +1 -1
  22. package/marketplace/plugins/litcodex/components/auto-update/package.json +1 -1
  23. package/marketplace/plugins/litcodex/components/comment-checker/package.json +1 -1
  24. package/marketplace/plugins/litcodex/components/git-bash/package.json +1 -1
  25. package/marketplace/plugins/litcodex/components/lit-loop/dist/cli.js +3 -20
  26. package/marketplace/plugins/litcodex/components/lit-loop/dist/codex-hook.js +1 -8
  27. package/marketplace/plugins/litcodex/components/lit-loop/dist/hook-cli.d.ts +2 -1
  28. package/marketplace/plugins/litcodex/components/lit-loop/dist/hook-cli.js +4 -27
  29. package/marketplace/plugins/litcodex/components/lit-loop/dist/markers.d.ts +0 -2
  30. package/marketplace/plugins/litcodex/components/lit-loop/dist/markers.js +0 -2
  31. package/marketplace/plugins/litcodex/components/lit-loop/dist/modes.d.ts +1 -3
  32. package/marketplace/plugins/litcodex/components/lit-loop/dist/modes.js +1 -10
  33. package/marketplace/plugins/litcodex/components/lit-loop/package.json +1 -1
  34. package/marketplace/plugins/litcodex/components/lsp/package.json +1 -1
  35. package/marketplace/plugins/litcodex/components/rules/dist/static-injection.js +2 -2
  36. package/marketplace/plugins/litcodex/components/rules/package.json +1 -1
  37. package/marketplace/plugins/litcodex/components/start-work-continuation/package.json +1 -1
  38. package/marketplace/plugins/litcodex/components/telemetry/package.json +1 -1
  39. package/marketplace/plugins/litcodex/components/wikify-knowledge/package.json +1 -1
  40. package/marketplace/plugins/litcodex/skills/browser-drive/scripts/capability-probe.mjs +13 -5
  41. package/node_modules/@litcodex/lit-loop/README.md +1 -7
  42. package/node_modules/@litcodex/lit-loop/dist/cli.js +3 -20
  43. package/node_modules/@litcodex/lit-loop/dist/codex-hook.js +1 -8
  44. package/node_modules/@litcodex/lit-loop/dist/hook-cli.d.ts +2 -1
  45. package/node_modules/@litcodex/lit-loop/dist/hook-cli.js +4 -27
  46. package/node_modules/@litcodex/lit-loop/dist/markers.d.ts +0 -2
  47. package/node_modules/@litcodex/lit-loop/dist/markers.js +0 -2
  48. package/node_modules/@litcodex/lit-loop/dist/modes.d.ts +1 -3
  49. package/node_modules/@litcodex/lit-loop/dist/modes.js +1 -10
  50. package/node_modules/@litcodex/lit-loop/package.json +1 -1
  51. package/node_modules/@litcodex/lit-loop/skills/browser-drive/scripts/capability-probe.mjs +13 -5
  52. package/package.json +2 -2
  53. package/readme-assets/badge-version.svg +1 -1
  54. package/readme-assets/cover-motion.webp +0 -0
  55. package/marketplace/plugins/litcodex/components/lit-loop/directives/skill-observer.md +0 -105
  56. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/apply.d.ts +0 -16
  57. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/apply.js +0 -227
  58. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/config.d.ts +0 -13
  59. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/config.js +0 -12
  60. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/curator.d.ts +0 -14
  61. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/curator.js +0 -112
  62. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/hook.d.ts +0 -19
  63. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/hook.js +0 -435
  64. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/ownership.d.ts +0 -6
  65. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/ownership.js +0 -145
  66. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/paths.d.ts +0 -18
  67. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/paths.js +0 -43
  68. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/review.d.ts +0 -28
  69. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/review.js +0 -702
  70. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/store.d.ts +0 -32
  71. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/store.js +0 -667
  72. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/transaction.d.ts +0 -26
  73. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/transaction.js +0 -524
  74. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/types.d.ts +0 -81
  75. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/types.js +0 -246
  76. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/usage.d.ts +0 -6
  77. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop/usage.js +0 -119
  78. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop-cli.d.ts +0 -8
  79. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-loop-cli.js +0 -111
  80. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-cli.d.ts +0 -3
  81. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-cli.js +0 -116
  82. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-route.d.ts +0 -6
  83. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer-route.js +0 -12
  84. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer.d.ts +0 -18
  85. package/marketplace/plugins/litcodex/components/lit-loop/dist/skill-observer.js +0 -946
  86. package/marketplace/plugins/litcodex/skills/skill-observer/SKILL.md +0 -215
  87. package/marketplace/plugins/litcodex/skills/skill-observer/agents/openai.yaml +0 -6
  88. package/marketplace/plugins/litcodex/skills/skill-observer/references/review-contract.md +0 -77
  89. package/node_modules/@litcodex/lit-loop/directives/skill-observer.md +0 -105
  90. package/node_modules/@litcodex/lit-loop/dist/skill-loop/apply.d.ts +0 -16
  91. package/node_modules/@litcodex/lit-loop/dist/skill-loop/apply.js +0 -227
  92. package/node_modules/@litcodex/lit-loop/dist/skill-loop/config.d.ts +0 -13
  93. package/node_modules/@litcodex/lit-loop/dist/skill-loop/config.js +0 -12
  94. package/node_modules/@litcodex/lit-loop/dist/skill-loop/curator.d.ts +0 -14
  95. package/node_modules/@litcodex/lit-loop/dist/skill-loop/curator.js +0 -112
  96. package/node_modules/@litcodex/lit-loop/dist/skill-loop/hook.d.ts +0 -19
  97. package/node_modules/@litcodex/lit-loop/dist/skill-loop/hook.js +0 -435
  98. package/node_modules/@litcodex/lit-loop/dist/skill-loop/ownership.d.ts +0 -6
  99. package/node_modules/@litcodex/lit-loop/dist/skill-loop/ownership.js +0 -145
  100. package/node_modules/@litcodex/lit-loop/dist/skill-loop/paths.d.ts +0 -18
  101. package/node_modules/@litcodex/lit-loop/dist/skill-loop/paths.js +0 -43
  102. package/node_modules/@litcodex/lit-loop/dist/skill-loop/review.d.ts +0 -28
  103. package/node_modules/@litcodex/lit-loop/dist/skill-loop/review.js +0 -702
  104. package/node_modules/@litcodex/lit-loop/dist/skill-loop/store.d.ts +0 -32
  105. package/node_modules/@litcodex/lit-loop/dist/skill-loop/store.js +0 -667
  106. package/node_modules/@litcodex/lit-loop/dist/skill-loop/transaction.d.ts +0 -26
  107. package/node_modules/@litcodex/lit-loop/dist/skill-loop/transaction.js +0 -524
  108. package/node_modules/@litcodex/lit-loop/dist/skill-loop/types.d.ts +0 -81
  109. package/node_modules/@litcodex/lit-loop/dist/skill-loop/types.js +0 -246
  110. package/node_modules/@litcodex/lit-loop/dist/skill-loop/usage.d.ts +0 -6
  111. package/node_modules/@litcodex/lit-loop/dist/skill-loop/usage.js +0 -119
  112. package/node_modules/@litcodex/lit-loop/dist/skill-loop-cli.d.ts +0 -8
  113. package/node_modules/@litcodex/lit-loop/dist/skill-loop-cli.js +0 -111
  114. package/node_modules/@litcodex/lit-loop/dist/skill-observer-cli.d.ts +0 -3
  115. package/node_modules/@litcodex/lit-loop/dist/skill-observer-cli.js +0 -116
  116. package/node_modules/@litcodex/lit-loop/dist/skill-observer-route.d.ts +0 -6
  117. package/node_modules/@litcodex/lit-loop/dist/skill-observer-route.js +0 -12
  118. package/node_modules/@litcodex/lit-loop/dist/skill-observer.d.ts +0 -18
  119. package/node_modules/@litcodex/lit-loop/dist/skill-observer.js +0 -946
  120. package/node_modules/@litcodex/lit-loop/skills/skill-observer/SKILL.md +0 -215
  121. package/node_modules/@litcodex/lit-loop/skills/skill-observer/references/review-contract.md +0 -77
@@ -1,12 +0,0 @@
1
- export const DEFAULT_SKILL_LOOP_CONFIG = Object.freeze({
2
- autoApply: false,
3
- nudgeInterval: 10,
4
- curator: Object.freeze({
5
- intervalDays: 7,
6
- minimumIdleHours: 2,
7
- staleAfterDays: 30,
8
- archiveAfterDays: 90,
9
- delete: false,
10
- consolidation: false,
11
- }),
12
- });
@@ -1,14 +0,0 @@
1
- export interface CuratorOptions {
2
- readonly env?: NodeJS.ProcessEnv | undefined;
3
- readonly now?: () => Date;
4
- readonly lastActivityAt?: Date;
5
- readonly id?: () => string;
6
- }
7
- export interface CuratorResult {
8
- readonly stale: readonly string[];
9
- readonly archived: readonly string[];
10
- readonly skipped: readonly string[];
11
- readonly ledgerIds: readonly string[];
12
- }
13
- export declare function runCurator(workspaceRoot: string, options?: CuratorOptions): CuratorResult;
14
- export declare function assertCuratorNeverDeletes(): false;
@@ -1,112 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { DEFAULT_SKILL_LOOP_CONFIG } from "./config.js";
3
- import { snapshotIsAgentOwned } from "./ownership.js";
4
- import { skillLoopPaths } from "./paths.js";
5
- import { readUsage, snapshotSkill, withAgentMutationLock, withSkillLoopLock } from "./store.js";
6
- import { recoverMutationJournalWhileLocked, writeMutationJournal } from "./transaction.js";
7
- import { SkillLoopError } from "./types.js";
8
- import { validateUsageSidecar } from "./usage.js";
9
- const DAY = 24 * 60 * 60 * 1000;
10
- function mostRecent(entry) {
11
- return Math.max(...[entry.created_at, entry.last_used_at, entry.last_viewed_at, entry.last_patched_at, entry.archived_at]
12
- .filter((value) => value !== null)
13
- .map((value) => Date.parse(value)));
14
- }
15
- function nextSidecar(sidecar, skillId, entry, timestamp) {
16
- return {
17
- schema: "litfamily.skill-usage/v1",
18
- updatedAt: timestamp,
19
- skills: { ...sidecar.skills, [skillId]: entry },
20
- };
21
- }
22
- export function runCurator(workspaceRoot, options = {}) {
23
- const paths = skillLoopPaths(workspaceRoot, options.env);
24
- return withSkillLoopLock(paths, () => withAgentMutationLock(paths, () => {
25
- recoverMutationJournalWhileLocked(workspaceRoot, options.env, { operation: "curator-transition" });
26
- const rawUsage = readUsage(paths);
27
- if (rawUsage === null)
28
- return { stale: [], archived: [], skipped: [], ledgerIds: [] };
29
- let sidecar = validateUsageSidecar(rawUsage);
30
- const instant = (options.now ?? (() => new Date()))();
31
- const timestamp = instant.toISOString();
32
- const lastActivity = options.lastActivityAt?.getTime() ?? Date.parse(sidecar.updatedAt);
33
- if (instant.getTime() - lastActivity < DEFAULT_SKILL_LOOP_CONFIG.curator.minimumIdleHours * 60 * 60 * 1000) {
34
- return { stale: [], archived: [], skipped: Object.keys(sidecar.skills).sort(), ledgerIds: [] };
35
- }
36
- const stale = [];
37
- const archived = [];
38
- const skipped = [];
39
- const ledgerIds = [];
40
- for (const skillId of Object.keys(sidecar.skills).sort()) {
41
- const entry = sidecar.skills[skillId];
42
- if (entry === undefined || entry.pinned || entry.state === "archived") {
43
- skipped.push(skillId);
44
- continue;
45
- }
46
- const ageDays = (instant.getTime() - mostRecent(entry)) / DAY;
47
- const transition = entry.state === "active" && ageDays >= DEFAULT_SKILL_LOOP_CONFIG.curator.staleAfterDays
48
- ? "stale"
49
- : entry.state === "stale" && ageDays >= DEFAULT_SKILL_LOOP_CONFIG.curator.archiveAfterDays
50
- ? "archived"
51
- : null;
52
- if (transition === null) {
53
- skipped.push(skillId);
54
- continue;
55
- }
56
- const before = snapshotSkill(paths, skillId);
57
- if (before.length === 0 || !snapshotIsAgentOwned(paths, before)) {
58
- skipped.push(skillId);
59
- continue;
60
- }
61
- const runId = `${timestamp.replace(/[:.]/gu, "-")}-${randomUUID()}`;
62
- const after = transition === "archived" ? [] : before;
63
- const transitioned = {
64
- ...entry,
65
- state: transition,
66
- archived_at: transition === "archived" ? timestamp : null,
67
- };
68
- const updated = nextSidecar(sidecar, skillId, transitioned, timestamp);
69
- const ledger = {
70
- schema: "litfamily.skill-ledger/v1",
71
- id: (options.id ?? (() => `ledger-${randomUUID()}`))(),
72
- createdAt: timestamp,
73
- proposalId: null,
74
- actor: "curator",
75
- operation: "curator-transition",
76
- targetSkill: skillId,
77
- targetRoot: paths.agentRoot,
78
- before,
79
- after,
80
- reason: transition === "archived"
81
- ? "usage exceeded the archive threshold"
82
- : "usage exceeded the stale threshold",
83
- };
84
- writeMutationJournal(workspaceRoot, {
85
- schema: "litcodex.skill-transaction/v1",
86
- operation: "curator-transition",
87
- ledger,
88
- usageAfter: updated,
89
- copies: [
90
- { kind: "backup", relativePath: `${runId}/${skillId}` },
91
- ...(transition === "archived"
92
- ? [{ kind: "archive", relativePath: `${skillId}/${runId}` }]
93
- : []),
94
- ],
95
- }, options.env);
96
- recoverMutationJournalWhileLocked(workspaceRoot, options.env, { operation: "curator-transition" });
97
- sidecar = updated;
98
- ledgerIds.push(ledger.id);
99
- if (transition === "archived")
100
- archived.push(skillId);
101
- else
102
- stale.push(skillId);
103
- }
104
- return { stale, archived, skipped, ledgerIds };
105
- }));
106
- }
107
- export function assertCuratorNeverDeletes() {
108
- if (DEFAULT_SKILL_LOOP_CONFIG.curator.delete !== false) {
109
- throw new SkillLoopError("CURATOR_CONFIG_INVALID", "the curator delete setting must remain false");
110
- }
111
- return false;
112
- }
@@ -1,19 +0,0 @@
1
- interface PendingOptions {
2
- readonly now?: () => Date;
3
- readonly env?: NodeJS.ProcessEnv | undefined;
4
- readonly beforeRead?: ((path: string) => void) | undefined;
5
- /** Test-only race seam; production callers leave this unset. */
6
- readonly afterEnumerate?: (() => void) | undefined;
7
- /** Test-only race seam; production callers leave this unset. */
8
- readonly beforeWrite?: ((path: string) => void) | undefined;
9
- /** Test-only last-boundary race seam; production callers leave this unset. */
10
- readonly afterWriteVerify?: ((path: string) => void) | undefined;
11
- }
12
- export declare function markPendingReviews(workspaceRoot: string, input: unknown, options?: PendingOptions): {
13
- readonly marked: number;
14
- readonly skipped: number;
15
- };
16
- export declare function pendingReviewNotice(workspaceRoot: string, options?: PendingOptions): string | null;
17
- export declare function pendingReviewDirectories(workspaceRoot: string, options?: PendingOptions): string[];
18
- export declare function markReviewsComplete(workspaceRoot: string, timestamp: string): void;
19
- export {};
@@ -1,435 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { closeSync, constants, fstatSync, lstatSync, opendirSync, openSync, readSync, realpathSync } from "node:fs";
4
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
5
- import { normalizeSessionId } from "../state-paths.js";
6
- import { SkillLoopError } from "./types.js";
7
- const PENDING_SCHEMA = "litcodex.skill-review-pending/v1";
8
- const MAX_GOALS_BYTES = 64_000;
9
- const MAX_SESSION_DIRECTORY_ENTRIES = 64;
10
- const MAX_SESSION_DIRECTORIES = 16;
11
- const SOURCE_FILE_CAPS = new Map([
12
- ["brief.md", 64_000],
13
- ["goals.json", MAX_GOALS_BYTES],
14
- ["ledger.jsonl", 256_000],
15
- ]);
16
- const MAX_SESSION_SOURCE_BYTES = 384_000;
17
- const ANCHORED_PENDING_WRITE = `
18
- import {
19
- closeSync,
20
- constants,
21
- existsSync,
22
- fsyncSync,
23
- lstatSync,
24
- openSync,
25
- readFileSync,
26
- realpathSync,
27
- renameSync,
28
- unlinkSync,
29
- writeSync,
30
- } from "node:fs";
31
- import { randomBytes } from "node:crypto";
32
-
33
- const [expectedRealPath, expectedDev, expectedIno, expectedUid, expectedMode, expectedBirthtimeMs] = process.argv.slice(1);
34
- const matches = () => {
35
- const stat = lstatSync(".");
36
- return stat.isDirectory() &&
37
- !stat.isSymbolicLink() &&
38
- String(stat.dev) === expectedDev &&
39
- String(stat.ino) === expectedIno &&
40
- String(stat.uid) === expectedUid &&
41
- String(stat.mode) === expectedMode &&
42
- String(stat.birthtimeMs) === expectedBirthtimeMs &&
43
- (stat.mode & 0o022) === 0 &&
44
- realpathSync.native(".") === expectedRealPath;
45
- };
46
-
47
- if (!matches()) process.exit(41);
48
- const bytes = readFileSync(0);
49
- if (bytes.byteLength > 16_384) process.exit(42);
50
- const temporary = ".pending-review.json.litcodex-" + process.pid + "-" + randomBytes(8).toString("hex");
51
- let fileDescriptor;
52
- let directoryDescriptor;
53
- try {
54
- fileDescriptor = openSync(
55
- temporary,
56
- constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
57
- 0o600,
58
- );
59
- let offset = 0;
60
- while (offset < bytes.byteLength) {
61
- const written = writeSync(fileDescriptor, bytes, offset, bytes.byteLength - offset, null);
62
- if (written <= 0) throw new Error("short write");
63
- offset += written;
64
- }
65
- fsyncSync(fileDescriptor);
66
- closeSync(fileDescriptor);
67
- fileDescriptor = undefined;
68
- if (!matches()) throw new Error("directory authority changed before rename");
69
- renameSync(temporary, "pending-review.json");
70
- directoryDescriptor = openSync(".", constants.O_RDONLY | constants.O_NOFOLLOW);
71
- fsyncSync(directoryDescriptor);
72
- closeSync(directoryDescriptor);
73
- directoryDescriptor = undefined;
74
- if (!matches()) throw new Error("directory authority changed after rename");
75
- } finally {
76
- if (fileDescriptor !== undefined) closeSync(fileDescriptor);
77
- if (directoryDescriptor !== undefined) closeSync(directoryDescriptor);
78
- if (existsSync(temporary)) unlinkSync(temporary);
79
- }
80
- `;
81
- function record(value) {
82
- return typeof value === "object" && value !== null && !Array.isArray(value);
83
- }
84
- function sameStat(left, right) {
85
- return (left.dev === right.dev &&
86
- left.ino === right.ino &&
87
- left.uid === right.uid &&
88
- left.mode === right.mode &&
89
- left.nlink === right.nlink &&
90
- left.size === right.size &&
91
- left.mtimeMs === right.mtimeMs &&
92
- left.ctimeMs === right.ctimeMs &&
93
- left.birthtimeMs === right.birthtimeMs);
94
- }
95
- function captureDirectoryPin(path, expectedRealPath) {
96
- const stat = lstatSync(path);
97
- const uid = typeof process.getuid === "function" ? process.getuid() : stat.uid;
98
- if (stat.isSymbolicLink() ||
99
- !stat.isDirectory() ||
100
- (stat.uid !== uid && stat.uid !== 0) ||
101
- (stat.mode & 0o022) !== 0 ||
102
- realpathSync.native(path) !== expectedRealPath) {
103
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop directory authority is unsafe");
104
- }
105
- return {
106
- path,
107
- realPath: expectedRealPath,
108
- dev: stat.dev,
109
- ino: stat.ino,
110
- uid: stat.uid,
111
- mode: stat.mode,
112
- birthtimeMs: stat.birthtimeMs,
113
- };
114
- }
115
- function sameDirectoryPin(left, right) {
116
- return (left.path === right.path &&
117
- left.realPath === right.realPath &&
118
- left.dev === right.dev &&
119
- left.ino === right.ino &&
120
- left.uid === right.uid &&
121
- left.mode === right.mode &&
122
- left.birthtimeMs === right.birthtimeMs);
123
- }
124
- function verifyDirectoryAuthority(authority) {
125
- for (const expected of authority) {
126
- let current;
127
- try {
128
- current = captureDirectoryPin(expected.path, expected.realPath);
129
- }
130
- catch {
131
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop directory authority changed");
132
- }
133
- if (!sameDirectoryPin(current, expected)) {
134
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop directory authority changed");
135
- }
136
- }
137
- }
138
- function appendDirectoryAuthority(authority, path, expectedRealPath) {
139
- verifyDirectoryAuthority(authority);
140
- let pin;
141
- try {
142
- pin = captureDirectoryPin(path, expectedRealPath);
143
- }
144
- catch {
145
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop directory authority is unsafe");
146
- }
147
- verifyDirectoryAuthority(authority);
148
- return [...authority, pin];
149
- }
150
- function workspaceAuthority(workspaceRoot) {
151
- const workspace = resolve(workspaceRoot);
152
- let physical;
153
- try {
154
- physical = realpathSync.native(workspace);
155
- }
156
- catch {
157
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "the workspace authority is unavailable");
158
- }
159
- return [captureDirectoryPin(workspace, physical)];
160
- }
161
- function childAuthorityOrNull(authority, path, expectedRealPath) {
162
- verifyDirectoryAuthority(authority);
163
- try {
164
- lstatSync(path);
165
- }
166
- catch (error) {
167
- if (error.code === "ENOENT") {
168
- verifyDirectoryAuthority(authority);
169
- return null;
170
- }
171
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop directory could not be inspected safely");
172
- }
173
- return appendDirectoryAuthority(authority, path, expectedRealPath);
174
- }
175
- function pinnedBytes(path, cap, authority, beforeRead) {
176
- verifyDirectoryAuthority(authority);
177
- let before;
178
- try {
179
- before = lstatSync(path);
180
- }
181
- catch (error) {
182
- if (error.code === "ENOENT") {
183
- verifyDirectoryAuthority(authority);
184
- return null;
185
- }
186
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop source could not be inspected safely");
187
- }
188
- if (before.size > cap)
189
- throw new SkillLoopError("REVIEW_SESSION_LIMIT", "a lit-loop source exceeded its byte cap");
190
- const uid = typeof process.getuid === "function" ? process.getuid() : before.uid;
191
- if (before.isSymbolicLink() ||
192
- !before.isFile() ||
193
- before.nlink !== 1 ||
194
- (before.uid !== uid && before.uid !== 0) ||
195
- dirname(path) !== authority.at(-1)?.path) {
196
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop source path is unsafe");
197
- }
198
- beforeRead?.(path);
199
- verifyDirectoryAuthority(authority);
200
- let descriptor;
201
- try {
202
- descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
203
- const opened = fstatSync(descriptor);
204
- const current = lstatSync(path);
205
- if (!opened.isFile() ||
206
- opened.isSymbolicLink() ||
207
- opened.nlink !== 1 ||
208
- !sameStat(opened, before) ||
209
- !sameStat(current, before)) {
210
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop source changed before descriptor read");
211
- }
212
- const bytes = Buffer.alloc(opened.size);
213
- let total = 0;
214
- while (total < opened.size) {
215
- const count = readSync(descriptor, bytes, total, opened.size - total, total);
216
- if (count === 0)
217
- break;
218
- total += count;
219
- }
220
- const after = fstatSync(descriptor);
221
- const pathAfter = lstatSync(path);
222
- verifyDirectoryAuthority(authority);
223
- if (!sameStat(after, before) || !sameStat(pathAfter, before)) {
224
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop source changed during descriptor read");
225
- }
226
- return bytes.subarray(0, total);
227
- }
228
- catch (error) {
229
- if (error instanceof SkillLoopError)
230
- throw error;
231
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop source could not be opened without following links");
232
- }
233
- finally {
234
- if (descriptor !== undefined)
235
- closeSync(descriptor);
236
- }
237
- }
238
- function validatedSession(session, loopRoot, beforeRead) {
239
- const directory = session.path;
240
- const goalsPath = join(directory, "goals.json");
241
- const bytes = pinnedBytes(goalsPath, MAX_GOALS_BYTES, session.authority, beforeRead);
242
- if (bytes === null)
243
- return null;
244
- let goals;
245
- try {
246
- goals = JSON.parse(bytes.toString("utf8"));
247
- }
248
- catch {
249
- return null;
250
- }
251
- if (!record(goals) || goals["version"] !== 1 || !Array.isArray(goals["goals"]) || goals["goals"].length === 0) {
252
- return null;
253
- }
254
- if (goals["goals"].some((goal) => !record(goal) ||
255
- typeof goal["id"] !== "string" ||
256
- !["pending", "in_progress", "complete", "failed", "blocked"].includes(goal["status"]))) {
257
- return null;
258
- }
259
- if (directory === loopRoot)
260
- return "unscoped";
261
- const directoryId = basename(directory);
262
- const declared = typeof goals["sessionId"] === "string" ? normalizeSessionId(goals["sessionId"]) : directoryId;
263
- return declared === directoryId ? directoryId : null;
264
- }
265
- function sessionDirectories(workspaceRoot, beforeRead, afterEnumerate) {
266
- const workspace = resolve(workspaceRoot);
267
- const stateRoot = join(workspace, ".litcodex");
268
- const loopRoot = join(stateRoot, "lit-loop");
269
- const baseAuthority = workspaceAuthority(workspace);
270
- const physicalWorkspace = baseAuthority[0]?.realPath;
271
- const stateAuthority = childAuthorityOrNull(baseAuthority, stateRoot, join(physicalWorkspace, ".litcodex"));
272
- if (stateAuthority === null)
273
- return [];
274
- const loopAuthority = childAuthorityOrNull(stateAuthority, loopRoot, join(physicalWorkspace, ".litcodex", "lit-loop"));
275
- if (loopAuthority === null)
276
- return [];
277
- const directories = [{ path: loopRoot, authority: loopAuthority }];
278
- const handle = opendirSync(loopRoot);
279
- let visited = 0;
280
- try {
281
- for (let entry = handle.readSync(); entry !== null; entry = handle.readSync()) {
282
- visited += 1;
283
- if (visited > MAX_SESSION_DIRECTORY_ENTRIES) {
284
- throw new SkillLoopError("REVIEW_SESSION_LIMIT", "the lit-loop session directory cap was exceeded");
285
- }
286
- if (!entry.isDirectory() || entry.isSymbolicLink())
287
- continue;
288
- if (directories.length >= MAX_SESSION_DIRECTORIES) {
289
- throw new SkillLoopError("REVIEW_SESSION_LIMIT", "the lit-loop session count cap was exceeded");
290
- }
291
- const path = join(loopRoot, entry.name);
292
- const relation = relative(loopRoot, path);
293
- if (relation.length === 0 || relation.includes(sep) || relation === ".." || relation.startsWith(`..${sep}`)) {
294
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "a lit-loop session path escaped its root");
295
- }
296
- const authority = appendDirectoryAuthority(loopAuthority, path, join(loopAuthority.at(-1)?.realPath, relation));
297
- directories.push({ path, authority });
298
- }
299
- }
300
- finally {
301
- handle.closeSync();
302
- }
303
- for (const directory of directories)
304
- verifyDirectoryAuthority(directory.authority);
305
- afterEnumerate?.();
306
- for (const directory of directories)
307
- verifyDirectoryAuthority(directory.authority);
308
- return directories.filter((directory) => validatedSession(directory, loopRoot, beforeRead) !== null);
309
- }
310
- function readPending(session, beforeRead) {
311
- const path = join(session.path, "pending-review.json");
312
- const bytes = pinnedBytes(path, 16_384, session.authority, beforeRead);
313
- if (bytes === null)
314
- return null;
315
- try {
316
- const value = JSON.parse(bytes.toString("utf8"));
317
- if (value.schema !== PENDING_SCHEMA ||
318
- typeof value.sessionRef !== "string" ||
319
- typeof value.createdAt !== "string" ||
320
- (value.status !== "pending" && value.status !== "reviewed")) {
321
- return null;
322
- }
323
- return value;
324
- }
325
- catch {
326
- return null;
327
- }
328
- }
329
- function writePending(session, pending, beforeWrite, afterWriteVerify) {
330
- const path = join(session.path, "pending-review.json");
331
- verifyDirectoryAuthority(session.authority);
332
- beforeWrite?.(path);
333
- verifyDirectoryAuthority(session.authority);
334
- afterWriteVerify?.(path);
335
- const pin = session.authority.at(-1);
336
- if (pin === undefined)
337
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "the pending write authority is missing");
338
- const content = `${JSON.stringify(pending, null, 2)}\n`;
339
- if (Buffer.byteLength(content) > 16_384) {
340
- throw new SkillLoopError("REVIEW_SESSION_LIMIT", "the pending review state exceeded its byte cap");
341
- }
342
- const result = spawnSync(process.execPath, [
343
- "--input-type=module",
344
- "--eval",
345
- ANCHORED_PENDING_WRITE,
346
- pin.realPath,
347
- String(pin.dev),
348
- String(pin.ino),
349
- String(pin.uid),
350
- String(pin.mode),
351
- String(pin.birthtimeMs),
352
- ], {
353
- cwd: session.path,
354
- env: {},
355
- input: content,
356
- encoding: "utf8",
357
- stdio: ["pipe", "pipe", "pipe"],
358
- maxBuffer: 1_024,
359
- timeout: 5_000,
360
- });
361
- if (result.error !== undefined || result.status !== 0) {
362
- throw new SkillLoopError("REVIEW_SESSION_UNSAFE", "the anchored pending-state write was refused");
363
- }
364
- verifyDirectoryAuthority(session.authority);
365
- }
366
- function sourceDigest(session, beforeRead) {
367
- const hash = createHash("sha256");
368
- let aggregate = 0;
369
- for (const [name, cap] of SOURCE_FILE_CAPS) {
370
- const path = join(session.path, name);
371
- const bytes = pinnedBytes(path, cap, session.authority, beforeRead);
372
- if (bytes === null)
373
- continue;
374
- if (aggregate + bytes.byteLength > MAX_SESSION_SOURCE_BYTES) {
375
- throw new SkillLoopError("REVIEW_SESSION_LIMIT", "a lit-loop session source exceeded its byte cap");
376
- }
377
- aggregate += bytes.byteLength;
378
- hash.update(name);
379
- hash.update(bytes);
380
- }
381
- return hash.digest("hex");
382
- }
383
- export function markPendingReviews(workspaceRoot, input, options = {}) {
384
- void input;
385
- if (options.env?.["LITFAMILY_SKILL_REVIEW"] === "1")
386
- return { marked: 0, skipped: 0 };
387
- let marked = 0;
388
- let skipped = 0;
389
- const loopRoot = join(resolve(workspaceRoot), ".litcodex", "lit-loop");
390
- for (const session of sessionDirectories(workspaceRoot, options.beforeRead, options.afterEnumerate)) {
391
- const existing = readPending(session, options.beforeRead);
392
- const digest = sourceDigest(session, options.beforeRead);
393
- if (existing !== null && (existing.status === "pending" || existing.sourceDigest === digest)) {
394
- skipped += 1;
395
- continue;
396
- }
397
- const sessionRef = validatedSession(session, loopRoot, options.beforeRead);
398
- if (sessionRef === null) {
399
- skipped += 1;
400
- continue;
401
- }
402
- writePending(session, {
403
- schema: PENDING_SCHEMA,
404
- sessionRef,
405
- createdAt: (options.now ?? (() => new Date()))().toISOString(),
406
- status: "pending",
407
- sourceDigest: digest,
408
- }, options.beforeWrite, options.afterWriteVerify);
409
- marked += 1;
410
- }
411
- return { marked, skipped };
412
- }
413
- export function pendingReviewNotice(workspaceRoot, options = {}) {
414
- const pending = sessionDirectories(workspaceRoot, options.beforeRead, options.afterEnumerate)
415
- .map((session) => ({ session, value: readPending(session, options.beforeRead) }))
416
- .filter((item) => item.value !== null && item.value.status === "pending" && item.value.surfacedAt === undefined);
417
- if (pending.length === 0)
418
- return null;
419
- const timestamp = (options.now ?? (() => new Date()))().toISOString();
420
- for (const item of pending)
421
- writePending(item.session, { ...item.value, surfacedAt: timestamp }, options.beforeWrite, options.afterWriteVerify);
422
- return `${pending.length} skill review${pending.length === 1 ? "" : "s"} pending — run \`litcodex skill-loop review\`.`;
423
- }
424
- export function pendingReviewDirectories(workspaceRoot, options = {}) {
425
- return sessionDirectories(workspaceRoot, options.beforeRead, options.afterEnumerate)
426
- .filter((session) => readPending(session, options.beforeRead)?.status === "pending")
427
- .map((session) => session.path);
428
- }
429
- export function markReviewsComplete(workspaceRoot, timestamp) {
430
- for (const session of sessionDirectories(workspaceRoot)) {
431
- const pending = readPending(session);
432
- if (pending?.status === "pending")
433
- writePending(session, { ...pending, status: "reviewed", reviewedAt: timestamp });
434
- }
435
- }
@@ -1,6 +0,0 @@
1
- import type { skillLoopPaths } from "./paths.js";
2
- import type { SkillSnapshotItem } from "./types.js";
3
- export declare function markerPresent(content: string): boolean;
4
- export declare function stampMarker(content: string, skillId: string): string;
5
- export declare function decodeSkillItem(paths: ReturnType<typeof skillLoopPaths>, item: SkillSnapshotItem): string;
6
- export declare function snapshotIsAgentOwned(paths: ReturnType<typeof skillLoopPaths>, snapshot: readonly SkillSnapshotItem[]): boolean;