@open-agent-toolkit/cli 0.2.65 → 0.2.66

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 (142) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +19 -1
  3. package/assets/docs/cli-utilities/index.md +3 -0
  4. package/assets/docs/cli-utilities/remote-project-management.md +191 -0
  5. package/assets/docs/reference/cli-reference.md +12 -1
  6. package/assets/docs/reference/file-locations.md +22 -0
  7. package/assets/docs/reference/oat-directory-structure.md +30 -17
  8. package/assets/docs/reference/troubleshooting.md +44 -0
  9. package/assets/docs/workflows/skills/index.md +33 -0
  10. package/assets/public-package-versions.json +4 -4
  11. package/assets/skills/oat-doctor/SKILL.md +4 -4
  12. package/assets/skills/oat-pjm-remote/SKILL.md +101 -0
  13. package/assets/skills/oat-pjm-remote/references/external-action-protocol.md +43 -0
  14. package/dist/commands/backlog/new.d.ts +1 -0
  15. package/dist/commands/backlog/new.d.ts.map +1 -1
  16. package/dist/commands/backlog/new.js +3 -1
  17. package/dist/commands/config/index.d.ts +1 -0
  18. package/dist/commands/config/index.d.ts.map +1 -1
  19. package/dist/commands/config/index.js +232 -1
  20. package/dist/commands/pjm/doctor.d.ts +2 -0
  21. package/dist/commands/pjm/doctor.d.ts.map +1 -1
  22. package/dist/commands/pjm/doctor.js +71 -1
  23. package/dist/commands/pjm/index.d.ts +2 -0
  24. package/dist/commands/pjm/index.d.ts.map +1 -1
  25. package/dist/commands/pjm/index.js +3 -0
  26. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts +60 -0
  27. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts.map +1 -0
  28. package/dist/commands/pjm/remote/__integration__/lifecycle-harness.js +151 -0
  29. package/dist/commands/pjm/remote/association.d.ts +27 -0
  30. package/dist/commands/pjm/remote/association.d.ts.map +1 -0
  31. package/dist/commands/pjm/remote/association.js +68 -0
  32. package/dist/commands/pjm/remote/authority.d.ts +210 -0
  33. package/dist/commands/pjm/remote/authority.d.ts.map +1 -0
  34. package/dist/commands/pjm/remote/authority.js +341 -0
  35. package/dist/commands/pjm/remote/batch.d.ts +15 -0
  36. package/dist/commands/pjm/remote/batch.d.ts.map +1 -0
  37. package/dist/commands/pjm/remote/batch.js +126 -0
  38. package/dist/commands/pjm/remote/closeout.d.ts +69 -0
  39. package/dist/commands/pjm/remote/closeout.d.ts.map +1 -0
  40. package/dist/commands/pjm/remote/closeout.js +208 -0
  41. package/dist/commands/pjm/remote/create-binding.d.ts +62 -0
  42. package/dist/commands/pjm/remote/create-binding.d.ts.map +1 -0
  43. package/dist/commands/pjm/remote/create-binding.js +126 -0
  44. package/dist/commands/pjm/remote/credential-safety.d.ts +7 -0
  45. package/dist/commands/pjm/remote/credential-safety.d.ts.map +1 -0
  46. package/dist/commands/pjm/remote/credential-safety.js +21 -0
  47. package/dist/commands/pjm/remote/discussion.d.ts +49 -0
  48. package/dist/commands/pjm/remote/discussion.d.ts.map +1 -0
  49. package/dist/commands/pjm/remote/discussion.js +124 -0
  50. package/dist/commands/pjm/remote/doctor.d.ts +22 -0
  51. package/dist/commands/pjm/remote/doctor.d.ts.map +1 -0
  52. package/dist/commands/pjm/remote/doctor.js +380 -0
  53. package/dist/commands/pjm/remote/external-action.d.ts +208 -0
  54. package/dist/commands/pjm/remote/external-action.d.ts.map +1 -0
  55. package/dist/commands/pjm/remote/external-action.js +367 -0
  56. package/dist/commands/pjm/remote/host-execution.d.ts +28 -0
  57. package/dist/commands/pjm/remote/host-execution.d.ts.map +1 -0
  58. package/dist/commands/pjm/remote/host-execution.js +63 -0
  59. package/dist/commands/pjm/remote/index.d.ts +41 -0
  60. package/dist/commands/pjm/remote/index.d.ts.map +1 -0
  61. package/dist/commands/pjm/remote/index.js +280 -0
  62. package/dist/commands/pjm/remote/jira-adf.d.ts +35 -0
  63. package/dist/commands/pjm/remote/jira-adf.d.ts.map +1 -0
  64. package/dist/commands/pjm/remote/jira-adf.js +169 -0
  65. package/dist/commands/pjm/remote/lifecycle.d.ts +163 -0
  66. package/dist/commands/pjm/remote/lifecycle.d.ts.map +1 -0
  67. package/dist/commands/pjm/remote/lifecycle.js +224 -0
  68. package/dist/commands/pjm/remote/local-projection.d.ts +30 -0
  69. package/dist/commands/pjm/remote/local-projection.d.ts.map +1 -0
  70. package/dist/commands/pjm/remote/local-projection.js +95 -0
  71. package/dist/commands/pjm/remote/managed-markdown.d.ts +30 -0
  72. package/dist/commands/pjm/remote/managed-markdown.d.ts.map +1 -0
  73. package/dist/commands/pjm/remote/managed-markdown.js +128 -0
  74. package/dist/commands/pjm/remote/migrate.d.ts +14 -0
  75. package/dist/commands/pjm/remote/migrate.d.ts.map +1 -0
  76. package/dist/commands/pjm/remote/migrate.js +32 -0
  77. package/dist/commands/pjm/remote/operation-state.d.ts +15 -0
  78. package/dist/commands/pjm/remote/operation-state.d.ts.map +1 -0
  79. package/dist/commands/pjm/remote/operation-state.js +143 -0
  80. package/dist/commands/pjm/remote/outbound-projection-safety.d.ts +25 -0
  81. package/dist/commands/pjm/remote/outbound-projection-safety.d.ts.map +1 -0
  82. package/dist/commands/pjm/remote/outbound-projection-safety.js +99 -0
  83. package/dist/commands/pjm/remote/output.d.ts +60 -0
  84. package/dist/commands/pjm/remote/output.d.ts.map +1 -0
  85. package/dist/commands/pjm/remote/output.js +48 -0
  86. package/dist/commands/pjm/remote/preview.d.ts +88 -0
  87. package/dist/commands/pjm/remote/preview.d.ts.map +1 -0
  88. package/dist/commands/pjm/remote/preview.js +154 -0
  89. package/dist/commands/pjm/remote/provider-conformance.d.ts +13 -0
  90. package/dist/commands/pjm/remote/provider-conformance.d.ts.map +1 -0
  91. package/dist/commands/pjm/remote/provider-conformance.js +37 -0
  92. package/dist/commands/pjm/remote/provider.d.ts +57 -0
  93. package/dist/commands/pjm/remote/provider.d.ts.map +1 -0
  94. package/dist/commands/pjm/remote/provider.js +28 -0
  95. package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts +46 -0
  96. package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts.map +1 -0
  97. package/dist/commands/pjm/remote/providers/github-publication-safety.js +126 -0
  98. package/dist/commands/pjm/remote/providers/github.d.ts +207 -0
  99. package/dist/commands/pjm/remote/providers/github.d.ts.map +1 -0
  100. package/dist/commands/pjm/remote/providers/github.js +1456 -0
  101. package/dist/commands/pjm/remote/providers/jira.d.ts +245 -0
  102. package/dist/commands/pjm/remote/providers/jira.d.ts.map +1 -0
  103. package/dist/commands/pjm/remote/providers/jira.js +1650 -0
  104. package/dist/commands/pjm/remote/providers/linear.d.ts +182 -0
  105. package/dist/commands/pjm/remote/providers/linear.d.ts.map +1 -0
  106. package/dist/commands/pjm/remote/providers/linear.js +1289 -0
  107. package/dist/commands/pjm/remote/purpose-policy.d.ts +29 -0
  108. package/dist/commands/pjm/remote/purpose-policy.d.ts.map +1 -0
  109. package/dist/commands/pjm/remote/purpose-policy.js +118 -0
  110. package/dist/commands/pjm/remote/reconcile.d.ts +42 -0
  111. package/dist/commands/pjm/remote/reconcile.d.ts.map +1 -0
  112. package/dist/commands/pjm/remote/reconcile.js +101 -0
  113. package/dist/commands/pjm/remote/resolution.d.ts +122 -0
  114. package/dist/commands/pjm/remote/resolution.d.ts.map +1 -0
  115. package/dist/commands/pjm/remote/resolution.js +243 -0
  116. package/dist/commands/pjm/remote/schema.d.ts +9309 -0
  117. package/dist/commands/pjm/remote/schema.d.ts.map +1 -0
  118. package/dist/commands/pjm/remote/schema.js +1503 -0
  119. package/dist/commands/pjm/remote/service.d.ts +20 -0
  120. package/dist/commands/pjm/remote/service.d.ts.map +1 -0
  121. package/dist/commands/pjm/remote/service.js +5121 -0
  122. package/dist/commands/pjm/remote/shared-storage.d.ts +36 -0
  123. package/dist/commands/pjm/remote/shared-storage.d.ts.map +1 -0
  124. package/dist/commands/pjm/remote/shared-storage.js +110 -0
  125. package/dist/commands/pjm/remote/snapshot.d.ts +32 -0
  126. package/dist/commands/pjm/remote/snapshot.d.ts.map +1 -0
  127. package/dist/commands/pjm/remote/snapshot.js +140 -0
  128. package/dist/commands/pjm/remote/storage-locator.d.ts +29 -0
  129. package/dist/commands/pjm/remote/storage-locator.d.ts.map +1 -0
  130. package/dist/commands/pjm/remote/storage-locator.js +62 -0
  131. package/dist/commands/pjm/remote/store.d.ts +85 -0
  132. package/dist/commands/pjm/remote/store.d.ts.map +1 -0
  133. package/dist/commands/pjm/remote/store.js +553 -0
  134. package/dist/commands/pjm/remote/verification.d.ts +40 -0
  135. package/dist/commands/pjm/remote/verification.d.ts.map +1 -0
  136. package/dist/commands/pjm/remote/verification.js +100 -0
  137. package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
  138. package/dist/commands/tools/shared/pack-manifest.js +1 -0
  139. package/dist/config/oat-config.d.ts +26 -0
  140. package/dist/config/oat-config.d.ts.map +1 -1
  141. package/dist/config/oat-config.js +186 -2
  142. package/package.json +2 -2
