@phyzess/pappra 0.1.0-beta.12 → 0.1.0-beta.13

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.
@@ -0,0 +1,91 @@
1
+ import { discoverWorkspace } from '../workspace/discover.js';
2
+ import { KnowledgeService } from './index.js';
3
+ export function classifyScannedDocumentKind(path) {
4
+ const lower = path.toLowerCase();
5
+ if (lower.endsWith('agents.md') || lower.endsWith('claude.md'))
6
+ return 'rule';
7
+ if (lower.includes('/research/'))
8
+ return 'research';
9
+ if (lower.includes('/adr/') || lower.includes('decision'))
10
+ return 'decision';
11
+ if (lower.includes('review') || lower.includes('retro'))
12
+ return 'review';
13
+ if (lower.includes('plan'))
14
+ return 'plan';
15
+ return 'architecture';
16
+ }
17
+ /**
18
+ * Registers documents discovered under the configured knowledge sources.
19
+ * Without apply this is read-only and reports the candidates.
20
+ */
21
+ export async function scanDiscoveredDocuments(root, options) {
22
+ const discovery = await discoverWorkspace(root);
23
+ const config = discovery.config;
24
+ if (config === null) {
25
+ return {
26
+ ok: false,
27
+ message: 'docs scan requires a valid .pappra/workspace.json; run pappra init first.',
28
+ };
29
+ }
30
+ const service = new KnowledgeService(root);
31
+ const listed = await service.list({ includePrivate: true });
32
+ const registered = new Set(listed.records.flatMap((record) => record.location.kind === 'workspace' ? [record.location.path] : []));
33
+ const sources = config.knowledge.sources;
34
+ const visibility = config.knowledge.taskVisibility === 'shared' ? 'shareable' : 'private';
35
+ const candidates = [];
36
+ for (const document of discovery.documents ?? []) {
37
+ const path = document.workspaceRelativePath;
38
+ if (path === null)
39
+ continue;
40
+ if (!sources.some((source) => path === source || path.startsWith(`${source}/`))) {
41
+ continue;
42
+ }
43
+ if (registered.has(path))
44
+ continue;
45
+ candidates.push({
46
+ path,
47
+ kind: classifyScannedDocumentKind(path),
48
+ visibility,
49
+ });
50
+ }
51
+ const diagnostics = [...listed.diagnostics];
52
+ if (!options.apply) {
53
+ return {
54
+ ok: true,
55
+ schema: 'pappra/docs-scan/1',
56
+ mode: 'plan',
57
+ workspaceRoot: root,
58
+ candidates,
59
+ registered: [],
60
+ skipped: candidates.length,
61
+ nextActions: [
62
+ 'Rerun with apply: true to register these documents in the knowledge catalog.',
63
+ ],
64
+ diagnostics,
65
+ };
66
+ }
67
+ const registeredNow = [];
68
+ for (const candidate of candidates) {
69
+ const result = await service.register({
70
+ location: { kind: 'workspace', path: candidate.path },
71
+ kind: candidate.kind,
72
+ maturity: 'draft',
73
+ owner: 'human',
74
+ visibility: candidate.visibility,
75
+ });
76
+ diagnostics.push(...result.diagnostics);
77
+ if (result.ok)
78
+ registeredNow.push(result.record);
79
+ }
80
+ return {
81
+ ok: diagnostics.every((entry) => entry.severity !== 'error'),
82
+ schema: 'pappra/docs-scan/1',
83
+ mode: 'apply',
84
+ workspaceRoot: root,
85
+ candidates,
86
+ registered: registeredNow,
87
+ skipped: candidates.length - registeredNow.length,
88
+ nextActions: [],
89
+ diagnostics,
90
+ };
91
+ }
@@ -0,0 +1,11 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare const MCP_SERVER_NAME = "pappra";
3
+ export interface PappraMcpServerOptions {
4
+ readonly root: string;
5
+ }
6
+ /**
7
+ * Builds the stdio MCP server for one workspace. Tools are read-only here; write
8
+ * operations are added separately and must stay behind apply/confirm gates.
9
+ */
10
+ export declare function createPappraMcpServer(options: PappraMcpServerOptions): McpServer;
11
+ export declare function runMcpServer(root: string): Promise<void>;
@@ -0,0 +1,518 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { discoverWorkspace } from '../workspace/discover.js';
5
+ import { inspectMachineConfig, inspectWorkspaceConfig, } from '../workspace/config.js';
6
+ import { inspectRuntimeStatePassive } from '../install/runtime.js';
7
+ import { CheckpointService } from '../continuity/service.js';
8
+ import { KnowledgeService } from '../knowledge/index.js';
9
+ import { CodeIntelligenceService } from '../code/index.js';
10
+ import { HOST_SUPPORT } from '../hosts/integration/support.js';
11
+ import { getPackageInfo } from '../package-info.js';
12
+ import { scanDiscoveredDocuments } from '../knowledge/scan.js';
13
+ import { installRuntime } from '../install/runtime.js';
14
+ import { bindAndRecordRuntime, ensureDefaultRuntime } from '../app/init.js';
15
+ import { prepareFromObjective } from '../app/collab-from.js';
16
+ import { FixtureController, PiRpcController, finishStarted, startPrepared, submitResultReport, } from '../app/collaboration.js';
17
+ export const MCP_SERVER_NAME = 'pappra';
18
+ function toolResult(value, isError = false) {
19
+ return {
20
+ content: [
21
+ {
22
+ type: 'text',
23
+ text: `${JSON.stringify(value)}\n`,
24
+ },
25
+ ],
26
+ ...(isError ? { isError: true } : {}),
27
+ };
28
+ }
29
+ async function errorResult(action) {
30
+ try {
31
+ return toolResult(await action());
32
+ }
33
+ catch (error) {
34
+ return toolResult({
35
+ ok: false,
36
+ message: error instanceof Error ? error.message : String(error),
37
+ }, true);
38
+ }
39
+ }
40
+ /**
41
+ * Builds the stdio MCP server for one workspace. Tools are read-only here; write
42
+ * operations are added separately and must stay behind apply/confirm gates.
43
+ */
44
+ export function createPappraMcpServer(options) {
45
+ const root = options.root;
46
+ const server = new McpServer({
47
+ name: MCP_SERVER_NAME,
48
+ version: getPackageInfo().version,
49
+ });
50
+ server.registerTool('workspace_status', {
51
+ title: 'Workspace status',
52
+ description: 'Read-only summary of the workspace configuration, repositories, knowledge sources, hosts and passive runtime state.',
53
+ annotations: { readOnlyHint: true },
54
+ }, async () => errorResult(async () => {
55
+ const workspace = await inspectWorkspaceConfig(root, {
56
+ missing: 'info',
57
+ });
58
+ const machine = await inspectMachineConfig(root);
59
+ const discovery = await discoverWorkspace(root);
60
+ const config = workspace.value;
61
+ const runtime = machine.value?.runtime === null ||
62
+ machine.value?.runtime === undefined
63
+ ? null
64
+ : await inspectRuntimeStatePassive(machine.value.runtime.version);
65
+ return {
66
+ ok: workspace.exists && machine.exists,
67
+ workspaceRoot: root,
68
+ config: config === null
69
+ ? null
70
+ : {
71
+ name: config.name,
72
+ repositories: config.repositories.map((repository) => ({
73
+ id: repository.id,
74
+ path: repository.path,
75
+ access: repository.access,
76
+ })),
77
+ knowledgeSources: config.knowledge.sources,
78
+ code: config.code,
79
+ validation: config.validation.map((entry) => entry.id),
80
+ excludes: config.excludes,
81
+ },
82
+ hosts: {
83
+ selected: machine.value?.selectedHosts ?? [],
84
+ pinned: Object.fromEntries(['pi', 'codex', 'claude'].map((host) => [
85
+ host,
86
+ HOST_SUPPORT[host].version,
87
+ ])),
88
+ },
89
+ discoveredRepositories: discovery.repositories.length,
90
+ discoveredDocuments: discovery.documents.length,
91
+ runtime: runtime === null
92
+ ? null
93
+ : {
94
+ version: runtime.version,
95
+ status: runtime.status,
96
+ executableExists: runtime.executableExists,
97
+ },
98
+ diagnostics: [
99
+ ...workspace.diagnostics,
100
+ ...machine.diagnostics,
101
+ ...discovery.diagnostics,
102
+ ].filter((entry) => entry.severity !== 'info'),
103
+ };
104
+ }));
105
+ server.registerTool('resume', {
106
+ title: 'Resume a task checkpoint',
107
+ description: 'Reads the stored checkpoint for a task and returns the advisory resume packet. A packet never grants execution.',
108
+ inputSchema: {
109
+ task: z.string().min(1).max(256),
110
+ session: z.string().min(1).max(256).optional(),
111
+ },
112
+ annotations: { readOnlyHint: true },
113
+ }, async (args) => errorResult(async () => {
114
+ const service = new CheckpointService(root);
115
+ const result = await service.resume({
116
+ taskId: args.task,
117
+ ...(args.session === undefined
118
+ ? {}
119
+ : { currentSession: args.session }),
120
+ });
121
+ return {
122
+ ok: result.ok,
123
+ status: result.status,
124
+ packet: result.packet,
125
+ diagnostics: result.diagnostics,
126
+ };
127
+ }));
128
+ server.registerTool('docs_list', {
129
+ title: 'List knowledge documents',
130
+ description: 'Lists registered knowledge documents. Private documents require includePrivate.',
131
+ inputSchema: {
132
+ includePrivate: z.boolean().optional(),
133
+ includeArchived: z.boolean().optional(),
134
+ },
135
+ annotations: { readOnlyHint: true },
136
+ }, async (args) => errorResult(async () => {
137
+ const service = new KnowledgeService(root);
138
+ const result = await service.list({
139
+ includePrivate: args.includePrivate === true,
140
+ includeArchived: args.includeArchived === true,
141
+ });
142
+ return {
143
+ ok: result.ok,
144
+ catalogDigest: result.catalogDigest,
145
+ documents: result.records.map((record) => ({
146
+ documentId: record.documentId,
147
+ kind: record.kind,
148
+ lifecycle: record.lifecycle,
149
+ maturity: record.maturity,
150
+ visibility: record.visibility,
151
+ location: record.location,
152
+ })),
153
+ diagnostics: result.diagnostics,
154
+ };
155
+ }));
156
+ server.registerTool('docs_check', {
157
+ title: 'Check knowledge freshness',
158
+ description: 'Checks registered documents for freshness and source drift.',
159
+ inputSchema: { includePrivate: z.boolean().optional() },
160
+ annotations: { readOnlyHint: true },
161
+ }, async (args) => errorResult(async () => {
162
+ const service = new KnowledgeService(root);
163
+ const result = await service.check({
164
+ includePrivate: args.includePrivate === true,
165
+ });
166
+ return {
167
+ ok: result.ok,
168
+ summary: result.summary,
169
+ coverage: result.coverage,
170
+ diagnostics: result.diagnostics,
171
+ };
172
+ }));
173
+ server.registerTool('code_status', {
174
+ title: 'Code index status',
175
+ description: 'Reports the selected repository code-index identity and health without preparing it.',
176
+ inputSchema: { repo: z.string().min(1).max(128) },
177
+ annotations: { readOnlyHint: true },
178
+ }, async (args) => errorResult(async () => {
179
+ const service = new CodeIntelligenceService(root);
180
+ const result = await service.status({ repositoryId: args.repo });
181
+ return {
182
+ ok: result.ok,
183
+ health: result.health,
184
+ registrationPresent: result.registrationPresent,
185
+ registrationPath: result.registrationPath,
186
+ diagnostics: result.diagnostics,
187
+ };
188
+ }));
189
+ server.registerTool('code_query', {
190
+ title: 'Query repository code',
191
+ description: 'Bounded code query for one selected repository. Semantic kinds need a prepared index; text works without one.',
192
+ inputSchema: {
193
+ repo: z.string().min(1).max(128),
194
+ kind: z.enum(['explore', 'symbol', 'references', 'impact', 'text']),
195
+ query: z.string().min(1).max(4096),
196
+ limit: z.number().int().min(1).max(100).optional(),
197
+ maxBytes: z.number().int().min(1).max(1_048_576).optional(),
198
+ },
199
+ annotations: { readOnlyHint: true },
200
+ }, async (args) => errorResult(async () => {
201
+ const service = new CodeIntelligenceService(root);
202
+ const result = await service.query({
203
+ root,
204
+ repositoryId: args.repo,
205
+ kind: args.kind,
206
+ query: args.query,
207
+ ...(args.limit === undefined && args.maxBytes === undefined
208
+ ? {}
209
+ : {
210
+ limits: {
211
+ ...(args.limit === undefined
212
+ ? {}
213
+ : { maxResults: args.limit }),
214
+ ...(args.maxBytes === undefined
215
+ ? {}
216
+ : { maxBytes: args.maxBytes }),
217
+ },
218
+ }),
219
+ });
220
+ return {
221
+ ok: result.ok,
222
+ provider: result.result?.provider ?? null,
223
+ coverage: result.result?.coverage ?? null,
224
+ items: result.result?.items ?? [],
225
+ health: result.health,
226
+ diagnostics: result.diagnostics,
227
+ };
228
+ }));
229
+ server.registerTool('checkpoint', {
230
+ title: 'Write a task checkpoint',
231
+ description: 'Writes an advisory checkpoint for handoff. Requires apply: true; a checkpoint is context, not permission to execute.',
232
+ inputSchema: {
233
+ task: z.string().min(1).max(256),
234
+ objective: z.string().min(1).max(4000),
235
+ acceptance: z.array(z.string().min(1).max(1000)).max(32).optional(),
236
+ constraints: z.array(z.string().min(1).max(1000)).max(32).optional(),
237
+ decisions: z.array(z.string().min(1).max(1000)).max(32).optional(),
238
+ attempted: z.array(z.string().min(1).max(1000)).max(32).optional(),
239
+ nextActions: z.array(z.string().min(1).max(1000)).max(32).optional(),
240
+ limitations: z.array(z.string().min(1).max(1000)).max(32).optional(),
241
+ artifacts: z.array(z.string().min(1).max(2048)).max(32).optional(),
242
+ contextCompleteness: z.enum(['complete', 'partial']).optional(),
243
+ authorSession: z.string().min(1).max(256).optional(),
244
+ nativeHarness: z.enum(['pi', 'codex', 'claude', 'unknown']).optional(),
245
+ nativeSession: z.string().min(1).max(256).optional(),
246
+ apply: z.boolean(),
247
+ },
248
+ annotations: { readOnlyHint: false },
249
+ }, async (args) => errorResult(async () => {
250
+ if (args.apply !== true) {
251
+ return {
252
+ ok: false,
253
+ status: 'confirmation_required',
254
+ message: 'checkpoint writes state; call again with apply: true after the user agrees.',
255
+ };
256
+ }
257
+ const nativeRefs = args.nativeSession === undefined
258
+ ? []
259
+ : [
260
+ {
261
+ harness: args.nativeHarness ?? 'unknown',
262
+ sessionId: args.nativeSession,
263
+ turnId: null,
264
+ runId: null,
265
+ },
266
+ ];
267
+ const service = new CheckpointService(root);
268
+ const result = await service.save({
269
+ taskId: args.task,
270
+ objective: args.objective,
271
+ ...(args.acceptance === undefined
272
+ ? {}
273
+ : { acceptance: args.acceptance }),
274
+ ...(args.constraints === undefined
275
+ ? {}
276
+ : { constraints: args.constraints }),
277
+ ...(args.decisions === undefined
278
+ ? {}
279
+ : { decisions: args.decisions }),
280
+ ...(args.attempted === undefined
281
+ ? {}
282
+ : { attempted: args.attempted }),
283
+ ...(args.nextActions === undefined
284
+ ? {}
285
+ : { nextActions: args.nextActions }),
286
+ ...(args.limitations === undefined
287
+ ? {}
288
+ : { limitations: args.limitations }),
289
+ ...(args.artifacts === undefined
290
+ ? {}
291
+ : { artifacts: args.artifacts }),
292
+ ...(args.contextCompleteness === undefined
293
+ ? {}
294
+ : { contextCompleteness: args.contextCompleteness }),
295
+ ...(args.authorSession === undefined
296
+ ? {}
297
+ : { authorSession: args.authorSession }),
298
+ ...(nativeRefs.length === 0 ? {} : { nativeRefs }),
299
+ });
300
+ return {
301
+ ok: result.ok,
302
+ status: result.status,
303
+ checkpointId: result.checkpoint?.checkpointId ?? null,
304
+ checkpointPath: result.checkpointPath,
305
+ diagnostics: result.diagnostics,
306
+ residualPaths: result.residualPaths,
307
+ };
308
+ }));
309
+ server.registerTool('docs_scan', {
310
+ title: 'Scan knowledge sources',
311
+ description: 'Registers documents discovered under the configured knowledge sources. Without apply it only reports candidates.',
312
+ inputSchema: { apply: z.boolean().optional() },
313
+ annotations: { readOnlyHint: false },
314
+ }, async (args) => errorResult(async () => {
315
+ const result = await scanDiscoveredDocuments(root, {
316
+ apply: args.apply === true,
317
+ });
318
+ return result;
319
+ }));
320
+ server.registerTool('code_prepare', {
321
+ title: 'Prepare a repository code index',
322
+ description: 'Registers the selected repository index and optionally runs the external engine. Without apply it only reports the plan.',
323
+ inputSchema: {
324
+ repo: z.string().min(1).max(128),
325
+ apply: z.boolean().optional(),
326
+ rebuild: z.boolean().optional(),
327
+ },
328
+ annotations: { readOnlyHint: false },
329
+ }, async (args) => errorResult(async () => {
330
+ const service = new CodeIntelligenceService(root);
331
+ const result = await service.prepare({
332
+ root,
333
+ repositoryId: args.repo,
334
+ apply: args.apply === true,
335
+ rebuild: args.rebuild === true,
336
+ });
337
+ return {
338
+ ok: result.ok,
339
+ status: result.status,
340
+ plan: result.plan,
341
+ health: result.health,
342
+ registrationPath: result.registrationPath,
343
+ diagnostics: result.diagnostics,
344
+ };
345
+ }));
346
+ server.registerTool('runtime_install', {
347
+ title: 'Install the durable runtime',
348
+ description: 'Installs and binds a durable runtime copy. Requires apply: true; the default source is the running package.',
349
+ inputSchema: {
350
+ from: z.enum(['self', 'tarball', 'version']).optional(),
351
+ version: z.string().min(1).max(128).optional(),
352
+ tarball: z.string().min(1).max(4096).optional(),
353
+ apply: z.boolean(),
354
+ },
355
+ annotations: { readOnlyHint: false },
356
+ }, async (args) => errorResult(async () => {
357
+ const from = args.from ?? 'self';
358
+ const version = args.version ?? getPackageInfo().version;
359
+ if (args.apply !== true) {
360
+ return {
361
+ ok: false,
362
+ status: 'confirmation_required',
363
+ message: 'runtime_install writes a durable copy and machine.json; call again with apply: true.',
364
+ version,
365
+ from,
366
+ };
367
+ }
368
+ if (from === 'self') {
369
+ const result = await ensureDefaultRuntime(root, version);
370
+ return {
371
+ ok: result.ok && result.completion,
372
+ completion: result.completion,
373
+ runtime: result.runtime,
374
+ diagnostics: result.diagnostics,
375
+ };
376
+ }
377
+ const tarball = args.tarball;
378
+ if (from === 'tarball' && tarball === undefined) {
379
+ return {
380
+ ok: false,
381
+ message: 'from: tarball requires the tarball path.',
382
+ };
383
+ }
384
+ const installed = await installRuntime({
385
+ version,
386
+ source: from === 'tarball' && tarball !== undefined
387
+ ? { kind: 'tarball', path: tarball }
388
+ : { kind: 'version' },
389
+ packageName: getPackageInfo().name,
390
+ });
391
+ const bound = await bindAndRecordRuntime(root, installed);
392
+ return {
393
+ ok: bound.ok && bound.completion,
394
+ completion: bound.completion,
395
+ runtime: installed,
396
+ diagnostics: bound.diagnostics,
397
+ };
398
+ }));
399
+ server.registerTool('collab_prepare', {
400
+ title: 'Prepare a bounded collaboration',
401
+ description: 'Builds a collaboration goal and a declared capability card from an objective and explicit resources, then claims them. A declared card cannot authorize a work launch.',
402
+ inputSchema: {
403
+ domainRoot: z.string().min(1).max(4096),
404
+ objective: z.string().min(1).max(4000),
405
+ task: z.string().min(1).max(128),
406
+ acceptance: z.array(z.string().min(1).max(1000)).min(1).max(32),
407
+ resources: z
408
+ .array(z.object({
409
+ path: z.string().min(1).max(4096),
410
+ access: z.enum(['read', 'write']),
411
+ }))
412
+ .min(1)
413
+ .max(32),
414
+ coordinatorSession: z.string().min(1).max(256),
415
+ assigneeSession: z.string().min(1).max(256),
416
+ harness: z.enum(['pi', 'codex', 'claude']).optional(),
417
+ subtaskId: z.string().min(1).max(128).optional(),
418
+ attempt: z.number().int().min(1).max(1000).optional(),
419
+ phase: z
420
+ .enum(['discovery', 'implementation', 'review', 'handoff', 'fix'])
421
+ .optional(),
422
+ prompt: z.string().min(1).max(20000).optional(),
423
+ },
424
+ annotations: { readOnlyHint: false },
425
+ }, async (args) => errorResult(async () => {
426
+ const result = await prepareFromObjective({
427
+ domainRoot: args.domainRoot,
428
+ objective: args.objective,
429
+ taskId: args.task,
430
+ acceptance: args.acceptance,
431
+ resources: args.resources,
432
+ coordinatorSession: args.coordinatorSession,
433
+ assigneeSession: args.assigneeSession,
434
+ ...(args.harness === undefined ? {} : { harness: args.harness }),
435
+ ...(args.subtaskId === undefined
436
+ ? {}
437
+ : { subtaskId: args.subtaskId }),
438
+ ...(args.attempt === undefined ? {} : { attempt: args.attempt }),
439
+ ...(args.phase === undefined ? {} : { phase: args.phase }),
440
+ ...(args.prompt === undefined ? {} : { prompt: args.prompt }),
441
+ });
442
+ return result;
443
+ }));
444
+ server.registerTool('collab_start', {
445
+ title: 'Start one admitted collaboration launch',
446
+ description: 'Performs one admitted launch with an explicit controller and returns the report. Requires confirm: true and a runtime capability card; a declared card is refused by admission.',
447
+ inputSchema: {
448
+ prepared: z.string().min(1).max(4096),
449
+ capabilities: z.string().min(1).max(4096),
450
+ controller: z.enum(['fixture', 'pi-rpc']),
451
+ confirm: z.boolean(),
452
+ },
453
+ annotations: { readOnlyHint: false, destructiveHint: true },
454
+ }, async (args) => errorResult(async () => {
455
+ if (args.confirm !== true) {
456
+ return {
457
+ ok: false,
458
+ status: 'confirmation_required',
459
+ message: 'collab_start launches a process; the user must confirm before calling it with confirm: true.',
460
+ };
461
+ }
462
+ const controller = args.controller === 'fixture'
463
+ ? new FixtureController()
464
+ : new PiRpcController();
465
+ const started = await startPrepared(args.prepared, {
466
+ capabilitiesFile: args.capabilities,
467
+ controller,
468
+ controllerId: controller.id,
469
+ });
470
+ if (!started.ok)
471
+ return started;
472
+ try {
473
+ const finished = await finishStarted(started.value);
474
+ if (!finished.ok) {
475
+ return finished;
476
+ }
477
+ const report = submitResultReport(started.value, finished.value);
478
+ return {
479
+ ok: report.ok,
480
+ status: finished.value.status,
481
+ report,
482
+ finish: finished.value,
483
+ };
484
+ }
485
+ finally {
486
+ await started.value.session.dispose?.().catch(() => undefined);
487
+ }
488
+ }));
489
+ server.registerTool('runtime_status', {
490
+ title: 'Runtime status',
491
+ description: 'Reports the workspace runtime binding and passive runtime state.',
492
+ annotations: { readOnlyHint: true },
493
+ }, async () => errorResult(async () => {
494
+ const machine = await inspectMachineConfig(root);
495
+ const binding = machine.value?.runtime ?? null;
496
+ if (binding === null) {
497
+ return { ok: true, binding: null, runtime: null };
498
+ }
499
+ const state = await inspectRuntimeStatePassive(binding.version);
500
+ return {
501
+ ok: true,
502
+ binding,
503
+ runtime: {
504
+ version: state.version,
505
+ status: state.status,
506
+ packageDigestMatch: state.packageDigestMatch,
507
+ executableExists: state.executableExists,
508
+ },
509
+ diagnostics: state.diagnostics,
510
+ };
511
+ }));
512
+ return server;
513
+ }
514
+ export async function runMcpServer(root) {
515
+ const server = createPappraMcpServer({ root });
516
+ const transport = new StdioServerTransport();
517
+ await server.connect(transport);
518
+ }
@@ -59,6 +59,9 @@ export declare const workspaceValidationSchema: z.ZodObject<{
59
59
  executable: z.ZodString;
60
60
  args: z.ZodArray<z.ZodString>;
61
61
  }, z.core.$strict>;
