@zq-silk/yui 2.3.0 → 2.4.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.
- package/README.md +15 -1
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +17 -8
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +25 -2
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +10 -1
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +25 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +13 -5
- package/dist/integration/gitIntegrationService.js +7 -12
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +48 -71
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +2 -1
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +295 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +106 -8
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
- package/docs/release-workflow.md +28 -0
- package/docs/release-workflow.zh-CN.md +21 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/dist/web/assets/client/app.js +0 -808
- package/dist/web/assets/client/components.js +0 -477
- package/dist/web/assets/client/detail.js +0 -379
- package/dist/web/assets/client/dock.js +0 -309
- package/dist/web/assets/client/evidence.js +0 -364
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1277
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -198
- package/dist/web/assets/client/sections.js +0 -349
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -216
- package/dist/web/assets/styles/views.js +0 -309
- /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
|
@@ -20,6 +20,7 @@ import { CliError } from "../errors/cliError.js";
|
|
|
20
20
|
import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
|
|
21
21
|
import { assertContextRecordReadable } from "../context/taskContext.js";
|
|
22
22
|
import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
|
|
23
|
+
import { validateHttpPreview } from "../job/httpPreview.js";
|
|
23
24
|
export function createDurableJobControl(store) {
|
|
24
25
|
const authority = createJobCallAuthority(store);
|
|
25
26
|
return {
|
|
@@ -32,6 +33,7 @@ export function createDurableJobControl(store) {
|
|
|
32
33
|
const context = authority.authenticate(Object.freeze({ ...params.caller }), params.taskId);
|
|
33
34
|
requireJobAssignment(tx, params, params.caller);
|
|
34
35
|
assertNonSecretJobInput(params);
|
|
36
|
+
validateHttpPreview(params.env, params.steps);
|
|
35
37
|
const baseKey = durableJobIdempotencyKey({
|
|
36
38
|
owner: params.owner,
|
|
37
39
|
projectId: params.projectId,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { applyTaskInputControl } from "../commands/taskCommands.js";
|
|
2
|
+
import { applyGlobalInputControl } from "../commands/globalRoleCommands.js";
|
|
3
|
+
/** The Host terminal is a human ingress, like the authenticated local Web
|
|
4
|
+
* surface. Fence that ingress before reusing the existing Message/control
|
|
5
|
+
* transaction; the Host performs the one Provider write after this returns. */
|
|
6
|
+
export function prepareNativeHostSteer(store, input) {
|
|
7
|
+
return store.transaction(tx => {
|
|
8
|
+
const set = input.taskId === undefined ? tx.getGlobalRoleSessionSet(input.roleName)
|
|
9
|
+
: tx.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
10
|
+
const session = set?.sessions[input.agentId], binding = set?.providerBinding;
|
|
11
|
+
if (!session || session.status !== "active" || session.adapterId !== "codex"
|
|
12
|
+
|| set?.activeAgentId !== input.agentId || session.nativeSessionId !== input.nativeSessionId
|
|
13
|
+
|| binding?.run?.status !== "accepted" || binding.run.nativeTurnId !== input.nativeTurnId
|
|
14
|
+
|| binding.authority.owner !== input.authorityOwner || binding.authority.epoch !== input.authorityEpoch
|
|
15
|
+
|| binding.authority.holderId !== input.holderId
|
|
16
|
+
|| (input.taskId !== undefined && input.roleName !== "leader")) {
|
|
17
|
+
throw new Error("Native steer carries a stale or unsupported Session/Turn/writer fence.");
|
|
18
|
+
}
|
|
19
|
+
if (!input.boundedText.trim() || input.boundedText.includes("\0") || Buffer.byteLength(input.boundedText) > 12000) {
|
|
20
|
+
throw new Error("Native steer exceeds its text bound.");
|
|
21
|
+
}
|
|
22
|
+
const control = { action: "steer", body: input.boundedText, to: input.roleName,
|
|
23
|
+
requestId: input.attemptId, expectedTarget: input.nativeTurnId, expectedSessionId: input.nativeSessionId };
|
|
24
|
+
if (input.taskId === undefined) {
|
|
25
|
+
applyGlobalInputControl(input.roleName, control, tx, { env: {} });
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
applyTaskInputControl(input.taskId, { ...control, to: "leader" }, tx, { environment: {}, now: () => input.now });
|
|
29
|
+
}
|
|
30
|
+
const messages = input.taskId === undefined ? tx.listGlobalRoleMessages(input.roleName) : tx.listMessages(input.taskId);
|
|
31
|
+
const message = messages.find(message => message.inputControl?.requestId === input.attemptId);
|
|
32
|
+
if (message?.control?.outcome !== "pending") {
|
|
33
|
+
throw new Error("Native steer is already settled or unconfirmed; do not resubmit.");
|
|
34
|
+
}
|
|
35
|
+
return { attemptId: message.control.receiptId, boundedText: input.boundedText };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { prepareNativeHostSteer } from "./nativeHostInput.js";
|
|
2
3
|
import { archiveLeaderTask } from "../task/leaderArchive.js";
|
|
3
4
|
import { TaskWorkspaceCoordinator } from "../repository/taskWorkspaceCoordinator.js";
|
|
4
5
|
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
@@ -33,8 +34,15 @@ import { openSchedulerTelemetry } from "../telemetry/telemetryWiring.js";
|
|
|
33
34
|
import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
|
|
34
35
|
import { TmuxManager, yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
35
36
|
import { createControllerWeb } from "../web/controllerWeb.js";
|
|
37
|
+
import { createTaskPreviews } from "../web/taskPreviews.js";
|
|
36
38
|
import { TmuxWebTerminalService } from "../web/tmuxWebTerminal.js";
|
|
37
39
|
import { createWebTaskSurface } from "../web/webTaskSurface.js";
|
|
40
|
+
import { createWebConversationSurface } from "../web/webConversation.js";
|
|
41
|
+
import { WebRequestRejected } from "../web/webMutation.js";
|
|
42
|
+
import { inspectAgentHost, sendAgentHostNativeControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
|
|
43
|
+
import { archiveOrdinaryTask } from "../task/ordinaryArchive.js";
|
|
44
|
+
import { createWebSettings } from "../web/webSettings.js";
|
|
45
|
+
import { createWebSkills } from "../web/webSkills.js";
|
|
38
46
|
import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
39
47
|
import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
|
|
40
48
|
import { createCapabilityDispatcher } from "./capabilityBridge.js";
|
|
@@ -378,10 +386,64 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
378
386
|
runningRuntime?.signal(`task:${target.taskId}`);
|
|
379
387
|
},
|
|
380
388
|
reconcileTask: (taskId) => runningRuntime?.signal(`task:${taskId}`)
|
|
381
|
-
}, yuiHome: home }
|
|
389
|
+
}, yuiHome: home }, [], undefined, {
|
|
390
|
+
archive: async (taskId) => {
|
|
391
|
+
if (!(workspacePreparer instanceof FileTaskWorkspacePreparer))
|
|
392
|
+
throw new Error("Archive workspace owner unavailable.");
|
|
393
|
+
return archiveOrdinaryTask(new TaskWorkspaceCoordinator(store, workspacePreparer, webWorkflow), taskId, { environment: {}, yuiHome: home });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
382
396
|
const surfaces = new SurfaceContributions(kernel.capabilities.registry);
|
|
397
|
+
const validateNativeWrite = (request) => {
|
|
398
|
+
const sessions = request.scope === "task" ? store.getTaskRoleSessionSet(request.taskId, request.roleName)
|
|
399
|
+
: store.getGlobalRoleSessionSet(request.roleName);
|
|
400
|
+
const selected = sessions?.sessions[sessions.activeAgentId];
|
|
401
|
+
if (!request.nativeSessionId || selected?.nativeSessionId !== request.nativeSessionId
|
|
402
|
+
|| selected.status !== "active"
|
|
403
|
+
|| (request.scope === "task" && (request.roleName !== "leader"
|
|
404
|
+
|| selected.adapterId !== "codex" || !sessions?.providerBinding
|
|
405
|
+
|| !["active", "draft"].includes(store.getTask(request.taskId)?.status ?? "")
|
|
406
|
+
|| store.getTask(request.taskId)?.executionGate.state !== "enabled"))) {
|
|
407
|
+
throw new Error("Native input requires the selected current controlled Codex Operator/Leader.");
|
|
408
|
+
}
|
|
409
|
+
};
|
|
383
410
|
const web = createControllerWeb(store, {
|
|
411
|
+
previews: () => createTaskPreviews(store, home),
|
|
412
|
+
skills: createWebSkills(store),
|
|
413
|
+
settings: createWebSettings(store, {
|
|
414
|
+
environment: options.environment ?? process.env, catalogs,
|
|
415
|
+
refreshConfiguration: () => {
|
|
416
|
+
if (!runningRuntime)
|
|
417
|
+
throw new Error("Controller runtime unavailable.");
|
|
418
|
+
runningRuntime.reloadReconciliationInterval();
|
|
419
|
+
}
|
|
420
|
+
}),
|
|
384
421
|
surface: webSurface,
|
|
422
|
+
conversation: createWebConversationSurface(store, webSurface, (owner, id) => planner.planNativeControl(owner, id), undefined, (owner) => inspectAgentHost({ home, ...owner }), {
|
|
423
|
+
hasWriter: owner => tmux.hasWritableClient(owner.scope === "task" ? owner.taskId : "operator", owner.scope === "task" ? owner.roleName : undefined),
|
|
424
|
+
respond: async (owner, id, requestId, nativeTurnId, result) => {
|
|
425
|
+
const receipt = await sendAgentHostNativeControl({
|
|
426
|
+
home, ...owner, control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "native-respond",
|
|
427
|
+
nativeSessionId: id, requestId, nativeTurnId, result }
|
|
428
|
+
});
|
|
429
|
+
if (receipt.outcome !== "accepted")
|
|
430
|
+
throw new Error(receipt.snapshot.detail ?? "Native answer was not confirmed.");
|
|
431
|
+
return { outcome: receipt.outcome };
|
|
432
|
+
},
|
|
433
|
+
setModel: async (owner, id, model) => {
|
|
434
|
+
const result = await sendAgentHostNativeControl({
|
|
435
|
+
home, ...owner, control: { protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
436
|
+
type: "native-model", nativeSessionId: id, model }
|
|
437
|
+
});
|
|
438
|
+
if (result.outcome !== "accepted") {
|
|
439
|
+
const detail = result.snapshot.detail ?? "Model adoption is unconfirmed.";
|
|
440
|
+
if (result.failure?.inputDisposition === "not-accepted")
|
|
441
|
+
throw new WebRequestRejected(detail);
|
|
442
|
+
throw new Error(detail);
|
|
443
|
+
}
|
|
444
|
+
return { outcome: result.outcome, runConfiguration: result.snapshot.runConfiguration };
|
|
445
|
+
}
|
|
446
|
+
}, catalogs),
|
|
385
447
|
answerInput: async ({ taskId, inputId, answer }) => webSurface.answer(taskId, inputId, answer),
|
|
386
448
|
panels: {
|
|
387
449
|
list: (taskId) => surfaces.listPanels(kernel.capabilities.authenticateWebQuery(taskId)),
|
|
@@ -390,6 +452,26 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
390
452
|
terminal: new TmuxWebTerminalService({
|
|
391
453
|
yuiHome: home, tmuxBin: resolveTmuxBin(store.getConfig().tmuxBin), tmux,
|
|
392
454
|
prepareGlobalRole: (roleName) => webWorkflow.prepareGlobalRoleEnter(roleName),
|
|
455
|
+
prepareNative: async (request) => {
|
|
456
|
+
if (!request.nativeSessionId)
|
|
457
|
+
return false;
|
|
458
|
+
const sessions = request.scope === "task" ? store.getTaskRoleSessionSet(request.taskId, request.roleName)
|
|
459
|
+
: store.getGlobalRoleSessionSet(request.roleName);
|
|
460
|
+
const selected = sessions?.sessions[sessions.activeAgentId];
|
|
461
|
+
if (request.scope === "task" && (request.roleName !== "leader" || selected?.adapterId !== "codex"))
|
|
462
|
+
return false;
|
|
463
|
+
validateNativeWrite(request);
|
|
464
|
+
// Preserve ordinary global native access on other adapters and the
|
|
465
|
+
// existing uncontrolled Codex TUI. No lifecycle operation on a switch.
|
|
466
|
+
if (selected?.adapterId !== "codex" || !sessions?.providerBinding)
|
|
467
|
+
return request.scope === "global";
|
|
468
|
+
const result = await sendAgentHostNativeControl({ home, ...request,
|
|
469
|
+
control: { protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "native-open", nativeSessionId: request.nativeSessionId } });
|
|
470
|
+
if (result.outcome !== "accepted" || !result.snapshot.nativeTui)
|
|
471
|
+
throw new Error("Native TUI could not be opened.");
|
|
472
|
+
return true;
|
|
473
|
+
},
|
|
474
|
+
validateWrite: validateNativeWrite,
|
|
393
475
|
environment: options.environment ?? process.env, onError: options.onError
|
|
394
476
|
})
|
|
395
477
|
});
|
|
@@ -612,6 +694,12 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
612
694
|
throw applicationError("INVALID_PARAMS", error instanceof Error ? error.message : String(error));
|
|
613
695
|
}
|
|
614
696
|
}
|
|
697
|
+
if (method === "runtime.native-steer-prepare") {
|
|
698
|
+
const value = providerTurnControlParams(params);
|
|
699
|
+
return prepareNativeHostSteer(store, { ...value,
|
|
700
|
+
nativeTurnId: requiredParam(params.nativeTurnId),
|
|
701
|
+
boundedText: requiredParam(value.boundedText) });
|
|
702
|
+
}
|
|
615
703
|
if (method === "runtime.provider-turn-begin") {
|
|
616
704
|
const value = providerTurnControlParams(params);
|
|
617
705
|
try {
|
|
@@ -927,6 +1015,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
927
1015
|
function resolveRuntimeDesiredEffective(store, request, role) {
|
|
928
1016
|
if (request.scope === "global") {
|
|
929
1017
|
return resolveEffectiveLaunch({
|
|
1018
|
+
store,
|
|
930
1019
|
role: role,
|
|
931
1020
|
purpose: "execution"
|
|
932
1021
|
});
|
|
@@ -940,6 +1029,7 @@ function resolveRuntimeDesiredEffective(store, request, role) {
|
|
|
940
1029
|
?? store.getTaskWorkspace(request.taskId)
|
|
941
1030
|
?? undefined;
|
|
942
1031
|
return resolveEffectiveLaunch({
|
|
1032
|
+
store,
|
|
943
1033
|
role: taskRole,
|
|
944
1034
|
purpose: store.getTask(request.taskId)?.status === "draft" && request.roleName === "leader"
|
|
945
1035
|
? "planning" : "execution",
|
|
@@ -1018,7 +1108,7 @@ function assertRuntimeLaunchRequestCurrent(store, request) {
|
|
|
1018
1108
|
}
|
|
1019
1109
|
function currentDesiredEffective(store, request, role) {
|
|
1020
1110
|
if (request.owner.scope === "global") {
|
|
1021
|
-
return resolveEffectiveLaunch({ role: role, purpose: "execution" });
|
|
1111
|
+
return resolveEffectiveLaunch({ store, role: role, purpose: "execution" });
|
|
1022
1112
|
}
|
|
1023
1113
|
const item = store.listWorkItems(request.owner.taskId).find((candidate) => (candidate.assignee === request.owner.roleName
|
|
1024
1114
|
&& !["accepted", "retired"].includes(candidate.status))) ?? null;
|
|
@@ -1028,6 +1118,7 @@ function currentDesiredEffective(store, request, role) {
|
|
|
1028
1118
|
?? store.getTaskWorkspace(request.owner.taskId)
|
|
1029
1119
|
?? undefined;
|
|
1030
1120
|
return resolveEffectiveLaunch({
|
|
1121
|
+
store,
|
|
1031
1122
|
role: role,
|
|
1032
1123
|
purpose: store.getTask(request.owner.taskId)?.status === "draft" && request.owner.roleName === "leader"
|
|
1033
1124
|
? "planning" : "execution",
|
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
3
|
import { closeSync, constants, existsSync, fchmodSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { createRuntimeObservation,
|
|
5
|
+
import { createRuntimeObservation, isRuntimeUsageEvidence } from "../runtime/runtimeObservation.js";
|
|
6
6
|
import { boundedRunFailureDiagnostic, transportAgentResult } from "../domain/agentResultTransport.js";
|
|
7
7
|
import { readAgentHostObservationSource } from "../runtime/agentHostProtocol.js";
|
|
8
8
|
export const MAX_RUNTIME_EVENT_FILE_BYTES = 16 * 1024 * 1024;
|
|
@@ -174,7 +174,7 @@ export class FileRuntimeEventInbox {
|
|
|
174
174
|
publish(event) {
|
|
175
175
|
this.hooks.afterAdmission?.();
|
|
176
176
|
if (event.type === "runtime-observation"
|
|
177
|
-
&&
|
|
177
|
+
&& isRuntimeUsageEvidence(event.observation)) {
|
|
178
178
|
const existing = this.list().find((candidate) => (candidate.type === "runtime-observation"
|
|
179
179
|
&& candidate.taskId === event.taskId
|
|
180
180
|
&& candidate.observation.eventId === event.observation.eventId));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRuntimeUsageEvidence } from "../runtime/runtimeObservation.js";
|
|
2
2
|
const DEFAULT_MAX_RUNTIME_EVENTS_PER_DRAIN = 64;
|
|
3
3
|
/** Folds immutable Hook facts in one bounded transaction before acknowledging them. */
|
|
4
4
|
export class FileRuntimeEventProcessor {
|
|
@@ -299,7 +299,7 @@ export function coalesceRuntimeProgress(events, instrumentation = {}) {
|
|
|
299
299
|
}
|
|
300
300
|
const representedByIndex = new Map();
|
|
301
301
|
for (const indices of indicesByStream.values()) {
|
|
302
|
-
const hasUsage =
|
|
302
|
+
const hasUsage = isRuntimeUsageEvidence(segment[indices[0]].observation);
|
|
303
303
|
// Usage facts and incomplete boundaries are authoritative history for
|
|
304
304
|
// read-only Session token projection, so ingress never coalesces them.
|
|
305
305
|
const retainedPositions = hasUsage ? indices.map((_, position) => position) : [];
|
|
@@ -403,7 +403,7 @@ function progressStreamKey(event) {
|
|
|
403
403
|
fence.nativeTurnId ?? null,
|
|
404
404
|
event.host ?? null,
|
|
405
405
|
payload.activity,
|
|
406
|
-
|
|
406
|
+
isRuntimeUsageEvidence(event.observation) ? "usage" : "signal"
|
|
407
407
|
]);
|
|
408
408
|
}
|
|
409
409
|
function isRuntimeActivityEvent(event) {
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -2,6 +2,8 @@ import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node
|
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
import { inspectPty } from "./ptyProbe.js";
|
|
5
|
+
import { gitVersionSupport } from "../repository/gitCompatibility.js";
|
|
6
|
+
import { resolveExecutable } from "../external/pinnedCommandRunner.js";
|
|
5
7
|
import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
|
|
6
8
|
import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
|
|
7
9
|
import { resolveTmuxBin } from "../config/yuiConfig.js";
|
|
@@ -684,6 +686,14 @@ function checkReviewerDispatch(_policy, role, binding, agent, launch, command, c
|
|
|
684
686
|
function checkExecutable(name, command, args, executor) {
|
|
685
687
|
try {
|
|
686
688
|
const output = firstLine(executor.run(command, args));
|
|
689
|
+
if (name === "git") {
|
|
690
|
+
const support = gitVersionSupport(output);
|
|
691
|
+
return {
|
|
692
|
+
name, status: support.supported ? "ok" : "invalid",
|
|
693
|
+
detail: `${resolveExecutable(command, process.env.PATH) ?? command}: ${support.detail}`
|
|
694
|
+
+ (support.supported ? "" : " Select a supported Git in PATH and rerun doctor; no upgrade was performed.")
|
|
695
|
+
};
|
|
696
|
+
}
|
|
687
697
|
return {
|
|
688
698
|
name,
|
|
689
699
|
status: "ok",
|
|
@@ -65,6 +65,7 @@ export function dispatchReviewSynthesis(store, taskId, reviewRoundId, sourceRunI
|
|
|
65
65
|
throw new Error(`Review main workspace is missing: ${taskId}/${round.id}.`);
|
|
66
66
|
}
|
|
67
67
|
const effective = resolveEffectiveLaunch({
|
|
68
|
+
store,
|
|
68
69
|
role,
|
|
69
70
|
purpose: "review",
|
|
70
71
|
workspace,
|
|
@@ -78,6 +78,7 @@ export function dispatchWorkItemSynthesis(store, taskId, workItemId, sourceRunId
|
|
|
78
78
|
throw new Error(`WorkItem main workspace does not match its approved scope: ${item.id}.`);
|
|
79
79
|
}
|
|
80
80
|
const effective = resolveEffectiveLaunch({
|
|
81
|
+
store,
|
|
81
82
|
role,
|
|
82
83
|
purpose: "execution",
|
|
83
84
|
workspace,
|
|
@@ -196,13 +196,15 @@ function exactTrust(directory, configured) {
|
|
|
196
196
|
return undefined;
|
|
197
197
|
}
|
|
198
198
|
function resolveRepositoryRootForTrust(workspace) {
|
|
199
|
-
const result = spawnSync("git", ["-C", workspace, "rev-parse", "--
|
|
199
|
+
const result = spawnSync("git", ["-C", workspace, "rev-parse", "--git-common-dir"], { encoding: "utf8", timeout: 1_000, windowsHide: true });
|
|
200
200
|
if (result.status !== 0 || result.error !== undefined)
|
|
201
201
|
return undefined;
|
|
202
|
-
|
|
203
|
-
if (!isAbsolute(commonDirectory) || commonDirectory.includes("\0"))
|
|
202
|
+
if (!result.stdout.endsWith("\n"))
|
|
204
203
|
return undefined;
|
|
205
|
-
const
|
|
204
|
+
const commonDirectory = result.stdout.slice(0, -1);
|
|
205
|
+
if (commonDirectory.length === 0 || commonDirectory.includes("\0"))
|
|
206
|
+
return undefined;
|
|
207
|
+
const normalized = resolve(workspace, commonDirectory);
|
|
206
208
|
return basename(normalized) === ".git" ? dirname(normalized) : undefined;
|
|
207
209
|
}
|
|
208
210
|
function inspectProjectDiscovery(contents) {
|
|
@@ -2,7 +2,8 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import { validateExecutionEnvironmentSnapshot } from "../resources/projectResource.js";
|
|
3
3
|
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
4
4
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
5
|
-
import { roleSessionKind } from "../context/roleSessionContext.js";
|
|
5
|
+
import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessionContext.js";
|
|
6
|
+
import { validateSkillPackageRef } from "../context/skillPackage.js";
|
|
6
7
|
import { isAgentAdapterId } from "../agent/adapterCatalog.js";
|
|
7
8
|
import { adapterIdForExecutionComponent, isAgentExecutionComponentId, resolveAgentExecutionComponent } from "../agent/executionComponents.js";
|
|
8
9
|
import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, sessionManifestCompatibilityDigest } from "../context/sessionProtocolIdentity.js";
|
|
@@ -15,6 +16,15 @@ export function resolveEffectiveLaunch(input) {
|
|
|
15
16
|
const workspace = snapshotWorkspace(input.role.workspace, input.workspace);
|
|
16
17
|
const writeProjectIds = effectiveWriteProjects(input, workspace);
|
|
17
18
|
const config = resolveAgentAdapter(binding.adapterId).canonicalizeConfig(clone(binding.config));
|
|
19
|
+
const home = input.store?.rootDirectory() ?? input.yuiHome;
|
|
20
|
+
const sessions = "taskId" in input.role
|
|
21
|
+
? input.store?.getTaskRoleSessionSet(input.role.taskId, input.role.name)
|
|
22
|
+
: input.store?.getGlobalRoleSessionSet(input.role.name);
|
|
23
|
+
const existing = sessions?.sessions[binding.agentId];
|
|
24
|
+
const continuing = existing?.status === "active";
|
|
25
|
+
const skillPackages = continuing ? existing.effective.skillPackages
|
|
26
|
+
: home === undefined ? undefined
|
|
27
|
+
: compileRoleSessionContext(home, input.role, "taskId" in input.role ? { scope: "task", taskId: input.role.taskId } : { scope: "global" }, { purpose: input.purpose }).skills.map(skill => skill.package);
|
|
18
28
|
return snapshotFromConfig({
|
|
19
29
|
executionAuthority: input.executionAuthority ?? (input.purpose === "planning" ? "planning" : "delivery"),
|
|
20
30
|
sourceDesiredRevision: input.role.launchRevision,
|
|
@@ -28,6 +38,7 @@ export function resolveEffectiveLaunch(input) {
|
|
|
28
38
|
? { executionEnvironment: input.role.executionEnvironment }
|
|
29
39
|
: {}),
|
|
30
40
|
context: snapshotContext(input.role),
|
|
41
|
+
...(skillPackages === undefined ? {} : { skillPackages }),
|
|
31
42
|
contextProtocolVersion: SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION,
|
|
32
43
|
sessionManifestCompatibilityDigest: sessionManifestCompatibilityDigest(input.role.name, roleSessionKind(input.role, "taskId" in input.role
|
|
33
44
|
? { scope: "task", taskId: input.role.taskId }
|
|
@@ -216,6 +227,17 @@ export function validateEffectiveLaunchSnapshot(snapshot) {
|
|
|
216
227
|
throw new Error("Effective launch Session Manifest compatibility digest is invalid.");
|
|
217
228
|
}
|
|
218
229
|
validateWorkspace(snapshot.workspace);
|
|
230
|
+
if (snapshot.skillPackages !== undefined) {
|
|
231
|
+
if (!Array.isArray(snapshot.skillPackages))
|
|
232
|
+
throw new Error("Effective Skill packages must be an array.");
|
|
233
|
+
const ids = new Set();
|
|
234
|
+
for (const ref of snapshot.skillPackages) {
|
|
235
|
+
validateSkillPackageRef(ref);
|
|
236
|
+
if (ids.has(ref.id))
|
|
237
|
+
throw new Error("Effective Skill packages must be unique.");
|
|
238
|
+
ids.add(ref.id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
219
241
|
if (snapshot.executionEnvironment !== undefined) {
|
|
220
242
|
validateExecutionEnvironmentSnapshot(snapshot.executionEnvironment);
|
|
221
243
|
if (snapshot.executionEnvironment.access === "read") {
|
|
@@ -299,6 +321,7 @@ function snapshotFromConfig(input) {
|
|
|
299
321
|
? {}
|
|
300
322
|
: { executionEnvironment: clone(input.executionEnvironment) }),
|
|
301
323
|
context: cloneContext(input.context),
|
|
324
|
+
...(input.skillPackages === undefined ? {} : { skillPackages: clone(input.skillPackages) }),
|
|
302
325
|
contextProtocolVersion: input.contextProtocolVersion,
|
|
303
326
|
sessionManifestCompatibilityDigest: input.sessionManifestCompatibilityDigest,
|
|
304
327
|
...review
|
|
@@ -70,11 +70,13 @@ export class FileRoleLaunchPlanner {
|
|
|
70
70
|
}
|
|
71
71
|
/** Recovery addresses the recorded Agent/Session, independent of delivery
|
|
72
72
|
* admission, a missing worktree, or the old Endpoint code generation. */
|
|
73
|
-
planNativeControl(owner) {
|
|
73
|
+
planNativeControl(owner, nativeSessionId) {
|
|
74
74
|
const set = owner.scope === "task"
|
|
75
75
|
? this.store.getTaskRoleSessionSet(owner.taskId, owner.roleName)
|
|
76
76
|
: this.store.getGlobalRoleSessionSet(owner.roleName);
|
|
77
|
-
const session = roleSessionControlTarget(set)
|
|
77
|
+
const session = nativeSessionId === undefined ? roleSessionControlTarget(set)
|
|
78
|
+
: [...Object.values(set?.sessions ?? {}), ...Object.values(set?.history ?? {})]
|
|
79
|
+
.find(entry => entry.nativeSessionId === nativeSessionId);
|
|
78
80
|
if (session?.adapterId !== "codex")
|
|
79
81
|
throw new Error("Native metadata control requires a recorded Codex Session.");
|
|
80
82
|
const configured = this.store.getConfiguredAgent(session.agentId);
|
|
@@ -229,7 +231,7 @@ export class FileRoleLaunchPlanner {
|
|
|
229
231
|
throw new Error("Global Role has an unmanaged live Session. Stop that exact Session before opening controlled input; it will not be silently replaced.");
|
|
230
232
|
}
|
|
231
233
|
const resolvedEffective = activeLiveRoleAgentSession(sessionSet)?.effective
|
|
232
|
-
?? resolveEffectiveLaunch({ role, purpose: "execution" });
|
|
234
|
+
?? resolveEffectiveLaunch({ store: this.store, role, purpose: "execution" });
|
|
233
235
|
if (input.effective !== undefined
|
|
234
236
|
&& !isDeepStrictEqual(resolvedEffective, input.effective)) {
|
|
235
237
|
throw new Error(`Global Role launch effective snapshot changed: ${role.name}.`);
|
|
@@ -335,7 +337,12 @@ export class FileRoleLaunchPlanner {
|
|
|
335
337
|
Object.assign(launchEnvironment, runtimeIsolation === undefined
|
|
336
338
|
? {}
|
|
337
339
|
: taskRuntimeIsolationEnvironment(runtimeIsolation));
|
|
338
|
-
const
|
|
340
|
+
const skillPackages = input.mode === "resume"
|
|
341
|
+
? existingSession?.effective.skillPackages : effective.skillPackages;
|
|
342
|
+
if (skillPackages === undefined) {
|
|
343
|
+
throw new Error("This launch has no frozen Skill package evidence (pre-1.5 or externally recorded Session). Create a new Session from current Role configuration; historical Skill versions cannot be reconstructed.");
|
|
344
|
+
}
|
|
345
|
+
const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, { ...sessionPolicy, skillPackages });
|
|
339
346
|
const bootstrap = materializeSessionBootstrap({
|
|
340
347
|
yuiHome: this.home,
|
|
341
348
|
role: launchRole,
|
|
@@ -686,7 +693,7 @@ function resolveTaskRoleEffectiveLaunch(store, role, purpose = "execution") {
|
|
|
686
693
|
if (purpose === "planning") {
|
|
687
694
|
// Planning has no WorkItem assignment and no managed workspace, so it
|
|
688
695
|
// resolves the Role's own configured workspace with no writable Project.
|
|
689
|
-
return resolveEffectiveLaunch({ role, purpose: "planning" });
|
|
696
|
+
return resolveEffectiveLaunch({ store, role, purpose: "planning" });
|
|
690
697
|
}
|
|
691
698
|
const item = store.listWorkItems(role.taskId).find((candidate) => (candidate.assignee === role.name
|
|
692
699
|
&& !["accepted", "retired"].includes(candidate.status))) ?? null;
|
|
@@ -696,6 +703,7 @@ function resolveTaskRoleEffectiveLaunch(store, role, purpose = "execution") {
|
|
|
696
703
|
?? store.getTaskWorkspace(role.taskId)
|
|
697
704
|
?? undefined;
|
|
698
705
|
return resolveEffectiveLaunch({
|
|
706
|
+
store,
|
|
699
707
|
role,
|
|
700
708
|
purpose: "execution",
|
|
701
709
|
...(workspace === undefined ? {} : { workspace }),
|
|
@@ -5,6 +5,8 @@ import { promisify } from "node:util";
|
|
|
5
5
|
import { selectEnvironment } from "../agent/launchEnvironment.js";
|
|
6
6
|
import { durableJobIdempotencyKey } from "../job/durableJob.js";
|
|
7
7
|
import { readRuntimeIdentity } from "../release/runtimeRelease.js";
|
|
8
|
+
import { compatibleGitArguments, supportedGit } from "../repository/gitCompatibility.js";
|
|
9
|
+
import { readGitWorktrees } from "../repository/gitWorktreeInventory.js";
|
|
8
10
|
import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
|
|
9
11
|
import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
|
|
10
12
|
import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
|
|
@@ -1047,8 +1049,10 @@ function integrationCheckDirectory(home, taskId, integrationId) {
|
|
|
1047
1049
|
return join(home, "artifacts", "integration-checks", taskId, integrationId);
|
|
1048
1050
|
}
|
|
1049
1051
|
async function git(args) {
|
|
1052
|
+
const program = supportedGit();
|
|
1053
|
+
const invocation = compatibleGitArguments(args);
|
|
1050
1054
|
try {
|
|
1051
|
-
const result = await executeFile(
|
|
1055
|
+
const result = await executeFile(program.path, invocation, {
|
|
1052
1056
|
encoding: "utf8",
|
|
1053
1057
|
maxBuffer: 16 * 1024 * 1024,
|
|
1054
1058
|
timeout: 120_000
|
|
@@ -1182,15 +1186,6 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
|
|
|
1182
1186
|
}
|
|
1183
1187
|
}
|
|
1184
1188
|
async function checkedOutWorktreePaths(repositoryPath, targetRef) {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
])).trim();
|
|
1188
|
-
if (porcelain.length === 0)
|
|
1189
|
-
return [];
|
|
1190
|
-
return porcelain.split(/\n\n+/u).flatMap((record) => {
|
|
1191
|
-
const lines = record.split("\n");
|
|
1192
|
-
const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
|
|
1193
|
-
const branch = lines.find((line) => line.startsWith("branch "))?.slice("branch ".length);
|
|
1194
|
-
return path !== undefined && branch === targetRef ? [path] : [];
|
|
1195
|
-
});
|
|
1189
|
+
return (await readGitWorktrees(repositoryPath))
|
|
1190
|
+
.filter(record => record.branch === targetRef).map(record => record.path);
|
|
1196
1191
|
}
|
|
@@ -3,6 +3,7 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { isAbsolute, resolve } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { gitOutputLine, supportedGit } from "../repository/gitCompatibility.js";
|
|
6
7
|
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
7
8
|
import { recordIntegrationConflict, requireResolutionDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
|
|
8
9
|
const execute = promisify(execFile);
|
|
@@ -281,7 +282,7 @@ async function hasExactRefReceipt(path, branch, before, action) {
|
|
|
281
282
|
}
|
|
282
283
|
const actionId = () => `yui-integration-${randomBytes(16).toString("hex")}`;
|
|
283
284
|
async function gitPath(path, name) {
|
|
284
|
-
const value = (await command(path, ["rev-parse", "--git-path", name]))
|
|
285
|
+
const value = gitOutputLine(await command(path, ["rev-parse", "--git-path", name]));
|
|
285
286
|
return isAbsolute(value) ? value : resolve(path, value);
|
|
286
287
|
}
|
|
287
288
|
async function conflicts(path) {
|
|
@@ -300,8 +301,9 @@ async function succeeds(path, args, action) {
|
|
|
300
301
|
}
|
|
301
302
|
}
|
|
302
303
|
async function command(path, args, action) {
|
|
304
|
+
const program = supportedGit();
|
|
303
305
|
try {
|
|
304
|
-
return (await execute(
|
|
306
|
+
return (await execute(program.path, [
|
|
305
307
|
"-C", path, "-c", "user.name=Yui", "-c", "user.email=yui@local",
|
|
306
308
|
"-c", "core.editor=true", "-c", "rebase.backend=merge", ...args
|
|
307
309
|
], {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { lstatSync, mkdirSync, rmdirSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { jobPreviewSocketRoot } from "../storage/homeLayout.js";
|
|
4
|
+
/** An opt-in launch contract, not a second service registry. The Job remains
|
|
5
|
+
* the sole owner of the process, cancellation and exit evidence. */
|
|
6
|
+
export function isHttpPreview(env) {
|
|
7
|
+
return env.YUI_HTTP_PREVIEW === "1";
|
|
8
|
+
}
|
|
9
|
+
export function validateHttpPreview(env, steps) {
|
|
10
|
+
if (env.YUI_HTTP_PREVIEW !== undefined && !isHttpPreview(env)) {
|
|
11
|
+
throw new Error("YUI_HTTP_PREVIEW must be 1 when supplied.");
|
|
12
|
+
}
|
|
13
|
+
if (env.YUI_PREVIEW_SOCKET !== undefined || steps.some(step => step.env?.YUI_PREVIEW_SOCKET !== undefined || step.env?.YUI_HTTP_PREVIEW !== undefined)) {
|
|
14
|
+
throw new Error("Preview environment is Job-scoped; YUI_PREVIEW_SOCKET is assigned by the runner.");
|
|
15
|
+
}
|
|
16
|
+
if (isHttpPreview(env) && steps.length !== 1) {
|
|
17
|
+
throw new Error("An HTTP preview Job must have exactly one foreground service step.");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function httpPreviewSocket(artifactDir) {
|
|
21
|
+
return join(jobPreviewSocketRoot(artifactDir), "http.sock");
|
|
22
|
+
}
|
|
23
|
+
export function assertHttpPreviewSocket(artifactDir) {
|
|
24
|
+
const dir = jobPreviewSocketRoot(artifactDir);
|
|
25
|
+
return assertSocketAt(dir);
|
|
26
|
+
}
|
|
27
|
+
function assertSocketAt(dir) {
|
|
28
|
+
const parent = lstatSync(dir);
|
|
29
|
+
const path = join(dir, "http.sock");
|
|
30
|
+
const socket = lstatSync(path);
|
|
31
|
+
if (!parent.isDirectory() || parent.uid !== process.getuid?.() || (parent.mode & 0o077)
|
|
32
|
+
|| !socket.isSocket() || socket.uid !== parent.uid) {
|
|
33
|
+
throw new Error("Preview endpoint is not an owned Unix socket.");
|
|
34
|
+
}
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
/** Exclusive creation; never adopt or unlink another process's old endpoint.
|
|
38
|
+
* Cleanup checks the exact directory inode and removes no unknown contents. */
|
|
39
|
+
export function prepareHttpPreview(artifactDir) {
|
|
40
|
+
const dir = jobPreviewSocketRoot(artifactDir);
|
|
41
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
42
|
+
const owner = lstatSync(dir);
|
|
43
|
+
return {
|
|
44
|
+
socket: join(dir, "http.sock"),
|
|
45
|
+
cleanup() {
|
|
46
|
+
const current = lstatSync(dir);
|
|
47
|
+
if (!current.isDirectory() || current.dev !== owner.dev || current.ino !== owner.ino) {
|
|
48
|
+
throw new Error(`Preview owner changed; retained endpoint: ${dir}`);
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const path = assertSocketAt(dir);
|
|
52
|
+
unlinkSync(path);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code !== "ENOENT")
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
rmdirSync(dir);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/job/jobRunner.js
CHANGED
|
@@ -12,6 +12,7 @@ import { chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, ut
|
|
|
12
12
|
import { join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { jobStepDirectory } from "./stepDirectory.js";
|
|
15
|
+
import { prepareHttpPreview, isHttpPreview } from "./httpPreview.js";
|
|
15
16
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
16
17
|
import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
|
|
17
18
|
const DEFAULT_STEP_TIMEOUT_MS = 30 * 60_000;
|
|
@@ -108,6 +109,7 @@ export async function runDurableJobRunner(specPath) {
|
|
|
108
109
|
let exitCode = 0;
|
|
109
110
|
let signal = null;
|
|
110
111
|
let failedStep;
|
|
112
|
+
let preview;
|
|
111
113
|
try {
|
|
112
114
|
for (let index = 0; index < spec.steps.length; index += 1) {
|
|
113
115
|
if (cancelled || existsSync(cancelPath)) {
|
|
@@ -134,13 +136,16 @@ export async function runDurableJobRunner(specPath) {
|
|
|
134
136
|
const stepEnv = step.env === undefined
|
|
135
137
|
? spec.env
|
|
136
138
|
: { ...spec.env, ...step.env };
|
|
139
|
+
const launchEnv = isHttpPreview(spec.env)
|
|
140
|
+
? { ...stepEnv, YUI_PREVIEW_SOCKET: (preview ??= prepareHttpPreview(artifactDir)).socket }
|
|
141
|
+
: stepEnv;
|
|
137
142
|
// f4: a new process group (detached) lets the runner kill the whole
|
|
138
143
|
// step tree on timeout or cancel, for both argv and shell forms.
|
|
139
144
|
if (step.argv !== undefined) {
|
|
140
145
|
const [file, ...args] = step.argv;
|
|
141
146
|
child = spawn(file, args, {
|
|
142
147
|
cwd: stepCwd,
|
|
143
|
-
env:
|
|
148
|
+
env: launchEnv,
|
|
144
149
|
stdio: ["ignore", logFd, logFd],
|
|
145
150
|
detached: true
|
|
146
151
|
});
|
|
@@ -148,13 +153,14 @@ export async function runDurableJobRunner(specPath) {
|
|
|
148
153
|
else {
|
|
149
154
|
child = spawn("/bin/sh", ["-lc", step.command], {
|
|
150
155
|
cwd: stepCwd,
|
|
151
|
-
env:
|
|
156
|
+
env: launchEnv,
|
|
152
157
|
stdio: ["ignore", logFd, logFd],
|
|
153
158
|
detached: true
|
|
154
159
|
});
|
|
155
160
|
}
|
|
156
161
|
}
|
|
157
162
|
catch (error) {
|
|
163
|
+
writeFileSync(logFd, `Unable to start Job step: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
158
164
|
closeSync(logFd);
|
|
159
165
|
completedSteps.push({
|
|
160
166
|
name: step.name,
|
|
@@ -231,6 +237,10 @@ export async function runDurableJobRunner(specPath) {
|
|
|
231
237
|
}
|
|
232
238
|
finally {
|
|
233
239
|
clearInterval(heartbeatTimer);
|
|
240
|
+
// The foreground step has settled. Withdraw only its exact endpoint;
|
|
241
|
+
// existing Job/resource checks still own surviving descendant evidence.
|
|
242
|
+
// Failed socket cleanup retains evidence instead of publishing success.
|
|
243
|
+
preview?.cleanup();
|
|
234
244
|
// f2/rr5: Do NOT remove the signal handlers here. A repeated SIGTERM
|
|
235
245
|
// between the step loop and the exit.json write must still hit the
|
|
236
246
|
// idempotent handler (which ignores it) rather than the default handler
|