@@ -0,0 +1,553 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, open, readFile, readdir, rename, unlink, } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { isDeepStrictEqual } from 'node:util';
5
+ import { parseExternalAction, } from './external-action.js';
6
+ import { transitionRemoteOperation } from './operation-state.js';
7
+ import { PlannedBindingCreateSchema, RemoteBatchRecordSchema, RemoteBindingMetadataSchema, RemoteBindingStateSchema, RemoteOperationRecordSchema, VerifiedDurableRemoteIdentitySchema, assertRecordIdMatchesFilename, } from './schema.js';
8
+ import { parseSharedStoragePreview, } from './shared-storage.js';
9
+ async function syncDirectory(path) {
10
+ let handle;
11
+ try {
12
+ handle = await open(path, 'r');
13
+ await handle.sync();
14
+ }
15
+ catch (error) {
16
+ if (!isFilesystemError(error, 'EINVAL') &&
17
+ !isFilesystemError(error, 'ENOTSUP')) {
18
+ throw error;
19
+ }
20
+ }
21
+ finally {
22
+ await handle?.close();
23
+ }
24
+ }
25
+ export const defaultRemoteStoreFilesystem = {
26
+ mkdir,
27
+ open,
28
+ readFile,
29
+ readdir,
30
+ rename,
31
+ unlink,
32
+ syncDirectory,
33
+ };
34
+ const DEFAULT_DEPENDENCIES = {
35
+ filesystem: defaultRemoteStoreFilesystem,
36
+ randomId: randomUUID,
37
+ };
38
+ const ACTIVE_OPERATION_STATES = new Set([
39
+ 'planned',
40
+ 'pending',
41
+ 'authorized',
42
+ 'attempt-started',
43
+ 'verification-pending',
44
+ 'partial',
45
+ 'uncertain',
46
+ ]);
47
+ export class RemoteSyncStore {
48
+ locations;
49
+ #dependencies;
50
+ constructor(locations, dependencies = {}) {
51
+ this.locations = locations;
52
+ this.#dependencies = { ...DEFAULT_DEPENDENCIES, ...dependencies };
53
+ }
54
+ async readBindingMetadata(bindingId) {
55
+ return this.#readRecord(join(this.locations.portable.bindingsDir, `${bindingId}.json`), bindingId, RemoteBindingMetadataSchema);
56
+ }
57
+ async materializeVerifiedBinding(operationId, record, verification) {
58
+ const parsed = RemoteBindingMetadataSchema.parse(record);
59
+ assertVerifiedDurableRemoteIdentity(parsed, verification);
60
+ const operation = await this.readOperation(operationId);
61
+ assertMaterializationMatchesCreateIntent(operationId, operation, parsed, verification);
62
+ const path = join(this.locations.portable.bindingsDir, `${parsed.bindingId}.json`);
63
+ assertRecordIdMatchesFilename(path, parsed.bindingId);
64
+ const existing = await this.readBindingMetadata(parsed.bindingId);
65
+ if (existing) {
66
+ if (!isDeepStrictEqual(existing, parsed)) {
67
+ throw new Error(`Binding metadata '${parsed.bindingId}' conflicts with its materialization plan.`);
68
+ }
69
+ return;
70
+ }
71
+ await this.#atomicWrite(path, parsed);
72
+ }
73
+ async listBindingMetadata() {
74
+ return this.#listRecords(this.locations.portable.bindingsDir, RemoteBindingMetadataSchema, (record) => record.bindingId);
75
+ }
76
+ async updateBindingMetadata(record) {
77
+ const parsed = RemoteBindingMetadataSchema.parse(record);
78
+ if (!(await this.readBindingMetadata(parsed.bindingId))) {
79
+ throw new Error(`Remote binding '${parsed.bindingId}' does not exist.`);
80
+ }
81
+ await this.#atomicWrite(join(this.locations.portable.bindingsDir, `${parsed.bindingId}.json`), parsed);
82
+ }
83
+ async readBindingState(bindingId) {
84
+ return this.#readRecord(join(this.locations.operational.bindingsDir, `${bindingId}.json`), bindingId, RemoteBindingStateSchema);
85
+ }
86
+ async writeBindingState(record) {
87
+ const parsed = RemoteBindingStateSchema.parse(record);
88
+ const path = join(this.locations.operational.bindingsDir, `${parsed.bindingId}.json`);
89
+ assertRecordIdMatchesFilename(path, parsed.bindingId);
90
+ await this.#atomicWrite(path, parsed);
91
+ }
92
+ async readOperation(operationId) {
93
+ return this.#readRecord(join(this.locations.operational.operationsDir, `${operationId}.json`), operationId, RemoteOperationRecordSchema);
94
+ }
95
+ async readCurrentAction(operationId) {
96
+ const operation = await this.readOperation(operationId);
97
+ if (operation?.currentAction) {
98
+ return parseExternalAction(operation.currentAction);
99
+ }
100
+ try {
101
+ return parseExternalAction(JSON.parse(await this.#dependencies.filesystem.readFile(join(this.locations.operational.operationsDir, `${operationId}.action`), 'utf8')));
102
+ }
103
+ catch (error) {
104
+ if (isFilesystemError(error, 'ENOENT'))
105
+ return null;
106
+ throw error;
107
+ }
108
+ }
109
+ async writeCurrentAction(operationId, action) {
110
+ const parsed = await this.writeActionEvidence(operationId, action);
111
+ await this.#atomicWrite(join(this.locations.operational.operationsDir, `${operationId}.action`), parsed);
112
+ }
113
+ async writeActionEvidence(operationId, action) {
114
+ const parsed = parseExternalAction(action);
115
+ if (parsed.operationId !== operationId) {
116
+ throw new Error('External action operationId does not match its durable path.');
117
+ }
118
+ const evidencePath = join(this.locations.operational.operationsDir, `${operationId}.${parsed.stepId}.action`);
119
+ try {
120
+ await this.#exclusiveWrite(evidencePath, parsed);
121
+ }
122
+ catch (error) {
123
+ const existing = await this.readAction(operationId, parsed.stepId);
124
+ if (!existing || !isDeepStrictEqual(existing, parsed))
125
+ throw error;
126
+ }
127
+ return parsed;
128
+ }
129
+ async retireCurrentAction(operationId, expectedAction) {
130
+ const parsed = parseExternalAction(expectedAction);
131
+ if (parsed.operationId !== operationId) {
132
+ throw new Error('External action operationId does not match its durable path.');
133
+ }
134
+ const pointerPath = join(this.locations.operational.operationsDir, `${operationId}.action`);
135
+ let current;
136
+ try {
137
+ current = parseExternalAction(JSON.parse(await this.#dependencies.filesystem.readFile(pointerPath, 'utf8')));
138
+ }
139
+ catch (error) {
140
+ if (isFilesystemError(error, 'ENOENT')) {
141
+ throw new Error('Durable current action pointer is missing before verification handoff.', { cause: error });
142
+ }
143
+ throw error;
144
+ }
145
+ if (!isDeepStrictEqual(current, parsed)) {
146
+ throw new Error('Durable current action pointer contradicts the accepted mutation.');
147
+ }
148
+ await this.#dependencies.filesystem.unlink(pointerPath);
149
+ await this.#dependencies.filesystem.syncDirectory(this.locations.operational.operationsDir);
150
+ }
151
+ async retireCurrentActionPointerIfPresent(operationId, expectedAction) {
152
+ const parsed = parseExternalAction(expectedAction);
153
+ if (parsed.operationId !== operationId) {
154
+ throw new Error('External action operationId does not match its durable path.');
155
+ }
156
+ const pointerPath = join(this.locations.operational.operationsDir, `${operationId}.action`);
157
+ let current;
158
+ try {
159
+ current = parseExternalAction(JSON.parse(await this.#dependencies.filesystem.readFile(pointerPath, 'utf8')));
160
+ }
161
+ catch (error) {
162
+ if (isFilesystemError(error, 'ENOENT'))
163
+ return;
164
+ throw error;
165
+ }
166
+ if (!isDeepStrictEqual(current, parsed)) {
167
+ throw new Error('Durable current action pointer contradicts the accepted mutation.');
168
+ }
169
+ await this.#dependencies.filesystem.unlink(pointerPath);
170
+ await this.#dependencies.filesystem.syncDirectory(this.locations.operational.operationsDir);
171
+ }
172
+ async readAction(operationId, stepId) {
173
+ try {
174
+ return parseExternalAction(JSON.parse(await this.#dependencies.filesystem.readFile(join(this.locations.operational.operationsDir, `${operationId}.${stepId}.action`), 'utf8')));
175
+ }
176
+ catch (error) {
177
+ if (isFilesystemError(error, 'ENOENT'))
178
+ return null;
179
+ throw error;
180
+ }
181
+ }
182
+ async readSharedStoragePreview() {
183
+ try {
184
+ return parseSharedStoragePreview(JSON.parse(await this.#dependencies.filesystem.readFile(join(this.locations.operational.root, 'shared-storage-preview.json'), 'utf8')));
185
+ }
186
+ catch (error) {
187
+ if (isFilesystemError(error, 'ENOENT'))
188
+ return null;
189
+ throw error;
190
+ }
191
+ }
192
+ async writeSharedStoragePreview(preview) {
193
+ await this.#atomicWrite(join(this.locations.operational.root, 'shared-storage-preview.json'), parseSharedStoragePreview(preview));
194
+ }
195
+ async materializeIntakeBinding(record) {
196
+ const parsed = RemoteBindingMetadataSchema.parse(record);
197
+ const path = join(this.locations.portable.bindingsDir, `${parsed.bindingId}.json`);
198
+ assertRecordIdMatchesFilename(path, parsed.bindingId);
199
+ try {
200
+ await this.#exclusiveWrite(path, parsed);
201
+ }
202
+ catch (error) {
203
+ const existing = await this.readBindingMetadata(parsed.bindingId);
204
+ if (!existing || !isDeepStrictEqual(existing, parsed))
205
+ throw error;
206
+ }
207
+ }
208
+ async createOperation(record) {
209
+ const parsed = RemoteOperationRecordSchema.parse(record);
210
+ const path = join(this.locations.operational.operationsDir, `${parsed.operationId}.json`);
211
+ assertRecordIdMatchesFilename(path, parsed.operationId);
212
+ await this.#exclusiveWrite(path, parsed);
213
+ }
214
+ async updateOperation(record) {
215
+ const parsed = RemoteOperationRecordSchema.parse(record);
216
+ if (!(await this.readOperation(parsed.operationId))) {
217
+ throw new Error(`Remote operation '${parsed.operationId}' does not exist.`);
218
+ }
219
+ await this.#atomicWrite(join(this.locations.operational.operationsDir, `${parsed.operationId}.json`), parsed);
220
+ }
221
+ async createBindingIntent(intent) {
222
+ const parsed = PlannedBindingCreateSchema.parse(intent);
223
+ await this.createOperation({
224
+ recordType: 'operation',
225
+ schemaVersion: 2,
226
+ operationId: parsed.operationId,
227
+ correlationId: parsed.operationId,
228
+ bindingId: parsed.bindingId,
229
+ provider: parsed.provider,
230
+ providerContext: parsed.providerContext,
231
+ lifecycleOperation: 'publish',
232
+ operationClass: 'create',
233
+ state: 'planned',
234
+ reason: null,
235
+ lastSafeStep: 'planned',
236
+ preview: {
237
+ digest: parsed.provenanceToken,
238
+ bindingId: parsed.bindingId,
239
+ provider: parsed.provider,
240
+ providerContext: parsed.providerContext,
241
+ capabilityEvidenceDigest: 'unprobed',
242
+ revisionDigest: 'unbound',
243
+ policyDigest: parsed.provenanceToken,
244
+ },
245
+ authority: {
246
+ effective: parsed.policyRestrictions.authority?.operations?.create ??
247
+ parsed.policyRestrictions.authority?.default ??
248
+ 'read-only',
249
+ sourceDigest: parsed.provenanceToken,
250
+ },
251
+ approval: null,
252
+ createdAt: parsed.createdAt,
253
+ updatedAt: parsed.createdAt,
254
+ selectedExecution: null,
255
+ attempts: [],
256
+ observations: [],
257
+ verification: [],
258
+ retryDisposition: 'safe-before-attempt',
259
+ steps: [],
260
+ outcome: {
261
+ classification: 'pending',
262
+ message: null,
263
+ verifiedAt: null,
264
+ },
265
+ createIntent: parsed,
266
+ });
267
+ }
268
+ async listActiveOperations(bindingId) {
269
+ const operations = await this.#listRecords(this.locations.operational.operationsDir, RemoteOperationRecordSchema, (record) => record.operationId);
270
+ return operations.filter((operation) => operation.bindingId === bindingId &&
271
+ ACTIVE_OPERATION_STATES.has(operation.state));
272
+ }
273
+ async detectConcurrentOperationIntents(bindingId) {
274
+ const bindingState = await this.readBindingState(bindingId);
275
+ const activeOperations = await this.listActiveOperations(bindingId);
276
+ return {
277
+ bindingState,
278
+ activeOperations,
279
+ hasConcurrentIntents: activeOperations.length > 1,
280
+ };
281
+ }
282
+ async transitionOperation(operationId, expectedState, update) {
283
+ const current = await this.readOperation(operationId);
284
+ if (!current) {
285
+ throw new Error(`Remote operation '${operationId}' does not exist.`);
286
+ }
287
+ if (current.state !== expectedState) {
288
+ throw new Error(`Remote operation '${operationId}' expected state '${expectedState}' but found '${current.state}'.`);
289
+ }
290
+ if (current.operationClass !== null &&
291
+ update.state !== current.state &&
292
+ (current.attempts.length > 0 ||
293
+ update.appendAttempt !== undefined ||
294
+ update.appendObservation !== undefined)) {
295
+ transitionRemoteOperation(current.state, update.state);
296
+ }
297
+ if (update.appendAttempt &&
298
+ current.attempts.some((attempt) => attempt.attemptId === update.appendAttempt.attemptId)) {
299
+ throw new Error('Remote operation attempt evidence is a duplicate.');
300
+ }
301
+ if (update.appendObservation?.actionDigest &&
302
+ current.observations.some((item) => item.actionDigest === update.appendObservation.actionDigest)) {
303
+ throw new Error('Remote operation observation evidence is a duplicate.');
304
+ }
305
+ if (update.appendMaterializationStep &&
306
+ (current.materializationSteps ?? []).some((item) => item.step === update.appendMaterializationStep.step)) {
307
+ throw new Error('Remote operation materialization step is a duplicate.');
308
+ }
309
+ if (update.materializationPlan &&
310
+ current.materializationPlan &&
311
+ !isDeepStrictEqual(update.materializationPlan, current.materializationPlan)) {
312
+ throw new Error('Remote operation materialization plan is immutable.');
313
+ }
314
+ if (update.verificationHandoff &&
315
+ current.verificationHandoff &&
316
+ !isDeepStrictEqual(update.verificationHandoff, current.verificationHandoff)) {
317
+ throw new Error('Remote operation verification handoff is immutable.');
318
+ }
319
+ if (update.currentAction &&
320
+ current.currentAction &&
321
+ !isDeepStrictEqual(update.currentAction, current.currentAction)) {
322
+ throw new Error('Remote operation canonical current action is immutable.');
323
+ }
324
+ let attempts = update.appendAttempt
325
+ ? [...current.attempts, update.appendAttempt]
326
+ : current.attempts;
327
+ if (update.completeAttempt) {
328
+ const index = attempts.findIndex((attempt) => attempt.attemptId === update.completeAttempt.attemptId);
329
+ if (index < 0 || attempts[index].completedAt) {
330
+ throw new Error('Remote operation attempt completion is stale.');
331
+ }
332
+ attempts = attempts.map((attempt, attemptIndex) => attemptIndex === index
333
+ ? {
334
+ ...attempt,
335
+ completedAt: update.completeAttempt.completedAt,
336
+ receiptDigest: update.completeAttempt.receiptDigest,
337
+ }
338
+ : attempt);
339
+ }
340
+ const next = RemoteOperationRecordSchema.parse({
341
+ ...current,
342
+ state: update.state,
343
+ updatedAt: update.updatedAt,
344
+ ...(update.selectedExecution !== undefined
345
+ ? { selectedExecution: update.selectedExecution }
346
+ : {}),
347
+ ...(update.outcome !== undefined ? { outcome: update.outcome } : {}),
348
+ ...(update.approval !== undefined ? { approval: update.approval } : {}),
349
+ ...(update.verification !== undefined
350
+ ? { verification: update.verification }
351
+ : {}),
352
+ ...(update.lastSafeStep !== undefined
353
+ ? { lastSafeStep: update.lastSafeStep }
354
+ : {}),
355
+ ...(update.retryDisposition !== undefined
356
+ ? { retryDisposition: update.retryDisposition }
357
+ : {}),
358
+ attempts,
359
+ observations: update.appendObservation
360
+ ? [...current.observations, update.appendObservation]
361
+ : current.observations,
362
+ steps: update.appendStep
363
+ ? [...current.steps, update.appendStep]
364
+ : current.steps,
365
+ materializationSteps: update.appendMaterializationStep
366
+ ? [
367
+ ...(current.materializationSteps ?? []),
368
+ update.appendMaterializationStep,
369
+ ]
370
+ : current.materializationSteps,
371
+ ...(update.materializationPlan !== undefined
372
+ ? { materializationPlan: update.materializationPlan }
373
+ : {}),
374
+ ...(update.verificationHandoff !== undefined
375
+ ? { verificationHandoff: update.verificationHandoff }
376
+ : {}),
377
+ ...(update.currentAction !== undefined
378
+ ? { currentAction: update.currentAction }
379
+ : {}),
380
+ });
381
+ await this.#atomicWrite(join(this.locations.operational.operationsDir, `${operationId}.json`), next);
382
+ return next;
383
+ }
384
+ async readBatch(batchId) {
385
+ return this.#readRecord(join(this.locations.operational.batchesDir, `${batchId}.json`), batchId, RemoteBatchRecordSchema);
386
+ }
387
+ async createBatch(record) {
388
+ const parsed = RemoteBatchRecordSchema.parse(record);
389
+ const path = join(this.locations.operational.batchesDir, `${parsed.batchId}.json`);
390
+ assertRecordIdMatchesFilename(path, parsed.batchId);
391
+ await this.#exclusiveWrite(path, parsed);
392
+ }
393
+ async updateBatch(record) {
394
+ const parsed = RemoteBatchRecordSchema.parse(record);
395
+ const existing = await this.readBatch(parsed.batchId);
396
+ if (!existing) {
397
+ throw new Error(`Remote batch '${parsed.batchId}' does not exist.`);
398
+ }
399
+ await this.#atomicWrite(join(this.locations.operational.batchesDir, `${parsed.batchId}.json`), parsed);
400
+ }
401
+ async #readRecord(path, expectedId, schema) {
402
+ try {
403
+ const parsed = schema.parse(JSON.parse(await this.#dependencies.filesystem.readFile(path, 'utf8')));
404
+ assertRecordIdMatchesFilename(path, expectedId);
405
+ return parsed;
406
+ }
407
+ catch (error) {
408
+ if (isFilesystemError(error, 'ENOENT'))
409
+ return null;
410
+ throw error;
411
+ }
412
+ }
413
+ async #listRecords(directory, schema, id) {
414
+ let filenames;
415
+ try {
416
+ filenames = await this.#dependencies.filesystem.readdir(directory);
417
+ }
418
+ catch (error) {
419
+ if (isFilesystemError(error, 'ENOENT'))
420
+ return [];
421
+ throw error;
422
+ }
423
+ const records = [];
424
+ for (const filename of filenames
425
+ .filter((name) => name.endsWith('.json'))
426
+ .sort()) {
427
+ const path = join(directory, filename);
428
+ const record = schema.parse(JSON.parse(await this.#dependencies.filesystem.readFile(path, 'utf8')));
429
+ assertRecordIdMatchesFilename(path, id(record));
430
+ records.push(record);
431
+ }
432
+ return records;
433
+ }
434
+ async #atomicWrite(path, value) {
435
+ const filesystem = this.#dependencies.filesystem;
436
+ const directory = dirname(path);
437
+ await filesystem.mkdir(directory, { recursive: true, mode: 0o700 });
438
+ const tempPath = `${path}.${this.#dependencies.randomId()}.tmp`;
439
+ const handle = await filesystem.open(tempPath, 'wx', 0o600);
440
+ try {
441
+ await handle.writeFile(serializeRecord(value));
442
+ await handle.sync();
443
+ }
444
+ finally {
445
+ await handle.close();
446
+ }
447
+ try {
448
+ await filesystem.rename(tempPath, path);
449
+ await filesystem.syncDirectory(directory);
450
+ }
451
+ catch (error) {
452
+ await ignoreMissing(() => filesystem.unlink(tempPath));
453
+ throw error;
454
+ }
455
+ }
456
+ async #exclusiveWrite(path, value) {
457
+ const filesystem = this.#dependencies.filesystem;
458
+ const directory = dirname(path);
459
+ await filesystem.mkdir(directory, { recursive: true, mode: 0o700 });
460
+ let handle;
461
+ try {
462
+ handle = await filesystem.open(path, 'wx', 0o600);
463
+ }
464
+ catch (error) {
465
+ if (isFilesystemError(error, 'EEXIST')) {
466
+ throw new Error(`Remote record '${path}' already exists.`, {
467
+ cause: error,
468
+ });
469
+ }
470
+ throw error;
471
+ }
472
+ try {
473
+ await handle.writeFile(serializeRecord(value));
474
+ await handle.sync();
475
+ await handle.close();
476
+ await filesystem.syncDirectory(directory);
477
+ }
478
+ catch (error) {
479
+ await handle.close().catch(() => undefined);
480
+ await ignoreMissing(() => filesystem.unlink(path));
481
+ throw error;
482
+ }
483
+ }
484
+ }
485
+ function assertVerifiedDurableRemoteIdentity(record, verification) {
486
+ const result = VerifiedDurableRemoteIdentitySchema.safeParse(verification);
487
+ if (!result.success ||
488
+ result.data.provider !== record.provider ||
489
+ result.data.stableId !== record.remoteIdentity.stableId) {
490
+ throw new Error('Portable binding metadata requires a verified durable remote identity.');
491
+ }
492
+ }
493
+ function assertMaterializationMatchesCreateIntent(operationId, operation, record, verification) {
494
+ if (!operation?.createIntent || operation.operationClass !== 'create') {
495
+ throw new Error(`Create operation journal '${operationId}' does not contain binding intent.`);
496
+ }
497
+ if (!['pending', 'verification-pending', 'partial', 'verified'].includes(operation.state) ||
498
+ !['partial', 'verified'].includes(operation.outcome.classification) ||
499
+ !operation.verification.some((item) => item.field === 'remoteIdentity' &&
500
+ item.status === 'verified' &&
501
+ item.observedHash === verification.evidenceDigest)) {
502
+ throw new Error(`Create operation journal '${operationId}' is not in a verified materialization state.`);
503
+ }
504
+ const intent = operation.createIntent;
505
+ const mismatches = [];
506
+ if (operation.bindingId !== record.bindingId)
507
+ mismatches.push('bindingId');
508
+ if (operation.provider !== record.provider)
509
+ mismatches.push('provider');
510
+ if (!isDeepStrictEqual(intent.target, record.target))
511
+ mismatches.push('target');
512
+ if (!isDeepStrictEqual(intent.providerContext, record.remoteIdentity.context)) {
513
+ mismatches.push('providerContext');
514
+ }
515
+ if (!isDeepStrictEqual(intent.purposes, record.purposes)) {
516
+ mismatches.push('purposes');
517
+ }
518
+ if (!isDeepStrictEqual(intent.policyRestrictions, record.policyRestrictions)) {
519
+ mismatches.push('policyRestrictions');
520
+ }
521
+ if (!isDeepStrictEqual(intent.publicationProjection, record.publicationProjection)) {
522
+ mismatches.push('publicationProjection');
523
+ }
524
+ if (intent.provenanceToken !== record.provenanceToken) {
525
+ mismatches.push('provenance');
526
+ }
527
+ if (intent.target.kind === 'project' &&
528
+ (intent.projectionStatus !== 'complete' ||
529
+ intent.localProjection?.source !== 'explicit-project-publication')) {
530
+ mismatches.push('localProjection');
531
+ }
532
+ if (mismatches.length > 0) {
533
+ throw new Error(`Binding metadata does not match create intent fields: ${mismatches.join(', ')}.`);
534
+ }
535
+ }
536
+ function serializeRecord(value) {
537
+ return `${JSON.stringify(value, null, 2)}\n`;
538
+ }
539
+ function isFilesystemError(error, code) {
540
+ return (typeof error === 'object' &&
541
+ error !== null &&
542
+ 'code' in error &&
543
+ error.code === code);
544
+ }
545
+ async function ignoreMissing(action) {
546
+ try {
547
+ await action();
548
+ }
549
+ catch (error) {
550
+ if (!isFilesystemError(error, 'ENOENT'))
551
+ throw error;
552
+ }
553
+ }
@@ -0,0 +1,40 @@
1
+ import type { SharedRemoteField } from './purpose-policy.js';
2
+ export interface PostconditionVerificationInput {
3
+ operationId: string;
4
+ requested: {
5
+ fieldMask: SharedRemoteField[];
6
+ expected: Partial<Record<SharedRemoteField, string | null>>;
7
+ };
8
+ providerOutcome: 'accepted' | 'rejected' | 'ambiguous';
9
+ commandExitCode: number | null;
10
+ revisionEvidence: {
11
+ attemptedRevisionDigest: string;
12
+ readbackBaseRevisionDigest: string;
13
+ };
14
+ readback: {
15
+ values: Partial<Record<SharedRemoteField, string | null>>;
16
+ unavailableFields: SharedRemoteField[];
17
+ } | null;
18
+ priorVerification: {
19
+ requestDigest: string;
20
+ classification: 'verified';
21
+ } | null;
22
+ }
23
+ export interface PostconditionFieldVerification {
24
+ field: SharedRemoteField;
25
+ expectedHash: string;
26
+ observedHash: string | null;
27
+ status: 'verified' | 'mismatch' | 'unavailable';
28
+ }
29
+ export type PostconditionClassification = 'verified' | 'already-verified' | 'partial' | 'uncertain' | 'rejected';
30
+ export interface PostconditionVerificationResult {
31
+ classification: PostconditionClassification;
32
+ reason: string;
33
+ requestDigest: string;
34
+ fields: PostconditionFieldVerification[];
35
+ retryDisposition: 'not-applicable' | 'reconcile-required';
36
+ requiresReconciliation: boolean;
37
+ }
38
+ export declare function buildVerificationRequestDigest(input: Pick<PostconditionVerificationInput, 'operationId' | 'requested' | 'revisionEvidence'>): string;
39
+ export declare function verifyRemotePostconditions(input: PostconditionVerificationInput): PostconditionVerificationResult;
40
+ //# sourceMappingURL=verification.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verification.d.ts","sourceRoot":"","sources":["../../../../src/commands/pjm/remote/verification.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE;QACT,SAAS,EAAE,iBAAiB,EAAE,CAAC;QAC/B,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;KAC7D,CAAC;IACF,eAAe,EAAE,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC;IACvD,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE;QAChB,uBAAuB,EAAE,MAAM,CAAC;QAChC,0BAA0B,EAAE,MAAM,CAAC;KACpC,CAAC;IACF,QAAQ,EAAE;QACR,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;QAC1D,iBAAiB,EAAE,iBAAiB,EAAE,CAAC;KACxC,GAAG,IAAI,CAAC;IACT,iBAAiB,EAAE;QACjB,aAAa,EAAE,MAAM,CAAC;QACtB,cAAc,EAAE,UAAU,CAAC;KAC5B,GAAG,IAAI,CAAC;CACV;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,iBAAiB,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,UAAU,GAAG,UAAU,GAAG,aAAa,CAAC;CACjD;AAED,MAAM,MAAM,2BAA2B,GACnC,UAAU,GACV,kBAAkB,GAClB,SAAS,GACT,WAAW,GACX,UAAU,CAAC;AAEf,MAAM,WAAW,+BAA+B;IAC9C,cAAc,EAAE,2BAA2B,CAAC;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,8BAA8B,EAAE,CAAC;IACzC,gBAAgB,EAAE,gBAAgB,GAAG,oBAAoB,CAAC;IAC1D,sBAAsB,EAAE,OAAO,CAAC;CACjC;AAQD,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,IAAI,CACT,8BAA8B,EAC9B,aAAa,GAAG,WAAW,GAAG,kBAAkB,CACjD,GACA,MAAM,CAUR;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,8BAA8B,GACpC,+BAA+B,CAwGjC"}