@zq-silk/yui 0.2.0 → 0.4.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 +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -1,23 +1,88 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
6
|
import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
|
|
7
|
+
import { NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment } from "../agent/launchEnvironment.js";
|
|
5
8
|
import { activeRoleAgentBinding } from "../role/role.js";
|
|
9
|
+
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
10
|
+
import { compileRoleSessionContext } from "../context/roleSessionContext.js";
|
|
6
11
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
12
|
+
import { inspectCodexLaunchConfig } from "./codexConfigConflict.js";
|
|
13
|
+
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
14
|
+
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
15
|
+
import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
|
|
16
|
+
import { activeLiveRoleAgentSession } from "./agentExecutor.js";
|
|
17
|
+
import { effectiveLaunchSnapshotsCompatible, effectiveRoleForLaunch, resolveEffectiveLaunch } from "./effectiveLaunch.js";
|
|
18
|
+
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeState, createExactControlPlaneDescriptor, createExactTaskRuntimeDescriptor, exactControlPlaneCommandPrefix, exactControlPlaneDigest, exactTaskRuntimeDescriptorPath, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
|
|
19
|
+
import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment } from "../runtime/taskRuntimeIsolation.js";
|
|
7
20
|
/** Builds managed native Agent launches from the authoritative Task records. */
|
|
8
21
|
export class FileRoleLaunchPlanner {
|
|
9
22
|
home;
|
|
10
23
|
store;
|
|
11
|
-
#
|
|
24
|
+
#operationalEnvironment;
|
|
25
|
+
#agentEnvironment;
|
|
26
|
+
#nativeAgentEnvironment;
|
|
12
27
|
#createNativeSessionId;
|
|
13
28
|
#cliPath;
|
|
29
|
+
#controlPlane;
|
|
14
30
|
constructor(home, store, options = {}) {
|
|
15
31
|
this.home = home;
|
|
16
32
|
this.store = store;
|
|
17
|
-
|
|
33
|
+
// Operational launch context is stable for the Controller lifetime. Agent
|
|
34
|
+
// binding sources are a separate replaceable snapshot so an unset/removed
|
|
35
|
+
// secret cannot survive a later configuration refresh.
|
|
36
|
+
const sourceEnvironment = { ...(options.environment ?? process.env) };
|
|
37
|
+
this.#operationalEnvironment = { ...sourceEnvironment };
|
|
38
|
+
for (const name of NATIVE_AGENT_ENVIRONMENT_NAMES) {
|
|
39
|
+
delete this.#operationalEnvironment[name];
|
|
40
|
+
}
|
|
41
|
+
this.#agentEnvironment = this.#selectConfiguredAgentEnvironment(sourceEnvironment);
|
|
42
|
+
this.#nativeAgentEnvironment = this.#selectConfiguredNativeEnvironment(sourceEnvironment);
|
|
18
43
|
this.#createNativeSessionId = options.createNativeSessionId ?? randomUUID;
|
|
19
|
-
this.#cliPath = options.cliPath
|
|
20
|
-
?? fileURLToPath(new URL("../cli.js", import.meta.url));
|
|
44
|
+
this.#cliPath = canonicalPath(options.cliPath
|
|
45
|
+
?? fileURLToPath(new URL("../cli.js", import.meta.url)));
|
|
46
|
+
// Freeze the complete control identity before any native process exists.
|
|
47
|
+
// This value is immutable for the planner/Controller lifetime and is never
|
|
48
|
+
// rewritten through a shared PATH launcher.
|
|
49
|
+
this.#controlPlane = createExactControlPlaneDescriptor({
|
|
50
|
+
executable: process.execPath,
|
|
51
|
+
cliEntry: this.#cliPath,
|
|
52
|
+
yuiHome: this.home
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
refreshAgentEnvironment(refresh) {
|
|
56
|
+
this.#agentEnvironment = patchEnvironment(this.#agentEnvironment, refresh.sourceNames, refresh.sources);
|
|
57
|
+
this.#nativeAgentEnvironment = patchEnvironment(this.#nativeAgentEnvironment, refresh.nativeNames, refresh.nativeSources);
|
|
58
|
+
}
|
|
59
|
+
/** Re-publishes provider-discovered native identity after its durable fold. */
|
|
60
|
+
refreshTaskRuntimeDescriptor(input) {
|
|
61
|
+
const task = this.store.getTask(input.taskId);
|
|
62
|
+
const role = this.store.getRole(input.taskId, input.roleName);
|
|
63
|
+
if (task === null || task.status !== "active" || role === null) {
|
|
64
|
+
throw new Error(`Task runtime is not current: ${input.taskId}/${input.roleName}.`);
|
|
65
|
+
}
|
|
66
|
+
const run = this.store.getActiveAgentRun(input.taskId, input.roleName);
|
|
67
|
+
const sessions = this.store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
68
|
+
const session = sessions?.sessions[role.activeAgentId];
|
|
69
|
+
if (session === undefined || session.launchId === undefined) {
|
|
70
|
+
throw new Error(`Task runtime native Session is not ready: ${input.taskId}/${input.roleName}.`);
|
|
71
|
+
}
|
|
72
|
+
const effective = run?.effective ?? session.effective;
|
|
73
|
+
const descriptor = createExactTaskRuntimeDescriptor({
|
|
74
|
+
controlPlaneDigest: exactControlPlaneDigest(this.#controlPlane),
|
|
75
|
+
taskId: input.taskId,
|
|
76
|
+
roleName: input.roleName,
|
|
77
|
+
agentId: session.agentId,
|
|
78
|
+
adapterId: session.adapterId,
|
|
79
|
+
workspace: effective.workspace.root,
|
|
80
|
+
...(run === null ? {} : { runId: run.id }),
|
|
81
|
+
launchId: session.launchId,
|
|
82
|
+
nativeSessionId: session.nativeSessionId
|
|
83
|
+
});
|
|
84
|
+
assertExactTaskRuntimeState(descriptor, this.store);
|
|
85
|
+
writeTextFileAtomically(exactTaskRuntimeDescriptorPath(this.home, descriptor), `${serializeExactDescriptor(descriptor)}\n`);
|
|
21
86
|
}
|
|
22
87
|
plan(input) {
|
|
23
88
|
const task = this.store.getTask(input.taskId);
|
|
@@ -28,25 +93,119 @@ export class FileRoleLaunchPlanner {
|
|
|
28
93
|
const role = this.store.getRole(input.taskId, input.roleName);
|
|
29
94
|
if (role === null)
|
|
30
95
|
throw new Error(`Role not found: ${input.taskId}/${input.roleName}.`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
96
|
+
const activeRun = this.store.getActiveAgentRun(task.id, role.name);
|
|
97
|
+
if (input.runId !== undefined && activeRun?.id !== input.runId) {
|
|
98
|
+
throw new Error(`Role Run is no longer current: ${input.runId}.`);
|
|
99
|
+
}
|
|
100
|
+
const runWorkspace = activeRun?.workspace;
|
|
101
|
+
const main = this.store.getTaskWorkspace(task.id);
|
|
102
|
+
if (!isTaskOwnedWorkspace(main, task.id, task.cwd, task.projectBindings.map(({ projectId, directory }) => ({ projectId, directory })))) {
|
|
103
|
+
throw new Error(`Role workspace is not ready: ${input.taskId}/${input.roleName}.`);
|
|
104
|
+
}
|
|
105
|
+
if (runWorkspace !== undefined) {
|
|
106
|
+
const durableRunWorkspace = this.store.getManagedWorkspace(runWorkspace.owner);
|
|
107
|
+
if (durableRunWorkspace === null || !isDeepStrictEqual(durableRunWorkspace, runWorkspace)) {
|
|
108
|
+
throw new Error(`Role Run workspace is not the durable owner: ${input.taskId}/${input.roleName}.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const assignedWorkItem = this.store.listWorkItems(task.id).find((item) => item.assignee === role.name
|
|
112
|
+
&& !["completed", "failed", "retired"].includes(item.status));
|
|
113
|
+
// A Run snapshot is authoritative for the live launch. In particular,
|
|
114
|
+
// a reviewer Run must launch from its ReviewRound-owned workspace rather
|
|
115
|
+
// than falling back to the WorkItem Develop workspace. Without an
|
|
116
|
+
// active snapshot, resolve the normal Role/WorkItem assignment.
|
|
117
|
+
const workspace = runWorkspace !== undefined
|
|
118
|
+
? runWorkspace
|
|
119
|
+
: task.projectBindings.length === 0
|
|
120
|
+
? main
|
|
121
|
+
: assignedWorkItem === undefined
|
|
122
|
+
? main
|
|
123
|
+
: this.store.getWorkItemWorkspace(task.id, assignedWorkItem.id);
|
|
124
|
+
if (task.projectBindings.length === 0) {
|
|
125
|
+
if (workspace === null || !isDeepStrictEqual(workspace, main)) {
|
|
126
|
+
throw new Error(`Role workspace is not ready: ${input.taskId}/${input.roleName}.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const sharedMain = main !== null
|
|
131
|
+
&& (workspace === null || workspace.owner.type === "task")
|
|
132
|
+
&& sameWorkspaceProjects(main, task.projectBindings.map(({ projectId }) => projectId))
|
|
133
|
+
&& (role.name === "leader" || role.workspace === main.root);
|
|
134
|
+
const isolatedWorkItem = workspace?.owner.type === "work-item"
|
|
135
|
+
? this.store.getWorkItem(task.id, workspace.owner.workItemId)
|
|
136
|
+
: null;
|
|
137
|
+
const isolated = workspace !== null
|
|
138
|
+
&& workspace.owner.type === "work-item"
|
|
139
|
+
&& isolatedWorkItem !== null
|
|
140
|
+
&& (isolatedWorkItem.assignee === undefined || isolatedWorkItem.assignee === role.name)
|
|
141
|
+
&& !["completed", "failed", "retired"].includes(isolatedWorkItem.status)
|
|
142
|
+
&& (activeRun === null
|
|
143
|
+
|| activeRun.workItemId === workspace.owner.workItemId)
|
|
144
|
+
&& sameWorkspaceProjects(workspace, task.projectBindings.map(({ projectId }) => projectId))
|
|
145
|
+
&& sameWritableProjects(workspace, isolatedWorkItem.writeProjectIds);
|
|
146
|
+
const runScoped = runWorkspace !== undefined
|
|
147
|
+
&& runWorkspace.owner.taskId === task.id
|
|
148
|
+
&& sameWorkspaceProjects(runWorkspace, task.projectBindings.map(({ projectId }) => projectId))
|
|
149
|
+
&& (runWorkspace.owner.type === "task"
|
|
150
|
+
|| (runWorkspace.owner.type === "work-item"
|
|
151
|
+
&& runWorkspace.owner.workItemId === activeRun?.workItemId)
|
|
152
|
+
|| (runWorkspace.owner.type === "review-round"
|
|
153
|
+
&& activeRun?.purpose === "review"
|
|
154
|
+
&& runWorkspace.owner.reviewRoundId === activeRun.reviewRoundId)
|
|
155
|
+
|| (runWorkspace.owner.type === "execution-lane"
|
|
156
|
+
&& runWorkspace.owner.executionGroupId === activeRun?.executionGroupId
|
|
157
|
+
&& runWorkspace.owner.executionLaneId === activeRun?.executionLaneId
|
|
158
|
+
&& ((runWorkspace.owner.purpose === "review" && activeRun?.purpose === "review")
|
|
159
|
+
|| (runWorkspace.owner.purpose === "execution" && activeRun?.purpose === "execution"))));
|
|
160
|
+
if (!runScoped && !sharedMain && !isolated) {
|
|
37
161
|
throw new Error(`Role workspace is not ready: ${input.taskId}/${input.roleName}.`);
|
|
38
162
|
}
|
|
39
163
|
}
|
|
40
|
-
|
|
164
|
+
const sessionSet = this.store.getTaskRoleSessionSet(task.id, role.name);
|
|
165
|
+
const resolvedEffective = activeRun?.effective
|
|
166
|
+
?? activeLiveRoleAgentSession(sessionSet)?.effective
|
|
167
|
+
?? resolveTaskRoleEffectiveLaunch(this.store, role);
|
|
168
|
+
if (input.effective !== undefined
|
|
169
|
+
&& !isDeepStrictEqual(resolvedEffective, input.effective)) {
|
|
170
|
+
throw new Error(`Role launch effective Run snapshot changed: ${input.taskId}/${input.roleName}.`);
|
|
171
|
+
}
|
|
172
|
+
if (activeRun !== null && input.effective === undefined) {
|
|
173
|
+
throw new Error(`Role launch is missing the effective Run snapshot: ${input.taskId}/${input.roleName}.`);
|
|
174
|
+
}
|
|
175
|
+
const effective = input.effective ?? resolvedEffective;
|
|
176
|
+
const existing = sessionSet?.sessions[effective.agentId];
|
|
177
|
+
const compatibleExisting = existing !== undefined
|
|
178
|
+
&& effectiveLaunchSnapshotsCompatible(existing.effective, effective);
|
|
179
|
+
if (input.mode === "resume" && !compatibleExisting) {
|
|
180
|
+
throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
|
|
181
|
+
}
|
|
182
|
+
return this.#compile(role, input, { scope: "task", taskId: task.id }, taskRoleSessionTitle(task, role.name), compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
|
|
183
|
+
purpose: activeRun?.purpose ?? "execution"
|
|
184
|
+
});
|
|
41
185
|
}
|
|
42
186
|
planGlobalRole(input) {
|
|
43
187
|
const role = this.store.getGlobalRole(input.roleName);
|
|
44
188
|
if (role === null)
|
|
45
189
|
throw new Error(`Global Role not found: ${input.roleName}.`);
|
|
46
|
-
|
|
190
|
+
const sessionSet = this.store.getGlobalRoleSessionSet(role.name);
|
|
191
|
+
const resolvedEffective = activeLiveRoleAgentSession(sessionSet)?.effective
|
|
192
|
+
?? resolveEffectiveLaunch({ role, purpose: "execution" });
|
|
193
|
+
if (input.effective !== undefined
|
|
194
|
+
&& !isDeepStrictEqual(resolvedEffective, input.effective)) {
|
|
195
|
+
throw new Error(`Global Role launch effective snapshot changed: ${role.name}.`);
|
|
196
|
+
}
|
|
197
|
+
const effective = input.effective ?? resolvedEffective;
|
|
198
|
+
const existing = sessionSet?.sessions[effective.agentId];
|
|
199
|
+
const compatibleExisting = existing !== undefined
|
|
200
|
+
&& effectiveLaunchSnapshotsCompatible(existing.effective, effective);
|
|
201
|
+
if (input.mode === "resume" && !compatibleExisting) {
|
|
202
|
+
throw new Error(`Global Role resume effective snapshot drifted: ${role.name}.`);
|
|
203
|
+
}
|
|
204
|
+
return this.#compile(role, input, { scope: "global" }, undefined, compatibleExisting ? existing.nativeSessionId : undefined, undefined, effective, { purpose: "execution" });
|
|
47
205
|
}
|
|
48
|
-
#compile(role, input, owner, knownNativeSessionId) {
|
|
49
|
-
const
|
|
206
|
+
#compile(role, input, owner, sessionTitle, knownNativeSessionId, workspaceOverride, effective, sessionPolicy) {
|
|
207
|
+
const launchRole = effectiveRoleForLaunch(role, effective);
|
|
208
|
+
const binding = activeRoleAgentBinding(launchRole);
|
|
50
209
|
if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
|
|
51
210
|
throw new Error(`Role launch identity changed: ${role.name}.`);
|
|
52
211
|
}
|
|
@@ -57,12 +216,78 @@ export class FileRoleLaunchPlanner {
|
|
|
57
216
|
throw new Error(`Configured Agent adapter changed: ${input.agentId}.`);
|
|
58
217
|
}
|
|
59
218
|
const agent = configuredAgentToDefinition(configured);
|
|
219
|
+
const agentSourceEnvironment = input.environment ?? this.#agentEnvironment;
|
|
220
|
+
const operationalSourceEnvironment = input.environment ?? {
|
|
221
|
+
...this.#operationalEnvironment,
|
|
222
|
+
...this.#nativeAgentEnvironment
|
|
223
|
+
};
|
|
224
|
+
const resolvedAgentEnvironment = resolveAgentEnvironment(agent, agentSourceEnvironment);
|
|
225
|
+
const inheritedLaunchEnvironment = {
|
|
226
|
+
...operationalAgentEnvironment(configured.adapterId, operationalSourceEnvironment),
|
|
227
|
+
...resolvedAgentEnvironment
|
|
228
|
+
};
|
|
229
|
+
const launchEnvironment = { ...inheritedLaunchEnvironment };
|
|
60
230
|
const adapter = resolveAgentAdapter(binding.adapterId);
|
|
231
|
+
const effectiveWorkspace = effective.workspace.root;
|
|
232
|
+
const agentWorkspace = nativeAgentWorkspace(effective.workspace);
|
|
233
|
+
const runtimeIsolation = input.runtimeIsolation === undefined
|
|
234
|
+
? undefined
|
|
235
|
+
: parseTaskRuntimeIsolationDescriptor(JSON.stringify(input.runtimeIsolation));
|
|
236
|
+
if (runtimeIsolation !== undefined && (owner.scope !== "task"
|
|
237
|
+
|| runtimeIsolation.taskId !== owner.taskId
|
|
238
|
+
|| runtimeIsolation.workspace.root !== effectiveWorkspace
|
|
239
|
+
|| runtimeIsolation.generation.runId !== input.runId
|
|
240
|
+
|| runtimeIsolation.generation.launchId !== input.launchId)) {
|
|
241
|
+
throw new Error("Role launch does not match its Task runtime isolation descriptor.");
|
|
242
|
+
}
|
|
243
|
+
Object.assign(launchEnvironment, runtimeIsolation === undefined
|
|
244
|
+
? {}
|
|
245
|
+
: taskRuntimeIsolationEnvironment(runtimeIsolation));
|
|
246
|
+
const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, sessionPolicy);
|
|
247
|
+
const sessionContext = owner.scope === "task"
|
|
248
|
+
? {
|
|
249
|
+
...baseSessionContext,
|
|
250
|
+
developerInstructions: [
|
|
251
|
+
baseSessionContext.developerInstructions,
|
|
252
|
+
renderExactControlPlaneInstructions(this.#controlPlane)
|
|
253
|
+
].join("\n")
|
|
254
|
+
}
|
|
255
|
+
: baseSessionContext;
|
|
256
|
+
const codexConfig = binding.config.adapterId === "codex"
|
|
257
|
+
? inspectCodexLaunchConfig({
|
|
258
|
+
environment: {
|
|
259
|
+
...operationalSourceEnvironment,
|
|
260
|
+
...agentSourceEnvironment,
|
|
261
|
+
...launchEnvironment
|
|
262
|
+
},
|
|
263
|
+
workspace: agentWorkspace,
|
|
264
|
+
profile: binding.config.profile,
|
|
265
|
+
trustWorkspace: true
|
|
266
|
+
})
|
|
267
|
+
: undefined;
|
|
268
|
+
if (codexConfig?.notify.status === "configured") {
|
|
269
|
+
throw new Error("Codex notify is already configured by "
|
|
270
|
+
+ `${codexConfig.notify.source}; Yui requires exclusive ownership of the structured `
|
|
271
|
+
+ "notify callback and refuses to replace or be replaced by native configuration.");
|
|
272
|
+
}
|
|
273
|
+
const managedRun = owner.scope === "task" && input.runId !== undefined
|
|
274
|
+
? this.store.getAgentRun(owner.taskId, input.runId)
|
|
275
|
+
: null;
|
|
276
|
+
const roleConfig = binding.config.adapterId === "claude"
|
|
277
|
+
&& owner.scope === "task"
|
|
278
|
+
&& input.runId !== undefined
|
|
279
|
+
? managedClaudeControlPlaneConfig(binding.config, owner.taskId, managedRun?.workItemId, input.runId, this.#controlPlane)
|
|
280
|
+
: binding.config;
|
|
281
|
+
const effectiveConfig = withNativeProjectDirectories(roleConfig, nativeAdditionalDirectories(effective.workspace, agentWorkspace));
|
|
61
282
|
const compileInput = {
|
|
62
283
|
agent,
|
|
63
|
-
config:
|
|
64
|
-
workspace:
|
|
65
|
-
|
|
284
|
+
config: effectiveConfig,
|
|
285
|
+
workspace: agentWorkspace,
|
|
286
|
+
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
287
|
+
...sessionContext,
|
|
288
|
+
...(codexConfig === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: { codexDeveloperInstructions: codexConfig.developerInstructions })
|
|
66
291
|
};
|
|
67
292
|
if (input.mode === "resume"
|
|
68
293
|
&& knownNativeSessionId !== undefined
|
|
@@ -88,41 +313,280 @@ export class FileRoleLaunchPlanner {
|
|
|
88
313
|
let session;
|
|
89
314
|
if (binding.adapterId === "codex") {
|
|
90
315
|
args = addCodexSessionNotify(args, launchMode, this.#cliPath);
|
|
316
|
+
// A fresh Codex TUI has no provider event before its first prompt. Carry
|
|
317
|
+
// the exact Run input as the provider's positional launch prompt so it is
|
|
318
|
+
// submitted only after Codex completes startup; never race terminal bytes
|
|
319
|
+
// and Enter against TUI initialization.
|
|
320
|
+
if (owner.scope === "task" && input.runId !== undefined) {
|
|
321
|
+
if (managedRun === null || managedRun.status !== "active") {
|
|
322
|
+
throw new Error(`Managed Codex Run is no longer active: ${input.runId}.`);
|
|
323
|
+
}
|
|
324
|
+
args = addCodexLifecycleHooks(args, launchMode, this.#cliPath);
|
|
325
|
+
// End option parsing before the opaque prompt so a wakeup beginning
|
|
326
|
+
// with '-' can never be reinterpreted as a Codex CLI flag.
|
|
327
|
+
args.push("--", managedRun.input);
|
|
328
|
+
}
|
|
91
329
|
session = launchMode === "resume"
|
|
92
|
-
? readySession(input.agentId, binding.adapterId, resumeNativeSessionId)
|
|
330
|
+
? readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective)
|
|
93
331
|
: null;
|
|
94
332
|
}
|
|
95
333
|
else if (launchMode === "new") {
|
|
96
|
-
|
|
334
|
+
if (owner.scope === "task" && input.runId !== undefined) {
|
|
335
|
+
args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
336
|
+
}
|
|
337
|
+
const nativeSessionId = requireText(input.launchId === undefined
|
|
338
|
+
? this.#createNativeSessionId()
|
|
339
|
+
: nativeSessionIdForLaunch(this.home, input.launchId, input.agentId, input.adapterId), "Native session id");
|
|
97
340
|
args.push("--session-id", nativeSessionId);
|
|
98
|
-
session = readySession(input.agentId, binding.adapterId, nativeSessionId);
|
|
341
|
+
session = readySession(input.agentId, binding.adapterId, nativeSessionId, effective);
|
|
99
342
|
}
|
|
100
343
|
else {
|
|
101
|
-
|
|
344
|
+
if (owner.scope === "task" && input.runId !== undefined) {
|
|
345
|
+
args.push("--plugin-dir", ensureManagedClaudeLifecyclePlugin(this.home, this.#cliPath));
|
|
346
|
+
}
|
|
347
|
+
session = readySession(input.agentId, binding.adapterId, resumeNativeSessionId, effective);
|
|
348
|
+
}
|
|
349
|
+
const runtimeDescriptor = owner.scope === "task"
|
|
350
|
+
? createExactTaskRuntimeDescriptor({
|
|
351
|
+
controlPlaneDigest: exactControlPlaneDigest(this.#controlPlane),
|
|
352
|
+
taskId: owner.taskId,
|
|
353
|
+
roleName: role.name,
|
|
354
|
+
agentId: configured.id,
|
|
355
|
+
adapterId: configured.adapterId,
|
|
356
|
+
workspace: effectiveWorkspace,
|
|
357
|
+
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
358
|
+
...(input.launchId === undefined ? {} : { launchId: input.launchId }),
|
|
359
|
+
...(session === null
|
|
360
|
+
? {}
|
|
361
|
+
: { nativeSessionId: session.nativeSessionId })
|
|
362
|
+
})
|
|
363
|
+
: undefined;
|
|
364
|
+
const runtimeDescriptorSource = runtimeDescriptor === undefined
|
|
365
|
+
? undefined
|
|
366
|
+
: exactTaskRuntimeDescriptorPath(this.home, runtimeDescriptor);
|
|
367
|
+
if (runtimeDescriptor !== undefined && runtimeDescriptorSource !== undefined) {
|
|
368
|
+
writeTextFileAtomically(runtimeDescriptorSource, `${serializeExactDescriptor(runtimeDescriptor)}\n`);
|
|
102
369
|
}
|
|
370
|
+
const launch = {
|
|
371
|
+
command: configured.command,
|
|
372
|
+
args,
|
|
373
|
+
env: {
|
|
374
|
+
...launchEnvironment,
|
|
375
|
+
YUI_HOME: resolve(this.home),
|
|
376
|
+
YUI_SESSION_SCOPE: owner.scope,
|
|
377
|
+
...(owner.scope === "task" ? { YUI_TASK_ID: owner.taskId } : {}),
|
|
378
|
+
YUI_ROLE: role.name,
|
|
379
|
+
YUI_AGENT_ID: configured.id,
|
|
380
|
+
YUI_ADAPTER_ID: configured.adapterId,
|
|
381
|
+
YUI_WORKSPACE: effectiveWorkspace,
|
|
382
|
+
...(runtimeDescriptor === undefined
|
|
383
|
+
? {}
|
|
384
|
+
: {
|
|
385
|
+
[YUI_CONTROL_PLANE_DESCRIPTOR]: serializeExactDescriptor(this.#controlPlane),
|
|
386
|
+
[YUI_TASK_RUNTIME_DESCRIPTOR]: runtimeDescriptorSource
|
|
387
|
+
}),
|
|
388
|
+
...(sessionTitle === undefined
|
|
389
|
+
? {}
|
|
390
|
+
: {
|
|
391
|
+
YUI_SESSION_TITLE: sessionTitle,
|
|
392
|
+
...(configured.adapterId === "codex"
|
|
393
|
+
? {
|
|
394
|
+
YUI_AGENT_COMMAND: configured.command,
|
|
395
|
+
YUI_AGENT_BASE_ARGS: JSON.stringify(configured.baseArgs)
|
|
396
|
+
}
|
|
397
|
+
: {})
|
|
398
|
+
}),
|
|
399
|
+
...(input.launchId === undefined
|
|
400
|
+
? {}
|
|
401
|
+
: { YUI_LAUNCH_ID: input.launchId }),
|
|
402
|
+
...(input.runId === undefined ? {} : { YUI_RUN_ID: input.runId }),
|
|
403
|
+
...(session === null
|
|
404
|
+
? {}
|
|
405
|
+
: { YUI_NATIVE_SESSION_ID: session.nativeSessionId })
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const scopedLaunch = owner.scope === "task"
|
|
409
|
+
? this.#applyWorkspaceScope(owner.taskId, role, launch, workspaceOverride)
|
|
410
|
+
: launch;
|
|
103
411
|
return {
|
|
104
412
|
role: {
|
|
105
413
|
name: role.name,
|
|
106
|
-
workspace:
|
|
414
|
+
workspace: effectiveWorkspace,
|
|
415
|
+
...(agentWorkspace === effectiveWorkspace ? {} : { cwd: agentWorkspace }),
|
|
107
416
|
...(owner.scope === "task" ? { status: role.status } : {})
|
|
108
417
|
},
|
|
109
|
-
launch:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
418
|
+
launch: scopedLaunch,
|
|
419
|
+
session,
|
|
420
|
+
...(binding.adapterId === "codex" && input.runId !== undefined
|
|
421
|
+
? { initialPromptRunId: input.runId }
|
|
422
|
+
: {})
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
#applyWorkspaceScope(taskId, role, launch, workspaceOverride) {
|
|
426
|
+
const workspace = workspaceOverride
|
|
427
|
+
?? (role.name === "leader"
|
|
428
|
+
? this.store.getTaskWorkspace(taskId)
|
|
429
|
+
: this.store.listWorkItems(taskId)
|
|
430
|
+
.find((item) => item.assignee === role.name && item.status === "running") === undefined
|
|
431
|
+
? this.store.getTaskWorkspace(taskId)
|
|
432
|
+
: this.store.getWorkItemWorkspace(taskId, this.store.listWorkItems(taskId).find((item) => item.assignee === role.name && item.status === "running").id));
|
|
433
|
+
if (workspace === null || workspace === undefined)
|
|
434
|
+
return launch;
|
|
435
|
+
return {
|
|
436
|
+
...launch,
|
|
437
|
+
env: {
|
|
438
|
+
...workspaceScopeEnvironment(launch.env, workspace)
|
|
439
|
+
}
|
|
124
440
|
};
|
|
125
441
|
}
|
|
442
|
+
#selectConfiguredAgentEnvironment(source) {
|
|
443
|
+
const selected = {};
|
|
444
|
+
for (const agent of this.store.listConfiguredAgents()) {
|
|
445
|
+
for (const binding of agent.environment) {
|
|
446
|
+
const value = source[binding.sourceName];
|
|
447
|
+
if (value !== undefined)
|
|
448
|
+
selected[binding.sourceName] = value;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return selected;
|
|
452
|
+
}
|
|
453
|
+
#selectConfiguredNativeEnvironment(source) {
|
|
454
|
+
const names = new Set(this.store.listConfiguredAgents().flatMap((agent) => (nativeAgentEnvironmentNames(agent.adapterId))));
|
|
455
|
+
return selectEnvironment(source, names);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function nativeAgentWorkspace(workspace) {
|
|
459
|
+
return workspace.entries.length === 1
|
|
460
|
+
? workspace.entries[0].path
|
|
461
|
+
: workspace.root;
|
|
462
|
+
}
|
|
463
|
+
function nativeAdditionalDirectories(workspace, agentWorkspace) {
|
|
464
|
+
return [workspace.root, ...workspace.entries.map(({ path }) => path)]
|
|
465
|
+
.filter((path) => path !== agentWorkspace);
|
|
466
|
+
}
|
|
467
|
+
function withNativeProjectDirectories(config, projectDirectories) {
|
|
468
|
+
if (projectDirectories.length === 0)
|
|
469
|
+
return config;
|
|
470
|
+
return {
|
|
471
|
+
...config,
|
|
472
|
+
additionalDirectories: [...new Set([
|
|
473
|
+
...(config.additionalDirectories ?? []),
|
|
474
|
+
...projectDirectories
|
|
475
|
+
])]
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function resolveTaskRoleEffectiveLaunch(store, role) {
|
|
479
|
+
const item = store.listWorkItems(role.taskId).find((candidate) => (candidate.assignee === role.name
|
|
480
|
+
&& !["completed", "failed", "retired"].includes(candidate.status))) ?? null;
|
|
481
|
+
const workspace = (item === null
|
|
482
|
+
? store.getTaskWorkspace(role.taskId)
|
|
483
|
+
: store.getWorkItemWorkspace(role.taskId, item.id))
|
|
484
|
+
?? store.getTaskWorkspace(role.taskId)
|
|
485
|
+
?? undefined;
|
|
486
|
+
return resolveEffectiveLaunch({
|
|
487
|
+
role,
|
|
488
|
+
purpose: "execution",
|
|
489
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
490
|
+
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function workspaceScopeEnvironment(environment, workspace) {
|
|
494
|
+
return {
|
|
495
|
+
...environment,
|
|
496
|
+
YUI_WRITABLE_PROJECT_IDS: JSON.stringify(workspace.entries
|
|
497
|
+
.filter(({ access }) => access === "write")
|
|
498
|
+
.map(({ projectId }) => projectId)),
|
|
499
|
+
YUI_CONTEXT_PROJECT_IDS: JSON.stringify(workspace.entries
|
|
500
|
+
.filter(({ access }) => access === "read")
|
|
501
|
+
.map(({ projectId }) => projectId)),
|
|
502
|
+
YUI_WORKSPACE_PROJECTS: JSON.stringify(Object.fromEntries(workspace.entries.map(({ projectId, directory, access, path }) => [
|
|
503
|
+
projectId,
|
|
504
|
+
{ directory, access, path }
|
|
505
|
+
])))
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function ensureManagedClaudeLifecyclePlugin(home, cliPath) {
|
|
509
|
+
const root = join(resolve(home), "runtime", "claude-lifecycle-plugin");
|
|
510
|
+
writeTextFileAtomically(join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify({
|
|
511
|
+
name: "yui-runtime-lifecycle",
|
|
512
|
+
description: "Yui-owned exact Run lifecycle transport",
|
|
513
|
+
version: "1.0.0"
|
|
514
|
+
}, null, 2)}\n`);
|
|
515
|
+
const command = {
|
|
516
|
+
type: "command",
|
|
517
|
+
command: canonicalPath(process.execPath),
|
|
518
|
+
args: [canonicalPath(cliPath), "internal", "claude-hook"]
|
|
519
|
+
};
|
|
520
|
+
writeTextFileAtomically(join(root, "hooks", "hooks.json"), `${JSON.stringify({
|
|
521
|
+
hooks: {
|
|
522
|
+
// SessionStart proves the session exists (and, for source=startup,
|
|
523
|
+
// pre-input readiness); UserPromptSubmit is the exact provider-accepted
|
|
524
|
+
// fence; PostToolUse is a provider-native in-turn progress fact and
|
|
525
|
+
// StopFailure is the terminal failure fact. All route to the same
|
|
526
|
+
// Yui-owned entrypoint, which parses by hook_event_name.
|
|
527
|
+
SessionStart: [{ hooks: [command] }],
|
|
528
|
+
UserPromptSubmit: [{ hooks: [command] }],
|
|
529
|
+
PostToolUse: [{ hooks: [command] }],
|
|
530
|
+
StopFailure: [{ hooks: [command] }]
|
|
531
|
+
}
|
|
532
|
+
}, null, 2)}\n`);
|
|
533
|
+
return root;
|
|
534
|
+
}
|
|
535
|
+
function managedClaudeControlPlaneConfig(config, taskId, workItemId, runId, controlPlane) {
|
|
536
|
+
if (config.permission.strategy !== "configured")
|
|
537
|
+
return config;
|
|
538
|
+
const exact = exactControlPlaneCommandPrefix(controlPlane);
|
|
539
|
+
const managed = [
|
|
540
|
+
`Bash(${exact} --json task context ${taskId})`,
|
|
541
|
+
`Bash(${exact} --json task work list ${taskId})`,
|
|
542
|
+
...(workItemId === undefined
|
|
543
|
+
? []
|
|
544
|
+
: [`Bash(${exact} --json task work show ${workItemId})`]),
|
|
545
|
+
`Bash(${exact} task run yield ${runId} --summary-file -:*)`
|
|
546
|
+
];
|
|
547
|
+
const existing = (config.permission.allowedTools ?? [])
|
|
548
|
+
.filter((rule) => !isManagedYuiBashRule(rule));
|
|
549
|
+
return {
|
|
550
|
+
...config,
|
|
551
|
+
permission: {
|
|
552
|
+
...config.permission,
|
|
553
|
+
allowedTools: [...new Set([...existing, ...managed])]
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function isManagedYuiBashRule(rule) {
|
|
558
|
+
const normalized = rule.trim();
|
|
559
|
+
return /^Bash\(yui(?:\s|:\*|\*|\))/u.test(normalized)
|
|
560
|
+
|| /^Bash\(.*\s--yui-control\s/u.test(normalized);
|
|
561
|
+
}
|
|
562
|
+
function renderExactControlPlaneInstructions(descriptor) {
|
|
563
|
+
const command = exactControlPlaneCommandPrefix(descriptor);
|
|
564
|
+
return [
|
|
565
|
+
"Exact Yui control-plane command prefix for this managed Task session:",
|
|
566
|
+
`\`${command}\``,
|
|
567
|
+
"Replace the portable bare `yui` token in Yui Role Skills and dispatch instructions with this exact prefix.",
|
|
568
|
+
"Bare `yui`, a PATH launcher, another checkout CLI, another YUI_HOME, or a changed schema/Controller identity is invalid and fails before Task state is read or written."
|
|
569
|
+
].join("\n");
|
|
570
|
+
}
|
|
571
|
+
function canonicalPath(path) {
|
|
572
|
+
const absolute = resolve(path);
|
|
573
|
+
try {
|
|
574
|
+
return realpathSync(absolute);
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return absolute;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function shellQuote(value) {
|
|
581
|
+
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
582
|
+
}
|
|
583
|
+
function patchEnvironment(current, names, values) {
|
|
584
|
+
const next = { ...current };
|
|
585
|
+
for (const name of names)
|
|
586
|
+
delete next[name];
|
|
587
|
+
for (const [name, value] of Object.entries(values))
|
|
588
|
+
next[name] = value;
|
|
589
|
+
return next;
|
|
126
590
|
}
|
|
127
591
|
/**
|
|
128
592
|
* Codex invokes this argv after each completed turn and appends one JSON
|
|
@@ -140,12 +604,43 @@ function addCodexSessionNotify(args, mode, cliPath) {
|
|
|
140
604
|
}
|
|
141
605
|
return [...args.slice(0, -2), ...managed, ...args.slice(-2)];
|
|
142
606
|
}
|
|
143
|
-
|
|
607
|
+
/**
|
|
608
|
+
* Codex 0.145 discovers lifecycle hooks from its effective config. Keep Yui's
|
|
609
|
+
* two handlers invocation-local: this avoids mutating CODEX_HOME or the Task
|
|
610
|
+
* workspace, while the exact launch environment supplies the durable Run fence.
|
|
611
|
+
*/
|
|
612
|
+
function codexLifecycleHooksConfig(cliPath) {
|
|
613
|
+
const command = [
|
|
614
|
+
shellQuote(canonicalPath(process.execPath)),
|
|
615
|
+
shellQuote(canonicalPath(cliPath)),
|
|
616
|
+
"internal",
|
|
617
|
+
"codex-hook"
|
|
618
|
+
].join(" ");
|
|
619
|
+
const handler = `{hooks=[{type="command",command=${JSON.stringify(command)}}]}`;
|
|
620
|
+
return `hooks={SessionStart=[${handler}],UserPromptSubmit=[${handler}]}`;
|
|
621
|
+
}
|
|
622
|
+
function addCodexLifecycleHooks(args, mode, cliPath) {
|
|
623
|
+
// Session flags are Yui-owned and exact to this launch. Hook trust bypass is
|
|
624
|
+
// still explicit because these handlers execute a local command.
|
|
625
|
+
const managed = [
|
|
626
|
+
"--enable", "hooks",
|
|
627
|
+
"--config", codexLifecycleHooksConfig(cliPath),
|
|
628
|
+
"--dangerously-bypass-hook-trust"
|
|
629
|
+
];
|
|
630
|
+
if (mode === "new")
|
|
631
|
+
return [...args, ...managed];
|
|
632
|
+
if (args.length < 2 || args.at(-2) !== "resume") {
|
|
633
|
+
throw new Error("Codex resume launch shape is invalid.");
|
|
634
|
+
}
|
|
635
|
+
return [...args.slice(0, -2), ...managed, ...args.slice(-2)];
|
|
636
|
+
}
|
|
637
|
+
function readySession(agentId, adapterId, nativeSessionId, effective) {
|
|
144
638
|
return {
|
|
145
639
|
agentId,
|
|
146
640
|
adapterId,
|
|
147
641
|
nativeSessionId: requireText(nativeSessionId, "Native session id"),
|
|
148
|
-
status: "ready"
|
|
642
|
+
status: "ready",
|
|
643
|
+
effective
|
|
149
644
|
};
|
|
150
645
|
}
|
|
151
646
|
function requireText(value, label) {
|
|
@@ -154,3 +649,18 @@ function requireText(value, label) {
|
|
|
154
649
|
}
|
|
155
650
|
return value.trim();
|
|
156
651
|
}
|
|
652
|
+
function sameWorkspaceProjects(workspace, projectIds) {
|
|
653
|
+
const actual = workspace.entries.map(({ projectId }) => projectId).sort();
|
|
654
|
+
const expected = [...projectIds].sort();
|
|
655
|
+
return actual.length === expected.length
|
|
656
|
+
&& actual.every((projectId, index) => projectId === expected[index]);
|
|
657
|
+
}
|
|
658
|
+
function sameWritableProjects(workspace, projectIds) {
|
|
659
|
+
const actual = workspace.entries
|
|
660
|
+
.filter(({ access }) => access === "write")
|
|
661
|
+
.map(({ projectId }) => projectId)
|
|
662
|
+
.sort();
|
|
663
|
+
const expected = [...projectIds].sort();
|
|
664
|
+
return actual.length === expected.length
|
|
665
|
+
&& actual.every((projectId, index) => projectId === expected[index]);
|
|
666
|
+
}
|