@dsh-enhanced/plugin-control-plane 0.1.48 → 0.1.53

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 (185) hide show
  1. package/README.md +187 -15
  2. package/bin/dsh-live-qualification-authority.js +15 -0
  3. package/bin/dsh-local-release-adapter.js +331 -9
  4. package/bin/dsh-systemd-host-attestor.js +359 -9
  5. package/bin/dsh-systemd-host-authority.js +12 -0
  6. package/lib/adoption-coordinator.d.ts +3 -1
  7. package/lib/adoption-coordinator.d.ts.map +1 -1
  8. package/lib/adoption-coordinator.js +24 -9
  9. package/lib/adoption-coordinator.js.map +1 -1
  10. package/lib/cli.d.ts.map +1 -1
  11. package/lib/cli.js +33 -14
  12. package/lib/cli.js.map +1 -1
  13. package/lib/creation-capability-feedback.d.ts +23 -0
  14. package/lib/creation-capability-feedback.d.ts.map +1 -0
  15. package/lib/creation-capability-feedback.js +132 -0
  16. package/lib/creation-capability-feedback.js.map +1 -0
  17. package/lib/creation-capability-journal.d.ts +66 -0
  18. package/lib/creation-capability-journal.d.ts.map +1 -0
  19. package/lib/creation-capability-journal.js +775 -0
  20. package/lib/creation-capability-journal.js.map +1 -0
  21. package/lib/creation-capability-runtime.d.ts +63 -0
  22. package/lib/creation-capability-runtime.d.ts.map +1 -0
  23. package/lib/creation-capability-runtime.js +578 -0
  24. package/lib/creation-capability-runtime.js.map +1 -0
  25. package/lib/creation-capability-source.d.ts +37 -0
  26. package/lib/creation-capability-source.d.ts.map +1 -0
  27. package/lib/creation-capability-source.js +328 -0
  28. package/lib/creation-capability-source.js.map +1 -0
  29. package/lib/creation-capability-types.d.ts +265 -0
  30. package/lib/creation-capability-types.d.ts.map +1 -0
  31. package/lib/creation-capability-types.js +2 -0
  32. package/lib/creation-capability-types.js.map +1 -0
  33. package/lib/deployment-readiness.d.ts +16 -0
  34. package/lib/deployment-readiness.d.ts.map +1 -1
  35. package/lib/deployment-readiness.js +48 -1
  36. package/lib/deployment-readiness.js.map +1 -1
  37. package/lib/effect-blocked-replay.js +2 -2
  38. package/lib/effect-blocked-replay.js.map +1 -1
  39. package/lib/foreground-deployment-runtime.d.ts +3 -0
  40. package/lib/foreground-deployment-runtime.d.ts.map +1 -1
  41. package/lib/foreground-deployment-runtime.js +55 -13
  42. package/lib/foreground-deployment-runtime.js.map +1 -1
  43. package/lib/foreground-deployment.d.ts.map +1 -1
  44. package/lib/foreground-deployment.js +5 -0
  45. package/lib/foreground-deployment.js.map +1 -1
  46. package/lib/host-attestor.d.ts +2 -0
  47. package/lib/host-attestor.d.ts.map +1 -1
  48. package/lib/host-attestor.js +8 -1
  49. package/lib/host-attestor.js.map +1 -1
  50. package/lib/host-inputs.d.ts +13 -0
  51. package/lib/host-inputs.d.ts.map +1 -0
  52. package/lib/host-inputs.js +162 -0
  53. package/lib/host-inputs.js.map +1 -0
  54. package/lib/host-maintenance.d.ts +61 -0
  55. package/lib/host-maintenance.d.ts.map +1 -0
  56. package/lib/host-maintenance.js +158 -0
  57. package/lib/host-maintenance.js.map +1 -0
  58. package/lib/host-readiness.d.ts +5 -0
  59. package/lib/host-readiness.d.ts.map +1 -0
  60. package/lib/host-readiness.js +22 -0
  61. package/lib/host-readiness.js.map +1 -0
  62. package/lib/index.d.ts +16 -0
  63. package/lib/index.d.ts.map +1 -1
  64. package/lib/index.js +7 -0
  65. package/lib/index.js.map +1 -1
  66. package/lib/live-qualification-authority.d.ts +32 -0
  67. package/lib/live-qualification-authority.d.ts.map +1 -0
  68. package/lib/live-qualification-authority.js +280 -0
  69. package/lib/live-qualification-authority.js.map +1 -0
  70. package/lib/live-qualification-client.d.ts +4 -0
  71. package/lib/live-qualification-client.d.ts.map +1 -0
  72. package/lib/live-qualification-client.js +22 -0
  73. package/lib/live-qualification-client.js.map +1 -0
  74. package/lib/live-qualification-runtime.d.ts +78 -0
  75. package/lib/live-qualification-runtime.d.ts.map +1 -0
  76. package/lib/live-qualification-runtime.js +367 -0
  77. package/lib/live-qualification-runtime.js.map +1 -0
  78. package/lib/live-qualification.d.ts +73 -0
  79. package/lib/live-qualification.d.ts.map +1 -0
  80. package/lib/live-qualification.js +164 -0
  81. package/lib/live-qualification.js.map +1 -0
  82. package/lib/native-source-build-worker.d.ts +4 -0
  83. package/lib/native-source-build-worker.d.ts.map +1 -0
  84. package/lib/native-source-build-worker.js +87 -0
  85. package/lib/native-source-build-worker.js.map +1 -0
  86. package/lib/native-source-build.d.ts +36 -0
  87. package/lib/native-source-build.d.ts.map +1 -0
  88. package/lib/native-source-build.js +143 -0
  89. package/lib/native-source-build.js.map +1 -0
  90. package/lib/native-source-pack.d.ts +11 -0
  91. package/lib/native-source-pack.d.ts.map +1 -0
  92. package/lib/native-source-pack.js +83 -0
  93. package/lib/native-source-pack.js.map +1 -0
  94. package/lib/owner-task-gaps.d.ts +2 -0
  95. package/lib/owner-task-gaps.d.ts.map +1 -1
  96. package/lib/owner-task-gaps.js +9 -1
  97. package/lib/owner-task-gaps.js.map +1 -1
  98. package/lib/runtime-epoch.d.ts +69 -0
  99. package/lib/runtime-epoch.d.ts.map +1 -0
  100. package/lib/runtime-epoch.js +139 -0
  101. package/lib/runtime-epoch.js.map +1 -0
  102. package/lib/runtime-observer-protocol.d.ts +3 -3
  103. package/lib/runtime-observer-protocol.js +5 -5
  104. package/lib/runtime-observer-protocol.js.map +1 -1
  105. package/lib/runtime-observer.d.ts.map +1 -1
  106. package/lib/runtime-observer.js +42 -6
  107. package/lib/runtime-observer.js.map +1 -1
  108. package/lib/service.d.ts +248 -4
  109. package/lib/service.d.ts.map +1 -1
  110. package/lib/service.js +1431 -64
  111. package/lib/service.js.map +1 -1
  112. package/lib/source-adoption-authority.d.ts +3 -0
  113. package/lib/source-adoption-authority.d.ts.map +1 -1
  114. package/lib/source-adoption-authority.js +13 -3
  115. package/lib/source-adoption-authority.js.map +1 -1
  116. package/lib/source-adoption-runner.d.ts +3 -0
  117. package/lib/source-adoption-runner.d.ts.map +1 -1
  118. package/lib/source-adoption-runner.js +16 -4
  119. package/lib/source-adoption-runner.js.map +1 -1
  120. package/lib/source-approval-authority.d.ts.map +1 -1
  121. package/lib/source-approval-authority.js +1 -0
  122. package/lib/source-approval-authority.js.map +1 -1
  123. package/lib/source-baseline.d.ts +37 -0
  124. package/lib/source-baseline.d.ts.map +1 -0
  125. package/lib/source-baseline.js +250 -0
  126. package/lib/source-baseline.js.map +1 -0
  127. package/lib/source-build.d.ts +43 -9
  128. package/lib/source-build.d.ts.map +1 -1
  129. package/lib/source-build.js +670 -57
  130. package/lib/source-build.js.map +1 -1
  131. package/lib/source-context.d.ts +18 -0
  132. package/lib/source-context.d.ts.map +1 -1
  133. package/lib/source-context.js +85 -11
  134. package/lib/source-context.js.map +1 -1
  135. package/lib/source-creation.d.ts +106 -0
  136. package/lib/source-creation.d.ts.map +1 -0
  137. package/lib/source-creation.js +664 -0
  138. package/lib/source-creation.js.map +1 -0
  139. package/lib/source-job-types.d.ts +28 -1
  140. package/lib/source-job-types.d.ts.map +1 -1
  141. package/lib/source-jobs.d.ts +33 -3
  142. package/lib/source-jobs.d.ts.map +1 -1
  143. package/lib/source-jobs.js +420 -43
  144. package/lib/source-jobs.js.map +1 -1
  145. package/lib/source-maintenance.d.ts +66 -0
  146. package/lib/source-maintenance.d.ts.map +1 -0
  147. package/lib/source-maintenance.js +177 -0
  148. package/lib/source-maintenance.js.map +1 -0
  149. package/lib/source-revision.d.ts +57 -0
  150. package/lib/source-revision.d.ts.map +1 -0
  151. package/lib/source-revision.js +175 -0
  152. package/lib/source-revision.js.map +1 -0
  153. package/lib/source-workspace.d.ts +19 -14
  154. package/lib/source-workspace.d.ts.map +1 -1
  155. package/lib/source-workspace.js +90 -17
  156. package/lib/source-workspace.js.map +1 -1
  157. package/lib/sqlite.d.ts +13 -3
  158. package/lib/sqlite.d.ts.map +1 -1
  159. package/lib/sqlite.js +362 -1
  160. package/lib/sqlite.js.map +1 -1
  161. package/lib/store.d.ts +258 -3
  162. package/lib/store.d.ts.map +1 -1
  163. package/lib/store.js +2257 -96
  164. package/lib/store.js.map +1 -1
  165. package/lib/systemd-host-authority.d.ts +68 -0
  166. package/lib/systemd-host-authority.d.ts.map +1 -0
  167. package/lib/systemd-host-authority.js +424 -0
  168. package/lib/systemd-host-authority.js.map +1 -0
  169. package/lib/task-observation-evidence.d.ts +32 -0
  170. package/lib/task-observation-evidence.d.ts.map +1 -0
  171. package/lib/task-observation-evidence.js +62 -0
  172. package/lib/task-observation-evidence.js.map +1 -0
  173. package/lib/task-observation-runtime.d.ts +2 -3
  174. package/lib/task-observation-runtime.d.ts.map +1 -1
  175. package/lib/task-observation-runtime.js +7 -39
  176. package/lib/task-observation-runtime.js.map +1 -1
  177. package/lib/task-observation-store.js +1 -1
  178. package/lib/trust.d.ts.map +1 -1
  179. package/lib/trust.js +0 -0
  180. package/lib/trust.js.map +1 -1
  181. package/lib/types.d.ts +57 -7
  182. package/lib/types.d.ts.map +1 -1
  183. package/lib/version.d.ts +1 -1
  184. package/lib/version.js +1 -1
  185. package/package.json +22 -14
