@prodero/curator 0.0.0-stage → 0.1.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.
Files changed (137) hide show
  1. package/ARCHITECTURE.md +298 -0
  2. package/CHANGELOG.md +16 -0
  3. package/CONTRIBUTING.md +9 -0
  4. package/LICENSE +21 -0
  5. package/README.md +94 -2
  6. package/SECURITY.md +7 -0
  7. package/bin/prodero +7 -0
  8. package/dist/cli.js +228 -0
  9. package/dist/context-instructions.js +2 -0
  10. package/dist/contracts.js +50 -0
  11. package/dist/conversation-presentation.js +262 -0
  12. package/dist/core/agent-instructions.js +39 -0
  13. package/dist/core/applicability-label.js +11 -0
  14. package/dist/core/applicability.js +143 -0
  15. package/dist/core/bootstrap.js +129 -0
  16. package/dist/core/consolidation.js +315 -0
  17. package/dist/core/constants.js +21 -0
  18. package/dist/core/context-allocation.js +54 -0
  19. package/dist/core/context-snapshot.js +12 -0
  20. package/dist/core/context.js +489 -0
  21. package/dist/core/curation-clauses.js +68 -0
  22. package/dist/core/curation-evidence.js +98 -0
  23. package/dist/core/curation-graphical.js +105 -0
  24. package/dist/core/curation-review-presentation.js +4 -0
  25. package/dist/core/curation-review-state.js +62 -0
  26. package/dist/core/curation-review.js +573 -0
  27. package/dist/core/curation-revision.js +106 -0
  28. package/dist/core/curation-semantic-contract.js +239 -0
  29. package/dist/core/curation-semantic-jobs.js +280 -0
  30. package/dist/core/diagnostics.js +17 -0
  31. package/dist/core/format.js +114 -0
  32. package/dist/core/ids.js +6 -0
  33. package/dist/core/imports.js +169 -0
  34. package/dist/core/index.js +93 -0
  35. package/dist/core/instruction-review.js +86 -0
  36. package/dist/core/integrity-files.js +174 -0
  37. package/dist/core/intelligence.js +95 -0
  38. package/dist/core/knowledge-analysis.js +46 -0
  39. package/dist/core/knowledge-authority.js +143 -0
  40. package/dist/core/knowledge-curation.js +316 -0
  41. package/dist/core/knowledge-engine.js +475 -0
  42. package/dist/core/knowledge-graph.js +191 -0
  43. package/dist/core/knowledge-lifecycle.js +212 -0
  44. package/dist/core/knowledge-management.js +113 -0
  45. package/dist/core/knowledge-migration.js +84 -0
  46. package/dist/core/knowledge-policy.js +38 -0
  47. package/dist/core/knowledge-provenance.js +90 -0
  48. package/dist/core/knowledge-write-policy.js +85 -0
  49. package/dist/core/language.js +14 -0
  50. package/dist/core/matching.js +6 -0
  51. package/dist/core/memory-index.js +239 -0
  52. package/dist/core/memory-intelligence.js +288 -0
  53. package/dist/core/memory-link-repair.js +66 -0
  54. package/dist/core/memory-operations.js +122 -0
  55. package/dist/core/memory-pressure.js +212 -0
  56. package/dist/core/memory-sync-curator.js +62 -0
  57. package/dist/core/memory-sync.js +463 -0
  58. package/dist/core/memory-transformation-bundle.js +163 -0
  59. package/dist/core/memory-transformation-proposal.js +135 -0
  60. package/dist/core/memory-transformation.js +108 -0
  61. package/dist/core/memory.js +186 -0
  62. package/dist/core/migrations.js +569 -0
  63. package/dist/core/path-scope.js +27 -0
  64. package/dist/core/plugin-files.js +116 -0
  65. package/dist/core/project-root.js +39 -0
  66. package/dist/core/project.js +634 -0
  67. package/dist/core/promotion.js +279 -0
  68. package/dist/core/reconciliation.js +61 -0
  69. package/dist/core/secrets.js +67 -0
  70. package/dist/core/semantic-safety.js +33 -0
  71. package/dist/core/session-index.js +112 -0
  72. package/dist/core/session-review.js +150 -0
  73. package/dist/core/sessions.js +289 -0
  74. package/dist/core/settings.js +76 -0
  75. package/dist/core/skill-curation.js +239 -0
  76. package/dist/core/skill-polish.js +41 -0
  77. package/dist/core/skills.js +368 -0
  78. package/dist/core/storage.js +408 -0
  79. package/dist/core/sync-transaction.js +254 -0
  80. package/dist/core/telemetry.js +64 -0
  81. package/dist/core/templates.js +53 -0
  82. package/dist/core/types.js +2 -0
  83. package/dist/core/update.js +320 -0
  84. package/dist/curation-review-adapter.js +92 -0
  85. package/dist/diagnostic-boundary.js +54 -0
  86. package/dist/filesystem/permissions.js +39 -0
  87. package/dist/hosts/adapters.js +119 -0
  88. package/dist/hosts/files.js +133 -0
  89. package/dist/hosts/manager.js +150 -0
  90. package/dist/hosts/public-entry.js +47 -0
  91. package/dist/hosts/runtime-command.js +66 -0
  92. package/dist/hosts/runtime.js +139 -0
  93. package/dist/hosts/wizard.js +114 -0
  94. package/dist/knowledge-table.js +38 -0
  95. package/dist/mcp-development.js +69 -0
  96. package/dist/mcp-workspace.js +141 -0
  97. package/dist/memory-presentation.js +172 -0
  98. package/dist/plugins/api.js +2 -0
  99. package/dist/plugins/context.js +20 -0
  100. package/dist/plugins/mcp.js +72 -0
  101. package/dist/plugins/registry.js +106 -0
  102. package/dist/presentation-cards.js +32 -0
  103. package/dist/presentation-server.js +79 -0
  104. package/dist/presentation-trace.js +174 -0
  105. package/dist/presentation.js +412 -0
  106. package/dist/providers/mcp-sampling.js +45 -0
  107. package/dist/server.js +656 -0
  108. package/dist/settings-definition.js +22 -0
  109. package/dist/skill-instructions.js +2 -0
  110. package/dist-ui/index.html +214 -0
  111. package/docs/APPLICABILITY.md +151 -0
  112. package/docs/COMPATIBILITY.md +49 -0
  113. package/docs/CONTEXT-SELECTION-V2.md +61 -0
  114. package/docs/CURATION-APP-REVIEW.md +156 -0
  115. package/docs/CURATION-CLAUSE-REFINEMENT.md +45 -0
  116. package/docs/IMPORTER-CURATION.md +142 -0
  117. package/docs/KNOWLEDGE-AUTHORITY.md +165 -0
  118. package/docs/KNOWLEDGE-SCHEMA-V2.md +150 -0
  119. package/docs/MIGRATIONS.md +99 -0
  120. package/docs/PRIVACY.md +28 -0
  121. package/docs/RECOVERY.md +56 -0
  122. package/docs/RELEASING.md +40 -0
  123. package/docs/hosts.md +134 -0
  124. package/docs/plugins.md +83 -0
  125. package/docs/ui-themes.md +43 -0
  126. package/instructions/AGENTS.md +33 -0
  127. package/package.json +88 -4
  128. package/plugins/migrate_ai_studio/README.md +101 -0
  129. package/plugins/migrate_ai_studio/index.cjs +396 -0
  130. package/plugins/migrate_ai_studio/instructions/audit.md +25 -0
  131. package/plugins/migrate_ai_studio/instructions/classify.md +41 -0
  132. package/plugins/migrate_ai_studio/instructions/migrate.md +34 -0
  133. package/plugins/migrate_ai_studio/instructions/verify.md +20 -0
  134. package/plugins/migrate_ai_studio/plugin.json +11 -0
  135. package/plugins/migrate_ai_studio/schema.cjs +20 -0
  136. package/plugins/migrate_ai_studio/ui/dist/index.html +77 -0
  137. package/prodero.release.json +5 -0
