@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,71 @@
|
|
|
1
|
+
const DESIGN_TOOLKIT_ENV = {
|
|
2
|
+
DESIGN_PIPELINE: '1',
|
|
3
|
+
DESIGN_TOOLKIT: '1'
|
|
4
|
+
};
|
|
5
|
+
export const hiFiDesignToolkitPipeline = {
|
|
6
|
+
id: 'hi-fi-design-toolkit',
|
|
7
|
+
title: 'Hi-Fi Design Toolkit',
|
|
8
|
+
description: 'Runs the hi-fi design toolkit pipeline to extract, tokenize, self-correct, and publish design artifacts.',
|
|
9
|
+
tags: ['design', 'hi-fi'],
|
|
10
|
+
stages: [
|
|
11
|
+
{
|
|
12
|
+
kind: 'command',
|
|
13
|
+
id: 'design-config',
|
|
14
|
+
title: 'Resolve design configuration',
|
|
15
|
+
command: 'node "$CODEX_ORCHESTRATOR_PACKAGE_ROOT/dist/scripts/design/pipeline/prepare.js"',
|
|
16
|
+
env: { ...DESIGN_TOOLKIT_ENV }
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
kind: 'command',
|
|
20
|
+
id: 'design-toolkit-extract',
|
|
21
|
+
title: 'Wrap external toolkit extractor',
|
|
22
|
+
command: 'node "$CODEX_ORCHESTRATOR_PACKAGE_ROOT/dist/scripts/design/pipeline/toolkit/extract.js"',
|
|
23
|
+
env: { ...DESIGN_TOOLKIT_ENV }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
kind: 'command',
|
|
27
|
+
id: 'design-toolkit-tokens',
|
|
28
|
+
title: 'Generate tokens and style guides',
|
|
29
|
+
command: 'node "$CODEX_ORCHESTRATOR_PACKAGE_ROOT/dist/scripts/design/pipeline/toolkit/tokens.js"',
|
|
30
|
+
env: { ...DESIGN_TOOLKIT_ENV }
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
kind: 'command',
|
|
34
|
+
id: 'design-toolkit-reference',
|
|
35
|
+
title: 'Build reference pages + self-correction',
|
|
36
|
+
command: 'node "$CODEX_ORCHESTRATOR_PACKAGE_ROOT/dist/scripts/design/pipeline/toolkit/reference.js"',
|
|
37
|
+
env: { ...DESIGN_TOOLKIT_ENV }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
kind: 'command',
|
|
41
|
+
id: 'design-advanced-assets',
|
|
42
|
+
title: 'Generate advanced design assets',
|
|
43
|
+
command: 'node "$CODEX_ORCHESTRATOR_PACKAGE_ROOT/dist/scripts/design/pipeline/advanced-assets.js"',
|
|
44
|
+
env: { ...DESIGN_TOOLKIT_ENV },
|
|
45
|
+
allowFailure: true,
|
|
46
|
+
summaryHint: 'Optional motion capture via Framer Motion + FFmpeg'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
kind: 'command',
|
|
50
|
+
id: 'design-toolkit-publish',
|
|
51
|
+
title: 'Publish toolkit outputs to packages/design-system',
|
|
52
|
+
command: 'node "$CODEX_ORCHESTRATOR_PACKAGE_ROOT/dist/scripts/design/pipeline/toolkit/publish.js"',
|
|
53
|
+
env: { ...DESIGN_TOOLKIT_ENV }
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
kind: 'command',
|
|
57
|
+
id: 'design-spec-guard',
|
|
58
|
+
title: 'Optional spec-guard (if scripts/spec-guard.mjs exists)',
|
|
59
|
+
command: 'node "$CODEX_ORCHESTRATOR_PACKAGE_ROOT/dist/orchestrator/src/cli/utils/specGuardRunner.js" --dry-run',
|
|
60
|
+
env: { ...DESIGN_TOOLKIT_ENV },
|
|
61
|
+
summaryHint: 'Ensures HI-FI design specs are current before artifact write'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
kind: 'command',
|
|
65
|
+
id: 'design-artifact-writer',
|
|
66
|
+
title: 'Persist design manifests',
|
|
67
|
+
command: 'node "$CODEX_ORCHESTRATOR_PACKAGE_ROOT/dist/scripts/design/pipeline/write-artifacts.js"',
|
|
68
|
+
env: { ...DESIGN_TOOLKIT_ENV }
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { defaultDiagnosticsPipeline } from './defaultDiagnostics.js';
|
|
2
|
+
import { designReferencePipeline } from './designReference.js';
|
|
3
|
+
import { hiFiDesignToolkitPipeline } from './hiFiDesignToolkit.js';
|
|
4
|
+
import { findPipeline } from '../config/userConfig.js';
|
|
5
|
+
const builtinPipelines = new Map([
|
|
6
|
+
[defaultDiagnosticsPipeline.id, defaultDiagnosticsPipeline],
|
|
7
|
+
[designReferencePipeline.id, designReferencePipeline],
|
|
8
|
+
[hiFiDesignToolkitPipeline.id, hiFiDesignToolkitPipeline]
|
|
9
|
+
]);
|
|
10
|
+
function getBuiltinPipeline(id) {
|
|
11
|
+
if (!id) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return builtinPipelines.get(id) ?? null;
|
|
15
|
+
}
|
|
16
|
+
export function resolvePipeline(env, options) {
|
|
17
|
+
const { pipelineId, config } = options;
|
|
18
|
+
if (pipelineId) {
|
|
19
|
+
const fromUser = findPipeline(config, pipelineId);
|
|
20
|
+
if (fromUser) {
|
|
21
|
+
return { pipeline: fromUser, source: 'user' };
|
|
22
|
+
}
|
|
23
|
+
const builtin = getBuiltinPipeline(pipelineId);
|
|
24
|
+
if (builtin) {
|
|
25
|
+
return { pipeline: builtin, source: 'default' };
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Pipeline '${pipelineId}' not found.`);
|
|
28
|
+
}
|
|
29
|
+
const defaultId = config?.defaultPipeline ?? defaultDiagnosticsPipeline.id;
|
|
30
|
+
const userPipeline = findPipeline(config, defaultId);
|
|
31
|
+
const chosen = userPipeline ?? getBuiltinPipeline(defaultId) ?? defaultDiagnosticsPipeline;
|
|
32
|
+
const source = userPipeline ? 'user' : 'default';
|
|
33
|
+
return { pipeline: chosen, source };
|
|
34
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { join, resolve } from 'node:path';
|
|
2
|
+
import { sanitizeTaskId } from '../../persistence/sanitizeTaskId.js';
|
|
3
|
+
const DEFAULT_TASK_ID = '0101';
|
|
4
|
+
export function resolveEnvironment() {
|
|
5
|
+
const repoRoot = resolve(process.env.CODEX_ORCHESTRATOR_ROOT ?? process.cwd());
|
|
6
|
+
const runsRoot = resolve(process.env.CODEX_ORCHESTRATOR_RUNS_DIR ?? join(repoRoot, '.runs'));
|
|
7
|
+
const outRoot = resolve(process.env.CODEX_ORCHESTRATOR_OUT_DIR ?? join(repoRoot, 'out'));
|
|
8
|
+
const rawTaskId = process.env.MCP_RUNNER_TASK_ID ?? DEFAULT_TASK_ID;
|
|
9
|
+
const taskId = normalizeTaskId(rawTaskId);
|
|
10
|
+
return { repoRoot, runsRoot, outRoot, taskId };
|
|
11
|
+
}
|
|
12
|
+
function normalizeTaskId(value) {
|
|
13
|
+
try {
|
|
14
|
+
return sanitizeTaskId(value);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
if (value) {
|
|
19
|
+
throw new Error(`Invalid MCP_RUNNER_TASK_ID "${value}": ${message}`);
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Invalid MCP_RUNNER_TASK_ID: ${message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export { sanitizeTaskId };
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, symlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, relative } from 'node:path';
|
|
4
|
+
import { writeJsonAtomic } from '../utils/fs.js';
|
|
5
|
+
import { slugify } from '../utils/strings.js';
|
|
6
|
+
import { isoTimestamp } from '../utils/time.js';
|
|
7
|
+
import { loadInstructionSet } from '../../../../packages/orchestrator/src/instructions/loader.js';
|
|
8
|
+
import { resolveRunPaths, relativeToRepo } from './runPaths.js';
|
|
9
|
+
import { ExperienceStore } from '../../persistence/ExperienceStore.js';
|
|
10
|
+
import { formatExperienceInjections } from '../exec/experience.js';
|
|
11
|
+
const HEARTBEAT_INTERVAL_SECONDS = 5;
|
|
12
|
+
const HEARTBEAT_STALE_AFTER_SECONDS = 30;
|
|
13
|
+
const MAX_ERROR_DETAIL_CHARS = 8 * 1024;
|
|
14
|
+
export async function bootstrapManifest(runId, options) {
|
|
15
|
+
const { env, pipeline, parentRunId = null, taskSlug, approvalPolicy = null } = options;
|
|
16
|
+
const paths = resolveRunPaths(env, runId);
|
|
17
|
+
await mkdir(paths.runDir, { recursive: true });
|
|
18
|
+
await mkdir(paths.commandsDir, { recursive: true });
|
|
19
|
+
await mkdir(paths.errorsDir, { recursive: true });
|
|
20
|
+
const now = isoTimestamp();
|
|
21
|
+
const resumeToken = randomBytes(32).toString('hex');
|
|
22
|
+
const commands = buildCommandEntries(pipeline);
|
|
23
|
+
const manifest = {
|
|
24
|
+
version: 1,
|
|
25
|
+
task_id: env.taskId,
|
|
26
|
+
task_slug: taskSlug ?? null,
|
|
27
|
+
run_id: runId,
|
|
28
|
+
parent_run_id: parentRunId ?? null,
|
|
29
|
+
pipeline_id: pipeline.id,
|
|
30
|
+
pipeline_title: pipeline.title,
|
|
31
|
+
runner: 'codex-cli',
|
|
32
|
+
approval_policy: approvalPolicy,
|
|
33
|
+
status: 'queued',
|
|
34
|
+
status_detail: null,
|
|
35
|
+
started_at: now,
|
|
36
|
+
completed_at: null,
|
|
37
|
+
updated_at: now,
|
|
38
|
+
heartbeat_at: now,
|
|
39
|
+
heartbeat_interval_seconds: HEARTBEAT_INTERVAL_SECONDS,
|
|
40
|
+
heartbeat_stale_after_seconds: HEARTBEAT_STALE_AFTER_SECONDS,
|
|
41
|
+
artifact_root: relativeToRepo(env, paths.runDir),
|
|
42
|
+
compat_path: relative(env.repoRoot, dirname(paths.compatManifestPath)),
|
|
43
|
+
log_path: relativeToRepo(env, paths.logPath),
|
|
44
|
+
summary: null,
|
|
45
|
+
metrics_recorded: false,
|
|
46
|
+
resume_token: resumeToken,
|
|
47
|
+
resume_events: [],
|
|
48
|
+
approvals: [],
|
|
49
|
+
commands,
|
|
50
|
+
child_runs: [],
|
|
51
|
+
run_summary_path: null,
|
|
52
|
+
plan_target_id: options.planTargetId ?? null,
|
|
53
|
+
instructions_hash: null,
|
|
54
|
+
instructions_sources: [],
|
|
55
|
+
prompt_packs: [],
|
|
56
|
+
guardrails_required: pipeline.guardrailsRequired !== false,
|
|
57
|
+
learning: {
|
|
58
|
+
validation: {
|
|
59
|
+
mode: 'per-task',
|
|
60
|
+
grouping: null,
|
|
61
|
+
status: 'pending'
|
|
62
|
+
},
|
|
63
|
+
alerts: [],
|
|
64
|
+
approvals: []
|
|
65
|
+
},
|
|
66
|
+
tfgrpo: null
|
|
67
|
+
};
|
|
68
|
+
const instructions = await loadInstructionSet(env.repoRoot);
|
|
69
|
+
const experienceStore = new ExperienceStore({
|
|
70
|
+
outDir: env.outRoot,
|
|
71
|
+
runsDir: env.runsRoot,
|
|
72
|
+
maxSummaryWords: instructions.experienceMaxWords
|
|
73
|
+
});
|
|
74
|
+
const experienceSnippets = await Promise.all(instructions.promptPacks.map(async (pack) => {
|
|
75
|
+
if (!pack.experienceSlots) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const records = await experienceStore.fetchTop({
|
|
79
|
+
domain: pack.domain,
|
|
80
|
+
limit: pack.experienceSlots,
|
|
81
|
+
taskId: env.taskId
|
|
82
|
+
});
|
|
83
|
+
return formatExperienceInjections(records, pack.experienceSlots);
|
|
84
|
+
}));
|
|
85
|
+
manifest.instructions_hash = instructions.hash || null;
|
|
86
|
+
manifest.instructions_sources = instructions.sources.map((source) => source.path);
|
|
87
|
+
manifest.prompt_packs = instructions.promptPacks.map((pack, index) => {
|
|
88
|
+
const experiences = experienceSnippets[index] ?? [];
|
|
89
|
+
return {
|
|
90
|
+
id: pack.id,
|
|
91
|
+
domain: pack.domain,
|
|
92
|
+
stamp: pack.stamp,
|
|
93
|
+
experience_slots: pack.experienceSlots,
|
|
94
|
+
sources: pack.sources.map((source) => source.path),
|
|
95
|
+
...(experiences.length > 0 ? { experiences } : {})
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
await writeJsonAtomic(paths.manifestPath, manifest);
|
|
99
|
+
await writeFile(paths.resumeTokenPath, `${resumeToken}\n`, 'utf8');
|
|
100
|
+
await writeFile(paths.heartbeatPath, `${now}\n`, 'utf8');
|
|
101
|
+
await createCompatibilityPointer(env, paths);
|
|
102
|
+
return { manifest, paths };
|
|
103
|
+
}
|
|
104
|
+
export async function saveManifest(paths, manifest) {
|
|
105
|
+
manifest.updated_at = isoTimestamp();
|
|
106
|
+
await writeJsonAtomic(paths.manifestPath, manifest);
|
|
107
|
+
}
|
|
108
|
+
export async function loadManifest(env, runId) {
|
|
109
|
+
const paths = resolveRunPaths(env, runId);
|
|
110
|
+
const raw = await readFile(paths.manifestPath, 'utf8');
|
|
111
|
+
const manifest = JSON.parse(raw);
|
|
112
|
+
return { manifest, paths };
|
|
113
|
+
}
|
|
114
|
+
export function updateCommandStatus(manifest, commandIndex, changes) {
|
|
115
|
+
const entry = manifest.commands[commandIndex];
|
|
116
|
+
if (!entry) {
|
|
117
|
+
throw new Error(`Manifest command index ${commandIndex} missing.`);
|
|
118
|
+
}
|
|
119
|
+
Object.assign(entry, changes);
|
|
120
|
+
manifest.updated_at = isoTimestamp();
|
|
121
|
+
return entry;
|
|
122
|
+
}
|
|
123
|
+
export async function appendCommandError(env, paths, manifest, command, reason, details) {
|
|
124
|
+
const filename = `${String(command.index).padStart(2, '0')}-${slugify(command.id)}.json`;
|
|
125
|
+
const errorPath = join(paths.errorsDir, filename);
|
|
126
|
+
const payload = {
|
|
127
|
+
run_id: manifest.run_id,
|
|
128
|
+
task_id: manifest.task_id,
|
|
129
|
+
command_index: command.index,
|
|
130
|
+
command_id: command.id,
|
|
131
|
+
command: command.command,
|
|
132
|
+
reason,
|
|
133
|
+
details: sanitizeErrorDetails(details),
|
|
134
|
+
created_at: isoTimestamp()
|
|
135
|
+
};
|
|
136
|
+
await writeJsonAtomic(errorPath, payload);
|
|
137
|
+
return relativeToRepo(env, errorPath);
|
|
138
|
+
}
|
|
139
|
+
function sanitizeErrorDetails(details) {
|
|
140
|
+
const sanitized = {};
|
|
141
|
+
for (const [key, value] of Object.entries(details)) {
|
|
142
|
+
if (typeof value === 'string') {
|
|
143
|
+
if (value.length > MAX_ERROR_DETAIL_CHARS) {
|
|
144
|
+
sanitized[key] = `${value.slice(0, MAX_ERROR_DETAIL_CHARS)}…`;
|
|
145
|
+
const truncatedKey = `${key}_truncated`;
|
|
146
|
+
if (!(truncatedKey in details)) {
|
|
147
|
+
sanitized[truncatedKey] = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
sanitized[key] = value;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
sanitized[key] = value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return sanitized;
|
|
159
|
+
}
|
|
160
|
+
export function updateHeartbeat(manifest) {
|
|
161
|
+
manifest.heartbeat_at = isoTimestamp();
|
|
162
|
+
if (manifest.status === 'in_progress') {
|
|
163
|
+
manifest.status_detail = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export function resetForResume(manifest) {
|
|
167
|
+
manifest.completed_at = null;
|
|
168
|
+
manifest.metrics_recorded = false;
|
|
169
|
+
manifest.status = 'in_progress';
|
|
170
|
+
manifest.status_detail = 'resuming';
|
|
171
|
+
manifest.guardrail_status = undefined;
|
|
172
|
+
}
|
|
173
|
+
export function recordResumeEvent(manifest, event) {
|
|
174
|
+
manifest.resume_events.push({ ...event, timestamp: isoTimestamp() });
|
|
175
|
+
}
|
|
176
|
+
export function ensureGuardrailStatus(manifest) {
|
|
177
|
+
if (manifest.guardrail_status) {
|
|
178
|
+
return manifest.guardrail_status;
|
|
179
|
+
}
|
|
180
|
+
const snapshot = computeGuardrailStatus(manifest);
|
|
181
|
+
manifest.guardrail_status = snapshot;
|
|
182
|
+
return snapshot;
|
|
183
|
+
}
|
|
184
|
+
export function buildGuardrailSummary(manifest) {
|
|
185
|
+
return ensureGuardrailStatus(manifest).summary;
|
|
186
|
+
}
|
|
187
|
+
export function upsertGuardrailSummary(manifest) {
|
|
188
|
+
const summary = buildGuardrailSummary(manifest);
|
|
189
|
+
const existing = manifest.summary ? manifest.summary.split('\n') : [];
|
|
190
|
+
const filtered = existing.filter((line) => !line.toLowerCase().startsWith('guardrails:'));
|
|
191
|
+
filtered.push(summary);
|
|
192
|
+
manifest.summary = filtered.join('\n').trim();
|
|
193
|
+
if (manifest.summary.length === 0) {
|
|
194
|
+
manifest.summary = summary;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function computeGuardrailStatus(manifest) {
|
|
198
|
+
const guardrailCommands = selectGuardrailCommands(manifest);
|
|
199
|
+
const guardrailsRequired = manifest.guardrails_required ?? true;
|
|
200
|
+
const counts = {
|
|
201
|
+
total: guardrailCommands.length,
|
|
202
|
+
succeeded: 0,
|
|
203
|
+
failed: 0,
|
|
204
|
+
skipped: 0,
|
|
205
|
+
other: 0
|
|
206
|
+
};
|
|
207
|
+
for (const entry of guardrailCommands) {
|
|
208
|
+
if (entry.status === 'succeeded') {
|
|
209
|
+
counts.succeeded += 1;
|
|
210
|
+
}
|
|
211
|
+
else if (entry.status === 'failed') {
|
|
212
|
+
counts.failed += 1;
|
|
213
|
+
}
|
|
214
|
+
else if (entry.status === 'skipped') {
|
|
215
|
+
counts.skipped += 1;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
counts.other += 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const present = counts.succeeded > 0;
|
|
222
|
+
let recommendation = null;
|
|
223
|
+
if (counts.total === 0) {
|
|
224
|
+
recommendation = guardrailsRequired
|
|
225
|
+
? 'Guardrail command missing; run "codex-orchestrator start diagnostics --approval-policy never --format json --no-interactive" to capture reviewer diagnostics.'
|
|
226
|
+
: null;
|
|
227
|
+
}
|
|
228
|
+
else if (counts.failed > 0) {
|
|
229
|
+
recommendation =
|
|
230
|
+
'Guardrail command failed; re-run "codex-orchestrator start diagnostics --approval-policy never --format json --no-interactive" to gather failure artifacts.';
|
|
231
|
+
}
|
|
232
|
+
const summary = formatGuardrailSummary(counts);
|
|
233
|
+
return {
|
|
234
|
+
present,
|
|
235
|
+
recommendation,
|
|
236
|
+
summary,
|
|
237
|
+
computed_at: isoTimestamp(),
|
|
238
|
+
counts
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function selectGuardrailCommands(manifest) {
|
|
242
|
+
return manifest.commands.filter((entry) => entry.command?.includes('spec-guard'));
|
|
243
|
+
}
|
|
244
|
+
function formatGuardrailSummary(counts) {
|
|
245
|
+
if (counts.total === 0) {
|
|
246
|
+
return 'Guardrails: spec-guard command not found.';
|
|
247
|
+
}
|
|
248
|
+
if (counts.failed > 0) {
|
|
249
|
+
return `Guardrails: spec-guard failed (${counts.failed}/${counts.total} failed).`;
|
|
250
|
+
}
|
|
251
|
+
if (counts.succeeded === counts.total) {
|
|
252
|
+
return `Guardrails: spec-guard succeeded (${counts.total} passed).`;
|
|
253
|
+
}
|
|
254
|
+
if (counts.skipped === counts.total) {
|
|
255
|
+
return `Guardrails: spec-guard skipped (all ${counts.total} skipped).`;
|
|
256
|
+
}
|
|
257
|
+
const parts = [];
|
|
258
|
+
if (counts.succeeded > 0) {
|
|
259
|
+
parts.push(`${counts.succeeded} passed`);
|
|
260
|
+
}
|
|
261
|
+
if (counts.skipped > 0) {
|
|
262
|
+
parts.push(`${counts.skipped} skipped`);
|
|
263
|
+
}
|
|
264
|
+
if (counts.other > 0) {
|
|
265
|
+
parts.push(`${counts.other} pending`);
|
|
266
|
+
}
|
|
267
|
+
return `Guardrails: spec-guard partial (${parts.join(', ')} of ${counts.total}).`;
|
|
268
|
+
}
|
|
269
|
+
export function appendSummary(manifest, message) {
|
|
270
|
+
if (!message) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (!manifest.summary) {
|
|
274
|
+
manifest.summary = message;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (manifest.summary.includes(message)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
manifest.summary = `${manifest.summary}\n${message}`;
|
|
281
|
+
}
|
|
282
|
+
export function finalizeStatus(manifest, status, detail) {
|
|
283
|
+
manifest.status = status;
|
|
284
|
+
manifest.status_detail = detail ?? null;
|
|
285
|
+
manifest.completed_at = isoTimestamp();
|
|
286
|
+
}
|
|
287
|
+
export async function writeHeartbeatFile(paths, manifest) {
|
|
288
|
+
await writeFile(paths.heartbeatPath, `${manifest.heartbeat_at ?? isoTimestamp()}\n`, 'utf8');
|
|
289
|
+
}
|
|
290
|
+
async function createCompatibilityPointer(env, paths) {
|
|
291
|
+
await mkdir(paths.compatDir, { recursive: true });
|
|
292
|
+
await mkdir(paths.localCompatDir, { recursive: true });
|
|
293
|
+
const relativeManifest = relative(env.repoRoot, paths.manifestPath);
|
|
294
|
+
const artifactRootRelative = relative(env.repoRoot, paths.runDir);
|
|
295
|
+
try {
|
|
296
|
+
await writeJsonAtomic(paths.compatManifestPath, {
|
|
297
|
+
redirect_to: artifactRootRelative,
|
|
298
|
+
manifest: relativeManifest,
|
|
299
|
+
created_at: isoTimestamp(),
|
|
300
|
+
note: 'Generated by codex CLI for backward compatibility.'
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
// Best effort; leave a stub if atomic write fails.
|
|
305
|
+
await writeJsonAtomic(join(paths.compatDir, 'compat.json'), {
|
|
306
|
+
redirect_to: artifactRootRelative,
|
|
307
|
+
manifest: relativeManifest,
|
|
308
|
+
error: error?.message ?? String(error)
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const localCompatPath = join(paths.localCompatDir, 'manifest.json');
|
|
312
|
+
try {
|
|
313
|
+
await symlink(relative(paths.localCompatDir, paths.manifestPath), localCompatPath);
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
if (error.code === 'EEXIST') {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
await writeJsonAtomic(localCompatPath, {
|
|
320
|
+
redirect_to: artifactRootRelative,
|
|
321
|
+
manifest: relativeManifest
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function buildCommandEntries(pipeline) {
|
|
326
|
+
return pipeline.stages.map((stage, index) => buildCommandEntry(stage, index + 1));
|
|
327
|
+
}
|
|
328
|
+
function buildCommandEntry(stage, index) {
|
|
329
|
+
if (stage.kind === 'command') {
|
|
330
|
+
return commandStageToManifest(stage, index);
|
|
331
|
+
}
|
|
332
|
+
return subPipelineStageToManifest(stage, index);
|
|
333
|
+
}
|
|
334
|
+
function commandStageToManifest(stage, index) {
|
|
335
|
+
return {
|
|
336
|
+
index,
|
|
337
|
+
id: stage.id,
|
|
338
|
+
title: stage.title,
|
|
339
|
+
command: stage.command,
|
|
340
|
+
kind: 'command',
|
|
341
|
+
status: 'pending',
|
|
342
|
+
started_at: null,
|
|
343
|
+
completed_at: null,
|
|
344
|
+
exit_code: null,
|
|
345
|
+
summary: null,
|
|
346
|
+
log_path: null,
|
|
347
|
+
error_file: null,
|
|
348
|
+
sub_run_id: null
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function subPipelineStageToManifest(stage, index) {
|
|
352
|
+
return {
|
|
353
|
+
index,
|
|
354
|
+
id: stage.id,
|
|
355
|
+
title: stage.title,
|
|
356
|
+
command: null,
|
|
357
|
+
kind: 'subpipeline',
|
|
358
|
+
status: 'pending',
|
|
359
|
+
started_at: null,
|
|
360
|
+
completed_at: null,
|
|
361
|
+
exit_code: null,
|
|
362
|
+
summary: null,
|
|
363
|
+
log_path: null,
|
|
364
|
+
error_file: null,
|
|
365
|
+
sub_run_id: null
|
|
366
|
+
};
|
|
367
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { saveManifest, writeHeartbeatFile } from './manifest.js';
|
|
2
|
+
export class ManifestPersister {
|
|
3
|
+
manifest;
|
|
4
|
+
paths;
|
|
5
|
+
persistIntervalMs;
|
|
6
|
+
now;
|
|
7
|
+
writeManifest;
|
|
8
|
+
writeHeartbeat;
|
|
9
|
+
dirtyManifest = false;
|
|
10
|
+
dirtyHeartbeat = false;
|
|
11
|
+
timer = null;
|
|
12
|
+
timerResolver = null;
|
|
13
|
+
lastPersistAt = 0;
|
|
14
|
+
pendingPersist = Promise.resolve();
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.manifest = options.manifest;
|
|
17
|
+
this.paths = options.paths;
|
|
18
|
+
this.persistIntervalMs = options.persistIntervalMs;
|
|
19
|
+
this.now = options.now ?? Date.now;
|
|
20
|
+
this.writeManifest = options.writeManifest ?? saveManifest;
|
|
21
|
+
this.writeHeartbeat = options.writeHeartbeat ?? writeHeartbeatFile;
|
|
22
|
+
}
|
|
23
|
+
schedule(options = {}) {
|
|
24
|
+
const { manifest: includeManifest = false, heartbeat: includeHeartbeat = false, force = false } = options;
|
|
25
|
+
this.dirtyManifest = this.dirtyManifest || includeManifest;
|
|
26
|
+
this.dirtyHeartbeat = this.dirtyHeartbeat || includeHeartbeat;
|
|
27
|
+
if (!this.dirtyManifest && !this.dirtyHeartbeat) {
|
|
28
|
+
return this.pendingPersist;
|
|
29
|
+
}
|
|
30
|
+
if (force) {
|
|
31
|
+
this.clearTimer();
|
|
32
|
+
if (this.timerResolver) {
|
|
33
|
+
const resolver = this.timerResolver;
|
|
34
|
+
this.timerResolver = null;
|
|
35
|
+
resolver();
|
|
36
|
+
return this.pendingPersist;
|
|
37
|
+
}
|
|
38
|
+
this.pendingPersist = this.pendingPersist.then(() => this.flushPersist());
|
|
39
|
+
return this.pendingPersist;
|
|
40
|
+
}
|
|
41
|
+
if (this.timer) {
|
|
42
|
+
return this.pendingPersist;
|
|
43
|
+
}
|
|
44
|
+
const waitMs = Math.max(0, this.lastPersistAt + this.persistIntervalMs - this.now());
|
|
45
|
+
this.pendingPersist = this.pendingPersist
|
|
46
|
+
.then(() => new Promise((resolve) => {
|
|
47
|
+
this.timerResolver = resolve;
|
|
48
|
+
this.timer = setTimeout(() => {
|
|
49
|
+
this.clearTimer();
|
|
50
|
+
resolve();
|
|
51
|
+
}, waitMs);
|
|
52
|
+
}))
|
|
53
|
+
.then(() => this.flushPersist());
|
|
54
|
+
return this.pendingPersist;
|
|
55
|
+
}
|
|
56
|
+
flush() {
|
|
57
|
+
return this.schedule({ force: true });
|
|
58
|
+
}
|
|
59
|
+
clearTimer() {
|
|
60
|
+
if (this.timer) {
|
|
61
|
+
clearTimeout(this.timer);
|
|
62
|
+
this.timer = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async flushPersist() {
|
|
66
|
+
if (!this.dirtyManifest && !this.dirtyHeartbeat) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const writeManifest = this.dirtyManifest;
|
|
70
|
+
const writeHeartbeat = this.dirtyHeartbeat;
|
|
71
|
+
this.dirtyManifest = false;
|
|
72
|
+
this.dirtyHeartbeat = false;
|
|
73
|
+
this.lastPersistAt = this.now();
|
|
74
|
+
if (writeManifest) {
|
|
75
|
+
await this.writeManifest(this.paths, this.manifest);
|
|
76
|
+
}
|
|
77
|
+
if (writeHeartbeat) {
|
|
78
|
+
await this.writeHeartbeat(this.paths, this.manifest);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export async function persistManifest(paths, manifest, persister, options = {}) {
|
|
83
|
+
if (persister) {
|
|
84
|
+
await persister.schedule({ manifest: true, force: options.force });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
await saveManifest(paths, manifest);
|
|
88
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { join, relative } from 'node:path';
|
|
2
|
+
import { sanitizeRunId } from '../../persistence/sanitizeRunId.js';
|
|
3
|
+
export function resolveRunPaths(env, runId) {
|
|
4
|
+
const safeRunId = sanitizeRunId(runId);
|
|
5
|
+
const runDir = join(env.runsRoot, env.taskId, 'cli', safeRunId);
|
|
6
|
+
const manifestPath = join(runDir, 'manifest.json');
|
|
7
|
+
const heartbeatPath = join(runDir, '.heartbeat');
|
|
8
|
+
const resumeTokenPath = join(runDir, '.resume-token');
|
|
9
|
+
const logPath = join(runDir, 'runner.ndjson');
|
|
10
|
+
const commandsDir = join(runDir, 'commands');
|
|
11
|
+
const errorsDir = join(runDir, 'errors');
|
|
12
|
+
const compatDir = join(env.runsRoot, env.taskId, 'mcp', safeRunId);
|
|
13
|
+
const compatManifestPath = join(compatDir, 'manifest.json');
|
|
14
|
+
const localCompatDir = join(env.runsRoot, 'local-mcp', safeRunId);
|
|
15
|
+
return {
|
|
16
|
+
runDir,
|
|
17
|
+
manifestPath,
|
|
18
|
+
heartbeatPath,
|
|
19
|
+
resumeTokenPath,
|
|
20
|
+
logPath,
|
|
21
|
+
commandsDir,
|
|
22
|
+
errorsDir,
|
|
23
|
+
compatDir,
|
|
24
|
+
compatManifestPath,
|
|
25
|
+
localCompatDir
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function relativeToRepo(env, targetPath) {
|
|
29
|
+
return relative(env.repoRoot, targetPath);
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { loadPackageInfo } from './utils/packageInfo.js';
|
|
3
|
+
export function buildSelfCheckResult() {
|
|
4
|
+
const pkg = loadPackageInfo();
|
|
5
|
+
return {
|
|
6
|
+
status: 'ok',
|
|
7
|
+
name: pkg.name ?? 'unknown',
|
|
8
|
+
version: pkg.version ?? 'unknown',
|
|
9
|
+
node: process.version,
|
|
10
|
+
timestamp: new Date().toISOString()
|
|
11
|
+
};
|
|
12
|
+
}
|