@prodero/curator 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/CHANGELOG.md +19 -2
  3. package/README.md +32 -33
  4. package/dist/cli.js +58 -20
  5. package/dist/contracts.js +1 -0
  6. package/dist/conversation-presentation.js +62 -27
  7. package/dist/core/agent-instructions.js +31 -18
  8. package/dist/core/curation-graphical.js +22 -10
  9. package/dist/core/curation-question.js +28 -0
  10. package/dist/core/curation-semantic-contract.js +4 -1
  11. package/dist/core/curation-semantic-jobs.js +78 -18
  12. package/dist/core/knowledge-graph.js +5 -4
  13. package/dist/core/local-preferences.js +76 -0
  14. package/dist/core/memory-index.js +2 -0
  15. package/dist/core/memory-link-repair.js +6 -2
  16. package/dist/core/memory-sync-curator.js +25 -5
  17. package/dist/core/memory-sync.js +124 -29
  18. package/dist/core/npm-update.js +289 -0
  19. package/dist/core/project.js +8 -8
  20. package/dist/core/session-index.js +10 -6
  21. package/dist/core/sessions.js +3 -3
  22. package/dist/core/settings.js +34 -16
  23. package/dist/core/skills.js +21 -2
  24. package/dist/core/storage.js +55 -1
  25. package/dist/core/sync-transaction.js +85 -49
  26. package/dist/core/update.js +9 -3
  27. package/dist/curation-review-adapter.js +1 -1
  28. package/dist/diagnostic-boundary.js +4 -3
  29. package/dist/memory-presentation.js +26 -16
  30. package/dist/memory-sync-operation.js +88 -0
  31. package/dist/presentation-server.js +25 -10
  32. package/dist/presentation.js +92 -61
  33. package/dist/server.js +59 -51
  34. package/dist/settings-definition.js +3 -3
  35. package/dist/status-presentation.js +20 -0
  36. package/dist/text-layout.js +278 -0
  37. package/dist/update-command.js +53 -0
  38. package/dist/update-instructions.js +4 -0
  39. package/dist-ui/index.html +117 -115
  40. package/docs/COMPATIBILITY.md +29 -10
  41. package/docs/MIGRATIONS.md +4 -0
  42. package/docs/RECOVERY.md +3 -1
  43. package/docs/RELEASING.md +68 -10
  44. package/docs/hosts.md +25 -7
  45. package/docs/images/prodero-context-ready-cropped.png +0 -0
  46. package/docs/images/prodero-overview-cropped.png +0 -0
  47. package/docs/images/prodero-session-learned-cropped.png +0 -0
  48. package/docs/ui-themes.md +10 -4
  49. package/instructions/AGENTS.md +24 -18
  50. 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
- await runtimeDirectory(root);
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
- try {
55
- await fs.mkdir(lock, { mode: PRIVATE_DIRECTORY_MODE });
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 fs.mkdir(reaper, { mode: PRIVATE_DIRECTORY_MODE });
76
- reaping = true;
99
+ await writeFileExclusive(await checkedDataPath(root, '.transactions/lock/owner'), String(process.pid));
77
100
  }
78
101
  catch (error) {
79
- if (error.code !== 'EEXIST')
80
- throw error;
102
+ await removeOwnedLock(root, acquired);
103
+ throw error;
81
104
  }
82
- if (reaping) {
83
- const owner = await fs.readFile(await checkedDataPath(root, '.transactions/lock/owner'), 'utf8').catch(error => { if (error.code === 'ENOENT')
84
- return ''; throw error; });
85
- const pid = /^\d+$/.test(owner) ? Number(owner) : 0;
86
- if (pid > 0 && Number.isSafeInteger(pid)) {
87
- try {
88
- process.kill(pid, 0);
89
- }
90
- catch (probe) {
91
- if (probe.code !== 'ESRCH')
92
- throw probe;
93
- await fs.rm(lock, { recursive: true, force: true });
94
- recovered = true;
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
- finally {
100
- if (reaping)
101
- await fs.rm(reaper, { recursive: true, force: true });
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
- await fs.rm(lock, { recursive: true, force: true });
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) {
@@ -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
- export const MANUAL_UPDATE_COMMAND = "npm install --global @prodero/curator@latest";
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
- /** One-click adapter for an MCP runtime launched from a development checkout. */
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} ` : ""}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.` };
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 Sync Memory operation.');
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: 'Sync Memory keeps that memory useful and shareable.',
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 Sync started. Re-analyze before continuing.',
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 Sync Memory again when current work has finished.');
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 Sync Memory run.`);
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 Sync is in progress. Continue when you are ready.');
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 Sync is in progress.' : null };
67
- return { headline, notes, areas, conflicts, workflow, dashboard, button: workflow ? (workflow.state === 'stale' ? 'Re-analyze memory' : 'Continue Memory Sync') : 'Sync Memory' };
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 headlines = { awaiting_selection: 'Sync 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: 'Saving changes…', awaiting_promotion_selection: 'Review Local knowledge for sharing', completed: 'Memory synced', stale: 'Re-analyze memory' };
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 === 'semantic_work')
78
- notes.push('Prodero is reviewing related knowledge and making safe Local improvements. You will only be asked about missing project facts.');
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('Your approved changes are being saved safely. Continue to check their result.');
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 Sync.`);
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')} shared with the team.`);
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, 'memory', 'memories')} still need clarification and remain current.`);
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 Sync Memory run.`);
170
- return { headline: headlines[w.state] ?? 'Sync Memory', graphicalHeadline: headlines[w.state] ?? 'Sync Memory', reviewAreaLabel: 'Memory changes', notes, warnings, metrics, sections, areas };
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 canonical project presentation.mode. 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.");
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
- const humanText = renderText(viewModel, diagnostics, details || diagnostics);
53
- 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 + (viewModel.knowledgeTable ? `\n\n${humanText}\n\n${JSON.stringify({ knowledgeRows: operational.knowledgeRows, numberedActions: operational.numberedActions })}` : ""), annotations: { audience: ["assistant"] } }],
54
- structuredContent: { ...payload, projectSettings: { root: settings.root, presentation: settings.presentation }, presentation: { requested, resolved, scope: input.presentation === undefined ? "project" : "invocation", actions: resolved === "text" ? operational.availableActions : viewModel.actions, ...(resolved === "text" ? { text: humanText, operational } : {}), ...(card ? { card } : {}) } } };
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
- 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."), diagnostics: z.boolean().optional().describe("Include technical diagnostics, allocation evidence and exact workflow data. Implies details.") } }, run);
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 concisely, without expanding other result fields into a dashboard or listing every action. Operational data are for the agent; use exact continuations for follow-up replies. Show full proposed wording before approval.",
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 },