@phyzess/pappra 0.1.0-beta.18 → 0.1.0-beta.19

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pappra
3
- description: Use Pappra workspace knowledge, checkpoint/resume, selected-repository code intelligence, and task-scoped cross-harness collaboration from any supported coding agent. Use when a task needs durable handoff context, workspace documents, bounded code navigation, or a visible/direct multi-agent collaboration workflow.
3
+ description: Use Pappra workspace knowledge, checkpoint/resume, selected-repository code intelligence, and task-scoped cross-harness collaboration from any supported coding agent. Use before nontrivial repository work to reach the project's own skill and CodeGraph entry, when a task needs durable handoff context, workspace documents, or bounded code navigation, and when independent parallel implementation or independent review would reduce risk even if the user never names collaboration.
4
4
  ---
5
5
 
6
6
  # Pappra
@@ -74,6 +74,34 @@ No approval is auto-answered, no cancel is inferred from an acknowledgement, and
74
74
  arbitrary PID is adopted. Do not launch a model or real user session without
75
75
  the task's explicit authorization.
76
76
 
77
+ ## Role plans and host-agent role selection
78
+
79
+ Choosing whether work needs independent implementation or an independent reviewer
80
+ is the host agent's judgment from the user's objective and the actual change
81
+ surface: complexity, dependency blast radius, disjoint write scopes, and risk.
82
+ There is no backend role inferencer, no fixed participant count, and no repeated
83
+ permission prompt once the user authorized the work.
84
+
85
+ Use the installed commands in this order; roles never grant authority:
86
+
87
+ - `pappra collab role-plan expand --request <abs role-plan.json> --json` expands
88
+ explicit participants, assignments and disjoint writer scopes into the existing
89
+ work-plan format. Save the returned `plan`.
90
+ - `pappra collab plan --task-root <abs> --config <abs expanded-plan.json>` and
91
+ `pappra collab work-run --request <abs work-execution.json> [--apply]` keep
92
+ their existing admission, capability and dispatch requirements.
93
+ - `pappra collab role-run --request <abs role-orchestration.json> [--apply]`
94
+ reads authoritative role/dependency state; `--apply` handles one bounded event
95
+ tick and reserves before dispatch. Neither infers acceptance.
96
+ - `pappra collab role-plan aggregate --request <abs role-plan.json>` with
97
+ `--task-root <abs> --config <abs expanded-plan.json>` aggregates authoritative
98
+ results.
99
+
100
+ Roles describe responsibility and scope only: they approve nothing and never
101
+ synthesize a participant identity or capability card. Hooks record lifecycle
102
+ observations only and never select roles or launch a model. Keep the manual path:
103
+ a simple, reversible, local change needs no role plan.
104
+
77
105
  ## Recovery and parallel work
78
106
 
79
107
  Use `pappra collab work-run --request <abs-request.json>` for a read-only plan and add `--apply` only when the existing assignments, current capability cards, coordinator authority, resource claims and visible participant binding are ready. Work-run notifies only registered participants; it does not create sessions, merge, accept reports, or replay ambiguous delivery.
@@ -200,6 +200,19 @@ evidence through the work-plan inspector. Rejected, unknown, missing and pending
200
200
  outcomes remain visible. A delivery acknowledgement never becomes acceptance.
201
201
  Neither command launches a model, claims resources or creates an assignment.
202
202
 
203
+ `pappra collab role-run --request <abs role-orchestration.json> [--apply]
204
+ [--watch] --json` advances the plan's recorded role/dependency events. Plan mode
205
+ reads authoritative state with no effects; `--apply` handles one bounded event
206
+ tick, reserves before dispatch and never infers acceptance; `--watch` requires
207
+ `--apply`. Repeated or ambiguous effects are reconciled, and rejected work needs an
208
+ authorized repair attempt. It notifies registered participants only and creates no
209
+ session, assignment or claim of its own.
210
+
211
+ Which roles exist, and how many writers or reviewers run, is the host agent's
212
+ judgment of the actual objective and change surface, not a product default.
213
+ Nothing here infers semantic roles: hooks record bounded lifecycle observations,
214
+ the backend resolves protocol state, and no command launches a model from prose.
215
+
203
216
  ## Native capability evidence
204
217
 
