@zq-silk/yui 0.14.0 → 0.14.2
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.
- package/ARCHITECTURE.md +64 -35
- package/README.md +258 -125
- package/dist/agent/argumentPolicy.js +2 -1
- package/dist/agent/managedRuntimeEnvironment.js +5 -8
- package/dist/cli/commandCatalog.js +74 -109
- package/dist/cli/helpRenderer.js +1 -2
- package/dist/cli/interactionCandidates.js +10 -0
- package/dist/cli/interactionPolicy.js +76 -37
- package/dist/cli/roleWizard.js +5 -6
- package/dist/cli/updateOrchestrator.js +10 -8
- package/dist/cli/updatePorts.js +17 -13
- package/dist/cli/upgradeCommand.js +2 -5
- package/dist/cli.js +268 -231
- package/dist/commands/agentCommands.js +13 -0
- package/dist/commands/configCommands.js +7 -19
- package/dist/commands/configOverview.js +3 -2
- package/dist/commands/durableJobCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +4 -9
- package/dist/commands/globalRoleCommands.js +17 -0
- package/dist/commands/profileCommands.js +200 -30
- package/dist/commands/sessionCommands.js +1 -1
- package/dist/commands/taskActor.js +29 -127
- package/dist/commands/taskChangeSetCommands.js +1 -3
- package/dist/commands/taskCommands.js +1771 -2107
- package/dist/commands/taskCompletionGate.js +27 -103
- package/dist/commands/taskContextCommand.js +47 -80
- package/dist/commands/taskExecutionCommands.js +9 -27
- package/dist/commands/taskInputCommands.js +10 -10
- package/dist/commands/taskIntegrationCommands.js +69 -61
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -29
- package/dist/commands/taskOverviewCommand.js +25 -3
- package/dist/commands/taskPublicationCommands.js +151 -85
- package/dist/commands/taskPublicationVerifyCommand.js +181 -0
- package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
- package/dist/commands/taskRoleRuntimeStatus.js +5 -6
- package/dist/commands/taskUpstreamCommands.js +79 -95
- package/dist/config/configCatalog.js +1 -1
- package/dist/context/contextSnapshot.js +17 -2
- package/dist/context/sessionBootstrapManifest.js +18 -1
- package/dist/context/sourceTurnContext.js +30 -0
- package/dist/context/turnContextPack.js +107 -48
- package/dist/context/turnInputContract.js +11 -4
- package/dist/context/wakeNotification.js +84 -22
- package/dist/controller/agentRuntimeObserver.js +9 -12
- package/dist/controller/clientRuntime.js +13 -13
- package/dist/controller/controller.js +11 -11
- package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
- package/dist/controller/jobControl.js +11 -10
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +2 -2
- package/dist/controller/runtime.js +18 -61
- package/dist/controller/runtimeEventInbox.js +57 -34
- package/dist/controller/runtimeEventProcessor.js +36 -96
- package/dist/controller/runtimeHookTurnFence.js +33 -57
- package/dist/controller/runtimeLaunchCoordinator.js +44 -40
- package/dist/controller/runtimeObservationHook.js +18 -12
- package/dist/controller/sessionNotify.js +43 -28
- package/dist/controller/sessionOwnerReconciliation.js +21 -21
- package/dist/controller/structuredProviderObservation.js +13 -16
- package/dist/coordination/workMailboxQueue.js +132 -0
- package/dist/core/controllerServer.js +1 -0
- package/dist/domain/agentResultTransport.js +60 -0
- package/dist/execution/executionHealth.js +111 -118
- package/dist/execution/reviewMainTurn.js +161 -0
- package/dist/execution/workItemExecution.js +454 -0
- package/dist/execution/workItemExecutionProjection.js +309 -0
- package/dist/execution/workItemMainTurn.js +164 -0
- package/dist/executor/agentExecutor.js +19 -19
- package/dist/executor/executorRegistry.js +7 -28
- package/dist/executor/fileRoleLaunchPlanner.js +51 -83
- package/dist/external/githubPublicationVerifier.js +114 -0
- package/dist/external/gitlabPublicationVerifier.js +158 -0
- package/dist/external/pinnedCommandRunner.js +73 -0
- package/dist/integration/changeSetManifest.js +0 -3
- package/dist/integration/deliveryObligation.js +43 -6
- package/dist/integration/gitIntegrationService.js +284 -101
- package/dist/integration/integrationAttempt.js +64 -14
- package/dist/integration/integrationQueueEntry.js +5 -35
- package/dist/integration/integrationQueueService.js +65 -194
- package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
- package/dist/lifecycle/exactTurnTerminalization.js +100 -97
- package/dist/message/message.js +16 -0
- package/dist/observability/executionAudit.js +4 -25
- package/dist/observability/faultClassification.js +58 -70
- package/dist/observability/orchestrationMetrics.js +8 -50
- package/dist/profile/agentProfile.js +60 -15
- package/dist/profile/agentProfileRuntime.js +100 -0
- package/dist/release/releaseWorkflowPorts.js +5 -68
- package/dist/repository/executionLaneGitSnapshot.js +80 -0
- package/dist/repository/gitWorkspace.js +9 -67
- package/dist/repository/taskBaseFreshness.js +3 -1
- package/dist/repository/taskWorkspaceCoordinator.js +32 -4
- package/dist/repository/taskWorkspacePreparer.js +252 -411
- package/dist/resources/liveReferences.js +6 -55
- package/dist/resources/resourceDiscovery.js +1 -30
- package/dist/resources/resourceRegistrar.js +0 -23
- package/dist/review/deltaRecheck.js +8 -65
- package/dist/review/reviewAcceptance.js +22 -19
- package/dist/review/reviewConfig.js +0 -10
- package/dist/review/reviewDecision.js +2 -2
- package/dist/review/reviewRound.js +147 -273
- package/dist/review/reviewerAvailability.js +13 -23
- package/dist/role/role.js +3 -0
- package/dist/runtime/agentDriverObservation.js +1 -1
- package/dist/runtime/agentHost.js +51 -51
- package/dist/runtime/builtinAgentDrivers.js +9 -2
- package/dist/runtime/codexAppServerRuntime.js +23 -5
- package/dist/runtime/exactControlPlane.js +2 -267
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/launchBroker.js +11 -11
- package/dist/runtime/lifecycleReservation.js +2 -2
- package/dist/runtime/managedCaller.js +111 -0
- package/dist/runtime/ports.js +16 -3
- package/dist/runtime/preallocatedNativeSession.js +2 -2
- package/dist/runtime/processExitObservation.js +3 -2
- package/dist/runtime/providerContinuationReconciliationService.js +1 -1
- package/dist/runtime/runtimeBinding.js +1 -1
- package/dist/runtime/runtimeObservation.js +33 -16
- package/dist/runtime/runtimeSessionCandidate.js +2 -2
- package/dist/runtime/runtimeStopReceipt.js +11 -11
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/sessionOwnerIdentity.js +9 -9
- package/dist/runtime/sessionOwnerRegistry.js +13 -13
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTerminationGuard.js +9 -9
- package/dist/runtime/sessionTokenMetrics.js +5 -7
- package/dist/runtime/structuredProviderHost.js +5 -5
- package/dist/runtime/taskRuntimeIsolation.js +24 -24
- package/dist/runtime/tmuxAdapters.js +32 -30
- package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
- package/dist/scheduler/leaderWakeupProcessor.js +78 -55
- package/dist/scheduler/roleTurnLiveness.js +6 -6
- package/dist/scheduler/roleTurnStall.js +8 -9
- package/dist/scheduler/taskExecutionProjection.js +19 -41
- package/dist/scheduler/taskObservabilityProjection.js +75 -122
- package/dist/storage/sqliteSchema.js +1 -21
- package/dist/storage/sqliteStore.js +62 -47
- package/dist/storage/storageSchema.js +1 -8
- package/dist/storage/storageVersions.js +7 -4
- package/dist/storage/taskStore.js +8 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
- package/dist/task/completionReadiness.js +17 -98
- package/dist/task/deliveryGuard.js +9 -11
- package/dist/task/draftPlan.js +142 -0
- package/dist/task/nextAction.js +179 -477
- package/dist/task/publicationVerification.js +1 -0
- package/dist/task/remoteDelivery.js +233 -0
- package/dist/task/task.js +108 -4
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/turn/turn.js +109 -16
- package/dist/verification/verificationGateService.js +0 -26
- package/dist/web/assets/client/components.js +99 -121
- package/dist/web/assets/client/i18n.js +136 -58
- package/dist/web/assets/client/view.js +44 -0
- package/dist/web/assets/styles/cards.js +3 -15
- package/dist/web/assets/styles/widgets.js +4 -0
- package/dist/web/webSnapshot.js +29 -14
- package/dist/workItem/dependencyGate.js +34 -0
- package/dist/workItem/workItem.js +99 -266
- package/dist/workspace/workItemChangeSetManager.js +15 -59
- package/docs/task-local-identity.md +1 -1
- package/i18n/README.zh-CN.md +165 -59
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +272 -782
- package/skills/yui-operator/SKILL.md +190 -365
- package/skills/yui-reviewer/SKILL.md +40 -33
- package/skills/yui-runtime/SKILL.md +27 -6
- package/skills/yui-worker/SKILL.md +19 -6
- package/dist/execution/candidateConvergence.js +0 -623
- package/dist/execution/executionGroup.js +0 -854
- package/dist/execution/resourceBroker.js +0 -426
- package/dist/executor/launchPlan.js +0 -16
- package/dist/review/reviewFinding.js +0 -240
- package/dist/review/reviewFindingLedger.js +0 -550
- package/dist/review/reviewOutcomeClassifier.js +0 -255
- package/dist/scheduler/resourceQueueProjection.js +0 -71
- package/dist/task/repairWave.js +0 -150
|
@@ -16,7 +16,7 @@ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.j
|
|
|
16
16
|
import { classifyWorkspacePreflight, formatWorkspacePreflightError } from "./workspacePreflightClassification.js";
|
|
17
17
|
import { activeLiveRoleAgentSession } from "./agentExecutor.js";
|
|
18
18
|
import { effectiveLaunchSnapshotsCompatibleForTaskSession, effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
|
|
19
|
-
import { YUI_CONTROL_PLANE_DESCRIPTOR,
|
|
19
|
+
import { YUI_CONTROL_PLANE_DESCRIPTOR, createExactControlPlaneDescriptor, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
|
|
20
20
|
import { detectRunningRelease } from "../release/runtimeRelease.js";
|
|
21
21
|
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
22
22
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
@@ -87,49 +87,6 @@ export class FileRoleLaunchPlanner {
|
|
|
87
87
|
const hash = createHash("sha256").update(input.callerKey).digest("hex");
|
|
88
88
|
this.store.setJobCallerKeyHash(input.taskId, input.roleName, input.agentId, hash);
|
|
89
89
|
}
|
|
90
|
-
/** Re-publishes provider-discovered native identity after its durable fold. */
|
|
91
|
-
refreshTaskRuntimeDescriptor(input) {
|
|
92
|
-
const task = this.store.getTask(input.taskId);
|
|
93
|
-
const role = this.store.getRole(input.taskId, input.roleName);
|
|
94
|
-
if (task === null
|
|
95
|
-
|| task.status !== "active"
|
|
96
|
-
|| task.executionGate.state !== "enabled"
|
|
97
|
-
|| role === null) {
|
|
98
|
-
throw new Error(`Task runtime is not current: ${input.taskId}/${input.roleName}.`);
|
|
99
|
-
}
|
|
100
|
-
const run = this.store.getActiveTurn(input.taskId, input.roleName);
|
|
101
|
-
const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
102
|
-
const session = sessions?.sessions[role.activeAgentId];
|
|
103
|
-
if (session === undefined || session.launchId === undefined) {
|
|
104
|
-
throw new Error(`Task runtime native Session is not ready: ${input.taskId}/${input.roleName}.`);
|
|
105
|
-
}
|
|
106
|
-
const effective = run?.effective ?? session.effective;
|
|
107
|
-
const descriptor = createExactTaskRuntimeDescriptor({
|
|
108
|
-
controlPlaneDigest: exactControlPlaneDigest(this.#controlPlane),
|
|
109
|
-
taskId: input.taskId,
|
|
110
|
-
roleName: input.roleName,
|
|
111
|
-
agentId: session.agentId,
|
|
112
|
-
adapterId: session.adapterId,
|
|
113
|
-
workspace: effective.workspace.root,
|
|
114
|
-
...(run === null ? {} : { turnId: run.id }),
|
|
115
|
-
launchId: session.launchId,
|
|
116
|
-
nativeSessionId: session.nativeSessionId
|
|
117
|
-
});
|
|
118
|
-
if (descriptor.turnId !== input.turnId
|
|
119
|
-
|| descriptor.launchId !== input.launchId
|
|
120
|
-
|| descriptor.nativeSessionId !== input.nativeSessionId
|
|
121
|
-
|| descriptor.agentId !== input.agentId
|
|
122
|
-
|| descriptor.adapterId !== input.adapterId
|
|
123
|
-
|| descriptor.workspace !== canonicalPath(input.workspace)) {
|
|
124
|
-
throw new Error("Prepared Task runtime generation is not current.");
|
|
125
|
-
}
|
|
126
|
-
assertExactTaskRuntimeState(descriptor, this.store);
|
|
127
|
-
// Publish only the current-control source. A reused native pane keeps its
|
|
128
|
-
// own stable source path; its Hook self-refreshes that source before the
|
|
129
|
-
// volatile fence instead of the Controller scanning history to find it.
|
|
130
|
-
const currentSource = exactTaskRuntimeDescriptorPath(this.home, descriptor);
|
|
131
|
-
this.#writeExactTaskRuntimeDescriptor(descriptor, currentSource);
|
|
132
|
-
}
|
|
133
90
|
plan(input) {
|
|
134
91
|
const task = this.store.getTask(input.taskId);
|
|
135
92
|
if (task === null)
|
|
@@ -257,7 +214,7 @@ export class FileRoleLaunchPlanner {
|
|
|
257
214
|
const launchRole = effectiveRoleForLaunch(role, effective);
|
|
258
215
|
const binding = activeRoleAgentBinding(launchRole);
|
|
259
216
|
if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
|
|
260
|
-
throw new Error(`Role
|
|
217
|
+
throw new Error(`Role runtime generation identity changed: ${role.name}.`);
|
|
261
218
|
}
|
|
262
219
|
const configured = this.store.getConfiguredAgent(input.agentId);
|
|
263
220
|
if (configured === null)
|
|
@@ -297,7 +254,7 @@ export class FileRoleLaunchPlanner {
|
|
|
297
254
|
if (runtimeIsolation !== undefined && (owner.scope !== "task"
|
|
298
255
|
|| runtimeIsolation.taskId !== owner.taskId
|
|
299
256
|
|| runtimeIsolation.workspace.root !== effectiveWorkspace
|
|
300
|
-
|| runtimeIsolation.generation.
|
|
257
|
+
|| runtimeIsolation.generation.runtimeGenerationId !== input.runtimeGenerationId)) {
|
|
301
258
|
throw new Error("Role launch does not match its Task runtime isolation descriptor.");
|
|
302
259
|
}
|
|
303
260
|
Object.assign(launchEnvironment, runtimeIsolation === undefined
|
|
@@ -374,9 +331,9 @@ export class FileRoleLaunchPlanner {
|
|
|
374
331
|
: {};
|
|
375
332
|
const preallocatedNativeSessionId = binding.adapterId === "claude"
|
|
376
333
|
&& resumeNativeSessionId === undefined
|
|
377
|
-
? requireText(input.
|
|
334
|
+
? requireText(input.runtimeGenerationId === undefined
|
|
378
335
|
? this.#createNativeSessionId()
|
|
379
|
-
: nativeSessionIdForLaunch(this.home, input.
|
|
336
|
+
: nativeSessionIdForLaunch(this.home, input.runtimeGenerationId, input.agentId, input.adapterId), "Native session id")
|
|
380
337
|
: resumeNativeSessionId;
|
|
381
338
|
const managedCompiled = managedControl
|
|
382
339
|
? adapter.compileManagedControl(compileInput, launchMode, preallocatedNativeSessionId)
|
|
@@ -443,27 +400,6 @@ export class FileRoleLaunchPlanner {
|
|
|
443
400
|
if (managedClaudeRun && (managedRun === null || managedRun.status !== "active")) {
|
|
444
401
|
throw new Error(`Managed Claude Turn is no longer active: ${input.turnId}.`);
|
|
445
402
|
}
|
|
446
|
-
const runtimeDescriptor = owner.scope === "task"
|
|
447
|
-
? createExactTaskRuntimeDescriptor({
|
|
448
|
-
controlPlaneDigest: exactControlPlaneDigest(this.#controlPlane),
|
|
449
|
-
taskId: owner.taskId,
|
|
450
|
-
roleName: role.name,
|
|
451
|
-
agentId: configured.id,
|
|
452
|
-
adapterId: configured.adapterId,
|
|
453
|
-
workspace: effectiveWorkspace,
|
|
454
|
-
...(input.turnId === undefined ? {} : { turnId: input.turnId }),
|
|
455
|
-
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
456
|
-
...(session === null
|
|
457
|
-
? {}
|
|
458
|
-
: { nativeSessionId: session.nativeSessionId })
|
|
459
|
-
})
|
|
460
|
-
: undefined;
|
|
461
|
-
const runtimeDescriptorSource = runtimeDescriptor === undefined
|
|
462
|
-
? undefined
|
|
463
|
-
: exactTaskRuntimeDescriptorPath(this.home, runtimeDescriptor);
|
|
464
|
-
if (runtimeDescriptor !== undefined && runtimeDescriptorSource !== undefined) {
|
|
465
|
-
this.#writeExactTaskRuntimeDescriptor(runtimeDescriptor, runtimeDescriptorSource);
|
|
466
|
-
}
|
|
467
403
|
// Generate a candidate per-Provider-process DurableJob caller key for
|
|
468
404
|
// every task-scope launch. A reused live Conversation keeps the key that
|
|
469
405
|
// its process inherited; a new Host or a Conversation replacement commits
|
|
@@ -479,7 +415,7 @@ export class FileRoleLaunchPlanner {
|
|
|
479
415
|
throw new Error(`Managed Turn is no longer active: ${input.turnId}.`);
|
|
480
416
|
}
|
|
481
417
|
const providerAuthority = managedControl
|
|
482
|
-
? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.
|
|
418
|
+
? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.runtimeGenerationId, input.mode)
|
|
483
419
|
: undefined;
|
|
484
420
|
const providerOwnedTurn = managedControl && input.mode === "resume"
|
|
485
421
|
? this.#providerOwnedTurnForLaunch(owner.taskId, role.name, input.turnId)
|
|
@@ -537,11 +473,10 @@ export class FileRoleLaunchPlanner {
|
|
|
537
473
|
YUI_SESSION_MANIFEST: sessionContext.sessionManifestPath,
|
|
538
474
|
YUI_SESSION_CLI: sessionContext.sessionCliPath,
|
|
539
475
|
...(jobCallerKey === undefined ? {} : { YUI_JOB_CALLER_KEY: jobCallerKey }),
|
|
540
|
-
...(
|
|
476
|
+
...(owner.scope !== "task"
|
|
541
477
|
? {}
|
|
542
478
|
: {
|
|
543
|
-
[YUI_CONTROL_PLANE_DESCRIPTOR]: serializeExactDescriptor(this.#controlPlane)
|
|
544
|
-
[YUI_TASK_RUNTIME_DESCRIPTOR]: runtimeDescriptorSource
|
|
479
|
+
[YUI_CONTROL_PLANE_DESCRIPTOR]: serializeExactDescriptor(this.#controlPlane)
|
|
545
480
|
}),
|
|
546
481
|
...(sessionTitle === undefined
|
|
547
482
|
? {}
|
|
@@ -554,10 +489,13 @@ export class FileRoleLaunchPlanner {
|
|
|
554
489
|
}
|
|
555
490
|
: {})
|
|
556
491
|
}),
|
|
557
|
-
...(input.
|
|
492
|
+
...(input.runtimeGenerationId === undefined
|
|
558
493
|
? {}
|
|
559
|
-
: {
|
|
560
|
-
|
|
494
|
+
: { YUI_RUNTIME_GENERATION_ID: input.runtimeGenerationId }),
|
|
495
|
+
// No Turn is exported into the Session environment. A native pane
|
|
496
|
+
// outlives its Turn, so a Turn id frozen here would be stale for every
|
|
497
|
+
// later Turn. The Agent Host sets it explicitly per spawned Provider
|
|
498
|
+
// Turn, and every durable decision reads the active Turn from state.
|
|
561
499
|
...(session === null
|
|
562
500
|
? {}
|
|
563
501
|
: { YUI_NATIVE_SESSION_ID: session.nativeSessionId })
|
|
@@ -567,7 +505,13 @@ export class FileRoleLaunchPlanner {
|
|
|
567
505
|
const scopedLaunch = owner.scope === "task"
|
|
568
506
|
? this.#applyWorkspaceScope(owner.taskId, role, launch, workspaceOverride)
|
|
569
507
|
: launch;
|
|
570
|
-
const
|
|
508
|
+
const sharedDaemonLaunch = owner.scope === "global" && configured.adapterId === "codex"
|
|
509
|
+
? {
|
|
510
|
+
...scopedLaunch,
|
|
511
|
+
args: addCodexSharedDaemonRemote(scopedLaunch.args, launchMode, scopedLaunch.env)
|
|
512
|
+
}
|
|
513
|
+
: scopedLaunch;
|
|
514
|
+
const ordinaryConversationLaunch = withCodexThreadEnvironment(sharedDaemonLaunch);
|
|
571
515
|
return {
|
|
572
516
|
role: {
|
|
573
517
|
name: role.name,
|
|
@@ -579,8 +523,8 @@ export class FileRoleLaunchPlanner {
|
|
|
579
523
|
...(sessionTitle === undefined ? {} : { sessionTitle })
|
|
580
524
|
};
|
|
581
525
|
}
|
|
582
|
-
#providerAuthorityForLaunch(taskId, roleName,
|
|
583
|
-
const activationId = requireText(
|
|
526
|
+
#providerAuthorityForLaunch(taskId, roleName, runtimeGenerationId, mode) {
|
|
527
|
+
const activationId = requireText(runtimeGenerationId, "Managed Provider Activation id");
|
|
584
528
|
const binding = this.store.getTaskRoleSessionSet(taskId, roleName)?.providerBinding;
|
|
585
529
|
if (binding === null || binding === undefined) {
|
|
586
530
|
return { epoch: 1, owner: "controller", holderId: activationId };
|
|
@@ -647,10 +591,6 @@ export class FileRoleLaunchPlanner {
|
|
|
647
591
|
}
|
|
648
592
|
};
|
|
649
593
|
}
|
|
650
|
-
#writeExactTaskRuntimeDescriptor(descriptor, sourcePath) {
|
|
651
|
-
writeTextFileAtomically(sourcePath, `${serializeExactDescriptor(descriptor)}\n`);
|
|
652
|
-
this.#resourceRegistrar().registerExactTaskRuntimeDescriptor(descriptor, sourcePath);
|
|
653
|
-
}
|
|
654
594
|
#selectConfiguredAgentEnvironment(source) {
|
|
655
595
|
const selected = {};
|
|
656
596
|
for (const agent of this.store.listConfiguredAgents()) {
|
|
@@ -830,6 +770,34 @@ function addCodexSessionNotify(args, mode, cliPath) {
|
|
|
830
770
|
}
|
|
831
771
|
return [...args.slice(0, -2), ...managed, ...args.slice(-2)];
|
|
832
772
|
}
|
|
773
|
+
/**
|
|
774
|
+
* Global Codex keeps its native TUI, but the TUI is only a client attachment
|
|
775
|
+
* to the user's shared App Server. The daemon owns the Thread and its writer,
|
|
776
|
+
* so Desktop can open the same Yui-created Thread without a second rollout
|
|
777
|
+
* writer or a Yui-specific takeover.
|
|
778
|
+
*/
|
|
779
|
+
export function addCodexSharedDaemonRemote(args, mode, environment = {}) {
|
|
780
|
+
const sessionEnvironment = Object.fromEntries(Object.entries(environment).filter(([name]) => name.startsWith("YUI_")));
|
|
781
|
+
const remote = [
|
|
782
|
+
"--remote",
|
|
783
|
+
"unix://",
|
|
784
|
+
...(Object.keys(sessionEnvironment).length === 0
|
|
785
|
+
? []
|
|
786
|
+
: ["--config", codexShellEnvironmentConfig(sessionEnvironment)])
|
|
787
|
+
];
|
|
788
|
+
if (mode === "new")
|
|
789
|
+
return [...args, ...remote];
|
|
790
|
+
if (args.length < 2 || args.at(-2) !== "resume") {
|
|
791
|
+
throw new Error("Codex resume launch shape is invalid.");
|
|
792
|
+
}
|
|
793
|
+
return [...args.slice(0, -2), ...remote, ...args.slice(-2)];
|
|
794
|
+
}
|
|
795
|
+
function codexShellEnvironmentConfig(environment) {
|
|
796
|
+
const entries = Object.entries(environment)
|
|
797
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
798
|
+
.map(([name, value]) => `${JSON.stringify(name)}=${JSON.stringify(value)}`);
|
|
799
|
+
return `shell_environment_policy.set={${entries.join(",")}}`;
|
|
800
|
+
}
|
|
833
801
|
/**
|
|
834
802
|
* Codex 0.145 discovers lifecycle hooks from its effective config. Keep Yui's
|
|
835
803
|
* two handlers invocation-local: this avoids mutating CODEX_HOME or the Task
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createExecFileCommandRunner, createPinnedCommandRunner } from "./pinnedCommandRunner.js";
|
|
2
|
+
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
3
|
+
const GIT_COMMIT = /^[0-9a-f]{40}$/iu;
|
|
4
|
+
const GITHUB_REPOSITORY = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u;
|
|
5
|
+
const MAX_FAILURE_DETAIL_CHARS = 4_000;
|
|
6
|
+
const PULL_REQUEST_NUMBER = /^[1-9][0-9]*$/u;
|
|
7
|
+
export function createGitHubCliPublicationVerifier(options = {}) {
|
|
8
|
+
const run = options.runCommand ?? createPinnedCommandRunner(createExecFileCommandRunner(), ["gh"], options.environmentPath);
|
|
9
|
+
return {
|
|
10
|
+
inspect: (input) => inspectGitHubPullRequest(run, input)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function inspectGitHubPullRequest(run, input) {
|
|
14
|
+
if (input.provider !== "github" || input.externalKind !== "pull-request") {
|
|
15
|
+
throw new Error(`GitHub verifier cannot inspect ${input.provider}/${input.externalKind}.`);
|
|
16
|
+
}
|
|
17
|
+
if (!GITHUB_REPOSITORY.test(input.repository)) {
|
|
18
|
+
throw new Error(`GitHub repository must use owner/name: ${input.repository}.`);
|
|
19
|
+
}
|
|
20
|
+
if (!PULL_REQUEST_NUMBER.test(input.externalId)) {
|
|
21
|
+
throw new Error(`GitHub pull request id must be numeric: ${input.externalId}.`);
|
|
22
|
+
}
|
|
23
|
+
const result = await run("gh", [
|
|
24
|
+
"pr", "view", input.externalId,
|
|
25
|
+
"--repo", input.repository,
|
|
26
|
+
"--json", "number,state,headRefOid,mergeCommit,mergedAt,url"
|
|
27
|
+
]);
|
|
28
|
+
if (result.code !== 0) {
|
|
29
|
+
if (result.code === 127) {
|
|
30
|
+
throw new Error("GitHub verification requires a trusted gh executable on PATH.");
|
|
31
|
+
}
|
|
32
|
+
const detail = boundedFailureDetail(redactLaunchText(result.stderr || result.stdout).trim());
|
|
33
|
+
throw new Error(`gh pr view failed for ${input.repository}#${input.externalId} `
|
|
34
|
+
+ `(exit ${result.code})${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
35
|
+
}
|
|
36
|
+
return parseGitHubPullRequest(result.stdout, input.repository, input.externalId);
|
|
37
|
+
}
|
|
38
|
+
export function parseGitHubPullRequest(stdout, repository, externalId) {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(stdout.trim());
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error("gh pr view returned invalid JSON.");
|
|
45
|
+
}
|
|
46
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
47
|
+
throw new Error("gh pr view returned an invalid pull request object.");
|
|
48
|
+
}
|
|
49
|
+
const record = parsed;
|
|
50
|
+
const number = typeof record.number === "number"
|
|
51
|
+
? String(record.number)
|
|
52
|
+
: typeof record.number === "string" ? record.number : undefined;
|
|
53
|
+
if (number !== externalId) {
|
|
54
|
+
throw new Error(`gh pr view returned pull request ${number ?? "unknown"}, expected ${externalId}.`);
|
|
55
|
+
}
|
|
56
|
+
const state = githubState(record.state);
|
|
57
|
+
const headCommit = gitCommit(record.headRefOid, "GitHub PR head commit");
|
|
58
|
+
const remoteCommit = mergeCommit(record.mergeCommit);
|
|
59
|
+
if (state === "merged" && remoteCommit === undefined) {
|
|
60
|
+
throw new Error("Merged GitHub PR did not expose a merge commit.");
|
|
61
|
+
}
|
|
62
|
+
const externalUrl = optionalText(record.url);
|
|
63
|
+
const mergedAt = optionalText(record.mergedAt);
|
|
64
|
+
return {
|
|
65
|
+
provider: "github",
|
|
66
|
+
repository,
|
|
67
|
+
externalKind: "pull-request",
|
|
68
|
+
externalId,
|
|
69
|
+
...(externalUrl === undefined ? {} : { externalUrl }),
|
|
70
|
+
state,
|
|
71
|
+
headCommit,
|
|
72
|
+
...(remoteCommit === undefined ? {} : { remoteCommit }),
|
|
73
|
+
...(mergedAt === undefined ? {} : { mergedAt }),
|
|
74
|
+
evidence: `gh pr view ${repository}#${externalId}: state=${state}; `
|
|
75
|
+
+ `head=${headCommit}; remote=${remoteCommit ?? "none"}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function githubState(value) {
|
|
79
|
+
if (typeof value !== "string") {
|
|
80
|
+
throw new Error("GitHub PR state is unavailable.");
|
|
81
|
+
}
|
|
82
|
+
switch (value.toUpperCase()) {
|
|
83
|
+
case "OPEN": return "open";
|
|
84
|
+
case "MERGED": return "merged";
|
|
85
|
+
case "CLOSED": return "closed";
|
|
86
|
+
default: throw new Error(`GitHub PR state is unsupported: ${value}.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function mergeCommit(value) {
|
|
90
|
+
if (value === null || value === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (typeof value === "string")
|
|
93
|
+
return gitCommit(value, "GitHub merge commit");
|
|
94
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
95
|
+
throw new Error("GitHub merge commit is invalid.");
|
|
96
|
+
}
|
|
97
|
+
return gitCommit(value.oid, "GitHub merge commit");
|
|
98
|
+
}
|
|
99
|
+
function gitCommit(value, label) {
|
|
100
|
+
if (typeof value !== "string" || !GIT_COMMIT.test(value)) {
|
|
101
|
+
throw new Error(`${label} must be a full 40-character Git SHA.`);
|
|
102
|
+
}
|
|
103
|
+
return value.toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
function optionalText(value) {
|
|
106
|
+
return typeof value === "string" && value.trim().length > 0
|
|
107
|
+
? value.trim()
|
|
108
|
+
: undefined;
|
|
109
|
+
}
|
|
110
|
+
function boundedFailureDetail(value) {
|
|
111
|
+
return value.length <= MAX_FAILURE_DETAIL_CHARS
|
|
112
|
+
? value
|
|
113
|
+
: `${value.slice(0, MAX_FAILURE_DETAIL_CHARS)}…`;
|
|
114
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createExecFileCommandRunner, createPinnedCommandRunner } from "./pinnedCommandRunner.js";
|
|
2
|
+
import { redactLaunchText } from "../runtime/launchDiagnostics.js";
|
|
3
|
+
const GIT_COMMIT = /^[0-9a-f]{40}$/iu;
|
|
4
|
+
const GITLAB_REPOSITORY = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/u;
|
|
5
|
+
const MAX_FAILURE_DETAIL_CHARS = 4_000;
|
|
6
|
+
const MERGE_REQUEST_IID = /^[1-9][0-9]*$/u;
|
|
7
|
+
export function createGitLabCliPublicationVerifier(options = {}) {
|
|
8
|
+
const run = options.runCommand ?? createPinnedCommandRunner(createExecFileCommandRunner(), ["glab"], options.environmentPath);
|
|
9
|
+
return {
|
|
10
|
+
inspect: (input) => inspectGitLabMergeRequest(run, input)
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
async function inspectGitLabMergeRequest(run, input) {
|
|
14
|
+
if (input.provider !== "gitlab" || input.externalKind !== "merge-request") {
|
|
15
|
+
throw new Error(`GitLab verifier cannot inspect ${input.provider}/${input.externalKind}.`);
|
|
16
|
+
}
|
|
17
|
+
if (!GITLAB_REPOSITORY.test(input.repository)) {
|
|
18
|
+
throw new Error(`GitLab repository must use namespace/project: ${input.repository}.`);
|
|
19
|
+
}
|
|
20
|
+
if (!MERGE_REQUEST_IID.test(input.externalId)) {
|
|
21
|
+
throw new Error(`GitLab merge request id must be numeric: ${input.externalId}.`);
|
|
22
|
+
}
|
|
23
|
+
const hostname = gitLabHostname(input.externalUrl);
|
|
24
|
+
const endpoint = `projects/${encodeURIComponent(input.repository)}`
|
|
25
|
+
+ `/merge_requests/${input.externalId}`;
|
|
26
|
+
const result = await run("glab", [
|
|
27
|
+
"api",
|
|
28
|
+
endpoint,
|
|
29
|
+
...(hostname === undefined ? [] : ["--hostname", hostname])
|
|
30
|
+
]);
|
|
31
|
+
if (result.code !== 0) {
|
|
32
|
+
if (result.code === 127) {
|
|
33
|
+
throw new Error("GitLab verification requires a trusted glab executable on PATH.");
|
|
34
|
+
}
|
|
35
|
+
const detail = boundedFailureDetail(redactLaunchText(result.stderr || result.stdout).trim());
|
|
36
|
+
throw new Error(`glab api failed for ${input.repository}!${input.externalId} `
|
|
37
|
+
+ `(exit ${result.code})${detail.length === 0 ? "" : `: ${detail}`}`);
|
|
38
|
+
}
|
|
39
|
+
return parseGitLabMergeRequest(result.stdout, input.repository, input.externalId, hostname ?? "gitlab.com");
|
|
40
|
+
}
|
|
41
|
+
export function parseGitLabMergeRequest(stdout, repository, externalId, expectedHost) {
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(stdout.trim());
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error("glab api returned invalid JSON.");
|
|
48
|
+
}
|
|
49
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
50
|
+
throw new Error("glab api returned an invalid merge request object.");
|
|
51
|
+
}
|
|
52
|
+
const record = parsed;
|
|
53
|
+
const iid = typeof record.iid === "number"
|
|
54
|
+
? String(record.iid)
|
|
55
|
+
: typeof record.iid === "string" ? record.iid : undefined;
|
|
56
|
+
if (iid !== externalId) {
|
|
57
|
+
throw new Error(`glab api returned merge request ${iid ?? "unknown"}, expected ${externalId}.`);
|
|
58
|
+
}
|
|
59
|
+
const state = gitLabState(record.state);
|
|
60
|
+
const headCommit = gitCommit(record.sha, "GitLab MR head commit");
|
|
61
|
+
const remoteCommit = firstCommit(record.merge_commit_sha, record.squash_commit_sha);
|
|
62
|
+
if (state === "merged" && remoteCommit === undefined) {
|
|
63
|
+
throw new Error("Merged GitLab MR did not expose an integrated commit.");
|
|
64
|
+
}
|
|
65
|
+
const externalUrl = optionalText(record.web_url);
|
|
66
|
+
assertGitLabReference(record.references, repository, externalId);
|
|
67
|
+
if (externalUrl !== undefined && expectedHost !== undefined) {
|
|
68
|
+
assertGitLabWebUrl(externalUrl, expectedHost, repository, externalId);
|
|
69
|
+
}
|
|
70
|
+
const mergedAt = optionalText(record.merged_at);
|
|
71
|
+
return {
|
|
72
|
+
provider: "gitlab",
|
|
73
|
+
repository,
|
|
74
|
+
externalKind: "merge-request",
|
|
75
|
+
externalId,
|
|
76
|
+
...(externalUrl === undefined ? {} : { externalUrl }),
|
|
77
|
+
state,
|
|
78
|
+
headCommit,
|
|
79
|
+
...(remoteCommit === undefined ? {} : { remoteCommit }),
|
|
80
|
+
...(mergedAt === undefined ? {} : { mergedAt }),
|
|
81
|
+
evidence: `glab api ${repository}!${externalId}: state=${state}; `
|
|
82
|
+
+ `head=${headCommit}; remote=${remoteCommit ?? "none"}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function gitLabState(value) {
|
|
86
|
+
if (typeof value !== "string")
|
|
87
|
+
throw new Error("GitLab MR state is unavailable.");
|
|
88
|
+
switch (value.toLowerCase()) {
|
|
89
|
+
case "opened": return "open";
|
|
90
|
+
case "merged": return "merged";
|
|
91
|
+
case "closed": return "closed";
|
|
92
|
+
default: throw new Error(`GitLab MR state is unsupported: ${value}.`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function assertGitLabReference(value, repository, externalId) {
|
|
96
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
97
|
+
throw new Error("GitLab MR repository reference is unavailable.");
|
|
98
|
+
}
|
|
99
|
+
const full = optionalText(value.full);
|
|
100
|
+
if (full !== `${repository}!${externalId}`) {
|
|
101
|
+
throw new Error(`GitLab MR repository reference ${full ?? "unknown"} does not match `
|
|
102
|
+
+ `${repository}!${externalId}.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function assertGitLabWebUrl(value, expectedHost, repository, externalId) {
|
|
106
|
+
let url;
|
|
107
|
+
try {
|
|
108
|
+
url = new URL(value);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
throw new Error("GitLab MR web URL is invalid.");
|
|
112
|
+
}
|
|
113
|
+
const expectedSuffix = `/${repository}/-/merge_requests/${externalId}`;
|
|
114
|
+
if ((url.protocol !== "https:" && url.protocol !== "http:")
|
|
115
|
+
|| url.host.toLowerCase() !== expectedHost.toLowerCase()
|
|
116
|
+
|| !url.pathname.endsWith(expectedSuffix)) {
|
|
117
|
+
throw new Error(`GitLab MR web URL does not match ${expectedHost}/${repository}!${externalId}.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function firstCommit(...values) {
|
|
121
|
+
for (const value of values) {
|
|
122
|
+
if (value === null || value === undefined || value === "")
|
|
123
|
+
continue;
|
|
124
|
+
return gitCommit(value, "GitLab integrated commit");
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
function gitCommit(value, label) {
|
|
129
|
+
if (typeof value !== "string" || !GIT_COMMIT.test(value)) {
|
|
130
|
+
throw new Error(`${label} must be a full 40-character Git SHA.`);
|
|
131
|
+
}
|
|
132
|
+
return value.toLowerCase();
|
|
133
|
+
}
|
|
134
|
+
function optionalText(value) {
|
|
135
|
+
return typeof value === "string" && value.trim().length > 0
|
|
136
|
+
? value.trim()
|
|
137
|
+
: undefined;
|
|
138
|
+
}
|
|
139
|
+
function gitLabHostname(externalUrl) {
|
|
140
|
+
if (externalUrl === undefined)
|
|
141
|
+
return undefined;
|
|
142
|
+
let url;
|
|
143
|
+
try {
|
|
144
|
+
url = new URL(externalUrl);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new Error(`GitLab external URL is invalid: ${externalUrl}.`);
|
|
148
|
+
}
|
|
149
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
150
|
+
throw new Error(`GitLab external URL protocol is invalid: ${url.protocol}.`);
|
|
151
|
+
}
|
|
152
|
+
return url.hostname === "gitlab.com" ? undefined : url.host;
|
|
153
|
+
}
|
|
154
|
+
function boundedFailureDetail(value) {
|
|
155
|
+
return value.length <= MAX_FAILURE_DETAIL_CHARS
|
|
156
|
+
? value
|
|
157
|
+
: `${value.slice(0, MAX_FAILURE_DETAIL_CHARS)}…`;
|
|
158
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { accessSync, constants } from "node:fs";
|
|
3
|
+
import { delimiter, isAbsolute, resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
export function createExecFileCommandRunner() {
|
|
6
|
+
const exec = promisify(execFile);
|
|
7
|
+
return async (command, args, cwd, env) => {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout, stderr } = await exec(command, args, {
|
|
10
|
+
cwd,
|
|
11
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
12
|
+
...(env === undefined ? {} : { env: { ...process.env, ...env } })
|
|
13
|
+
});
|
|
14
|
+
return { code: 0, stdout, stderr };
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const failure = error;
|
|
18
|
+
return {
|
|
19
|
+
code: typeof failure.code === "number" ? failure.code : 1,
|
|
20
|
+
stdout: failure.stdout ?? "",
|
|
21
|
+
stderr: failure.stderr ?? failure.message ?? String(error)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve an external command once through the supplied PATH. Callers pin the
|
|
28
|
+
* result for the lifetime of an adapter so a later PATH change cannot swap the
|
|
29
|
+
* executable that supplies an external fact or effect.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveExecutable(command, environmentPath) {
|
|
32
|
+
if (isAbsolute(command))
|
|
33
|
+
return command;
|
|
34
|
+
for (const directory of (environmentPath ?? "").split(delimiter)) {
|
|
35
|
+
if (directory.length === 0)
|
|
36
|
+
continue;
|
|
37
|
+
const candidate = resolve(directory, command);
|
|
38
|
+
try {
|
|
39
|
+
accessSync(candidate, constants.X_OK);
|
|
40
|
+
return candidate;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Keep walking PATH.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
export function createPinnedCommandRunner(base, commands, environmentPath = process.env.PATH) {
|
|
49
|
+
const pinned = new Map();
|
|
50
|
+
for (const command of commands) {
|
|
51
|
+
pinned.set(command, resolveExecutable(command, environmentPath));
|
|
52
|
+
}
|
|
53
|
+
return (command, args, cwd, env) => {
|
|
54
|
+
let resolved;
|
|
55
|
+
if (pinned.has(command)) {
|
|
56
|
+
resolved = pinned.get(command);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
resolved = isAbsolute(command)
|
|
60
|
+
? command
|
|
61
|
+
: resolveExecutable(command, environmentPath);
|
|
62
|
+
pinned.set(command, resolved);
|
|
63
|
+
}
|
|
64
|
+
if (resolved === undefined) {
|
|
65
|
+
return Promise.resolve({
|
|
66
|
+
code: 127,
|
|
67
|
+
stdout: "",
|
|
68
|
+
stderr: `Unable to resolve trusted executable: ${command}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return base(resolved, args, cwd, env);
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -15,7 +15,6 @@ export function createChangeSetManifest(input) {
|
|
|
15
15
|
schemaVersion: CHANGE_SET_MANIFEST_SCHEMA_VERSION,
|
|
16
16
|
tags: [...input.tags],
|
|
17
17
|
deletedPaths: [...input.deletedPaths],
|
|
18
|
-
evidenceRefs: [...(input.evidenceRefs ?? [])],
|
|
19
18
|
...(input.targetRef === undefined ? {} : { targetRef: input.targetRef })
|
|
20
19
|
});
|
|
21
20
|
}
|
|
@@ -33,12 +32,10 @@ export function validateChangeSetManifest(manifest) {
|
|
|
33
32
|
}
|
|
34
33
|
const tags = normalizedUniqueText(manifest.tags, "ChangeSet manifest tag");
|
|
35
34
|
const deletedPaths = normalizedUniqueText(manifest.deletedPaths, "ChangeSet manifest deleted path");
|
|
36
|
-
const evidenceRefs = normalizedUniqueText(manifest.evidenceRefs, "ChangeSet manifest evidence reference");
|
|
37
35
|
return {
|
|
38
36
|
schemaVersion: CHANGE_SET_MANIFEST_SCHEMA_VERSION,
|
|
39
37
|
tags,
|
|
40
38
|
deletedPaths,
|
|
41
|
-
evidenceRefs,
|
|
42
39
|
...(manifest.targetRef === undefined ? {} : {
|
|
43
40
|
targetRef: requireText(manifest.targetRef, "ChangeSet manifest target ref")
|
|
44
41
|
})
|
|
@@ -1,4 +1,38 @@
|
|
|
1
1
|
import { governingWorkItemCandidate } from "../workItem/workItem.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the exact Git boundary produced by the Candidate that currently
|
|
4
|
+
* governs a WorkItem. Read-only context Projects are not delivery sources.
|
|
5
|
+
*/
|
|
6
|
+
export function governingWorkItemDeliveries(workItems) {
|
|
7
|
+
return workItems.flatMap((item) => {
|
|
8
|
+
const candidate = governingWorkItemCandidate(item);
|
|
9
|
+
if (candidate?.workspace === undefined || candidate.gitSnapshot === undefined)
|
|
10
|
+
return [];
|
|
11
|
+
const results = new Map(candidate.gitSnapshot.projects.map(({ projectId, commit }) => [projectId, commit]));
|
|
12
|
+
return candidate.workspace.entries
|
|
13
|
+
.filter(({ access }) => access === "write")
|
|
14
|
+
.map((entry) => {
|
|
15
|
+
const resultCommit = results.get(entry.projectId);
|
|
16
|
+
if (resultCommit === undefined) {
|
|
17
|
+
throw new Error(`WorkItem Candidate result is missing Project ${item.id}/${entry.projectId}.`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
workItemId: item.id,
|
|
21
|
+
projectId: entry.projectId,
|
|
22
|
+
startCommit: entry.baseCommit,
|
|
23
|
+
resultCommit
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function workItemDeliverySettled(delivery, integrations) {
|
|
29
|
+
return integrations.some((attempt) => (attempt.status === "committed"
|
|
30
|
+
&& attempt.projectId === delivery.projectId
|
|
31
|
+
&& attempt.source.kind === "work-item"
|
|
32
|
+
&& attempt.source.workItemId === delivery.workItemId
|
|
33
|
+
&& attempt.source.startCommit === delivery.startCommit
|
|
34
|
+
&& attempt.source.resultCommit === delivery.resultCommit));
|
|
35
|
+
}
|
|
2
36
|
/**
|
|
3
37
|
* Delivery obligations follow the Candidate that currently governs each
|
|
4
38
|
* WorkItem. Older Candidates and their ChangeSets remain audit evidence, but
|
|
@@ -26,7 +60,11 @@ export function governingChangeSets(workItems, changeSets) {
|
|
|
26
60
|
return [...selected.values()].sort(compareChangeSets);
|
|
27
61
|
}
|
|
28
62
|
export function changeSetDeliverySettled(changeSet, integrations, queueEntries = []) {
|
|
29
|
-
if (integrations.some((attempt) => (attempt.status === "committed"
|
|
63
|
+
if (integrations.some((attempt) => (attempt.status === "committed"
|
|
64
|
+
&& attempt.projectId === changeSet.projectId
|
|
65
|
+
&& attempt.source.kind === "work-item"
|
|
66
|
+
&& attempt.source.workItemId === changeSet.workItemId
|
|
67
|
+
&& attempt.source.resultCommit === changeSet.headCommit)))
|
|
30
68
|
return true;
|
|
31
69
|
const latestQueueEntry = queueEntries
|
|
32
70
|
.filter((entry) => entry.changeSetId === changeSet.id)
|
|
@@ -52,11 +90,10 @@ export function latestGoverningQueueEntries(changeSets, queueEntries) {
|
|
|
52
90
|
* reference has been replaced by a newer governing Candidate. Attempts that
|
|
53
91
|
* may still be writing remain blockers regardless of Candidate history.
|
|
54
92
|
*/
|
|
55
|
-
export function integrationAttemptRequiresSettlement(attempt
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return attempt.changeSetIds.some((id) => governingIds.has(id));
|
|
93
|
+
export function integrationAttemptRequiresSettlement(attempt) {
|
|
94
|
+
return attempt.status === "running"
|
|
95
|
+
|| attempt.status === "validating"
|
|
96
|
+
|| attempt.status === "blocked";
|
|
60
97
|
}
|
|
61
98
|
function candidateProjectHead(candidate, projectId) {
|
|
62
99
|
return candidate.gitSnapshot?.projects.find((project) => project.projectId === projectId)?.commit
|