@prodero/curator 0.1.0 → 0.2.1
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 +1 -1
- package/CHANGELOG.md +25 -2
- package/README.md +32 -33
- package/dist/cli.js +58 -20
- package/dist/contracts.js +1 -0
- package/dist/conversation-presentation.js +62 -27
- package/dist/core/agent-instructions.js +31 -18
- package/dist/core/curation-graphical.js +22 -10
- package/dist/core/curation-question.js +28 -0
- package/dist/core/curation-semantic-contract.js +4 -1
- package/dist/core/curation-semantic-jobs.js +78 -18
- package/dist/core/knowledge-graph.js +5 -4
- package/dist/core/local-preferences.js +76 -0
- package/dist/core/memory-index.js +2 -0
- package/dist/core/memory-link-repair.js +6 -2
- package/dist/core/memory-sync-curator.js +25 -5
- package/dist/core/memory-sync.js +124 -29
- package/dist/core/npm-update.js +289 -0
- package/dist/core/project.js +8 -8
- package/dist/core/session-index.js +10 -6
- package/dist/core/sessions.js +3 -3
- package/dist/core/settings.js +34 -16
- package/dist/core/skills.js +21 -2
- package/dist/core/storage.js +55 -1
- package/dist/core/sync-transaction.js +85 -49
- package/dist/core/update.js +9 -3
- package/dist/curation-review-adapter.js +1 -1
- package/dist/diagnostic-boundary.js +4 -3
- package/dist/memory-presentation.js +26 -16
- package/dist/memory-sync-operation.js +88 -0
- package/dist/presentation-server.js +25 -10
- package/dist/presentation.js +92 -61
- package/dist/server.js +59 -51
- package/dist/settings-definition.js +3 -3
- 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 +117 -115
- package/docs/COMPATIBILITY.md +36 -15
- package/docs/MIGRATIONS.md +4 -0
- package/docs/RECOVERY.md +3 -1
- package/docs/RELEASING.md +69 -11
- package/docs/hosts.md +25 -7
- 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/ui-themes.md +10 -4
- package/instructions/AGENTS.md +24 -18
- package/package.json +6 -2
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { parseKnowledge } from './memory.js';
|
|
10
10
|
import { parseSkill } from './skills.js';
|
|
11
|
-
import { readConfig, checkedDataPath, exists, parseCheckpointData, ensureRuntimeDirectories, ensureDataDirectory, sha256, writeFileAtomic } from './storage.js';
|
|
11
|
+
import { readConfig, checkedDataPath, exists, parseCheckpointData, ensureRuntimeDirectories, ensureDataDirectory, ensureSelfIgnoredPrivateDirectory, sha256, writeFileAtomic } from './storage.js';
|
|
12
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
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
14
|
const journalSchema = z.discriminatedUnion('version', [
|
|
@@ -45,67 +45,98 @@ export function ownsSyncLock(root) {
|
|
|
45
45
|
const owner = writerContext.getStore();
|
|
46
46
|
return Boolean(owner?.active && owner.root === path.resolve(root));
|
|
47
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');
|
|
48
82
|
/** Per-worktree writer exclusion shared by migration, Sync and session mutations. */
|
|
49
83
|
export async function withSyncLock(root, operation, options = {}) {
|
|
50
|
-
|
|
84
|
+
if (options.maintainIntegration === false)
|
|
85
|
+
await ensureSelfIgnoredPrivateDirectory(root, '.transactions');
|
|
86
|
+
else
|
|
87
|
+
await runtimeDirectory(root);
|
|
51
88
|
const lock = await checkedDataPath(root, '.transactions/lock');
|
|
52
89
|
const deadline = Date.now() + (options.waitMs ?? 0);
|
|
90
|
+
let identity;
|
|
53
91
|
while (true) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
await writeFileExclusive(await checkedDataPath(root, '.transactions/lock/owner'), String(process.pid));
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
await fs.rm(lock, { recursive: true, force: true });
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
break;
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
if (error.code !== 'EEXIST')
|
|
67
|
-
throw error;
|
|
68
|
-
await checkedDataPath(root, '.transactions/lock/owner');
|
|
69
|
-
// Only one stale-owner reaper may inspect/remove a lock. Re-read under that guard
|
|
70
|
-
// so a second reaper cannot delete a newly acquired writer's lock.
|
|
71
|
-
const reaper = await checkedDataPath(root, '.transactions/lock-recovery');
|
|
72
|
-
let reaping = false, recovered = false;
|
|
92
|
+
const attempt = await tryLockTransition(root, async () => {
|
|
93
|
+
let created = false;
|
|
73
94
|
try {
|
|
95
|
+
await fs.mkdir(lock, { mode: PRIVATE_DIRECTORY_MODE });
|
|
96
|
+
created = true;
|
|
97
|
+
const acquired = await fs.lstat(lock);
|
|
74
98
|
try {
|
|
75
|
-
await
|
|
76
|
-
reaping = true;
|
|
99
|
+
await writeFileExclusive(await checkedDataPath(root, '.transactions/lock/owner'), String(process.pid));
|
|
77
100
|
}
|
|
78
101
|
catch (error) {
|
|
79
|
-
|
|
80
|
-
|
|
102
|
+
await removeOwnedLock(root, acquired);
|
|
103
|
+
throw error;
|
|
81
104
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 };
|
|
96
126
|
}
|
|
97
127
|
}
|
|
128
|
+
return { identity: null, recovered: false };
|
|
98
129
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (recovered)
|
|
104
|
-
continue;
|
|
105
|
-
if (Date.now() >= deadline)
|
|
106
|
-
throw new Error('Another Sync or storage writer is already running, or its lock has no live owner; inspect .prodero/.transactions/lock before retrying');
|
|
107
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
130
|
+
});
|
|
131
|
+
if (attempt?.value.identity) {
|
|
132
|
+
identity = attempt.value.identity;
|
|
133
|
+
break;
|
|
108
134
|
}
|
|
135
|
+
if (attempt?.value.recovered)
|
|
136
|
+
continue;
|
|
137
|
+
if (Date.now() >= deadline)
|
|
138
|
+
throw writerBusy();
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
109
140
|
}
|
|
110
141
|
try {
|
|
111
142
|
if (!options.migrationRecovery && await exists(await checkedDataPath(root, '.transactions/migration.json')))
|
|
@@ -133,7 +164,12 @@ export async function withSyncLock(root, operation, options = {}) {
|
|
|
133
164
|
}
|
|
134
165
|
}
|
|
135
166
|
finally {
|
|
136
|
-
|
|
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
|
+
}
|
|
137
173
|
}
|
|
138
174
|
}
|
|
139
175
|
async function validateWriteContent(root, writes) {
|
package/dist/core/update.js
CHANGED
|
@@ -7,6 +7,8 @@ import path from "node:path";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { PRODERO_SCHEMA_VERSION, PRODERO_VERSION } from "./constants.js";
|
|
9
9
|
import { writeFileAtomic } from "./storage.js";
|
|
10
|
+
import { MANUAL_UPDATE_COMMAND, UPDATE_INSTRUCTIONS } from '../update-instructions.js';
|
|
11
|
+
export { MANUAL_UPDATE_COMMAND } from '../update-instructions.js';
|
|
10
12
|
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
13
|
const versionSchema = z.string().regex(semverPattern, "Expected a valid semantic version");
|
|
12
14
|
export const releaseMetadataSchema = z.object({
|
|
@@ -17,7 +19,11 @@ export const releaseMetadataSchema = z.object({
|
|
|
17
19
|
notes: z.string().max(4_000).optional(),
|
|
18
20
|
artifact: z.object({ locator: z.string().trim().min(1), integrity: z.string().regex(/^sha256:[a-f0-9]{64}$/) }).strict(),
|
|
19
21
|
}).strict();
|
|
20
|
-
|
|
22
|
+
/** MCP/App instructions are inert for every release source, including dev adapters. */
|
|
23
|
+
export function updateInstructions(source = { id: 'npm' }, installedVersion = PRODERO_VERSION) {
|
|
24
|
+
return { ok: true, action: 'update-instructions', instructionOnly: true, status: 'manual-update',
|
|
25
|
+
installedVersion, availableVersion: null, source: source.id, restartRequired: false, command: MANUAL_UPDATE_COMMAND, message: UPDATE_INSTRUCTIONS };
|
|
26
|
+
}
|
|
21
27
|
export class ReleaseSourceUnavailableError extends Error {
|
|
22
28
|
}
|
|
23
29
|
function parseVersion(value) {
|
|
@@ -113,7 +119,7 @@ async function runLocalBuild(root, signal) {
|
|
|
113
119
|
});
|
|
114
120
|
});
|
|
115
121
|
}
|
|
116
|
-
/**
|
|
122
|
+
/** Legacy internal development build adapter. Never invoked by MCP update instructions. */
|
|
117
123
|
export class LocalCheckoutReleaseSource {
|
|
118
124
|
root;
|
|
119
125
|
id = "local-checkout";
|
|
@@ -275,7 +281,7 @@ export async function installLatest(source = configuredReleaseSource(), installe
|
|
|
275
281
|
if (source instanceof NpmReleaseSource || source instanceof UnconfiguredReleaseSource) {
|
|
276
282
|
const check = await checkForUpdate(source, installedVersion);
|
|
277
283
|
return { ok: true, status: "manual-update", installedVersion, availableVersion: check.latest, source: source.id, restartRequired: false,
|
|
278
|
-
message: `${check.error ? `${check.error} ` : ""}
|
|
284
|
+
message: `${check.error ? `${check.error} ` : ""}${UPDATE_INSTRUCTIONS}` };
|
|
279
285
|
}
|
|
280
286
|
let raw;
|
|
281
287
|
try {
|
|
@@ -40,7 +40,7 @@ export async function executeCurationReview(root, input) {
|
|
|
40
40
|
}
|
|
41
41
|
const review = await loadCurationReview(root, input.reviewId);
|
|
42
42
|
if (review.snapshot.plan.memorySyncSources)
|
|
43
|
-
throw new CurationReviewError('invalid_selection', 'Sync analysis is read-only; continue its owning
|
|
43
|
+
throw new CurationReviewError('invalid_selection', 'Sync analysis is read-only; continue its owning Curate Memory operation.');
|
|
44
44
|
if (review.snapshot.planHash !== input.planHash)
|
|
45
45
|
throw new CurationReviewError('stale_review', 'Knowledge changed since this review.');
|
|
46
46
|
if (review.state.revision !== input.decisionRevision)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { renderTextError } from './text-layout.js';
|
|
1
2
|
import { publicDiagnostic } from "./core/diagnostics.js";
|
|
2
3
|
// Preserve only known protocol states, with fixed public prose. Never forward
|
|
3
4
|
// arbitrary plugin error payloads or client-supplied diagnostics.
|
|
@@ -29,19 +30,19 @@ export function installDiagnosticBoundary(server) {
|
|
|
29
30
|
const state = raw.structuredContent;
|
|
30
31
|
if (state?.action === 'curation-review-error' && typeof state.code === 'string' && Object.hasOwn(reviewMessages, state.code)) {
|
|
31
32
|
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
|
+
return { isError: true, content: [{ type: 'text', text: renderTextError(message) }], structuredContent: { ok: false, action: 'curation-review-error', code: state.code, error: message, message, ...(state.code === 'dependency_blocked' ? { details: safeDependencyDetails(state.details) } : {}) } };
|
|
33
34
|
}
|
|
34
35
|
const message = raw.structuredContent?.error ?? raw.content?.find(item => item.type === "text")?.text;
|
|
35
36
|
const safe = publicDiagnostic(message);
|
|
36
37
|
// 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
|
+
return { isError: true, content: [{ type: "text", text: renderTextError(safe) }], structuredContent: { ok: false, error: safe, message: safe, ...(safe.startsWith("POLICY_REVIEW_REQUIRED:") ? { policyTransition: { decision: "review_required", blocking: true, preserved: true } } : {}) } };
|
|
38
39
|
}
|
|
39
40
|
return result;
|
|
40
41
|
}
|
|
41
42
|
catch (error) {
|
|
42
43
|
if (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' && Object.hasOwn(reviewMessages, error.code)) {
|
|
43
44
|
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
|
+
return { isError: true, content: [{ type: 'text', text: renderTextError(message) }], structuredContent: { ok: false, action: 'curation-review-error', code: error.code, error: message, message } };
|
|
45
46
|
}
|
|
46
47
|
throw new Error(publicDiagnostic(error));
|
|
47
48
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
/** Pure product copy shared by Open, Curate Memory and text clients. Core owns decisions. */
|
|
2
|
+
import { memorySyncOperation } from './memory-sync-operation.js';
|
|
1
3
|
const count = (n, one, many = one + 's') => `${n} ${n === 1 ? one : many}`;
|
|
2
4
|
const tokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(2)}K` : String(n);
|
|
3
5
|
export const MEMORY_COPY = {
|
|
4
6
|
automatic: 'Prodero remembers automatically. Sessions save useful knowledge into Local Knowledge when they close.',
|
|
5
|
-
purpose: '
|
|
7
|
+
purpose: 'Curate Memory keeps that memory useful and shareable.',
|
|
6
8
|
noHistory: 'Not enough usage history yet to evaluate memory optimization.',
|
|
7
|
-
stale: 'Project memory changed since this
|
|
9
|
+
stale: 'Project memory changed since this curation started. Re-analyze before continuing.',
|
|
10
|
+
incompleteProvenance: 'Some provenance fields are incomplete; this is not proof of lost history. Existing records are preserved. Verify actual sources before proposing repairs; diagnostics do not infer or backfill provenance.',
|
|
8
11
|
shared: 'Versioned Knowledge is Git-shareable project state that your agent can change. Curator does not commit or push; review the repository diff before doing so.',
|
|
9
12
|
};
|
|
10
13
|
/** Display only: never infer a topic from a knowledge title. Full labels remain in text/details. */
|
|
@@ -41,7 +44,7 @@ export function memoryOpenPresentation(p) {
|
|
|
41
44
|
const headline = !o?.available ? 'Memory overview unavailable' : areas.length ? `${count(areas.length, 'area')} can be improved` : hasHistory ? 'No improvements suggested by recent usage' : 'Memory is ready to learn';
|
|
42
45
|
const notes = [MEMORY_COPY.automatic, MEMORY_COPY.purpose];
|
|
43
46
|
if (!o?.available)
|
|
44
|
-
notes.push('Memory analysis is temporarily unavailable. Try
|
|
47
|
+
notes.push('Memory analysis is temporarily unavailable. Try Curate Memory again when current work has finished.');
|
|
45
48
|
else if (!hasHistory)
|
|
46
49
|
notes.push(MEMORY_COPY.noHistory);
|
|
47
50
|
else if (!areas.length)
|
|
@@ -53,31 +56,32 @@ export function memoryOpenPresentation(p) {
|
|
|
53
56
|
if (areas.some((a) => a.reasons.some((r) => r.includes('did not fit'))))
|
|
54
57
|
notes.push('Some relevant knowledge has not fit into recent task context.');
|
|
55
58
|
if (o?.areasOmitted)
|
|
56
|
-
notes.push(`${count(o.areasOmitted, 'additional area')} can be inspected in a later
|
|
59
|
+
notes.push(`${count(o.areasOmitted, 'additional area')} can be inspected in a later Curate Memory run.`);
|
|
57
60
|
const workflow = o?.workflow;
|
|
58
61
|
if (workflow)
|
|
59
|
-
notes.push(workflow.state === 'stale' ? MEMORY_COPY.stale : 'Memory
|
|
62
|
+
notes.push(workflow.state === 'stale' ? MEMORY_COPY.stale : 'Curate Memory is in progress. Continue when you are ready.');
|
|
60
63
|
const signal = areas.some((a) => a.reasons.some((r) => r.includes('did not fit'))) ? 'Relevant knowledge has not fit into recent task context.'
|
|
61
64
|
: areas.length ? areas[0].reasons.find((r) => !r.includes('incompatible')) ?? 'Related knowledge is ready for review.'
|
|
62
65
|
: !o?.available ? 'Memory analysis is temporarily unavailable.' : !hasHistory ? 'Not enough usage history yet to evaluate improvements.' : 'No improvements suggested by recent usage.';
|
|
63
66
|
const total = areas.length + (o?.areasOmitted ?? 0);
|
|
64
67
|
const dashboard = { headline: areas.length ? 'Memory can be improved' : headline, areasLabel: total ? `${count(total, 'area')} identified` : null, signal,
|
|
65
68
|
sharing: typeof p.localKnowledgeCount === 'number' ? p.localKnowledgeCount ? `${count(p.localKnowledgeCount, 'Local memory', 'Local memories')} available for sharing review.` : 'No Local memories to share.' : null,
|
|
66
|
-
established: p.knowledgeCount > 0, workflowNotice: workflow ? workflow.state === 'stale' ? MEMORY_COPY.stale : 'Memory
|
|
67
|
-
return { headline, notes, areas, conflicts, workflow, dashboard, button: workflow ? (workflow.state === 'stale' ? 'Re-analyze memory' : 'Continue
|
|
69
|
+
established: p.knowledgeCount > 0, workflowNotice: workflow ? workflow.state === 'stale' ? MEMORY_COPY.stale : 'Curate Memory is in progress.' : null };
|
|
70
|
+
return { headline, notes, areas, conflicts, workflow, dashboard, button: workflow ? (workflow.state === 'stale' ? 'Re-analyze memory' : 'Continue curation') : 'Curate Memory' };
|
|
68
71
|
}
|
|
69
72
|
export function memorySyncPresentation(w) {
|
|
70
73
|
const areas = (w.jobs ?? []).map(memoryArea), s = w.summary ?? {}, p = w.review?.preview;
|
|
71
|
-
const
|
|
74
|
+
const operation = w.operation ?? memorySyncOperation(w);
|
|
75
|
+
const headlines = { awaiting_selection: 'Curate Memory', semantic_work: 'Improving memory…', awaiting_clarification: 'A question about your project', awaiting_review: w.review?.kind === 'promotion' ? 'Review knowledge for team sharing' : p?.requiresVersionedReview ? 'Review shared-memory changes' : 'Review memory changes', applying: 'Approved changes pending verification', awaiting_promotion_selection: 'Review Local knowledge for sharing', completed: 'Memory curated', stale: 'Re-analyze memory' };
|
|
72
76
|
const notes = [], warnings = [], metrics = [], sections = [];
|
|
73
77
|
const section = (title, body) => sections.push({ title, items: [{ title: '', body, notes: [] }] });
|
|
74
78
|
if (w.state === 'awaiting_selection') {
|
|
75
79
|
// Operation choices are the entry surface; planner data is diagnostic.
|
|
76
80
|
}
|
|
77
|
-
if (w.state
|
|
78
|
-
notes.push('
|
|
81
|
+
if (['semantic_work', 'awaiting_clarification'].includes(w.state) && operation.phase !== 'question')
|
|
82
|
+
notes.push(operation.phase === 'submitted' ? 'The result was submitted and is awaiting validation.' : operation.phase === 'awaiting_agent' ? 'Prepared evidence is waiting for host-agent analysis. No background worker is confirmed.' : 'Analysis is queued. Continue with the prepared evidence; you may be asked about missing project facts.');
|
|
79
83
|
if (w.state === 'applying')
|
|
80
|
-
notes.push('
|
|
84
|
+
notes.push('The approved intention is saved. Check progress to verify the result or resume recovery; no active writer is assumed.');
|
|
81
85
|
if (w.state === 'stale')
|
|
82
86
|
notes.push(MEMORY_COPY.stale);
|
|
83
87
|
if (w.state === 'awaiting_promotion_selection')
|
|
@@ -117,7 +121,7 @@ export function memorySyncPresentation(w) {
|
|
|
117
121
|
if (p.requiresVersionedReview)
|
|
118
122
|
notes.push(MEMORY_COPY.shared);
|
|
119
123
|
if (p.remaining)
|
|
120
|
-
notes.push(`${count(p.remaining, 'additional broken link')} will remain for a later
|
|
124
|
+
notes.push(`${count(p.remaining, 'additional broken link')} will remain for a later curation run.`);
|
|
121
125
|
sections.push({ title: 'Broken links to remove', items: p.changes.map((c) => ({ title: c.title, body: c.links.map((l) => `${l.type} → ${l.target}`).join('\n'), notes: [`${c.layer === 'versioned' ? 'Shared / Versioned' : 'Local'} · ${c.id}`] })) });
|
|
122
126
|
}
|
|
123
127
|
if (w.state !== 'stale' && w.review?.kind === 'promotion') {
|
|
@@ -148,11 +152,11 @@ export function memorySyncPresentation(w) {
|
|
|
148
152
|
if (s.brokenLinksRemaining)
|
|
149
153
|
notes.push(`${count(s.brokenLinksRemaining, 'broken link')} remain unchanged.`);
|
|
150
154
|
if (s.promoted)
|
|
151
|
-
notes.push(`${count(s.promoted, 'Local memory', 'Local memories')}
|
|
155
|
+
notes.push(`${count(s.promoted, 'Local memory', 'Local memories')} prepared in Versioned files. Review the Git diff before sharing; Curator did not commit or push.`);
|
|
152
156
|
if (s.areasProcessed)
|
|
153
157
|
notes.push(`Estimated context for reviewed knowledge: ${tokens(s.estimatedTokens.before)} → ${tokens(s.estimatedTokens.after)} tokens.`);
|
|
154
158
|
if (s.unresolved)
|
|
155
|
-
notes.push(`${count(s.unresolved, '
|
|
159
|
+
notes.push(`${count(s.unresolved, 'unresolved item')} still need clarification and remain current.`);
|
|
156
160
|
if (s.areasSkipped)
|
|
157
161
|
notes.push('Some knowledge was retained because a safe improvement could not be established.');
|
|
158
162
|
if (s.areasFailed)
|
|
@@ -163,10 +167,16 @@ export function memorySyncPresentation(w) {
|
|
|
163
167
|
notes.push(`${count(s.promotionConflicts, 'sharing item')} still need review and remain Local.`);
|
|
164
168
|
notes.push('Previous knowledge remains available through history.');
|
|
165
169
|
}
|
|
170
|
+
notes.push('Your place and saved answers are retained. Leave and return with Continue curation.');
|
|
171
|
+
if (['semantic_work', 'awaiting_clarification', 'awaiting_review'].includes(w.state))
|
|
172
|
+
notes.push('Skip or Answer later retains original knowledge and saved answers, then continues other selected work.');
|
|
173
|
+
if (w.state === 'stale' || w.state === 'completed')
|
|
174
|
+
notes.push('Re-analyze starts a fresh workflow from current project data. Changes already saved remain in place.');
|
|
166
175
|
if (w.error && w.state !== 'stale')
|
|
167
176
|
notes.push('This step could not finish. Continue to check saved progress before retrying.');
|
|
168
177
|
if (w.state === 'awaiting_promotion_selection' && w.coverage?.promotionCandidatesOmitted)
|
|
169
|
-
notes.push(`${count(w.coverage.promotionCandidatesOmitted, 'additional Local memory', 'additional Local memories')} can be reviewed in a later
|
|
170
|
-
|
|
178
|
+
notes.push(`${count(w.coverage.promotionCandidatesOmitted, 'additional Local memory', 'additional Local memories')} can be reviewed in a later Curate Memory run.`);
|
|
179
|
+
const headline = w.state === 'completed' && operation.outcome === 'remaining' ? 'Completed with remaining work' : operation.phase === 'awaiting_agent' ? 'Awaiting agent analysis' : operation.phase === 'submitted' ? 'Submitted · validation pending' : w.state === 'semantic_work' ? 'Analysis queued' : headlines[w.state] ?? 'Curate Memory';
|
|
180
|
+
return { headline, graphicalHeadline: headline, operation, reviewAreaLabel: 'Memory changes', notes, warnings, metrics, sections, areas };
|
|
171
181
|
}
|
|
172
182
|
//# sourceMappingURL=memory-presentation.js.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** Pure derived Sync state/actions. No I/O, writes, host detection or authority. */
|
|
2
|
+
import { publicDiagnostic } from './core/diagnostics.js';
|
|
3
|
+
import { hasSecret } from './core/secrets.js';
|
|
4
|
+
/** Only returned Core diagnostics are screened for display. Raw transport
|
|
5
|
+
* exceptions are deliberately replaced with fixed client-owned wording. */
|
|
6
|
+
export function syncClientFailure(workflowId, stage, response, confirmedState = false) {
|
|
7
|
+
const payload = response?.structuredContent;
|
|
8
|
+
const rejected = stage === 'action' && Boolean(payload), code = payload && typeof payload.code === 'string' && /^[a-zA-Z0-9_-]{1,100}$/.test(payload.code) && !hasSecret(payload.code) ? payload.code : stage === 'handoff' ? 'SYNC_HANDOFF_UNKNOWN' : rejected ? 'SYNC_ACTION_REJECTED' : 'SYNC_TRANSPORT_UNKNOWN';
|
|
9
|
+
const cause = payload ? publicDiagnostic(typeof payload.message === 'string' ? payload.message : typeof payload.error === 'string' ? payload.error : 'The server rejected this step.').slice(0, 1000) : stage === 'handoff' ? 'Agent continuation outcome is unknown. Saved workflow changes remain confirmed.' : 'The action outcome is unknown because the host did not return a usable result.';
|
|
10
|
+
return { workflowId, stage, certainty: rejected && confirmedState ? 'rejected' : 'unknown', confirmedState, diagnostic: { code, cause, recovery: stage === 'handoff' ? 'Check saved progress or continue the saved request in chat.' : confirmedState ? 'Saved state refreshed. Review it before retrying; the action was not replayed.' : 'Check saved progress before retrying. Do not repeat approval while its outcome is unknown.' } };
|
|
11
|
+
}
|
|
12
|
+
export function syncClientNotice(evidence) {
|
|
13
|
+
if (!evidence)
|
|
14
|
+
return '';
|
|
15
|
+
return [evidence.diagnostic ? `${evidence.diagnostic.code}: ${evidence.diagnostic.cause} ${evidence.diagnostic.recovery}` : evidence.stage === 'handoff' ? evidence.certainty === 'requested' ? 'Continuation requested from the host; agent execution is not confirmed.' : evidence.certainty === 'acknowledged' ? 'The host acknowledged the request. Waiting for saved analysis; agent execution is not confirmed.' : '' : '', evidence.pollPaused ? 'Automatic checks paused after 12 attempts. Waiting for saved progress; use Check progress or continue the saved request in chat.' : ''].filter(Boolean).join(' ');
|
|
16
|
+
}
|
|
17
|
+
const finished = new Set(['applied', 'skipped', 'failed', 'deferred']);
|
|
18
|
+
export function memorySyncOperation(w, selection) {
|
|
19
|
+
const counts = { areasProcessed: 0, areasFailed: 0, areasSkipped: 0, questionsDeferred: 0, unresolved: 0, brokenLinksRemaining: 0, promotionConflicts: 0, ...w.summary };
|
|
20
|
+
const selected = (w.jobs ?? []).filter(j => j.selected), current = selected.find(j => ['pending', 'semantic_work', 'awaiting_review', 'awaiting_clarification'].includes(j.status)), reasoning = w.reasoning ?? w.clarification?.graphical;
|
|
21
|
+
const remaining = counts.areasFailed + counts.areasSkipped + counts.questionsDeferred + counts.unresolved + counts.brokenLinksRemaining + counts.promotionConflicts > 0;
|
|
22
|
+
let phase = { awaiting_selection: 'selection', semantic_work: 'queued', awaiting_clarification: 'question', awaiting_review: 'review', applying: 'recovery', awaiting_promotion_selection: 'sharing', completed: 'completed', stale: 'stale' }[w.state];
|
|
23
|
+
const pendingAnalysis = w.state === 'semantic_work' || w.state === 'awaiting_clarification' && !w.clarification?.graphical?.questions.some(q => q.needsInput);
|
|
24
|
+
if (pendingAnalysis && reasoning?.jobs.some(j => !j.suspended && ['prepared', 'awaiting_reasoning'].includes(j.status)))
|
|
25
|
+
phase = 'awaiting_agent';
|
|
26
|
+
if (pendingAnalysis && reasoning?.jobs.some(j => !j.suspended && j.status === 'submitted'))
|
|
27
|
+
phase = 'submitted';
|
|
28
|
+
const actions = [], args = { workflowId: w.id, revision: w.revision };
|
|
29
|
+
const add = (id, label, request, approval = false) => actions.push({ id, label, tool: 'prodero_memory_sync', arguments: { projectRoot: w.root, request }, ...(approval ? { approval: true } : {}) });
|
|
30
|
+
if (w.state === 'awaiting_selection' && (selection?.improve !== false || selection?.share !== false || Boolean(w.brokenLinks) && selection?.repairLinks !== false))
|
|
31
|
+
add('select', 'Start curation', { action: 'select', ...args, improve: selection?.improve ?? true, share: selection?.share ?? true, ...(w.brokenLinks ? { repairLinks: selection?.repairLinks ?? true } : {}) });
|
|
32
|
+
if (w.state === 'semantic_work' && current)
|
|
33
|
+
add('prepare', 'Continue improving memory', { action: 'prepare', ...args, candidateId: current.id });
|
|
34
|
+
if (w.state === 'semantic_work' && current)
|
|
35
|
+
add('skip', 'Continue with other knowledge', { action: 'skip', ...args, candidateId: current.id });
|
|
36
|
+
if (w.state === 'awaiting_review' && w.review) {
|
|
37
|
+
add('approve', w.review.kind === 'promotion' ? 'Share reviewed knowledge' : 'Approve reviewed changes', { action: 'approve', ...args, reviewId: w.review.reviewId, approved: true }, true);
|
|
38
|
+
add('skip', 'Skip this review', { action: 'skip', ...args, ...(w.review.kind === 'transformation' && current ? { candidateId: current.id } : {}) });
|
|
39
|
+
}
|
|
40
|
+
const availableIds = (w.promotionCandidates ?? []).map(c => c.id), ids = selection?.promotionIds ?? availableIds;
|
|
41
|
+
const validSelection = ids.length > 0 && new Set(ids).size === ids.length && ids.every(id => availableIds.includes(id));
|
|
42
|
+
if (w.state === 'awaiting_promotion_selection') {
|
|
43
|
+
if (validSelection)
|
|
44
|
+
add('promotions', ids.length === availableIds.length ? `Review all ${ids.length} for sharing` : `Review ${ids.length} selected for sharing`, { action: 'promotions', ...args, ids: [...ids] });
|
|
45
|
+
add('complete', 'Finish without sharing', { action: 'complete', ...args });
|
|
46
|
+
}
|
|
47
|
+
if (!['stale', 'applying'].includes(w.state) && !w.review && !w.promotionResult)
|
|
48
|
+
for (const job of selected.filter(j => j.status === 'failed'))
|
|
49
|
+
add(`retry:${job.id}`, 'Retry incomplete analysis', { action: 'retry', ...args, candidateId: job.id });
|
|
50
|
+
for (const q of w.deferredQuestions ?? []) {
|
|
51
|
+
const { candidateId, title, prompt, expired, affectedQuestions, ...binding } = q;
|
|
52
|
+
if (expired && w.review)
|
|
53
|
+
continue;
|
|
54
|
+
if (expired) {
|
|
55
|
+
const { action, questionId, questionVersion, ...exact } = binding;
|
|
56
|
+
add(`refresh:${candidateId}:${q.questionId}`, 'Refresh expired question review', { action: 'refresh_questions', ...args, candidateId, ...exact });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const app = { tool: 'prodero_curation_review', arguments: { projectRoot: w.root, ...binding } };
|
|
60
|
+
actions.push({ id: `revisit:${q.questionId}`, label: affectedQuestions.length > 1 ? `Revisit ${affectedQuestions.length} related questions` : `Revisit ${title}`, tool: 'prodero_curation_question', arguments: { projectRoot: w.root, ...binding, workflowId: w.id, candidateId }, app });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const data = w.clarification, review = data?.review, g = data?.graphical, owner = selected.find(j => j.curationReviewId === review?.snapshot.reviewId);
|
|
64
|
+
if (review?.snapshot.revision && g && owner) {
|
|
65
|
+
const binding = { workflowId: w.id, candidateId: owner.id, reviewId: review.snapshot.reviewId, reviewRevisionId: review.snapshot.revision.id, planHash: review.snapshot.planHash, decisionRevision: review.state.revision, answerLogHash: g.answerLogHash };
|
|
66
|
+
for (const q of g.questions.filter(q => q.needsInput || q.state === 'answered_pending_analysis')) {
|
|
67
|
+
if (q.needsInput && !g.analysisPending)
|
|
68
|
+
for (const choice of q.question.choices)
|
|
69
|
+
actions.push({ id: `answer:${q.question.id}:${choice.id}`, label: choice.label, tool: 'prodero_curation_question', arguments: { projectRoot: w.root, ...binding, action: 'answer', questionId: q.question.id, questionVersion: q.question.version, choiceId: choice.id, requestId: `chat_${globalThis.crypto.randomUUID()}`, ...(q.answer ? { replacesAnswerId: q.answer.id } : {}) } });
|
|
70
|
+
actions.push({ id: `later:${q.question.id}`, label: q.relatedQuestions && q.relatedQuestions.length > 1 ? `Defer ${q.relatedQuestions.length} related questions` : 'Answer later', tool: 'prodero_curation_question', arguments: { projectRoot: w.root, ...binding, action: 'question_later', questionId: q.question.id, questionVersion: q.question.version } });
|
|
71
|
+
}
|
|
72
|
+
if (g.canRetryAnalysis)
|
|
73
|
+
actions.push({ id: 'retry_analysis', label: 'Retry saved answer analysis', tool: 'prodero_curation_question', arguments: { projectRoot: w.root, ...binding, action: 'retry_analysis' } });
|
|
74
|
+
else if (g.needsPreparation)
|
|
75
|
+
actions.push({ id: 'analyze', label: 'Analyze saved answer', tool: 'prodero_curation_question', arguments: { projectRoot: w.root, ...binding, action: 'analyze' } });
|
|
76
|
+
}
|
|
77
|
+
if (reasoning?.continuationJobId)
|
|
78
|
+
actions.push({ id: 'reasoning', label: 'Continue bounded analysis', tool: 'prodero_curation_reasoning_prepare', arguments: { projectRoot: w.root, jobId: reasoning.continuationJobId } });
|
|
79
|
+
if (w.state === 'stale' || w.state === 'applying')
|
|
80
|
+
actions.length = 0;
|
|
81
|
+
if (w.state === 'stale' || w.state === 'completed')
|
|
82
|
+
add('restart', 'Re-analyze memory', { action: 'start', restart: true });
|
|
83
|
+
add('inspect', 'Check progress', { action: 'inspect', workflowId: w.id });
|
|
84
|
+
actions.push({ id: 'details', label: 'Show details', tool: 'prodero_memory_sync', arguments: { projectRoot: w.root, request: { action: 'inspect', workflowId: w.id }, details: true } });
|
|
85
|
+
const error = w.error ?? selected.find(j => j.status === 'failed' && j.error)?.error;
|
|
86
|
+
return { version: 1, phase, outcome: w.state === 'stale' ? 'stale' : w.state === 'completed' ? remaining ? 'remaining' : 'complete' : 'pending', counts: { ...counts, deferredAreas: counts.questionsDeferred, questionsDeferred: w.questionCounts?.deferred ?? null, areasTotal: selected.length, areasFinished: selected.filter(j => finished.has(j.status)).length, savedAnswers: w.questionCounts?.savedAnswerEvents ?? null, unsupersededAnswerEvents: w.questionCounts?.unsupersededAnswerEvents ?? null }, ...(current ? { currentItem: { id: current.id, label: current.label, status: current.status } } : {}), actions, ...(error ? { diagnostic: { code: 'SYNC_STEP_INCOMPLETE', cause: publicDiagnostic(error), recovery: 'Inspect saved progress before retrying.' } } : {}), ...(!['stale', 'applying'].includes(w.state) && reasoning?.continuationJobId ? { continuation: { jobId: reasoning.continuationJobId } } : {}), selection: { ids: validSelection ? [...ids] : [], availableIds } };
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=memory-sync-operation.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { projectSettings } from "./core/settings.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { registerAppTool, getUiCapability, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
|
|
4
|
-
import { buildViewModel, renderText, PRESENTATION_REPLY } from "./presentation.js";
|
|
4
|
+
import { buildViewModel, renderText, PRESENTATION_REPLY, LIFECYCLE_PRESENTATION_REPLY } from "./presentation.js";
|
|
5
5
|
import { createPresentationCards } from './presentation-cards.js';
|
|
6
6
|
import { conversationOperation } from './conversation-presentation.js';
|
|
7
7
|
import { PRODERO_TOOLS } from './contracts.js';
|
|
8
|
-
export const PRESENTED_TOOLS = ["prodero_initialize", "prodero_context", "prodero_curation", "prodero_migrate", "prodero_open", "prodero_status", "prodero_status_details", "prodero_prepare", "prodero_session_close", "prodero_knowledge", "prodero_knowledge_mutate", "prodero_instruction_review", "prodero_instruction_optimize", "prodero_session_review", "prodero_session_review_mutate", "prodero_promotion_preview", "prodero_promote", "prodero_sync", "prodero_memory_sync"];
|
|
9
|
-
const modeSchema = z.enum(["auto", "ui", "text"]).optional().describe("Optional invocation override; defaults to
|
|
8
|
+
export const PRESENTED_TOOLS = ["prodero_initialize", "prodero_context", "prodero_curation", "prodero_curation_question", "prodero_migrate", "prodero_open", "prodero_status", "prodero_status_details", "prodero_prepare", "prodero_session_close", "prodero_knowledge", "prodero_knowledge_mutate", "prodero_instruction_review", "prodero_instruction_optimize", "prodero_session_review", "prodero_session_review_mutate", "prodero_promotion_preview", "prodero_promote", "prodero_sync", "prodero_memory_sync", "prodero_skill_preview", "prodero_skill_review"];
|
|
9
|
+
const modeSchema = z.enum(["auto", "ui", "text"]).optional().describe("Optional invocation override; defaults to effective presentation.mode on this checkout (local preference, legacy project default, then built-in default). Auto uses advertised MCP Apps capability; UI prefers App; text supplies a compact conversational summary with details on request. Use prodero_text to guarantee no App attachment.");
|
|
10
10
|
export function resolvePresentation(mode, capabilities) {
|
|
11
11
|
return mode === "auto" ? (getUiCapability(capabilities)?.mimeTypes?.includes(RESOURCE_MIME_TYPE) ? "ui" : "text") : mode;
|
|
12
12
|
}
|
|
@@ -27,20 +27,23 @@ export function presentationTools(server, resolveRoot) {
|
|
|
27
27
|
const resolved = resolvePresentation(settings.presentation.mode, server.server.getClientCapabilities());
|
|
28
28
|
const group = groupSchema.parse(presentationGroup);
|
|
29
29
|
const card = hasUi && resolved === 'ui' && group ? cards.issue(settings.root, `conversation:${group}`) : undefined;
|
|
30
|
-
return { ...result, structuredContent: { ...result.structuredContent, projectSettings: { root: settings.root, presentation: settings.presentation }, presentation: { requested: settings.presentation.mode, resolved, scope: "project", ...(card ? { card } : {}) } } };
|
|
30
|
+
return { ...result, structuredContent: { ...result.structuredContent, projectSettings: { root: settings.root, presentation: settings.presentation, presentationSources: settings.presentationSources, diagnostics: settings.diagnostics }, presentation: { requested: settings.presentation.mode, resolved, scope: "project", ...(card ? { card } : {}) } } };
|
|
31
31
|
});
|
|
32
32
|
const schema = z.object(config.inputSchema);
|
|
33
33
|
const run = async (input, extra) => {
|
|
34
|
+
if (name === PRODERO_TOOLS.curationQuestion)
|
|
35
|
+
z.object({ ...config.inputSchema, presentation: modeSchema, presentationGroup: groupSchema, details: z.boolean().optional(), diagnostics: z.boolean().optional(), plain: z.boolean().optional() }).strict().parse(input);
|
|
34
36
|
const root = await resolveRoot(input.projectRoot);
|
|
35
37
|
const settings = await projectSettings(root);
|
|
36
38
|
const details = z.boolean().optional().parse(input.details);
|
|
37
39
|
const diagnostics = z.boolean().optional().parse(input.diagnostics);
|
|
40
|
+
const plain = z.boolean().optional().parse(input.plain);
|
|
38
41
|
const requested = modeSchema.parse(input.presentation) ?? settings.presentation.mode;
|
|
39
42
|
const presentationGroup = groupSchema.parse(input.presentationGroup);
|
|
40
43
|
const resolved = resolvePresentation(requested, server.server.getClientCapabilities());
|
|
41
44
|
// Parse using the original schema and strip presentation before invoking Core.
|
|
42
45
|
const args = schema.parse({ ...input, projectRoot: root });
|
|
43
|
-
const result = await handler(args, extra);
|
|
46
|
+
const result = await handler(args, extra, { requested, resolved });
|
|
44
47
|
if (result.isError || !result.structuredContent)
|
|
45
48
|
return result;
|
|
46
49
|
const payload = result.structuredContent;
|
|
@@ -49,12 +52,24 @@ export function presentationTools(server, resolveRoot) {
|
|
|
49
52
|
const group = presentationGroup ? `conversation:${presentationGroup}` : sessionId ? `session:${sessionId}` : undefined;
|
|
50
53
|
const card = resolved === 'ui' && hasUi && group ? cards.issue(root, group) : undefined;
|
|
51
54
|
const operational = conversationOperation(payload, viewModel, details || diagnostics, diagnostics);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
// Skill preview already supplies its complete exact Markdown and approval guidance.
|
|
56
|
+
// A generic compact projection must not discard that reviewed procedure.
|
|
57
|
+
const skillCompanion = ['skill-preview', 'skill-review'].includes(String(payload.action)) ? result.content.find(c => c.type === 'text') : undefined;
|
|
58
|
+
const humanText = skillCompanion?.type === 'text' ? skillCompanion.text : renderText(viewModel, diagnostics, details || diagnostics, plain);
|
|
59
|
+
// A capable connection can still have a surface where its App is invisible.
|
|
60
|
+
// Keep the existing compact lifecycle fallback; no extra Core call or card.
|
|
61
|
+
const lifecycle = ['prepare', 'session-close'].includes(String(payload.action));
|
|
62
|
+
const fallback = lifecycle ? ` ${LIFECYCLE_PRESENTATION_REPLY}\n\n${humanText}` : '';
|
|
63
|
+
// UI already carries these full Core snapshots in result; retain only the
|
|
64
|
+
// exact relay/action bindings alongside the fallback, avoiding duplication.
|
|
65
|
+
const { context: _context, session: _session, automaticSync: _sync, ...fallbackOperational } = operational;
|
|
66
|
+
return { ...result, content: resolved === "text" ? [{ type: "text", text: humanText }, { type: "text", text: JSON.stringify({ proderoOperation: operational }), annotations: { audience: ["assistant"] } }] : [...result.content, { type: "text", text: PRESENTATION_REPLY + fallback + (viewModel.knowledgeTable ? `\n\n${humanText}\n\n${JSON.stringify({ knowledgeRows: operational.knowledgeRows, numberedActions: operational.numberedActions })}` : ""), annotations: { audience: ["assistant"] } }],
|
|
67
|
+
structuredContent: { ...payload, projectSettings: { root: settings.root, presentation: settings.presentation, presentationSources: settings.presentationSources, diagnostics: settings.diagnostics }, presentation: { requested, resolved, scope: input.presentation === undefined ? "project" : "invocation", actions: resolved === "text" ? operational.availableActions : viewModel.actions, ...(resolved === "text" || lifecycle ? { text: humanText, operational: resolved === "text" ? operational : fallbackOperational } : {}), ...(card ? { card } : {}) } } };
|
|
55
68
|
};
|
|
56
69
|
handlers.set(name, (args, extra) => run({ ...args, presentation: "text" }, extra));
|
|
57
|
-
|
|
70
|
+
if (name === PRODERO_TOOLS.curationQuestion)
|
|
71
|
+
return target.registerTool(name, { ...config, inputSchema: z.object({ ...config.inputSchema, presentation: modeSchema, presentationGroup: groupSchema, details: z.boolean().optional(), diagnostics: z.boolean().optional(), plain: z.boolean().optional() }).strict() }, run);
|
|
72
|
+
return registerAppTool(target, name, { ...config, _meta: config._meta ?? {}, inputSchema: { ...config.inputSchema, presentation: modeSchema, presentationGroup: groupSchema, details: z.boolean().optional().describe("Show full details and proposed wording instead of the compact summary."), plain: z.boolean().optional().describe("Use ASCII frame and meter glyphs when Unicode rendering is unsuitable."), diagnostics: z.boolean().optional().describe("Include technical diagnostics, allocation evidence and exact workflow data. Implies details.") } }, run);
|
|
58
73
|
}
|
|
59
74
|
function registerGateway() {
|
|
60
75
|
// Register after the server installs its diagnostic boundary. Registering a
|
|
@@ -68,7 +83,7 @@ export function presentationTools(server, resolveRoot) {
|
|
|
68
83
|
}, async ({ cardId, activate }) => ({ content: [], structuredContent: cards.inspect(cardId, activate) }));
|
|
69
84
|
server.registerTool("prodero_text", {
|
|
70
85
|
title: "Prodero Text Presentation",
|
|
71
|
-
description: PRESENTATION_REPLY + " Open Prodero as compact conversational text (details:true for full inspection), or present a covered lifecycle/knowledge tool without an MCP App attachment. Uses the original tool schema, Core operation and approval rules. Pass its normal arguments unchanged. Mutating operations require the same explicit human review. No preference is persisted. Relay presentation.text
|
|
86
|
+
description: PRESENTATION_REPLY + " Open Prodero as compact conversational text (details:true for full inspection), or present a covered lifecycle/knowledge tool without an MCP App attachment. Uses the original tool schema, Core operation and approval rules. Pass its normal arguments unchanged. Mutating operations require the same explicit human review. No preference is persisted. Relay presentation.text exactly as supplied, preserving code fences, entry letters and the external Pulse menu. Healthy Prepare continues automatically; details use its returned context snapshot, never repeat retrieval. Operational data are for the agent; use exact continuations for follow-up replies. Show full proposed wording before approval.",
|
|
72
87
|
inputSchema: { tool: z.enum(PRESENTED_TOOLS).default("prodero_open"), arguments: z.record(z.unknown()).default({}) },
|
|
73
88
|
// Conservatively describe the entire gateway; never mark a mixed operation tool read-only.
|
|
74
89
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false },
|