205
218
  `pappra collab capabilities derive --harness <pi|codex|claude> --assignment <abs assignment.json> --template <abs card.declared.json> --execution <abs execution.json> [--session <abs pi-session.jsonl> | --events <abs events.jsonl>] [--json]`
@@ -51,6 +51,13 @@ stays `process_affiliation: "not-established"`.
51
51
  Pick participants, roles, harness, and configuration from the task rather than a
52
52
  fixed agent count. Everyone can implement, review, or verify; assign by need.
53
53
 
54
+ Which roles and how many participants are needed follows the real work split, and
55
+ that choice belongs to the host agent reading the objective and change surface;
56
+ neither the backend nor the hooks infer responsibilities or launch anyone.
57
+ Authorization the user already gave for the task persists, so do not re-request the
58
+ same delegation at every step, and never fabricate a participant identity,
59
+ capability or claim to fill a role.
60
+
54
61
  Reuse an existing peer only when all of the following match what the task will
55
62
  bind: current agent name, pane, harness, cwd, native session (where the host
56
63
  reports one), and the capabilities the assignment will require. Otherwise, when
@@ -0,0 +1,50 @@
1
+ import { type ManagedPiExecutionScope } from '../schemas/managed-pi-execution.js';
2
+ import type { RolloverBundleRequest, RolloverIntentRequest } from './collab-rollover.js';
3
+ export declare const COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA = "pappra/collaboration-session-transition-request/1";
4
+ export interface SessionTransitionRequest {
5
+ readonly schema: typeof COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA;
6
+ readonly managed_execution_scope?: ManagedPiExecutionScope;
7
+ readonly task_root: string;
8
+ readonly transition_id: string;
9
+ readonly intent: RolloverIntentRequest;
10
+ readonly bundle: RolloverBundleRequest;
11
+ readonly bootstrap_request: {
12
+ readonly ref: string;
13
+ readonly digest: string;
14
+ };
15
+ readonly evidence: {
16
+ readonly ack_message_id: string | null;
17
+ readonly first_work_report_id: string | null;
18
+ };
19
+ readonly archive_mode: 'task-retirement';
20
+ }
21
+ export interface LoadedSessionTransitionRequest {
22
+ readonly request: SessionTransitionRequest;
23
+ readonly file: string;
24
+ readonly file_digest: string;
25
+ readonly immutable_digest: string;
26
+ }
27
+ export type SessionTransitionRequestResult = {
28
+ readonly ok: true;
29
+ readonly value: LoadedSessionTransitionRequest;
30
+ } | {
31
+ readonly ok: false;
32
+ readonly kind: 'invalid';
33
+ readonly code: string;
34
+ readonly message: string;
35
+ readonly next_action: string | null;
36
+ };
37
+ /**
38
+ * The immutable digest deliberately excludes `evidence`: adding an ACK or a
39
+ * first-work REPORT pointer must not change a request's identity.
40
+ */
41
+ export declare function immutableDigest(request: SessionTransitionRequest): string;
42
+ export declare function validateSessionTransitionRequest(value: unknown): {
43
+ readonly ok: true;
44
+ readonly request: SessionTransitionRequest;
45
+ } | {
46
+ readonly ok: false;
47
+ readonly code: string;
48
+ readonly message: string;
49
+ };
50
+ export declare function loadSessionTransitionRequest(file: string): SessionTransitionRequestResult;
@@ -0,0 +1,339 @@
1
+ import path from 'node:path';
2
+ import { object as isRecord } from '../infra/validation.js';
3
+ import { canonicalJson } from '../collab/envelope.js';
4
+ import { MANAGED_PI_EXECUTION_SCOPE_SCHEMA, } from '../schemas/managed-pi-execution.js';
5
+ import { readBoundedFile } from './bounded-file.js';
6
+ import { digestText, isDigest, isNonNegativeInt, isPositiveInt, isSafeId, isText, safePathEqual, sameIdentity, } from './collab-session-transition-validation.js';
7
+ export const COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA = 'pappra/collaboration-session-transition-request/1';
8
+ const MAX_REQUEST_BYTES = 64 * 1024;
9
+ function hasOnlyKeys(value, keys) {
10
+ const allowed = new Set(keys);
11
+ return Object.keys(value).every((key) => allowed.has(key));
12
+ }
13
+ /**
14
+ * The immutable digest deliberately excludes `evidence`: adding an ACK or a
15
+ * first-work REPORT pointer must not change a request's identity.
16
+ */
17
+ export function immutableDigest(request) {
18
+ return digestText(canonicalJson({
19
+ schema: request.schema,
20
+ task_root: request.task_root,
21
+ transition_id: request.transition_id,
22
+ intent: request.intent,
23
+ bundle: request.bundle,
24
+ bootstrap_request: request.bootstrap_request,
25
+ archive_mode: request.archive_mode,
26
+ ...(request.managed_execution_scope === undefined
27
+ ? {}
28
+ : { managed_execution_scope: request.managed_execution_scope }),
29
+ }));
30
+ }
31
+ function parseIdentity(value) {
32
+ if (!isRecord(value) ||
33
+ !hasOnlyKeys(value, ['agent_id', 'session_id', 'harness', 'cwd']))
34
+ return null;
35
+ if (!isText(value['agent_id'], 128) || !isText(value['session_id'], 256))
36
+ return null;
37
+ if (value['harness'] !== 'pi')
38
+ return null;
39
+ if (!isText(value['cwd'], 4096) || !path.isAbsolute(value['cwd']))
40
+ return null;
41
+ return {
42
+ agent_id: value['agent_id'],
43
+ session_id: value['session_id'],
44
+ harness: 'pi',
45
+ cwd: value['cwd'],
46
+ };
47
+ }
48
+ function parseIntent(value) {
49
+ if (!isRecord(value) ||
50
+ !hasOnlyKeys(value, [
51
+ 'intent_id',
52
+ 'generation',
53
+ 'handoff_id',
54
+ 'subtask_id',
55
+ 'attempt',
56
+ 'input_revision',
57
+ 'from',
58
+ 'to',
59
+ 'claims',
60
+ 'coordinator_epoch',
61
+ ]))
62
+ return null;
63
+ const from = parseIdentity(value['from']);
64
+ const to = parseIdentity(value['to']);
65
+ const claims = value['claims'];
66
+ if (!isSafeId(value['intent_id']) ||
67
+ !isPositiveInt(value['generation']) ||
68
+ !isSafeId(value['handoff_id']) ||
69
+ !isSafeId(value['subtask_id']) ||
70
+ !isPositiveInt(value['attempt']) ||
71
+ !isDigest(value['input_revision']) ||
72
+ from === null ||
73
+ to === null ||
74
+ !isPositiveInt(value['coordinator_epoch']) ||
75
+ !Array.isArray(claims) ||
76
+ claims.length === 0 ||
77
+ claims.length > 64 ||
78
+ sameIdentity(from, to))
79
+ return null;
80
+ const parsedClaims = [];
81
+ const seen = new Set();
82
+ for (const claim of claims) {
83
+ if (!isRecord(claim) ||
84
+ !hasOnlyKeys(claim, ['claim_id', 'resource_epoch']) ||
85
+ !isSafeId(claim['claim_id']) ||
86
+ !isNonNegativeInt(claim['resource_epoch']) ||
87
+ seen.has(claim['claim_id']))
88
+ return null;
89
+ seen.add(claim['claim_id']);
90
+ parsedClaims.push({
91
+ claim_id: claim['claim_id'],
92
+ resource_epoch: claim['resource_epoch'],
93
+ });
94
+ }
95
+ return {
96
+ intent_id: value['intent_id'],
97
+ generation: value['generation'],
98
+ handoff_id: value['handoff_id'],
99
+ subtask_id: value['subtask_id'],
100
+ attempt: value['attempt'],
101
+ input_revision: value['input_revision'],
102
+ from,
103
+ to,
104
+ claims: parsedClaims,
105
+ coordinator_epoch: value['coordinator_epoch'],
106
+ };
107
+ }
108
+ function parseManagedPiExecutionScope(value) {
109
+ if (!isRecord(value) ||
110
+ !hasOnlyKeys(value, [
111
+ 'schema',
112
+ 'task_id',
113
+ 'task_root',
114
+ 'session_id',
115
+ 'cwd',
116
+ 'subtask_id',
117
+ 'attempt',
118
+ 'owner_epoch',
119
+ 'claim_epochs',
120
+ ]) ||
121
+ value['schema'] !== MANAGED_PI_EXECUTION_SCOPE_SCHEMA ||
122
+ !isSafeId(value['task_id']) ||
123
+ !isText(value['task_root'], 4096) ||
124
+ !path.isAbsolute(value['task_root']) ||
125
+ !isText(value['session_id'], 256) ||
126
+ !isText(value['cwd'], 4096) ||
127
+ !path.isAbsolute(value['cwd']) ||
128
+ !isSafeId(value['subtask_id']) ||
129
+ !isPositiveInt(value['attempt']) ||
130
+ !isPositiveInt(value['owner_epoch']) ||
131
+ !Array.isArray(value['claim_epochs']) ||
132
+ value['claim_epochs'].length === 0 ||
133
+ value['claim_epochs'].length > 64)
134
+ return null;
135
+ const claim_epochs = [];
136
+ const seen = new Set();
137
+ for (const raw of value['claim_epochs']) {
138
+ if (!isRecord(raw) ||
139
+ !hasOnlyKeys(raw, ['claim_id', 'resource_epoch']) ||
140
+ !isSafeId(raw['claim_id']) ||
141
+ !isPositiveInt(raw['resource_epoch']) ||
142
+ seen.has(raw['claim_id']))
143
+ return null;
144
+ seen.add(raw['claim_id']);
145
+ claim_epochs.push({
146
+ claim_id: raw['claim_id'],
147
+ resource_epoch: raw['resource_epoch'],
148
+ });
149
+ }
150
+ return {
151
+ schema: MANAGED_PI_EXECUTION_SCOPE_SCHEMA,
152
+ task_id: value['task_id'],
153
+ task_root: value['task_root'],
154
+ session_id: value['session_id'],
155
+ cwd: value['cwd'],
156
+ subtask_id: value['subtask_id'],
157
+ attempt: value['attempt'],
158
+ owner_epoch: value['owner_epoch'],
159
+ claim_epochs,
160
+ };
161
+ }
162
+ export function validateSessionTransitionRequest(value) {
163
+ if (!isRecord(value) ||
164
+ !hasOnlyKeys(value, [
165
+ 'schema',
166
+ 'task_root',
167
+ 'transition_id',
168
+ 'intent',
169
+ 'bundle',
170
+ 'bootstrap_request',
171
+ 'evidence',
172
+ 'archive_mode',
173
+ 'managed_execution_scope',
174
+ ]))
175
+ return {
176
+ ok: false,
177
+ code: 'request_shape_invalid',
178
+ message: 'Session-transition request must use only documented fields.',
179
+ };
180
+ if (value['schema'] !== COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA)
181
+ return {
182
+ ok: false,
183
+ code: 'request_schema_invalid',
184
+ message: `request.schema must be ${COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA}.`,
185
+ };
186
+ if (!isText(value['task_root'], 4096) || !path.isAbsolute(value['task_root']))
187
+ return {
188
+ ok: false,
189
+ code: 'task_root_invalid',
190
+ message: 'task_root must be an absolute task-root path.',
191
+ };
192
+ const intent = parseIntent(value['intent']);
193
+ if (intent === null)
194
+ return {
195
+ ok: false,
196
+ code: 'intent_invalid',
197
+ message: 'intent must pin exact source/successor identities, attempt, input, epoch and claims.',
198
+ };
199
+ if (value['transition_id'] !== intent.intent_id)
200
+ return {
201
+ ok: false,
202
+ code: 'transition_id_mismatch',
203
+ message: 'transition_id must equal intent.intent_id.',
204
+ };
205
+ const bundleValue = value['bundle'];
206
+ if (!isRecord(bundleValue) ||
207
+ !hasOnlyKeys(bundleValue, [
208
+ 'root',
209
+ 'continuity_task_id',
210
+ 'bundle_id',
211
+ 'digest',
212
+ ]) ||
213
+ !isText(bundleValue['root'], 4096) ||
214
+ !path.isAbsolute(bundleValue['root']) ||
215
+ !isSafeId(bundleValue['continuity_task_id']) ||
216
+ !isSafeId(bundleValue['bundle_id']) ||
217
+ !isDigest(bundleValue['digest']))
218
+ return {
219
+ ok: false,
220
+ code: 'bundle_invalid',
221
+ message: 'bundle must name one absolute workspace root and exact linked bundle digest.',
222
+ };
223
+ const bootstrapValue = value['bootstrap_request'];
224
+ if (!isRecord(bootstrapValue) ||
225
+ !hasOnlyKeys(bootstrapValue, ['ref', 'digest']) ||
226
+ !isText(bootstrapValue['ref'], 4096) ||
227
+ !path.isAbsolute(bootstrapValue['ref']) ||
228
+ !isDigest(bootstrapValue['digest']))
229
+ return {
230
+ ok: false,
231
+ code: 'bootstrap_request_invalid',
232
+ message: 'bootstrap_request must pin one absolute bounded request file and raw digest.',
233
+ };
234
+ const evidenceValue = value['evidence'];
235
+ if (!isRecord(evidenceValue) ||
236
+ !hasOnlyKeys(evidenceValue, ['ack_message_id', 'first_work_report_id']))
237
+ return {
238
+ ok: false,
239
+ code: 'evidence_invalid',
240
+ message: 'evidence must contain only the optional registered ACK and REPORT ids.',
241
+ };
242
+ const ack = evidenceValue['ack_message_id'];
243
+ const report = evidenceValue['first_work_report_id'];
244
+ if ((ack !== null && !isSafeId(ack)) ||
245
+ (report !== null && !isSafeId(report)))
246
+ return {
247
+ ok: false,
248
+ code: 'evidence_id_invalid',
249
+ message: 'Evidence ids must be null or safe registered message ids.',
250
+ };
251
+ if (value['archive_mode'] !== 'task-retirement')
252
+ return {
253
+ ok: false,
254
+ code: 'archive_mode_invalid',
255
+ message: 'Only the reversible task-retirement marker is supported; native archive is unavailable.',
256
+ };
257
+ const scopeValue = value['managed_execution_scope'];
258
+ const managedExecutionScope = scopeValue === undefined ? null : parseManagedPiExecutionScope(scopeValue);
259
+ if (scopeValue !== undefined &&
260
+ (managedExecutionScope === null ||
261
+ managedExecutionScope.task_id !== path.basename(value['task_root']) ||
262
+ !safePathEqual(managedExecutionScope.task_root, value['task_root']) ||
263
+ managedExecutionScope.session_id !== intent.from.session_id ||
264
+ !safePathEqual(managedExecutionScope.cwd, intent.from.cwd ?? '') ||
265
+ managedExecutionScope.subtask_id !== intent.subtask_id ||
266
+ managedExecutionScope.attempt !== intent.attempt ||
267
+ managedExecutionScope.owner_epoch !== intent.coordinator_epoch ||
268
+ managedExecutionScope.claim_epochs.length !== intent.claims.length ||
269
+ managedExecutionScope.claim_epochs.some((pin, index) => pin.claim_id !== intent.claims[index]?.claim_id ||
270
+ pin.resource_epoch !== intent.claims[index]?.resource_epoch)))
271
+ return {
272
+ ok: false,
273
+ code: 'managed_execution_scope_mismatch',
274
+ message: 'Managed execution scope must exactly match task, source session, attempt, owner epoch and claim pins.',
275
+ };
276
+ return {
277
+ ok: true,
278
+ request: {
279
+ schema: COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA,
280
+ task_root: value['task_root'],
281
+ transition_id: value['transition_id'],
282
+ intent,
283
+ bundle: {
284
+ root: bundleValue['root'],
285
+ continuity_task_id: bundleValue['continuity_task_id'],
286
+ bundle_id: bundleValue['bundle_id'],
287
+ digest: bundleValue['digest'],
288
+ },
289
+ bootstrap_request: {
290
+ ref: bootstrapValue['ref'],
291
+ digest: bootstrapValue['digest'],
292
+ },
293
+ evidence: {
294
+ ack_message_id: typeof ack === 'string' ? ack : null,
295
+ first_work_report_id: typeof report === 'string' ? report : null,
296
+ },
297
+ archive_mode: 'task-retirement',
298
+ ...(managedExecutionScope === null
299
+ ? {}
300
+ : { managed_execution_scope: managedExecutionScope }),
301
+ },
302
+ };
303
+ }
304
+ export function loadSessionTransitionRequest(file) {
305
+ const read = readBoundedFile(file, MAX_REQUEST_BYTES);
306
+ if (!read.ok)
307
+ return {
308
+ ok: false,
309
+ kind: 'invalid',
310
+ code: 'request_unreadable',
311
+ message: `Request must be an absolute bounded regular file (${read.code}).`,
312
+ next_action: 'Pass a regular JSON request file; no task mutation was attempted.',
313
+ };
314
+ let parsed;
315
+ try {
316
+ parsed = JSON.parse(read.text);
317
+ }
318
+ catch {
319
+ return {
320
+ ok: false,
321
+ kind: 'invalid',
322
+ code: 'request_invalid_json',
323
+ message: 'Session-transition request is not valid JSON.',
324
+ next_action: 'Fix the request document and retry explicitly.',
325
+ };
326
+ }
327
+ const validated = validateSessionTransitionRequest(parsed);
328
+ if (!validated.ok)
329
+ return { ...validated, kind: 'invalid', next_action: null };
330
+ return {
331
+ ok: true,
332
+ value: {
333
+ request: validated.request,
334
+ file,
335
+ file_digest: read.digest,
336
+ immutable_digest: immutableDigest(validated.request),
337
+ },
338
+ };
339
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Primitive guards shared by the session-transition request parser and the
3
+ * orchestrator. They live below both so neither has to import the other's
4
+ * parsing module for a predicate.
5
+ */
6
+ export declare function digestText(bytes: string | Buffer): string;
7
+ export declare function isText(value: unknown, max?: number): value is string;
8
+ export declare function isPositiveInt(value: unknown): value is number;
9
+ export declare function isNonNegativeInt(value: unknown): value is number;
10
+ export declare function isDigest(value: unknown): value is string;
11
+ export declare function isSafeId(value: unknown): value is string;
12
+ export declare function sameIdentity(left: {
13
+ readonly agent_id?: string | null;
14
+ readonly session_id?: string | null;
15
+ } | null | undefined, right: {
16
+ readonly agent_id: string;
17
+ readonly session_id: string;
18
+ }): boolean;
19
+ export declare function safePathEqual(left: string, right: string): boolean;
@@ -0,0 +1,44 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
4
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
5
+ /**
6
+ * Primitive guards shared by the session-transition request parser and the
7
+ * orchestrator. They live below both so neither has to import the other's
8
+ * parsing module for a predicate.
9
+ */
10
+ export function digestText(bytes) {
11
+ return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
12
+ }
13
+ export function isText(value, max = 2048) {
14
+ return (typeof value === 'string' &&
15
+ value.length > 0 &&
16
+ value.length <= max &&
17
+ !value.includes('\u0000'));
18
+ }
19
+ export function isPositiveInt(value) {
20
+ return Number.isSafeInteger(value) && value > 0;
21
+ }
22
+ export function isNonNegativeInt(value) {
23
+ return Number.isSafeInteger(value) && value >= 0;
24
+ }
25
+ export function isDigest(value) {
26
+ return typeof value === 'string' && DIGEST.test(value);
27
+ }
28
+ export function isSafeId(value) {
29
+ return typeof value === 'string' && SAFE_ID.test(value);
30
+ }
31
+ export function sameIdentity(left, right) {
32
+ return (left !== null &&
33
+ left !== undefined &&
34
+ left.agent_id === right.agent_id &&
35
+ left.session_id === right.session_id);
36
+ }
37
+ export function safePathEqual(left, right) {
38
+ try {
39
+ return fs.realpathSync(left) === fs.realpathSync(right);
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
@@ -2,12 +2,14 @@ import * as rollover from '../collab/rollover.js';
2
2
  import type { FreezeOutcome, FreezeStatus, FreezeToken } from '../hosts/integration/pi-rollover-extension.js';
3
3
  import type { NativeFactsSnapshot } from '../hosts/integration/pi-native-events.js';
4
4
  import { type LoadedVisibleBootstrapRequest, type VisibleBootstrapDependencies, type VisibleBootstrapReport } from './collab-bootstrap.js';
5
- import { type RolloverBundleRequest, type RolloverIntentRequest, type RolloverReport } from './collab-rollover.js';
5
+ import { type RolloverReport } from './collab-rollover.js';
6
6
  import { type HerdrBootstrapOps } from './collab-bootstrap-transport.js';
7
7
  import { MANAGED_PI_EXECUTION_SCOPE_SCHEMA, type ManagedPiExecutionScope } from '../schemas/managed-pi-execution.js';
8
8
  export { MANAGED_PI_EXECUTION_SCOPE_SCHEMA };
9
9
  export type { ManagedPiExecutionScope };
10
- export declare const COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA = "pappra/collaboration-session-transition-request/1";
10
+ export { COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA, loadSessionTransitionRequest, validateSessionTransitionRequest, } from './collab-session-transition-request.js';
11
+ export type { LoadedSessionTransitionRequest, SessionTransitionRequest, SessionTransitionRequestResult, } from './collab-session-transition-request.js';
12
+ import { type LoadedSessionTransitionRequest, type SessionTransitionRequest } from './collab-session-transition-request.js';
11
13
  export declare const COLLAB_SESSION_TRANSITION_REPORT_SCHEMA = "pappra/collaboration-session-transition/1";
12
14
  export declare const SESSION_TRANSITION_ARCHIVE_SCHEMA = "pappra/session-transition-archive/1";
13
15
  export interface SessionTransitionRetirementMarker {
@@ -38,39 +40,6 @@ export type SessionTransitionStatus = 'planned' | 'progressed' | 'waiting' | 'ne
38
40
  export type SessionTransitionStage = 'not_started' | 'intent_created' | 'awaiting_quiescence' | 'prepared' | 'successor_bootstrap' | 'awaiting_receiver_ack' | 'receiver_confirmed' | 'ownership_transferred' | 'awaiting_first_work' | 'first_work_recorded' | 'task_retired' | 'unknown';
39
41
  export declare const MANAGED_PI_SCOPED_PROOF_ADAPTER_ID = "pappra/managed-pi-session-transition-host/2";
40
42
  export declare const MANAGED_PI_SCOPED_RECEIPT_SCHEMA = "pappra/session-transition-quiescence-receipt/2";
41
- export interface SessionTransitionRequest {
42
- readonly schema: typeof COLLAB_SESSION_TRANSITION_REQUEST_SCHEMA;
43
- readonly managed_execution_scope?: ManagedPiExecutionScope;
44
- readonly task_root: string;
45
- readonly transition_id: string;
46
- readonly intent: RolloverIntentRequest;
47
- readonly bundle: RolloverBundleRequest;
48
- readonly bootstrap_request: {
49
- readonly ref: string;
50
- readonly digest: string;
51
- };
52
- readonly evidence: {
53
- readonly ack_message_id: string | null;
54
- readonly first_work_report_id: string | null;
55
- };
56
- readonly archive_mode: 'task-retirement';
57
- }
58
- export interface LoadedSessionTransitionRequest {
59
- readonly request: SessionTransitionRequest;
60
- readonly file: string;
61
- readonly file_digest: string;
62
- readonly immutable_digest: string;
63
- }
64
- export type SessionTransitionRequestResult = {
65
- readonly ok: true;
66
- readonly value: LoadedSessionTransitionRequest;
67
- } | {
68
- readonly ok: false;
69
- readonly kind: 'invalid';
70
- readonly code: string;
71
- readonly message: string;
72
- readonly next_action: string | null;
73
- };
74
43
  export interface SessionTransitionQuiescenceProof {
75
44
  readonly adapter_id: 'pappra/pi-session-transition-host/1' | typeof MANAGED_PI_SCOPED_PROOF_ADAPTER_ID;
76
45
  readonly session_id: string;
@@ -207,15 +176,6 @@ export interface SessionTransitionJournal {
207
176
  readonly schema: 'pappra/session-transition-journal/1';
208
177
  readonly entries: readonly SessionTransitionJournalEntry[];
209
178
  }
210
- export declare function validateSessionTransitionRequest(value: unknown): {
211
- readonly ok: true;
212
- readonly request: SessionTransitionRequest;
213
- } | {
214
- readonly ok: false;
215
- readonly code: string;
216
- readonly message: string;
217
- };
218
- export declare function loadSessionTransitionRequest(file: string): SessionTransitionRequestResult;
219
179
  export type SessionTransitionEffectName = 'source-freeze' | 'successor-route-binding' | 'task-retirement-marker';
220
180
  export declare function planSessionTransition(input: LoadedSessionTransitionRequest | SessionTransitionRequest, dependencies?: SessionTransitionDependencies): Promise<SessionTransitionReport>;
221
181
  export declare function applySessionTransition(input: LoadedSessionTransitionRequest | SessionTransitionRequest, dependencies?: SessionTransitionDependencies): Promise<SessionTransitionReport>;