@zq-silk/yui 0.15.11 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/ARCHITECTURE.zh-CN.md +8 -4
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/artifacts/artifactCommitLock.js +16 -55
  6. package/dist/artifacts/taskArtifactRepository.js +3 -3
  7. package/dist/cli/agentConfigurationPicker.js +1 -1
  8. package/dist/cli/commandCatalog.js +92 -48
  9. package/dist/cli/completion.js +18 -18
  10. package/dist/cli/completionWizard.js +1 -1
  11. package/dist/cli/dynamicCompletion.js +1 -1
  12. package/dist/cli/interactionPolicy.js +11 -3
  13. package/dist/cli/invocationAuthority.js +64 -0
  14. package/dist/cli/managedDiagnostics.js +2 -2
  15. package/dist/cli/parseRepeatable.js +35 -0
  16. package/dist/cli/updatePorts.js +26 -5
  17. package/dist/cli.js +1303 -1235
  18. package/dist/commands/agentCommands.js +4 -4
  19. package/dist/commands/capabilityCommands.js +1 -1
  20. package/dist/commands/configCommands.js +6 -57
  21. package/dist/commands/configOverview.js +2 -2
  22. package/dist/commands/durableJobCommands.js +12 -6
  23. package/dist/commands/executionAuditCommands.js +10 -0
  24. package/dist/commands/globalRoleCommands.js +78 -29
  25. package/dist/commands/grantCommands.js +0 -3
  26. package/dist/commands/jobCommands.js +1 -4
  27. package/dist/commands/operatorCommands.js +3 -3
  28. package/dist/commands/projectCommands.js +45 -25
  29. package/dist/commands/resourcesCommands.js +10 -40
  30. package/dist/commands/roleConfiguration.js +2 -6
  31. package/dist/commands/taskActivationCommands.js +4 -8
  32. package/dist/commands/taskAgentCapabilities.js +9 -0
  33. package/dist/commands/taskCommandSupport.js +155 -0
  34. package/dist/commands/taskCommandTypes.js +1 -0
  35. package/dist/commands/taskCommands.js +355 -732
  36. package/dist/commands/taskCompletionGate.js +1 -1
  37. package/dist/commands/taskExecutionCommands.js +1 -1
  38. package/dist/commands/taskFactCommands.js +325 -0
  39. package/dist/commands/taskInputCommands.js +12 -42
  40. package/dist/commands/taskIntegrationCommands.js +20 -58
  41. package/dist/commands/taskNextActionCommand.js +7 -6
  42. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  43. package/dist/commands/taskPublicationCommands.js +12 -3
  44. package/dist/commands/taskPublicationVerifyCommand.js +24 -40
  45. package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
  46. package/dist/commands/taskUpstreamCommands.js +19 -8
  47. package/dist/commands/workflowCommands.js +6 -1
  48. package/dist/completion/completionInstaller.js +26 -26
  49. package/dist/completion/completionState.js +26 -26
  50. package/dist/completion/fileCompletionManager.js +6 -11
  51. package/dist/config/configCatalog.js +0 -2
  52. package/dist/config/timeZone.js +14 -0
  53. package/dist/config/yuiConfig.js +0 -29
  54. package/dist/context/runContextPack.js +5 -3
  55. package/dist/context/taskCatalog.js +185 -0
  56. package/dist/context/taskContext.js +152 -151
  57. package/dist/context/wakeRunReferences.js +11 -0
  58. package/dist/controller/agentCapabilities.js +58 -0
  59. package/dist/controller/agentRuntimeObserver.js +6 -11
  60. package/dist/controller/clientRuntime.js +22 -14
  61. package/dist/controller/controller.js +20 -24
  62. package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
  63. package/dist/controller/globalInputDelivery.js +13 -0
  64. package/dist/controller/jobClient.js +4 -4
  65. package/dist/controller/jobControl.js +60 -25
  66. package/dist/controller/jobSupervisor.js +4 -4
  67. package/dist/controller/providerRetryAdmission.js +100 -0
  68. package/dist/controller/providerRetryDelivery.js +218 -0
  69. package/dist/controller/runtime.js +85 -43
  70. package/dist/controller/runtimeEventProcessor.js +0 -5
  71. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  72. package/dist/controller/taskAgentError.js +35 -0
  73. package/dist/coordination/workMailboxQueue.js +2 -24
  74. package/dist/core/controllerClient.js +2 -2
  75. package/dist/core/fileLockOwner.js +74 -0
  76. package/dist/decision/decision.js +17 -0
  77. package/dist/doctor/doctor.js +18 -9
  78. package/dist/event/taskEvent.js +12 -0
  79. package/dist/execution/workItemExecutionProjection.js +2 -2
  80. package/dist/executor/agentCapabilityConfig.js +43 -0
  81. package/dist/executor/agentConfigurationCatalog.js +49 -11
  82. package/dist/executor/agentExecutor.js +5 -8
  83. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  84. package/dist/executor/taskAgentCapabilities.js +64 -0
  85. package/dist/integration/deliveryObligation.js +1 -69
  86. package/dist/integration/gitIntegrationService.js +79 -90
  87. package/dist/integration/integrationAttempt.js +3 -14
  88. package/dist/integration/integrationSourceApplication.js +4 -12
  89. package/dist/integration/manifestTags.js +2 -2
  90. package/dist/job/durableJob.js +4 -25
  91. package/dist/job/jobAssignmentScope.js +22 -0
  92. package/dist/job/jobOperation.js +16 -0
  93. package/dist/job/jobRunner.js +2 -1
  94. package/dist/job/stepDirectory.js +15 -0
  95. package/dist/kernel/builtinCapabilities.js +15 -11
  96. package/dist/kernel/kernelPorts.js +1 -17
  97. package/dist/lifecycle/exactRunTerminalization.js +5 -10
  98. package/dist/message/globalProviderRetry.js +15 -0
  99. package/dist/message/inputControlResolution.js +3 -4
  100. package/dist/message/message.js +17 -20
  101. package/dist/message/messageContinuation.js +1 -1
  102. package/dist/milestone/milestone.js +11 -0
  103. package/dist/observability/executionAudit.js +19 -0
  104. package/dist/observability/orchestrationMetrics.js +24 -1
  105. package/dist/observability/runtimeIdentity.js +1 -17
  106. package/dist/output/agentConfigurationPresentation.js +2 -0
  107. package/dist/output/timePresentation.js +1 -14
  108. package/dist/plugins/pluginService.js +12 -4
  109. package/dist/release/releaseWorkflowPorts.js +45 -96
  110. package/dist/release/runtimeRelease.js +9 -15
  111. package/dist/repository/gitWorkspace.js +371 -105
  112. package/dist/repository/projectMaintenanceLock.js +98 -82
  113. package/dist/repository/taskBaseFreshness.js +1 -1
  114. package/dist/repository/taskWorkspaceCoordinator.js +82 -144
  115. package/dist/repository/taskWorkspacePreparer.js +113 -47
  116. package/dist/repository/workspaceCleanupInspection.js +187 -0
  117. package/dist/resources/autoResourceGc.js +4 -77
  118. package/dist/resources/liveReferences.js +7 -54
  119. package/dist/resources/resourceDiscovery.js +5 -118
  120. package/dist/resources/resourceGc.js +233 -181
  121. package/dist/resources/resourceRegistrar.js +5 -4
  122. package/dist/resources/resourceRegistry.js +3 -37
  123. package/dist/resources/resourceRegistryStore.js +13 -36
  124. package/dist/resources/sqliteResourceRegistry.js +43 -26
  125. package/dist/review/deltaRecheck.js +1 -1
  126. package/dist/review/reviewAcceptance.js +1 -1
  127. package/dist/review/reviewDecision.js +1 -1
  128. package/dist/review/reviewRound.js +8 -7
  129. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  130. package/dist/runtime/acpProtocol.js +4 -51
  131. package/dist/runtime/acpSession.js +10 -75
  132. package/dist/runtime/acpSessionConfiguration.js +1 -8
  133. package/dist/runtime/agentEndpoint.js +3 -2
  134. package/dist/runtime/agentError.js +5 -3
  135. package/dist/runtime/agentFailureContext.js +43 -0
  136. package/dist/runtime/agentHost.js +42 -25
  137. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  138. package/dist/runtime/codexAppServerRuntime.js +34 -3
  139. package/dist/runtime/codexInteractiveHost.js +2 -2
  140. package/dist/runtime/index.js +0 -1
  141. package/dist/runtime/managedCaller.js +21 -1
  142. package/dist/runtime/providerControl.js +5 -1
  143. package/dist/runtime/providerErrors.js +38 -0
  144. package/dist/runtime/providerRetry.js +198 -0
  145. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  146. package/dist/runtime/runtimeObservation.js +0 -5
  147. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  148. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  149. package/dist/runtime/sessionTokenMetrics.js +15 -5
  150. package/dist/runtime/structuredProviderHost.js +10 -43
  151. package/dist/runtime/taskUsageMetrics.js +275 -0
  152. package/dist/runtime/tmuxAdapters.js +6 -8
  153. package/dist/scheduler/activeRoleRunDelivery.js +19 -7
  154. package/dist/scheduler/leaderWakeupProcessor.js +24 -6
  155. package/dist/scheduler/operatorEvent.js +12 -20
  156. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  157. package/dist/scheduler/ports.js +5 -8
  158. package/dist/scheduler/roleRunLiveness.js +4 -4
  159. package/dist/scheduler/roleRunStall.js +16 -23
  160. package/dist/scheduler/taskExecutionProjection.js +49 -36
  161. package/dist/scheduler/taskObservabilityProjection.js +6 -45
  162. package/dist/scheduler/taskWake.js +5 -10
  163. package/dist/scheduler/wakeReason.js +2 -0
  164. package/dist/scheduler/wakeupQueue.js +2 -4
  165. package/dist/setup/setupCommand.js +1 -1
  166. package/dist/storage/contextRecords.js +106 -0
  167. package/dist/storage/currentTaskStore.js +1 -1
  168. package/dist/storage/homeLayout.js +13 -17
  169. package/dist/storage/migrations/agentFailureContext.js +22 -0
  170. package/dist/storage/migrations/currentInputContract.js +86 -0
  171. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  172. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  173. package/dist/storage/migrations/integrationContinuation.js +5 -4
  174. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  175. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  176. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  177. package/dist/storage/migrations/verificationPolicy.js +74 -0
  178. package/dist/storage/migrations/workItemHistory.js +46 -0
  179. package/dist/storage/persistenceWorker.js +12 -4
  180. package/dist/storage/recordValidation.js +87 -0
  181. package/dist/storage/sqliteSchema.js +173 -1
  182. package/dist/storage/sqliteStore.js +172 -161
  183. package/dist/storage/storageSchema.js +2 -14
  184. package/dist/storage/storageVersions.js +1 -1
  185. package/dist/storage/storeRpc.js +16 -7
  186. package/dist/storage/taskCatalog.js +123 -0
  187. package/dist/storage/taskStore.js +5 -7
  188. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  189. package/dist/task/archiveDiagnostics.js +1 -0
  190. package/dist/task/archivePreflight.js +124 -0
  191. package/dist/task/completionReadiness.js +5 -24
  192. package/dist/task/draftPlan.js +0 -53
  193. package/dist/task/nextAction.js +7 -13
  194. package/dist/task/publicationAdoption.js +56 -0
  195. package/dist/task/publicationReference.js +10 -0
  196. package/dist/task/remoteDelivery.js +31 -16
  197. package/dist/task/remoteDeliveryService.js +89 -0
  198. package/dist/task/taskActivation.js +2 -25
  199. package/dist/task/taskActivationService.js +0 -2
  200. package/dist/task/taskRecordReference.js +0 -1
  201. package/dist/task/taskSubmission.js +3 -14
  202. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  203. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  204. package/dist/telemetry/telemetryWiring.js +4 -3
  205. package/dist/tmux/tmuxManager.js +29 -20
  206. package/dist/verification/gateArtifact.js +15 -24
  207. package/dist/verification/gateArtifactStore.js +14 -7
  208. package/dist/verification/verificationGateService.js +29 -88
  209. package/dist/verification/verificationPlan.js +27 -72
  210. package/dist/web/assets/client/app.js +92 -17
  211. package/dist/web/assets/client/components.js +55 -13
  212. package/dist/web/assets/client/i18n.js +72 -4
  213. package/dist/web/assets/client/taskSurface.js +6 -5
  214. package/dist/web/assets/client/view.js +35 -7
  215. package/dist/web/assets/shell.js +6 -0
  216. package/dist/web/assets/styles/layout.js +7 -0
  217. package/dist/web/webServer.js +12 -3
  218. package/dist/web/webSnapshot.js +14 -85
  219. package/dist/web/webTaskSurface.js +17 -46
  220. package/dist/workItem/workItem.js +1 -12
  221. package/dist/workspace/cleanupInspection.js +63 -0
  222. package/dist/workspace/workItemChangeSetManager.js +110 -50
  223. package/docs/agent-result-consumption.md +4 -0
  224. package/docs/agent-result-consumption.zh-CN.md +3 -0
  225. package/docs/agent-runtime-drivers.md +7 -0
  226. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  227. package/docs/architecture/README.md +2 -0
  228. package/docs/architecture/README.zh-CN.md +3 -1
  229. package/docs/architecture/capabilities-and-resources.md +42 -5
  230. package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
  231. package/docs/managed-turn-and-session-runtime.md +121 -1
  232. package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
  233. package/docs/observability/README.md +62 -0
  234. package/docs/observability/README.zh-CN.md +47 -0
  235. package/docs/plugin-sdk.md +4 -2
  236. package/docs/project-refresh.md +77 -0
  237. package/docs/project-refresh.zh-CN.md +59 -0
  238. package/docs/provider-retry.md +70 -0
  239. package/docs/provider-runtime.md +3 -1
  240. package/docs/provider-runtime.zh-CN.md +4 -2
  241. package/docs/release-workflow.md +190 -7
  242. package/docs/release-workflow.zh-CN.md +139 -5
  243. package/docs/roles-and-configuration.md +29 -0
  244. package/docs/roles-and-configuration.zh-CN.md +21 -0
  245. package/docs/sqlite-control-plane-design.md +16 -3
  246. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  247. package/docs/task-dag-semantics.md +3 -3
  248. package/docs/task-dag-semantics.zh-CN.md +2 -2
  249. package/docs/task-delivery.md +223 -17
  250. package/docs/task-delivery.zh-CN.md +171 -14
  251. package/docs/task-discovery.md +101 -0
  252. package/docs/task-discovery.zh-CN.md +85 -0
  253. package/docs/testing/verification-levels.md +161 -9
  254. package/docs/testing/verification-levels.zh-CN.md +116 -9
  255. package/i18n/README.zh-CN.md +13 -7
  256. package/package.json +1 -1
  257. package/skills/yui-leader/SKILL.md +5 -0
  258. package/skills/yui-leader/references/execution.md +3 -2
  259. package/skills/yui-leader/references/integration.md +9 -5
  260. package/skills/yui-leader/references/planning.md +9 -3
  261. package/skills/yui-operator/SKILL.md +24 -8
  262. package/skills/yui-reviewer/SKILL.md +4 -0
  263. package/skills/yui-runtime/SKILL.md +17 -2
  264. package/skills/yui-runtime/references/publication.md +26 -4
  265. package/skills/yui-runtime/references/recovery.md +71 -0
  266. package/dist/agent/agentRegistry.js +0 -10
  267. package/dist/agentRun/runIdentity.js +0 -22
  268. package/dist/commands/deliveryGuardPreflight.js +0 -30
  269. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  270. package/dist/commands/taskOverviewCommand.js +0 -376
  271. package/dist/completion/completionWizard.js +0 -125
  272. package/dist/context/dispatchContext.js +0 -110
  273. package/dist/context/wakeNotification.js +0 -144
  274. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  275. package/dist/integration/integrationQueueEntry.js +0 -191
  276. package/dist/integration/integrationQueueService.js +0 -810
  277. package/dist/release/fakeReleasePorts.js +0 -55
  278. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  279. package/dist/task/deliveryGuard.js +0 -226
  280. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -1,31 +1,30 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
