engineering-memory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bin/engineering-memory.mjs +120 -0
  2. package/dispatcher/managed-section.mjs +59 -0
  3. package/dispatcher/sections.mjs +14 -0
  4. package/install/api-url.mjs +39 -0
  5. package/install/cli.mjs +93 -0
  6. package/install/commands.mjs +140 -0
  7. package/install/files.mjs +416 -0
  8. package/install/git-hook.mjs +270 -0
  9. package/install/installer.mjs +279 -0
  10. package/install/mcp-registration.mjs +457 -0
  11. package/package.json +28 -0
  12. package/runtime/dist/src/auth/browser-auth.js +184 -0
  13. package/runtime/dist/src/auth/credential-store.js +181 -0
  14. package/runtime/dist/src/cache/etag-cache.js +123 -0
  15. package/runtime/dist/src/config.js +59 -0
  16. package/runtime/dist/src/git/git-inspector.js +375 -0
  17. package/runtime/dist/src/git/pre-commit.js +44 -0
  18. package/runtime/dist/src/git/verification-gate.js +221 -0
  19. package/runtime/dist/src/index.js +60 -0
  20. package/runtime/dist/src/journal/journal-store.js +1300 -0
  21. package/runtime/dist/src/mcp/server.js +11 -0
  22. package/runtime/dist/src/mcp/tool-definitions.js +405 -0
  23. package/runtime/dist/src/project/repository.js +79 -0
  24. package/runtime/dist/src/runtime/active-context-store.js +356 -0
  25. package/runtime/dist/src/runtime/api-client.js +229 -0
  26. package/runtime/dist/src/runtime/bridge-service.js +2226 -0
  27. package/runtime/dist/src/runtime/offline-outbox.js +274 -0
  28. package/runtime/dist/src/runtime/principal-state.js +97 -0
  29. package/runtime/dist/src/types.js +2 -0
  30. package/runtime/dist/src/utilities/files.js +189 -0
  31. package/runtime/dist/src/utilities/hash.js +19 -0
  32. package/runtime/dist/src/utilities/process.js +32 -0
  33. package/runtime/package-lock.json +137 -0
  34. package/runtime/package.json +32 -0
  35. package/skill/SKILL.md +29 -0
  36. package/skill/agents/openai.yaml +6 -0
  37. package/skill/references/lifecycle.md +102 -0
  38. package/skill/references/memory-updates.md +25 -0
  39. package/skill/references/questionnaires.md +98 -0
  40. package/skill/references/scaffolding.md +38 -0
