@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,146 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, join, relative } from 'node:path';
|
|
6
|
+
import { isoTimestamp } from '../cli/utils/time.js';
|
|
7
|
+
import { logger } from '../logger.js';
|
|
8
|
+
import { sanitizeRunId } from '../persistence/sanitizeRunId.js';
|
|
9
|
+
import { sanitizeTaskId } from '../persistence/sanitizeTaskId.js';
|
|
10
|
+
import { appendLearningAlert, ensureLearningSection, updateLearningValidation } from './manifest.js';
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
export async function runLearningHarvester(manifest, options) {
|
|
13
|
+
const { repoRoot, runsRoot, manifestPath, taskId, runId, diffPath = null, promptPath = null, executionHistoryPath = null, maxAttempts = 3, backoffMs = [500, 1_000], storageDir, alertTargets, uploader } = options;
|
|
14
|
+
const learning = ensureLearningSection(manifest);
|
|
15
|
+
const safeTaskId = sanitizeTaskId(taskId);
|
|
16
|
+
const safeRunId = sanitizeRunId(runId);
|
|
17
|
+
const learningDir = join(runsRoot, safeTaskId, 'cli', safeRunId, 'learning');
|
|
18
|
+
const storageRoot = storageDir ?? join(runsRoot, 'learning-snapshots');
|
|
19
|
+
await mkdir(learningDir, { recursive: true });
|
|
20
|
+
let snapshotPath = null;
|
|
21
|
+
let queuePayloadPath = null;
|
|
22
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
23
|
+
try {
|
|
24
|
+
const snapshot = await createSnapshot({ repoRoot, learningDir, runId });
|
|
25
|
+
snapshotPath = snapshot.tarballPath;
|
|
26
|
+
const storagePath = await persistSnapshot({
|
|
27
|
+
uploader,
|
|
28
|
+
storageRoot,
|
|
29
|
+
taskId: safeTaskId,
|
|
30
|
+
runId: safeRunId,
|
|
31
|
+
tarballPath: snapshot.tarballPath,
|
|
32
|
+
retentionDays: 30
|
|
33
|
+
});
|
|
34
|
+
const queuePayload = {
|
|
35
|
+
snapshot_id: snapshot.tag,
|
|
36
|
+
snapshot_commit: snapshot.commitSha,
|
|
37
|
+
diff_path: diffPath,
|
|
38
|
+
prompt_path: promptPath,
|
|
39
|
+
execution_history_path: executionHistoryPath,
|
|
40
|
+
manifest_path: manifestPath
|
|
41
|
+
};
|
|
42
|
+
queuePayloadPath = join(learningDir, 'queue-payload.json');
|
|
43
|
+
await writeFile(queuePayloadPath, JSON.stringify(queuePayload, null, 2), 'utf8');
|
|
44
|
+
learning.snapshot = {
|
|
45
|
+
tag: snapshot.tag,
|
|
46
|
+
commit_sha: snapshot.commitSha,
|
|
47
|
+
tarball_path: relative(repoRoot, snapshot.tarballPath),
|
|
48
|
+
tarball_digest: snapshot.tarballDigest,
|
|
49
|
+
storage_path: relative(repoRoot, storagePath),
|
|
50
|
+
retention_days: 30,
|
|
51
|
+
status: 'captured',
|
|
52
|
+
attempts: attempt,
|
|
53
|
+
created_at: isoTimestamp(),
|
|
54
|
+
last_error: null
|
|
55
|
+
};
|
|
56
|
+
learning.queue = {
|
|
57
|
+
snapshot_id: snapshot.tag,
|
|
58
|
+
diff_path: diffPath,
|
|
59
|
+
prompt_path: promptPath,
|
|
60
|
+
execution_history_path: executionHistoryPath,
|
|
61
|
+
manifest_path: relative(repoRoot, manifestPath),
|
|
62
|
+
enqueued_at: isoTimestamp(),
|
|
63
|
+
payload_path: relative(repoRoot, queuePayloadPath),
|
|
64
|
+
status: 'queued'
|
|
65
|
+
};
|
|
66
|
+
if (learning.validation?.status === 'snapshot_failed') {
|
|
67
|
+
updateLearningValidation(manifest, 'pending');
|
|
68
|
+
}
|
|
69
|
+
return { manifest, snapshotPath, queuePayloadPath };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
logger.warn(`[learning] snapshot attempt ${attempt} failed: ${message}`);
|
|
74
|
+
learning.snapshot = {
|
|
75
|
+
tag: learning.snapshot?.tag ?? `learning-snapshot-${randomUUID()}`,
|
|
76
|
+
commit_sha: learning.snapshot?.commit_sha ?? 'unknown',
|
|
77
|
+
tarball_path: learning.snapshot?.tarball_path ?? 'unavailable',
|
|
78
|
+
tarball_digest: learning.snapshot?.tarball_digest ?? 'unavailable',
|
|
79
|
+
storage_path: learning.snapshot?.storage_path ??
|
|
80
|
+
relative(repoRoot, join(storageRoot, safeTaskId, `${safeRunId}.tar.gz`)),
|
|
81
|
+
retention_days: 30,
|
|
82
|
+
status: 'snapshot_failed',
|
|
83
|
+
attempts: attempt,
|
|
84
|
+
created_at: learning.snapshot?.created_at ?? isoTimestamp(),
|
|
85
|
+
last_error: message
|
|
86
|
+
};
|
|
87
|
+
appendLearningAlert(manifest, {
|
|
88
|
+
type: 'snapshot_failed',
|
|
89
|
+
channel: 'slack',
|
|
90
|
+
target: alertTargets?.slack ?? '#learning-alerts',
|
|
91
|
+
message: `Snapshot attempt ${attempt} failed: ${message}`
|
|
92
|
+
});
|
|
93
|
+
appendLearningAlert(manifest, {
|
|
94
|
+
type: 'snapshot_failed',
|
|
95
|
+
channel: 'pagerduty',
|
|
96
|
+
target: alertTargets?.pagerduty ?? 'learning-pipeline',
|
|
97
|
+
message: `Snapshot attempt ${attempt} failed for ${taskId}/${runId}`
|
|
98
|
+
});
|
|
99
|
+
if (attempt >= maxAttempts) {
|
|
100
|
+
updateLearningValidation(manifest, 'snapshot_failed');
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
const delayMs = backoffMs[Math.min(attempt - 1, backoffMs.length - 1)] ?? 500;
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { manifest, snapshotPath, queuePayloadPath };
|
|
108
|
+
}
|
|
109
|
+
async function createSnapshot(params) {
|
|
110
|
+
const { repoRoot, learningDir, runId } = params;
|
|
111
|
+
const commitSha = (await safeGit(['rev-parse', 'HEAD'], repoRoot)).trim();
|
|
112
|
+
const tag = `learning-snapshot-${randomUUID()}`;
|
|
113
|
+
await safeGit(['tag', tag, commitSha], repoRoot);
|
|
114
|
+
await mkdir(learningDir, { recursive: true });
|
|
115
|
+
const tarballPath = join(learningDir, `${runId}.tar.gz`);
|
|
116
|
+
const fileListPath = join(learningDir, 'snapshot-files.txt');
|
|
117
|
+
const { stdout: fileListRaw } = await execFileAsync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
|
|
118
|
+
cwd: repoRoot
|
|
119
|
+
});
|
|
120
|
+
const files = fileListRaw.split('\0').filter(Boolean);
|
|
121
|
+
await writeFile(fileListPath, files.join('\n'), 'utf8');
|
|
122
|
+
await execFileAsync('tar', ['-czf', tarballPath, '-C', repoRoot, '-T', fileListPath]);
|
|
123
|
+
const digest = await hashFile(tarballPath);
|
|
124
|
+
return { tag, commitSha, tarballPath, tarballDigest: digest };
|
|
125
|
+
}
|
|
126
|
+
async function hashFile(path) {
|
|
127
|
+
const raw = await readFile(path);
|
|
128
|
+
return createHash('sha256').update(raw).digest('hex');
|
|
129
|
+
}
|
|
130
|
+
async function persistSnapshot(params) {
|
|
131
|
+
const { uploader, storageRoot, taskId, runId, tarballPath, retentionDays } = params;
|
|
132
|
+
const key = join(taskId, `${runId}.tar.gz`);
|
|
133
|
+
const upload = uploader ?? filesystemUploader;
|
|
134
|
+
return upload({ storageRoot, key, file: tarballPath, retentionDays });
|
|
135
|
+
}
|
|
136
|
+
async function filesystemUploader(params) {
|
|
137
|
+
const { storageRoot, key, file } = params;
|
|
138
|
+
const destination = join(storageRoot, key);
|
|
139
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
140
|
+
await copyFile(file, destination);
|
|
141
|
+
return destination;
|
|
142
|
+
}
|
|
143
|
+
async function safeGit(args, cwd) {
|
|
144
|
+
const { stdout } = await execFileAsync('git', args, { cwd });
|
|
145
|
+
return stdout;
|
|
146
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { isoTimestamp } from '../cli/utils/time.js';
|
|
2
|
+
const DEFAULT_VALIDATION = {
|
|
3
|
+
mode: 'per-task',
|
|
4
|
+
grouping: null,
|
|
5
|
+
status: 'pending',
|
|
6
|
+
reason: null,
|
|
7
|
+
log_path: null,
|
|
8
|
+
last_error: null,
|
|
9
|
+
git_status_path: null,
|
|
10
|
+
git_log_path: null
|
|
11
|
+
};
|
|
12
|
+
function normalizeValidation(validation) {
|
|
13
|
+
return {
|
|
14
|
+
...DEFAULT_VALIDATION,
|
|
15
|
+
...validation,
|
|
16
|
+
mode: validation?.mode ?? 'per-task',
|
|
17
|
+
grouping: validation?.grouping ?? null,
|
|
18
|
+
status: validation?.status ?? 'pending'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function ensureLearningSection(manifest) {
|
|
22
|
+
if (!manifest.learning) {
|
|
23
|
+
manifest.learning = {
|
|
24
|
+
validation: { ...DEFAULT_VALIDATION },
|
|
25
|
+
alerts: [],
|
|
26
|
+
approvals: []
|
|
27
|
+
};
|
|
28
|
+
return manifest.learning;
|
|
29
|
+
}
|
|
30
|
+
manifest.learning.validation = normalizeValidation(manifest.learning.validation);
|
|
31
|
+
if (!Array.isArray(manifest.learning.alerts)) {
|
|
32
|
+
manifest.learning.alerts = [];
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(manifest.learning.approvals)) {
|
|
35
|
+
manifest.learning.approvals = [];
|
|
36
|
+
}
|
|
37
|
+
return manifest.learning;
|
|
38
|
+
}
|
|
39
|
+
export function appendLearningAlert(manifest, alert) {
|
|
40
|
+
const section = ensureLearningSection(manifest);
|
|
41
|
+
const next = {
|
|
42
|
+
...alert,
|
|
43
|
+
created_at: alert.created_at ?? isoTimestamp()
|
|
44
|
+
};
|
|
45
|
+
section.alerts = [...(section.alerts ?? []), next];
|
|
46
|
+
return next;
|
|
47
|
+
}
|
|
48
|
+
export function updateLearningValidation(manifest, status, updates = {}) {
|
|
49
|
+
const section = ensureLearningSection(manifest);
|
|
50
|
+
section.validation = {
|
|
51
|
+
...normalizeValidation(section.validation),
|
|
52
|
+
...updates,
|
|
53
|
+
status
|
|
54
|
+
};
|
|
55
|
+
return section;
|
|
56
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { isoTimestamp } from '../cli/utils/time.js';
|
|
4
|
+
import { logger } from '../logger.js';
|
|
5
|
+
import { sanitizeRunId } from '../persistence/sanitizeRunId.js';
|
|
6
|
+
import { sanitizeTaskId } from '../persistence/sanitizeTaskId.js';
|
|
7
|
+
import { appendLearningAlert, ensureLearningSection, updateLearningValidation } from './manifest.js';
|
|
8
|
+
const DEFAULT_MANUAL_TEMPLATE = '.agent/task/templates/manual-scenario-template.md';
|
|
9
|
+
export async function synthesizeScenario(options) {
|
|
10
|
+
const { manifest, taskId, runId, runsRoot, prompt = null, diff = null, executionHistory = [], templatePath = DEFAULT_MANUAL_TEMPLATE, maxAttempts = 2, grouping = null, alertTargets, pagerDutySeverity = 'none' } = options;
|
|
11
|
+
const learning = ensureLearningSection(manifest);
|
|
12
|
+
if (grouping && grouping.members.length > 0) {
|
|
13
|
+
learning.validation = {
|
|
14
|
+
mode: 'grouped',
|
|
15
|
+
grouping,
|
|
16
|
+
status: learning.validation?.status ?? 'pending'
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
else if (!learning.validation) {
|
|
20
|
+
learning.validation = { mode: 'per-task', grouping: null, status: 'pending' };
|
|
21
|
+
}
|
|
22
|
+
const safeTaskId = sanitizeTaskId(taskId);
|
|
23
|
+
const runDir = join(runsRoot, safeTaskId, 'cli', sanitizeRunId(runId));
|
|
24
|
+
const learningDir = join(runDir, 'learning');
|
|
25
|
+
await mkdir(learningDir, { recursive: true });
|
|
26
|
+
const attempts = (learning.scenario?.attempts ?? 0) + 1;
|
|
27
|
+
const inferred = inferEntrypoint(executionHistory, prompt, diff, templatePath);
|
|
28
|
+
if (inferred.command) {
|
|
29
|
+
const scenarioPath = join(learningDir, 'scenario.json');
|
|
30
|
+
const scenarioPayload = buildScenarioPayload(runId, inferred.command, {
|
|
31
|
+
prompt,
|
|
32
|
+
diff,
|
|
33
|
+
executionHistory
|
|
34
|
+
});
|
|
35
|
+
await writeFile(scenarioPath, JSON.stringify(scenarioPayload, null, 2), 'utf8');
|
|
36
|
+
learning.scenario = {
|
|
37
|
+
path: relative(process.cwd(), scenarioPath),
|
|
38
|
+
generated_at: isoTimestamp(),
|
|
39
|
+
source: inferred.source,
|
|
40
|
+
status: 'synthesized',
|
|
41
|
+
attempts,
|
|
42
|
+
partial_path: null,
|
|
43
|
+
manual_template: null,
|
|
44
|
+
approver: null,
|
|
45
|
+
reason: null
|
|
46
|
+
};
|
|
47
|
+
if (learning.validation?.status === 'needs_manual_scenario') {
|
|
48
|
+
updateLearningValidation(manifest, 'pending');
|
|
49
|
+
}
|
|
50
|
+
return { manifest, scenarioPath, partialPath: null };
|
|
51
|
+
}
|
|
52
|
+
if (attempts < maxAttempts) {
|
|
53
|
+
learning.scenario = {
|
|
54
|
+
path: null,
|
|
55
|
+
generated_at: null,
|
|
56
|
+
source: inferred.source,
|
|
57
|
+
status: 'pending',
|
|
58
|
+
attempts,
|
|
59
|
+
partial_path: null,
|
|
60
|
+
manual_template: null,
|
|
61
|
+
approver: null,
|
|
62
|
+
reason: 'insufficient signals; retrying heuristics'
|
|
63
|
+
};
|
|
64
|
+
logger.warn('[learning] scenario heuristics did not resolve entrypoint; will retry');
|
|
65
|
+
return { manifest, scenarioPath: null, partialPath: null };
|
|
66
|
+
}
|
|
67
|
+
const partialScenario = {
|
|
68
|
+
id: `learning-${sanitizeRunId(runId)}`,
|
|
69
|
+
fallback_reason: inferred.reason ?? 'heuristics exhausted',
|
|
70
|
+
prompt,
|
|
71
|
+
diff,
|
|
72
|
+
execution_history: executionHistory
|
|
73
|
+
};
|
|
74
|
+
const partialPath = join(learningDir, 'scenario.partial.json');
|
|
75
|
+
await writeFile(partialPath, JSON.stringify(partialScenario, null, 2), 'utf8');
|
|
76
|
+
learning.scenario = {
|
|
77
|
+
path: null,
|
|
78
|
+
generated_at: null,
|
|
79
|
+
source: inferred.source,
|
|
80
|
+
status: 'needs_manual_scenario',
|
|
81
|
+
attempts,
|
|
82
|
+
partial_path: relative(process.cwd(), partialPath),
|
|
83
|
+
manual_template: templatePath,
|
|
84
|
+
approver: null,
|
|
85
|
+
reason: inferred.reason ?? 'Heuristic synthesis failed twice'
|
|
86
|
+
};
|
|
87
|
+
updateLearningValidation(manifest, 'needs_manual_scenario');
|
|
88
|
+
appendLearningAlert(manifest, {
|
|
89
|
+
type: 'needs_manual_scenario',
|
|
90
|
+
channel: 'slack',
|
|
91
|
+
target: alertTargets?.slack ?? '#learning-alerts',
|
|
92
|
+
message: `Scenario synthesis needs manual input for ${taskId}/${runId}`
|
|
93
|
+
});
|
|
94
|
+
if (pagerDutySeverity && pagerDutySeverity !== 'none') {
|
|
95
|
+
appendLearningAlert(manifest, {
|
|
96
|
+
type: 'needs_manual_scenario',
|
|
97
|
+
channel: 'pagerduty',
|
|
98
|
+
target: alertTargets?.pagerduty ?? 'learning-pipeline',
|
|
99
|
+
message: `Manual scenario required (${pagerDutySeverity}) for ${taskId}/${runId}`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return { manifest, scenarioPath: null, partialPath: partialScenario ? partialPath : null };
|
|
103
|
+
}
|
|
104
|
+
function inferEntrypoint(executionHistory, prompt, diff, templatePath) {
|
|
105
|
+
const recentSuccess = [...executionHistory].reverse().find((entry) => entry.exitCode === 0 && entry.command);
|
|
106
|
+
if (recentSuccess?.command) {
|
|
107
|
+
return { source: 'execution_history', command: recentSuccess.command };
|
|
108
|
+
}
|
|
109
|
+
const promptCommand = extractCommandFromPrompt(prompt);
|
|
110
|
+
if (promptCommand) {
|
|
111
|
+
return { source: 'prompt', command: promptCommand };
|
|
112
|
+
}
|
|
113
|
+
const diffCommand = extractCommandFromDiff(diff);
|
|
114
|
+
if (diffCommand) {
|
|
115
|
+
return { source: 'diff', command: diffCommand };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
source: 'template',
|
|
119
|
+
command: null,
|
|
120
|
+
reason: `No command inferred; manual template (${templatePath}) required`
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function extractCommandFromPrompt(prompt) {
|
|
124
|
+
if (!prompt)
|
|
125
|
+
return null;
|
|
126
|
+
const fenced = /`{3,}[^`]*\n([^`]+)\n`{3,}/m.exec(prompt);
|
|
127
|
+
if (fenced?.[1]) {
|
|
128
|
+
const candidate = fenced[1].trim().split('\n').find((line) => line.trim().length > 0);
|
|
129
|
+
if (candidate)
|
|
130
|
+
return candidate.trim();
|
|
131
|
+
}
|
|
132
|
+
const inline = /`([^`]+)`/.exec(prompt);
|
|
133
|
+
if (inline?.[1]) {
|
|
134
|
+
return inline[1].trim();
|
|
135
|
+
}
|
|
136
|
+
const dollar = /^\s*\$ ([^\n]+)/m.exec(prompt);
|
|
137
|
+
if (dollar?.[1]) {
|
|
138
|
+
return dollar[1].trim();
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function extractCommandFromDiff(diff) {
|
|
143
|
+
if (!diff)
|
|
144
|
+
return null;
|
|
145
|
+
const match = /[+-]{3}\s+[ab]\/([^\n\r]+)/.exec(diff);
|
|
146
|
+
const path = match?.[1];
|
|
147
|
+
if (!path)
|
|
148
|
+
return null;
|
|
149
|
+
const normalized = path.trim();
|
|
150
|
+
const isTest = normalized.includes('.test.') ||
|
|
151
|
+
normalized.includes('.spec.') ||
|
|
152
|
+
normalized.includes('__tests__') ||
|
|
153
|
+
normalized.endsWith('.snap');
|
|
154
|
+
if (isTest) {
|
|
155
|
+
return `npm test ${normalized}`;
|
|
156
|
+
}
|
|
157
|
+
if (normalized.endsWith('.ts') || normalized.endsWith('.js')) {
|
|
158
|
+
return `npm test -- ${normalized}`;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
function buildScenarioPayload(runId, command, context) {
|
|
163
|
+
return {
|
|
164
|
+
id: `learning-${sanitizeRunId(runId)}`,
|
|
165
|
+
entrypoint: command,
|
|
166
|
+
commands: [command],
|
|
167
|
+
validation: {
|
|
168
|
+
requiresCleanFixture: true
|
|
169
|
+
},
|
|
170
|
+
context: {
|
|
171
|
+
prompt: context.prompt,
|
|
172
|
+
diff: context.diff,
|
|
173
|
+
execution_history: context.executionHistory
|
|
174
|
+
},
|
|
175
|
+
generated_at: isoTimestamp()
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createWriteStream } from 'node:fs';
|
|
2
|
+
import { exec, execFile } from 'node:child_process';
|
|
3
|
+
import { promisify } from 'node:util';
|
|
4
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { isAbsolute, join, relative } from 'node:path';
|
|
6
|
+
import { saveManifest } from '../cli/run/manifest.js';
|
|
7
|
+
import { ensureLearningSection, updateLearningValidation } from './manifest.js';
|
|
8
|
+
import { sanitizeTaskId } from '../persistence/sanitizeTaskId.js';
|
|
9
|
+
import { sanitizeRunId } from '../persistence/sanitizeRunId.js';
|
|
10
|
+
import { isoTimestamp } from '../cli/utils/time.js';
|
|
11
|
+
import { logger } from '../logger.js';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
export async function runScenarioValidation(options) {
|
|
15
|
+
const { manifest, repoRoot, runsRoot, taskId, runId, paths, scenarioPath } = options;
|
|
16
|
+
const learning = ensureLearningSection(manifest);
|
|
17
|
+
const scenarioLocation = scenarioPath ?? learning.scenario?.path ?? null;
|
|
18
|
+
if (!scenarioLocation) {
|
|
19
|
+
logger.debug('[learning] scenario validation skipped: no scenario path');
|
|
20
|
+
return { manifest, logPath: null };
|
|
21
|
+
}
|
|
22
|
+
const resolvedScenarioPath = isAbsolute(scenarioLocation)
|
|
23
|
+
? scenarioLocation
|
|
24
|
+
: join(repoRoot, scenarioLocation);
|
|
25
|
+
let scenarioContents;
|
|
26
|
+
try {
|
|
27
|
+
scenarioContents = await readFile(resolvedScenarioPath, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logger.warn(`[learning] scenario validation skipped: unable to read scenario ${resolvedScenarioPath}: ${error?.message ?? String(error)}`);
|
|
31
|
+
return { manifest, logPath: null };
|
|
32
|
+
}
|
|
33
|
+
let scenario;
|
|
34
|
+
try {
|
|
35
|
+
scenario = JSON.parse(scenarioContents);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
const reason = `Invalid scenario JSON: ${error?.message ?? String(error)}`;
|
|
39
|
+
updateLearningValidation(manifest, 'snapshot_failed', { reason, last_error: reason });
|
|
40
|
+
await saveManifest(paths, manifest);
|
|
41
|
+
return { manifest, logPath: null };
|
|
42
|
+
}
|
|
43
|
+
const commands = Array.isArray(scenario.commands)
|
|
44
|
+
? scenario.commands.filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
|
|
45
|
+
: [];
|
|
46
|
+
if (commands.length === 0) {
|
|
47
|
+
const reason = 'Scenario has no commands to validate';
|
|
48
|
+
updateLearningValidation(manifest, 'snapshot_failed', { reason, last_error: reason });
|
|
49
|
+
await saveManifest(paths, manifest);
|
|
50
|
+
return { manifest, logPath: null };
|
|
51
|
+
}
|
|
52
|
+
const learningDir = join(runsRoot, sanitizeTaskId(taskId), 'cli', sanitizeRunId(runId), 'learning');
|
|
53
|
+
await mkdir(learningDir, { recursive: true });
|
|
54
|
+
const logPath = join(learningDir, 'scenario-validation.log');
|
|
55
|
+
const relativeLogPath = relative(repoRoot, logPath);
|
|
56
|
+
const logStream = createWriteStream(logPath, { flags: 'a' });
|
|
57
|
+
const writeLog = (line) => logStream.write(`[${isoTimestamp()}] ${line}\n`);
|
|
58
|
+
writeLog(`Scenario: ${resolvedScenarioPath}`);
|
|
59
|
+
const requiresCleanFixture = Boolean(scenario.validation?.requiresCleanFixture);
|
|
60
|
+
let gitStatusPath = null;
|
|
61
|
+
let gitLogPath = null;
|
|
62
|
+
try {
|
|
63
|
+
if (requiresCleanFixture) {
|
|
64
|
+
const gitState = await collectGitState(repoRoot);
|
|
65
|
+
if (gitState.dirty) {
|
|
66
|
+
gitStatusPath = join(learningDir, 'validation-git-status.txt');
|
|
67
|
+
gitLogPath = join(learningDir, 'validation-git-log.txt');
|
|
68
|
+
await Promise.all([
|
|
69
|
+
writeFile(gitStatusPath, gitState.statusOutput, 'utf8'),
|
|
70
|
+
writeFile(gitLogPath, gitState.logOutput, 'utf8')
|
|
71
|
+
]);
|
|
72
|
+
const reason = gitState.reason ?? 'Validation requires a clean fixture';
|
|
73
|
+
writeLog(reason);
|
|
74
|
+
updateLearningValidation(manifest, 'stalled_snapshot', {
|
|
75
|
+
reason,
|
|
76
|
+
log_path: relativeLogPath,
|
|
77
|
+
last_error: reason,
|
|
78
|
+
git_status_path: relative(repoRoot, gitStatusPath),
|
|
79
|
+
git_log_path: relative(repoRoot, gitLogPath)
|
|
80
|
+
});
|
|
81
|
+
await saveManifest(paths, manifest);
|
|
82
|
+
return { manifest, logPath: relativeLogPath };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const command of commands) {
|
|
86
|
+
writeLog(`$ ${command}`);
|
|
87
|
+
const result = await runCommand(command, repoRoot);
|
|
88
|
+
if (result.stdout.trim()) {
|
|
89
|
+
writeLog(result.stdout.trimEnd());
|
|
90
|
+
}
|
|
91
|
+
if (result.stderr.trim()) {
|
|
92
|
+
writeLog(result.stderr.trimEnd());
|
|
93
|
+
}
|
|
94
|
+
if (result.exitCode !== 0) {
|
|
95
|
+
const reason = `Command "${command}" exited with code ${result.exitCode}`;
|
|
96
|
+
updateLearningValidation(manifest, 'snapshot_failed', {
|
|
97
|
+
reason,
|
|
98
|
+
log_path: relativeLogPath,
|
|
99
|
+
last_error: result.stderr.trim() || reason
|
|
100
|
+
});
|
|
101
|
+
await saveManifest(paths, manifest);
|
|
102
|
+
return { manifest, logPath: relativeLogPath };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
updateLearningValidation(manifest, 'validated', {
|
|
106
|
+
reason: null,
|
|
107
|
+
log_path: relativeLogPath,
|
|
108
|
+
last_error: null,
|
|
109
|
+
git_status_path: gitStatusPath ? relative(repoRoot, gitStatusPath) : null,
|
|
110
|
+
git_log_path: gitLogPath ? relative(repoRoot, gitLogPath) : null
|
|
111
|
+
});
|
|
112
|
+
await saveManifest(paths, manifest);
|
|
113
|
+
return { manifest, logPath: relativeLogPath };
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
logStream.end();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function runCommand(command, cwd) {
|
|
120
|
+
try {
|
|
121
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
122
|
+
cwd,
|
|
123
|
+
env: { ...process.env },
|
|
124
|
+
maxBuffer: 10 * 1024 * 1024
|
|
125
|
+
});
|
|
126
|
+
return { exitCode: 0, stdout: stdout ?? '', stderr: stderr ?? '' };
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const execError = error;
|
|
130
|
+
const exitCode = normalizeExitCode(execError.code);
|
|
131
|
+
return { exitCode, stdout: execError.stdout ?? '', stderr: execError.stderr ?? '' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function collectGitState(repoRoot) {
|
|
135
|
+
try {
|
|
136
|
+
const [statusOutput, logOutput] = await Promise.all([
|
|
137
|
+
execFileAsync('git', ['status', '--short'], { cwd: repoRoot }).then((result) => result.stdout),
|
|
138
|
+
execFileAsync('git', ['log', '-5', '--oneline'], { cwd: repoRoot }).then((result) => result.stdout)
|
|
139
|
+
]);
|
|
140
|
+
const dirty = statusOutput.trim().length > 0;
|
|
141
|
+
return { dirty, statusOutput, logOutput };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
return {
|
|
146
|
+
dirty: true,
|
|
147
|
+
statusOutput: `git status failed: ${message}`,
|
|
148
|
+
logOutput: `git log failed: ${message}`,
|
|
149
|
+
reason: 'Validation requires git metadata but repository is unavailable'
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function normalizeExitCode(code) {
|
|
154
|
+
if (typeof code === 'number' && Number.isInteger(code)) {
|
|
155
|
+
return code;
|
|
156
|
+
}
|
|
157
|
+
if (typeof code === 'string') {
|
|
158
|
+
const parsed = Number.parseInt(code, 10);
|
|
159
|
+
if (Number.isInteger(parsed)) {
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* eslint-disable patterns/prefer-logger-over-console */
|
|
2
|
+
// Base logger lives at the bottom of the stack, so it writes to console directly to avoid recursive dependencies.
|
|
3
|
+
const prefix = '[Codex-Orchestrator]';
|
|
4
|
+
function format(message) {
|
|
5
|
+
return `${prefix} ${message}`;
|
|
6
|
+
}
|
|
7
|
+
export const logger = {
|
|
8
|
+
info(message, ...args) {
|
|
9
|
+
console.info(format(message), ...args);
|
|
10
|
+
},
|
|
11
|
+
warn(message, ...args) {
|
|
12
|
+
console.warn(format(message), ...args);
|
|
13
|
+
},
|
|
14
|
+
error(message, ...args) {
|
|
15
|
+
console.error(format(message), ...args);
|
|
16
|
+
},
|
|
17
|
+
debug(message, ...args) {
|
|
18
|
+
console.debug(format(message), ...args);
|
|
19
|
+
}
|
|
20
|
+
};
|