- import { existsSync } from "node:fs";
4
2
  import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
5
3
  import { dirname, join, relative, resolve, sep } from "node:path";
6
4
  import { promisify } from "node:util";
7
5
  import { selectEnvironment } from "../agent/launchEnvironment.js";
8
6
  import { controllerSocketPath } from "../core/controllerEndpoint.js";
9
7
  import { durableJobIdempotencyKey } from "../job/durableJob.js";
10
- import { applyIntegrationSource, assertIntegrationCandidate, assertRecordedSourceCandidate } from "./integrationSourceApplication.js";
11
- import { planBootstrapJobSteps, planL2JobSteps } from "../verification/verificationPlan.js";
12
- import { assertNoAdHocFullSuiteChecks, checkResultsFromGateArtifact, checkResultsFromGateJob, gateIdentityForCandidate, lookupReusableGateArtifact, recordGateArtifactFromJob, recordGateArtifactFromStepOutcomes, resolveVerificationGate, runGateStepsInProcess } from "../verification/verificationGateService.js";
13
- import { recordGateArtifactPotentialReuse, recordGateArtifactReuse } from "../verification/gateArtifact.js";
14
- import { touchGateArtifact } from "../verification/gateArtifactStore.js";
8
+ import { readRuntimeIdentity } from "../release/runtimeRelease.js";
15
9
  import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
