@prodero/curator 0.0.0-stage → 0.2.0
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/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/SECURITY.md +7 -0
- package/bin/prodero +7 -0
- package/dist/cli.js +266 -0
- package/dist/context-instructions.js +2 -0
- package/dist/contracts.js +51 -0
- package/dist/conversation-presentation.js +297 -0
- package/dist/core/agent-instructions.js +52 -0
- package/dist/core/applicability-label.js +11 -0
- package/dist/core/applicability.js +143 -0
- package/dist/core/bootstrap.js +129 -0
- package/dist/core/consolidation.js +315 -0
- package/dist/core/constants.js +21 -0
- package/dist/core/context-allocation.js +54 -0
- package/dist/core/context-snapshot.js +12 -0
- package/dist/core/context.js +489 -0
- package/dist/core/curation-clauses.js +68 -0
- package/dist/core/curation-evidence.js +98 -0
- package/dist/core/curation-graphical.js +117 -0
- package/dist/core/curation-question.js +28 -0
- package/dist/core/curation-review-presentation.js +4 -0
- package/dist/core/curation-review-state.js +62 -0
- package/dist/core/curation-review.js +573 -0
- package/dist/core/curation-revision.js +106 -0
- package/dist/core/curation-semantic-contract.js +242 -0
- package/dist/core/curation-semantic-jobs.js +340 -0
- package/dist/core/diagnostics.js +17 -0
- package/dist/core/format.js +114 -0
- package/dist/core/ids.js +6 -0
- package/dist/core/imports.js +169 -0
- package/dist/core/index.js +93 -0
- package/dist/core/instruction-review.js +86 -0
- package/dist/core/integrity-files.js +174 -0
- package/dist/core/intelligence.js +95 -0
- package/dist/core/knowledge-analysis.js +46 -0
- package/dist/core/knowledge-authority.js +143 -0
- package/dist/core/knowledge-curation.js +316 -0
- package/dist/core/knowledge-engine.js +475 -0
- package/dist/core/knowledge-graph.js +192 -0
- package/dist/core/knowledge-lifecycle.js +212 -0
- package/dist/core/knowledge-management.js +113 -0
- package/dist/core/knowledge-migration.js +84 -0
- package/dist/core/knowledge-policy.js +38 -0
- package/dist/core/knowledge-provenance.js +90 -0
- package/dist/core/knowledge-write-policy.js +85 -0
- package/dist/core/language.js +14 -0
- package/dist/core/local-preferences.js +76 -0
- package/dist/core/matching.js +6 -0
- package/dist/core/memory-index.js +241 -0
- package/dist/core/memory-intelligence.js +288 -0
- package/dist/core/memory-link-repair.js +70 -0
- package/dist/core/memory-operations.js +122 -0
- package/dist/core/memory-pressure.js +212 -0
- package/dist/core/memory-sync-curator.js +82 -0
- package/dist/core/memory-sync.js +558 -0
- package/dist/core/memory-transformation-bundle.js +163 -0
- package/dist/core/memory-transformation-proposal.js +135 -0
- package/dist/core/memory-transformation.js +108 -0
- package/dist/core/memory.js +186 -0
- package/dist/core/migrations.js +569 -0
- package/dist/core/npm-update.js +289 -0
- package/dist/core/path-scope.js +27 -0
- package/dist/core/plugin-files.js +116 -0
- package/dist/core/project-root.js +39 -0
- package/dist/core/project.js +634 -0
- package/dist/core/promotion.js +279 -0
- package/dist/core/reconciliation.js +61 -0
- package/dist/core/secrets.js +67 -0
- package/dist/core/semantic-safety.js +33 -0
- package/dist/core/session-index.js +116 -0
- package/dist/core/session-review.js +150 -0
- package/dist/core/sessions.js +289 -0
- package/dist/core/settings.js +94 -0
- package/dist/core/skill-curation.js +239 -0
- package/dist/core/skill-polish.js +41 -0
- package/dist/core/skills.js +387 -0
- package/dist/core/storage.js +462 -0
- package/dist/core/sync-transaction.js +290 -0
- package/dist/core/telemetry.js +64 -0
- package/dist/core/templates.js +53 -0
- package/dist/core/types.js +2 -0
- package/dist/core/update.js +326 -0
- package/dist/curation-review-adapter.js +92 -0
- package/dist/diagnostic-boundary.js +55 -0
- package/dist/filesystem/permissions.js +39 -0
- package/dist/hosts/adapters.js +119 -0
- package/dist/hosts/files.js +133 -0
- package/dist/hosts/manager.js +150 -0
- package/dist/hosts/public-entry.js +47 -0
- package/dist/hosts/runtime-command.js +66 -0
- package/dist/hosts/runtime.js +139 -0
- package/dist/hosts/wizard.js +114 -0
- package/dist/knowledge-table.js +38 -0
- package/dist/mcp-development.js +69 -0
- package/dist/mcp-workspace.js +141 -0
- package/dist/memory-presentation.js +182 -0
- package/dist/memory-sync-operation.js +88 -0
- package/dist/plugins/api.js +2 -0
- package/dist/plugins/context.js +20 -0
- package/dist/plugins/mcp.js +72 -0
- package/dist/plugins/registry.js +106 -0
- package/dist/presentation-cards.js +32 -0
- package/dist/presentation-server.js +94 -0
- package/dist/presentation-trace.js +174 -0
- package/dist/presentation.js +443 -0
- package/dist/providers/mcp-sampling.js +45 -0
- package/dist/server.js +664 -0
- package/dist/settings-definition.js +22 -0
- package/dist/skill-instructions.js +2 -0
- package/dist/status-presentation.js +20 -0
- package/dist/text-layout.js +278 -0
- package/dist/update-command.js +53 -0
- package/dist/update-instructions.js +4 -0
- package/dist-ui/index.html +216 -0
- package/docs/APPLICABILITY.md +151 -0
- package/docs/COMPATIBILITY.md +68 -0
- package/docs/CONTEXT-SELECTION-V2.md +61 -0
- package/docs/CURATION-APP-REVIEW.md +156 -0
- package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
- package/docs/IMPORTER-CURATION.md +142 -0
- package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
- package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
- package/docs/MIGRATIONS.md +103 -0
- package/docs/PRIVACY.md +28 -0
- package/docs/RECOVERY.md +58 -0
- package/docs/RELEASING.md +98 -0
- package/docs/hosts.md +152 -0
- package/docs/images/prodero-context-ready-cropped.png +0 -0
- package/docs/images/prodero-overview-cropped.png +0 -0
- package/docs/images/prodero-session-learned-cropped.png +0 -0
- package/docs/plugins.md +83 -0
- package/docs/ui-themes.md +49 -0
- package/instructions/AGENTS.md +39 -0
- package/package.json +92 -4
- package/plugins/migrate_ai_studio/README.md +101 -0
- package/plugins/migrate_ai_studio/index.cjs +396 -0
- package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
- package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
- package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
- package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
- package/plugins/migrate_ai_studio/plugin.json +11 -0
- package/plugins/migrate_ai_studio/schema.cjs +20 -0
- package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
- package/prodero.release.json +5 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { PRIVATE_DIRECTORY_MODE } from "../filesystem/permissions.js";
|
|
2
|
+
import { writeFileExclusive } from "./storage.js";
|
|
3
|
+
import { protectKnowledgeWrites } from './knowledge-write-policy.js';
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
|
+
import { assertNoSecrets } from "./secrets.js";
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { parseKnowledge } from './memory.js';
|
|
10
|
+
import { parseSkill } from './skills.js';
|
|
11
|
+
import { readConfig, checkedDataPath, exists, parseCheckpointData, ensureRuntimeDirectories, ensureDataDirectory, ensureSelfIgnoredPrivateDirectory, sha256, writeFileAtomic } from './storage.js';
|
|
12
|
+
const writeSchema = z.object({ path: z.string().regex(/^(memory|local\/memory|local\/skills|skills|(local\/)?archive\/\d{4}_(0[1-9]|1[0-2]))\/[a-zA-Z0-9_-]+\.md$|^checkpoints\/chk_[a-zA-Z0-9_-]+\.json$/), before: z.string().nullable(), after: z.string() }).strict();
|
|
13
|
+
const moveSchema = z.object({ from: z.string().regex(/^(local\/)?memory\/[a-zA-Z0-9_-]+\.md$/), to: z.string().regex(/^(local\/)?archive\/\d{4}_(0[1-9]|1[0-2])\/[a-zA-Z0-9_-]+\.md$/), before: z.string().regex(/^[a-f0-9]{64}$/) }).strict();
|
|
14
|
+
const journalSchema = z.discriminatedUnion('version', [
|
|
15
|
+
z.object({ version: z.literal(1), writes: z.array(writeSchema).min(1) }).strict(),
|
|
16
|
+
z.object({ version: z.literal(2), writes: z.array(writeSchema).min(1), moves: z.array(moveSchema) }).strict(),
|
|
17
|
+
]);
|
|
18
|
+
export async function safeProjectPath(root, relative) {
|
|
19
|
+
if (!/^(memory|local\/memory|local\/skills|skills|sessions|checkpoints|(local\/)?archive\/\d{4}_(0[1-9]|1[0-2]))\/[a-zA-Z0-9_-]+\.(md|json)$/.test(relative))
|
|
20
|
+
throw new Error('Unsafe canonical path');
|
|
21
|
+
return checkedDataPath(root, relative);
|
|
22
|
+
}
|
|
23
|
+
async function runtimeDirectory(root) {
|
|
24
|
+
await ensureRuntimeDirectories(root);
|
|
25
|
+
for (const directory of ['memory', 'skills'])
|
|
26
|
+
await checkedDataPath(root, directory);
|
|
27
|
+
const dir = await ensureDataDirectory(root, '.transactions');
|
|
28
|
+
await writeFileExclusive(await checkedDataPath(root, '.transactions/.gitignore'), '*\n').catch((error) => { if (error.code !== 'EEXIST')
|
|
29
|
+
throw error; });
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
32
|
+
export async function fileHash(target) {
|
|
33
|
+
try {
|
|
34
|
+
return sha256(await fs.readFile(target, 'utf8'));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT')
|
|
38
|
+
return null;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// An unforgeable async-context capability for Core composition, never a caller boolean.
|
|
43
|
+
const writerContext = new AsyncLocalStorage();
|
|
44
|
+
export function ownsSyncLock(root) {
|
|
45
|
+
const owner = writerContext.getStore();
|
|
46
|
+
return Boolean(owner?.active && owner.root === path.resolve(root));
|
|
47
|
+
}
|
|
48
|
+
function sameLock(a, b) { return a.dev === b.dev && a.ino === b.ino; }
|
|
49
|
+
// Recovery may inspect a PID only while every cooperating acquisition/release is
|
|
50
|
+
// excluded. Otherwise a dead PID can describe the previous directory generation.
|
|
51
|
+
// An interrupted transition guard remains fail-closed, as documented in RECOVERY.
|
|
52
|
+
async function tryLockTransition(root, transition) {
|
|
53
|
+
const guard = await checkedDataPath(root, '.transactions/lock-recovery');
|
|
54
|
+
try {
|
|
55
|
+
await fs.mkdir(guard, { mode: PRIVATE_DIRECTORY_MODE });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error.code === 'EEXIST')
|
|
59
|
+
return null;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const identity = await fs.lstat(guard);
|
|
63
|
+
try {
|
|
64
|
+
return { value: await transition() };
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
const current = await fs.lstat(await checkedDataPath(root, '.transactions/lock-recovery'));
|
|
68
|
+
if (!sameLock(identity, current))
|
|
69
|
+
throw new Error('Sync lock recovery guard changed; preserve transaction state for inspection');
|
|
70
|
+
await fs.rm(guard, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function removeOwnedLock(root, identity, owner) {
|
|
74
|
+
const lock = await checkedDataPath(root, '.transactions/lock');
|
|
75
|
+
const current = await fs.lstat(lock);
|
|
76
|
+
if (!sameLock(identity, current) || (owner !== undefined && await fs.readFile(await checkedDataPath(root, '.transactions/lock/owner'), 'utf8') !== owner)) {
|
|
77
|
+
throw new Error('Sync storage writer lock ownership changed; preserve transaction state for inspection');
|
|
78
|
+
}
|
|
79
|
+
await fs.rm(lock, { recursive: true, force: true });
|
|
80
|
+
}
|
|
81
|
+
const writerBusy = () => new Error('Another Sync or storage writer is already running, or its lock has no live owner; inspect .prodero/.transactions/lock and .prodero/.transactions/lock-recovery before retrying');
|
|
82
|
+
/** Per-worktree writer exclusion shared by migration, Sync and session mutations. */
|
|
83
|
+
export async function withSyncLock(root, operation, options = {}) {
|
|
84
|
+
if (options.maintainIntegration === false)
|
|
85
|
+
await ensureSelfIgnoredPrivateDirectory(root, '.transactions');
|
|
86
|
+
else
|
|
87
|
+
await runtimeDirectory(root);
|
|
88
|
+
const lock = await checkedDataPath(root, '.transactions/lock');
|
|
89
|
+
const deadline = Date.now() + (options.waitMs ?? 0);
|
|
90
|
+
let identity;
|
|
91
|
+
while (true) {
|
|
92
|
+
const attempt = await tryLockTransition(root, async () => {
|
|
93
|
+
let created = false;
|
|
94
|
+
try {
|
|
95
|
+
await fs.mkdir(lock, { mode: PRIVATE_DIRECTORY_MODE });
|
|
96
|
+
created = true;
|
|
97
|
+
const acquired = await fs.lstat(lock);
|
|
98
|
+
try {
|
|
99
|
+
await writeFileExclusive(await checkedDataPath(root, '.transactions/lock/owner'), String(process.pid));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
await removeOwnedLock(root, acquired);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
return { identity: acquired, recovered: false };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (created || error.code !== 'EEXIST')
|
|
109
|
+
throw error;
|
|
110
|
+
const stale = await fs.lstat(await checkedDataPath(root, '.transactions/lock')).catch(error => { if (error.code === 'ENOENT')
|
|
111
|
+
return null; throw error; });
|
|
112
|
+
if (!stale)
|
|
113
|
+
return { identity: null, recovered: true };
|
|
114
|
+
const owner = await fs.readFile(await checkedDataPath(root, '.transactions/lock/owner'), 'utf8').catch(error => { if (error.code === 'ENOENT')
|
|
115
|
+
return ''; throw error; });
|
|
116
|
+
const pid = /^\d+$/.test(owner) ? Number(owner) : 0;
|
|
117
|
+
if (pid > 0 && Number.isSafeInteger(pid)) {
|
|
118
|
+
try {
|
|
119
|
+
process.kill(pid, 0);
|
|
120
|
+
}
|
|
121
|
+
catch (probe) {
|
|
122
|
+
if (probe.code !== 'ESRCH')
|
|
123
|
+
throw probe;
|
|
124
|
+
await removeOwnedLock(root, stale, owner);
|
|
125
|
+
return { identity: null, recovered: true };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { identity: null, recovered: false };
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
if (attempt?.value.identity) {
|
|
132
|
+
identity = attempt.value.identity;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (attempt?.value.recovered)
|
|
136
|
+
continue;
|
|
137
|
+
if (Date.now() >= deadline)
|
|
138
|
+
throw writerBusy();
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
if (!options.migrationRecovery && await exists(await checkedDataPath(root, '.transactions/migration.json')))
|
|
143
|
+
throw new Error('Interrupted migration requires recovery; run Prodero init or prepare before recording new data. Preserve .prodero/.backups and .prodero/.transactions.');
|
|
144
|
+
const pending = await fs.readFile(await checkedDataPath(root, '.transactions/pending.json'), 'utf8').catch(error => { if (error.code === 'ENOENT')
|
|
145
|
+
return null; throw error; });
|
|
146
|
+
let pendingVersion;
|
|
147
|
+
try {
|
|
148
|
+
pendingVersion = pending ? JSON.parse(pending).version : undefined;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
throw new Error("PRODERO_INVALID_JOURNAL: invalid pending journal JSON; preserve it for recovery.");
|
|
152
|
+
}
|
|
153
|
+
if (pendingVersion === 2) {
|
|
154
|
+
if (options.recoverLifecycle === false)
|
|
155
|
+
throw new Error('Lifecycle transaction pending; explicit recovery required before preview');
|
|
156
|
+
await recoverSyncTransaction(root);
|
|
157
|
+
}
|
|
158
|
+
const owner = { root: path.resolve(root), active: true };
|
|
159
|
+
try {
|
|
160
|
+
return await writerContext.run(owner, operation);
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
owner.active = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
const releaseDeadline = Date.now() + 5_000;
|
|
168
|
+
while (!await tryLockTransition(root, () => removeOwnedLock(root, identity, String(process.pid)))) {
|
|
169
|
+
if (Date.now() >= releaseDeadline)
|
|
170
|
+
throw writerBusy();
|
|
171
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function validateWriteContent(root, writes) {
|
|
176
|
+
assertNoSecrets(writes.map(write => write.after));
|
|
177
|
+
const schema = (await readConfig(root))?.knowledgeSchemaVersion;
|
|
178
|
+
for (const write of writes) {
|
|
179
|
+
const filename = path.basename(write.path);
|
|
180
|
+
if (write.path.startsWith('checkpoints/'))
|
|
181
|
+
parseCheckpointData(write.after, filename);
|
|
182
|
+
else if ((write.path.startsWith('skills/') || write.path.startsWith('local/skills/')))
|
|
183
|
+
parseSkill(write.after, filename);
|
|
184
|
+
else
|
|
185
|
+
parseKnowledge(write.after, filename, schema);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function validateMoves(writes, moves) {
|
|
189
|
+
const sources = new Set();
|
|
190
|
+
for (const move of moves) {
|
|
191
|
+
if (sources.has(move.from) || writes.some(w => w.path === move.from))
|
|
192
|
+
throw new Error('Invalid lifecycle move source');
|
|
193
|
+
sources.add(move.from);
|
|
194
|
+
const destination = writes.find(w => w.path === move.to);
|
|
195
|
+
if (!destination || destination.before !== null || path.basename(move.from) !== path.basename(move.to) || move.from.startsWith('local/') !== move.to.startsWith('local/'))
|
|
196
|
+
throw new Error('Invalid lifecycle archive move');
|
|
197
|
+
const archived = parseKnowledge(destination.after, path.basename(move.to));
|
|
198
|
+
if (['active', 'disputed'].includes(archived.status))
|
|
199
|
+
throw new Error('Only historical knowledge can be archived');
|
|
200
|
+
if (!archived.archivedAt || !move.to.includes(`archive/${new Date(archived.archivedAt).toISOString().slice(0, 7).replace('-', '_')}/`))
|
|
201
|
+
throw new Error('Archive move requires matching archived_at');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function applyWrites(root, writes, moves = []) {
|
|
205
|
+
// Validate the entire journal and every before-image before the first write.
|
|
206
|
+
const targets = await Promise.all(writes.map((write) => safeProjectPath(root, write.path)));
|
|
207
|
+
await validateWriteContent(root, writes);
|
|
208
|
+
validateMoves(writes, moves);
|
|
209
|
+
for (const move of moves) {
|
|
210
|
+
const actual = await fileHash(await safeProjectPath(root, move.from));
|
|
211
|
+
const destination = writes.find(w => w.path === move.to);
|
|
212
|
+
if (actual !== move.before && (actual !== null || await fileHash(await safeProjectPath(root, move.to)) !== sha256(destination.after)))
|
|
213
|
+
throw new Error('Lifecycle recovery found a changed or missing source; journal preserved');
|
|
214
|
+
}
|
|
215
|
+
for (let i = 0; i < writes.length; i++) {
|
|
216
|
+
const actual = await fileHash(targets[i]);
|
|
217
|
+
if (actual !== writes[i].before && actual !== sha256(writes[i].after))
|
|
218
|
+
throw new Error(`Sync recovery found concurrently changed ${writes[i].path}; preserved journal for review`);
|
|
219
|
+
}
|
|
220
|
+
for (let i = 0; i < writes.length; i++) {
|
|
221
|
+
if (i === writes.length - 1)
|
|
222
|
+
for (const move of moves)
|
|
223
|
+
await fs.rm(await safeProjectPath(root, move.from), { force: true });
|
|
224
|
+
if (await fileHash(targets[i]) !== sha256(writes[i].after)) {
|
|
225
|
+
if (/^(local\/)?archive\//.test(writes[i].path))
|
|
226
|
+
await ensureDataDirectory(root, path.dirname(writes[i].path));
|
|
227
|
+
await writeFileAtomic(targets[i], writes[i].after);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
export async function recoverSyncTransaction(root) {
|
|
232
|
+
await runtimeDirectory(root);
|
|
233
|
+
const target = await checkedDataPath(root, '.transactions/pending.json');
|
|
234
|
+
const content = await fs.readFile(target, 'utf8').catch((error) => { if (error.code === 'ENOENT')
|
|
235
|
+
return null; throw error; });
|
|
236
|
+
if (!content)
|
|
237
|
+
return false;
|
|
238
|
+
let journal;
|
|
239
|
+
try {
|
|
240
|
+
journal = journalSchema.parse(JSON.parse(content));
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
throw new Error("PRODERO_INVALID_JOURNAL: .prodero/.transactions/pending.json has invalid JSON or metadata. Preserve the journal and inspect or restore it before retrying Sync.");
|
|
244
|
+
}
|
|
245
|
+
validateWriteOrder(journal.writes);
|
|
246
|
+
await applyWrites(root, journal.writes, journal.version === 2 ? journal.moves : []);
|
|
247
|
+
await fs.rm(target);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
function validateWriteOrder(writes) {
|
|
251
|
+
if (new Set(writes.map((w) => w.path)).size !== writes.length || !writes.at(-1)?.path.startsWith('checkpoints/') || writes.slice(0, -1).some((w) => w.path.startsWith('checkpoints/')))
|
|
252
|
+
throw new Error('Invalid Sync transaction order');
|
|
253
|
+
if (writes.at(-1).before !== null)
|
|
254
|
+
throw new Error('Checkpoint must be immutable');
|
|
255
|
+
}
|
|
256
|
+
/** Durable journal is NOT a disposable cache. Checkpoint is committed last. */
|
|
257
|
+
export async function commitSyncTransaction(root, writes, lifecycle) {
|
|
258
|
+
assertNoSecrets(writes.map(w => w.after));
|
|
259
|
+
// Validate paths and preimages before interpreting any untrusted content.
|
|
260
|
+
journalSchema.parse(lifecycle?.moves ? { version: 2, writes, moves: lifecycle.moves } : { version: 1, writes });
|
|
261
|
+
validateWriteOrder(writes);
|
|
262
|
+
for (const write of writes) {
|
|
263
|
+
if (await fileHash(await safeProjectPath(root, write.path)) !== write.before)
|
|
264
|
+
throw new Error(`Knowledge changed while reasoning: ${write.path}; retry Sync`);
|
|
265
|
+
}
|
|
266
|
+
const guarded = await protectKnowledgeWrites(root, writes, lifecycle?.policyMode === 'exact', lifecycle?.moves);
|
|
267
|
+
writes = guarded.writes;
|
|
268
|
+
const journal = journalSchema.parse(lifecycle?.moves ? { version: 2, writes, moves: lifecycle.moves } : { version: 1, writes });
|
|
269
|
+
if (lifecycle?.moves)
|
|
270
|
+
validateMoves(writes, lifecycle.moves);
|
|
271
|
+
validateWriteOrder(journal.writes);
|
|
272
|
+
for (const write of writes) {
|
|
273
|
+
const target = await safeProjectPath(root, write.path);
|
|
274
|
+
if (await fileHash(target) !== write.before)
|
|
275
|
+
throw new Error(`Knowledge changed while reasoning: ${write.path}; retry Sync`);
|
|
276
|
+
}
|
|
277
|
+
for (const move of lifecycle?.moves ?? [])
|
|
278
|
+
if (await fileHash(await safeProjectPath(root, move.from)) !== move.before)
|
|
279
|
+
throw new Error('Knowledge source changed before archive; retry lifecycle preview');
|
|
280
|
+
await runtimeDirectory(root);
|
|
281
|
+
const target = await checkedDataPath(root, '.transactions/pending.json');
|
|
282
|
+
if (await fileHash(target))
|
|
283
|
+
throw new Error('Unrecovered Sync journal');
|
|
284
|
+
await validateWriteContent(root, writes);
|
|
285
|
+
await writeFileAtomic(target, JSON.stringify(journal));
|
|
286
|
+
await applyWrites(root, journal.writes, journal.version === 2 ? journal.moves : []);
|
|
287
|
+
await fs.rm(target);
|
|
288
|
+
return guarded.policy;
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=sync-transaction.js.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { parseObservationAnnotations } from "./format.js";
|
|
2
|
+
export const TOKEN_COUNT_METHOD = "estimated-utf8-bytes-div-4-v1";
|
|
3
|
+
export const DEFAULT_CONTEXT_BUDGET_TOKENS = 6_000;
|
|
4
|
+
export const DEFAULT_REQUIRED_CONTEXT_BUDGET_TOKENS = 2_000;
|
|
5
|
+
export const MAX_CANONICAL_ITEM_TOKENS = 2_000;
|
|
6
|
+
export const STALE_STATE_AFTER_DAYS = 180;
|
|
7
|
+
/**
|
|
8
|
+
* Deterministic local approximation: ceil(UTF-8 byte length / 4).
|
|
9
|
+
* It is deliberately model-neutral and must never be presented as provider billing usage.
|
|
10
|
+
*/
|
|
11
|
+
export function estimateTokens(value) {
|
|
12
|
+
return value ? Math.ceil(Buffer.byteLength(value, "utf8") / 4) : 0;
|
|
13
|
+
}
|
|
14
|
+
export function knowledgeText(item) {
|
|
15
|
+
return `${item.title}\n${item.content}`;
|
|
16
|
+
}
|
|
17
|
+
export function safeRatio(numerator, denominator) {
|
|
18
|
+
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0)
|
|
19
|
+
return 0;
|
|
20
|
+
return Math.max(0, numerator) / denominator;
|
|
21
|
+
}
|
|
22
|
+
export function contextPressure(requiredTokens, budgetTokens) {
|
|
23
|
+
const ratio = safeRatio(requiredTokens, budgetTokens);
|
|
24
|
+
return ratio >= 0.75 ? "high" : ratio >= 0.5 ? "moderate" : "normal";
|
|
25
|
+
}
|
|
26
|
+
export function memoryCompression(rawTokens, canonicalTokens) {
|
|
27
|
+
if (rawTokens <= 0)
|
|
28
|
+
return { kind: "unavailable", ratio: null };
|
|
29
|
+
if (canonicalTokens < rawTokens)
|
|
30
|
+
return { kind: "compression", ratio: 1 - canonicalTokens / rawTokens };
|
|
31
|
+
if (canonicalTokens > rawTokens)
|
|
32
|
+
return { kind: "expansion", ratio: canonicalTokens / rawTokens - 1 };
|
|
33
|
+
return { kind: "unchanged", ratio: 0 };
|
|
34
|
+
}
|
|
35
|
+
const SECTION_TYPES = {
|
|
36
|
+
Decisions: "decision",
|
|
37
|
+
Lessons: "lesson",
|
|
38
|
+
Discoveries: "discovery",
|
|
39
|
+
"Project State Changes": "state_change",
|
|
40
|
+
"Global Instruction Candidates": "global_instruction_candidate",
|
|
41
|
+
"Skill Candidates": "skill_candidate",
|
|
42
|
+
};
|
|
43
|
+
export function summarizeSessionMemory(session) {
|
|
44
|
+
const byType = {
|
|
45
|
+
decision: 0, lesson: 0, discovery: 0, state_change: 0, global_instruction_candidate: 0, skill_candidate: 0,
|
|
46
|
+
};
|
|
47
|
+
let observations = 0;
|
|
48
|
+
let rawSessionTokens = 0;
|
|
49
|
+
for (const [section, entries] of Object.entries(session.sections)) {
|
|
50
|
+
const type = SECTION_TYPES[section];
|
|
51
|
+
if (!type)
|
|
52
|
+
continue;
|
|
53
|
+
for (const raw of entries) {
|
|
54
|
+
const content = parseObservationAnnotations(raw).content;
|
|
55
|
+
if (!content)
|
|
56
|
+
continue;
|
|
57
|
+
observations += 1;
|
|
58
|
+
byType[type] += 1;
|
|
59
|
+
rawSessionTokens += estimateTokens(content);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { observations, byType, rawSessionTokens };
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { MANAGED_END, MANAGED_START, PRODERO_SCHEMA_VERSION } from "./constants.js";
|
|
3
|
+
// The same relative location works from src/core (dev) and dist/core (installed).
|
|
4
|
+
export const AGENT_INSTRUCTIONS_PATH = fileURLToPath(new URL("../../instructions/AGENTS.md", import.meta.url));
|
|
5
|
+
export const AGENTS_BLOCK = `${MANAGED_START}
|
|
6
|
+
Read and follow the instructions returned by prodero_instructions_read from the connected Prodero MCP server.
|
|
7
|
+
${MANAGED_END}`;
|
|
8
|
+
export const INDEX_TEMPLATE = `---
|
|
9
|
+
prodero_schema: ${PRODERO_SCHEMA_VERSION}
|
|
10
|
+
kind: project-index
|
|
11
|
+
generated: true
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Prodero · Project Intelligence
|
|
15
|
+
|
|
16
|
+
> Compact router for progressive context loading. Open only references relevant to the current task.
|
|
17
|
+
|
|
18
|
+
## Required Global Context
|
|
19
|
+
|
|
20
|
+
_No required global knowledge._
|
|
21
|
+
|
|
22
|
+
## Contextual Knowledge
|
|
23
|
+
|
|
24
|
+
_No contextual knowledge._
|
|
25
|
+
|
|
26
|
+
## Local Knowledge
|
|
27
|
+
|
|
28
|
+
- [Developer-local knowledge](local/index.md) — ignored by Git and retrieved together with Versioned Knowledge.
|
|
29
|
+
|
|
30
|
+
## Project Skills
|
|
31
|
+
|
|
32
|
+
_No project skills._
|
|
33
|
+
|
|
34
|
+
## Loading Rules
|
|
35
|
+
|
|
36
|
+
1. Use bounded context resolution; required global knowledge is priority-ranked and budgeted.
|
|
37
|
+
2. Follow only bounded graph relationships selected by context resolution; never recursively load the graph or unrelated scopes.
|
|
38
|
+
3. Load relevant project skills; do not recursively load \`.prodero/\`.
|
|
39
|
+
4. Use session memory for agent-inferred observations; normal learning and conversational remember default to Local Knowledge.
|
|
40
|
+
5. Use the promotion review workflow for Versioned Knowledge. Review repository changes before committing or pushing; Prodero does neither.
|
|
41
|
+
`;
|
|
42
|
+
export function replaceManagedBlock(existing) {
|
|
43
|
+
const start = existing.indexOf(MANAGED_START);
|
|
44
|
+
const end = existing.indexOf(MANAGED_END);
|
|
45
|
+
if (start >= 0 && end >= start) {
|
|
46
|
+
const before = existing.slice(0, start).trimEnd();
|
|
47
|
+
const after = existing.slice(end + MANAGED_END.length).trimStart();
|
|
48
|
+
return `${before}${before ? "\n\n" : ""}${AGENTS_BLOCK}${after ? `\n${after}` : "\n"}`;
|
|
49
|
+
}
|
|
50
|
+
const prefix = existing.trimEnd();
|
|
51
|
+
return `${prefix}${prefix ? "\n\n" : ""}${AGENTS_BLOCK}\n`;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=templates.js.map
|