@@ -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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,320 @@
1
+ import { publicDiagnostic } from "./diagnostics.js";
2
+ import crypto from "node:crypto";
3
+ import { spawn } from "node:child_process";
4
+ import { lstatSync, readFileSync, readdirSync } from "node:fs";
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { z } from "zod";
8
+ import { PRODERO_SCHEMA_VERSION, PRODERO_VERSION } from "./constants.js";
9
+ import { writeFileAtomic } from "./storage.js";
10
+ const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
11
+ const versionSchema = z.string().regex(semverPattern, "Expected a valid semantic version");
12
+ export const releaseMetadataSchema = z.object({
13
+ version: versionSchema,
14
+ schemaVersion: z.number().int().positive(),
15
+ releaseId: z.string().regex(/^[A-Za-z0-9._-]+$/).max(200),
16
+ publishedAt: z.string().datetime().optional(),
17
+ notes: z.string().max(4_000).optional(),
18
+ artifact: z.object({ locator: z.string().trim().min(1), integrity: z.string().regex(/^sha256:[a-f0-9]{64}$/) }).strict(),
19
+ }).strict();
20
+ export const MANUAL_UPDATE_COMMAND = "npm install --global @prodero/curator@latest";
21
+ export class ReleaseSourceUnavailableError extends Error {
22
+ }
23
+ function parseVersion(value) {
24
+ const match = semverPattern.exec(value);
25
+ if (!match)
26
+ throw new Error(`Invalid semantic version: ${value}`);
27
+ return { core: [Number(match[1]), Number(match[2]), Number(match[3])], prerelease: match[4]?.split(".") ?? [] };
28
+ }
29
+ /** Compare strict SemVer values. Build metadata does not affect precedence. */
30
+ export function compareVersions(left, right) {
31
+ const a = parseVersion(left), b = parseVersion(right);
32
+ for (let index = 0; index < 3; index += 1)
33
+ if (a.core[index] !== b.core[index])
34
+ return a.core[index] > b.core[index] ? 1 : -1;
35
+ if (!a.prerelease.length || !b.prerelease.length)
36
+ return a.prerelease.length === b.prerelease.length ? 0 : a.prerelease.length ? -1 : 1;
37
+ for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
38
+ const av = a.prerelease[index], bv = b.prerelease[index];
39
+ if (av === undefined || bv === undefined)
40
+ return av === bv ? 0 : av === undefined ? -1 : 1;
41
+ if (av === bv)
42
+ continue;
43
+ const an = /^\d+$/.test(av), bn = /^\d+$/.test(bv);
44
+ if (an && bn)
45
+ return Number(av) > Number(bv) ? 1 : -1;
46
+ if (an !== bn)
47
+ return an ? -1 : 1;
48
+ return av > bv ? 1 : -1;
49
+ }
50
+ return 0;
51
+ }
52
+ export class UnconfiguredReleaseSource {
53
+ id = "unconfigured";
54
+ async latest() { return null; }
55
+ async stage() { throw new Error("No Prodero release source is configured."); }
56
+ }
57
+ const LOCAL_RELEASE_FILES = ["package.json", "package-lock.json", "prodero.release.json", "tsconfig.json", "vite.config.ts", "src", "scripts", "bin", "plugins"];
58
+ const MAX_LOCAL_RELEASE_FILES = 5_000;
59
+ const MAX_LOCAL_RELEASE_BYTES = 64 * 1024 * 1024;
60
+ function localCheckoutDigest(root) {
61
+ const digest = crypto.createHash("sha256");
62
+ let files = 0, bytes = 0;
63
+ const visit = (relative) => {
64
+ const absolute = path.join(root, relative);
65
+ let stat;
66
+ try {
67
+ stat = lstatSync(absolute);
68
+ }
69
+ catch (error) {
70
+ if (error.code === "ENOENT")
71
+ return;
72
+ throw error;
73
+ }
74
+ if (stat.isSymbolicLink())
75
+ throw new Error(`Local release source contains a symlink: ${relative}`);
76
+ if (stat.isDirectory()) {
77
+ for (const name of readdirSync(absolute).sort())
78
+ visit(path.join(relative, name));
79
+ return;
80
+ }
81
+ if (!stat.isFile())
82
+ return;
83
+ files += 1;
84
+ bytes += stat.size;
85
+ if (files > MAX_LOCAL_RELEASE_FILES || bytes > MAX_LOCAL_RELEASE_BYTES)
86
+ throw new Error("Local release source exceeds the bounded release snapshot.");
87
+ digest.update(`F\0${relative.split(path.sep).join("/")}\0${stat.size}\0`);
88
+ digest.update(readFileSync(absolute));
89
+ digest.update("\0");
90
+ };
91
+ for (const relative of LOCAL_RELEASE_FILES)
92
+ visit(relative);
93
+ return digest.digest("hex");
94
+ }
95
+ async function runLocalBuild(root, signal) {
96
+ await new Promise((resolve, reject) => {
97
+ const child = spawn(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build"], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
98
+ let output = "";
99
+ const append = (chunk) => { output = `${output}${chunk.toString("utf8")}`.slice(-8_000); };
100
+ child.stdout.on("data", append);
101
+ child.stderr.on("data", append);
102
+ const abort = () => child.kill("SIGTERM");
103
+ signal?.addEventListener("abort", abort, { once: true });
104
+ child.once("error", reject);
105
+ child.once("exit", (code, terminationSignal) => {
106
+ signal?.removeEventListener("abort", abort);
107
+ if (signal?.aborted)
108
+ reject(new InterruptedUpdateError("Local release build was interrupted."));
109
+ else if (code === 0)
110
+ resolve();
111
+ else
112
+ reject(new Error(`Local release build failed with ${terminationSignal ? `signal ${terminationSignal}` : `exit code ${code ?? "unknown"}`}.${output.trim() ? `\n${output.trim()}` : ""}`));
113
+ });
114
+ });
115
+ }
116
+ /** One-click adapter for an MCP runtime launched from a development checkout. */
117
+ export class LocalCheckoutReleaseSource {
118
+ root;
119
+ id = "local-checkout";
120
+ installedReleaseId;
121
+ constructor(root) {
122
+ this.root = root;
123
+ this.installedReleaseId = `local-${localCheckoutDigest(root)}`;
124
+ }
125
+ async latest() {
126
+ const identity = JSON.parse(await fs.readFile(path.join(this.root, "package.json"), "utf8"));
127
+ const releaseIdentity = z.object({ formatVersion: z.literal(1), schemaVersion: z.number().int().positive() }).strict().parse(JSON.parse(await fs.readFile(path.join(this.root, "prodero.release.json"), "utf8")));
128
+ const version = versionSchema.parse(identity.version);
129
+ const hash = localCheckoutDigest(this.root);
130
+ return { version, schemaVersion: releaseIdentity.schemaVersion, releaseId: `local-${hash}`, artifact: { locator: ".", integrity: `sha256:${hash}` } };
131
+ }
132
+ async stage(release, signal) {
133
+ const before = await this.latest();
134
+ if (before.releaseId !== release.releaseId || before.artifact.integrity !== release.artifact.integrity)
135
+ throw new InterruptedUpdateError("Local release source changed after update review; check again.");
136
+ await runLocalBuild(this.root, signal);
137
+ const after = await this.latest();
138
+ if (after.releaseId !== release.releaseId || after.artifact.integrity !== release.artifact.integrity)
139
+ throw new InterruptedUpdateError("Local release source changed while it was being built; no activation was recorded.");
140
+ return { restartRequired: true, detail: `Local checkout ${this.root} built and verified successfully.` };
141
+ }
142
+ }
143
+ const localManifestSchema = z.object({
144
+ formatVersion: z.literal(1), release: z.unknown().nullable(),
145
+ simulation: z.object({ outcome: z.enum(["success", "failed", "interrupted"]), message: z.string().optional() }).strict().optional(),
146
+ }).strict();
147
+ class InterruptedUpdateError extends Error {
148
+ }
149
+ /** Deterministic source used for local development and contract tests. */
150
+ export class LocalManifestReleaseSource {
151
+ manifestPath;
152
+ stateDirectory;
153
+ id = "local-manifest";
154
+ constructor(manifestPath, stateDirectory = path.join(path.dirname(manifestPath), ".prodero-update-state")) {
155
+ this.manifestPath = manifestPath;
156
+ this.stateDirectory = stateDirectory;
157
+ }
158
+ async manifest() { return localManifestSchema.parse(JSON.parse(await fs.readFile(this.manifestPath, "utf8"))); }
159
+ async latest() { return (await this.manifest()).release; }
160
+ async stage(release, signal) {
161
+ signal?.throwIfAborted();
162
+ const manifest = await this.manifest();
163
+ const simulation = manifest.simulation?.outcome ?? "success";
164
+ if (simulation === "failed")
165
+ throw new Error(manifest.simulation?.message ?? "Local release installation failed.");
166
+ if (simulation === "interrupted")
167
+ throw new InterruptedUpdateError(manifest.simulation?.message ?? "Local release installation was interrupted.");
168
+ const manifestDirectory = path.dirname(this.manifestPath);
169
+ const artifactPath = path.resolve(manifestDirectory, release.artifact.locator);
170
+ if (path.isAbsolute(release.artifact.locator) || (artifactPath !== manifestDirectory && !artifactPath.startsWith(`${manifestDirectory}${path.sep}`)))
171
+ throw new Error("Release artifact must stay inside the local manifest directory.");
172
+ const artifact = await fs.readFile(artifactPath);
173
+ const integrity = `sha256:${crypto.createHash("sha256").update(artifact).digest("hex")}`;
174
+ if (integrity !== release.artifact.integrity)
175
+ throw new Error("Release artifact integrity verification failed.");
176
+ await fs.mkdir(this.stateDirectory, { recursive: true });
177
+ const stagedPath = path.join(this.stateDirectory, `${release.releaseId}.artifact`);
178
+ const temporary = `${stagedPath}.partial-${process.pid}`;
179
+ try {
180
+ await fs.writeFile(temporary, artifact, { flag: "wx" });
181
+ signal?.throwIfAborted();
182
+ await fs.rename(temporary, stagedPath);
183
+ await writeFileAtomic(path.join(this.stateDirectory, "pending.json"), `${JSON.stringify({ formatVersion: 1, release, stagedPath, stagedAt: new Date().toISOString() }, null, 2)}\n`);
184
+ }
185
+ catch (error) {
186
+ await fs.rm(temporary, { force: true }).catch(() => { });
187
+ throw error;
188
+ }
189
+ return { restartRequired: true, detail: `Release ${release.version} was verified and staged at ${stagedPath}.` };
190
+ }
191
+ }
192
+ /** npm is read-only here: package installation always belongs to the user's package manager. */
193
+ export class NpmReleaseSource {
194
+ request;
195
+ id = "npm";
196
+ constructor(request = fetch) {
197
+ this.request = request;
198
+ }
199
+ async latest(signal) {
200
+ try {
201
+ const response = await this.request("https://registry.npmjs.org/@prodero%2fcurator/latest", { signal, redirect: "error" });
202
+ if (response.status === 404)
203
+ return null;
204
+ if (!response.ok)
205
+ throw new Error("Registry unavailable");
206
+ return await response.json();
207
+ }
208
+ catch {
209
+ throw new ReleaseSourceUnavailableError("Cannot reach the public npm release source. Installed knowledge is unaffected.");
210
+ }
211
+ }
212
+ async stage() { throw new Error(`Use ${MANUAL_UPDATE_COMMAND}`); }
213
+ }
214
+ export function configuredReleaseSource(environment = process.env) {
215
+ if (environment.PRODERO_RELEASE_SOURCE === "none")
216
+ return new UnconfiguredReleaseSource();
217
+ if (environment.PRODERO_DEV_UPDATE === "1") {
218
+ const manifest = environment.PRODERO_RELEASE_MANIFEST?.trim();
219
+ if (manifest)
220
+ return new LocalManifestReleaseSource(path.resolve(manifest), environment.PRODERO_UPDATE_STATE_DIR?.trim());
221
+ const checkout = environment.PRODERO_LOCAL_RELEASE_CHECKOUT?.trim();
222
+ if (checkout)
223
+ return new LocalCheckoutReleaseSource(path.resolve(checkout));
224
+ return new UnconfiguredReleaseSource();
225
+ }
226
+ return new NpmReleaseSource();
227
+ }
228
+ function updateAvailable(source, release, installedVersion) {
229
+ const precedence = compareVersions(release.version, installedVersion);
230
+ return precedence > 0 || (precedence === 0 && Boolean(source.installedReleaseId) && source.installedReleaseId !== release.releaseId);
231
+ }
232
+ async function checkReleaseUpdate(source = configuredReleaseSource(), installedVersion = PRODERO_VERSION) {
233
+ let raw;
234
+ try {
235
+ versionSchema.parse(installedVersion);
236
+ raw = await source.latest(AbortSignal.timeout(2_500));
237
+ }
238
+ catch (error) {
239
+ const status = error instanceof ReleaseSourceUnavailableError || (error instanceof DOMException && ["AbortError", "TimeoutError"].includes(error.name)) ? "unavailable" : "invalid";
240
+ return { current: installedVersion, latest: null, available: false, source: source.id, status, installedSchemaVersion: PRODERO_SCHEMA_VERSION, availableSchemaVersion: null, error: publicDiagnostic(error) };
241
+ }
242
+ if (raw === null)
243
+ return { current: installedVersion, latest: null, available: false, source: source.id, status: "unavailable", installedSchemaVersion: PRODERO_SCHEMA_VERSION, availableSchemaVersion: null };
244
+ try {
245
+ const release = releaseMetadataSchema.parse(raw);
246
+ if (release.schemaVersion < PRODERO_SCHEMA_VERSION)
247
+ throw new Error(`Release schema v${release.schemaVersion} is older than installed schema v${PRODERO_SCHEMA_VERSION}.`);
248
+ const available = updateAvailable(source, release, installedVersion);
249
+ return { current: installedVersion, latest: release.version, available, source: source.id, status: available ? "update-available" : "current", installedSchemaVersion: PRODERO_SCHEMA_VERSION, availableSchemaVersion: release.schemaVersion };
250
+ }
251
+ catch (error) {
252
+ return { current: installedVersion, latest: null, available: false, source: source.id, status: "invalid", installedSchemaVersion: PRODERO_SCHEMA_VERSION, availableSchemaVersion: null, error: publicDiagnostic(error) };
253
+ }
254
+ }
255
+ export async function checkForUpdate(source = configuredReleaseSource(), installedVersion = PRODERO_VERSION) {
256
+ if (!(source instanceof NpmReleaseSource)) {
257
+ const result = await checkReleaseUpdate(source, installedVersion);
258
+ return { ...result, manualUpdateCommand: MANUAL_UPDATE_COMMAND, developmentOnly: source.id !== "unconfigured",
259
+ ...(result.status === "unavailable" && !result.error ? { error: "No production release source is configured or no release is available." } : {}) };
260
+ }
261
+ const base = { current: installedVersion, latest: null, available: false, source: source.id, installedSchemaVersion: PRODERO_SCHEMA_VERSION, availableSchemaVersion: null, manualUpdateCommand: MANUAL_UPDATE_COMMAND, developmentOnly: false };
262
+ try {
263
+ const raw = await source.latest(AbortSignal.timeout(2_500));
264
+ if (raw === null)
265
+ return { ...base, status: "unavailable", error: "No public npm release is available yet." };
266
+ const { version } = z.object({ version: versionSchema }).parse(raw);
267
+ const available = compareVersions(version, installedVersion) > 0;
268
+ return { ...base, latest: version, available, status: available ? "update-available" : "current" };
269
+ }
270
+ catch (error) {
271
+ return { ...base, status: error instanceof ReleaseSourceUnavailableError ? "unavailable" : "invalid", error: error instanceof ReleaseSourceUnavailableError ? error.message : "Invalid version metadata from the npm release source." };
272
+ }
273
+ }
274
+ export async function installLatest(source = configuredReleaseSource(), installedVersion = PRODERO_VERSION) {
275
+ if (source instanceof NpmReleaseSource || source instanceof UnconfiguredReleaseSource) {
276
+ const check = await checkForUpdate(source, installedVersion);
277
+ return { ok: true, status: "manual-update", installedVersion, availableVersion: check.latest, source: source.id, restartRequired: false,
278
+ message: `${check.error ? `${check.error} ` : ""}Run: ${MANUAL_UPDATE_COMMAND}. Then restart your MCP host. This tool has not installed or changed anything. Project knowledge remains in the project .prodero directory.` };
279
+ }
280
+ let raw;
281
+ try {
282
+ raw = await source.latest(AbortSignal.timeout(2_500));
283
+ }
284
+ catch (error) {
285
+ if (error instanceof ReleaseSourceUnavailableError || (error instanceof DOMException && ["AbortError", "TimeoutError"].includes(error.name)))
286
+ return unavailable(source, installedVersion, error);
287
+ return failure("invalid", source, installedVersion, null, error);
288
+ }
289
+ if (raw === null)
290
+ return { ok: false, status: "unavailable", installedVersion, availableVersion: null, source: source.id, restartRequired: false, message: "No Prodero release source is configured or no release is available." };
291
+ let release;
292
+ try {
293
+ release = releaseMetadataSchema.parse(raw);
294
+ compareVersions(release.version, installedVersion);
295
+ if (release.schemaVersion < PRODERO_SCHEMA_VERSION)
296
+ throw new Error(`Release schema v${release.schemaVersion} is older than installed schema v${PRODERO_SCHEMA_VERSION}.`);
297
+ }
298
+ catch (error) {
299
+ return failure("invalid", source, installedVersion, null, error);
300
+ }
301
+ if (!updateAvailable(source, release, installedVersion))
302
+ return { ok: true, status: "current", installedVersion, availableVersion: release.version, source: source.id, restartRequired: false, message: `Prodero ${installedVersion} is already current.` };
303
+ try {
304
+ const staged = await source.stage(release, AbortSignal.timeout(30_000));
305
+ return { ok: true, status: "restart-required", installedVersion, availableVersion: release.version, source: source.id, restartRequired: staged.restartRequired, message: `${staged.detail ?? `Prodero ${release.version} was staged.`} Development-only staging. Restart the Prodero MCP runtime after a checkout rebuild; manifest simulation has no production activation consumer.` };
306
+ }
307
+ catch (error) {
308
+ const interrupted = error instanceof InterruptedUpdateError || (error instanceof DOMException && ["AbortError", "TimeoutError"].includes(error.name));
309
+ return failure(interrupted ? "interrupted" : "failed", source, installedVersion, release.version, error);
310
+ }
311
+ }
312
+ function unavailable(source, installedVersion, error) {
313
+ const detail = publicDiagnostic(error);
314
+ return { ok: false, status: "unavailable", installedVersion, availableVersion: null, source: source.id, restartRequired: false, message: `Prodero release source is unavailable: ${detail}` };
315
+ }
316
+ function failure(status, source, installedVersion, availableVersion, error) {
317
+ const detail = publicDiagnostic(error);
318
+ return { ok: false, status, installedVersion, availableVersion, source: source.id, restartRequired: false, message: `Prodero update ${status}: ${detail}` };
319
+ }
320
+ //# sourceMappingURL=update.js.map
@@ -0,0 +1,92 @@
1
+ /** App review is a workflow safeguard, not authentication of human consent.
2
+ * The adapter validates the saved review before issuing an exact Core grant;
3
+ * MCP Apps visibility/capability does not authenticate a physical user action.
4
+ * Future interactive text confirmation can call this same executor after its own
5
+ * protocol response; the public text gateway must never dispatch here directly. */
6
+ import { assertSafeCurationData } from './core/curation-evidence.js';
7
+ import { withSyncLock, recoverSyncTransaction } from './core/sync-transaction.js';
8
+ import { listCheckpoints } from './core/storage.js';
9
+ import { buildIndex } from './core/index.js';
10
+ import { planKnowledgeCuration, curationReviewEntries, applyKnowledgeCuration, StaleCurationPlanError } from './core/knowledge-curation.js';
11
+ import { withTrustedKnowledgeBatch } from './core/knowledge-authority.js';
12
+ import { curationReceiptProjection, curationReviewView, assertCurationHead, ensureCurationSuccessor, validateRevisionReceipt, reserveSuccessorCapacity, loadCurationReview, selectedCurationProposals, curationOperationId, CurationReviewError } from './core/curation-review.js';
13
+ import { validateRevision, deterministicBaseline, curationSelectionHash, selectionProblems, revisionHash } from './core/curation-revision.js';
14
+ import { semanticReviewStatus } from './core/curation-semantic-jobs.js';
15
+ import path from 'node:path';
16
+ import fs from 'node:fs/promises';
17
+ export async function executeCurationReview(root, input) {
18
+ const selected = [...input.selectedIds].sort();
19
+ if (!/^review_[a-zA-Z0-9_]+$/.test(input.reviewId) || !selected.length || new Set(selected).size !== selected.length)
20
+ throw new CurationReviewError('invalid_selection', 'Select a valid, non-empty set of suggestions.');
21
+ const operationId = curationOperationId(input.reviewId, selected, input.reviewRevisionId, input.selectionHash);
22
+ return withSyncLock(root, async () => {
23
+ await recoverSyncTransaction(root);
24
+ const history = await listCheckpoints(root);
25
+ const prior = history.find(c => c.curation?.review?.operationId === operationId);
26
+ const result = (receipt, checkpointId, replayed) => ({ status: 'applied', ...receipt, checkpointId, appliedGroupCount: receipt.reviewSnapshot?.units.filter(u => u.proposalIds.every(id => receipt.applied.includes(id))).length ?? receipt.applied.length, historyPath: path.join(path.resolve(root), '.prodero', 'checkpoints', `${checkpointId}.json`), replayed });
27
+ if (prior) {
28
+ const stored = prior.curation;
29
+ assertSafeCurationData(stored);
30
+ if (stored.plan.project !== path.resolve(root) || stored.review.planHash !== input.planHash || stored.review.projectIdentity !== await fs.realpath(root))
31
+ throw new CurationReviewError('review_changed', 'This request does not match the completed review.');
32
+ if (stored.review.reviewRevisionId) {
33
+ validateRevisionReceipt(stored.review);
34
+ if (input.reviewRevisionId !== stored.review.reviewRevisionId || input.decisionRevision !== stored.review.decisionRevision || input.selectionHash !== stored.review.selectionHash || JSON.stringify(selected) !== JSON.stringify(stored.review.selectedIds))
35
+ throw new CurationReviewError('review_changed', 'This request does not match the completed revision.');
36
+ }
37
+ await buildIndex(root);
38
+ const successor = await ensureCurationSuccessor(root, stored.review);
39
+ return { ...result(stored.review, prior.id, true), successor, projection: successor ? (await curationReviewView(root, successor.snapshot.plan, successor)).projection : curationReceiptProjection(stored.plan, stored.review) };
40
+ }
41
+ const review = await loadCurationReview(root, input.reviewId);
42
+ if (review.snapshot.plan.memorySyncSources)
43
+ throw new CurationReviewError('invalid_selection', 'Sync analysis is read-only; continue its owning Sync Memory operation.');
44
+ if (review.snapshot.planHash !== input.planHash)
45
+ throw new CurationReviewError('stale_review', 'Knowledge changed since this review.');
46
+ if (review.state.revision !== input.decisionRevision)
47
+ throw new CurationReviewError('review_changed', 'Your review changed in another window. Reload your decisions.');
48
+ const revision = review.snapshot.revision;
49
+ if (revision) {
50
+ validateRevision(revision);
51
+ if (input.reviewRevisionId !== revision.id)
52
+ throw new CurationReviewError('stale_review', 'Apply requires the exact review revision.');
53
+ if (input.selectionHash !== curationSelectionHash(revision.id, input.decisionRevision, selected))
54
+ throw new CurationReviewError('invalid_selection', 'Apply selection hash changed.');
55
+ if (history.some(c => c.curation?.review?.reviewRevisionId === revision.id))
56
+ throw new CurationReviewError('stale_review', 'This revision was applied. Review its successor.');
57
+ }
58
+ else if (input.reviewRevisionId || input.selectionHash)
59
+ throw new CurationReviewError('invalid_review', 'Legacy review has no revision binding.');
60
+ if (revision)
61
+ await assertCurationHead(root, review);
62
+ const derived = selectedCurationProposals(review);
63
+ if (revision ? selected.some(id => !derived.includes(id)) : JSON.stringify(derived) !== JSON.stringify(selected))
64
+ throw new CurationReviewError('invalid_selection', 'Apply must match your saved review decisions.');
65
+ if (revision) {
66
+ const problems = selectionProblems(revision.dependencies, selected);
67
+ if (problems.length)
68
+ throw new CurationReviewError('dependency_blocked', 'Selected changes require unresolved information or additional reviewed groups.', { problems });
69
+ }
70
+ if (revision) {
71
+ const semantic = await semanticReviewStatus(root, review);
72
+ if (revision.dependencies.groups.some(g => semantic.blockedGroupIds.includes(g.id) && g.proposalIds.some(id => selected.includes(id))))
73
+ throw new CurationReviewError('dependency_blocked', 'Selected changes await semantic analysis or updated answers.');
74
+ }
75
+ // Validate selection, including dependency closure, independently of the UI.
76
+ const entries = curationReviewEntries(review.snapshot.plan, selected, revision);
77
+ const current = await planKnowledgeCuration(root).catch(() => { throw new CurationReviewError('stale_review', 'Knowledge can no longer be validated against this review. Review updated changes.'); });
78
+ if (current.planId !== (revision ? deterministicBaseline(revision).planId : review.snapshot.planHash) || current.fingerprint !== review.snapshot.plan.fingerprint)
79
+ throw new CurationReviewError('stale_review', 'Knowledge changed since this review.');
80
+ if (revision)
81
+ await reserveSuccessorCapacity(root);
82
+ const receipt = { ...(revision ? { reviewRevisionId: revision.id, selectionHash: input.selectionHash, reviewSnapshot: review.snapshot, reviewState: review.state, executionHash: revisionHash([review.snapshot.snapshotHash, review.state, selected]) } : {}), projectIdentity: review.snapshot.projectIdentity, decisions: review.state.decisions, reviewId: input.reviewId, planHash: input.planHash, operationId, decisionRevision: input.decisionRevision, selectedIds: selected, applied: selected, unselected: review.snapshot.plan.proposals.filter(p => !selected.includes(p.id) && !p.attention.length).map(p => p.id), unresolved: review.snapshot.plan.proposals.filter(p => p.attention.length).map(p => p.id) };
83
+ const applied = await withTrustedKnowledgeBatch(root, entries, `app-review:${operationId}`, () => applyKnowledgeCuration(root, review.snapshot.plan, selected, receipt)).catch(error => {
84
+ if (error instanceof StaleCurationPlanError)
85
+ throw new CurationReviewError('stale_review', 'Knowledge changed since this review.');
86
+ throw error;
87
+ });
88
+ const successor = await ensureCurationSuccessor(root, receipt);
89
+ return { ...result(receipt, applied.checkpoint, false), successor, projection: successor ? (await curationReviewView(root, successor.snapshot.plan, successor)).projection : curationReceiptProjection(review.snapshot.plan, receipt) };
90
+ }, { waitMs: 2000 });
91
+ }
92
+ //# sourceMappingURL=curation-review-adapter.js.map
@@ -0,0 +1,54 @@
1
+ import { publicDiagnostic } from "./core/diagnostics.js";
2
+ // Preserve only known protocol states, with fixed public prose. Never forward
3
+ // arbitrary plugin error payloads or client-supplied diagnostics.
4
+ const reviewMessages = {
5
+ semantic_invalid: 'Semantic reasoning input or saved state is invalid.',
6
+ semantic_limit: 'Semantic reasoning reached its bounded limit. Saved state was preserved.',
7
+ semantic_stale: 'Semantic reasoning input is obsolete. Inspect the current review.',
8
+ dependency_blocked: 'Selected changes require unresolved information or additional reviewed groups.',
9
+ unsupported_review_schema: 'This review uses a newer format. Update Prodero.',
10
+ review_retention_full: 'Saved review decisions reached the retention limit. No decisions were deleted.',
11
+ review_paused: 'Resume this review before applying changes.',
12
+ stale_review: 'Knowledge changed since this review.',
13
+ invalid_review: 'Review is unavailable. Analyze knowledge again.',
14
+ invalid_selection: 'Apply must match valid saved review decisions and complete groups.',
15
+ project_mismatch: 'Review belongs to another project.',
16
+ review_changed: 'Your review changed in another window. Reload your decisions.',
17
+ review_too_large: 'Review is too large. Reduce the review scope first.',
18
+ recovery_required: 'Finish the pending transaction before starting a new review.',
19
+ review_unavailable: 'This connection does not advertise MCP Apps support. Knowledge review is preview-only.',
20
+ };
21
+ /** Wrap the public request registration API, including SDK validation before tool handlers. */
22
+ export function installDiagnosticBoundary(server) {
23
+ const register = server.server.setRequestHandler.bind(server.server);
24
+ server.server.setRequestHandler = (schema, handler) => register(schema, async (request, extra) => {
25
+ try {
26
+ const result = await handler(request, extra);
27
+ if (result && typeof result === "object" && "isError" in result && result.isError) {
28
+ const raw = result;
29
+ const state = raw.structuredContent;
30
+ if (state?.action === 'curation-review-error' && typeof state.code === 'string' && Object.hasOwn(reviewMessages, state.code)) {
31
+ const message = reviewMessages[state.code];
32
+ return { isError: true, content: [{ type: 'text', text: message }], structuredContent: { ok: false, action: 'curation-review-error', code: state.code, error: message, message, ...(state.code === 'dependency_blocked' ? { details: safeDependencyDetails(state.details) } : {}) } };
33
+ }
34
+ const message = raw.structuredContent?.error ?? raw.content?.find(item => item.type === "text")?.text;
35
+ const safe = publicDiagnostic(message);
36
+ // Drop arbitrary plugin error payloads too: they may contain rejected input.
37
+ return { isError: true, content: [{ type: "text", text: safe }], structuredContent: { ok: false, error: safe, message: safe, ...(safe.startsWith("POLICY_REVIEW_REQUIRED:") ? { policyTransition: { decision: "review_required", blocking: true, preserved: true } } : {}) } };
38
+ }
39
+ return result;
40
+ }
41
+ catch (error) {
42
+ if (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' && Object.hasOwn(reviewMessages, error.code)) {
43
+ const message = reviewMessages[error.code];
44
+ return { isError: true, content: [{ type: 'text', text: message }], structuredContent: { ok: false, action: 'curation-review-error', code: error.code, error: message, message } };
45
+ }
46
+ throw new Error(publicDiagnostic(error));
47
+ }
48
+ });
49
+ }
50
+ function safeDependencyDetails(value) {
51
+ const problems = value?.problems;
52
+ return { problems: Array.isArray(problems) ? problems.slice(0, 1000).flatMap((raw) => typeof raw?.groupId === 'string' && /^item_[a-f0-9]{24}$/.test(raw.groupId) && ['incomplete', 'blocked', 'requires', 'partial_group'].includes(raw.reason) ? [{ groupId: raw.groupId, reason: raw.reason, missing: Array.isArray(raw.missing) ? raw.missing.filter((v) => typeof v === 'string' && /^(?:item|issue)_[a-f0-9]{24}$/.test(v)) : [] }] : []) : [] };
53
+ }
54
+ //# sourceMappingURL=diagnostic-boundary.js.map
@@ -0,0 +1,39 @@
1
+ import fs from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ // Shared with host integration state. Windows uses its native ACL/inheritance model.
4
+ export const PRIVATE_DIRECTORY_MODE = 0o700;
5
+ export const PRIVATE_FILE_MODE = 0o600;
6
+ /** Tighten only the explicitly addressed inode, never a linked file or another owner's data. */
7
+ export async function tightenPrivateMode(target) {
8
+ if (process.platform === 'win32')
9
+ return;
10
+ const stat = await fs.lstat(target).catch(error => { if (error.code === 'ENOENT')
11
+ return null; throw error; });
12
+ if (!stat)
13
+ return;
14
+ // A concurrent lock release can unlink an inode while lstat is resolving it.
15
+ // Accept only a vanished, owned regular file; any replacement still fails closed.
16
+ if (stat.isFile() && stat.nlink === 0 && process.getuid && stat.uid === process.getuid()) {
17
+ const remaining = await fs.lstat(target).catch(error => { if (error.code === 'ENOENT')
18
+ return null; throw error; });
19
+ if (!remaining)
20
+ return;
21
+ }
22
+ if (stat.isSymbolicLink() || (!stat.isDirectory() && !stat.isFile()) || (stat.isFile() && stat.nlink !== 1)
23
+ || (process.getuid && stat.uid !== process.getuid()))
24
+ throw new Error('Unsafe private state permissions target');
25
+ const mode = stat.isDirectory() ? PRIVATE_DIRECTORY_MODE : PRIVATE_FILE_MODE;
26
+ if ((stat.mode & 0o7777) === mode)
27
+ return;
28
+ const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW);
29
+ try {
30
+ const current = await handle.stat();
31
+ if (current.dev !== stat.dev || current.ino !== stat.ino || current.nlink !== stat.nlink || current.uid !== stat.uid)
32
+ throw new Error('Private state changed while tightening permissions');
33
+ await handle.chmod(mode);
34
+ }
35
+ finally {
36
+ await handle.close();
37
+ }
38
+ }
39
+ //# sourceMappingURL=permissions.js.map