@pushpalsdev/cli 1.0.93 → 1.0.95
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/dist/pushpals-cli.js +120 -15
- package/package.json +1 -1
- package/runtime/prompts/remotebuddy/remotebuddy_system_prompt.md +2 -2
- package/runtime/sandbox/.pushpals-remotebuddy-fallback.js +207 -53
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +219 -130
- package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +57 -0
- package/runtime/sandbox/apps/workerpals/src/execute_job.ts +138 -105
|
@@ -3,10 +3,9 @@
|
|
|
3
3
|
* Used by both the host Worker (direct mode) and the Docker job runner.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync, rmSync, unlinkSync } from "fs";
|
|
6
|
+
import { existsSync, lstatSync, readFileSync, renameSync, rmSync, unlinkSync } from "fs";
|
|
7
7
|
import { resolve } from "path";
|
|
8
8
|
import {
|
|
9
|
-
deriveAutonomyComponentArea,
|
|
10
9
|
buildGitCommitArgs as buildSourceControlGitCommitArgs,
|
|
11
10
|
explicitSourceControlCommitIdentityFromEnv,
|
|
12
11
|
loadPromptTemplate,
|
|
@@ -15,11 +14,9 @@ import {
|
|
|
15
14
|
extractVisionKeyItems,
|
|
16
15
|
formatToolRequirement,
|
|
17
16
|
matchesGlob,
|
|
18
|
-
normalizeAutonomyComponentArea,
|
|
19
17
|
normalizeTargetPath,
|
|
20
18
|
requirementsForValidationCommand,
|
|
21
19
|
sanitizeSourceControlIdentityField,
|
|
22
|
-
type AutonomyComponentArea,
|
|
23
20
|
type SourceControlCommitIdentity,
|
|
24
21
|
type ToolRequirement,
|
|
25
22
|
} from "shared";
|
|
@@ -388,13 +385,6 @@ export function shouldEnqueueNoChangeReviewCompletion(
|
|
|
388
385
|
return extractReviewFixContext(params) == null;
|
|
389
386
|
}
|
|
390
387
|
|
|
391
|
-
function reviewAgentAllowsMultiRootScope(value: unknown): boolean {
|
|
392
|
-
const normalized = String(value ?? "")
|
|
393
|
-
.trim()
|
|
394
|
-
.toLowerCase();
|
|
395
|
-
return normalized === "review_fix" || normalized === "merge_conflict";
|
|
396
|
-
}
|
|
397
|
-
|
|
398
388
|
export function deriveQualityGatePolicy(
|
|
399
389
|
params: Record<string, unknown> | null | undefined,
|
|
400
390
|
runtimeConfig: WorkerpalsRuntimeConfig = DEFAULT_CONFIG,
|
|
@@ -1655,7 +1645,7 @@ async function runDeterministicQualityGate(
|
|
|
1655
1645
|
if (scopedValidationFailure === "outside_task_scope") {
|
|
1656
1646
|
onLog?.(
|
|
1657
1647
|
"stderr",
|
|
1658
|
-
"[ValidationGate] Required validation failures appear outside the task
|
|
1648
|
+
"[ValidationGate] Required validation failures appear outside the task target/relevance hints; treating them as publish blockers, not repair instructions.",
|
|
1659
1649
|
);
|
|
1660
1650
|
}
|
|
1661
1651
|
|
|
@@ -2176,6 +2166,12 @@ export type WorkerGitCommitIdentity = SourceControlCommitIdentity;
|
|
|
2176
2166
|
|
|
2177
2167
|
export const explicitWorkerCommitIdentityFromEnv = explicitSourceControlCommitIdentityFromEnv;
|
|
2178
2168
|
|
|
2169
|
+
async function unstageSandboxArtifactPaths(
|
|
2170
|
+
repo: string,
|
|
2171
|
+
): Promise<{ ok: boolean; stdout: string; stderr: string }> {
|
|
2172
|
+
return git(repo, ["reset", "-q", "--", ...SANDBOX_STAGE_ARTIFACT_PATHS]);
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2179
2175
|
async function resolveGitConfigValue(repo: string, key: string): Promise<string> {
|
|
2180
2176
|
const value = await git(repo, ["config", "--get", key]);
|
|
2181
2177
|
return value.ok ? sanitizeSourceControlIdentityField(value.stdout) : "";
|
|
@@ -2292,19 +2288,21 @@ export async function createJobCommit(
|
|
|
2292
2288
|
console.warn(
|
|
2293
2289
|
`[WorkerPals] Stage target invalid/missing for ${job.kind}; retrying with fallback "git add -A".`,
|
|
2294
2290
|
);
|
|
2295
|
-
result = await git(repo, [
|
|
2296
|
-
"add",
|
|
2297
|
-
"-A",
|
|
2298
|
-
"--",
|
|
2299
|
-
".",
|
|
2300
|
-
":(exclude)workspace/**",
|
|
2301
|
-
":(exclude)outputs/**",
|
|
2302
|
-
]);
|
|
2291
|
+
result = await git(repo, ["add", "-A"]);
|
|
2303
2292
|
}
|
|
2304
2293
|
if (!result.ok) {
|
|
2305
2294
|
return { ok: false, error: `Failed to stage changes: ${result.stderr || result.stdout}` };
|
|
2306
2295
|
}
|
|
2307
2296
|
}
|
|
2297
|
+
if (job.kind === "task.execute") {
|
|
2298
|
+
const unstageArtifacts = await unstageSandboxArtifactPaths(repo);
|
|
2299
|
+
if (!unstageArtifacts.ok) {
|
|
2300
|
+
return {
|
|
2301
|
+
ok: false,
|
|
2302
|
+
error: `Failed to unstage sandbox artifact paths: ${unstageArtifacts.stderr || unstageArtifacts.stdout}`,
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2308
2306
|
|
|
2309
2307
|
// Check if there are changes to commit
|
|
2310
2308
|
result = await git(repo, ["diff", "--cached", "--quiet"]);
|
|
@@ -2504,16 +2502,7 @@ function buildStageTargets(kind: string, params?: Record<string, unknown>): stri
|
|
|
2504
2502
|
|
|
2505
2503
|
export function buildStageCommand(kind: string, params?: Record<string, unknown>): string[] | null {
|
|
2506
2504
|
if (kind === "task.execute") {
|
|
2507
|
-
return [
|
|
2508
|
-
"add",
|
|
2509
|
-
"-A",
|
|
2510
|
-
"--",
|
|
2511
|
-
".",
|
|
2512
|
-
":(exclude)workspace/**",
|
|
2513
|
-
":(exclude)outputs/**",
|
|
2514
|
-
":(exclude).codex",
|
|
2515
|
-
":(exclude).codex/**",
|
|
2516
|
-
];
|
|
2505
|
+
return ["add", "-A"];
|
|
2517
2506
|
}
|
|
2518
2507
|
const targets = buildStageTargets(kind, params);
|
|
2519
2508
|
if (targets.length === 0) {
|
|
@@ -3058,25 +3047,11 @@ export async function resumePreparedMergeConflictRebase(
|
|
|
3058
3047
|
"stdout",
|
|
3059
3048
|
`[MergeConflict] Stage target invalid/missing for ${kind}; retrying with fallback "git add -A".`,
|
|
3060
3049
|
);
|
|
3061
|
-
stageResult = await git(repo, [
|
|
3062
|
-
"add",
|
|
3063
|
-
"-A",
|
|
3064
|
-
"--",
|
|
3065
|
-
".",
|
|
3066
|
-
":(exclude)workspace/**",
|
|
3067
|
-
":(exclude)outputs/**",
|
|
3068
|
-
]);
|
|
3050
|
+
stageResult = await git(repo, ["add", "-A"]);
|
|
3069
3051
|
}
|
|
3070
3052
|
}
|
|
3071
3053
|
} else {
|
|
3072
|
-
stageResult = await git(repo, [
|
|
3073
|
-
"add",
|
|
3074
|
-
"-A",
|
|
3075
|
-
"--",
|
|
3076
|
-
".",
|
|
3077
|
-
":(exclude)workspace/**",
|
|
3078
|
-
":(exclude)outputs/**",
|
|
3079
|
-
]);
|
|
3054
|
+
stageResult = await git(repo, ["add", "-A"]);
|
|
3080
3055
|
}
|
|
3081
3056
|
if (!stageResult.ok) {
|
|
3082
3057
|
return {
|
|
@@ -3086,6 +3061,15 @@ export async function resumePreparedMergeConflictRebase(
|
|
|
3086
3061
|
combinedGitOutput(stageResult),
|
|
3087
3062
|
};
|
|
3088
3063
|
}
|
|
3064
|
+
const unstageArtifacts = await unstageSandboxArtifactPaths(repo);
|
|
3065
|
+
if (!unstageArtifacts.ok) {
|
|
3066
|
+
return {
|
|
3067
|
+
ok: false,
|
|
3068
|
+
error:
|
|
3069
|
+
"Failed to unstage sandbox artifact paths before continuing rebase: " +
|
|
3070
|
+
combinedGitOutput(unstageArtifacts),
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
3089
3073
|
|
|
3090
3074
|
let rebaseContinue = await git(repo, ["-c", "core.editor=true", "rebase", "--continue"]);
|
|
3091
3075
|
let continueOutput = combinedGitOutput(rebaseContinue);
|
|
@@ -3235,19 +3219,19 @@ async function createMergeConflictJobCommit(
|
|
|
3235
3219
|
console.warn(
|
|
3236
3220
|
`[WorkerPals] Stage target invalid/missing for merge-conflict job ${job.id}; retrying with fallback "git add -A".`,
|
|
3237
3221
|
);
|
|
3238
|
-
result = await git(repo, [
|
|
3239
|
-
"add",
|
|
3240
|
-
"-A",
|
|
3241
|
-
"--",
|
|
3242
|
-
".",
|
|
3243
|
-
":(exclude)workspace/**",
|
|
3244
|
-
":(exclude)outputs/**",
|
|
3245
|
-
]);
|
|
3222
|
+
result = await git(repo, ["add", "-A"]);
|
|
3246
3223
|
}
|
|
3247
3224
|
if (!result.ok) {
|
|
3248
3225
|
return { ok: false, error: `Failed to stage merge-conflict changes: ${result.stderr || result.stdout}` };
|
|
3249
3226
|
}
|
|
3250
3227
|
}
|
|
3228
|
+
const unstageArtifacts = await unstageSandboxArtifactPaths(repo);
|
|
3229
|
+
if (!unstageArtifacts.ok) {
|
|
3230
|
+
return {
|
|
3231
|
+
ok: false,
|
|
3232
|
+
error: `Failed to unstage sandbox artifact paths: ${unstageArtifacts.stderr || unstageArtifacts.stdout}`,
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3251
3235
|
|
|
3252
3236
|
const cachedDiffQuiet = await git(repo, ["diff", "--cached", "--quiet"]);
|
|
3253
3237
|
let headSha = await currentRefSha(repo, "HEAD");
|
|
@@ -3610,6 +3594,85 @@ export function shouldUseCodexCliForExecutor(executor: string): boolean {
|
|
|
3610
3594
|
return executor.trim().toLowerCase() === "openai_codex";
|
|
3611
3595
|
}
|
|
3612
3596
|
|
|
3597
|
+
type MaskedRepoLocalCodexFile = {
|
|
3598
|
+
codexPath: string;
|
|
3599
|
+
backupPath: string;
|
|
3600
|
+
};
|
|
3601
|
+
|
|
3602
|
+
function codexProjectConfigRoots(repo: string, env: Record<string, string>): string[] {
|
|
3603
|
+
const roots: string[] = [];
|
|
3604
|
+
const seen = new Set<string>();
|
|
3605
|
+
const add = (raw: unknown) => {
|
|
3606
|
+
const text = String(raw ?? "").trim();
|
|
3607
|
+
if (!text) return;
|
|
3608
|
+
const root = resolve(text);
|
|
3609
|
+
const key = root.toLowerCase();
|
|
3610
|
+
if (seen.has(key)) return;
|
|
3611
|
+
seen.add(key);
|
|
3612
|
+
roots.push(root);
|
|
3613
|
+
};
|
|
3614
|
+
add(repo);
|
|
3615
|
+
for (const key of [
|
|
3616
|
+
"PUSHPALS_REPO_ROOT_OVERRIDE",
|
|
3617
|
+
"PUSHPALS_PROJECT_ROOT_OVERRIDE",
|
|
3618
|
+
"PUSHPALS_ASSIGNED_REPO_ROOT",
|
|
3619
|
+
"PUSHPALS_REPO_PATH",
|
|
3620
|
+
]) {
|
|
3621
|
+
add(env[key]);
|
|
3622
|
+
}
|
|
3623
|
+
return roots;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
function maskRepoLocalCodexFilesForCodexCli(
|
|
3627
|
+
repo: string,
|
|
3628
|
+
env: Record<string, string>,
|
|
3629
|
+
): MaskedRepoLocalCodexFile[] {
|
|
3630
|
+
const masked: MaskedRepoLocalCodexFile[] = [];
|
|
3631
|
+
for (const root of codexProjectConfigRoots(repo, env)) {
|
|
3632
|
+
const codexPath = resolve(root, ".codex");
|
|
3633
|
+
if (!existsSync(codexPath)) continue;
|
|
3634
|
+
try {
|
|
3635
|
+
if (lstatSync(codexPath).isDirectory()) continue;
|
|
3636
|
+
let backupPath = resolve(root, `.codex.pushpals-masked-${process.pid}-${masked.length}`);
|
|
3637
|
+
let suffix = 0;
|
|
3638
|
+
while (existsSync(backupPath)) {
|
|
3639
|
+
suffix += 1;
|
|
3640
|
+
backupPath = resolve(
|
|
3641
|
+
root,
|
|
3642
|
+
`.codex.pushpals-masked-${process.pid}-${masked.length}-${suffix}`,
|
|
3643
|
+
);
|
|
3644
|
+
}
|
|
3645
|
+
renameSync(codexPath, backupPath);
|
|
3646
|
+
masked.push({ codexPath, backupPath });
|
|
3647
|
+
console.warn(
|
|
3648
|
+
`[WorkerPals] Temporarily masked repo-local .codex file so Codex CLI can use CODEX_HOME: ${codexPath}`,
|
|
3649
|
+
);
|
|
3650
|
+
} catch (error) {
|
|
3651
|
+
console.warn(
|
|
3652
|
+
`[WorkerPals] Failed to mask repo-local .codex file ${codexPath}: ${String(error)}`,
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
return masked;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
function restoreRepoLocalCodexFilesForCodexCli(masked: MaskedRepoLocalCodexFile[]): void {
|
|
3660
|
+
for (const entry of [...masked].reverse()) {
|
|
3661
|
+
try {
|
|
3662
|
+
if (existsSync(entry.codexPath)) {
|
|
3663
|
+
rmSync(entry.codexPath, { recursive: true, force: true });
|
|
3664
|
+
}
|
|
3665
|
+
if (existsSync(entry.backupPath)) {
|
|
3666
|
+
renameSync(entry.backupPath, entry.codexPath);
|
|
3667
|
+
}
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
console.warn(
|
|
3670
|
+
`[WorkerPals] Failed to restore repo-local .codex file ${entry.codexPath}: ${String(error)}`,
|
|
3671
|
+
);
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
|
|
3613
3676
|
function normalizeCodexReasoningEffort(
|
|
3614
3677
|
value: unknown,
|
|
3615
3678
|
model = "",
|
|
@@ -3720,11 +3783,13 @@ async function generateCommitMessageFromDiffViaCodex(
|
|
|
3720
3783
|
if (model) cmd.push("-m", model);
|
|
3721
3784
|
cmd.push("-");
|
|
3722
3785
|
|
|
3786
|
+
const env = buildWorkerSandboxWritableEnv(repo);
|
|
3787
|
+
const codexMask = maskRepoLocalCodexFilesForCodexCli(repo, env);
|
|
3723
3788
|
try {
|
|
3724
3789
|
const stdinText = `${prompt.systemPrompt}\n\n${prompt.userMessage}`;
|
|
3725
3790
|
const proc = Bun.spawn(cmd, {
|
|
3726
3791
|
cwd: repo,
|
|
3727
|
-
env
|
|
3792
|
+
env,
|
|
3728
3793
|
stdout: "pipe",
|
|
3729
3794
|
stderr: "pipe",
|
|
3730
3795
|
stdin: new Blob([stdinText]),
|
|
@@ -3759,6 +3824,7 @@ async function generateCommitMessageFromDiffViaCodex(
|
|
|
3759
3824
|
} catch {
|
|
3760
3825
|
return null;
|
|
3761
3826
|
} finally {
|
|
3827
|
+
restoreRepoLocalCodexFilesForCodexCli(codexMask);
|
|
3762
3828
|
try {
|
|
3763
3829
|
unlinkSync(tmpOutputPath);
|
|
3764
3830
|
} catch {
|
|
@@ -3938,9 +4004,7 @@ function hasInvalidRepoPathHint(values: string[]): boolean {
|
|
|
3938
4004
|
return values.some((entry) => normalizeStagePath(entry) === null);
|
|
3939
4005
|
}
|
|
3940
4006
|
|
|
3941
|
-
|
|
3942
|
-
return normalizeAutonomyComponentArea(value);
|
|
3943
|
-
}
|
|
4007
|
+
const SANDBOX_STAGE_ARTIFACT_PATHS = ["workspace", "outputs", ".codex"];
|
|
3944
4008
|
|
|
3945
4009
|
function taskExecuteOrigin(params: Record<string, unknown>): "autonomy" | "user" {
|
|
3946
4010
|
const explicit = String(params.origin ?? "")
|
|
@@ -3961,20 +4025,11 @@ export function collectWriteScopeIssuesFromChangedPaths(
|
|
|
3961
4025
|
changedPaths: string[],
|
|
3962
4026
|
planning: TaskExecutePlanning,
|
|
3963
4027
|
): string[] {
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
const forbidden = toStringArray(planning.scope.forbiddenGlobs ?? []);
|
|
3970
|
-
const issues: string[] = [];
|
|
3971
|
-
const forbiddenTouched = normalizedChangedPaths.filter((path) =>
|
|
3972
|
-
forbidden.some((glob) => matchesGlob(path, glob)),
|
|
3973
|
-
);
|
|
3974
|
-
if (forbiddenTouched.length > 0) {
|
|
3975
|
-
issues.push(`modified paths matching forbiddenGlobs: ${forbiddenTouched.join(", ")}`);
|
|
3976
|
-
}
|
|
3977
|
-
return issues;
|
|
4028
|
+
void changedPaths;
|
|
4029
|
+
void planning;
|
|
4030
|
+
// WorkerPals run in isolated worktrees and may write anywhere in that repo sandbox.
|
|
4031
|
+
// Scope hints guide planning/review, but they are not hard write privileges.
|
|
4032
|
+
return [];
|
|
3978
4033
|
}
|
|
3979
4034
|
|
|
3980
4035
|
function sanitizeTaskExecutePlanningPathHints(value: unknown): unknown {
|
|
@@ -4096,37 +4151,6 @@ function validateTaskExecutePlanning(
|
|
|
4096
4151
|
message: "task.execute planning.targetPaths must contain literal repo-relative paths",
|
|
4097
4152
|
};
|
|
4098
4153
|
}
|
|
4099
|
-
const normalizedWriteGlobs = isStringArray(scope.writeGlobs)
|
|
4100
|
-
? toStringArray(scope.writeGlobs)
|
|
4101
|
-
: [];
|
|
4102
|
-
const allowMultiRootAutonomyScope =
|
|
4103
|
-
origin === "autonomy" &&
|
|
4104
|
-
reviewAgentAllowsMultiRootScope(options?.reviewAgentResolutionType);
|
|
4105
|
-
if (origin === "autonomy") {
|
|
4106
|
-
const declaredComponentArea = asAutonomyComponentArea(options?.autonomyComponentArea);
|
|
4107
|
-
if (!allowMultiRootAutonomyScope && declaredComponentArea) {
|
|
4108
|
-
const inferredComponentArea = deriveAutonomyComponentArea(
|
|
4109
|
-
normalizedTargetPaths,
|
|
4110
|
-
normalizedWriteGlobs,
|
|
4111
|
-
);
|
|
4112
|
-
if (inferredComponentArea && declaredComponentArea !== inferredComponentArea) {
|
|
4113
|
-
return {
|
|
4114
|
-
ok: false,
|
|
4115
|
-
message: "task.execute planning.targetPaths do not match autonomy componentArea",
|
|
4116
|
-
};
|
|
4117
|
-
}
|
|
4118
|
-
}
|
|
4119
|
-
} else if (normalizedWriteGlobs.length > 0) {
|
|
4120
|
-
const uncoveredPaths = normalizedTargetPaths.filter(
|
|
4121
|
-
(targetPath) => !normalizedWriteGlobs.some((glob) => matchesGlob(targetPath, glob)),
|
|
4122
|
-
);
|
|
4123
|
-
if (uncoveredPaths.length > 0) {
|
|
4124
|
-
return {
|
|
4125
|
-
ok: false,
|
|
4126
|
-
message: `task.execute planning.targetPaths must be covered by planning.scope.writeGlobs: ${uncoveredPaths.join(", ")}`,
|
|
4127
|
-
};
|
|
4128
|
-
}
|
|
4129
|
-
}
|
|
4130
4154
|
}
|
|
4131
4155
|
|
|
4132
4156
|
if (planning.discovery !== undefined) {
|
|
@@ -4353,10 +4377,12 @@ async function runCodexCriticReview(
|
|
|
4353
4377
|
"-",
|
|
4354
4378
|
];
|
|
4355
4379
|
|
|
4380
|
+
const env = buildWorkerSandboxWritableEnv(repo);
|
|
4381
|
+
const codexMask = maskRepoLocalCodexFilesForCodexCli(repo, env);
|
|
4356
4382
|
try {
|
|
4357
4383
|
const proc = Bun.spawn(cmd, {
|
|
4358
4384
|
cwd: repo,
|
|
4359
|
-
env
|
|
4385
|
+
env,
|
|
4360
4386
|
stdout: "pipe",
|
|
4361
4387
|
stderr: "pipe",
|
|
4362
4388
|
stdin: new Blob([criticInstruction]),
|
|
@@ -4436,6 +4462,13 @@ async function runCodexCriticReview(
|
|
|
4436
4462
|
} catch (err) {
|
|
4437
4463
|
onLog?.("stderr", `[CriticGate] Codex error: ${toSingleLine(err, 220)} (skipping).`);
|
|
4438
4464
|
return null;
|
|
4465
|
+
} finally {
|
|
4466
|
+
restoreRepoLocalCodexFilesForCodexCli(codexMask);
|
|
4467
|
+
try {
|
|
4468
|
+
unlinkSync(tmpOutputPath);
|
|
4469
|
+
} catch {
|
|
4470
|
+
/* ignore */
|
|
4471
|
+
}
|
|
4439
4472
|
}
|
|
4440
4473
|
}
|
|
4441
4474
|
|
|
@@ -4750,7 +4783,7 @@ export async function executeJob(
|
|
|
4750
4783
|
[
|
|
4751
4784
|
result.stderr ?? "",
|
|
4752
4785
|
validationOutsideTaskScope
|
|
4753
|
-
? "Validation failures appear outside the task
|
|
4786
|
+
? "Validation failures appear outside the task target/relevance hints and are treated as pre-existing repo blockers."
|
|
4754
4787
|
: "",
|
|
4755
4788
|
...quality.validationRuns.flatMap((run) => [run.stdout, run.stderr]).filter(Boolean),
|
|
4756
4789
|
]
|