@@ -1,11 +1,16 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { isAbsolute, join, resolve } from 'node:path';
3
3
  import { realpath } from 'node:fs/promises';
4
+ import { SourceGrowthRunUnavailableError, isSourceOwnerContinuation, validateSourceGrowthRunBinding } from '@dsh-enhanced/assistant-growth-contract';
5
+ import { automationDefinitionDigest } from '@dsh-enhanced/assistant-automations';
4
6
  import { controlPlaneDigest, expectedSourceRelease } from './store.js';
5
7
  import { awaitSourceSignal, inspectSourceContext } from './source-context.js';
6
8
  import { assertPluginModificationAllowed, removeSourceJobWorktree, validateScopedPluginFiles } from './source-workspace.js';
7
9
  import { assertManagedVersionPaths } from './source-versioning.js';
10
+ import { resolveSourceBaseline, validateSourceBaselineConfig } from './source-baseline.js';
8
11
  import { removeSourceJobContainer } from './source-build.js';
12
+ import { inspectSourceRevisionContext, validateSourceRevisionGrant } from './source-revision.js';
13
+ import { inspectSourceCreationContext, validateSourceCreationFiles, validateSourceCreationGrant } from './source-creation.js';
9
14
  import { inheritedEnvironment } from './trust.js';
10
15
  export const SOURCE_JOB_OWNER = 'plugin-control-plane-source';
11
16
  const EXECUTOR = 'plugin-control-plane-source-check';
