@kbediako/codex-orchestrator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +238 -0
- package/dist/bin/codex-orchestrator.js +507 -0
- package/dist/orchestrator/src/agents/builder.js +16 -0
- package/dist/orchestrator/src/agents/index.js +4 -0
- package/dist/orchestrator/src/agents/planner.js +17 -0
- package/dist/orchestrator/src/agents/reviewer.js +13 -0
- package/dist/orchestrator/src/agents/tester.js +13 -0
- package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +20 -0
- package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +164 -0
- package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +32 -0
- package/dist/orchestrator/src/cli/adapters/CommandTester.js +33 -0
- package/dist/orchestrator/src/cli/adapters/index.js +4 -0
- package/dist/orchestrator/src/cli/config/userConfig.js +28 -0
- package/dist/orchestrator/src/cli/doctor.js +48 -0
- package/dist/orchestrator/src/cli/events/runEvents.js +84 -0
- package/dist/orchestrator/src/cli/exec/command.js +56 -0
- package/dist/orchestrator/src/cli/exec/context.js +108 -0
- package/dist/orchestrator/src/cli/exec/experience.js +77 -0
- package/dist/orchestrator/src/cli/exec/finalization.js +140 -0
- package/dist/orchestrator/src/cli/exec/learning.js +62 -0
- package/dist/orchestrator/src/cli/exec/stageRunner.js +71 -0
- package/dist/orchestrator/src/cli/exec/summary.js +109 -0
- package/dist/orchestrator/src/cli/exec/telemetry.js +18 -0
- package/dist/orchestrator/src/cli/exec/tfgrpo.js +200 -0
- package/dist/orchestrator/src/cli/exec/tfgrpoArtifacts.js +19 -0
- package/dist/orchestrator/src/cli/exec/types.js +1 -0
- package/dist/orchestrator/src/cli/init.js +64 -0
- package/dist/orchestrator/src/cli/mcp.js +124 -0
- package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +404 -0
- package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +138 -0
- package/dist/orchestrator/src/cli/orchestrator.js +554 -0
- package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +32 -0
- package/dist/orchestrator/src/cli/pipelines/designReference.js +72 -0
- package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +71 -0
- package/dist/orchestrator/src/cli/pipelines/index.js +34 -0
- package/dist/orchestrator/src/cli/run/environment.js +24 -0
- package/dist/orchestrator/src/cli/run/manifest.js +367 -0
- package/dist/orchestrator/src/cli/run/manifestPersister.js +88 -0
- package/dist/orchestrator/src/cli/run/runPaths.js +30 -0
- package/dist/orchestrator/src/cli/selfCheck.js +12 -0
- package/dist/orchestrator/src/cli/services/commandRunner.js +420 -0
- package/dist/orchestrator/src/cli/services/controlPlaneService.js +107 -0
- package/dist/orchestrator/src/cli/services/execRuntime.js +69 -0
- package/dist/orchestrator/src/cli/services/pipelineResolver.js +47 -0
- package/dist/orchestrator/src/cli/services/runPreparation.js +82 -0
- package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
- package/dist/orchestrator/src/cli/services/schedulerService.js +42 -0
- package/dist/orchestrator/src/cli/tasks/taskMetadata.js +19 -0
- package/dist/orchestrator/src/cli/telemetry/schema.js +8 -0
- package/dist/orchestrator/src/cli/types.js +1 -0
- package/dist/orchestrator/src/cli/ui/HudApp.js +112 -0
- package/dist/orchestrator/src/cli/ui/controller.js +26 -0
- package/dist/orchestrator/src/cli/ui/store.js +240 -0
- package/dist/orchestrator/src/cli/utils/enforcementMode.js +12 -0
- package/dist/orchestrator/src/cli/utils/fs.js +8 -0
- package/dist/orchestrator/src/cli/utils/interactive.js +25 -0
- package/dist/orchestrator/src/cli/utils/jsonlWriter.js +10 -0
- package/dist/orchestrator/src/cli/utils/optionalDeps.js +30 -0
- package/dist/orchestrator/src/cli/utils/packageInfo.js +25 -0
- package/dist/orchestrator/src/cli/utils/planFormatter.js +49 -0
- package/dist/orchestrator/src/cli/utils/runId.js +7 -0
- package/dist/orchestrator/src/cli/utils/specGuardRunner.js +26 -0
- package/dist/orchestrator/src/cli/utils/strings.js +8 -0
- package/dist/orchestrator/src/cli/utils/time.js +6 -0
- package/dist/orchestrator/src/control-plane/drift-reporter.js +109 -0
- package/dist/orchestrator/src/control-plane/index.js +3 -0
- package/dist/orchestrator/src/control-plane/request-builder.js +217 -0
- package/dist/orchestrator/src/control-plane/types.js +1 -0
- package/dist/orchestrator/src/control-plane/validator.js +50 -0
- package/dist/orchestrator/src/credentials/CredentialBroker.js +1 -0
- package/dist/orchestrator/src/events/EventBus.js +25 -0
- package/dist/orchestrator/src/learning/crystalizer.js +108 -0
- package/dist/orchestrator/src/learning/harvester.js +146 -0
- package/dist/orchestrator/src/learning/manifest.js +56 -0
- package/dist/orchestrator/src/learning/runner.js +177 -0
- package/dist/orchestrator/src/learning/validator.js +164 -0
- package/dist/orchestrator/src/logger.js +20 -0
- package/dist/orchestrator/src/manager.js +388 -0
- package/dist/orchestrator/src/persistence/ArtifactStager.js +95 -0
- package/dist/orchestrator/src/persistence/ExperienceStore.js +210 -0
- package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +65 -0
- package/dist/orchestrator/src/persistence/RunManifestWriter.js +23 -0
- package/dist/orchestrator/src/persistence/TaskStateStore.js +172 -0
- package/dist/orchestrator/src/persistence/identifierGuards.js +1 -0
- package/dist/orchestrator/src/persistence/lockFile.js +26 -0
- package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +26 -0
- package/dist/orchestrator/src/persistence/sanitizeRunId.js +8 -0
- package/dist/orchestrator/src/persistence/sanitizeTaskId.js +8 -0
- package/dist/orchestrator/src/persistence/writeAtomicFile.js +4 -0
- package/dist/orchestrator/src/privacy/guard.js +111 -0
- package/dist/orchestrator/src/scheduler/index.js +1 -0
- package/dist/orchestrator/src/scheduler/plan.js +171 -0
- package/dist/orchestrator/src/scheduler/types.js +1 -0
- package/dist/orchestrator/src/sync/CloudRunsClient.js +1 -0
- package/dist/orchestrator/src/sync/CloudRunsHttpClient.js +82 -0
- package/dist/orchestrator/src/sync/CloudSyncWorker.js +206 -0
- package/dist/orchestrator/src/sync/createCloudSyncWorker.js +15 -0
- package/dist/orchestrator/src/types.js +1 -0
- package/dist/orchestrator/src/utils/atomicWrite.js +15 -0
- package/dist/orchestrator/src/utils/errorMessage.js +14 -0
- package/dist/orchestrator/src/utils/executionMode.js +69 -0
- package/dist/packages/control-plane-schemas/src/index.js +1 -0
- package/dist/packages/control-plane-schemas/src/run-request.js +548 -0
- package/dist/packages/orchestrator/src/exec/handle-service.js +203 -0
- package/dist/packages/orchestrator/src/exec/session-manager.js +147 -0
- package/dist/packages/orchestrator/src/exec/unified-exec.js +432 -0
- package/dist/packages/orchestrator/src/index.js +3 -0
- package/dist/packages/orchestrator/src/instructions/loader.js +101 -0
- package/dist/packages/orchestrator/src/instructions/promptPacks.js +151 -0
- package/dist/packages/orchestrator/src/notifications/index.js +74 -0
- package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +142 -0
- package/dist/packages/orchestrator/src/tool-orchestrator.js +161 -0
- package/dist/packages/sdk-node/src/orchestrator.js +195 -0
- package/dist/packages/shared/config/designConfig.js +495 -0
- package/dist/packages/shared/config/env.js +37 -0
- package/dist/packages/shared/config/index.js +2 -0
- package/dist/packages/shared/design-artifacts/writer.js +221 -0
- package/dist/packages/shared/events/serializer.js +84 -0
- package/dist/packages/shared/events/types.js +1 -0
- package/dist/packages/shared/manifest/artifactUtils.js +36 -0
- package/dist/packages/shared/manifest/designArtifacts.js +665 -0
- package/dist/packages/shared/manifest/fileIO.js +29 -0
- package/dist/packages/shared/manifest/toolRuns.js +78 -0
- package/dist/packages/shared/manifest/toolkitArtifacts.js +223 -0
- package/dist/packages/shared/manifest/types.js +5 -0
- package/dist/packages/shared/manifest/validator.js +73 -0
- package/dist/packages/shared/manifest/writer.js +2 -0
- package/dist/packages/shared/streams/stdio.js +112 -0
- package/dist/scripts/design/pipeline/advanced-assets.js +466 -0
- package/dist/scripts/design/pipeline/componentize.js +74 -0
- package/dist/scripts/design/pipeline/context.js +34 -0
- package/dist/scripts/design/pipeline/extract.js +249 -0
- package/dist/scripts/design/pipeline/optionalDeps.js +107 -0
- package/dist/scripts/design/pipeline/prepare.js +46 -0
- package/dist/scripts/design/pipeline/reference.js +94 -0
- package/dist/scripts/design/pipeline/state.js +206 -0
- package/dist/scripts/design/pipeline/toolkit/common.js +94 -0
- package/dist/scripts/design/pipeline/toolkit/extract.js +258 -0
- package/dist/scripts/design/pipeline/toolkit/publish.js +202 -0
- package/dist/scripts/design/pipeline/toolkit/publishActions.js +12 -0
- package/dist/scripts/design/pipeline/toolkit/reference.js +846 -0
- package/dist/scripts/design/pipeline/toolkit/snapshot.js +882 -0
- package/dist/scripts/design/pipeline/toolkit/tokens.js +456 -0
- package/dist/scripts/design/pipeline/visual-regression.js +137 -0
- package/dist/scripts/design/pipeline/write-artifacts.js +61 -0
- package/package.json +97 -0
- package/schemas/manifest.json +1064 -0
- package/templates/README.md +12 -0
- package/templates/codex/mcp-client.json +8 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { CommandPlanner } from '../adapters/index.js';
|
|
3
|
+
import { PipelineResolver } from './pipelineResolver.js';
|
|
4
|
+
import { sanitizeTaskId } from '../run/environment.js';
|
|
5
|
+
import { loadTaskMetadata } from '../tasks/taskMetadata.js';
|
|
6
|
+
import { resolvePipeline } from '../pipelines/index.js';
|
|
7
|
+
import { findPipeline } from '../config/userConfig.js';
|
|
8
|
+
import { logger } from '../../logger.js';
|
|
9
|
+
export function overrideTaskEnvironment(baseEnv, taskId) {
|
|
10
|
+
if (!taskId) {
|
|
11
|
+
return { ...baseEnv };
|
|
12
|
+
}
|
|
13
|
+
const sanitized = sanitizeTaskId(taskId);
|
|
14
|
+
return { ...baseEnv, taskId: sanitized };
|
|
15
|
+
}
|
|
16
|
+
export function resolveTargetStageId(explicit, fallback, envTarget = process.env.CODEX_ORCHESTRATOR_TARGET_STAGE) {
|
|
17
|
+
const normalizedExplicit = explicit?.trim();
|
|
18
|
+
if (normalizedExplicit) {
|
|
19
|
+
return normalizedExplicit;
|
|
20
|
+
}
|
|
21
|
+
const normalizedFallback = fallback?.trim();
|
|
22
|
+
if (normalizedFallback) {
|
|
23
|
+
return normalizedFallback;
|
|
24
|
+
}
|
|
25
|
+
if (typeof envTarget === 'string' && envTarget.trim().length > 0) {
|
|
26
|
+
return envTarget.trim();
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
export async function prepareRun(options) {
|
|
31
|
+
logger.info(`prepareRun start for pipeline ${options.pipelineId ?? options.pipeline?.id ?? '<default>'}`);
|
|
32
|
+
const env = overrideTaskEnvironment(options.baseEnv, options.taskIdOverride);
|
|
33
|
+
const resolver = options.resolver ?? new PipelineResolver();
|
|
34
|
+
logger.info(`prepareRun resolving pipeline ${options.pipelineId ?? '<default>'}`);
|
|
35
|
+
const resolvedPipeline = options.pipeline
|
|
36
|
+
? {
|
|
37
|
+
pipeline: options.pipeline,
|
|
38
|
+
source: options.pipelineSource ?? null,
|
|
39
|
+
envOverrides: options.envOverrides ?? {}
|
|
40
|
+
}
|
|
41
|
+
: await resolver.resolve(env, { pipelineId: options.pipelineId });
|
|
42
|
+
logger.info(`prepareRun resolved pipeline ${resolvedPipeline.pipeline.id}`);
|
|
43
|
+
const metadata = await loadTaskMetadata(env);
|
|
44
|
+
logger.info(`prepareRun loaded metadata for task ${metadata.id}`);
|
|
45
|
+
const taskContext = createTaskContext(metadata);
|
|
46
|
+
const targetId = resolveTargetStageId(options.targetStageId, options.planTargetFallback ?? null);
|
|
47
|
+
const planner = options.planner ?? new CommandPlanner(resolvedPipeline.pipeline, { targetStageId: targetId });
|
|
48
|
+
logger.info(`prepareRun running planner for pipeline ${resolvedPipeline.pipeline.id}`);
|
|
49
|
+
const planPreview = await planner.plan(taskContext);
|
|
50
|
+
logger.info(`prepareRun planner completed for pipeline ${resolvedPipeline.pipeline.id}`);
|
|
51
|
+
logger.info(`prepareRun complete for pipeline ${resolvedPipeline.pipeline.id}`);
|
|
52
|
+
return {
|
|
53
|
+
env,
|
|
54
|
+
pipeline: resolvedPipeline.pipeline,
|
|
55
|
+
pipelineSource: resolvedPipeline.source ?? null,
|
|
56
|
+
envOverrides: resolvedPipeline.envOverrides ?? {},
|
|
57
|
+
planner,
|
|
58
|
+
plannerTargetId: planPreview?.targetId ?? targetId,
|
|
59
|
+
taskContext,
|
|
60
|
+
metadata,
|
|
61
|
+
resolver,
|
|
62
|
+
planPreview
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function resolvePipelineForResume(env, manifest, config) {
|
|
66
|
+
const existing = findPipeline(config ?? null, manifest.pipeline_id);
|
|
67
|
+
if (existing) {
|
|
68
|
+
return existing;
|
|
69
|
+
}
|
|
70
|
+
const { pipeline } = resolvePipeline(env, { pipelineId: manifest.pipeline_id, config });
|
|
71
|
+
return pipeline;
|
|
72
|
+
}
|
|
73
|
+
export function createTaskContext(metadata) {
|
|
74
|
+
return {
|
|
75
|
+
id: metadata.id,
|
|
76
|
+
title: metadata.title,
|
|
77
|
+
description: undefined,
|
|
78
|
+
metadata: {
|
|
79
|
+
slug: metadata.slug
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { relativeToRepo } from '../run/runPaths.js';
|
|
3
|
+
import { writeJsonAtomic } from '../utils/fs.js';
|
|
4
|
+
import { persistManifest } from '../run/manifestPersister.js';
|
|
5
|
+
export function applyHandlesToRunSummary(runSummary, manifest) {
|
|
6
|
+
if (!manifest.handles || manifest.handles.length === 0) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
runSummary.handles = manifest.handles.map((handle) => ({
|
|
10
|
+
handleId: handle.handle_id,
|
|
11
|
+
correlationId: handle.correlation_id,
|
|
12
|
+
stageId: handle.stage_id,
|
|
13
|
+
status: handle.status,
|
|
14
|
+
frameCount: handle.frame_count,
|
|
15
|
+
latestSequence: handle.latest_sequence
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
export function applyPrivacyToRunSummary(runSummary, manifest) {
|
|
19
|
+
if (!manifest.privacy) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
runSummary.privacy = {
|
|
23
|
+
mode: manifest.privacy.mode,
|
|
24
|
+
totalFrames: manifest.privacy.totals.total_frames,
|
|
25
|
+
redactedFrames: manifest.privacy.totals.redacted_frames,
|
|
26
|
+
blockedFrames: manifest.privacy.totals.blocked_frames,
|
|
27
|
+
allowedFrames: manifest.privacy.totals.allowed_frames
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function persistRunSummary(env, paths, manifest, runSummary, persister) {
|
|
31
|
+
const summaryPath = join(paths.runDir, 'run-summary.json');
|
|
32
|
+
await writeJsonAtomic(summaryPath, runSummary);
|
|
33
|
+
manifest.run_summary_path = relativeToRepo(env, summaryPath);
|
|
34
|
+
await persistManifest(paths, manifest, persister, { force: true });
|
|
35
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { sanitizeTaskId } from '../run/environment.js';
|
|
2
|
+
import { persistManifest } from '../run/manifestPersister.js';
|
|
3
|
+
import { buildSchedulerRunSummary, createSchedulerPlan, finalizeSchedulerPlan, serializeSchedulerPlan } from '../../scheduler/index.js';
|
|
4
|
+
import { isoTimestamp } from '../utils/time.js';
|
|
5
|
+
export class SchedulerService {
|
|
6
|
+
now;
|
|
7
|
+
constructor(now = () => new Date()) {
|
|
8
|
+
this.now = now;
|
|
9
|
+
}
|
|
10
|
+
async createPlanForRun(options) {
|
|
11
|
+
const plan = createSchedulerPlan(options.controlPlaneResult.request, {
|
|
12
|
+
now: this.now,
|
|
13
|
+
instancePrefix: sanitizeTaskId(options.env.taskId)
|
|
14
|
+
});
|
|
15
|
+
this.attachSchedulerPlanToManifest(options.manifest, plan);
|
|
16
|
+
await persistManifest(options.paths, options.manifest, options.persister, { force: true });
|
|
17
|
+
return plan;
|
|
18
|
+
}
|
|
19
|
+
async finalizePlan(options) {
|
|
20
|
+
const finalStatus = this.resolveSchedulerFinalStatus(options.manifest.status);
|
|
21
|
+
const finalizedAt = options.manifest.completed_at ?? isoTimestamp();
|
|
22
|
+
finalizeSchedulerPlan(options.plan, finalStatus, finalizedAt);
|
|
23
|
+
this.attachSchedulerPlanToManifest(options.manifest, options.plan);
|
|
24
|
+
await persistManifest(options.paths, options.manifest, options.persister, { force: true });
|
|
25
|
+
}
|
|
26
|
+
applySchedulerToRunSummary(runSummary, plan) {
|
|
27
|
+
runSummary.scheduler = buildSchedulerRunSummary(plan);
|
|
28
|
+
}
|
|
29
|
+
resolveSchedulerFinalStatus(status) {
|
|
30
|
+
switch (status) {
|
|
31
|
+
case 'succeeded':
|
|
32
|
+
return 'succeeded';
|
|
33
|
+
case 'in_progress':
|
|
34
|
+
return 'running';
|
|
35
|
+
default:
|
|
36
|
+
return 'failed';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
attachSchedulerPlanToManifest(manifest, plan) {
|
|
40
|
+
manifest.scheduler = serializeSchedulerPlan(plan);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
export async function loadTaskMetadata(env) {
|
|
4
|
+
const tasksPath = join(env.repoRoot, 'tasks', 'index.json');
|
|
5
|
+
try {
|
|
6
|
+
const raw = await readFile(tasksPath, 'utf8');
|
|
7
|
+
const data = JSON.parse(raw);
|
|
8
|
+
const match = data.items.find((item) => item.id === env.taskId);
|
|
9
|
+
if (match) {
|
|
10
|
+
return { id: match.id, slug: match.slug, title: match.title };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code !== 'ENOENT') {
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return { id: env.taskId, slug: env.taskId, title: `Task ${env.taskId}` };
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getManifestSchema, validateManifest } from '../../../../packages/shared/manifest/validator.js';
|
|
2
|
+
export const CLI_MANIFEST_SCHEMA = getManifestSchema();
|
|
3
|
+
export function getTelemetrySchemas() {
|
|
4
|
+
return { manifest: CLI_MANIFEST_SCHEMA };
|
|
5
|
+
}
|
|
6
|
+
export function validateCliManifest(candidate) {
|
|
7
|
+
return validateManifest(candidate);
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { createInitialHudState } from './store.js';
|
|
5
|
+
export function HudApp({ store, footerNote }) {
|
|
6
|
+
const [state, setState] = useState(store.getState());
|
|
7
|
+
const [tick, setTick] = useState(() => Date.now());
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const unsubscribe = store.subscribe(setState);
|
|
10
|
+
return () => unsubscribe();
|
|
11
|
+
}, [store]);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const timer = setInterval(() => setTick(Date.now()), 1000);
|
|
14
|
+
return () => clearInterval(timer);
|
|
15
|
+
}, []);
|
|
16
|
+
const elapsed = useMemo(() => formatElapsed(state.startedAt, state.completedAt, tick), [state.startedAt, state.completedAt, tick]);
|
|
17
|
+
if (state.status === 'idle' && !state.runId) {
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "codex-orchestrator HUD" }), _jsx(Text, { color: "gray", children: "Awaiting run events..." })] }));
|
|
19
|
+
}
|
|
20
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, { state: state, elapsed: elapsed }), _jsx(Text, { children: " " }), _jsx(StageList, { stages: state.stages }), _jsx(Text, { children: " " }), _jsx(LogPanel, { logs: state.logs }), _jsx(Text, { children: " " }), _jsx(Footer, { manifestPath: state.manifestPath, metricsPath: state.metricsPath, runSummaryPath: state.runSummaryPath, note: footerNote })] }));
|
|
21
|
+
}
|
|
22
|
+
function Header({ state, elapsed }) {
|
|
23
|
+
const statusColor = colorForStatus(state.status);
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["HUD | TASK ", state.taskId ?? '-', " | RUN ", state.runId ?? '-', " | STATUS", ' ', _jsxs(Text, { color: statusColor, children: ["[", state.status.toUpperCase(), "]"] }), " | ELAPSED ", elapsed] }), _jsxs(Text, { color: "gray", children: [state.pipelineTitle ?? 'pipeline', " | manifest: ", state.manifestPath ?? 'pending', " | log: ", state.logPath ?? 'pending'] })] }));
|
|
25
|
+
}
|
|
26
|
+
function StageList({ stages }) {
|
|
27
|
+
if (!stages || stages.length === 0) {
|
|
28
|
+
return (_jsx(Box, { children: _jsx(Text, { color: "gray", children: "No stage metadata available yet." }) }));
|
|
29
|
+
}
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", children: "Stages" }), stages.map((stage) => (_jsx(StageRow, { stage: stage }, stage.id)))] }));
|
|
31
|
+
}
|
|
32
|
+
function StageRow({ stage }) {
|
|
33
|
+
const statusColor = colorForStatus(stage.status);
|
|
34
|
+
const label = stage.title || stage.id;
|
|
35
|
+
const summary = stage.summary ?? stage.logPath ?? '';
|
|
36
|
+
return (_jsxs(Text, { children: ["[", String(stage.index).padStart(2, '0'), "] ", _jsx(Text, { color: statusColor, children: stage.status.toUpperCase() }), " ", label, summary ? ` - ${summary}` : ''] }));
|
|
37
|
+
}
|
|
38
|
+
function LogPanel({ logs }) {
|
|
39
|
+
const tail = logs.slice(-8);
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "cyan", children: ["Log Tail (latest ", tail.length, ")"] }), tail.length === 0 ? (_jsx(Text, { color: "gray", children: "No logs yet." })) : (tail.map((log) => (_jsxs(Text, { color: colorForLevel(log.level), children: [formatClock(log.timestamp), " ", log.stageId ? `[${log.stageId}] ` : '', log.message.trim()] }, log.id))))] }));
|
|
41
|
+
}
|
|
42
|
+
function Footer({ manifestPath, metricsPath, runSummaryPath, note }) {
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "gray", children: ["read-only HUD | manifest: ", manifestPath ?? 'pending', " | metrics: ", metricsPath ?? 'pending', " | summary:", ' ', runSummaryPath ?? 'pending'] }), note ? _jsx(Text, { color: "gray", children: note }) : null] }));
|
|
44
|
+
}
|
|
45
|
+
function colorForStatus(status) {
|
|
46
|
+
switch (status) {
|
|
47
|
+
case 'running':
|
|
48
|
+
case 'in_progress':
|
|
49
|
+
return 'cyan';
|
|
50
|
+
case 'succeeded':
|
|
51
|
+
return 'green';
|
|
52
|
+
case 'skipped':
|
|
53
|
+
return 'yellow';
|
|
54
|
+
case 'failed':
|
|
55
|
+
case 'cancelled':
|
|
56
|
+
return 'red';
|
|
57
|
+
case 'pending':
|
|
58
|
+
return 'gray';
|
|
59
|
+
default:
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function colorForLevel(level) {
|
|
64
|
+
switch (level) {
|
|
65
|
+
case 'warn':
|
|
66
|
+
return 'yellow';
|
|
67
|
+
case 'error':
|
|
68
|
+
return 'red';
|
|
69
|
+
case 'debug':
|
|
70
|
+
return 'gray';
|
|
71
|
+
default:
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function formatElapsed(start, end, nowMs) {
|
|
76
|
+
if (!start) {
|
|
77
|
+
return '--:--';
|
|
78
|
+
}
|
|
79
|
+
const startMs = Date.parse(start);
|
|
80
|
+
const endMs = end ? Date.parse(end) : nowMs;
|
|
81
|
+
if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
|
|
82
|
+
return '--:--';
|
|
83
|
+
}
|
|
84
|
+
const totalSeconds = Math.max(0, Math.floor((endMs - startMs) / 1000));
|
|
85
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
86
|
+
const seconds = totalSeconds % 60;
|
|
87
|
+
const hours = Math.floor(minutes / 60);
|
|
88
|
+
const minsRemaining = minutes % 60;
|
|
89
|
+
if (hours > 0) {
|
|
90
|
+
return `${String(hours).padStart(2, '0')}:${String(minsRemaining).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
91
|
+
}
|
|
92
|
+
return `${String(minsRemaining).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
93
|
+
}
|
|
94
|
+
function formatClock(timestamp) {
|
|
95
|
+
const parsed = Date.parse(timestamp);
|
|
96
|
+
if (Number.isNaN(parsed)) {
|
|
97
|
+
return '--:--:--';
|
|
98
|
+
}
|
|
99
|
+
const date = new Date(parsed);
|
|
100
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
101
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
102
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
103
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
104
|
+
}
|
|
105
|
+
// Exported for tests
|
|
106
|
+
export const __private = {
|
|
107
|
+
formatElapsed,
|
|
108
|
+
formatClock,
|
|
109
|
+
colorForStatus,
|
|
110
|
+
colorForLevel,
|
|
111
|
+
createInitialHudState
|
|
112
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { HudApp } from './HudApp.js';
|
|
4
|
+
import { HudStore } from './store.js';
|
|
5
|
+
export function startHud(options) {
|
|
6
|
+
const store = new HudStore({
|
|
7
|
+
logLimit: options.logLimit,
|
|
8
|
+
batchIntervalMs: options.batchIntervalMs
|
|
9
|
+
});
|
|
10
|
+
const unsubscribe = options.emitter.on('*', (event) => {
|
|
11
|
+
store.enqueue(event);
|
|
12
|
+
});
|
|
13
|
+
const ink = render(_jsx(HudApp, { store: store, footerNote: options.footerNote }));
|
|
14
|
+
let stopped = false;
|
|
15
|
+
return {
|
|
16
|
+
store,
|
|
17
|
+
stop() {
|
|
18
|
+
if (stopped)
|
|
19
|
+
return;
|
|
20
|
+
stopped = true;
|
|
21
|
+
unsubscribe();
|
|
22
|
+
store.dispose();
|
|
23
|
+
ink.unmount();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const DEFAULT_LOG_LIMIT = 200;
|
|
2
|
+
const DEFAULT_BATCH_INTERVAL_MS = 120;
|
|
3
|
+
export function createInitialHudState() {
|
|
4
|
+
return {
|
|
5
|
+
runId: null,
|
|
6
|
+
taskId: null,
|
|
7
|
+
pipelineTitle: null,
|
|
8
|
+
status: 'idle',
|
|
9
|
+
manifestPath: null,
|
|
10
|
+
runSummaryPath: null,
|
|
11
|
+
metricsPath: null,
|
|
12
|
+
logPath: null,
|
|
13
|
+
stages: [],
|
|
14
|
+
logs: [],
|
|
15
|
+
startedAt: null,
|
|
16
|
+
completedAt: null,
|
|
17
|
+
lastUpdated: null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function reduceHudState(state, event, logLimit = DEFAULT_LOG_LIMIT) {
|
|
21
|
+
switch (event.type) {
|
|
22
|
+
case 'run:started':
|
|
23
|
+
return applyRunStarted(state, event);
|
|
24
|
+
case 'stage:started':
|
|
25
|
+
return applyStageStarted(state, event);
|
|
26
|
+
case 'stage:completed':
|
|
27
|
+
return applyStageCompleted(state, event);
|
|
28
|
+
case 'run:completed':
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
status: event.status,
|
|
32
|
+
manifestPath: event.manifestPath,
|
|
33
|
+
runSummaryPath: event.runSummaryPath,
|
|
34
|
+
metricsPath: event.metricsPath,
|
|
35
|
+
completedAt: event.timestamp,
|
|
36
|
+
lastUpdated: event.timestamp
|
|
37
|
+
};
|
|
38
|
+
case 'run:error': {
|
|
39
|
+
const status = state.status === 'succeeded' ? state.status : 'failed';
|
|
40
|
+
return appendLogEntry({
|
|
41
|
+
...state,
|
|
42
|
+
status,
|
|
43
|
+
lastUpdated: event.timestamp
|
|
44
|
+
}, {
|
|
45
|
+
id: buildLogId(state),
|
|
46
|
+
message: event.message,
|
|
47
|
+
level: 'error',
|
|
48
|
+
stageId: event.stageId ?? null,
|
|
49
|
+
timestamp: event.timestamp
|
|
50
|
+
}, logLimit);
|
|
51
|
+
}
|
|
52
|
+
case 'log':
|
|
53
|
+
return appendLogEntry(state, {
|
|
54
|
+
id: buildLogId(state),
|
|
55
|
+
message: event.message,
|
|
56
|
+
level: event.level,
|
|
57
|
+
stageId: event.stageId,
|
|
58
|
+
stageIndex: event.stageIndex ?? null,
|
|
59
|
+
timestamp: event.timestamp
|
|
60
|
+
}, logLimit);
|
|
61
|
+
case 'tool:call':
|
|
62
|
+
return appendLogEntry(state, {
|
|
63
|
+
id: buildLogId(state),
|
|
64
|
+
message: formatToolCall(event),
|
|
65
|
+
level: 'info',
|
|
66
|
+
stageId: event.stageId,
|
|
67
|
+
stageIndex: event.stageIndex ?? null,
|
|
68
|
+
timestamp: event.timestamp
|
|
69
|
+
}, logLimit);
|
|
70
|
+
default:
|
|
71
|
+
return state;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export class HudStore {
|
|
75
|
+
state = createInitialHudState();
|
|
76
|
+
logLimit;
|
|
77
|
+
batchIntervalMs;
|
|
78
|
+
listeners = new Set();
|
|
79
|
+
pendingEvents = [];
|
|
80
|
+
batchTimer = null;
|
|
81
|
+
constructor(options = {}) {
|
|
82
|
+
this.logLimit = options.logLimit ?? DEFAULT_LOG_LIMIT;
|
|
83
|
+
this.batchIntervalMs = options.batchIntervalMs ?? DEFAULT_BATCH_INTERVAL_MS;
|
|
84
|
+
}
|
|
85
|
+
getState() {
|
|
86
|
+
return this.state;
|
|
87
|
+
}
|
|
88
|
+
enqueue(event) {
|
|
89
|
+
this.pendingEvents.push(event);
|
|
90
|
+
this.scheduleFlush();
|
|
91
|
+
}
|
|
92
|
+
subscribe(listener) {
|
|
93
|
+
this.listeners.add(listener);
|
|
94
|
+
listener(this.state);
|
|
95
|
+
return () => {
|
|
96
|
+
this.listeners.delete(listener);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
dispose() {
|
|
100
|
+
if (this.batchTimer) {
|
|
101
|
+
clearTimeout(this.batchTimer);
|
|
102
|
+
this.batchTimer = null;
|
|
103
|
+
}
|
|
104
|
+
this.listeners.clear();
|
|
105
|
+
this.pendingEvents = [];
|
|
106
|
+
}
|
|
107
|
+
scheduleFlush() {
|
|
108
|
+
if (this.batchTimer) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.batchTimer = setTimeout(() => {
|
|
112
|
+
this.batchTimer = null;
|
|
113
|
+
this.flush();
|
|
114
|
+
}, this.batchIntervalMs);
|
|
115
|
+
}
|
|
116
|
+
flush() {
|
|
117
|
+
if (this.pendingEvents.length === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const events = this.pendingEvents;
|
|
121
|
+
this.pendingEvents = [];
|
|
122
|
+
let nextState = this.state;
|
|
123
|
+
for (const event of events) {
|
|
124
|
+
nextState = reduceHudState(nextState, event, this.logLimit);
|
|
125
|
+
}
|
|
126
|
+
this.state = nextState;
|
|
127
|
+
for (const listener of this.listeners) {
|
|
128
|
+
listener(this.state);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function applyRunStarted(state, event) {
|
|
133
|
+
const stages = event.stages
|
|
134
|
+
.slice()
|
|
135
|
+
.sort((a, b) => a.index - b.index)
|
|
136
|
+
.map((stage) => ({
|
|
137
|
+
id: stage.id,
|
|
138
|
+
index: stage.index,
|
|
139
|
+
title: stage.title,
|
|
140
|
+
kind: stage.kind,
|
|
141
|
+
status: stage.status,
|
|
142
|
+
summary: stage.summary,
|
|
143
|
+
exitCode: stage.exitCode,
|
|
144
|
+
logPath: stage.logPath,
|
|
145
|
+
subRunId: stage.subRunId,
|
|
146
|
+
startedAt: stage.status === 'running' ? event.timestamp : null,
|
|
147
|
+
completedAt: ['succeeded', 'failed', 'skipped'].includes(stage.status) ? event.timestamp : null
|
|
148
|
+
}));
|
|
149
|
+
return {
|
|
150
|
+
...state,
|
|
151
|
+
runId: event.runId,
|
|
152
|
+
taskId: event.taskId,
|
|
153
|
+
pipelineTitle: event.pipelineTitle,
|
|
154
|
+
status: event.status,
|
|
155
|
+
manifestPath: event.manifestPath,
|
|
156
|
+
logPath: event.logPath,
|
|
157
|
+
stages,
|
|
158
|
+
startedAt: event.timestamp,
|
|
159
|
+
completedAt: null,
|
|
160
|
+
lastUpdated: event.timestamp
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function applyStageStarted(state, event) {
|
|
164
|
+
const stages = updateStages(state.stages, {
|
|
165
|
+
id: event.stageId,
|
|
166
|
+
index: event.stageIndex,
|
|
167
|
+
title: event.title,
|
|
168
|
+
kind: event.kind,
|
|
169
|
+
status: 'running',
|
|
170
|
+
summary: null,
|
|
171
|
+
exitCode: null,
|
|
172
|
+
logPath: event.logPath ?? null,
|
|
173
|
+
startedAt: event.timestamp
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
...state,
|
|
177
|
+
stages,
|
|
178
|
+
status: state.status === 'idle' ? 'in_progress' : state.status,
|
|
179
|
+
lastUpdated: event.timestamp
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function applyStageCompleted(state, event) {
|
|
183
|
+
const stages = updateStages(state.stages, {
|
|
184
|
+
id: event.stageId,
|
|
185
|
+
index: event.stageIndex,
|
|
186
|
+
title: event.title,
|
|
187
|
+
kind: event.kind,
|
|
188
|
+
status: event.status,
|
|
189
|
+
summary: event.summary,
|
|
190
|
+
exitCode: event.exitCode,
|
|
191
|
+
logPath: event.logPath ?? null,
|
|
192
|
+
completedAt: event.timestamp,
|
|
193
|
+
subRunId: event.subRunId
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
...state,
|
|
197
|
+
stages,
|
|
198
|
+
lastUpdated: event.timestamp
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function updateStages(stages, update) {
|
|
202
|
+
const existingIndex = stages.findIndex((stage) => stage.id === update.id);
|
|
203
|
+
if (existingIndex >= 0) {
|
|
204
|
+
const next = stages.slice();
|
|
205
|
+
next[existingIndex] = {
|
|
206
|
+
...next[existingIndex],
|
|
207
|
+
...update
|
|
208
|
+
};
|
|
209
|
+
return next.sort((a, b) => a.index - b.index);
|
|
210
|
+
}
|
|
211
|
+
const summary = 'summary' in update ? update.summary ?? null : null;
|
|
212
|
+
const exitCode = 'exitCode' in update ? update.exitCode ?? null : null;
|
|
213
|
+
const logPath = 'logPath' in update ? update.logPath ?? null : null;
|
|
214
|
+
return [
|
|
215
|
+
...stages,
|
|
216
|
+
{
|
|
217
|
+
...update,
|
|
218
|
+
status: update.status ?? 'pending',
|
|
219
|
+
summary,
|
|
220
|
+
exitCode,
|
|
221
|
+
logPath
|
|
222
|
+
}
|
|
223
|
+
].sort((a, b) => a.index - b.index);
|
|
224
|
+
}
|
|
225
|
+
function appendLogEntry(state, entry, limit) {
|
|
226
|
+
const nextLogs = [...state.logs, entry];
|
|
227
|
+
if (nextLogs.length > limit) {
|
|
228
|
+
nextLogs.splice(0, nextLogs.length - limit);
|
|
229
|
+
}
|
|
230
|
+
return { ...state, logs: nextLogs, lastUpdated: entry.timestamp };
|
|
231
|
+
}
|
|
232
|
+
function buildLogId(state) {
|
|
233
|
+
return `${state.logs.length + 1}`;
|
|
234
|
+
}
|
|
235
|
+
function formatToolCall(event) {
|
|
236
|
+
const stageFragment = event.stageId ? ` [${event.stageId}]` : '';
|
|
237
|
+
const attempt = typeof event.attempt === 'number' ? ` attempt ${event.attempt}` : '';
|
|
238
|
+
const message = event.message ? ` - ${event.message}` : '';
|
|
239
|
+
return `${event.toolName}${stageFragment}: ${event.status}${attempt}${message}`;
|
|
240
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const TRUTHY_ENFORCE_VALUES = ['1', 'true', 'enforce', 'on', 'yes'];
|
|
2
|
+
export function resolveEnforcementMode(explicit, enforce) {
|
|
3
|
+
const candidate = (explicit ?? null) ?? (enforce ?? null);
|
|
4
|
+
if (!candidate) {
|
|
5
|
+
return 'shadow';
|
|
6
|
+
}
|
|
7
|
+
const normalized = candidate.trim().toLowerCase();
|
|
8
|
+
if (TRUTHY_ENFORCE_VALUES.includes(normalized)) {
|
|
9
|
+
return 'enforce';
|
|
10
|
+
}
|
|
11
|
+
return 'shadow';
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { writeAtomicFile as writeAtomicFileInternal } from '../../utils/atomicWrite.js';
|
|
2
|
+
export async function writeJsonAtomic(targetPath, data) {
|
|
3
|
+
const payload = `${JSON.stringify(data, null, 2)}\n`;
|
|
4
|
+
await writeFileAtomic(targetPath, payload);
|
|
5
|
+
}
|
|
6
|
+
export async function writeFileAtomic(targetPath, contents) {
|
|
7
|
+
await writeAtomicFileInternal(targetPath, contents, { ensureDir: true, encoding: 'utf8' });
|
|
8
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { EnvUtils } from '../../../../packages/shared/config/index.js';
|
|
3
|
+
export function evaluateInteractiveGate(options) {
|
|
4
|
+
const { requested, disabled = false, format, stdoutIsTTY = process.stdout.isTTY === true, stderrIsTTY = process.stderr.isTTY === true, term = process.env.TERM ?? null, env = process.env } = options;
|
|
5
|
+
if (!requested) {
|
|
6
|
+
return { enabled: false, reason: 'not requested' };
|
|
7
|
+
}
|
|
8
|
+
if (disabled) {
|
|
9
|
+
return { enabled: false, reason: 'flagged off' };
|
|
10
|
+
}
|
|
11
|
+
if (format === 'json') {
|
|
12
|
+
return { enabled: false, reason: 'json format requested' };
|
|
13
|
+
}
|
|
14
|
+
if (!stdoutIsTTY || !stderrIsTTY) {
|
|
15
|
+
return { enabled: false, reason: 'non-tty output streams' };
|
|
16
|
+
}
|
|
17
|
+
if ((term ?? '').toLowerCase() === 'dumb') {
|
|
18
|
+
return { enabled: false, reason: 'TERM=dumb' };
|
|
19
|
+
}
|
|
20
|
+
const ci = env.CI ?? '';
|
|
21
|
+
if (ci && EnvUtils.isTrue(ci)) {
|
|
22
|
+
return { enabled: false, reason: 'CI detected' };
|
|
23
|
+
}
|
|
24
|
+
return { enabled: true, reason: null };
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
export function resolveOptionalDependency(specifier, cwd = process.cwd()) {
|
|
6
|
+
const cwdPackage = join(cwd, 'package.json');
|
|
7
|
+
if (existsSync(cwdPackage)) {
|
|
8
|
+
try {
|
|
9
|
+
const cwdRequire = createRequire(cwdPackage);
|
|
10
|
+
return { path: cwdRequire.resolve(specifier), source: 'cwd' };
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// ignore and fall back
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const selfRequire = createRequire(import.meta.url);
|
|
18
|
+
return { path: selfRequire.resolve(specifier), source: 'package' };
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { path: null, source: null };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function importOptionalDependency(specifier, cwd) {
|
|
25
|
+
const resolved = resolveOptionalDependency(specifier, cwd);
|
|
26
|
+
if (!resolved.path) {
|
|
27
|
+
throw new Error(`Missing optional dependency: ${specifier}`);
|
|
28
|
+
}
|
|
29
|
+
return (await import(pathToFileURL(resolved.path).href));
|
|
30
|
+
}
|