@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
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
+
import { parse } from "smol-toml";
|
|
6
|
+
/**
|
|
7
|
+
* Inspects the local, file-backed Codex configuration layers supported by Yui
|
|
8
|
+
* that can conflict with launch-owned settings. Remote and platform-managed
|
|
9
|
+
* layers are outside this compatibility boundary. Project files are considered
|
|
10
|
+
* only when the directory, project root, or project root is trusted.
|
|
11
|
+
*/
|
|
12
|
+
export function inspectCodexDeveloperInstructions(input) {
|
|
13
|
+
return inspectCodexConfigKeys(input, ["developer_instructions"])
|
|
14
|
+
.developerInstructions;
|
|
15
|
+
}
|
|
16
|
+
export function inspectCodexLaunchConfig(input) {
|
|
17
|
+
return inspectCodexConfigKeys(input, ["developer_instructions", "notify"]);
|
|
18
|
+
}
|
|
19
|
+
function inspectCodexConfigKeys(input, keys) {
|
|
20
|
+
const environment = input.environment ?? process.env;
|
|
21
|
+
const home = codexHome(environment);
|
|
22
|
+
const systemPath = checkedAbsolutePath(input.systemConfigPath ?? "/etc/codex/config.toml", "Codex system config path");
|
|
23
|
+
const managedPath = checkedAbsolutePath(input.managedConfigPath ?? "/etc/codex/managed_config.toml", "Codex managed config path");
|
|
24
|
+
const userPath = join(home, "config.toml");
|
|
25
|
+
const profilePath = input.profile === undefined
|
|
26
|
+
? undefined
|
|
27
|
+
: profileConfigPath(home, input.profile);
|
|
28
|
+
const discoveryPaths = [
|
|
29
|
+
systemPath,
|
|
30
|
+
userPath,
|
|
31
|
+
...(profilePath === undefined ? [] : [profilePath])
|
|
32
|
+
];
|
|
33
|
+
const basePaths = [...discoveryPaths, managedPath];
|
|
34
|
+
const contentsByPath = new Map(basePaths.map((path) => [path, readOptionalConfig(path)]));
|
|
35
|
+
// Managed policy is applied after project configuration by Codex and cannot
|
|
36
|
+
// decide whether project configuration is discoverable.
|
|
37
|
+
const discovery = effectiveProjectDiscovery(discoveryPaths.flatMap((path) => {
|
|
38
|
+
const contents = contentsByPath.get(path);
|
|
39
|
+
if (contents === null || contents === undefined)
|
|
40
|
+
return [];
|
|
41
|
+
try {
|
|
42
|
+
return [inspectProjectDiscovery(contents)];
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw unreliableInspection(path, error);
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
const projectPaths = projectConfigPaths(input.workspace, input.trustWorkspace === true
|
|
49
|
+
? withExactWorkspaceTrust(discovery, input.workspace)
|
|
50
|
+
: discovery);
|
|
51
|
+
const candidates = [
|
|
52
|
+
...discoveryPaths.map((path) => ({ path, keys })),
|
|
53
|
+
...projectPaths.map((path) => ({
|
|
54
|
+
path,
|
|
55
|
+
keys: keys.filter((key) => key === "developer_instructions")
|
|
56
|
+
})),
|
|
57
|
+
{ path: managedPath, keys }
|
|
58
|
+
];
|
|
59
|
+
let developerInstructions = { status: "absent" };
|
|
60
|
+
let notify = { status: "absent" };
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
const contents = contentsByPath.has(candidate.path)
|
|
63
|
+
? contentsByPath.get(candidate.path)
|
|
64
|
+
: readOptionalConfig(candidate.path);
|
|
65
|
+
if (contents === null)
|
|
66
|
+
continue;
|
|
67
|
+
let configured;
|
|
68
|
+
try {
|
|
69
|
+
configured = inspectConfigContents(contents, candidate.keys);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw unreliableInspection(candidate.path, error);
|
|
73
|
+
}
|
|
74
|
+
if (developerInstructions.status === "absent"
|
|
75
|
+
&& configured.has("developer_instructions")) {
|
|
76
|
+
developerInstructions = { status: "configured", source: candidate.path };
|
|
77
|
+
}
|
|
78
|
+
if (notify.status === "absent" && configured.has("notify")) {
|
|
79
|
+
notify = { status: "configured", source: candidate.path };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { developerInstructions, notify };
|
|
83
|
+
}
|
|
84
|
+
function withExactWorkspaceTrust(discovery, workspace) {
|
|
85
|
+
return {
|
|
86
|
+
...discovery,
|
|
87
|
+
trust: new Map([...discovery.trust, [resolve(workspace), "trusted"]])
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function effectiveProjectDiscovery(layers) {
|
|
91
|
+
let rootMarkers;
|
|
92
|
+
const trust = new Map();
|
|
93
|
+
for (const layer of layers) {
|
|
94
|
+
if (layer.rootMarkers !== undefined)
|
|
95
|
+
rootMarkers = layer.rootMarkers;
|
|
96
|
+
for (const [path, value] of layer.trust)
|
|
97
|
+
trust.set(path, value);
|
|
98
|
+
}
|
|
99
|
+
return { rootMarkers, trust };
|
|
100
|
+
}
|
|
101
|
+
function projectConfigPaths(workspace, discovery) {
|
|
102
|
+
const start = resolve(workspace);
|
|
103
|
+
const projectRoot = findProjectRoot(start, discovery.rootMarkers ?? [".git"]);
|
|
104
|
+
let repositoryRoot;
|
|
105
|
+
let repositoryRootResolved = false;
|
|
106
|
+
const getRepositoryRoot = () => {
|
|
107
|
+
if (!repositoryRootResolved) {
|
|
108
|
+
repositoryRoot = resolveRepositoryRootForTrust(start);
|
|
109
|
+
repositoryRootResolved = true;
|
|
110
|
+
}
|
|
111
|
+
return repositoryRoot;
|
|
112
|
+
};
|
|
113
|
+
const paths = [];
|
|
114
|
+
let current = start;
|
|
115
|
+
while (true) {
|
|
116
|
+
const configPath = join(current, ".codex", "config.toml");
|
|
117
|
+
if (existsSync(configPath)
|
|
118
|
+
&&
|
|
119
|
+
trustForDirectory(current, projectRoot, getRepositoryRoot, discovery.trust)
|
|
120
|
+
=== "trusted") {
|
|
121
|
+
paths.push(configPath);
|
|
122
|
+
}
|
|
123
|
+
if (samePath(current, projectRoot))
|
|
124
|
+
break;
|
|
125
|
+
const parent = dirname(current);
|
|
126
|
+
if (parent === current)
|
|
127
|
+
break;
|
|
128
|
+
current = parent;
|
|
129
|
+
}
|
|
130
|
+
return paths.reverse();
|
|
131
|
+
}
|
|
132
|
+
function findProjectRoot(start, markers) {
|
|
133
|
+
// Native Codex treats an explicitly empty marker list as "current directory".
|
|
134
|
+
if (markers.length === 0)
|
|
135
|
+
return start;
|
|
136
|
+
let current = start;
|
|
137
|
+
while (true) {
|
|
138
|
+
if (markers.some((marker) => existsSync(isAbsolute(marker) ? marker : `${current}${sep}${marker}`))) {
|
|
139
|
+
return current;
|
|
140
|
+
}
|
|
141
|
+
const parent = dirname(current);
|
|
142
|
+
if (parent === current)
|
|
143
|
+
return start;
|
|
144
|
+
current = parent;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function trustForDirectory(directory, projectRoot, repositoryRoot, configured) {
|
|
148
|
+
for (const candidate of [directory, projectRoot]) {
|
|
149
|
+
const value = exactTrust(candidate, configured);
|
|
150
|
+
if (value !== undefined)
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
for (const candidate of [repositoryRoot()]) {
|
|
154
|
+
if (candidate === undefined)
|
|
155
|
+
continue;
|
|
156
|
+
const value = exactTrust(candidate, configured);
|
|
157
|
+
if (value !== undefined)
|
|
158
|
+
return value;
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
function exactTrust(directory, configured) {
|
|
163
|
+
const lexical = resolve(directory);
|
|
164
|
+
const canonical = canonicalPath(lexical);
|
|
165
|
+
for (const [path, value] of configured) {
|
|
166
|
+
const configuredLexical = resolve(path);
|
|
167
|
+
if (samePath(configuredLexical, lexical)
|
|
168
|
+
|| samePath(canonicalPath(configuredLexical), canonical)) {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
function resolveRepositoryRootForTrust(workspace) {
|
|
175
|
+
const result = spawnSync("git", ["-C", workspace, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", timeout: 1_000, windowsHide: true });
|
|
176
|
+
if (result.status !== 0 || result.error !== undefined)
|
|
177
|
+
return undefined;
|
|
178
|
+
const commonDirectory = result.stdout.trim();
|
|
179
|
+
if (!isAbsolute(commonDirectory) || commonDirectory.includes("\0"))
|
|
180
|
+
return undefined;
|
|
181
|
+
const normalized = resolve(commonDirectory);
|
|
182
|
+
return basename(normalized) === ".git" ? dirname(normalized) : undefined;
|
|
183
|
+
}
|
|
184
|
+
function inspectProjectDiscovery(contents) {
|
|
185
|
+
const table = parseConfig(contents);
|
|
186
|
+
const rootMarkers = parseRootMarkers(table.project_root_markers);
|
|
187
|
+
const trust = new Map();
|
|
188
|
+
const projects = table.projects;
|
|
189
|
+
if (projects !== undefined) {
|
|
190
|
+
if (!isTable(projects)) {
|
|
191
|
+
throw new Error("projects must be a TOML table");
|
|
192
|
+
}
|
|
193
|
+
for (const [path, project] of Object.entries(projects)) {
|
|
194
|
+
if (!isAbsolute(path) || path.includes("\0")) {
|
|
195
|
+
throw new Error("project trust path is not absolute");
|
|
196
|
+
}
|
|
197
|
+
if (!isTable(project)) {
|
|
198
|
+
throw new Error("project trust entry must be a TOML table");
|
|
199
|
+
}
|
|
200
|
+
const level = project.trust_level;
|
|
201
|
+
if (level === "trusted" || level === "untrusted")
|
|
202
|
+
trust.set(path, level);
|
|
203
|
+
else if (level !== undefined) {
|
|
204
|
+
throw new Error("project trust_level must be trusted or untrusted");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
...(rootMarkers === undefined ? {} : { rootMarkers }),
|
|
210
|
+
trust
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function parseRootMarkers(value) {
|
|
214
|
+
if (value === undefined)
|
|
215
|
+
return undefined;
|
|
216
|
+
if (!Array.isArray(value) || value.some((marker) => typeof marker !== "string")) {
|
|
217
|
+
throw new Error("project_root_markers must be an array of strings");
|
|
218
|
+
}
|
|
219
|
+
if (value.some((marker) => marker.includes("\0"))) {
|
|
220
|
+
throw new Error("project_root_markers cannot contain NUL bytes");
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
function inspectConfigContents(contents, keys) {
|
|
225
|
+
const table = parseConfig(contents);
|
|
226
|
+
return new Set(keys.filter((key) => Object.hasOwn(table, key)));
|
|
227
|
+
}
|
|
228
|
+
function parseConfig(contents) {
|
|
229
|
+
try {
|
|
230
|
+
return parse(contents);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
throw new Error(`ambiguous or invalid TOML: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function isTable(value) {
|
|
237
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
238
|
+
}
|
|
239
|
+
function checkedAbsolutePath(path, label) {
|
|
240
|
+
if (path.includes("\0") || resolve(path) !== path) {
|
|
241
|
+
throw new Error(`${label} must be an absolute normalized path.`);
|
|
242
|
+
}
|
|
243
|
+
return path;
|
|
244
|
+
}
|
|
245
|
+
function codexHome(environment) {
|
|
246
|
+
const configured = environment.CODEX_HOME?.trim();
|
|
247
|
+
const launchHome = environment.HOME?.trim();
|
|
248
|
+
if (configured?.includes("\0"))
|
|
249
|
+
throw new Error("CODEX_HOME cannot contain NUL bytes.");
|
|
250
|
+
if (launchHome?.includes("\0"))
|
|
251
|
+
throw new Error("HOME cannot contain NUL bytes.");
|
|
252
|
+
return resolve(configured === undefined || configured.length === 0
|
|
253
|
+
? join(launchHome === undefined || launchHome.length === 0 ? homedir() : launchHome, ".codex")
|
|
254
|
+
: configured);
|
|
255
|
+
}
|
|
256
|
+
function profileConfigPath(home, profile) {
|
|
257
|
+
const name = profile.trim();
|
|
258
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(name)) {
|
|
259
|
+
throw new Error(`Codex profile must be a plain name before its config can be inspected: ${profile}.`);
|
|
260
|
+
}
|
|
261
|
+
return join(home, `${name}.config.toml`);
|
|
262
|
+
}
|
|
263
|
+
function readOptionalConfig(path) {
|
|
264
|
+
try {
|
|
265
|
+
return readFileSync(path, "utf8");
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
269
|
+
return null;
|
|
270
|
+
throw new Error(`Codex config could not be inspected: ${path}: `
|
|
271
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function unreliableInspection(path, error) {
|
|
275
|
+
return new Error(`Codex launch config could not be inspected reliably: ${path}: `
|
|
276
|
+
+ `${error instanceof Error ? error.message : String(error)}`);
|
|
277
|
+
}
|
|
278
|
+
function canonicalPath(path) {
|
|
279
|
+
try {
|
|
280
|
+
return realpathSync.native(path);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return resolve(path);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function samePath(left, right) {
|
|
287
|
+
return process.platform === "win32"
|
|
288
|
+
? left.toLowerCase() === right.toLowerCase()
|
|
289
|
+
: left === right;
|
|
290
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
3
|
+
export function resolveEffectiveLaunch(input) {
|
|
4
|
+
validateDesiredRole(input.role);
|
|
5
|
+
const binding = input.role.agentBindings[input.role.activeAgentId];
|
|
6
|
+
const workspace = snapshotWorkspace(input.role.workspace, input.workspace);
|
|
7
|
+
const writeProjectIds = effectiveWriteProjects(input, workspace);
|
|
8
|
+
const config = resolveAgentAdapter(binding.adapterId).canonicalizeConfig(clone(binding.config));
|
|
9
|
+
return snapshotFromConfig({
|
|
10
|
+
sourceDesiredRevision: input.role.launchRevision,
|
|
11
|
+
agentId: binding.agentId,
|
|
12
|
+
config,
|
|
13
|
+
profileAccess: input.role.defaultAccess,
|
|
14
|
+
writeProjectIds,
|
|
15
|
+
workspace,
|
|
16
|
+
context: snapshotContext(input.role),
|
|
17
|
+
...(input.purpose === "review"
|
|
18
|
+
? {
|
|
19
|
+
reviewRoundId: identity(input.reviewRoundId ?? "", "ReviewRound id"),
|
|
20
|
+
reviewBaseCommit: commit(input.reviewBaseCommit ?? "", "Review base commit")
|
|
21
|
+
}
|
|
22
|
+
: {})
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function effectiveLaunchConfig(snapshot) {
|
|
26
|
+
validateEffectiveLaunchSnapshot(snapshot);
|
|
27
|
+
return effectiveLaunchConfigUnchecked(snapshot);
|
|
28
|
+
}
|
|
29
|
+
function codexConfigFromSnapshot(snapshot) {
|
|
30
|
+
return {
|
|
31
|
+
adapterId: "codex",
|
|
32
|
+
...(snapshot.model === undefined ? {} : { model: snapshot.model }),
|
|
33
|
+
...(snapshot.effort === undefined ? {} : { effort: snapshot.effort }),
|
|
34
|
+
permission: clone(snapshot.permission),
|
|
35
|
+
...(snapshot.additionalDirectories === undefined
|
|
36
|
+
? {}
|
|
37
|
+
: { additionalDirectories: [...snapshot.additionalDirectories] }),
|
|
38
|
+
...(snapshot.advanced === undefined
|
|
39
|
+
? {}
|
|
40
|
+
: { advanced: clone(snapshot.advanced) }),
|
|
41
|
+
...(snapshot.search ? { search: true } : {}),
|
|
42
|
+
...(snapshot.profile === undefined ? {} : { profile: snapshot.profile })
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function claudeConfigFromSnapshot(snapshot) {
|
|
46
|
+
return {
|
|
47
|
+
adapterId: "claude",
|
|
48
|
+
...(snapshot.model === undefined ? {} : { model: snapshot.model }),
|
|
49
|
+
...(snapshot.effort === undefined ? {} : { effort: snapshot.effort }),
|
|
50
|
+
permission: clone(snapshot.permission),
|
|
51
|
+
...(snapshot.additionalDirectories === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { additionalDirectories: [...snapshot.additionalDirectories] }),
|
|
54
|
+
...(snapshot.advanced === undefined
|
|
55
|
+
? {}
|
|
56
|
+
: { advanced: clone(snapshot.advanced) }),
|
|
57
|
+
...(snapshot.settingsFile === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: { settingsFile: snapshot.settingsFile }),
|
|
60
|
+
...(snapshot.settingsSources === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { settingsSources: [...snapshot.settingsSources] })
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function effectiveLaunchSnapshotsCompatible(existing, desired) {
|
|
66
|
+
validateEffectiveLaunchSnapshot(existing);
|
|
67
|
+
validateEffectiveLaunchSnapshot(desired);
|
|
68
|
+
const withoutDesiredRevision = (snapshot) => {
|
|
69
|
+
const { sourceDesiredRevision: _sourceDesiredRevision, ...actual } = snapshot;
|
|
70
|
+
return actual;
|
|
71
|
+
};
|
|
72
|
+
return isDeepStrictEqual(withoutDesiredRevision(existing), withoutDesiredRevision(desired));
|
|
73
|
+
}
|
|
74
|
+
export function validateEffectiveLaunchSnapshot(snapshot) {
|
|
75
|
+
if (snapshot.schemaVersion !== 2) {
|
|
76
|
+
throw new Error("Effective launch snapshot must use schemaVersion 2.");
|
|
77
|
+
}
|
|
78
|
+
positiveInteger(snapshot.sourceDesiredRevision, "Source desired revision");
|
|
79
|
+
identity(snapshot.agentId, "Effective Agent id");
|
|
80
|
+
if (snapshot.profileAccess !== "read" && snapshot.profileAccess !== "write") {
|
|
81
|
+
throw new Error(`Effective launch Profile access is invalid: ${String(snapshot.profileAccess)}.`);
|
|
82
|
+
}
|
|
83
|
+
if (typeof snapshot.search !== "boolean") {
|
|
84
|
+
throw new Error("Effective launch search flag must be boolean.");
|
|
85
|
+
}
|
|
86
|
+
if (snapshot.adapterId === "claude" && snapshot.search) {
|
|
87
|
+
throw new Error("Claude effective launch cannot enable Codex search.");
|
|
88
|
+
}
|
|
89
|
+
const writeProjectIds = uniqueIdentities(snapshot.writeProjectIds, "Effective writable Project");
|
|
90
|
+
if (!isDeepStrictEqual(writeProjectIds, snapshot.writeProjectIds)) {
|
|
91
|
+
throw new Error("Effective writable Projects must be unique and sorted.");
|
|
92
|
+
}
|
|
93
|
+
if (snapshot.permission === undefined) {
|
|
94
|
+
throw new Error("Effective launch requires an explicit permission strategy.");
|
|
95
|
+
}
|
|
96
|
+
if ((snapshot.reviewRoundId === undefined) !== (snapshot.reviewBaseCommit === undefined)) {
|
|
97
|
+
throw new Error("Effective Review base is incomplete.");
|
|
98
|
+
}
|
|
99
|
+
if (snapshot.reviewRoundId !== undefined && snapshot.reviewBaseCommit !== undefined) {
|
|
100
|
+
identity(snapshot.reviewRoundId, "Effective ReviewRound id");
|
|
101
|
+
commit(snapshot.reviewBaseCommit, "Effective review base commit");
|
|
102
|
+
}
|
|
103
|
+
validateWorkspace(snapshot.workspace);
|
|
104
|
+
cloneContext(snapshot.context);
|
|
105
|
+
const config = effectiveLaunchConfigUnchecked(snapshot);
|
|
106
|
+
resolveAgentAdapter(snapshot.adapterId).canonicalizeConfig(config);
|
|
107
|
+
return snapshot;
|
|
108
|
+
}
|
|
109
|
+
export function effectiveRoleForLaunch(role, snapshot) {
|
|
110
|
+
validateEffectiveLaunchSnapshot(snapshot);
|
|
111
|
+
const config = effectiveLaunchConfig(snapshot);
|
|
112
|
+
const binding = {
|
|
113
|
+
agentId: snapshot.agentId,
|
|
114
|
+
adapterId: snapshot.adapterId,
|
|
115
|
+
config
|
|
116
|
+
};
|
|
117
|
+
const result = {
|
|
118
|
+
...role,
|
|
119
|
+
...cloneContext(snapshot.context),
|
|
120
|
+
activeAgentId: snapshot.agentId,
|
|
121
|
+
agentBindings: { ...role.agentBindings, [snapshot.agentId]: binding },
|
|
122
|
+
launchRevision: snapshot.sourceDesiredRevision,
|
|
123
|
+
defaultAccess: snapshot.profileAccess,
|
|
124
|
+
workspace: snapshot.workspace.root
|
|
125
|
+
};
|
|
126
|
+
clearMissingContext(result, snapshot.context);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
function snapshotFromConfig(input) {
|
|
130
|
+
const config = clone(input.config);
|
|
131
|
+
if (config.permission === undefined) {
|
|
132
|
+
throw new Error("Effective launch requires an explicit permission strategy.");
|
|
133
|
+
}
|
|
134
|
+
const review = input.reviewRoundId === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: {
|
|
137
|
+
reviewRoundId: identity(input.reviewRoundId, "ReviewRound id"),
|
|
138
|
+
reviewBaseCommit: commit(input.reviewBaseCommit ?? "", "Review base commit")
|
|
139
|
+
};
|
|
140
|
+
const common = {
|
|
141
|
+
schemaVersion: 2,
|
|
142
|
+
sourceDesiredRevision: positiveInteger(input.sourceDesiredRevision, "Source desired revision"),
|
|
143
|
+
agentId: identity(input.agentId, "Effective Agent id"),
|
|
144
|
+
profileAccess: input.profileAccess,
|
|
145
|
+
...(config.model === undefined ? {} : { model: config.model }),
|
|
146
|
+
...(config.effort === undefined ? {} : { effort: config.effort }),
|
|
147
|
+
search: config.adapterId === "codex" && config.search === true,
|
|
148
|
+
...(config.additionalDirectories === undefined
|
|
149
|
+
? {}
|
|
150
|
+
: { additionalDirectories: [...config.additionalDirectories] }),
|
|
151
|
+
...(config.advanced === undefined ? {} : { advanced: clone(config.advanced) }),
|
|
152
|
+
writeProjectIds: [...input.writeProjectIds],
|
|
153
|
+
workspace: cloneWorkspace(input.workspace),
|
|
154
|
+
context: cloneContext(input.context),
|
|
155
|
+
...review
|
|
156
|
+
};
|
|
157
|
+
const snapshot = config.adapterId === "codex"
|
|
158
|
+
? {
|
|
159
|
+
...common,
|
|
160
|
+
adapterId: "codex",
|
|
161
|
+
permission: clone(config.permission),
|
|
162
|
+
...(config.profile === undefined ? {} : { profile: config.profile })
|
|
163
|
+
}
|
|
164
|
+
: {
|
|
165
|
+
...common,
|
|
166
|
+
adapterId: "claude",
|
|
167
|
+
permission: clone(config.permission),
|
|
168
|
+
...(config.settingsFile === undefined ? {} : { settingsFile: config.settingsFile }),
|
|
169
|
+
...(config.settingsSources === undefined
|
|
170
|
+
? {}
|
|
171
|
+
: { settingsSources: [...config.settingsSources] })
|
|
172
|
+
};
|
|
173
|
+
return validateEffectiveLaunchSnapshot(snapshot);
|
|
174
|
+
}
|
|
175
|
+
function effectiveLaunchConfigUnchecked(snapshot) {
|
|
176
|
+
return snapshot.adapterId === "codex"
|
|
177
|
+
? codexConfigFromSnapshot(snapshot)
|
|
178
|
+
: claudeConfigFromSnapshot(snapshot);
|
|
179
|
+
}
|
|
180
|
+
function effectiveWriteProjects(input, workspace) {
|
|
181
|
+
if (input.purpose === "review") {
|
|
182
|
+
if (!("taskId" in input.role)) {
|
|
183
|
+
throw new Error("Review launch requires a Task Role.");
|
|
184
|
+
}
|
|
185
|
+
if (input.reviewRoundId === undefined || input.reviewBaseCommit === undefined) {
|
|
186
|
+
throw new Error("Review launch requires exact ReviewRound provenance.");
|
|
187
|
+
}
|
|
188
|
+
commit(input.reviewBaseCommit, "Review base commit");
|
|
189
|
+
const reviewWorkspace = input.workspace;
|
|
190
|
+
if (reviewWorkspace === undefined
|
|
191
|
+
|| (reviewWorkspace.owner.type !== "review-round"
|
|
192
|
+
&& (reviewWorkspace.owner.type !== "execution-lane"
|
|
193
|
+
|| reviewWorkspace.owner.purpose !== "review"))) {
|
|
194
|
+
throw new Error("Review launch requires a ReviewRound-owned workspace.");
|
|
195
|
+
}
|
|
196
|
+
if (reviewWorkspace.owner.reviewRoundId !== input.reviewRoundId) {
|
|
197
|
+
throw new Error(`ReviewRound workspace owner does not match ${input.reviewRoundId}.`);
|
|
198
|
+
}
|
|
199
|
+
if (reviewWorkspace.entries.length === 0
|
|
200
|
+
|| reviewWorkspace.entries.some(({ access }) => access !== "write")) {
|
|
201
|
+
throw new Error("Every ReviewRound workspace Project must be an isolated writable entry.");
|
|
202
|
+
}
|
|
203
|
+
return uniqueIdentities(reviewWorkspace.entries.map(({ projectId }) => projectId), "Review workspace Project");
|
|
204
|
+
}
|
|
205
|
+
const workspaceWrite = uniqueIdentities(workspace.entries
|
|
206
|
+
.filter(({ access }) => access === "write")
|
|
207
|
+
.map(({ projectId }) => projectId), "Workspace writable Project");
|
|
208
|
+
if (!("taskId" in input.role))
|
|
209
|
+
return [];
|
|
210
|
+
// A managed workspace describes what exists, not who is authorized to
|
|
211
|
+
// mutate it. Only an explicit WorkItem write scope can grant Task writes.
|
|
212
|
+
if (input.workItemWriteProjectIds === undefined)
|
|
213
|
+
return [];
|
|
214
|
+
const requested = uniqueIdentities(input.workItemWriteProjectIds, "WorkItem writable Project");
|
|
215
|
+
if (requested.length === 0)
|
|
216
|
+
return [];
|
|
217
|
+
if (!isDeepStrictEqual(requested, workspaceWrite)) {
|
|
218
|
+
throw new Error("WorkItem write scope does not match the managed workspace write scope.");
|
|
219
|
+
}
|
|
220
|
+
return requested;
|
|
221
|
+
}
|
|
222
|
+
function snapshotWorkspace(roleWorkspace, workspace) {
|
|
223
|
+
if (workspace === undefined)
|
|
224
|
+
return { root: text(roleWorkspace, "Role workspace"), entries: [] };
|
|
225
|
+
return {
|
|
226
|
+
root: text(workspace.root, "Effective workspace root"),
|
|
227
|
+
entries: workspace.entries.map((entry) => ({ ...entry }))
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function snapshotContext(role) {
|
|
231
|
+
return cloneContext(role);
|
|
232
|
+
}
|
|
233
|
+
function cloneContext(context) {
|
|
234
|
+
return {
|
|
235
|
+
...(context.description === undefined ? {} : { description: text(context.description, "Role description") }),
|
|
236
|
+
...(context.responsibilities === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { responsibilities: context.responsibilities.map((value) => text(value, "Role responsibility")) }),
|
|
239
|
+
...(context.constraints === undefined
|
|
240
|
+
? {}
|
|
241
|
+
: { constraints: context.constraints.map((value) => text(value, "Role constraint")) }),
|
|
242
|
+
...(context.expectedOutput === undefined
|
|
243
|
+
? {}
|
|
244
|
+
: { expectedOutput: text(context.expectedOutput, "Role expected output") }),
|
|
245
|
+
...(context.systemPrompt === undefined
|
|
246
|
+
? {}
|
|
247
|
+
: { systemPrompt: text(context.systemPrompt, "Role system prompt") }),
|
|
248
|
+
...(context.skills === undefined
|
|
249
|
+
? {}
|
|
250
|
+
: { skills: context.skills.map((value) => identity(value, "Role Skill id")) })
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function clearMissingContext(role, context) {
|
|
254
|
+
for (const key of [
|
|
255
|
+
"description",
|
|
256
|
+
"responsibilities",
|
|
257
|
+
"constraints",
|
|
258
|
+
"expectedOutput",
|
|
259
|
+
"systemPrompt",
|
|
260
|
+
"skills"
|
|
261
|
+
]) {
|
|
262
|
+
if (!Object.hasOwn(context, key))
|
|
263
|
+
delete role[key];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function cloneWorkspace(workspace) {
|
|
267
|
+
validateWorkspace(workspace);
|
|
268
|
+
return {
|
|
269
|
+
root: workspace.root,
|
|
270
|
+
entries: workspace.entries.map((entry) => ({ ...entry }))
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function validateWorkspace(workspace) {
|
|
274
|
+
text(workspace.root, "Effective workspace root");
|
|
275
|
+
if (!Array.isArray(workspace.entries))
|
|
276
|
+
throw new Error("Effective workspace entries are invalid.");
|
|
277
|
+
uniqueIdentities(workspace.entries.map(({ projectId }) => projectId), "Effective workspace Project");
|
|
278
|
+
for (const entry of workspace.entries) {
|
|
279
|
+
identity(entry.projectId, "Effective workspace Project id");
|
|
280
|
+
identity(entry.directory, "Effective workspace Project directory");
|
|
281
|
+
if (entry.access !== "read" && entry.access !== "write") {
|
|
282
|
+
throw new Error("Effective workspace Project access is invalid.");
|
|
283
|
+
}
|
|
284
|
+
text(entry.path, "Effective workspace Project path");
|
|
285
|
+
text(entry.branch, "Effective workspace Project branch");
|
|
286
|
+
text(entry.baseRef, "Effective workspace Project base ref");
|
|
287
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(entry.baseCommit)) {
|
|
288
|
+
throw new Error("Effective workspace base commit is invalid.");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function validateDesiredRole(role) {
|
|
293
|
+
positiveInteger(role.launchRevision, "Role desired revision");
|
|
294
|
+
if (role.defaultAccess !== "read" && role.defaultAccess !== "write") {
|
|
295
|
+
throw new Error("Role default access is invalid.");
|
|
296
|
+
}
|
|
297
|
+
const binding = role.agentBindings[role.activeAgentId];
|
|
298
|
+
if (binding === undefined)
|
|
299
|
+
throw new Error("Role active Agent binding is missing.");
|
|
300
|
+
if (binding.adapterId !== "codex" && binding.adapterId !== "claude") {
|
|
301
|
+
throw new Error(`Role Agent adapter is unsupported: ${binding.adapterId}.`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function uniqueIdentities(values, label) {
|
|
305
|
+
if (!Array.isArray(values))
|
|
306
|
+
throw new Error(`${label} list is invalid.`);
|
|
307
|
+
const result = [...new Set(values.map((value) => identity(value, label)))].sort();
|
|
308
|
+
if (result.length !== values.length)
|
|
309
|
+
throw new Error(`${label} list contains duplicates.`);
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
function positiveInteger(value, label) {
|
|
313
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
314
|
+
throw new Error(`${label} must be positive.`);
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
function identity(value, label) {
|
|
318
|
+
const result = text(value, label);
|
|
319
|
+
if (["__proto__", "prototype", "constructor", ".", ".."].includes(result)
|
|
320
|
+
|| /[/\\\0]/u.test(result)) {
|
|
321
|
+
throw new Error(`${label} is invalid.`);
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
function commit(value, label) {
|
|
326
|
+
const result = text(value, label).toLowerCase();
|
|
327
|
+
if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(result)) {
|
|
328
|
+
throw new Error(`${label} is invalid.`);
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
}
|
|
332
|
+
function text(value, label) {
|
|
333
|
+
if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
|
|
334
|
+
throw new Error(`${label} is required.`);
|
|
335
|
+
}
|
|
336
|
+
return value.trim();
|
|
337
|
+
}
|
|
338
|
+
function clone(value) {
|
|
339
|
+
return JSON.parse(JSON.stringify(value));
|
|
340
|
+
}
|