@@ -13,8 +18,11 @@ const CATALOG = controlPlaneDigest({ executor: EXECUTOR, version: 1, operation:
13
18
  const CONTINUATION_EXECUTOR = 'plugin-control-plane-source-continuations-v1';
14
19
  const CONTINUATION_CATALOG = controlPlaneDigest({ executor: CONTINUATION_EXECUTOR, version: 1, operation: 'continue-prepared-source-plan' });
15
20
  const CONTINUATION_AUTOMATION = 'source-job-prepared-continuations';
21
+ export { SourceGrowthRunUnavailableError } from '@dsh-enhanced/assistant-growth-contract';
22
+ class SourceGrowthNativeMismatchError extends Error {
23
+ }
16
24
  export function validateSourceJobsConfig(value, build) {
17
- const allowed = new Set(['authorityId', 'expiresAt', 'maxSubmissions', 'repository', 'ownerRouteId', 'principalId', 'workspace', 'preset', 'budgetId', 'budgetAmount']);
25
+ const allowed = new Set(['authorityId', 'expiresAt', 'maxSubmissions', 'allowedModes', 'repository', 'baseline', 'creation', 'revision', 'ownerRouteId', 'principalId', 'workspace', 'preset', 'budgetId', 'budgetAmount']);
18
26
  if (value === null || typeof value !== 'object' || Array.isArray(value) || build === undefined
19
27
  || Object.keys(value).some(key => !allowed.has(key))
20
28
  || ![value.authorityId, value.ownerRouteId, value.principalId, value.repository, value.workspace, value.preset].every(text => typeof text === 'string' && text !== '' && text.normalize('NFC').trim() === text && text.length <= 4096 && !/[\p{Cc}]/u.test(text))
@@ -26,9 +34,26 @@ export function validateSourceJobsConfig(value, build) {
26
34
  || !Number.isSafeInteger(value.budgetAmount) || value.budgetAmount < 1) {
27
35
  throw new Error('plugin-control-plane: invalid sourceJobs authority configuration');
28
36
  }
37
+ if (value.baseline !== undefined)
38
+ validateSourceBaselineConfig(value.baseline);
39
+ if (value.allowedModes !== undefined && (!Array.isArray(value.allowedModes) || value.allowedModes.length < 1
40
+ || value.allowedModes.length > 3 || new Set(value.allowedModes).size !== value.allowedModes.length
41
+ || value.allowedModes.some(mode => !['create', 'modify', 'revise-created'].includes(mode)))) {
42
+ throw new Error('plugin-control-plane: invalid sourceJobs allowedModes');
43
+ }
44
+ if (value.creation !== undefined)
45
+ validateSourceCreationGrant(value.creation);
46
+ if (value.revision !== undefined)
47
+ validateSourceRevisionGrant(value.revision);
48
+ if (build !== undefined && 'backend' in build
49
+ && (!value.allowedModes || value.allowedModes.some(mode => mode === 'modify')
50
+ || value.creation === undefined && value.revision === undefined)) {
51
+ throw new Error('plugin-control-plane: native source jobs require finite create/revise-created grants');
52
+ }
29
53
  }
30
54
  function projection(job) {
31
55
  return { id: job.id, name: job.intent.name, gapId: job.intent.gapId, baseCommit: job.intent.baseCommit,
56
+ ...(job.intent.mode === undefined ? {} : { mode: job.intent.mode }),
32
57
  status: job.status, createdAt: job.createdAt, expiresAt: job.expiresAt,
33
58
  ...(job.planId === undefined ? {} : { planId: job.planId }), ...(job.failureCode === undefined ? {} : { failureCode: job.failureCode }) };
34
59
  }
@@ -37,6 +62,8 @@ export class SourceJobRuntime {
37
62
  options;
38
63
  abort = new AbortController();
39
64
  flights = new Set();
65
+ growthFlights = new Set();
66
+ growthControllers = new Set();
40
67
  authorityDigest;
41
68
  unregister;
42
69
  unregisterContinuation;
@@ -59,7 +86,15 @@ export class SourceJobRuntime {
59
86
  const executor = {
60
87
  descriptor: { executorId: EXECUTOR, contractVersion: 1, catalogDigest: CATALOG },
61
88
  accepts: spec => spec.executorId === EXECUTOR && spec.executorContractVersion === 1 && spec.catalogDigest === CATALOG && spec.runbookId === EXECUTOR && spec.runbookVersion === 1,
62
- execute: input => this.track(this.execute(input)),
89
+ execute: input => {
90
+ const job = this.options.store.getSourceJobByAutomation(input.automationId);
91
+ const flight = this.track(this.execute(input));
92
+ if ((job?.intent.creation?.growthRun ?? job?.intent.revision?.growthRun) !== undefined) {
93
+ this.growthFlights.add(flight);
94
+ void flight.then(() => this.growthFlights.delete(flight), () => this.growthFlights.delete(flight));
95
+ }
96
+ return flight;
97
+ },
63
98
  };
64
99
  try {
65
100
  this.unregister = this.options.ports.automations.registerHostExecutor(executor);
@@ -90,23 +125,57 @@ export class SourceJobRuntime {
90
125
  catch { /* original error wins */ }
91
126
  throw error;
92
127
  }
128
+ this.reconcileQueued();
129
+ }
130
+ /** Nudge the existing native one-shot registrations after a Host provider appears. */
131
+ reconcileQueued() {
132
+ if (!this.active)
133
+ return;
93
134
  // Reconcile only queued intents. A previous claim is unknown and never replayed.
94
135
  for (let job of this.options.store.listSourceJobs(100)) {
95
136
  if (job.status !== 'queued')
96
137
  continue;
97
138
  try {
98
139
  job = this.refreshQueued(job);
99
- if (job.status === 'queued') {
100
- this.assertOwner(job);
140
+ if (job.status === 'queued')
101
141
  this.schedule(job);
102
- }
103
142
  }
104
- catch {
143
+ catch (error) {
144
+ if (error instanceof SourceGrowthRunUnavailableError)
145
+ continue;
105
146
  const current = this.options.store.getSourceJob(job.id);
147
+ // A persisted rearm is an in-progress cross-ledger handoff. Retain it
148
+ // for exact native receipt replay after a crash or transient fault.
149
+ if (current?.previousDefinitionHash !== undefined && !(error instanceof SourceGrowthNativeMismatchError))
150
+ continue;
106
151
  if (current?.status === 'queued')
107
152
  this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: 'failed', failureCode: 'source-job-reconcile-rejected' });
108
153
  }
109
154
  }
155
+ this.reconcileContinuations();
156
+ }
157
+ /** Provider removal revokes in-flight authority without replaying a claimed job. */
158
+ growthProviderDisposed() {
159
+ for (const controller of this.growthControllers)
160
+ controller.abort(new Error('source growth run producer disposed'));
161
+ this.options.store.interruptGrowthSourceJobs();
162
+ if (!this.active)
163
+ return Promise.allSettled(this.growthFlights).then(() => undefined);
164
+ for (const job of this.options.store.listSourceJobs(100)) {
165
+ if (job.status !== 'queued' || (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined || job.definitionHash === undefined)
166
+ continue;
167
+ try {
168
+ const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
169
+ if (health.definitionHash !== job.definitionHash)
170
+ continue;
171
+ if (health.automationStatus === 'active')
172
+ this.options.ports.automations.pauseSystemOwned({ owner: SOURCE_JOB_OWNER,
173
+ operationId: `${job.id}:growth-provider-pause:v${health.definitionVersion}`, automationId: job.automationId,
174
+ definitionHash: job.definitionHash, expectedVersion: health.definitionVersion });
175
+ }
176
+ catch { /* claim guard remains authoritative */ }
177
+ }
178
+ return Promise.allSettled(this.growthFlights).then(() => undefined);
110
179
  }
111
180
  async continuePrepared(job, signal) {
112
181
  if (!job.planId || !this.options.store.getOwnerTaskFailureReference(job.intent.gapId))
@@ -122,6 +191,7 @@ export class SourceJobRuntime {
122
191
  }
123
192
  }
124
193
  async continuePreparedLocked(job, signal) {
194
+ this.assertMode(job.intent.mode);
125
195
  if (!job.planId || !this.options.store.getOwnerTaskFailureReference(job.intent.gapId))
126
196
  return;
127
197
  const planId = job.planId;
@@ -130,6 +200,8 @@ export class SourceJobRuntime {
130
200
  throw new Error('source job continuation changed');
131
201
  job = current;
132
202
  const initial = this.options.store.getSourcePlan(planId);
203
+ if (initial.mode !== 'modify' && initial.mode !== 'prepared-create' && initial.mode !== 'prepared-revise')
204
+ return;
133
205
  // An ordinary grant or plan must not be advanced after expiry. Adoption has
134
206
  // its own durable post-release path and is deliberately retained below.
135
207
  if (initial.expiresAt <= Date.now() && initial.status !== 'release-complete')
@@ -142,6 +214,35 @@ export class SourceJobRuntime {
142
214
  signal.throwIfAborted();
143
215
  this.assertOwner(job);
144
216
  };
217
+ if (initial.mode === 'prepared-revise') {
218
+ if (job.intent.mode !== 'revise-created' || !job.intent.revision?.growthRun.revisionAcceptance || !initial.sourceRevision?.growthRun.revisionAcceptance)
219
+ return;
220
+ await assertCurrent();
221
+ if (this.options.store.getRevisionVerificationStatus(planId) === undefined && this.options.verifyPreparedRevision)
222
+ await this.options.verifyPreparedRevision(job, signal);
223
+ if (this.options.store.getRevisionVerificationStatus(planId) === 'verified'
224
+ && this.options.store.getRevisionRegressionStatus(planId) === undefined
225
+ && job.intent.revision.growthRun.revisionRegressionAcceptance && this.options.verifyPreparedRevisionRegression) {
226
+ await assertCurrent();
227
+ await this.options.verifyPreparedRevisionRegression(job, signal);
228
+ }
229
+ return;
230
+ }
231
+ if (initial.mode === 'prepared-create') {
232
+ if (job.intent.mode !== 'create'
233
+ || !job.intent.creation?.growthRun?.creationAcceptance || !initial.creation?.growthRun?.creationAcceptance)
234
+ return;
235
+ await assertCurrent();
236
+ if (this.options.store.getCreationVerificationStatus(planId) === undefined && this.options.verifyPreparedCreation) {
237
+ await this.options.verifyPreparedCreation(job, signal);
238
+ }
239
+ if (this.options.store.getCreationVerificationStatus(planId) === 'verified'
240
+ && this.options.creationAdoptionEligible?.(planId) && this.options.adoptVerifiedCreation) {
241
+ await assertCurrent();
242
+ await this.options.adoptVerifiedCreation(job, signal);
243
+ }
244
+ return;
245
+ }
145
246
  if (this.options.adoptReleased && this.options.store.getSourcePlan(planId).status === 'release-complete') {
146
247
  await this.options.adoptReleased(job, signal, assertCurrent);
147
248
  return;
@@ -219,34 +320,100 @@ export class SourceJobRuntime {
219
320
  throw new Error('source job task provenance service unavailable');
220
321
  return this.options.withGapSourceFence(gapId, owner, callback);
221
322
  }
323
+ modeAllowed(mode) {
324
+ return this.options.config.allowedModes?.includes(mode ?? 'modify') ?? true;
325
+ }
326
+ assertMode(mode) {
327
+ if (!this.modeAllowed(mode))
328
+ throw new Error('source job mode is not authorized');
329
+ }
222
330
  assertOwner(job) {
223
- if (!this.available() || job.expiresAt <= Date.now() || job.intent.authority.digest !== this.authorityDigest
224
- || job.intent.authority.id !== this.options.config.authorityId || controlPlaneDigest(this.receipt()) !== job.intent.ownerDigest)
331
+ this.assertMode(job.intent.mode);
332
+ const current = this.receipt();
333
+ const taskBacked = this.options.store.getOwnerTaskFailureReference(job.intent.gapId) !== undefined;
334
+ if (!this.available() || job.expiresAt <= Date.now() || (job.intent.mode === 'create' && (!this.options.config.creation || !job.intent.creation
335
+ || job.intent.creation.grant.expiresAt <= Date.now() || controlPlaneDigest(job.intent.creation.grant) !== controlPlaneDigest(this.options.config.creation)))
336
+ || (job.intent.mode === 'revise-created' && (!this.options.config.revision || !job.intent.revision
337
+ || job.intent.revision.grant.expiresAt <= Date.now() || controlPlaneDigest(job.intent.revision.grant) !== controlPlaneDigest(this.options.config.revision)))
338
+ || job.intent.authority.digest !== this.authorityDigest
339
+ || job.intent.authority.id !== this.options.config.authorityId
340
+ || controlPlaneDigest(job.intent.owner) !== job.intent.ownerDigest
341
+ || (taskBacked ? !isSourceOwnerContinuation(current, job.intent.owner)
342
+ : controlPlaneDigest(current) !== job.intent.ownerDigest))
225
343
  throw new Error('source job authority changed or expired');
226
- this.withGapSource(job.intent.gapId, job.intent.owner, () => { });
344
+ if (job.intent.revision) {
345
+ const parent = this.options.inspectRevisionParent?.(job.intent.revision.parent.planId);
346
+ if (!parent || parent.name !== job.intent.name || controlPlaneDigest(parent.parent) !== controlPlaneDigest(job.intent.revision.parent))
347
+ throw new Error('source revision parent changed');
348
+ }
349
+ this.withGapSource(job.intent.gapId, job.intent.owner, () => this.assertGrowthRun((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun), job.intent.mode, job.intent.gapId, job.intent.owner, false));
350
+ }
351
+ assertGrowthRun(growthRun, mode, gapId, owner, generation) {
352
+ const taskBacked = this.options.store.getOwnerTaskFailureReference(gapId) !== undefined;
353
+ if ((mode !== 'create' && mode !== 'revise-created') || !taskBacked) {
354
+ if (growthRun !== undefined)
355
+ throw new Error('source growth run is outside a task-backed creation');
356
+ return;
357
+ }
358
+ if (growthRun === undefined)
359
+ throw new Error('task-backed creation requires a frozen source growth run');
360
+ validateSourceGrowthRunBinding(growthRun);
361
+ if (!this.options.assertGrowthRun)
362
+ throw new SourceGrowthRunUnavailableError('source growth run producer unavailable');
363
+ this.options.assertGrowthRun(growthRun, gapId, owner, generation);
227
364
  }
228
365
  enqueue(input) {
229
- validateScopedPluginFiles(input.files);
230
- if (this.options.build.versioning === 'patch')
231
- assertManagedVersionPaths(input.files);
366
+ this.assertMode(input.mode);
367
+ if (input.mode !== 'create' && input.mode !== 'revise-created' && input.growthRun !== undefined)
368
+ throw new Error('modify source job cannot carry a growth run');
369
+ if (input.mode === 'create' || input.mode === 'revise-created')
370
+ validateSourceCreationFiles(input.files);
371
+ else {
372
+ if (input.mode !== undefined)
373
+ throw new Error('invalid source job mode');
374
+ validateScopedPluginFiles(input.files);
375
+ if (this.options.build.versioning === 'patch')
376
+ assertManagedVersionPaths(input.files);
377
+ }
378
+ if ((input.mode === 'revise-created') !== (typeof input.parentPlanId === 'string' && input.parentPlanId.length > 0))
379
+ throw new Error('source revision parent is missing or outside revision mode');
232
380
  return this.track(this.enqueueOwned({ ...input, files: structuredClone(input.files), owner: structuredClone(input.owner) }));
233
381
  }
234
382
  async enqueueOwned(input) {
383
+ this.assertMode(input.mode);
235
384
  const signal = AbortSignal.any([input.signal, this.abort.signal, AbortSignal.timeout(15_000)]);
236
385
  const assertCurrent = async () => { signal.throwIfAborted(); await awaitSourceSignal(signal, input.assertCurrent); this.withGapSource(input.gapId, this.assertCaller(input.owner), () => { }); if (!this.available())
237
386
  throw new Error('source job authority unavailable'); signal.throwIfAborted(); };
238
387
  await assertCurrent();
239
- validateScopedPluginFiles(input.files);
240
- if (this.options.build.versioning === 'patch')
241
- assertManagedVersionPaths(input.files);
242
- assertPluginModificationAllowed(input.name);
388
+ if (input.mode === 'create' || input.mode === 'revise-created')
389
+ validateSourceCreationFiles(input.files);
390
+ else {
391
+ validateScopedPluginFiles(input.files);
392
+ if (this.options.build.versioning === 'patch')
393
+ assertManagedVersionPaths(input.files);
394
+ assertPluginModificationAllowed(input.name);
395
+ }
243
396
  if (typeof input.idempotencyKey !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/u.test(input.idempotencyKey)
244
397
  || !Number.isSafeInteger(input.ttlMs) || input.ttlMs < 900_000 || input.ttlMs > 86_400_000)
245
398
  throw new Error('invalid source job request');
246
399
  const config = this.options.config;
400
+ if (input.mode === 'create' && (config.creation === undefined || config.creation.expiresAt <= Date.now()))
401
+ throw new Error('source creation grant unavailable or expired');
402
+ if (input.mode === 'revise-created' && (config.revision === undefined || config.revision.expiresAt <= Date.now()))
403
+ throw new Error('source revision grant unavailable or expired');
247
404
  if (input.repository !== config.repository || await realpath(input.repository) !== config.repository)
248
405
  throw new Error('source job repository mismatch');
249
- const owner = this.assertCaller(input.owner);
406
+ const currentOwner = this.assertCaller(input.owner);
407
+ const reference = this.options.store.getOwnerTaskFailureReference(input.gapId);
408
+ if (input.mode === 'revise-created' && !reference)
409
+ throw new Error('revision requires an authenticated owner task failure');
410
+ const owner = reference?.owner ?? currentOwner;
411
+ if (reference && !isSourceOwnerContinuation(currentOwner, owner))
412
+ throw new Error('source job task owner changed');
413
+ this.withGapSource(input.gapId, currentOwner, () => this.assertGrowthRun(input.growthRun, input.mode, input.gapId, owner, true));
414
+ const parent = input.mode === 'revise-created' ? this.options.inspectRevisionParent?.(input.parentPlanId) : undefined;
415
+ if (input.mode === 'revise-created' && (!parent || parent.name !== input.name))
416
+ throw new Error('adopted revision parent unavailable or mismatched');
250
417
  const trust = await awaitSourceSignal(signal, this.options.trust);
251
418
  for (const outstanding of this.options.store.listSourceJobs(1))
252
419
  this.refreshQueued(outstanding);
@@ -256,6 +423,12 @@ export class SourceJobRuntime {
256
423
  if (prior !== undefined) {
257
424
  const frozen = prior.intent;
258
425
  if (frozen.authority.digest !== this.authorityDigest || frozen.ownerDigest !== controlPlaneDigest(owner) || frozen.trustDigest !== controlPlaneDigest(trust)
426
+ || frozen.mode !== input.mode || (input.mode === 'create' && (controlPlaneDigest(frozen.creation?.grant) !== controlPlaneDigest(config.creation)
427
+ || (frozen.creation?.growthRun === undefined) !== (input.growthRun === undefined)
428
+ || (input.growthRun !== undefined && controlPlaneDigest(frozen.creation?.growthRun) !== controlPlaneDigest(input.growthRun))))
429
+ || (input.mode === 'revise-created' && (controlPlaneDigest(frozen.revision?.grant) !== controlPlaneDigest(config.revision)
430
+ || controlPlaneDigest(frozen.revision?.parent) !== controlPlaneDigest(parent.parent)
431
+ || controlPlaneDigest(frozen.revision?.growthRun) !== controlPlaneDigest(input.growthRun)))
259
432
  || frozen.repository !== input.repository || frozen.name !== input.name || frozen.gapId !== input.gapId || frozen.baseCommit !== input.expectedBaseCommit
260
433
  || frozen.ttlMs !== input.ttlMs || controlPlaneDigest(frozen.files) !== controlPlaneDigest(input.files))
261
434
  throw new Error('source job idempotency conflict');
@@ -267,17 +440,39 @@ export class SourceJobRuntime {
267
440
  const gap = this.options.store.getGap(input.gapId);
268
441
  if (gap.status !== 'open' || gap.candidateId !== undefined)
269
442
  throw new Error('source job gap is not open');
270
- const source = await inspectSourceContext({ repository: config.repository, name: input.name, paths: [], baseCommit: input.expectedBaseCommit, environment: inheritedEnvironment(trust), signal, assertCurrent });
443
+ const environment = inheritedEnvironment(trust);
444
+ const baselineCommit = config.baseline === undefined ? undefined : await resolveSourceBaseline({ repository: config.repository,
445
+ config: config.baseline, environment, signal, assertCurrent, trust,
446
+ readHistory: () => this.options.store.getSourceBaselineHistory(config.repository),
447
+ readMaintenance: () => this.options.store.getSourceMaintenanceRecords(config.repository) });
448
+ const context = { repository: config.repository, name: input.name, paths: [], baseCommit: input.expectedBaseCommit,
449
+ ...(baselineCommit === undefined ? {} : { baselineCommit }), environment, signal, assertCurrent };
450
+ const source = input.mode === 'create'
451
+ ? await inspectSourceCreationContext({ ...context, grant: config.creation })
452
+ : input.mode === 'revise-created'
453
+ ? await inspectSourceRevisionContext({ ...context, grant: config.revision, parent: parent.parent, parentSource: parent.source, parentCertificate: parent.certificate })
454
+ : await inspectSourceContext(context);
271
455
  const intent = {
272
456
  authority: { id: config.authorityId, digest: this.authorityDigest, expiresAt: config.expiresAt, maxSubmissions: config.maxSubmissions },
273
457
  owner, ownerDigest: controlPlaneDigest(owner), trustDigest: controlPlaneDigest(trust), repository: config.repository, name: input.name,
458
+ ...(config.baseline === undefined ? {} : { baseline: structuredClone(config.baseline) }),
459
+ ...(input.mode === 'create' ? { mode: 'create', creation: { grant: structuredClone(config.creation), generatorDigest: source.generatorDigest,
460
+ ...(input.growthRun === undefined ? {} : { growthRun: structuredClone(input.growthRun) }) } } : {}),
461
+ ...(input.mode === 'revise-created' ? { mode: 'revise-created', revision: { grant: structuredClone(config.revision),
462
+ generatorDigest: source.generatorDigest, parent: structuredClone(parent.parent), growthRun: structuredClone(input.growthRun) } } : {}),
274
463
  gapId: gap.id, gapRevision: gap.revision, gapDigest: controlPlaneDigest(gap), baseCommit: source.baseCommit,
275
464
  files: structuredClone(input.files), ttlMs: input.ttlMs, build: structuredClone(this.options.build),
276
- worktree: join(this.options.statePath, 'source-worktrees', `worktree-job-${token}`), containerName: `dsh-source-job-${token}`,
465
+ worktree: join(this.options.statePath, 'source-worktrees', `worktree-job-${token}`),
466
+ ...('backend' in this.options.build
467
+ ? { nativeResource: { backend: 'macos-seatbelt', key: id } }
468
+ : { containerName: `dsh-source-job-${token}` }),
277
469
  };
278
470
  await assertCurrent();
279
471
  // Synchronous acceptance and registration: no Agent signal is persisted.
280
- const job = this.withGapSource(gap.id, owner, () => this.options.store.enqueueSourceJob({ id, automationId: id, idempotencyKey: input.idempotencyKey, intent }));
472
+ const job = this.withGapSource(gap.id, owner, () => {
473
+ this.assertGrowthRun(input.growthRun, input.mode, gap.id, owner, true);
474
+ return this.options.store.enqueueSourceJob({ id, automationId: id, idempotencyKey: input.idempotencyKey, intent });
475
+ });
281
476
  if (job.status === 'queued')
282
477
  this.scheduleOrFail(job);
283
478
  return projection(this.options.store.getSourceJob(id));
@@ -285,8 +480,8 @@ export class SourceJobRuntime {
285
480
  inspect(input) {
286
481
  const owner = this.assertCaller(input.owner);
287
482
  const job = this.options.store.getSourceJob(input.id);
288
- // A new session binding may inspect/clean this owner's old job. Execution
289
- // still requires the complete frozen receipt in assertOwner; never replay.
483
+ // Inspection and cleanup may cross a session binding. A task-backed job
484
+ // retains its frozen owner digest and may continue under the same owner.
290
485
  if (job === undefined || job.intent.owner.authorityId !== owner.authorityId
291
486
  || job.intent.owner.principalId !== owner.principalId || job.intent.owner.principalRecordId !== owner.principalRecordId
292
487
  || job.intent.owner.principalVersion !== owner.principalVersion || job.intent.owner.workspace !== owner.workspace
@@ -298,16 +493,25 @@ export class SourceJobRuntime {
298
493
  refreshQueued(job) {
299
494
  if (job.status !== 'queued')
300
495
  return job;
496
+ if (!this.modeAllowed(job.intent.mode))
497
+ return this.options.store.settleSourceJob({ id: job.id, revision: job.revision,
498
+ status: 'failed', failureCode: 'source-job-mode-revoked' });
301
499
  let code;
302
- try {
303
- this.withGapSource(job.intent.gapId, job.intent.owner, () => { });
304
- }
305
- catch {
306
- code = 'source-job-task-source-changed';
500
+ let providerUnavailable = false;
501
+ if (job.intent.mode === 'create' && this.options.store.getOwnerTaskFailureReference(job.intent.gapId) !== undefined
502
+ && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined)
503
+ code = 'source-job-growth-run-missing';
504
+ else {
505
+ try {
506
+ this.withGapSource(job.intent.gapId, job.intent.owner, () => { providerUnavailable = this.growthProducerUnavailable(job); });
507
+ }
508
+ catch {
509
+ code = 'source-job-task-source-changed';
510
+ }
307
511
  }
308
512
  if (job.expiresAt <= Date.now())
309
513
  code = 'source-job-expired';
310
- else if (job.definitionHash !== undefined) {
514
+ else if (code === undefined && job.definitionHash !== undefined && !providerUnavailable && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined) {
311
515
  const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
312
516
  const run = health.latestTerminalRuns.production;
313
517
  if (run !== undefined && run.createdAt >= job.createdAt && run.immutableContext.state === 'verified'
@@ -319,9 +523,20 @@ export class SourceJobRuntime {
319
523
  // loses its claim. No resubmission or second native occurrence is created.
320
524
  return code === undefined ? job : this.options.store.settleSourceJob({ id: job.id, revision: job.revision, status: 'failed', failureCode: code });
321
525
  }
526
+ growthProducerUnavailable(job) {
527
+ try {
528
+ this.assertGrowthRun((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun), job.intent.mode, job.intent.gapId, job.intent.owner, false);
529
+ return false;
530
+ }
531
+ catch (error) {
532
+ if (error instanceof SourceGrowthRunUnavailableError)
533
+ return true;
534
+ throw error;
535
+ }
536
+ }
322
537
  definition(job) {
323
538
  const config = this.options.config;
324
- return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.createdAt + 1000).toISOString() },
539
+ return { name: `Source check: ${job.intent.name}`, schedule: { kind: 'at', at: new Date(job.dispatchAt ?? job.createdAt + 1000).toISOString() },
325
540
  workspace: config.workspace, agentPreset: config.preset, timeoutMs: job.intent.build.timeoutMs + 120_000 + (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0),
326
541
  misfire: { kind: 'latest' }, overlap: 'skip', retrySafety: 'never', maxRetries: 0, principal: config.principalId,
327
542
  ...(config.budgetId === undefined ? {} : { budgetId: config.budgetId, budgetAmount: config.budgetAmount }),
@@ -332,27 +547,87 @@ export class SourceJobRuntime {
332
547
  schedule(job) {
333
548
  this.assertOwner(job);
334
549
  const automations = this.options.ports.automations;
550
+ if (job.definitionHash !== undefined && (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) !== undefined && job.occurrenceId === undefined) {
551
+ const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
552
+ if (health.definitionHash !== job.definitionHash)
553
+ throw new Error('source job native definition changed before rearm');
554
+ const dueAt = job.dispatchAt ?? job.createdAt + 1000;
555
+ const terminal = health.latestTerminalRuns.production;
556
+ if ((health.automationStatus === 'paused' && dueAt <= Date.now())
557
+ || (terminal !== undefined && terminal.immutableContext.state === 'verified'
558
+ && terminal.immutableContext.definitionHash === job.definitionHash && terminal.createdAt >= job.createdAt)) {
559
+ job = this.withGapSource(job.intent.gapId, job.intent.owner, () => {
560
+ this.assertGrowthRun(job.intent.creation?.growthRun ?? job.intent.revision?.growthRun, job.intent.mode, job.intent.gapId, job.intent.owner, false);
561
+ return this.options.store.rearmQueuedGrowthSourceJob({ id: job.id, revision: job.revision,
562
+ priorDefinitionHash: job.definitionHash, priorDefinitionVersion: health.definitionVersion, dispatchAt: Date.now() + 1000 });
563
+ });
564
+ }
565
+ }
335
566
  const definition = this.definition(job);
567
+ const expectedHash = automationDefinitionDigest(definition);
568
+ if (job.previousDefinitionHash !== undefined) {
569
+ if (typeof automations.reconcileSystemExact !== 'function')
570
+ throw new SourceGrowthRunUnavailableError('exact native source rearm unavailable');
571
+ const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
572
+ const activation = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
573
+ if (activation?.activationNonce !== job.intentDigest || activation.ownerRouteId !== this.options.config.ownerRouteId
574
+ || !((health.definitionHash === job.previousDefinitionHash && health.definitionVersion === job.previousDefinitionVersion)
575
+ || (health.definitionHash === expectedHash && health.definitionVersion === job.previousDefinitionVersion + 1))) {
576
+ throw new SourceGrowthNativeMismatchError('source job native rearm lineage changed');
577
+ }
578
+ automations.reconcileSystemExact({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
579
+ idempotencyKey: `${job.id}:rearm:${job.dispatchAt}`, desiredStatus: 'active', definition,
580
+ expectedDefinitionHash: job.previousDefinitionHash, expectedVersion: job.previousDefinitionVersion });
581
+ const activated = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
582
+ if (activated.definitionHash !== expectedHash || activated.definitionVersion !== job.previousDefinitionVersion + 1
583
+ || activated.automationStatus !== 'active')
584
+ throw new SourceGrowthNativeMismatchError('source job exact native rearm was not active');
585
+ this.assertOwner(job);
586
+ this.options.store.bindSourceJobDefinition({ id: job.id, revision: job.revision, definitionHash: expectedHash });
587
+ return;
588
+ }
336
589
  if (job.definitionHash === undefined) {
337
- automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId, idempotencyKey: `${job.id}:prepare`, desiredStatus: 'paused', definition });
590
+ const prior = job.previousDefinitionHash;
591
+ const existing = (() => {
592
+ try {
593
+ return automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
594
+ }
595
+ catch {
596
+ return undefined;
597
+ }
598
+ })();
599
+ if (existing !== undefined && (existing.activationNonce !== job.intentDigest || existing.ownerRouteId !== this.options.config.ownerRouteId
600
+ || (existing.definitionHash !== expectedHash && existing.definitionHash !== prior))) {
601
+ throw new Error('source job native definition differs from persisted rearm');
602
+ }
603
+ automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
604
+ idempotencyKey: `${job.id}:prepare:${job.dispatchAt ?? job.createdAt + 1000}`, desiredStatus: 'paused', definition });
338
605
  const registered = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
339
- if (registered === undefined || registered.activationNonce !== job.intentDigest || registered.ownerRouteId !== this.options.config.ownerRouteId)
606
+ if (registered === undefined || registered.activationNonce !== job.intentDigest || registered.ownerRouteId !== this.options.config.ownerRouteId
607
+ || registered.definitionHash !== expectedHash)
340
608
  throw new Error('source job registration binding mismatch');
341
609
  job = this.options.store.bindSourceJobDefinition({ id: job.id, revision: job.revision, definitionHash: registered.definitionHash });
342
610
  }
343
611
  const current = automations.inspectSystemOwnedActivation({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
344
612
  if (current === undefined || current.definitionHash !== job.definitionHash || current.activationNonce !== job.intentDigest || current.ownerRouteId !== this.options.config.ownerRouteId)
345
613
  throw new Error('source job automation changed');
346
- automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId, idempotencyKey: `${job.id}:activate`, desiredStatus: 'active', definition });
614
+ const health = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
615
+ if (health.definitionHash !== job.definitionHash)
616
+ throw new Error('source job native definition changed');
617
+ automations.reconcileSystem({ owner: SOURCE_JOB_OWNER, automationId: job.automationId,
618
+ idempotencyKey: `${job.id}:activate:v${health.definitionVersion}`, desiredStatus: 'active', definition });
619
+ const activated = automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
620
+ if (activated.definitionHash !== job.definitionHash || activated.automationStatus !== 'active')
621
+ throw new Error('source job native activation did not become active');
347
622
  }
348
623
  hasContinuations() {
349
- return !!(this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased);
624
+ return !!(this.options.approvePrepared || this.options.verifyPreparedRevisionRegression || this.options.verifyPreparedRevision || this.options.verifyPreparedCreation || this.options.adoptVerifiedCreation || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased);
350
625
  }
351
626
  continuationJobs() {
352
627
  if (!this.hasContinuations())
353
628
  return [];
354
- return this.options.store.listPreparedSourceApprovalJobs(this.options.releasePrepared !== undefined, this.options.advanceReleased !== undefined, this.options.adoptReleased !== undefined)
355
- .filter(job => this.eligibleContinuation(job));
629
+ return this.options.store.listPreparedSourceApprovalJobs(this.options.releasePrepared !== undefined, this.options.advanceReleased !== undefined, this.options.adoptReleased !== undefined, this.options.verifyPreparedCreation !== undefined, this.options.adoptVerifiedCreation !== undefined, this.options.verifyPreparedRevision !== undefined, this.options.verifyPreparedRevisionRegression !== undefined)
630
+ .filter(job => this.modeAllowed(job.intent.mode) && this.eligibleContinuation(job));
356
631
  }
357
632
  /** Admission reads durable state and validates the current owner; it never invokes continuation approval or release hooks. */
358
633
  eligibleContinuation(job) {
@@ -365,8 +640,50 @@ export class SourceJobRuntime {
365
640
  catch {
366
641
  return false;
367
642
  }
643
+ if (plan.mode === 'prepared-revise') {
644
+ if (job.intent.mode !== 'revise-created' || plan.status !== 'pending-approval' || !plan.sourceRevision?.growthRun.revisionAcceptance)
645
+ return false;
646
+ const status = this.options.store.getRevisionVerificationStatus(plan.id);
647
+ const verify = status === undefined && this.options.verifyPreparedRevision !== undefined;
648
+ const regression = status === 'verified' && plan.sourceRevision.growthRun.revisionRegressionAcceptance !== undefined
649
+ && this.options.store.getRevisionRegressionStatus(plan.id) === undefined && this.options.verifyPreparedRevisionRegression !== undefined;
650
+ if (!verify && !regression)
651
+ return false;
652
+ try {
653
+ this.assertOwner(job);
654
+ }
655
+ catch {
656
+ return false;
657
+ }
658
+ return plan.expiresAt > Date.now();
659
+ }
660
+ if (plan.mode === 'prepared-create') {
661
+ if (job.intent.mode !== 'create' || plan.status !== 'pending-approval'
662
+ || !job.intent.creation?.growthRun?.creationAcceptance || !plan.creation?.growthRun?.creationAcceptance)
663
+ return false;
664
+ const verification = this.options.store.getCreationVerificationStatus(plan.id);
665
+ const verify = verification === undefined && this.options.verifyPreparedCreation !== undefined;
666
+ const adopt = verification === 'verified' && this.options.adoptVerifiedCreation !== undefined
667
+ && this.options.creationAdoptionEligible?.(plan.id) === true;
668
+ if (!verify && !adopt)
669
+ return false;
670
+ if (job.intent.authority.digest !== this.authorityDigest || job.intent.authority.id !== this.options.config.authorityId)
671
+ return false;
672
+ try {
673
+ this.assertOwner(job);
674
+ this.withGapSource(job.intent.gapId, job.intent.owner, () => {
675
+ this.assertGrowthRun(job.intent.creation?.growthRun, job.intent.mode, job.intent.gapId, job.intent.owner, false);
676
+ });
677
+ }
678
+ catch {
679
+ return false;
680
+ }
681
+ return plan.expiresAt > Date.now();
682
+ }
683
+ if (plan.mode !== 'modify' || job.intent.mode === 'create')
684
+ return false;
368
685
  const adoption = plan.status === 'release-complete' ? this.options.store.findSourceAdoption(plan.id) : undefined;
369
- const recovery = adoption !== undefined && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status));
686
+ const recovery = adoption !== undefined && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status));
370
687
  if (recovery)
371
688
  return this.options.adoptReleased !== undefined;
372
689
  if (job.intent.authority.digest !== this.authorityDigest || job.intent.authority.id !== this.options.config.authorityId)
@@ -390,12 +707,12 @@ export class SourceJobRuntime {
390
707
  continuationTimeoutMs() {
391
708
  const release = this.options.releaseTimeoutMs ?? 40_000, adoption = this.options.adoptionTimeoutMs ?? 60_000;
392
709
  const needed = (this.options.adoptReleased ? adoption : 0) + (this.options.advanceReleased ? release + 40_000 : this.options.releasePrepared ? 40_000 : 10_000);
393
- return Math.max(10_000, needed);
710
+ return Math.max(this.options.verifyPreparedCreation ? 1_800_000 : 10_000, needed);
394
711
  }
395
712
  continuationSignalTimeoutMs(job) {
396
713
  if (job.planId) {
397
714
  const plan = this.options.store.getSourcePlan(job.planId), adoption = plan.status === 'release-complete' ? this.options.store.findSourceAdoption(plan.id) : undefined;
398
- if (adoption && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status)))
715
+ if (adoption && ((adoption.status === 'approved' && adoption.dossier.handoff !== undefined) || ['staging', 'awaiting-reload', 'awaiting-readiness', 'awaiting-live-tasks', 'awaiting-effect-blocked-replay', 'awaiting-shadow', 'awaiting-canary', 'awaiting-soak', 'awaiting-health', 'commit-pending', 'rollback-pending'].includes(adoption.status)))
399
716
  return this.continuationTimeoutMs();
400
717
  }
401
718
  return Math.max(1, Math.min(this.continuationTimeoutMs(), this.options.config.expiresAt - Date.now()));
@@ -461,8 +778,12 @@ export class SourceJobRuntime {
461
778
  try {
462
779
  this.schedule(job);
463
780
  }
464
- catch {
781
+ catch (error) {
782
+ if (error instanceof SourceGrowthRunUnavailableError)
783
+ return;
465
784
  const current = this.options.store.getSourceJob(job.id);
785
+ if (current?.previousDefinitionHash !== undefined && !(error instanceof SourceGrowthNativeMismatchError))
786
+ return;
466
787
  if (current?.status === 'queued')
467
788
  this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: 'failed', failureCode: 'source-job-registration-rejected' });
468
789
  }
@@ -472,8 +793,45 @@ export class SourceJobRuntime {
472
793
  const failure = (unknown) => ({ outcome: unknown ? 'unknown' : 'failed', failureClass: unknown ? 'unknown' : 'configuration', failurePhase: 'host-execution', failureCode: 'source-job-rejected', sideEffectState: unknown ? 'unknown' : 'none', retryability: unknown ? 'unsafe' : 'after-intervention' });
473
794
  if (job === undefined || job.status !== 'queued')
474
795
  return failure(job?.status === 'unknown' || job?.status === 'running');
796
+ // A stale occurrence from before a persisted rearm can never claim this
797
+ // job. It has acquired no source resource, so leave the queued CAS intact.
798
+ if ((job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) !== undefined && input.definitionHash !== job.definitionHash) {
799
+ const expectedPending = job.previousDefinitionHash === undefined ? undefined : automationDefinitionDigest(this.definition(job));
800
+ if (expectedPending === input.definitionHash && input.executionMode === 'production'
801
+ && input.activationNonce === job.intentDigest && input.catalogDigest === CATALOG
802
+ && input.ownerRouteId === job.intent.owner.authorityId && input.principal === job.intent.owner.principalId
803
+ && input.targetScope.workspace === job.intent.owner.workspace && input.targetScope.preset === job.intent.owner.agentPreset) {
804
+ try {
805
+ // The new native occurrence reached us before its cross-ledger DB
806
+ // acknowledgement. It has not claimed source resources. Bind the
807
+ // exact native receipt, then persist another one-shot before this
808
+ // occurrence can be considered consumed.
809
+ this.schedule(job);
810
+ const rebound = this.options.store.getSourceJob(job.id);
811
+ if (rebound?.status === 'queued' && rebound.definitionHash === input.definitionHash && rebound.occurrenceId === undefined) {
812
+ const health = this.options.ports.automations.inspectSystemOwned({ owner: SOURCE_JOB_OWNER, automationId: job.automationId });
813
+ if (health.definitionHash !== rebound.definitionHash)
814
+ throw new Error('source job premature occurrence native binding changed');
815
+ const pending = this.withGapSource(rebound.intent.gapId, rebound.intent.owner, () => {
816
+ this.assertGrowthRun(rebound.intent.creation?.growthRun ?? rebound.intent.revision?.growthRun, rebound.intent.mode, rebound.intent.gapId, rebound.intent.owner, false);
817
+ return this.options.store.rearmQueuedGrowthSourceJob({ id: rebound.id, revision: rebound.revision,
818
+ priorDefinitionHash: rebound.definitionHash, priorDefinitionVersion: health.definitionVersion, dispatchAt: Date.now() + 1000 });
819
+ });
820
+ this.schedule(pending);
821
+ }
822
+ }
823
+ catch { /* persisted queued state remains fail-closed for exact recovery */ }
824
+ }
825
+ return failure(false);
826
+ }
475
827
  let claimed = false;
476
- const signal = AbortSignal.any([input.signal, this.abort.signal, AbortSignal.timeout(Math.max(1, Math.min(job.intent.build.timeoutMs + 120_000 + (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0), job.expiresAt - Date.now())))]);
828
+ const growthController = (job.intent.creation?.growthRun ?? job.intent.revision?.growthRun) === undefined ? undefined : new AbortController();
829
+ if (growthController)
830
+ this.growthControllers.add(growthController);
831
+ const signal = AbortSignal.any([input.signal, this.abort.signal,
832
+ ...(growthController ? [growthController.signal] : []),
833
+ AbortSignal.timeout(Math.max(1, Math.min(job.intent.build.timeoutMs + 120_000 + (this.options.verifyPreparedCreation ? 1_800_000 : 0)
834
+ + (this.options.advanceReleased ? this.options.releaseTimeoutMs ?? 40_000 : 0) + (this.options.adoptReleased ? this.options.adoptionTimeoutMs ?? 60_000 : 0), job.expiresAt - Date.now())))]);
477
835
  try {
478
836
  this.assertOwner(job);
479
837
  signal.throwIfAborted();
@@ -499,7 +857,9 @@ export class SourceJobRuntime {
499
857
  await this.options.prepare(owned, signal, assertCurrent);
500
858
  if (this.options.store.getSourceJob(owned.id)?.status !== 'prepared')
501
859
  throw new Error('source job completion was not committed');
502
- if ((this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased) && this.options.store.getOwnerTaskFailureReference(owned.intent.gapId)) {
860
+ if ((owned.intent.mode === 'create' ? this.options.verifyPreparedCreation !== undefined
861
+ : owned.intent.mode === 'revise-created' ? this.options.verifyPreparedRevision !== undefined : !!(this.options.approvePrepared || this.options.releasePrepared || this.options.advanceReleased || this.options.adoptReleased))
862
+ && this.options.store.getOwnerTaskFailureReference(owned.intent.gapId)) {
503
863
  this.reconcileContinuations();
504
864
  this.assertOwner(owned);
505
865
  await this.continuePrepared(this.options.store.getSourceJob(owned.id), signal);
@@ -507,13 +867,22 @@ export class SourceJobRuntime {
507
867
  }
508
868
  return { outcome: 'succeeded', failureClass: 'none', failurePhase: 'none', failureCode: 'none', sideEffectState: 'possible', retryability: 'unsafe' };
509
869
  }
510
- catch {
870
+ catch (error) {
511
871
  const current = this.options.store.getSourceJob(job.id);
872
+ if (!claimed && error instanceof SourceGrowthRunUnavailableError)
873
+ return failure(false);
874
+ if (!claimed && current?.status === 'queued'
875
+ && (current.revision !== job.revision || current.definitionHash !== input.definitionHash))
876
+ return failure(false);
512
877
  if (current?.status === 'queued' || (current?.status === 'running' && current.occurrenceId === input.occurrenceId)) {
513
878
  this.options.store.settleSourceJob({ id: current.id, revision: current.revision, status: claimed ? 'unknown' : 'failed', failureCode: claimed ? 'source-job-interrupted' : 'source-job-preflight-rejected' });
514
879
  }
515
880
  return failure(claimed);
516
881
  }
882
+ finally {
883
+ if (growthController)
884
+ this.growthControllers.delete(growthController);
885
+ }
517
886
  }
518
887
  /** Explicit Host action. Unknown keeps its capacity until exact cleanup succeeds. */
519
888
  reconcileUnknown(input) {
@@ -525,7 +894,15 @@ export class SourceJobRuntime {
525
894
  const trust = await this.options.trust();
526
895
  if (controlPlaneDigest(trust) !== job.intent.trustDigest)
527
896
  throw new Error('source job trust changed');
528
- await removeSourceJobContainer(job.intent.build, { id: job.id, containerName: job.intent.containerName });
897
+ if ('backend' in job.intent.build) {
898
+ if (job.intent.nativeResource?.backend !== 'macos-seatbelt' || job.intent.nativeResource.key !== job.id
899
+ || await this.options.inspectNativeQuiescence?.(job.id) !== 'quiescent') {
900
+ throw new Error('native source job has no valid durable quiescence receipt');
901
+ }
902
+ }
903
+ else {
904
+ await removeSourceJobContainer(job.intent.build, { id: job.id, containerName: job.intent.containerName });
905
+ }
529
906
  await removeSourceJobWorktree({ stateRoot: join(this.options.statePath, 'source-worktrees'), repository: job.intent.repository, worktree: job.intent.worktree, baseCommit: job.intent.baseCommit, environment: inheritedEnvironment(trust) });
530
907
  this.inspect(input);
531
908
  return projection(this.options.store.settleSourceJob({ id: job.id, revision: job.revision, status: 'failed', failureCode: 'source-job-resources-reconciled' }));