16
- import { integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
17
10
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
18
11
  import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
12
+ import { integrationWorkspaceRoot } from "../repository/taskWorkspacePreparer.js";
13
+ import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
19
14
  import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
20
- import { managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
15
+ import { integrationTmuxSocketRoot, managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
21
16
  import { advanceTaskProjectCommit } from "../task/task.js";
22
17
  import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
23
- import { recordIntegrationCheckJob, updateIntegrationAttempt } from "./integrationAttempt.js";
18
+ import { recordGateArtifactReuse } from "../verification/gateArtifact.js";
19
+ import { touchGateArtifact } from "../verification/gateArtifactStore.js";
20
+ import { beginGateVerification, checkResultsFromGateArtifact, checkResultsFromGateJob, gateIdentityForCandidate, lookupReusableGateArtifact, recordGateArtifactFromJob, recordGateArtifactFromStepOutcomes, resolveVerificationGate, runGateStepsInProcess } from "../verification/verificationGateService.js";
21
+ import { planBootstrapJobSteps, planL2JobSteps } from "../verification/verificationPlan.js";
24
22
  import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
25
- import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
26
- import { readRuntimeIdentity } from "../release/runtimeRelease.js";
27
- import { findReusableIntegrationCheckEvidence, INTEGRATION_RUNTIME_RELEASE_ENV } from "./integrationCheckEvidenceReuse.js";
23
+ import { recordIntegrationCheckJob, updateIntegrationAttempt } from "./integrationAttempt.js";
24
+ import { applyIntegrationSource, assertIntegrationCandidate, assertRecordedSourceCandidate } from "./integrationSourceApplication.js";
28
25
  const executeFile = promisify(execFile);
26
+ /** Preserve the exact execution-environment fence in durable Job input digests. */
27
+ const INTEGRATION_RUNTIME_RELEASE_ENV = "YUI_INTEGRATION_RUNTIME_RELEASE_ID";
29
28
  const INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES = [
30
29
  "PATH",
31
30
  "USER",
@@ -76,7 +75,7 @@ export class GitIntegrationService {
76
75
  #resourceRegistrar() {
77
76
  return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
78
77
  }
79
- async integrate(taskId, integrationId) {
78
+ async integrate(taskId, integrationId, signal) {
80
79
  let initial = requireIntegration(this.store, taskId, integrationId);
81
80
  if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
82
81
  throw new Error(`Integration cannot continue from ${initial.status}.`);
@@ -86,7 +85,7 @@ export class GitIntegrationService {
86
85
  // CAS. A concurrent `project migrate` must not switch the catalog path or
87
86
  // remove the old checkout mid-Integration, so the per-Project maintenance
88
87
  // fence is held across every Git effect and released only on exit.
89
- const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
88
+ const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
90
89
  try {
91
90
  // A concurrent abort may have settled the attempt while this caller was
92
91
  // acquiring the same Project fence. Never execute a pre-fence snapshot.
@@ -260,9 +259,9 @@ export class GitIntegrationService {
260
259
  /** Abandon under the same fence as Git/CAS, not just a status whitelist.
261
260
  * A delivered validating candidate is recorded as committed; a provably
262
261
  * unadvanced one may be abandoned without requiring obsolete check settings. */
263
- async abort(taskId, integrationId, reason, authorize) {
262
+ async abort(taskId, integrationId, reason, authorize, signal) {
264
263
  const initial = requireIntegration(this.store, taskId, integrationId);
265
- const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
264
+ const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
266
265
  try {
267
266
  const attempt = requireIntegration(this.store, taskId, integrationId);
268
267
  authorize(this.store, taskId);
@@ -338,12 +337,25 @@ export class GitIntegrationService {
338
337
  release();
339
338
  }
340
339
  }
341
- async cleanup(integration) {
340
+ async cleanup(integration, options = {}) {
342
341
  // The whole cleanup is one Git transaction against the Project's
343
342
  // repository, mirroring integrate(): a concurrent `project migrate` must
344
343
  // not switch the catalog path or remove the old checkout mid-cleanup.
345
- const release = acquireProjectMaintenanceLocks(this.home, [integration.projectId]);
344
+ const release = await acquireProjectMaintenanceLocks(this.home, [integration.projectId], { signal: options.signal });
346
345
  try {
346
+ options.authorize?.();
347
+ const current = requireIntegration(this.store, integration.taskId, integration.id);
348
+ if (current.projectId !== integration.projectId
349
+ || !["committed", "superseded", "failed"].includes(current.status)) {
350
+ throw new Error("Integration changed before cleanup; read its current record.");
351
+ }
352
+ integration = current;
353
+ const activeJob = this.store.listDurableJobs(integration.taskId).find(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === integration.id
354
+ && (job.status === "queued" || job.status === "running"
355
+ || (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
356
+ if (activeJob !== undefined) {
357
+ throw new Error(`Integration ${integration.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
358
+ }
347
359
  const task = this.store.getTask(integration.taskId);
348
360
  if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
349
361
  throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
@@ -408,26 +420,7 @@ export class GitIntegrationService {
408
420
  }
409
421
  await assertIntegrationCandidate(path, attempt.candidateCommit, workspace.branch);
410
422
  await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
411
- const { runtime, head, steps, environment, inputDigest, releaseId } = await this.#checkSpecification(attempt, managedWorkspace, gate);
412
- const ownedJobs = this.store.listDurableJobs(attempt.taskId).filter(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === attempt.id);
413
- if (gate === undefined && releaseId !== null && attempt.checkInputDigest === undefined
414
- && ownedJobs.length === 0 && attempt.jobId === undefined) {
415
- const reusable = findReusableIntegrationCheckEvidence({
416
- taskId: attempt.taskId,
417
- projectId: attempt.projectId,
418
- currentAttemptId: attempt.id,
419
- candidateCommit: head,
420
- checkCommands: attempt.checkCommands,
421
- runtimeReleaseId: releaseId,
422
- attempts: this.store.listIntegrationAttempts(attempt.taskId),
423
- jobs: this.store.listDurableJobs(attempt.taskId),
424
- logExists: (homeRelativePath) => existsSync(join(this.home, homeRelativePath)),
425
- logPathFor: (job, relativeLogPath) => (join(job.artifactsLocator, "logs", relativeLogPath))
426
- });
427
- if (reusable !== null) {
428
- return this.#finalizeGateSuccess(updateIntegrationAttempt(attempt, { checkInputDigest: inputDigest }, this.now()), workspace, repositoryPath, head, [...reusable.checks]);
429
- }
430
- }
423
+ const { runtime, head, steps, environment, inputDigest } = await this.#checkSpecification(attempt, managedWorkspace, gate);
431
424
  if (attempt.checkInputDigest !== undefined && attempt.checkInputDigest !== inputDigest) {
432
425
  throw new Error("Integration check conditions changed since admission; inspect the original Job, then abort or start a new attempt.");
433
426
  }
@@ -444,6 +437,14 @@ export class GitIntegrationService {
444
437
  })
445
438
  }, this.now());
446
439
  this.store.saveIntegrationAttempt(attempt.taskId, persisted);
440
+ if (gate !== undefined) {
441
+ // Resumption can enter here after persisting admission but before the
442
+ // original call withdrew old proof or recorded its Job id.
443
+ beginGateVerification(this.store, gateIdentityForCandidate({
444
+ projectId: attempt.projectId, gate, level: "L2", commit: head,
445
+ targetRef: attempt.targetRef, baseHead: attempt.beforeCommit
446
+ }), gate.plan, this.now());
447
+ }
447
448
  // All domain admission and durable candidate identity precede Job effects.
448
449
  this.runtimeIsolation.activate(runtime);
449
450
  await prepareIntegrationRuntimeHome(runtime, this.home);
@@ -507,7 +508,7 @@ export class GitIntegrationService {
507
508
  baseHead: attempt.beforeCommit
508
509
  });
509
510
  try {
510
- const artifact = await recordGateArtifactFromJob(this.store, this.home, identity, gate.plan, job, this.now());
511
+ const artifact = await this.#publishGateEvidence(workspace, job.head, () => recordGateArtifactFromJob(this.store, this.home, identity, gate.plan, job, this.now()));
511
512
  if (checks.every((check) => check.outcome !== "failed")) {
512
513
  checks.push(...checkResultsFromGateArtifact(artifact));
513
514
  }
@@ -549,11 +550,10 @@ export class GitIntegrationService {
549
550
  /**
550
551
  * Issue 08: the VerificationPlan gate for a configured Project.
551
552
  *
552
- * Enforce mode rejects ad-hoc full-suite checks before the gate. Reuse mode
553
- * returns an existing successful artifact for the same identity tuple
553
+ * Returns an existing successful artifact for the same identity tuple
554
554
  * (project + exact commit + plan digest + toolchain digest + target
555
- * boundary); record mode always runs and only counts shadow potential
556
- * reuses. The gate itself runs as bootstrap + L2 DurableJob steps (or
555
+ * boundary), unless this attempt explicitly requests fresh checks.
556
+ * The gate itself runs as bootstrap + L2 DurableJob steps (or
557
557
  * in-process when no Controller job port is available) and records a
558
558
  * self-contained GateArtifact. The final CAS in
559
559
  * {@link #finalizeGateSuccess} still fences a target that moves during the
@@ -561,19 +561,26 @@ export class GitIntegrationService {
561
561
  */
562
562
  async #runVerificationGate(attempt, workspace, path, managedWorkspace, repositoryPath, gate) {
563
563
  const spec = await this.#checkSpecification(attempt, managedWorkspace, gate);
564
+ const unsettled = this.store.listIntegrationAttempts(attempt.taskId).find(other => {
565
+ if (other.id === attempt.id || other.projectId !== attempt.projectId
566
+ || other.targetRef !== attempt.targetRef || other.beforeCommit !== attempt.beforeCommit
567
+ || other.candidateCommit !== attempt.candidateCommit
568
+ || other.gatePlanDigest !== gate.planDigest || other.gateToolchainDigest !== gate.toolchainDigest)
569
+ return false;
570
+ if (other.status === "running" || other.status === "validating")
571
+ return true;
572
+ const job = other.jobId === undefined ? null : this.store.getDurableJob(other.taskId, other.jobId);
573
+ return job !== null && (job.status === "queued" || job.status === "running"
574
+ || (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined));
575
+ });
576
+ if (unsettled !== undefined) {
577
+ throw new Error(`Exact verification is unsettled in ${unsettled.taskId}/${unsettled.id}; continue or settle its original Job before requesting another check.`);
578
+ }
564
579
  attempt = updateIntegrationAttempt(attempt, {
565
580
  checkInputDigest: spec.inputDigest,
566
581
  gatePlanDigest: gate.planDigest, gateToolchainDigest: gate.toolchainDigest
567
582
  }, this.now());
568
583
  this.store.saveIntegrationAttempt(attempt.taskId, attempt);
569
- if (gate.mode === "enforce") {
570
- try {
571
- assertNoAdHocFullSuiteChecks(gate.plan, attempt.checkCommands);
572
- }
573
- catch (error) {
574
- return this.#fail(attempt, error instanceof Error ? error : new Error(String(error)), "verification-plan", workspace);
575
- }
576
- }
577
584
  const candidateCommit = await gitLine(["-C", path, "rev-parse", "HEAD^{commit}"]);
578
585
  const identity = gateIdentityForCandidate({
579
586
  projectId: attempt.projectId,
@@ -583,7 +590,7 @@ export class GitIntegrationService {
583
590
  targetRef: attempt.targetRef,
584
591
  baseHead: attempt.beforeCommit
585
592
  });
586
- if (gate.mode !== "record") {
593
+ if (!attempt.rerunChecks && attempt.checkCommands.length === 0) {
587
594
  const existing = await lookupReusableGateArtifact(this.store, identity);
588
595
  if (existing !== null) {
589
596
  touchGateArtifact(this.store, recordGateArtifactReuse(existing, this.now()));
@@ -591,31 +598,22 @@ export class GitIntegrationService {
591
598
  return this.#finalizeGateSuccess(attempt, workspace, repositoryPath, candidateCommit, checks);
592
599
  }
593
600
  }
594
- else {
595
- // Record mode: observe the potential reuse without skipping the gate.
596
- const existing = await lookupReusableGateArtifact(this.store, identity);
597
- if (existing !== null) {
598
- touchGateArtifact(this.store, recordGateArtifactPotentialReuse(existing, this.now()));
599
- }
600
- }
601
601
  if (this.jobPort !== undefined) {
602
602
  return this.#startCheckJob(attempt, workspace, path, managedWorkspace, repositoryPath, gate);
603
603
  }
604
- // Jobless fallback (queue processing without a Controller): run the
604
+ beginGateVerification(this.store, identity, gate.plan, this.now());
605
+ // An explicit local caller without a Controller Job port runs the
605
606
  // plan gate in-process and record the artifact directly.
606
607
  const runtime = this.#runtimePreparation(attempt, managedWorkspace);
607
608
  this.runtimeIsolation.activate(runtime);
608
609
  let cleanupReason = "failure";
609
610
  try {
610
611
  await prepareIntegrationRuntimeHome(runtime, this.home);
611
- const steps = [
612
- ...planBootstrapJobSteps(gate.plan),
613
- ...planL2JobSteps(gate.plan)
614
- ];
612
+ const steps = spec.steps;
615
613
  const outcomes = await runGateStepsInProcess(path, steps, spec.environment, integrationCheckDirectory(this.home, attempt.taskId, attempt.id), candidateCommit);
616
614
  const succeeded = outcomes.length === steps.length
617
615
  && outcomes.every((outcome) => outcome.exitCode === 0 && outcome.signal === null && !outcome.timedOut);
618
- const artifact = await recordGateArtifactFromStepOutcomes(this.store, identity, gate.plan, outcomes, succeeded, this.now());
616
+ const artifact = await this.#publishGateEvidence(workspace, candidateCommit, () => recordGateArtifactFromStepOutcomes(this.store, identity, gate.plan, outcomes, succeeded, this.now()));
619
617
  const checks = checkResultsFromGateArtifact(artifact);
620
618
  cleanupReason = succeeded ? "completion" : "failure";
621
619
  if (!succeeded) {
@@ -629,6 +627,13 @@ export class GitIntegrationService {
629
627
  this.runtimeIsolation.cleanup(runtime, cleanupReason);
630
628
  }
631
629
  }
630
+ /** Both executors publish proof only for the exact clean candidate they
631
+ * checked. A later delivery/CAS failure is separate from this evidence, but
632
+ * a mutated candidate must never enter the reusable-success cache. */
633
+ async #publishGateEvidence(workspace, candidateCommit, publish) {
634
+ await assertIntegrationCandidate(workspace.path, candidateCommit, workspace.branch);
635
+ return publish();
636
+ }
632
637
  async #finalizeGateSuccess(attempt, workspace, repositoryPath, candidateCommit, checks) {
633
638
  await assertIntegrationCandidate(workspace.path, candidateCommit, workspace.branch);
634
639
  const validating = updateIntegrationAttempt(attempt, {
@@ -641,7 +646,7 @@ export class GitIntegrationService {
641
646
  const committed = this.#recordCommitted(validating);
642
647
  return this.#terminalResult("committed", committed, workspace);
643
648
  }
644
- #runtimePreparation(attempt, workspace) {
649
+ #runtimePreparation(_attempt, workspace) {
645
650
  return this.runtimeIsolation.preflight({
646
651
  workspace,
647
652
  allowExactActive: true
@@ -655,11 +660,12 @@ export class GitIntegrationService {
655
660
  const head = attempt.candidateCommit;
656
661
  if (head === undefined)
657
662
  throw new Error("Integration check specification requires a candidate.");
658
- const steps = gate === undefined
659
- ? attempt.checkCommands.map((command, index) => ({
660
- name: `check-${index + 1}`, command, timeoutMs: 30 * 60_000
661
- }))
662
- : [...planBootstrapJobSteps(gate.plan), ...planL2JobSteps(gate.plan)]
663
+ const explicitChecks = attempt.checkCommands.map((command, index) => ({
664
+ name: `check-${index + 1}`, command, timeoutMs: 30 * 60_000
665
+ }));
666
+ const steps = gate === undefined ? explicitChecks
667
+ : [...planBootstrapJobSteps(gate.plan, managedWorkspace.root),
668
+ ...planL2JobSteps(gate.plan, managedWorkspace.root), ...explicitChecks]
663
669
  .map(step => ({ ...step, timeoutMs: 30 * 60_000 }));
664
670
  const releaseId = integrationRuntimeReleaseIdentity(this.home);
665
671
  const environment = Object.freeze({
@@ -671,7 +677,7 @@ export class GitIntegrationService {
671
677
  projectId: attempt.projectId, head, workspace: managedWorkspace.root,
672
678
  env: environment, steps
673
679
  });
674
- return { runtime, head, steps, environment, inputDigest, releaseId };
680
+ return { runtime, head, steps, environment, inputDigest };
675
681
  }
676
682
  async #recoverValidating(attempt, workspace, managedWorkspace, repositoryPath) {
677
683
  if (attempt.candidateCommit === undefined || attempt.checks === undefined) {
@@ -792,11 +798,6 @@ function integrationSummary(attempt) {
792
798
  ? `Upstream ${attempt.source.remoteCommit} was already represented; Task head was unchanged.`
793
799
  : `Rebased Task changes onto upstream ${attempt.source.remoteCommit}.`;
794
800
  }
795
- if (attempt.source.kind === "historical-change-sets") {
796
- return unchanged
797
- ? "Historical Integration was already represented; Task head was unchanged."
798
- : "Historical Integration advanced the Task head.";
799
- }
800
801
  if (attempt.source.strategy === "manual") {
801
802
  const decision = attempt.resolution;
802
803
  if (decision?.action !== "manual-resolution") {
@@ -1023,18 +1024,6 @@ function defaultIntegrationRuntimeIsolation(home, homeId) {
1023
1024
  }
1024
1025
  });
1025
1026
  }
1026
- /**
1027
- * Short `/tmp` directory that holds ONLY the integration check's tmux socket
1028
- * endpoint. tmux uses a `sockaddr_un` path whose length budget cannot absorb a
1029
- * deep Home path, so this single IPC endpoint is the one approved exception to
1030
- * the unified-Home contract. All other integration runtime state (data, cache,
1031
- * ordinary temp) lives in the Home partition from `managedIntegrationRuntimeRoot`.
1032
- */
1033
- function integrationTmuxSocketRoot(home) {
1034
- const uid = typeof process.getuid === "function" ? process.getuid() : 0;
1035
- const homeDigest = createHash("sha256").update(resolve(home)).digest("hex").slice(0, 16);
1036
- return join("/tmp", `yi-${uid.toString(36)}-${homeDigest}`);
1037
- }
1038
1027
  /**
1039
1028
  * Reuse is enabled only for an immutable installed release. Development
1040
1029
  * checkouts have no content-addressed release identity, so they return.
@@ -12,6 +12,7 @@ export function createIntegrationAttempt(input, now) {
12
12
  source: input.source,
13
13
  beforeCommit: input.beforeCommit,
14
14
  checkCommands: normalizedUniqueText(input.checkCommands ?? [], "Integration check command"),
15
+ rerunChecks: input.rerunChecks ?? false,
15
16
  status: "running",
16
17
  createdAt: timestamp,
17
18
  updatedAt: timestamp
@@ -143,6 +144,8 @@ export function validateIntegrationAttempt(attempt) {
143
144
  requireCommit(attempt.beforeCommit, "Integration before commit");
144
145
  validateIntegrationSource(attempt.taskId, attempt.source);
145
146
  normalizedUniqueText(attempt.checkCommands, "Integration check command");
147
+ if (typeof attempt.rerunChecks !== "boolean")
148
+ throw new Error("Integration rerunChecks must be explicit.");
146
149
  if (attempt.candidateCommit !== undefined) {
147
150
  requireCommit(attempt.candidateCommit, "Integration candidate commit");
148
151
  }
@@ -239,20 +242,6 @@ function validateIntegrationSource(taskId, source) {
239
242
  }
240
243
  return;
241
244
  }
242
- if (source.kind === "historical-change-sets") {
243
- if (!Array.isArray(source.changeSetIds) || source.changeSetIds.length === 0) {
244
- throw new Error("Historical Integration source requires ChangeSet ids.");
245
- }
246
- const seen = new Set();
247
- for (const changeSetId of source.changeSetIds) {
248
- validateTaskRecordReference({ taskId, localId: changeSetId }, "changeSet");
249
- if (seen.has(changeSetId)) {
250
- throw new Error(`Historical Integration ChangeSet is duplicated: ${changeSetId}.`);
251
- }
252
- seen.add(changeSetId);
253
- }
254
- return;
255
- }
256
245
  throw new Error("Integration source is invalid.");
257
246
  }
258
247
  function normalizeConflictReport(report) {
@@ -33,9 +33,6 @@ export async function applyIntegrationSource(input) {
33
33
  });
34
34
  return pending;
35
35
  };
36
- if (source.kind === "historical-change-sets") {
37
- throw new Error("Historical Integration source has no current application proof; preserve evidence and abort explicitly.");
38
- }
39
36
  if (source.kind === "work-item" && source.strategy === "manual") {
40
37
  if (current.candidateCommit !== undefined) {
41
38
  await assertIntegrationCandidate(path, current.candidateCommit);
@@ -61,15 +58,11 @@ export async function applyIntegrationSource(input) {
61
58
  const branch = workspace.branch.startsWith("refs/") ? workspace.branch : `refs/heads/${workspace.branch}`;
62
59
  const active = await activeGitOperation(path);
63
60
  if (current.sourceProgress === undefined) {
64
- // Historical unfinished operations can be adopted only when Git still
65
- // carries their exact source/target identity. Finished old operations
66
- // without a receipt are deliberately not inferred from a clean tree.
61
+ // Only a pristine candidate can establish this attempt's first receipt.
62
+ // An unfinished operation without that receipt belongs to explicit recovery.
67
63
  const head = await line(path, "rev-parse", "HEAD");
68
64
  if (active !== undefined) {
69
- if (kind === "cherry-pick") {
70
- throw new Error("Historical cherry-pick has no durable completed-step cursor; preserve it and choose explicit recovery.");
71
- }
72
- await assertOperation(path, kind, commits[0], current.beforeCommit, branch, source);
65
+ throw new Error("Active Git operation has no attempt-owned progress receipt; preserve it and choose explicit recovery.");
73
66
  }
74
67
  else if (head !== current.beforeCommit || current.status === "conflicted") {
75
68
  throw new Error("Integration source completion is unproven: no attempt-owned Git receipt. Preserve the candidate; do not replay.");
@@ -79,8 +72,7 @@ export async function applyIntegrationSource(input) {
79
72
  }
80
73
  save({ sourceProgress: {
81
74
  workspace: resolve(path), branch, sourceDigest, completedSteps: 0,
82
- head: current.beforeCommit,
83
- ...(active === undefined ? {} : { activeAction: actionId() })
75
+ head: current.beforeCommit
84
76
  } });
85
77
  }
86
78
  let progress = current.sourceProgress;
@@ -77,7 +77,7 @@ function isCommandPath(lowerPath, segments, lowerFile) {
77
77
  || lowerFile.startsWith("cli")
78
78
  || lowerPath.includes("/bin/");
79
79
  }
80
- function isContractPath(lowerPath, segments, lowerFile) {
80
+ function isContractPath(lowerPath, _segments, lowerFile) {
81
81
  return lowerFile === "index.ts"
82
82
  || lowerFile === "index.js"
83
83
  || lowerFile === "index.mjs"
@@ -87,7 +87,7 @@ function isContractPath(lowerPath, segments, lowerFile) {
87
87
  || lowerPath.includes("/contract/")
88
88
  || lowerPath.includes("/contracts/");
89
89
  }
90
- function isTestPath(lowerPath, segments, lowerFile) {
90
+ function isTestPath(_lowerPath, segments, lowerFile) {
91
91
  if (segments.includes("test") || segments.includes("tests") || segments.includes("__tests__")) {
92
92
  return true;
93
93
  }
@@ -16,27 +16,9 @@ export function isDurableJobTerminal(status) {
16
16
  }
17
17
  export function createDurableJob(input, now) {
18
18
  const timestamp = now.toISOString();
19
- const contentKey = input.retryOf === undefined
20
- ? durableJobIdempotencyKey({
21
- owner: input.owner,
22
- projectId: input.projectId,
23
- head: input.head,
24
- steps: input.steps,
25
- workspace: input.workspace,
26
- env: input.env
27
- })
28
- : retryDurableJobIdempotencyKey(durableJobIdempotencyKey({
29
- owner: input.owner,
30
- projectId: input.projectId,
31
- head: input.head,
32
- steps: input.steps,
33
- workspace: input.workspace,
34
- env: input.env
35
- }), input.retryOf);
36
- const idempotencyKey = input.operation === undefined ? contentKey
37
- : createHash("sha256").update(JSON.stringify([
38
- input.operation.actorId, input.operation.requestId
39
- ])).digest("hex");
19
+ const idempotencyKey = createHash("sha256").update(JSON.stringify([
20
+ input.operation.actorId, input.operation.requestId
21
+ ])).digest("hex");
40
22
  return validateDurableJob({
41
23
  schemaVersion: CURRENT_DURABLE_JOB_SCHEMA_VERSION,
42
24
  id: input.id,
@@ -49,10 +31,7 @@ export function createDurableJob(input, now) {
49
31
  steps: input.steps.map((step) => ({ ...step })),
50
32
  idempotencyKey,
51
33
  operation: {
52
- requestId: input.operation?.requestId ?? idempotencyKey,
53
- inputDigest: input.operation?.inputDigest ?? idempotencyKey,
54
- actorId: input.operation?.actorId ?? "internal:job",
55
- authorityRef: input.operation?.authorityRef ?? "internal:job",
34
+ ...input.operation,
56
35
  targetId: input.workspace,
57
36
  capability: "job.start",
58
37
  implementation: JOB_RUNNER_IMPLEMENTATION,
@@ -0,0 +1,22 @@
1
+ import { resolve } from "node:path";
2
+ export class JobAssignmentScopeError extends Error {
3
+ }
4
+ /** Authentication identifies the current Session; it does not grant another
5
+ * Assignment's workspace. Share this check across admission, management and
6
+ * the last pre-spawn boundary. Existing running Jobs settle independently. */
7
+ export function assertJobAssignmentScope(store, target, caller) {
8
+ if (caller.scope !== "task" || caller.role === "leader")
9
+ return;
10
+ const run = caller.role === undefined ? null : store.getActiveRun(target.taskId, caller.role);
11
+ const workspace = run?.workspace ?? run?.effective.workspace;
12
+ const item = run?.workItemId === undefined ? null : store.getWorkItem(target.taskId, run.workItemId);
13
+ if (run?.status !== "active" || run.reviewRoundId !== undefined
14
+ || target.owner.kind !== "work-item" || target.owner.workItemId !== run.workItemId
15
+ || item?.status !== "open" || item.assignee !== caller.role
16
+ || !item.writeProjectIds.includes(target.projectId)
17
+ || !run.effective.writeProjectIds.includes(target.projectId)
18
+ || workspace === undefined || resolve(workspace.root) !== resolve(target.workspace)
19
+ || !workspace.entries.some(entry => entry.projectId === target.projectId && entry.access === "write")) {
20
+ throw new JobAssignmentScopeError("Job is outside the caller's current Assignment. Use the exact assigned WorkItem workspace and writable Project; this Job owner cannot represent a Review or replica Lane.");
21
+ }
22
+ }
@@ -0,0 +1,16 @@
1
+ /** A read model over the one Job, never separately persisted.
2
+ * confirmed means the selected runner was observed, not that every action
3
+ * performed by an arbitrary command succeeded. Inspect original step receipts.
4
+ */
5
+ export function inspectJobOperation(job) {
6
+ return {
7
+ operationRef: { taskId: job.taskId, jobId: job.id },
8
+ ...job.operation,
9
+ state: job.status === "queued" ? "pending"
10
+ : job.status === "running" ? "running"
11
+ : job.status === "unknown-needs-attention" ? "unknown" : "finished",
12
+ outcome: job.result?.outcome,
13
+ result: job.result,
14
+ checkpoint: job.checkpoint
15
+ };
16
+ }
@@ -11,6 +11,7 @@ import { spawn } from "node:child_process";
11
11
  import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, utimesSync, writeFileSync } from "node:fs";
12
12
  import { join, resolve } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
+ import { jobStepDirectory } from "./stepDirectory.js";
14
15
  import { writeTextFileAtomically } from "../storage/durableFile.js";
15
16
  import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
16
17
  const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
@@ -129,7 +130,7 @@ export async function runDurableJobRunner(specPath) {
129
130
  // Issue 08: a structured argv step executes without a shell so its
130
131
  // tokens can never be reinterpreted; a legacy command step keeps the
131
132
  // shell form. A per-step cwd/env override applies to either form.
132
- const stepCwd = step.cwd ?? spec.workspace;
133
+ const stepCwd = jobStepDirectory(spec.workspace, step.cwd);
133
134
  const stepEnv = step.env === undefined
134
135
  ? spec.env
135
136
  : { ...spec.env, ...step.env };
@@ -0,0 +1,15 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ /** Recheck at execution, including directories created by a bootstrap step. */
4
+ export function jobStepDirectory(workspace, override) {
5
+ const root = realpathSync(workspace);
6
+ const cwd = override ?? workspace;
7
+ if (!isAbsolute(cwd))
8
+ throw new Error("Job step cwd must be absolute.");
9
+ const actual = realpathSync(cwd);
10
+ const nested = relative(root, actual);
11
+ if (nested === ".." || nested.startsWith(`..${sep}`) || isAbsolute(nested)) {
12
+ throw new Error(`Job step cwd is outside its managed workspace: ${resolve(cwd)}.`);
13
+ }
14
+ return actual;
15
+ }
@@ -1,13 +1,14 @@
1
- import { updateTaskMetadataCommand, sendTaskMessageCommand } from "../commands/taskCommands.js";
1
+ import { listArtifactsCapability, readArtifactCapability, saveArtifactCapability } from "../artifacts/artifactCapability.js";
2
2
  import { runConfigCommand } from "../commands/configCommands.js";
3
- import { readTaskContext, readTaskContextDelta, inspectTaskContext, withContextObservations } from "../context/taskContext.js";
3
+ import { sendTaskMessageCommand, updateTaskMetadataCommand } from "../commands/taskCommands.js";
4
4
  import { CONFIG_DOMAINS } from "../config/configCatalog.js";
5
+ import { inspectTaskContext, readTaskContext, readTaskContextDelta, withContextObservations } from "../context/taskContext.js";
5
6
  import { createJobCallAuthority, parseDurableJobStartParams } from "../controller/jobControl.js";
6
- import { inspectJobOperation } from "./kernelPorts.js";
7
- import { CapabilityRegistry, } from "./capabilityRegistry.js";
8
- import { createProjectResources } from "../resources/projectResourceService.js";
9
- import { saveArtifactCapability, readArtifactCapability, listArtifactsCapability } from "../artifacts/artifactCapability.js";
7
+ import { inspectJobOperation } from "../job/jobOperation.js";
8
+ import { assertJobAssignmentScope } from "../job/jobAssignmentScope.js";
10
9
  import { createPluginService } from "../plugins/pluginService.js";
10
+ import { createProjectResources } from "../resources/projectResourceService.js";
11
+ import { CapabilityRegistry, } from "./capabilityRegistry.js";
11
12
  const text = { type: "string", minLength: 1 };
12
13
  const strings = { type: "object", additionalProperties: { type: "string" } };
13
14
  const object = (properties, required = Object.keys(properties)) => ({
@@ -26,7 +27,6 @@ const definitions = [
26
27
  effect: "local-mutation", requiredPermissions: ["task:read"], source: "sendTaskMessageCommand",
27
28
  inputSchema: object({ taskId: text, body: text,
28
29
  intent: { enum: ["record", "discuss", "develop"] },
29
- wakePolicy: { enum: ["leader", "none"] },
30
30
  recipient: object({ roleName: text, workItemId: text, reviewRoundId: text }, ["roleName"])
31
31
  }, ["taskId", "body"]), outputSchema: recordOutput
32
32
  },
@@ -170,7 +170,8 @@ const definitions = [
170
170
  ] },
171
171
  env: strings,
172
172
  steps: { type: "array", minItems: 1, items: object({
173
- name: text, command: text, timeoutMs: { type: "integer" }
173
+ name: text, command: text, timeoutMs: { type: "integer" },
174
+ argv: { type: "array", minItems: 1, items: text }, cwd: text, env: strings
174
175
  }, ["name", "command"]) },
175
176
  retryOf: text
176
177
  }, ["taskId", "projectId", "head", "workspace", "owner", "env", "steps"]),
@@ -247,7 +248,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
247
248
  // gains develop from an intent argument; only a global Operator Session
248
249
  // (user-authority) may carry one. sendTaskMessageCommand enforces this,
249
250
  // so the capability just forwards the optional intent unchanged.
250
- const result = sendTaskMessageCommand(store, taskId, params.body, params.wakePolicy, { environment: callerEnvironment(caller) }, params.recipient, params.intent);
251
+ const result = sendTaskMessageCommand(store, taskId, params.body, { environment: callerEnvironment(caller) }, params.recipient, params.intent);
251
252
  signal(taskId);
252
253
  return result.message;
253
254
  }
@@ -338,11 +339,11 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
338
339
  ...(patch.tags?.length === 0 ? { tags: null } : {})
339
340
  }, {
340
341
  environment: callerEnvironment(caller),
341
- runtime: { notifyStateChanged: signal, reconcileTask: signal }
342
+ runtime: { notifyMailboxChanged: () => signal(taskId), reconcileTask: signal }
342
343
  });
343
344
  }
344
345
  if (name === "job.get") {
345
- const job = jobs.getJob(taskId, params.jobId);
346
+ const job = jobs.getJob(taskId, params.jobId, caller);
346
347
  if (!job)
347
348
  throw new Error(`Job not found: ${taskId}/${params.jobId}.`);
348
349
  const operation = inspectJobOperation(job);
@@ -379,6 +380,9 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
379
380
  if (sessions?.sessions[sessions.activeAgentId]?.effective.executionAuthority !== "delivery") {
380
381
  throw new Error("Planning Sessions cannot start delivery Jobs.");
381
382
  }
383
+ if (descriptor?.name === "job.start" && input !== undefined) {
384
+ assertJobAssignmentScope(store, input, caller);
385
+ }
382
386
  }
383
387
  if (permission === "task:read" || permission === "job:start")
384
388
  continue;
@@ -1,7 +1,7 @@
1
1
  import { createDurableJobControl } from "../controller/jobControl.js";
2
2
  import { JOB_RUNNER_IMPLEMENTATION } from "../job/durableJob.js";
3
- import { InstanceHost } from "./instanceHost.js";
4
3
  import { createBuiltinCapabilities } from "./builtinCapabilities.js";
4
+ import { InstanceHost } from "./instanceHost.js";
5
5
  /** Called once by the existing Controller root. Does not open a Store, start
6
6
  * another Controller, or provide arbitrary persistence to plugin code.
7
7
  * T02 registers contributions on this Host and wraps this same Job control.
@@ -29,19 +29,3 @@ export function createKernelPorts(store, runner, signal = () => undefined, conte
29
29
  }
30
30
  };
31
31
  }
32
- /** A read model over the one Job, never separately persisted.
33
- * confirmed means the selected runner was observed, not that every action
34
- * performed by an arbitrary command succeeded. Inspect original step receipts.
35
- */
36
- export function inspectJobOperation(job) {
37
- return {
38
- operationRef: { taskId: job.taskId, jobId: job.id },
39
- ...job.operation,
40
- state: job.status === "queued" ? "pending"
41
- : job.status === "running" ? "running"
42
- : job.status === "unknown-needs-attention" ? "unknown" : "finished",
43
- outcome: job.result?.outcome,
44
- result: job.result,
45
- checkpoint: job.checkpoint
46
- };
47
- }