@@ -0,0 +1,2226 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { minimatch } from 'minimatch';
5
+ import { endpoints } from '../config.js';
6
+ import { sha256, stableStringify } from '../utilities/hash.js';
7
+ import { ApiResponseError, BackendUnavailableError, } from './api-client.js';
8
+ import { assertSafeToPersist, normalizeRepositoryPaths } from './offline-outbox.js';
9
+ export const validationIds = [
10
+ 'format',
11
+ 'static_analysis',
12
+ 'tests',
13
+ 'build',
14
+ 'codegen',
15
+ 'localization',
16
+ 'ui',
17
+ 'network',
18
+ 'structure',
19
+ 'diff_check',
20
+ 'privacy_scan',
21
+ 'read_only_integrity',
22
+ 'dependency_audit',
23
+ 'deploy_smoke',
24
+ ];
25
+ export class BridgeService {
26
+ dependencies;
27
+ taskQueues = new Map();
28
+ constructor(dependencies) {
29
+ this.dependencies = dependencies;
30
+ }
31
+ async sessionBootstrap(input) {
32
+ return await this.execute(async () => {
33
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd(), input.projectId);
34
+ const persistedBootstrap = normalizePersistentInput({
35
+ externalTaskId: input.externalTaskId,
36
+ objective: input.objective,
37
+ taskKind: input.taskKind,
38
+ mode: input.mode,
39
+ knownRevisions: input.knownRevisions,
40
+ }, repository.repoRoot);
41
+ assertSafeToPersist(cleanJson(persistedBootstrap));
42
+ const authentication = await this.dependencies.browserAuth.ensureAuthenticated();
43
+ if (authentication) {
44
+ return asJsonValue({ authentication, repository: publicRepository(repository) });
45
+ }
46
+ const recoveredDeliveries = await this.recoverJournalOutbox();
47
+ const outbox = await this.flushOutbox();
48
+ let projectId = repository.projectId;
49
+ if (!repository.markerPath) {
50
+ const resolved = await this.dependencies.client.request(endpoints.projectResolve, {
51
+ method: 'POST',
52
+ body: { repoFingerprint: repository.repoFingerprint },
53
+ });
54
+ const project = objectValue(resolved.data);
55
+ const projects = await this.dependencies.client.request(endpoints.projectList);
56
+ return asJsonValue({
57
+ projectBindingRequired: true,
58
+ candidate: project,
59
+ requestedProjectId: projectId,
60
+ repository: publicRepository(repository),
61
+ projects: projects.data,
62
+ outbox: { recoveredDeliveries, ...objectOrEmpty(outbox) },
63
+ });
64
+ }
65
+ if (!projectId) {
66
+ throw new Error('Repository marker does not contain a project binding');
67
+ }
68
+ const checkpointId = deterministicUuid('session.bootstrap', projectId, repository.repoFingerprint, input.externalTaskId);
69
+ const response = await this.dependencies.client.request(endpoints.sessionBootstrap, {
70
+ method: 'POST',
71
+ idempotencyKey: checkpointId,
72
+ body: cleanJson({
73
+ projectId,
74
+ externalTaskId: persistedBootstrap.externalTaskId,
75
+ objective: persistedBootstrap.objective,
76
+ taskKind: persistedBootstrap.taskKind,
77
+ mode: persistedBootstrap.mode ?? 'write',
78
+ repoFingerprint: repository.repoFingerprint,
79
+ ...(input.mode === 'read_only' ? { baselineDiffHash: repository.git.diffHash } : {}),
80
+ checkpointIdempotencyKey: checkpointId,
81
+ knownRevisions: persistedBootstrap.knownRevisions,
82
+ }),
83
+ });
84
+ const data = objectValue(response.data);
85
+ const task = objectValue(data?.task);
86
+ const session = objectValue(data?.session);
87
+ const checkpoint = objectValue(data?.checkpoint);
88
+ const checkpointEvent = objectValue(checkpoint?.event);
89
+ const checkpointPayload = objectValue(checkpointEvent?.payload);
90
+ if (!data ||
91
+ !task ||
92
+ !session ||
93
+ !checkpointEvent ||
94
+ typeof task.id !== 'string' ||
95
+ typeof session.id !== 'string' ||
96
+ checkpointEvent.idempotencyKey !== checkpointId ||
97
+ typeof checkpointPayload?.createdAt !== 'string' ||
98
+ !isIsoTimestamp(checkpointPayload.createdAt)) {
99
+ throw new Error('Session bootstrap response is missing canonical checkpoint data');
100
+ }
101
+ const pointer = {
102
+ repoFingerprint: repository.repoFingerprint,
103
+ projectId,
104
+ taskId: task.id,
105
+ taskSlug: input.externalTaskId,
106
+ sessionId: session.id,
107
+ lastSequence: numericSequence(task.lastSequence),
108
+ taskVersion: numericTaskVersion(task.lockVersion),
109
+ resumeConflicts: [],
110
+ };
111
+ await this.dependencies.activeContexts.save(pointer);
112
+ await this.dependencies.journal.apply({
113
+ eventId: checkpointId,
114
+ taskId: task.id,
115
+ projectId,
116
+ taskSlug: input.externalTaskId,
117
+ checkpointType: 'bootstrap',
118
+ summary: 'Engineering Memory context bootstrapped',
119
+ documents: {
120
+ state: {
121
+ objective: persistedBootstrap.objective,
122
+ phase: 'bootstrap',
123
+ nextAction: 'Complete discovery and record the discovery checkpoint',
124
+ forbiddenActions: ['Commit, push, publish or close before task verification passes'],
125
+ },
126
+ },
127
+ createdAt: checkpointPayload.createdAt,
128
+ });
129
+ await this.seedResumeSnapshot(pointer, data);
130
+ return asJsonValue({
131
+ ...data,
132
+ repository: publicRepository(repository),
133
+ localJournal: await this.dependencies.journal.load(projectId, input.externalTaskId),
134
+ outbox: { recoveredDeliveries, ...objectOrEmpty(outbox) },
135
+ });
136
+ });
137
+ }
138
+ async sessionResume(input) {
139
+ return await this.execute(async () => {
140
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd(), input.projectId);
141
+ const authentication = await this.dependencies.browserAuth.ensureAuthenticated();
142
+ if (authentication) {
143
+ return asJsonValue({ authentication, repository: publicRepository(repository) });
144
+ }
145
+ let pointer = await this.dependencies.activeContexts.load(repository.repoFingerprint);
146
+ const projectId = input.projectId ?? repository.projectId ?? pointer?.projectId;
147
+ const taskSlug = input.taskSlug ?? pointer?.taskSlug;
148
+ const sessionId = input.sessionId ?? pointer?.sessionId;
149
+ if (!projectId || !taskSlug || !sessionId) {
150
+ throw new Error('No active Engineering Memory task is available for this repository');
151
+ }
152
+ if ((pointer &&
153
+ (pointer.projectId !== projectId ||
154
+ pointer.taskSlug !== taskSlug ||
155
+ pointer.sessionId !== sessionId)) ||
156
+ (repository.projectId && repository.projectId !== projectId)) {
157
+ throw new Error('Active task pointer does not match the repository project binding');
158
+ }
159
+ if (pointer?.closeIntent) {
160
+ const recoveredClose = await this.retryCloseIntent(repository, pointer);
161
+ return asJsonValue({
162
+ backend: { close: recoveredClose },
163
+ repository: publicRepository(repository),
164
+ localJournal: await this.dependencies.journal.load(projectId, taskSlug),
165
+ synchronization: {
166
+ taskClosed: true,
167
+ recoveredCloseAfterResponseLoss: true,
168
+ editLeaseAllowed: false,
169
+ offlineDevelopmentOnly: false,
170
+ },
171
+ });
172
+ }
173
+ const recoveredDeliveries = await this.recoverJournalOutbox(projectId, taskSlug);
174
+ const outbox = await this.flushOutbox();
175
+ const afterSequence = input.afterSequence ?? 0;
176
+ const response = await this.dependencies.client.request(endpoints.sessionResume, {
177
+ method: 'POST',
178
+ cacheKey: resumeCacheKey(sessionId, repository.repoFingerprint, afterSequence),
179
+ allowStaleOnUnavailable: true,
180
+ body: {
181
+ sessionId,
182
+ repoFingerprint: repository.repoFingerprint,
183
+ afterSequence,
184
+ },
185
+ });
186
+ const backend = objectValue(response.data);
187
+ const backendTask = objectValue(backend?.task);
188
+ const backendSession = objectValue(backend?.session);
189
+ if (!backend ||
190
+ !backendTask ||
191
+ !backendSession ||
192
+ typeof backendTask.id !== 'string' ||
193
+ typeof backendSession.id !== 'string' ||
194
+ backendSession.id !== sessionId) {
195
+ throw new Error('Session resume response is missing the active task snapshot');
196
+ }
197
+ let localJournal = await this.dependencies.journal.load(projectId, taskSlug);
198
+ let pendingOutbox = await this.dependencies.outbox.list();
199
+ const responseSource = this.dependencies.client.getResponseSource(response);
200
+ const recoveredVerification = responseSource !== 'stale_cache' && pointer
201
+ ? await this.retryVerificationRecovery(repository, pointer, backend)
202
+ : null;
203
+ if (recoveredVerification) {
204
+ pointer = await this.dependencies.activeContexts.load(repository.repoFingerprint);
205
+ }
206
+ let hydratedFromBackend = false;
207
+ let mergedFromBackend = false;
208
+ let blockedJournalEventIds = [];
209
+ const backendEvents = Array.isArray(backend.events) ? backend.events : [];
210
+ const backendDocuments = Array.isArray(backend.documents) ? backend.documents : [];
211
+ if (responseSource !== 'stale_cache' &&
212
+ afterSequence === 0 &&
213
+ backendEvents.length === numericSequence(backendTask.lastSequence) &&
214
+ backendDocuments.length === 5) {
215
+ const hadLocalProjection = Boolean(localJournal.projection);
216
+ const hydration = await this.dependencies.journal.hydrateFromBackend({
217
+ projectId,
218
+ taskId: backendTask.id,
219
+ taskSlug,
220
+ lastSequence: numericSequence(backendTask.lastSequence),
221
+ taskVersion: numericTaskVersion(backendTask.lockVersion),
222
+ events: backendEvents,
223
+ documents: backendDocuments,
224
+ });
225
+ blockedJournalEventIds = hydration.blockedEventIds;
226
+ const entries = await this.dependencies.outbox.list();
227
+ for (const eventId of hydration.synchronizedEventIds) {
228
+ const entry = entries.find((value) => value.journalRef?.eventId === eventId);
229
+ if (entry) {
230
+ await this.dependencies.outbox.acknowledge(entry.id);
231
+ }
232
+ }
233
+ for (const delivery of hydration.pendingDeliveries) {
234
+ if (hydration.blockedEventIds.includes(delivery.eventId)) {
235
+ continue;
236
+ }
237
+ const entry = entries.find((value) => value.journalRef?.eventId === delivery.eventId);
238
+ if (entry) {
239
+ await this.dependencies.outbox.replaceBody(entry.id, delivery.body);
240
+ }
241
+ else {
242
+ await this.dependencies.outbox.enqueue({
243
+ operation: delivery.operation,
244
+ method: delivery.method,
245
+ path: delivery.path,
246
+ body: delivery.body,
247
+ idempotencyKey: delivery.idempotencyKey,
248
+ journalRef: {
249
+ projectId: delivery.projectId,
250
+ taskSlug: delivery.taskSlug,
251
+ eventId: delivery.eventId,
252
+ },
253
+ });
254
+ }
255
+ }
256
+ localJournal = await this.dependencies.journal.load(projectId, taskSlug);
257
+ pendingOutbox = await this.dependencies.outbox.list();
258
+ hydratedFromBackend = !hadLocalProjection;
259
+ mergedFromBackend = hadLocalProjection;
260
+ }
261
+ const localDocuments = localJournal.projection
262
+ ? await this.dependencies.journal.readDocuments(projectId, taskSlug)
263
+ : {};
264
+ const conflicts = resumeConflicts(backend, backendTask, pointer, localJournal, localDocuments, pendingOutbox, responseSource);
265
+ const resumedLease = objectValue(backend.activeLease);
266
+ const resumeIdentityMatches = pointer !== null &&
267
+ pointer.repoFingerprint === repository.repoFingerprint &&
268
+ pointer.projectId === projectId &&
269
+ pointer.taskId === backendTask.id &&
270
+ pointer.sessionId === sessionId &&
271
+ backendTask.projectId === projectId &&
272
+ backendSession.id === sessionId;
273
+ const offlineDevelopmentAllowed = responseSource === 'stale_cache' &&
274
+ resumeIdentityMatches &&
275
+ conflicts.every(isOfflineDevelopmentWarning) &&
276
+ isOfflineLeaseUsable(resumedLease, sessionId, pointer?.changeBaseline?.leasePaths ?? [], pointer?.changeBaseline?.diffHash);
277
+ const resumedBaseline = pointer?.taskId === backendTask.id &&
278
+ pointer.sessionId === sessionId &&
279
+ pointer.changeBaseline &&
280
+ (resumedLease?.baselineDiffHash === pointer.changeBaseline.diffHash ||
281
+ objectValue(backend.verificationRecovery)?.diffHash === repository.git.diffHash)
282
+ ? pointer.changeBaseline
283
+ : undefined;
284
+ const resumedLastSequence = responseSource === 'stale_cache' && pointer
285
+ ? Math.max(pointer.lastSequence, numericSequence(backendTask.lastSequence), localJournal.projection?.appliedEventIds.length ?? 0)
286
+ : numericSequence(backendTask.lastSequence);
287
+ const resumedTaskVersion = responseSource === 'stale_cache' && pointer
288
+ ? Math.max(pointer.taskVersion, numericTaskVersion(backendTask.lockVersion))
289
+ : numericTaskVersion(backendTask.lockVersion);
290
+ const hasBlockingConflict = conflicts.some((conflict) => !isOfflineDevelopmentWarning(conflict));
291
+ if (pointer && (!resumeIdentityMatches || hasBlockingConflict)) {
292
+ await this.dependencies.activeContexts.save({ ...pointer, resumeConflicts: conflicts });
293
+ }
294
+ else {
295
+ await this.dependencies.activeContexts.save({
296
+ repoFingerprint: repository.repoFingerprint,
297
+ projectId,
298
+ taskId: backendTask.id,
299
+ taskSlug,
300
+ sessionId,
301
+ lastSequence: resumedLastSequence,
302
+ taskVersion: resumedTaskVersion,
303
+ resumeConflicts: conflicts,
304
+ ...(resumedBaseline ? { changeBaseline: resumedBaseline } : {}),
305
+ ...(pointer?.verificationIntent
306
+ ? { verificationIntent: pointer.verificationIntent }
307
+ : {}),
308
+ ...(pointer?.closeIntent ? { closeIntent: pointer.closeIntent } : {}),
309
+ });
310
+ }
311
+ return asJsonValue({
312
+ backend,
313
+ repository: publicRepository(repository),
314
+ localJournal,
315
+ synchronization: {
316
+ backendSource: responseSource,
317
+ backendFresh: responseSource !== 'stale_cache',
318
+ hydratedFromBackend,
319
+ mergedFromBackend,
320
+ blockedJournalEventIds,
321
+ recoveredDeliveries,
322
+ outbox,
323
+ pendingOutboxCount: pendingOutbox.length,
324
+ pendingOutbox: pendingOutbox.map((entry) => ({
325
+ id: entry.id,
326
+ operation: entry.operation,
327
+ attempts: entry.attempts,
328
+ lastError: entry.lastError,
329
+ createdAt: entry.createdAt,
330
+ })),
331
+ localPendingDeliveryCount: localJournal.pendingDeliveryCount,
332
+ conflicts,
333
+ requiresAttention: conflicts.length > 0,
334
+ editLeaseAllowed: conflicts.length === 0 || offlineDevelopmentAllowed,
335
+ offlineDevelopmentOnly: offlineDevelopmentAllowed,
336
+ recoveredVerification,
337
+ },
338
+ });
339
+ });
340
+ }
341
+ async contextPrepareChange(input) {
342
+ return await this.execute(async () => {
343
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
344
+ await this.recoverJournalOutbox();
345
+ const outbox = await this.flushOutbox();
346
+ const pendingOutbox = await this.dependencies.outbox.list();
347
+ const pointer = await this.dependencies.activeContexts.load(repository.repoFingerprint);
348
+ if (pointer?.verificationIntent || pointer?.closeIntent) {
349
+ throw new Error('Pending lifecycle intent must be recovered before preparing changes');
350
+ }
351
+ const conflicts = [
352
+ ...(pointer?.resumeConflicts ?? []),
353
+ ...(pendingOutbox.length > 0 ? ['pending_outbox_delivery'] : []),
354
+ ];
355
+ const offlineResume = pointer?.resumeConflicts.includes('backend_resume_is_stale') ?? false;
356
+ const offlineWarnings = new Set([
357
+ 'backend_resume_is_stale',
358
+ 'pending_outbox_delivery',
359
+ 'pending_local_journal_delivery',
360
+ 'backend_local_sequence_mismatch',
361
+ ]);
362
+ const blockingConflicts = conflicts.filter((value) => !(offlineResume &&
363
+ (offlineWarnings.has(value) || value.startsWith('backend_local_document_mismatch:'))));
364
+ if (blockingConflicts.length > 0) {
365
+ return asJsonValue({
366
+ editLeaseGranted: false,
367
+ conflicts: [...new Set(blockingConflicts)],
368
+ outbox,
369
+ repository: publicRepository(repository),
370
+ });
371
+ }
372
+ const activeBaseline = pointer?.sessionId === input.sessionId ? pointer.changeBaseline : undefined;
373
+ const changedPaths = normalizeChangedPaths([
374
+ ...(activeBaseline?.leasePaths ?? []),
375
+ ...input.changedPaths,
376
+ ]);
377
+ const baselineDiffHash = activeBaseline?.diffHash ?? repository.git.diffHash;
378
+ let transitionTaskVersion;
379
+ if (input.transitionToWrite) {
380
+ if (!pointer || pointer.sessionId !== input.sessionId) {
381
+ throw new Error('Read-only transition requires the active repository session');
382
+ }
383
+ const refreshed = await this.activeTaskSnapshot(pointer.taskId, pointer.projectId, repository.repoRoot);
384
+ transitionTaskVersion = refreshed.taskVersion;
385
+ }
386
+ const body = cleanJson({
387
+ sessionId: input.sessionId,
388
+ changedPaths,
389
+ baselineDiffHash,
390
+ ...(input.transitionToWrite
391
+ ? { transitionToWrite: true, expectedTaskVersion: transitionTaskVersion }
392
+ : {}),
393
+ });
394
+ const response = await this.dependencies.client.request(endpoints.contextPrepareChange, {
395
+ method: 'POST',
396
+ body,
397
+ ...(input.transitionToWrite
398
+ ? {}
399
+ : {
400
+ cacheKey: sha256(`context.prepare_change\n${stableStringify(body)}`),
401
+ allowStaleOnUnavailable: true,
402
+ }),
403
+ });
404
+ const responseSource = this.dependencies.client.getResponseSource(response);
405
+ if (responseSource === 'stale_cache') {
406
+ validateOfflineLease(response.data, input.sessionId, changedPaths, baselineDiffHash);
407
+ await this.dependencies.activeContexts.setChangeBaseline(repository.repoFingerprint, input.sessionId, repository.git, changedPaths);
408
+ return asJsonValue({
409
+ ...objectOrEmpty(response.data),
410
+ editLeaseGranted: true,
411
+ offlineDevelopmentOnly: true,
412
+ strictVerifyBlocked: true,
413
+ backendSource: responseSource,
414
+ conflicts: ['backend_resume_is_stale'],
415
+ repository: publicRepository(repository),
416
+ });
417
+ }
418
+ if (pointer?.resumeConflicts.includes('backend_resume_is_stale')) {
419
+ await this.dependencies.activeContexts.save({
420
+ ...pointer,
421
+ resumeConflicts: pointer.resumeConflicts.filter((value) => value !== 'backend_resume_is_stale'),
422
+ });
423
+ }
424
+ const preparedTask = objectValue(objectValue(response.data)?.task);
425
+ await this.dependencies.activeContexts.setChangeBaseline(repository.repoFingerprint, input.sessionId, repository.git, changedPaths, preparedTask &&
426
+ typeof preparedTask.id === 'string' &&
427
+ typeof preparedTask.lockVersion === 'number'
428
+ ? {
429
+ taskId: preparedTask.id,
430
+ taskVersion: numericTaskVersion(preparedTask.lockVersion),
431
+ lastSequence: numericSequence(preparedTask.lastSequence),
432
+ }
433
+ : undefined);
434
+ const preparedPointer = await this.dependencies.activeContexts.load(repository.repoFingerprint);
435
+ if (preparedPointer) {
436
+ const preparedData = objectValue(response.data);
437
+ await this.seedResumeSnapshot(preparedPointer, {
438
+ ...objectOrEmpty(response.data),
439
+ ...(preparedData?.lease ? { activeLease: preparedData.lease } : {}),
440
+ });
441
+ }
442
+ return asJsonValue({
443
+ ...objectOrEmpty(response.data),
444
+ editLeaseGranted: true,
445
+ offlineDevelopmentOnly: false,
446
+ strictVerifyBlocked: false,
447
+ backendSource: responseSource,
448
+ repository: publicRepository(repository),
449
+ });
450
+ });
451
+ }
452
+ async contextRefresh(input) {
453
+ return await this.execute(async () => {
454
+ const response = await this.dependencies.client.request(endpoints.contextRefresh, {
455
+ method: 'POST',
456
+ body: { sessionId: input.sessionId },
457
+ });
458
+ await this.dependencies.activeContexts.clearSessionConflict(input.sessionId, 'approved_memory_revision_requires_refresh');
459
+ return asJsonValue({
460
+ ...objectOrEmpty(response.data),
461
+ requiresPrepareChange: true,
462
+ });
463
+ });
464
+ }
465
+ async memoryQuery(input) {
466
+ return await this.execute(async () => {
467
+ const body = cleanJson(input);
468
+ const response = await this.dependencies.client.request(endpoints.memoryQuery, {
469
+ method: 'POST',
470
+ body,
471
+ cacheKey: sha256(`memory.query\n${stableStringify(body)}`),
472
+ allowStaleOnUnavailable: true,
473
+ });
474
+ return asJsonValue({ resources: response.data });
475
+ });
476
+ }
477
+ async memoryHistory(input) {
478
+ return await this.execute(async () => {
479
+ const body = cleanJson({
480
+ sessionId: input.sessionId,
481
+ resourceKeys: input.resourceKeys,
482
+ paths: input.paths,
483
+ depth: input.depth,
484
+ });
485
+ const response = await this.dependencies.client.request(endpoints.memoryHistory, {
486
+ method: 'POST',
487
+ body,
488
+ });
489
+ const relatedBranches = await this.branchTaskReferences(input.repoRoot ?? process.cwd(), input.paths ?? []);
490
+ return asJsonValue({ history: response.data, relatedBranches });
491
+ });
492
+ }
493
+ async branchTaskReferences(repoRoot, paths) {
494
+ if (paths.length === 0)
495
+ return [];
496
+ const log = await this.dependencies.repositories.git.pathHistorySubjects(repoRoot, paths);
497
+ const seen = new Map();
498
+ for (const subject of log) {
499
+ for (const match of subject.matchAll(/[A-Z][A-Z0-9]+-\d+/g)) {
500
+ if (!seen.has(match[0]))
501
+ seen.set(match[0], subject);
502
+ }
503
+ }
504
+ return [...seen.entries()].map(([taskReference, subject]) => ({
505
+ taskReference,
506
+ subject,
507
+ }));
508
+ }
509
+ async memoryProposeRevision(input) {
510
+ return await this.execute(async () => {
511
+ const { repoRoot, ...rawProposal } = input;
512
+ const proposal = normalizePersistentInput(rawProposal, repoRoot ?? process.cwd());
513
+ assertSafeToPersist(cleanJson(proposal));
514
+ const taskSnapshot = input.taskId
515
+ ? await this.activeTaskSnapshot(input.taskId, input.projectId, repoRoot)
516
+ : null;
517
+ const body = cleanJson({
518
+ ...proposal,
519
+ ...(taskSnapshot ? { expectedTaskVersion: taskSnapshot.taskVersion } : {}),
520
+ });
521
+ return await this.mutateWithOutbox('memory.propose_revision', endpoints.memoryProposeRevision, body);
522
+ });
523
+ }
524
+ async memoryListProposals(input) {
525
+ return await this.execute(async () => {
526
+ const response = await this.dependencies.client.request(endpoints.memoryListProposals, { method: 'POST', body: cleanJson(input) });
527
+ return asJsonValue({ proposals: response.data });
528
+ });
529
+ }
530
+ async memoryReviewProposal(input) {
531
+ return await this.execute(async () => {
532
+ const note = normalizePersistentInput(input.note, process.cwd());
533
+ assertSafeToPersist(cleanJson({ note }));
534
+ const response = await this.dependencies.client.request(endpoints.memoryReviewProposal(input.proposalId), {
535
+ method: 'POST',
536
+ body: cleanJson({
537
+ projectId: input.projectId,
538
+ decision: input.decision,
539
+ note,
540
+ }),
541
+ });
542
+ const result = objectValue(response.data);
543
+ const invalidatedTaskIds = Array.isArray(result?.invalidatedTaskIds)
544
+ ? result.invalidatedTaskIds.filter((value) => typeof value === 'string')
545
+ : [];
546
+ for (const taskId of invalidatedTaskIds) {
547
+ await this.dependencies.gate.invalidateTask(taskId);
548
+ }
549
+ await this.dependencies.activeContexts.markTasksConflict(invalidatedTaskIds, 'approved_memory_revision_requires_refresh');
550
+ return asJsonValue({
551
+ ...objectOrEmpty(response.data),
552
+ contextRefreshRequired: invalidatedTaskIds.length > 0,
553
+ });
554
+ });
555
+ }
556
+ async taskCheckpoint(input) {
557
+ return await this.writeTaskEvent(input, false);
558
+ }
559
+ async taskRecordCorrection(input) {
560
+ const reference = input.correctionRef ?? input.summary;
561
+ const result = await this.writeTaskEvent({
562
+ ...input,
563
+ checkpointType: 'correction',
564
+ documents: {
565
+ ...input.documents,
566
+ decisions: [
567
+ ...(input.documents?.decisions ?? []),
568
+ correctionScopeLine(input.correctionScope ?? 'pending', reference),
569
+ ],
570
+ },
571
+ }, true);
572
+ if (!result.ok)
573
+ return result;
574
+ const pending = await this.pendingCorrectionScopes(input.projectId, input.taskSlug);
575
+ return {
576
+ ...result,
577
+ data: asJsonValue({
578
+ ...objectOrEmpty(result.data ?? null),
579
+ correctionScopePending: pending,
580
+ ...(pending.length > 0
581
+ ? {
582
+ nextAction: 'Ask the user in this reply where each listed correction belongs: only this task, permanent for this project, permanent for the organization, or a change to the product defaults that reaches every customer. Record the answer with task.record_correction using the same correctionRef. Verification stays blocked until every correction has a scope.',
583
+ }
584
+ : {}),
585
+ }),
586
+ };
587
+ }
588
+ async pendingCorrectionScopes(projectId, taskSlug) {
589
+ const journal = await this.dependencies.journal.load(projectId, taskSlug);
590
+ const pending = new Set();
591
+ for (const line of journal.projection?.decisions ?? []) {
592
+ const parsed = parseCorrectionScopeLine(line.summary);
593
+ if (!parsed)
594
+ continue;
595
+ if (parsed.scope === 'pending')
596
+ pending.add(parsed.reference);
597
+ else
598
+ pending.delete(parsed.reference);
599
+ }
600
+ return [...pending];
601
+ }
602
+ async taskSelfReview(input) {
603
+ return await this.execute(async () => {
604
+ const findings = normalizePersistentInput(input.findings, input.repoRoot ?? process.cwd());
605
+ assertSafeToPersist(cleanJson({ findings }));
606
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
607
+ const snapshot = await this.activeTaskSnapshot(input.taskId, undefined, input.repoRoot);
608
+ const response = await this.dependencies.client.request(endpoints.taskSelfReview, {
609
+ method: 'POST',
610
+ body: cleanJson({
611
+ taskId: input.taskId,
612
+ expectedTaskVersion: snapshot.taskVersion,
613
+ diffHash: repository.git.diffHash,
614
+ reviewedResourceIds: input.reviewedResourceIds,
615
+ findings,
616
+ }),
617
+ });
618
+ await this.captureTaskSnapshot(asJsonValue({ task: response.data }));
619
+ return asJsonValue({
620
+ selfReview: {
621
+ diffHash: repository.git.diffHash,
622
+ reviewedResourceIds: input.reviewedResourceIds,
623
+ findings: findings.length,
624
+ },
625
+ });
626
+ });
627
+ }
628
+ async taskReconcile(input) {
629
+ return await this.execute(async () => {
630
+ const reason = normalizePersistentInput(input.reason, input.repoRoot ?? process.cwd());
631
+ assertSafeToPersist(cleanJson({
632
+ resourceId: input.resourceId,
633
+ type: input.type,
634
+ proposalId: input.proposalId,
635
+ revisionId: input.revisionId,
636
+ reason,
637
+ }));
638
+ const snapshot = await this.activeTaskSnapshot(input.taskId, undefined, input.repoRoot);
639
+ const response = await this.dependencies.client.request(endpoints.taskReconcile, {
640
+ method: 'POST',
641
+ body: cleanJson({
642
+ taskId: input.taskId,
643
+ resourceId: input.resourceId,
644
+ type: input.type,
645
+ proposalId: input.proposalId,
646
+ revisionId: input.revisionId,
647
+ reason,
648
+ expectedTaskVersion: snapshot.taskVersion,
649
+ }),
650
+ });
651
+ await this.dependencies.gate.invalidateTask(input.taskId);
652
+ return asJsonValue({ reconciliation: response.data });
653
+ });
654
+ }
655
+ async taskResolvePendingDelivery(input) {
656
+ return await this.execute(async () => {
657
+ const entry = await this.dependencies.outbox.get(input.outboxId);
658
+ if (!entry) {
659
+ throw new Error('Pending delivery could not be found');
660
+ }
661
+ const metadata = {
662
+ id: entry.id,
663
+ operation: entry.operation,
664
+ attempts: entry.attempts,
665
+ lastError: entry.lastError,
666
+ createdAt: entry.createdAt,
667
+ journalBacked: Boolean(entry.journalRef),
668
+ };
669
+ if (input.action === 'inspect') {
670
+ return asJsonValue({ pendingDelivery: metadata });
671
+ }
672
+ if (!input.confirm) {
673
+ return asJsonValue({
674
+ confirmationRequired: true,
675
+ action: input.action,
676
+ pendingDelivery: metadata,
677
+ });
678
+ }
679
+ const taskId = taskIdFromDeliverySafe(entry);
680
+ if (input.action === 'discard') {
681
+ if (entry.journalRef) {
682
+ await this.dependencies.journal.discardDelivery(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId);
683
+ }
684
+ await this.dependencies.outbox.acknowledge(entry.id);
685
+ if (taskId) {
686
+ await this.dependencies.gate.invalidateTask(taskId);
687
+ }
688
+ return asJsonValue({ discarded: true, taskId, pendingDelivery: metadata });
689
+ }
690
+ if (!entry.lastError?.startsWith('api_409_')) {
691
+ throw new Error('Only a confirmed backend version conflict can be rebased');
692
+ }
693
+ if (entry.journalRef) {
694
+ const taskSnapshot = await this.refreshTaskPointer(taskId, entry.journalRef.projectId);
695
+ const rebased = await this.dependencies.journal.rebaseDelivery(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId, taskSnapshot.taskVersion);
696
+ await this.dependencies.outbox.replaceBody(entry.id, rebased.body);
697
+ if (taskId) {
698
+ await this.dependencies.gate.invalidateTask(taskId);
699
+ }
700
+ return asJsonValue({
701
+ rebased: true,
702
+ taskId,
703
+ expectedTaskVersion: taskSnapshot.taskVersion,
704
+ pendingDelivery: metadata,
705
+ });
706
+ }
707
+ if (entry.operation !== 'memory.propose_revision' || !taskId) {
708
+ throw new Error('This delivery cannot be safely rebased and must be discarded or retried');
709
+ }
710
+ if (!input.proposalRebaseApproved || input.confirmedBaseRevision === undefined) {
711
+ return asJsonValue({
712
+ confirmationRequired: true,
713
+ requiresNativeApproval: true,
714
+ requiresFreshBaseRevision: true,
715
+ pendingDelivery: metadata,
716
+ });
717
+ }
718
+ const proposal = objectValue(entry.body);
719
+ const projectId = typeof proposal?.projectId === 'string' ? proposal.projectId : null;
720
+ const queuedBaseRevision = proposal?.baseRevision;
721
+ if (!projectId || typeof queuedBaseRevision !== 'number') {
722
+ throw new Error('Queued proposal cannot establish its project revision contract');
723
+ }
724
+ if (queuedBaseRevision !== input.confirmedBaseRevision) {
725
+ return asJsonValue({
726
+ rebased: false,
727
+ resourceRevisionChanged: true,
728
+ queuedBaseRevision,
729
+ confirmedBaseRevision: input.confirmedBaseRevision,
730
+ requiresNewProposal: true,
731
+ pendingDelivery: metadata,
732
+ });
733
+ }
734
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd(), projectId);
735
+ const pointer = await this.requireActivePointer(repository.repoFingerprint, taskId);
736
+ const refreshed = await this.dependencies.client.request(endpoints.contextRefresh, {
737
+ method: 'POST',
738
+ body: { sessionId: pointer.sessionId },
739
+ });
740
+ const refreshData = objectValue(refreshed.data);
741
+ const refreshedTask = objectValue(refreshData?.task);
742
+ if (!refreshedTask || refreshedTask.id !== taskId) {
743
+ throw new Error('Context refresh did not return the queued proposal task');
744
+ }
745
+ const expectedTaskVersion = numericTaskVersion(refreshedTask.lockVersion);
746
+ const rebasedBody = cleanJson({ ...proposal, expectedTaskVersion });
747
+ await this.dependencies.outbox.replaceBody(entry.id, rebasedBody);
748
+ await this.captureTaskSnapshot(refreshed.data);
749
+ await this.dependencies.gate.invalidateTask(taskId);
750
+ return asJsonValue({
751
+ rebased: true,
752
+ taskId,
753
+ expectedTaskVersion,
754
+ confirmedBaseRevision: input.confirmedBaseRevision,
755
+ requiresPrepareChange: true,
756
+ pendingDelivery: metadata,
757
+ });
758
+ }, true);
759
+ }
760
+ async taskVerify(input) {
761
+ return await this.execute(async () => {
762
+ return await this.taskExclusive(input.taskId, async () => {
763
+ await this.recoverJournalOutbox();
764
+ const outbox = await this.flushOutbox();
765
+ const pendingOutboxCount = (await this.dependencies.outbox.list()).length;
766
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
767
+ if (pendingOutboxCount > 0) {
768
+ return asJsonValue({
769
+ verified: false,
770
+ reason: 'pending_outbox',
771
+ pendingOutboxCount,
772
+ outbox,
773
+ diffHash: repository.git.diffHash,
774
+ });
775
+ }
776
+ const pointer = await this.requireActivePointer(repository.repoFingerprint, input.taskId, false, 'verify');
777
+ const pendingCorrectionScopes = await this.pendingCorrectionScopes(pointer.projectId, pointer.taskSlug);
778
+ if (pendingCorrectionScopes.length > 0) {
779
+ return asJsonValue({
780
+ verified: false,
781
+ reason: 'pending_correction_scope',
782
+ pendingCorrectionScopes,
783
+ nextAction: 'Ask the user where each listed correction belongs — only this task, permanent for this project, permanent for the organization, or a change to the product defaults that reaches every customer — then record the answer with task.record_correction using the same correctionRef.',
784
+ diffHash: repository.git.diffHash,
785
+ });
786
+ }
787
+ let taskChangedPaths = [];
788
+ let taskChanges = [];
789
+ const snapshotResponse = await this.dependencies.client.request(endpoints.sessionResume, {
790
+ method: 'POST',
791
+ body: {
792
+ sessionId: input.sessionId,
793
+ repoFingerprint: repository.repoFingerprint,
794
+ afterSequence: 0,
795
+ },
796
+ });
797
+ const snapshot = objectValue(snapshotResponse.data);
798
+ const snapshotTask = objectValue(snapshot?.task);
799
+ const activeLease = objectValue(snapshot?.activeLease);
800
+ if (!snapshotTask || snapshotTask.id !== input.taskId) {
801
+ throw new Error('Current backend task snapshot does not match verification');
802
+ }
803
+ const recoveredVerification = await this.retryVerificationRecovery(repository, pointer, snapshot);
804
+ if (recoveredVerification) {
805
+ return asJsonValue({
806
+ ...recoveredVerification,
807
+ repository: publicRepository(repository),
808
+ });
809
+ }
810
+ const taskMode = normalizeTaskMode(snapshotTask.mode);
811
+ const resourceDiscoveryPolicy = readResourceDiscoveryPolicy(snapshot, activeLease);
812
+ const validations = normalizePersistentInput(input.validations, repository.repoRoot);
813
+ if (taskMode === 'read_only') {
814
+ if (input.leaseId || input.changedPaths?.length) {
815
+ throw new Error('Read-only task verification does not accept a lease or changed paths');
816
+ }
817
+ }
818
+ else {
819
+ const baseline = pointer.changeBaseline;
820
+ const leaseChangedPaths = Array.isArray(activeLease?.changedPaths)
821
+ ? activeLease.changedPaths.filter((path) => typeof path === 'string')
822
+ : [];
823
+ if (!baseline || activeLease?.baselineDiffHash !== baseline.diffHash) {
824
+ throw new Error('Write task verification requires its locally pinned change baseline');
825
+ }
826
+ taskChanges = manifestDelta(baseline.changedPaths, repository.git.changedPaths);
827
+ taskChangedPaths = taskChanges.map((entry) => entry.path).sort();
828
+ if (input.changedPaths &&
829
+ stableStringify(normalizeChangedPaths(input.changedPaths)) !==
830
+ stableStringify(taskChangedPaths)) {
831
+ throw new Error('Reported changed paths do not match the actual Git manifest');
832
+ }
833
+ if (taskChangedPaths.length === 0 ||
834
+ !activeLease ||
835
+ !input.leaseId ||
836
+ activeLease.id !== input.leaseId ||
837
+ !pathsContainAll(baseline.leasePaths, taskChangedPaths) ||
838
+ !pathsContainAll(leaseChangedPaths, taskChangedPaths)) {
839
+ throw new Error('Write task verification requires the current backend change lease');
840
+ }
841
+ assertNewResourceEvidence(taskChanges.flatMap((entry) => newMemoryResourceCandidate(entry, resourceDiscoveryPolicy)), input.newResources ?? []);
842
+ }
843
+ assertSafeToPersist(cleanJson(validations));
844
+ assertValidationEvidence(validations);
845
+ const pathChangeManifest = taskMode === 'read_only'
846
+ ? []
847
+ : await buildPathChangeManifest(repository.repoRoot, taskChanges);
848
+ const verifyBody = cleanJson({
849
+ taskId: input.taskId,
850
+ sessionId: input.sessionId,
851
+ expectedTaskVersion: numericTaskVersion(snapshotTask.lockVersion),
852
+ ...(taskMode !== 'read_only'
853
+ ? {
854
+ leaseId: input.leaseId,
855
+ baselineDiffHash: activeLease?.baselineDiffHash,
856
+ }
857
+ : {}),
858
+ changedPaths: taskMode === 'read_only' ? [] : taskChangedPaths,
859
+ pathChanges: pathChangeManifest,
860
+ diffHash: repository.git.diffHash,
861
+ validations,
862
+ pendingOutboxCount,
863
+ newResources: input.newResources,
864
+ });
865
+ await this.dependencies.activeContexts.setVerificationIntent(repository.repoFingerprint, {
866
+ mode: taskMode,
867
+ body: verifyBody,
868
+ taskChanges,
869
+ });
870
+ let response;
871
+ try {
872
+ response = await this.dependencies.client.request(endpoints.taskVerify, {
873
+ method: 'POST',
874
+ body: verifyBody,
875
+ });
876
+ }
877
+ catch (error) {
878
+ if (!isBackendUnavailableError(error)) {
879
+ await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint);
880
+ }
881
+ throw error;
882
+ }
883
+ const responseData = objectValue(response.data);
884
+ if (responseData?.verified === true) {
885
+ const taskVersion = numericTaskVersion(responseData.taskVersion);
886
+ await this.dependencies.gate.record({
887
+ taskId: input.taskId,
888
+ projectId: repository.projectId,
889
+ repoFingerprint: repository.repoFingerprint,
890
+ repoRoot: repository.repoRoot,
891
+ diffHash: repository.git.diffHash,
892
+ taskVersion,
893
+ taskChanges,
894
+ });
895
+ await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint);
896
+ }
897
+ else {
898
+ await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint);
899
+ }
900
+ return asJsonValue({
901
+ ...objectOrEmpty(response.data),
902
+ mode: taskMode,
903
+ repository: publicRepository(repository),
904
+ });
905
+ });
906
+ });
907
+ }
908
+ async taskClose(input) {
909
+ return await this.execute(async () => {
910
+ return await this.taskExclusive(input.taskId, async () => {
911
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
912
+ const pointer = await this.requireActivePointer(repository.repoFingerprint, input.taskId, false, 'close');
913
+ if (pointer.closeIntent) {
914
+ return await this.retryCloseIntent(repository, pointer);
915
+ }
916
+ const snapshotResponse = await this.dependencies.client.request(endpoints.sessionResume, {
917
+ method: 'POST',
918
+ body: {
919
+ sessionId: pointer.sessionId,
920
+ repoFingerprint: repository.repoFingerprint,
921
+ afterSequence: 0,
922
+ },
923
+ });
924
+ const snapshot = objectValue(snapshotResponse.data);
925
+ const task = objectValue(snapshot?.task);
926
+ if (!task || task.id !== input.taskId) {
927
+ throw new Error('Current backend task snapshot does not match task close');
928
+ }
929
+ const taskChanges = pointer.changeBaseline
930
+ ? manifestDelta(pointer.changeBaseline.changedPaths, repository.git.changedPaths)
931
+ : [];
932
+ const closeBody = {
933
+ taskId: input.taskId,
934
+ sessionId: pointer.sessionId,
935
+ expectedTaskVersion: numericTaskVersion(task.lockVersion),
936
+ diffHash: repository.git.diffHash,
937
+ };
938
+ await this.dependencies.activeContexts.setCloseIntent(repository.repoFingerprint, {
939
+ body: closeBody,
940
+ taskChanges,
941
+ });
942
+ return await this.deliverCloseIntent(repository, closeBody, taskChanges, false);
943
+ });
944
+ });
945
+ }
946
+ async projectSetup(input) {
947
+ return await this.execute(async () => {
948
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
949
+ const body = cleanJson({
950
+ organizationId: input.organizationId,
951
+ projectName: input.projectName,
952
+ projectSlug: input.projectSlug ?? slugify(input.projectName),
953
+ repoFingerprint: repository.repoFingerprint,
954
+ framework: input.framework,
955
+ figmaConfig: input.figmaConfig,
956
+ initialProjectProfile: input.initialProjectProfile,
957
+ });
958
+ if ('resourceDiscovery' in input.initialProjectProfile.metadata) {
959
+ throw new Error('Project setup metadata cannot override canonical resource discovery');
960
+ }
961
+ const persistentBody = normalizeRepositoryPaths(body, repository.repoRoot);
962
+ assertSafeToPersist(persistentBody);
963
+ const response = await this.dependencies.client.request(endpoints.projectSetup, {
964
+ method: 'POST',
965
+ body: persistentBody,
966
+ });
967
+ const data = objectValue(response.data);
968
+ const project = objectValue(data?.project);
969
+ const projectProfile = objectValue(data?.projectProfile);
970
+ const profileResource = objectValue(projectProfile?.resource);
971
+ const profileRevision = objectValue(projectProfile?.revision);
972
+ const policy = objectValue(data?.resourceDiscoveryPolicy);
973
+ const marker = objectValue(data?.marker);
974
+ if (!project ||
975
+ typeof project.id !== 'string' ||
976
+ !profileResource ||
977
+ !profileRevision ||
978
+ !policy ||
979
+ !marker ||
980
+ marker.projectId !== project.id ||
981
+ stringArray(policy.screenPathPatterns).length === 0 ||
982
+ stringArray(policy.componentPathPatterns).length === 0) {
983
+ throw new Error('Project setup response is not policy-ready');
984
+ }
985
+ const markerPath = await this.dependencies.repositories.writeMarker(repository.repoRoot, project.id);
986
+ return asJsonValue({ ...data, markerPath });
987
+ });
988
+ }
989
+ async projectMemberAdd(input) {
990
+ return await this.execute(async () => {
991
+ const response = await this.dependencies.client.request(endpoints.projectMemberAdd(input.projectId), {
992
+ method: 'POST',
993
+ body: { email: input.email, role: input.role },
994
+ });
995
+ return asJsonValue({ membership: response.data });
996
+ });
997
+ }
998
+ async organizationList() {
999
+ return await this.execute(async () => {
1000
+ const authentication = await this.dependencies.browserAuth.ensureAuthenticated();
1001
+ if (authentication) {
1002
+ return asJsonValue({ authentication });
1003
+ }
1004
+ const response = await this.dependencies.client.request(endpoints.organizationList);
1005
+ return asJsonValue({
1006
+ selectionRequired: true,
1007
+ questionnaire: 'Select the organization whose engineering core and architecture templates this project should inherit',
1008
+ organizations: response.data,
1009
+ });
1010
+ });
1011
+ }
1012
+ async architecturePlan(input) {
1013
+ return await this.execute(async () => {
1014
+ const body = cleanJson(input);
1015
+ const response = await this.dependencies.client.request(endpoints.memoryScaffoldPlan, {
1016
+ method: 'POST',
1017
+ body,
1018
+ cacheKey: sha256(`architecture.plan\n${stableStringify(body)}`),
1019
+ allowStaleOnUnavailable: true,
1020
+ });
1021
+ return asJsonValue({ modules: response.data });
1022
+ });
1023
+ }
1024
+ async architectureModule(input) {
1025
+ return await this.execute(async () => {
1026
+ const body = cleanJson(input);
1027
+ const response = await this.dependencies.client.request(endpoints.memoryArchitectureModule, {
1028
+ method: 'POST',
1029
+ body,
1030
+ cacheKey: sha256(`architecture.module\n${stableStringify(body)}`),
1031
+ allowStaleOnUnavailable: true,
1032
+ });
1033
+ return asJsonValue({ module: response.data });
1034
+ });
1035
+ }
1036
+ async architectureRecordApplication(input) {
1037
+ return await this.execute(async () => {
1038
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd());
1039
+ const snapshot = await this.activeTaskSnapshot(input.taskId, input.projectId, repository.repoRoot);
1040
+ const files = await Promise.all(input.files.map(async (file) => {
1041
+ const hash = await hashWorkingTreeFile(repository.repoRoot, file.path);
1042
+ if (!hash) {
1043
+ throw new Error(`Applied template file is not readable: ${file.path}`);
1044
+ }
1045
+ return { templatePath: file.templatePath, path: file.path, sha256: hash };
1046
+ }));
1047
+ const body = cleanJson({
1048
+ taskId: input.taskId,
1049
+ projectId: input.projectId,
1050
+ expectedTaskVersion: snapshot.taskVersion,
1051
+ templateResourceId: input.templateResourceId,
1052
+ templateRevisionId: input.templateRevisionId,
1053
+ files,
1054
+ });
1055
+ assertSafeToPersist(body);
1056
+ const response = await this.dependencies.client.request(endpoints.taskScaffoldApplication, { method: 'POST', body });
1057
+ return asJsonValue({ application: response.data });
1058
+ });
1059
+ }
1060
+ async organizationCreate(input) {
1061
+ return await this.execute(async () => {
1062
+ const response = await this.dependencies.client.request(endpoints.organizationCreate, { method: 'POST', body: cleanJson(input) });
1063
+ return asJsonValue({ organization: response.data });
1064
+ });
1065
+ }
1066
+ async projectList() {
1067
+ return await this.execute(async () => {
1068
+ const authentication = await this.dependencies.browserAuth.ensureAuthenticated();
1069
+ if (authentication) {
1070
+ return asJsonValue({ authentication });
1071
+ }
1072
+ const response = await this.dependencies.client.request(endpoints.projectList);
1073
+ return asJsonValue({
1074
+ selectionRequired: true,
1075
+ questionnaire: 'Select an existing project or choose new project setup',
1076
+ projects: response.data,
1077
+ });
1078
+ });
1079
+ }
1080
+ async projectResolve(input) {
1081
+ return await this.execute(async () => {
1082
+ const authentication = await this.dependencies.browserAuth.ensureAuthenticated();
1083
+ if (authentication) {
1084
+ return asJsonValue({ authentication });
1085
+ }
1086
+ const repository = await this.dependencies.repositories.resolve(input.repoRoot ?? process.cwd(), input.bind ? input.projectId : undefined);
1087
+ if (input.bind) {
1088
+ if (!input.projectId) {
1089
+ const projects = await this.dependencies.client.request(endpoints.projectList);
1090
+ return asJsonValue({
1091
+ resolved: false,
1092
+ selectionRequired: true,
1093
+ projects: projects.data,
1094
+ repository: publicRepository(repository),
1095
+ });
1096
+ }
1097
+ const response = await this.dependencies.client.request(endpoints.projectBind(input.projectId), {
1098
+ method: 'POST',
1099
+ body: { repoFingerprint: repository.repoFingerprint },
1100
+ });
1101
+ const project = objectValue(response.data);
1102
+ if (!project || project.id !== input.projectId) {
1103
+ throw new Error('Project bind response does not match the selected project');
1104
+ }
1105
+ const markerPath = await this.dependencies.repositories.writeMarker(repository.repoRoot, input.projectId);
1106
+ return asJsonValue({
1107
+ resolved: true,
1108
+ bound: true,
1109
+ project,
1110
+ markerPath,
1111
+ repository: publicRepository({
1112
+ ...repository,
1113
+ markerPath,
1114
+ projectId: input.projectId,
1115
+ }),
1116
+ selectionRequired: false,
1117
+ });
1118
+ }
1119
+ const response = await this.dependencies.client.request(endpoints.projectResolve, {
1120
+ method: 'POST',
1121
+ body: { repoFingerprint: repository.repoFingerprint },
1122
+ });
1123
+ const project = objectValue(response.data);
1124
+ return asJsonValue({
1125
+ resolved: project !== null,
1126
+ project,
1127
+ markerPath: null,
1128
+ repository: publicRepository(repository),
1129
+ selectionRequired: project === null,
1130
+ });
1131
+ });
1132
+ }
1133
+ async authStatus() {
1134
+ return await this.execute(async () => {
1135
+ return asJsonValue(await this.dependencies.browserAuth.status());
1136
+ });
1137
+ }
1138
+ async authSigninBrowser(input = {}) {
1139
+ return await this.execute(async () => {
1140
+ const authentication = await this.dependencies.browserAuth.ensureAuthenticated({
1141
+ restart: input.restart ?? false,
1142
+ });
1143
+ return asJsonValue(authentication
1144
+ ? { authentication }
1145
+ : { authentication: { authenticated: true, pending: false } });
1146
+ });
1147
+ }
1148
+ async authLogout(input = {}) {
1149
+ return await this.execute(async () => {
1150
+ const pendingOutboxCount = (await this.dependencies.outbox.list()).length;
1151
+ if (pendingOutboxCount > 0) {
1152
+ return asJsonValue({
1153
+ loggedOut: false,
1154
+ remoteRevoked: false,
1155
+ remoteStatus: 'pending_outbox',
1156
+ pendingOutboxCount,
1157
+ requiresPendingDeliveryResolution: true,
1158
+ });
1159
+ }
1160
+ let remoteRevoked = false;
1161
+ let remoteStatus = 'no_refresh_session';
1162
+ let clearLocalSession = false;
1163
+ try {
1164
+ let refreshToken = await this.dependencies.credentials.get('refresh-token');
1165
+ if (refreshToken) {
1166
+ try {
1167
+ await this.dependencies.client.request(endpoints.authLogout, {
1168
+ method: 'POST',
1169
+ body: { refreshToken },
1170
+ retryRefresh: false,
1171
+ });
1172
+ }
1173
+ catch (error) {
1174
+ if (!(error instanceof ApiResponseError) || error.httpStatus !== 401) {
1175
+ throw error;
1176
+ }
1177
+ await this.dependencies.client.refreshAuthentication();
1178
+ refreshToken = await this.dependencies.credentials.get('refresh-token');
1179
+ if (!refreshToken) {
1180
+ throw error;
1181
+ }
1182
+ await this.dependencies.client.request(endpoints.authLogout, {
1183
+ method: 'POST',
1184
+ body: { refreshToken },
1185
+ retryRefresh: false,
1186
+ });
1187
+ }
1188
+ remoteRevoked = true;
1189
+ remoteStatus = 'revoked';
1190
+ clearLocalSession = true;
1191
+ }
1192
+ else {
1193
+ clearLocalSession = true;
1194
+ }
1195
+ }
1196
+ catch (error) {
1197
+ remoteStatus = classifyError(error);
1198
+ if (isDefinitiveAuthenticationFailure(error)) {
1199
+ remoteStatus = 'remote_session_invalid';
1200
+ clearLocalSession = true;
1201
+ }
1202
+ else if (input.confirmLocalOnly === true) {
1203
+ remoteStatus = 'local_only_confirmed';
1204
+ clearLocalSession = true;
1205
+ }
1206
+ }
1207
+ if (!clearLocalSession) {
1208
+ return asJsonValue({
1209
+ loggedOut: false,
1210
+ remoteRevoked: false,
1211
+ remoteStatus,
1212
+ retryRequired: true,
1213
+ localOnlyLogoutAvailable: true,
1214
+ });
1215
+ }
1216
+ await this.dependencies.principalState?.clearAfterLogout();
1217
+ if (!this.dependencies.principalState) {
1218
+ await this.dependencies.credentials.clear();
1219
+ }
1220
+ await this.dependencies.browserAuth.dispose();
1221
+ return asJsonValue({ loggedOut: true, remoteRevoked, remoteStatus });
1222
+ }, true);
1223
+ }
1224
+ async writeTaskEvent(input, correction) {
1225
+ const idempotencyKey = input.idempotencyKey ?? randomUUID();
1226
+ const createdAt = new Date().toISOString();
1227
+ return await this.execute(async () => {
1228
+ const { repoRoot, ...persistedInput } = input;
1229
+ const safeInput = normalizePersistentInput(persistedInput, repoRoot ?? process.cwd());
1230
+ assertSafeToPersist(cleanJson(safeInput));
1231
+ return await this.taskExclusive(input.taskId, async () => {
1232
+ await this.recoverJournalOutbox(input.projectId, input.taskSlug);
1233
+ await this.flushOutbox();
1234
+ const journalInput = {
1235
+ eventId: idempotencyKey,
1236
+ taskId: input.taskId,
1237
+ projectId: input.projectId,
1238
+ taskSlug: input.taskSlug,
1239
+ checkpointType: safeInput.checkpointType,
1240
+ summary: safeInput.summary,
1241
+ ...(safeInput.details ? { details: safeInput.details } : {}),
1242
+ ...(safeInput.documents ? { documents: safeInput.documents } : {}),
1243
+ createdAt,
1244
+ };
1245
+ await this.dependencies.journal.assertEventContent(journalInput);
1246
+ const journal = await this.dependencies.journal.load(input.projectId, input.taskSlug);
1247
+ const pendingJournal = (await this.dependencies.journal.listPendingDeliveries(input.projectId, input.taskSlug)).find((delivery) => delivery.eventId === idempotencyKey);
1248
+ if (journal.projection?.appliedEventIds.includes(idempotencyKey) && !pendingJournal) {
1249
+ return asJsonValue({
1250
+ local: { directory: journal.directory, applied: false },
1251
+ queued: false,
1252
+ replayed: true,
1253
+ idempotencyKey,
1254
+ });
1255
+ }
1256
+ const pointer = await this.resolveCheckpointPointer(input, repoRoot);
1257
+ const taskEntries = (await this.dependencies.outbox.list()).filter((entry) => taskIdFromDeliverySafe(entry) === input.taskId);
1258
+ const versionedTaskEntries = taskEntries.filter((entry) => entry.journalRef &&
1259
+ (entry.operation === 'task.checkpoint' || entry.operation === 'task.record_correction'));
1260
+ const blockedEntry = taskEntries.find((entry) => entry.lastError && entry.lastError !== 'backend_unavailable');
1261
+ if (blockedEntry && blockedEntry.idempotencyKey !== idempotencyKey) {
1262
+ throw new Error('A blocked task delivery must be explicitly resolved before checkpointing');
1263
+ }
1264
+ const existingExpected = pendingJournal
1265
+ ? expectedTaskVersionFromBody(pendingJournal.body)
1266
+ : null;
1267
+ const expectedTaskVersion = existingExpected ??
1268
+ Math.max(pointer.taskVersion, ...versionedTaskEntries.map((entry) => (expectedTaskVersionFromBody(entry.body) ?? pointer.taskVersion - 1) + 1));
1269
+ await this.dependencies.gate.invalidateTask(input.taskId);
1270
+ const staged = await this.dependencies.journal.stage(journalInput, {
1271
+ operation: correction ? 'task.record_correction' : 'task.checkpoint',
1272
+ method: 'POST',
1273
+ path: correction ? endpoints.taskCorrection : endpoints.taskCheckpoint,
1274
+ expectedTaskVersion,
1275
+ correction,
1276
+ });
1277
+ if (!staged.delivery) {
1278
+ throw new Error('Task journal delivery was not materialized');
1279
+ }
1280
+ const queued = await this.dependencies.outbox.enqueue({
1281
+ operation: staged.delivery.operation,
1282
+ method: staged.delivery.method,
1283
+ path: staged.delivery.path,
1284
+ body: staged.delivery.body,
1285
+ idempotencyKey,
1286
+ journalRef: {
1287
+ projectId: input.projectId,
1288
+ taskSlug: input.taskSlug,
1289
+ eventId: idempotencyKey,
1290
+ },
1291
+ });
1292
+ if (staged.applied) {
1293
+ await this.dependencies.activeContexts.updateTaskSnapshot(input.taskId, expectedTaskVersion + 1, pointer.lastSequence + 1);
1294
+ }
1295
+ const predecessorEntries = taskEntries.filter((entry) => entry.id !== queued.id);
1296
+ if (predecessorEntries.length > 0) {
1297
+ return asJsonValue({
1298
+ local: { directory: staged.directory, applied: staged.applied },
1299
+ queued: true,
1300
+ outboxId: queued.id,
1301
+ idempotencyKey,
1302
+ expectedTaskVersion,
1303
+ predictedTaskVersion: expectedTaskVersion + 1,
1304
+ deliveryStatus: 'pending',
1305
+ waitingForOrderedDeliveries: predecessorEntries.map((entry) => entry.id),
1306
+ });
1307
+ }
1308
+ try {
1309
+ const response = await this.dependencies.client.request(staged.delivery.path, {
1310
+ method: staged.delivery.method,
1311
+ idempotencyKey,
1312
+ body: staged.delivery.body,
1313
+ });
1314
+ await this.captureTaskSnapshot(response.data);
1315
+ await this.dependencies.journal.markDeliverySynchronized(input.projectId, input.taskSlug, idempotencyKey);
1316
+ const synchronizedPointer = await this.dependencies.activeContexts.findByTaskId(input.taskId, input.projectId);
1317
+ await this.seedResumeSnapshot(synchronizedPointer, objectValue(response.data) ?? undefined);
1318
+ await this.dependencies.outbox.acknowledge(queued.id);
1319
+ return asJsonValue({
1320
+ backend: response.data,
1321
+ local: { directory: staged.directory, applied: staged.applied },
1322
+ queued: false,
1323
+ idempotencyKey,
1324
+ expectedTaskVersion,
1325
+ });
1326
+ }
1327
+ catch (error) {
1328
+ const errorKind = classifyError(error);
1329
+ await this.dependencies.outbox.markAttempt(queued.id, errorKind);
1330
+ await this.dependencies.journal.markDeliveryAttempt(input.projectId, input.taskSlug, idempotencyKey, errorKind);
1331
+ return asJsonValue({
1332
+ local: { directory: staged.directory, applied: staged.applied },
1333
+ queued: true,
1334
+ outboxId: queued.id,
1335
+ idempotencyKey,
1336
+ expectedTaskVersion,
1337
+ predictedTaskVersion: expectedTaskVersion + 1,
1338
+ deliveryStatus: isBackendUnavailableError(error) ? 'pending' : 'blocked',
1339
+ backendError: publicError(error),
1340
+ });
1341
+ }
1342
+ });
1343
+ });
1344
+ }
1345
+ async mutateWithOutbox(operation, path, body) {
1346
+ assertSafeToPersist(body);
1347
+ const idempotencyKey = randomUUID();
1348
+ try {
1349
+ const response = await this.dependencies.client.request(path, {
1350
+ method: 'POST',
1351
+ body,
1352
+ idempotencyKey,
1353
+ });
1354
+ return asJsonValue({ data: response.data, queued: false, idempotencyKey });
1355
+ }
1356
+ catch (error) {
1357
+ if (!isBackendUnavailableError(error)) {
1358
+ throw error;
1359
+ }
1360
+ const queued = await this.dependencies.outbox.enqueue({
1361
+ operation,
1362
+ method: 'POST',
1363
+ path,
1364
+ body,
1365
+ idempotencyKey,
1366
+ });
1367
+ return asJsonValue({ queued: true, outboxId: queued.id, idempotencyKey });
1368
+ }
1369
+ }
1370
+ async flushOutbox() {
1371
+ const entries = await this.dependencies.outbox.list();
1372
+ const synchronized = [];
1373
+ const blocked = [];
1374
+ for (const entry of entries) {
1375
+ try {
1376
+ const response = await this.dependencies.client.request(entry.path, {
1377
+ method: entry.method,
1378
+ body: entry.body,
1379
+ idempotencyKey: entry.idempotencyKey,
1380
+ });
1381
+ await this.captureTaskSnapshot(response.data);
1382
+ if (entry.journalRef) {
1383
+ await this.dependencies.journal.markDeliverySynchronized(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId);
1384
+ const deliveryTaskId = taskIdFromDeliverySafe(entry);
1385
+ if (deliveryTaskId) {
1386
+ const synchronizedPointer = await this.dependencies.activeContexts.findByTaskId(deliveryTaskId, entry.journalRef.projectId, false);
1387
+ await this.seedResumeSnapshot(synchronizedPointer, objectValue(response.data) ?? undefined);
1388
+ }
1389
+ }
1390
+ await this.dependencies.outbox.acknowledge(entry.id);
1391
+ synchronized.push(entry.id);
1392
+ }
1393
+ catch (error) {
1394
+ const errorKind = classifyError(error);
1395
+ await this.dependencies.outbox.markAttempt(entry.id, errorKind);
1396
+ if (entry.journalRef) {
1397
+ await this.dependencies.journal.markDeliveryAttempt(entry.journalRef.projectId, entry.journalRef.taskSlug, entry.journalRef.eventId, errorKind);
1398
+ }
1399
+ blocked.push({ id: entry.id, error: errorKind });
1400
+ break;
1401
+ }
1402
+ }
1403
+ return asJsonValue({
1404
+ synchronized,
1405
+ blocked,
1406
+ pending: (await this.dependencies.outbox.list()).length,
1407
+ });
1408
+ }
1409
+ async recoverJournalOutbox(projectId, taskSlug) {
1410
+ const pending = await this.dependencies.journal.listPendingDeliveries(projectId, taskSlug);
1411
+ for (const delivery of pending) {
1412
+ await this.dependencies.outbox.enqueue({
1413
+ operation: delivery.operation,
1414
+ method: delivery.method,
1415
+ path: delivery.path,
1416
+ body: delivery.body,
1417
+ idempotencyKey: delivery.idempotencyKey,
1418
+ journalRef: {
1419
+ projectId: delivery.projectId,
1420
+ taskSlug: delivery.taskSlug,
1421
+ eventId: delivery.eventId,
1422
+ },
1423
+ });
1424
+ }
1425
+ return pending.length;
1426
+ }
1427
+ async activeTaskSnapshot(taskId, projectId, repoRoot) {
1428
+ const repository = await this.dependencies.repositories.resolve(repoRoot ?? process.cwd(), projectId);
1429
+ const pointer = await this.requireActivePointer(repository.repoFingerprint, taskId);
1430
+ if ((projectId && pointer.projectId !== projectId) ||
1431
+ (repository.projectId && pointer.projectId !== repository.projectId)) {
1432
+ throw new Error('Active task snapshot does not match the repository project');
1433
+ }
1434
+ if ((await this.dependencies.outbox.list()).some((entry) => taskIdFromDeliverySafe(entry) === taskId)) {
1435
+ throw new Error('Pending task deliveries must be resolved before using the task snapshot');
1436
+ }
1437
+ const response = await this.dependencies.client.request(endpoints.sessionResume, {
1438
+ method: 'POST',
1439
+ body: {
1440
+ sessionId: pointer.sessionId,
1441
+ repoFingerprint: repository.repoFingerprint,
1442
+ afterSequence: 0,
1443
+ },
1444
+ });
1445
+ const backend = objectValue(response.data);
1446
+ const task = objectValue(backend?.task);
1447
+ if (!task || task.id !== taskId || backend?.requiresContextRefresh === true) {
1448
+ throw new Error('Backend task snapshot is stale or does not match the active task');
1449
+ }
1450
+ const refreshed = {
1451
+ ...pointer,
1452
+ taskVersion: numericTaskVersion(task.lockVersion),
1453
+ lastSequence: numericSequence(task.lastSequence),
1454
+ };
1455
+ await this.dependencies.activeContexts.save(refreshed);
1456
+ return refreshed;
1457
+ }
1458
+ async refreshTaskPointer(taskId, projectId) {
1459
+ const pointer = await this.dependencies.activeContexts.findByTaskId(taskId, projectId);
1460
+ const response = await this.dependencies.client.request(endpoints.sessionResume, {
1461
+ method: 'POST',
1462
+ body: {
1463
+ sessionId: pointer.sessionId,
1464
+ repoFingerprint: pointer.repoFingerprint,
1465
+ afterSequence: 0,
1466
+ },
1467
+ });
1468
+ const backend = objectValue(response.data);
1469
+ const task = objectValue(backend?.task);
1470
+ if (!task || task.id !== taskId || backend?.requiresContextRefresh === true) {
1471
+ throw new Error('Backend task snapshot is stale or does not match the pending delivery');
1472
+ }
1473
+ const refreshed = {
1474
+ ...pointer,
1475
+ taskVersion: numericTaskVersion(task.lockVersion),
1476
+ lastSequence: numericSequence(task.lastSequence),
1477
+ };
1478
+ await this.dependencies.activeContexts.save(refreshed);
1479
+ return refreshed;
1480
+ }
1481
+ async requireActivePointer(repoFingerprint, taskId, allowOfflineWarnings = false, allowedIntent) {
1482
+ const pointer = await this.dependencies.activeContexts.load(repoFingerprint);
1483
+ const blockingConflicts = pointer?.resumeConflicts.filter((conflict) => !(allowOfflineWarnings && isOfflineDevelopmentWarning(conflict)));
1484
+ const lifecycleIntentBlocked = (pointer?.verificationIntent && allowedIntent !== 'verify') ||
1485
+ (pointer?.closeIntent && allowedIntent !== 'close');
1486
+ if (!pointer ||
1487
+ pointer.taskId !== taskId ||
1488
+ (blockingConflicts?.length ?? 0) > 0 ||
1489
+ lifecycleIntentBlocked) {
1490
+ throw new Error('A clean active task snapshot is required for this operation');
1491
+ }
1492
+ return pointer;
1493
+ }
1494
+ async captureTaskSnapshot(value) {
1495
+ const data = objectValue(value ?? undefined);
1496
+ const task = objectValue(data?.task);
1497
+ if (task && typeof task.id === 'string' && typeof task.lockVersion === 'number') {
1498
+ await this.dependencies.activeContexts.updateTaskSnapshot(task.id, numericTaskVersion(task.lockVersion), numericSequence(task.lastSequence));
1499
+ }
1500
+ }
1501
+ async retryVerificationRecovery(repository, pointer, backend) {
1502
+ const intent = pointer.verificationIntent;
1503
+ const recovery = objectValue(backend?.verificationRecovery);
1504
+ if (!intent) {
1505
+ return null;
1506
+ }
1507
+ const body = intent.body;
1508
+ const backendTask = objectValue(backend?.task);
1509
+ const backendSession = objectValue(backend?.session);
1510
+ const expectedTaskVersion = numericTaskVersion(body.expectedTaskVersion);
1511
+ if (repository.projectId !== pointer.projectId ||
1512
+ repository.git.diffHash !== body.diffHash ||
1513
+ pointer.taskId !== body.taskId ||
1514
+ pointer.sessionId !== body.sessionId ||
1515
+ backendTask?.id !== pointer.taskId ||
1516
+ backendTask.projectId !== pointer.projectId ||
1517
+ backendSession?.id !== pointer.sessionId ||
1518
+ body.pendingOutboxCount !== 0) {
1519
+ throw new Error('Verification recovery does not match the durable verification intent');
1520
+ }
1521
+ if (recovery) {
1522
+ const recoveryTaskVersion = numericTaskVersion(recovery.taskVersion);
1523
+ if (recovery.diffHash !== body.diffHash ||
1524
+ recovery.mode !== intent.mode ||
1525
+ (recoveryTaskVersion !== expectedTaskVersion &&
1526
+ recoveryTaskVersion !== expectedTaskVersion + 1)) {
1527
+ throw new Error('Verification recovery does not match the verified backend task');
1528
+ }
1529
+ }
1530
+ else if (numericTaskVersion(backendTask.lockVersion) !== expectedTaskVersion ||
1531
+ normalizeTaskMode(backendTask.mode) !== intent.mode) {
1532
+ throw new Error('Open backend task does not match the durable verification intent');
1533
+ }
1534
+ const currentTaskChanges = intent.mode !== 'read_only'
1535
+ ? pointer.changeBaseline
1536
+ ? manifestDelta(pointer.changeBaseline.changedPaths, repository.git.changedPaths)
1537
+ : null
1538
+ : [];
1539
+ if (!currentTaskChanges) {
1540
+ throw new Error('Verification recovery is missing its locally pinned baseline');
1541
+ }
1542
+ const currentChangedPaths = currentTaskChanges.map((entry) => entry.path).sort();
1543
+ const currentPathChanges = intent.mode === 'read_only'
1544
+ ? []
1545
+ : await buildPathChangeManifest(repository.repoRoot, currentTaskChanges);
1546
+ if (stableStringify(body.changedPaths ?? null) !== stableStringify(currentChangedPaths) ||
1547
+ stableStringify(body.pathChanges ?? null) !== stableStringify(currentPathChanges) ||
1548
+ stableStringify(intent.taskChanges.map(changedPathIdentity)) !==
1549
+ stableStringify(currentTaskChanges.map(changedPathIdentity))) {
1550
+ throw new Error('Current Git state does not match the durable verification intent');
1551
+ }
1552
+ if (intent.mode !== 'read_only') {
1553
+ const retryLease = recovery
1554
+ ? objectValue(recovery.consumedLease)
1555
+ : objectValue(backend?.activeLease);
1556
+ const retryLeasePaths = Array.isArray(retryLease?.changedPaths)
1557
+ ? retryLease.changedPaths.filter((path) => typeof path === 'string')
1558
+ : [];
1559
+ if (!retryLease ||
1560
+ retryLease.id !== body.leaseId ||
1561
+ retryLease.taskId !== pointer.taskId ||
1562
+ retryLease.sessionId !== pointer.sessionId ||
1563
+ retryLease.baselineDiffHash !== body.baselineDiffHash ||
1564
+ body.baselineDiffHash !== pointer.changeBaseline?.diffHash ||
1565
+ !pathsContainAll(retryLeasePaths, currentChangedPaths)) {
1566
+ throw new Error('Backend lease does not match the durable verification intent');
1567
+ }
1568
+ }
1569
+ else if (currentChangedPaths.length !== 0 ||
1570
+ body.leaseId !== undefined ||
1571
+ body.baselineDiffHash !== undefined) {
1572
+ throw new Error('Read-only verification recovery contains a write lease');
1573
+ }
1574
+ let response;
1575
+ try {
1576
+ response = await this.dependencies.client.request(endpoints.taskVerify, {
1577
+ method: 'POST',
1578
+ body,
1579
+ });
1580
+ }
1581
+ catch (error) {
1582
+ if (!isBackendUnavailableError(error)) {
1583
+ await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint);
1584
+ }
1585
+ throw error;
1586
+ }
1587
+ const responseData = objectValue(response.data);
1588
+ if (responseData?.verified !== true || responseData.diffHash !== body.diffHash) {
1589
+ await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint);
1590
+ throw new Error('Verification retry did not confirm the durable verification intent');
1591
+ }
1592
+ const taskVersion = numericTaskVersion(responseData.taskVersion);
1593
+ await this.dependencies.gate.record({
1594
+ taskId: pointer.taskId,
1595
+ projectId: pointer.projectId,
1596
+ repoFingerprint: pointer.repoFingerprint,
1597
+ repoRoot: repository.repoRoot,
1598
+ diffHash: repository.git.diffHash,
1599
+ taskVersion,
1600
+ taskChanges: intent.taskChanges,
1601
+ });
1602
+ await this.dependencies.activeContexts.clearVerificationIntent(repository.repoFingerprint);
1603
+ await this.dependencies.activeContexts.updateTaskSnapshot(pointer.taskId, taskVersion, pointer.lastSequence);
1604
+ return { ...responseData, recoveredAfterResponseLoss: true };
1605
+ }
1606
+ async retryCloseIntent(repository, pointer) {
1607
+ const intent = pointer.closeIntent;
1608
+ if (!intent) {
1609
+ throw new Error('Durable close intent is unavailable');
1610
+ }
1611
+ const body = intent.body;
1612
+ const currentTaskChanges = pointer.changeBaseline
1613
+ ? manifestDelta(pointer.changeBaseline.changedPaths, repository.git.changedPaths)
1614
+ : [];
1615
+ if (repository.projectId !== pointer.projectId ||
1616
+ repository.git.diffHash !== body.diffHash ||
1617
+ pointer.taskId !== body.taskId ||
1618
+ pointer.sessionId !== body.sessionId ||
1619
+ stableStringify(intent.taskChanges.map(changedPathIdentity)) !==
1620
+ stableStringify(currentTaskChanges.map(changedPathIdentity))) {
1621
+ throw new Error('Current state does not match the durable close intent');
1622
+ }
1623
+ return await this.deliverCloseIntent(repository, body, intent.taskChanges, true);
1624
+ }
1625
+ async deliverCloseIntent(repository, body, taskChanges, recoveredAfterResponseLoss) {
1626
+ let response;
1627
+ try {
1628
+ response = await this.dependencies.client.request(endpoints.taskClose, {
1629
+ method: 'POST',
1630
+ body,
1631
+ });
1632
+ }
1633
+ catch (error) {
1634
+ if (!isBackendUnavailableError(error)) {
1635
+ await this.dependencies.activeContexts.clearCloseIntent(repository.repoFingerprint);
1636
+ }
1637
+ throw error;
1638
+ }
1639
+ const closedTask = objectValue(response.data);
1640
+ if (closedTask?.closed !== true) {
1641
+ await this.dependencies.activeContexts.clearCloseIntent(repository.repoFingerprint);
1642
+ throw new Error('Task close response does not confirm task closure');
1643
+ }
1644
+ const closedTaskVersion = numericTaskVersion(closedTask.taskVersion);
1645
+ await this.dependencies.gate.record({
1646
+ taskId: String(body.taskId),
1647
+ projectId: repository.projectId,
1648
+ repoFingerprint: repository.repoFingerprint,
1649
+ repoRoot: repository.repoRoot,
1650
+ diffHash: String(body.diffHash),
1651
+ taskClosed: true,
1652
+ taskVersion: closedTaskVersion,
1653
+ taskChanges,
1654
+ });
1655
+ await this.dependencies.activeContexts.clearCloseIntent(repository.repoFingerprint);
1656
+ return asJsonValue({
1657
+ ...closedTask,
1658
+ repository: publicRepository(repository),
1659
+ recoveredAfterResponseLoss,
1660
+ delivery: deliveryQuestion(),
1661
+ });
1662
+ }
1663
+ async seedResumeSnapshot(pointer, backendPatch) {
1664
+ const cacheKey = resumeCacheKey(pointer.sessionId, pointer.repoFingerprint, 0);
1665
+ const cached = await this.dependencies.client.readCached(cacheKey);
1666
+ const cachedData = objectValue(cached?.data ?? undefined);
1667
+ const base = { ...objectOrEmpty(cachedData), ...objectOrEmpty(backendPatch ?? null) };
1668
+ const baseTask = objectValue(base.task);
1669
+ const patchTask = objectValue(backendPatch?.task);
1670
+ const baseSession = objectValue(base.session);
1671
+ const patchSession = objectValue(backendPatch?.session);
1672
+ const journal = await this.dependencies.journal.load(pointer.projectId, pointer.taskSlug);
1673
+ if (!journal.projection) {
1674
+ return;
1675
+ }
1676
+ const local = await this.dependencies.journal.canonicalResumeState(pointer.projectId, pointer.taskSlug);
1677
+ const data = asJsonValue({
1678
+ ...base,
1679
+ task: {
1680
+ ...objectOrEmpty(baseTask),
1681
+ ...objectOrEmpty(patchTask),
1682
+ id: pointer.taskId,
1683
+ projectId: pointer.projectId,
1684
+ lockVersion: pointer.taskVersion,
1685
+ lastSequence: pointer.lastSequence,
1686
+ },
1687
+ session: {
1688
+ ...objectOrEmpty(baseSession),
1689
+ ...objectOrEmpty(patchSession),
1690
+ id: pointer.sessionId,
1691
+ },
1692
+ events: local.events,
1693
+ documents: local.documents,
1694
+ requiresContextRefresh: false,
1695
+ });
1696
+ await this.dependencies.client.seedCache(cacheKey, {
1697
+ data,
1698
+ message: null,
1699
+ status: 'success',
1700
+ errorModel: null,
1701
+ });
1702
+ }
1703
+ async resolveCheckpointPointer(input, repoRoot) {
1704
+ const pointer = repoRoot
1705
+ ? await (async () => {
1706
+ const repository = await this.dependencies.repositories.resolve(repoRoot, input.projectId);
1707
+ return await this.requireActivePointer(repository.repoFingerprint, input.taskId, true);
1708
+ })()
1709
+ : await this.dependencies.activeContexts.findByTaskId(input.taskId, input.projectId, false);
1710
+ if (pointer.resumeConflicts.some((conflict) => !isOfflineDevelopmentWarning(conflict))) {
1711
+ throw new Error('A clean active task snapshot is required for checkpointing');
1712
+ }
1713
+ if (pointer.verificationIntent || pointer.closeIntent) {
1714
+ throw new Error('A clean active task snapshot is required for checkpointing');
1715
+ }
1716
+ if (pointer.taskSlug !== input.taskSlug) {
1717
+ throw new Error('Checkpoint task slug does not match the active task pointer');
1718
+ }
1719
+ return pointer;
1720
+ }
1721
+ async taskExclusive(taskId, action) {
1722
+ const previous = this.taskQueues.get(taskId) ?? Promise.resolve();
1723
+ let release = () => undefined;
1724
+ const current = new Promise((resolvePromise) => {
1725
+ release = resolvePromise;
1726
+ });
1727
+ const tail = previous.then(() => current);
1728
+ this.taskQueues.set(taskId, tail);
1729
+ await previous;
1730
+ try {
1731
+ return await action();
1732
+ }
1733
+ finally {
1734
+ release();
1735
+ if (this.taskQueues.get(taskId) === tail) {
1736
+ this.taskQueues.delete(taskId);
1737
+ }
1738
+ }
1739
+ }
1740
+ async execute(action, skipPrincipalCheck = false) {
1741
+ try {
1742
+ if (!skipPrincipalCheck) {
1743
+ await this.dependencies.principalState?.ensure();
1744
+ }
1745
+ return { ok: true, data: await action() };
1746
+ }
1747
+ catch (error) {
1748
+ if (isBackendUnavailableError(error)) {
1749
+ return {
1750
+ ok: false,
1751
+ error: {
1752
+ kind: 'backend_unavailable',
1753
+ message: error instanceof Error ? error.message : 'Backend is unavailable',
1754
+ retryable: true,
1755
+ },
1756
+ };
1757
+ }
1758
+ if (error instanceof ApiResponseError) {
1759
+ return {
1760
+ ok: false,
1761
+ error: {
1762
+ kind: 'api_response',
1763
+ message: error.message,
1764
+ code: error.code ?? error.httpStatus,
1765
+ retryable: error.retryable,
1766
+ },
1767
+ };
1768
+ }
1769
+ return {
1770
+ ok: false,
1771
+ error: {
1772
+ kind: 'bridge_error',
1773
+ message: error instanceof Error ? error.message : 'Bridge operation failed',
1774
+ retryable: false,
1775
+ },
1776
+ };
1777
+ }
1778
+ }
1779
+ }
1780
+ function normalizeChangedPaths(paths) {
1781
+ const normalized = paths.map((path) => path.replace(/\\/g, '/').replace(/^\.\//, ''));
1782
+ for (const path of normalized) {
1783
+ if (!path ||
1784
+ path.startsWith('/') ||
1785
+ /^[A-Za-z]:\//.test(path) ||
1786
+ path.split('/').includes('..')) {
1787
+ throw new Error(`Changed path must be repository-relative: ${path}`);
1788
+ }
1789
+ }
1790
+ return [...new Set(normalized)].sort();
1791
+ }
1792
+ function pathsContainAll(allowedPaths, actualPaths) {
1793
+ const allowed = new Set(normalizeChangedPaths(allowedPaths));
1794
+ return normalizeChangedPaths(actualPaths).every((path) => allowed.has(path));
1795
+ }
1796
+ function manifestDelta(baseline, current) {
1797
+ const baselineByPath = new Map(baseline.map((entry) => [entry.path, entry]));
1798
+ const currentByPath = new Map(current.map((entry) => [entry.path, entry]));
1799
+ const renamedOriginalPaths = new Set(current.flatMap((entry) => (entry.originalPath ? [entry.originalPath] : [])));
1800
+ const delta = current.filter((entry) => {
1801
+ const previous = baselineByPath.get(entry.path);
1802
+ return (!previous ||
1803
+ stableStringify(changedPathIdentity(previous)) !== stableStringify(changedPathIdentity(entry)));
1804
+ });
1805
+ for (const entry of baseline) {
1806
+ if (!currentByPath.has(entry.path) && !renamedOriginalPaths.has(entry.path)) {
1807
+ delta.push({
1808
+ path: entry.path,
1809
+ status: 'D ',
1810
+ contentHash: null,
1811
+ size: null,
1812
+ mode: null,
1813
+ });
1814
+ }
1815
+ }
1816
+ return delta.sort((left, right) => left.path.localeCompare(right.path));
1817
+ }
1818
+ function changedPathIdentity(entry) {
1819
+ return {
1820
+ path: entry.path,
1821
+ originalPath: entry.originalPath ?? null,
1822
+ status: semanticGitStatus(entry.status),
1823
+ contentHash: entry.contentHash,
1824
+ size: entry.size,
1825
+ mode: entry.mode ?? null,
1826
+ };
1827
+ }
1828
+ function semanticGitStatus(status) {
1829
+ if (status === '??') {
1830
+ return 'A';
1831
+ }
1832
+ for (const candidate of ['R', 'C', 'A', 'D', 'T', 'U', 'M']) {
1833
+ if (status.includes(candidate)) {
1834
+ return candidate;
1835
+ }
1836
+ }
1837
+ return status.trim() || status;
1838
+ }
1839
+ function semanticPathStatus(status) {
1840
+ const semantic = semanticGitStatus(status);
1841
+ if (!['A', 'R', 'C', 'M', 'D', 'T', 'U'].includes(semantic)) {
1842
+ throw new Error(`Unsupported semantic Git status: ${status}`);
1843
+ }
1844
+ return semantic;
1845
+ }
1846
+ function normalizeTaskMode(mode) {
1847
+ if (mode === 'read_only')
1848
+ return 'read_only';
1849
+ if (mode === 'scaffold')
1850
+ return 'scaffold';
1851
+ return 'write';
1852
+ }
1853
+ async function hashWorkingTreeFile(repoRoot, path) {
1854
+ try {
1855
+ return sha256(await readFile(join(repoRoot, path)));
1856
+ }
1857
+ catch {
1858
+ return undefined;
1859
+ }
1860
+ }
1861
+ async function buildPathChangeManifest(repoRoot, changes) {
1862
+ return await Promise.all(changes.map(async (entry) => {
1863
+ const hash = entry.status === 'D' ? undefined : await hashWorkingTreeFile(repoRoot, entry.path);
1864
+ return cleanJson({
1865
+ path: entry.path,
1866
+ status: semanticPathStatus(entry.status),
1867
+ ...(entry.originalPath ? { originalPath: entry.originalPath } : {}),
1868
+ ...(hash ? { sha256: hash } : {}),
1869
+ });
1870
+ }));
1871
+ }
1872
+ function publicRepository(repository) {
1873
+ return asJsonValue({
1874
+ repoRoot: repository.repoRoot,
1875
+ markerPath: repository.markerPath,
1876
+ projectId: repository.projectId,
1877
+ schemaVersion: repository.schemaVersion,
1878
+ repoFingerprint: repository.repoFingerprint,
1879
+ git: {
1880
+ repoRoot: repository.git.repoRoot,
1881
+ head: repository.git.head,
1882
+ diffHash: repository.git.diffHash,
1883
+ changedPaths: repository.git.changedPaths.map(({ path, status, originalPath }) => ({
1884
+ path,
1885
+ status,
1886
+ ...(originalPath ? { originalPath } : {}),
1887
+ })),
1888
+ },
1889
+ });
1890
+ }
1891
+ export function correctionScopeLine(scope, reference) {
1892
+ return `Correction scope ${scope}: ${reference}`;
1893
+ }
1894
+ export function parseCorrectionScopeLine(summary) {
1895
+ const match = /^Correction scope ([a-z_]+): (.+)$/.exec(summary);
1896
+ return match ? { scope: match[1], reference: match[2] } : null;
1897
+ }
1898
+ function deliveryQuestion() {
1899
+ return asJsonValue({
1900
+ required: true,
1901
+ question: 'The task is closed and nothing has been committed. Ask the user which of these to do, and do only what they choose.',
1902
+ options: [
1903
+ { id: 'commit', label: 'Commit' },
1904
+ { id: 'commit_push', label: 'Commit and push' },
1905
+ {
1906
+ id: 'commit_push_draft_pr',
1907
+ label: 'Commit, push and open a draft pull request',
1908
+ input: 'base branch the pull request targets',
1909
+ },
1910
+ {
1911
+ id: 'commit_push_pr',
1912
+ label: 'Commit, push and open a pull request',
1913
+ input: 'base branch the pull request targets',
1914
+ },
1915
+ ],
1916
+ afterPullRequest: 'Do not end the turn once a pull request exists. Check whether it merges cleanly, report the conflicting files if it does not, and ask whether to resolve them before touching anything.',
1917
+ });
1918
+ }
1919
+ function cleanJson(value) {
1920
+ return JSON.parse(JSON.stringify(value));
1921
+ }
1922
+ function normalizePersistentInput(value, repoRoot) {
1923
+ if (value === undefined) {
1924
+ return value;
1925
+ }
1926
+ return normalizeRepositoryPaths(cleanJson(value), repoRoot);
1927
+ }
1928
+ function asJsonValue(value) {
1929
+ return cleanJson(value);
1930
+ }
1931
+ function objectValue(value) {
1932
+ return value !== null && value !== undefined && !Array.isArray(value) && typeof value === 'object'
1933
+ ? value
1934
+ : null;
1935
+ }
1936
+ function objectOrEmpty(value) {
1937
+ return objectValue(value ?? undefined) ?? {};
1938
+ }
1939
+ function slugify(value) {
1940
+ return value
1941
+ .trim()
1942
+ .toLowerCase()
1943
+ .normalize('NFKD')
1944
+ .replace(/[^a-z0-9]+/g, '-')
1945
+ .replace(/^-|-$/g, '');
1946
+ }
1947
+ function deterministicUuid(...parts) {
1948
+ const value = sha256(parts.join('\n')).slice(0, 32).split('');
1949
+ value[12] = '5';
1950
+ value[16] = ((Number.parseInt(value[16], 16) & 0x3) | 0x8).toString(16);
1951
+ const hex = value.join('');
1952
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1953
+ }
1954
+ function resumeCacheKey(sessionId, repoFingerprint, afterSequence) {
1955
+ return sha256(`session.resume\n${sessionId}\n${repoFingerprint}\n${afterSequence}`);
1956
+ }
1957
+ function classifyError(error) {
1958
+ if (isBackendUnavailableError(error)) {
1959
+ return 'backend_unavailable';
1960
+ }
1961
+ if (error instanceof ApiResponseError) {
1962
+ return `api_${error.httpStatus}_${error.code ?? 'unknown'}`;
1963
+ }
1964
+ return 'bridge_delivery_error';
1965
+ }
1966
+ function isDefinitiveAuthenticationFailure(error) {
1967
+ return (error instanceof ApiResponseError &&
1968
+ (error.httpStatus === 400 || error.httpStatus === 401 || error.httpStatus === 403));
1969
+ }
1970
+ function publicError(error) {
1971
+ if (isBackendUnavailableError(error)) {
1972
+ return asJsonValue({
1973
+ kind: 'backend_unavailable',
1974
+ retryable: true,
1975
+ ...(error instanceof ApiResponseError ? { httpStatus: error.httpStatus } : {}),
1976
+ });
1977
+ }
1978
+ if (error instanceof ApiResponseError) {
1979
+ return asJsonValue({
1980
+ kind: 'api_response',
1981
+ httpStatus: error.httpStatus,
1982
+ code: error.code,
1983
+ retryable: error.retryable,
1984
+ });
1985
+ }
1986
+ return asJsonValue({
1987
+ kind: 'bridge_error',
1988
+ retryable: false,
1989
+ });
1990
+ }
1991
+ function isBackendUnavailableError(error) {
1992
+ return (error instanceof BackendUnavailableError ||
1993
+ (error instanceof ApiResponseError && [502, 503, 504].includes(error.httpStatus)));
1994
+ }
1995
+ function numericSequence(value) {
1996
+ const sequence = typeof value === 'number' ? value : Number(value ?? 0);
1997
+ if (!Number.isSafeInteger(sequence) || sequence < 0) {
1998
+ throw new Error('Backend task sequence is invalid');
1999
+ }
2000
+ return sequence;
2001
+ }
2002
+ function isIsoTimestamp(value) {
2003
+ const parsed = Date.parse(value);
2004
+ return Number.isFinite(parsed) && new Date(parsed).toISOString() === value;
2005
+ }
2006
+ function numericTaskVersion(value) {
2007
+ const version = typeof value === 'number' ? value : Number(value);
2008
+ if (!Number.isSafeInteger(version) || version < 1) {
2009
+ throw new Error('Backend task version is invalid');
2010
+ }
2011
+ return version;
2012
+ }
2013
+ function validateOfflineLease(value, sessionId, changedPaths, baselineDiffHash) {
2014
+ const data = objectValue(value ?? undefined);
2015
+ const lease = objectValue(data?.lease);
2016
+ const leasePaths = Array.isArray(lease?.changedPaths)
2017
+ ? lease.changedPaths.filter((path) => typeof path === 'string')
2018
+ : [];
2019
+ const expiresAt = typeof lease?.expiresAt === 'string' ? Date.parse(lease.expiresAt) : NaN;
2020
+ if (!lease ||
2021
+ typeof lease.id !== 'string' ||
2022
+ lease.sessionId !== sessionId ||
2023
+ lease.baselineDiffHash !== baselineDiffHash ||
2024
+ stableStringify(normalizeChangedPaths(leasePaths)) !== stableStringify(changedPaths) ||
2025
+ !Number.isFinite(expiresAt) ||
2026
+ expiresAt <= Date.now()) {
2027
+ throw new Error('Cached change lease is missing, expired or does not match the current Git state');
2028
+ }
2029
+ }
2030
+ function isOfflineLeaseUsable(lease, sessionId, changedPaths, baselineDiffHash) {
2031
+ if (!lease || !baselineDiffHash) {
2032
+ return false;
2033
+ }
2034
+ try {
2035
+ validateOfflineLease({ lease }, sessionId, normalizeChangedPaths(changedPaths), baselineDiffHash);
2036
+ return true;
2037
+ }
2038
+ catch {
2039
+ return false;
2040
+ }
2041
+ }
2042
+ function isOfflineDevelopmentWarning(value) {
2043
+ return (value === 'backend_resume_is_stale' ||
2044
+ value === 'pending_outbox_delivery' ||
2045
+ value === 'pending_local_journal_delivery' ||
2046
+ value === 'backend_local_sequence_mismatch' ||
2047
+ value.startsWith('backend_local_document_mismatch:'));
2048
+ }
2049
+ function taskIdFromDelivery(value) {
2050
+ const body = objectValue(value);
2051
+ if (!body || typeof body.taskId !== 'string') {
2052
+ throw new Error('Pending task delivery does not contain a task identifier');
2053
+ }
2054
+ return body.taskId;
2055
+ }
2056
+ function expectedTaskVersionFromBody(value) {
2057
+ const body = objectValue(value);
2058
+ return body && typeof body.expectedTaskVersion === 'number'
2059
+ ? numericTaskVersion(body.expectedTaskVersion)
2060
+ : null;
2061
+ }
2062
+ function taskIdFromDeliverySafe(entry) {
2063
+ try {
2064
+ return taskIdFromDelivery(entry.body);
2065
+ }
2066
+ catch {
2067
+ return null;
2068
+ }
2069
+ }
2070
+ function resumeConflicts(backend, backendTask, pointer, localJournal, localDocuments, pendingOutbox, responseSource) {
2071
+ const conflicts = [];
2072
+ const backendTaskId = typeof backendTask.id === 'string' ? backendTask.id : null;
2073
+ if (pointer && backendTaskId !== pointer.taskId) {
2074
+ conflicts.push('active_task_identity_mismatch');
2075
+ }
2076
+ if (responseSource === 'stale_cache') {
2077
+ conflicts.push('backend_resume_is_stale');
2078
+ }
2079
+ if (pendingOutbox.length > 0) {
2080
+ conflicts.push('pending_outbox_delivery');
2081
+ }
2082
+ if (localJournal.pendingDeliveryCount > 0) {
2083
+ conflicts.push('pending_local_journal_delivery');
2084
+ }
2085
+ const localEventCount = localJournal.projection?.appliedEventIds.length ?? 0;
2086
+ if (numericSequence(backendTask.lastSequence) !== localEventCount) {
2087
+ conflicts.push('backend_local_sequence_mismatch');
2088
+ }
2089
+ const backendDocuments = Array.isArray(backend.documents) ? backend.documents : [];
2090
+ for (const value of backendDocuments) {
2091
+ const document = objectValue(value);
2092
+ if (!document ||
2093
+ typeof document.documentType !== 'string' ||
2094
+ typeof document.content !== 'string') {
2095
+ conflicts.push('backend_document_snapshot_invalid');
2096
+ continue;
2097
+ }
2098
+ const localContent = localDocuments[document.documentType];
2099
+ if (!localContent || sha256(localContent) !== sha256(document.content)) {
2100
+ conflicts.push(`backend_local_document_mismatch:${document.documentType}`);
2101
+ }
2102
+ }
2103
+ return [...new Set(conflicts)].sort();
2104
+ }
2105
+ export function newMemoryResourceCandidate(entry, policy) {
2106
+ const added = entry.status === '??' || /[ARC]/.test(entry.status);
2107
+ if (!added) {
2108
+ return [];
2109
+ }
2110
+ if (policy) {
2111
+ const screen = policy.screenPathPatterns.some((pattern) => globMatches(entry.path, pattern));
2112
+ const component = policy.componentPathPatterns.some((pattern) => globMatches(entry.path, pattern));
2113
+ if (screen && component) {
2114
+ throw new Error(`Resource discovery policy is ambiguous for path: ${entry.path}`);
2115
+ }
2116
+ if (screen) {
2117
+ return [{ path: entry.path, kind: 'screen_logic' }];
2118
+ }
2119
+ if (component) {
2120
+ return [{ path: entry.path, kind: 'component_mapping' }];
2121
+ }
2122
+ return [];
2123
+ }
2124
+ const extension = entry.path.toLowerCase().match(/\.[^.\/]+$/)?.[0];
2125
+ if (!extension ||
2126
+ !new Set([
2127
+ '.dart',
2128
+ '.ts',
2129
+ '.tsx',
2130
+ '.js',
2131
+ '.jsx',
2132
+ '.swift',
2133
+ '.kt',
2134
+ '.kts',
2135
+ '.java',
2136
+ '.cs',
2137
+ '.go',
2138
+ '.rs',
2139
+ '.py',
2140
+ ]).has(extension)) {
2141
+ return [];
2142
+ }
2143
+ const segments = entry.path.toLowerCase().split('/').slice(0, -1);
2144
+ const screenIndex = segments.findIndex((segment) => ['screen', 'screens', 'view', 'views'].includes(segment));
2145
+ const componentIndex = segments.findIndex((segment) => ['widget', 'widgets', 'component', 'components'].includes(segment));
2146
+ if (screenIndex >= 0 && (componentIndex < 0 || screenIndex < componentIndex)) {
2147
+ return [{ path: entry.path, kind: 'screen_logic' }];
2148
+ }
2149
+ if (componentIndex >= 0) {
2150
+ return [{ path: entry.path, kind: 'component_mapping' }];
2151
+ }
2152
+ return [];
2153
+ }
2154
+ function readResourceDiscoveryPolicy(snapshot, activeLease) {
2155
+ const policy = objectValue(snapshot?.resourceDiscoveryPolicy) ??
2156
+ objectValue(activeLease?.resourceDiscoveryPolicy);
2157
+ const metadata = objectValue(policy?.metadata);
2158
+ const discovery = objectValue(metadata?.resourceDiscovery) ?? policy;
2159
+ if (!discovery) {
2160
+ return null;
2161
+ }
2162
+ const screenPathPatterns = stringArray(discovery.screenPathPatterns);
2163
+ const componentPathPatterns = stringArray(discovery.componentPathPatterns);
2164
+ if (screenPathPatterns.length === 0 || componentPathPatterns.length === 0) {
2165
+ throw new Error('Pinned resource discovery policy is incomplete');
2166
+ }
2167
+ return { screenPathPatterns, componentPathPatterns };
2168
+ }
2169
+ function stringArray(value) {
2170
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
2171
+ return [];
2172
+ }
2173
+ return [...new Set(value.filter((entry) => typeof entry === 'string'))].sort();
2174
+ }
2175
+ function globMatches(path, pattern) {
2176
+ const normalizedPattern = pattern.replace(/\\/g, '/').replace(/^\.\//, '');
2177
+ if (!normalizedPattern ||
2178
+ normalizedPattern.startsWith('/') ||
2179
+ /^[A-Za-z]:\//.test(normalizedPattern) ||
2180
+ normalizedPattern.split('/').includes('..')) {
2181
+ throw new Error(`Resource discovery pattern is unsafe: ${pattern}`);
2182
+ }
2183
+ return minimatch(path.replace(/\\/g, '/'), normalizedPattern, { dot: true });
2184
+ }
2185
+ function assertNewResourceEvidence(candidates, evidence) {
2186
+ const normalizedEvidence = evidence.map((entry) => ({
2187
+ ...entry,
2188
+ path: normalizeChangedPaths([entry.path])[0],
2189
+ }));
2190
+ if (normalizedEvidence.some((entry) => entry.resourceKey.trim().length < 2)) {
2191
+ throw new Error('New resource evidence requires a memory resource key');
2192
+ }
2193
+ if (new Set(normalizedEvidence.map((entry) => entry.path)).size !== normalizedEvidence.length) {
2194
+ throw new Error('New resource evidence contains duplicate paths');
2195
+ }
2196
+ const expected = [...candidates].sort((left, right) => left.path.localeCompare(right.path));
2197
+ const actual = normalizedEvidence
2198
+ .map(({ path, kind }) => ({ path, kind }))
2199
+ .sort((left, right) => left.path.localeCompare(right.path));
2200
+ if (stableStringify(expected) !== stableStringify(actual)) {
2201
+ throw new Error('New screen or shared component paths require exact memory reconciliation');
2202
+ }
2203
+ }
2204
+ function assertValidationEvidence(validations) {
2205
+ if (validations.length === 0) {
2206
+ throw new Error('Validation evidence is required');
2207
+ }
2208
+ const allowed = new Set(validationIds);
2209
+ const seen = new Set();
2210
+ for (const validation of validations) {
2211
+ if (!allowed.has(validation.validationId)) {
2212
+ throw new Error(`Unknown validation evidence: ${validation.validationId}`);
2213
+ }
2214
+ if (seen.has(validation.validationId)) {
2215
+ throw new Error(`Duplicate validation evidence: ${validation.validationId}`);
2216
+ }
2217
+ if (!validation.command.trim()) {
2218
+ throw new Error(`Validation command is required: ${validation.validationId}`);
2219
+ }
2220
+ if (!/^[0-9a-f]{64}$/.test(validation.outputHash)) {
2221
+ throw new Error(`Validation output hash is invalid: ${validation.validationId}`);
2222
+ }
2223
+ seen.add(validation.validationId);
2224
+ }
2225
+ }
2226
+ //# sourceMappingURL=bridge-service.js.map