62
+ export declare const workspaceContinuitySchema: z.ZodObject<{
63
+ autoResume: z.ZodBoolean;
64
+ }, z.core.$strict>;
62
65
  export declare const workspaceConfigSchema: z.ZodObject<{
63
66
  schema: z.ZodLiteral<"pappra/workspace/1">;
64
67
  workspaceId: z.ZodUUID;
@@ -103,6 +106,9 @@ export declare const workspaceConfigSchema: z.ZodObject<{
103
106
  }, z.core.$strict>>;
104
107
  excludes: z.ZodArray<z.ZodString>;
105
108
  extensions: z.ZodRecord<z.ZodString, z.ZodUnknown>;
109
+ continuity: z.ZodOptional<z.ZodObject<{
110
+ autoResume: z.ZodBoolean;
111
+ }, z.core.$strict>>;
106
112
  }, z.core.$strict>;
107
113
  export type WorkspaceConfig = z.infer<typeof workspaceConfigSchema>;
108
114
  export type WorkspaceRepository = z.infer<typeof workspaceRepositorySchema>;
@@ -44,6 +44,9 @@ export const workspaceValidationSchema = z.strictObject({
44
44
  executable: executableSchema,
45
45
  args: z.array(argumentSchema).max(256),
46
46
  });
47
+ export const workspaceContinuitySchema = z.strictObject({
48
+ autoResume: z.boolean(),
49
+ });
47
50
  export const workspaceConfigSchema = z.strictObject({
48
51
  schema: z.literal(WORKSPACE_SCHEMA),
49
52
  workspaceId: z.uuid(),
@@ -55,6 +58,7 @@ export const workspaceConfigSchema = z.strictObject({
55
58
  validation: z.array(workspaceValidationSchema).max(128),
56
59
  excludes: z.array(portablePathSchema).max(128),
57
60
  extensions: z.record(z.string().min(1).max(128), z.unknown()),
61
+ continuity: workspaceContinuitySchema.optional(),
58
62
  });
59
63
  function pathDiagnostic(value, source, fieldPointer, label) {
60
64
  const hint = 'Use a workspace-relative path such as "docs" or "repos/app".';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phyzess/pappra",
3
- "version": "0.1.0-beta.12",
3
+ "version": "0.1.0-beta.13",
4
4
  "description": "Portable workspace knowledge and continuity for coding agents.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -98,6 +98,7 @@
98
98
  },
99
99
  "dependencies": {
100
100
  "@clack/prompts": "1.8.1",
101
+ "@modelcontextprotocol/sdk": "^1.32.1",
101
102
  "zod": "4.6.6"
102
103
  },
103
104
  "repository": {
@@ -21,6 +21,8 @@
21
21
  host. Codex 0.163.0 loads hooks only from `$CODEX_HOME/hooks.json`, so use
22
22
  `install --host codex --scope user`; `bundle --marketplace` exports a plugin
23
23
  directory without registering it, and `install --mode plugin` fails closed.
24
+ - `pappra mcp` starts the stdio MCP server; `pappra host mcp install --host <name> --apply`
25
+ registers it. `pappra task pin <id> --auto-resume` controls hook resume injection.
24
26
  - `pappra collab <prepare|run|query>` runs one bounded collaboration request.
25
27
  `run` requires `--controller fixture|pi-rpc`; `pi-rpc` launches a real Pi RPC
26
28
  session.