@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.
Files changed (150) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +238 -0
  3. package/dist/bin/codex-orchestrator.js +507 -0
  4. package/dist/orchestrator/src/agents/builder.js +16 -0
  5. package/dist/orchestrator/src/agents/index.js +4 -0
  6. package/dist/orchestrator/src/agents/planner.js +17 -0
  7. package/dist/orchestrator/src/agents/reviewer.js +13 -0
  8. package/dist/orchestrator/src/agents/tester.js +13 -0
  9. package/dist/orchestrator/src/cli/adapters/CommandBuilder.js +20 -0
  10. package/dist/orchestrator/src/cli/adapters/CommandPlanner.js +164 -0
  11. package/dist/orchestrator/src/cli/adapters/CommandReviewer.js +32 -0
  12. package/dist/orchestrator/src/cli/adapters/CommandTester.js +33 -0
  13. package/dist/orchestrator/src/cli/adapters/index.js +4 -0
  14. package/dist/orchestrator/src/cli/config/userConfig.js +28 -0
  15. package/dist/orchestrator/src/cli/doctor.js +48 -0
  16. package/dist/orchestrator/src/cli/events/runEvents.js +84 -0
  17. package/dist/orchestrator/src/cli/exec/command.js +56 -0
  18. package/dist/orchestrator/src/cli/exec/context.js +108 -0
  19. package/dist/orchestrator/src/cli/exec/experience.js +77 -0
  20. package/dist/orchestrator/src/cli/exec/finalization.js +140 -0
  21. package/dist/orchestrator/src/cli/exec/learning.js +62 -0
  22. package/dist/orchestrator/src/cli/exec/stageRunner.js +71 -0
  23. package/dist/orchestrator/src/cli/exec/summary.js +109 -0
  24. package/dist/orchestrator/src/cli/exec/telemetry.js +18 -0
  25. package/dist/orchestrator/src/cli/exec/tfgrpo.js +200 -0
  26. package/dist/orchestrator/src/cli/exec/tfgrpoArtifacts.js +19 -0
  27. package/dist/orchestrator/src/cli/exec/types.js +1 -0
  28. package/dist/orchestrator/src/cli/init.js +64 -0
  29. package/dist/orchestrator/src/cli/mcp.js +124 -0
  30. package/dist/orchestrator/src/cli/metrics/metricsAggregator.js +404 -0
  31. package/dist/orchestrator/src/cli/metrics/metricsRecorder.js +138 -0
  32. package/dist/orchestrator/src/cli/orchestrator.js +554 -0
  33. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +32 -0
  34. package/dist/orchestrator/src/cli/pipelines/designReference.js +72 -0
  35. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +71 -0
  36. package/dist/orchestrator/src/cli/pipelines/index.js +34 -0
  37. package/dist/orchestrator/src/cli/run/environment.js +24 -0
  38. package/dist/orchestrator/src/cli/run/manifest.js +367 -0
  39. package/dist/orchestrator/src/cli/run/manifestPersister.js +88 -0
  40. package/dist/orchestrator/src/cli/run/runPaths.js +30 -0
  41. package/dist/orchestrator/src/cli/selfCheck.js +12 -0
  42. package/dist/orchestrator/src/cli/services/commandRunner.js +420 -0
  43. package/dist/orchestrator/src/cli/services/controlPlaneService.js +107 -0
  44. package/dist/orchestrator/src/cli/services/execRuntime.js +69 -0
  45. package/dist/orchestrator/src/cli/services/pipelineResolver.js +47 -0
  46. package/dist/orchestrator/src/cli/services/runPreparation.js +82 -0
  47. package/dist/orchestrator/src/cli/services/runSummaryWriter.js +35 -0
  48. package/dist/orchestrator/src/cli/services/schedulerService.js +42 -0
  49. package/dist/orchestrator/src/cli/tasks/taskMetadata.js +19 -0
  50. package/dist/orchestrator/src/cli/telemetry/schema.js +8 -0
  51. package/dist/orchestrator/src/cli/types.js +1 -0
  52. package/dist/orchestrator/src/cli/ui/HudApp.js +112 -0
  53. package/dist/orchestrator/src/cli/ui/controller.js +26 -0
  54. package/dist/orchestrator/src/cli/ui/store.js +240 -0
  55. package/dist/orchestrator/src/cli/utils/enforcementMode.js +12 -0
  56. package/dist/orchestrator/src/cli/utils/fs.js +8 -0
  57. package/dist/orchestrator/src/cli/utils/interactive.js +25 -0
  58. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +10 -0
  59. package/dist/orchestrator/src/cli/utils/optionalDeps.js +30 -0
  60. package/dist/orchestrator/src/cli/utils/packageInfo.js +25 -0
  61. package/dist/orchestrator/src/cli/utils/planFormatter.js +49 -0
  62. package/dist/orchestrator/src/cli/utils/runId.js +7 -0
  63. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +26 -0
  64. package/dist/orchestrator/src/cli/utils/strings.js +8 -0
  65. package/dist/orchestrator/src/cli/utils/time.js +6 -0
  66. package/dist/orchestrator/src/control-plane/drift-reporter.js +109 -0
  67. package/dist/orchestrator/src/control-plane/index.js +3 -0
  68. package/dist/orchestrator/src/control-plane/request-builder.js +217 -0
  69. package/dist/orchestrator/src/control-plane/types.js +1 -0
  70. package/dist/orchestrator/src/control-plane/validator.js +50 -0
  71. package/dist/orchestrator/src/credentials/CredentialBroker.js +1 -0
  72. package/dist/orchestrator/src/events/EventBus.js +25 -0
  73. package/dist/orchestrator/src/learning/crystalizer.js +108 -0
  74. package/dist/orchestrator/src/learning/harvester.js +146 -0
  75. package/dist/orchestrator/src/learning/manifest.js +56 -0
  76. package/dist/orchestrator/src/learning/runner.js +177 -0
  77. package/dist/orchestrator/src/learning/validator.js +164 -0
  78. package/dist/orchestrator/src/logger.js +20 -0
  79. package/dist/orchestrator/src/manager.js +388 -0
  80. package/dist/orchestrator/src/persistence/ArtifactStager.js +95 -0
  81. package/dist/orchestrator/src/persistence/ExperienceStore.js +210 -0
  82. package/dist/orchestrator/src/persistence/PersistenceCoordinator.js +65 -0
  83. package/dist/orchestrator/src/persistence/RunManifestWriter.js +23 -0
  84. package/dist/orchestrator/src/persistence/TaskStateStore.js +172 -0
  85. package/dist/orchestrator/src/persistence/identifierGuards.js +1 -0
  86. package/dist/orchestrator/src/persistence/lockFile.js +26 -0
  87. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +26 -0
  88. package/dist/orchestrator/src/persistence/sanitizeRunId.js +8 -0
  89. package/dist/orchestrator/src/persistence/sanitizeTaskId.js +8 -0
  90. package/dist/orchestrator/src/persistence/writeAtomicFile.js +4 -0
  91. package/dist/orchestrator/src/privacy/guard.js +111 -0
  92. package/dist/orchestrator/src/scheduler/index.js +1 -0
  93. package/dist/orchestrator/src/scheduler/plan.js +171 -0
  94. package/dist/orchestrator/src/scheduler/types.js +1 -0
  95. package/dist/orchestrator/src/sync/CloudRunsClient.js +1 -0
  96. package/dist/orchestrator/src/sync/CloudRunsHttpClient.js +82 -0
  97. package/dist/orchestrator/src/sync/CloudSyncWorker.js +206 -0
  98. package/dist/orchestrator/src/sync/createCloudSyncWorker.js +15 -0
  99. package/dist/orchestrator/src/types.js +1 -0
  100. package/dist/orchestrator/src/utils/atomicWrite.js +15 -0
  101. package/dist/orchestrator/src/utils/errorMessage.js +14 -0
  102. package/dist/orchestrator/src/utils/executionMode.js +69 -0
  103. package/dist/packages/control-plane-schemas/src/index.js +1 -0
  104. package/dist/packages/control-plane-schemas/src/run-request.js +548 -0
  105. package/dist/packages/orchestrator/src/exec/handle-service.js +203 -0
  106. package/dist/packages/orchestrator/src/exec/session-manager.js +147 -0
  107. package/dist/packages/orchestrator/src/exec/unified-exec.js +432 -0
  108. package/dist/packages/orchestrator/src/index.js +3 -0
  109. package/dist/packages/orchestrator/src/instructions/loader.js +101 -0
  110. package/dist/packages/orchestrator/src/instructions/promptPacks.js +151 -0
  111. package/dist/packages/orchestrator/src/notifications/index.js +74 -0
  112. package/dist/packages/orchestrator/src/telemetry/otel-exporter.js +142 -0
  113. package/dist/packages/orchestrator/src/tool-orchestrator.js +161 -0
  114. package/dist/packages/sdk-node/src/orchestrator.js +195 -0
  115. package/dist/packages/shared/config/designConfig.js +495 -0
  116. package/dist/packages/shared/config/env.js +37 -0
  117. package/dist/packages/shared/config/index.js +2 -0
  118. package/dist/packages/shared/design-artifacts/writer.js +221 -0
  119. package/dist/packages/shared/events/serializer.js +84 -0
  120. package/dist/packages/shared/events/types.js +1 -0
  121. package/dist/packages/shared/manifest/artifactUtils.js +36 -0
  122. package/dist/packages/shared/manifest/designArtifacts.js +665 -0
  123. package/dist/packages/shared/manifest/fileIO.js +29 -0
  124. package/dist/packages/shared/manifest/toolRuns.js +78 -0
  125. package/dist/packages/shared/manifest/toolkitArtifacts.js +223 -0
  126. package/dist/packages/shared/manifest/types.js +5 -0
  127. package/dist/packages/shared/manifest/validator.js +73 -0
  128. package/dist/packages/shared/manifest/writer.js +2 -0
  129. package/dist/packages/shared/streams/stdio.js +112 -0
  130. package/dist/scripts/design/pipeline/advanced-assets.js +466 -0
  131. package/dist/scripts/design/pipeline/componentize.js +74 -0
  132. package/dist/scripts/design/pipeline/context.js +34 -0
  133. package/dist/scripts/design/pipeline/extract.js +249 -0
  134. package/dist/scripts/design/pipeline/optionalDeps.js +107 -0
  135. package/dist/scripts/design/pipeline/prepare.js +46 -0
  136. package/dist/scripts/design/pipeline/reference.js +94 -0
  137. package/dist/scripts/design/pipeline/state.js +206 -0
  138. package/dist/scripts/design/pipeline/toolkit/common.js +94 -0
  139. package/dist/scripts/design/pipeline/toolkit/extract.js +258 -0
  140. package/dist/scripts/design/pipeline/toolkit/publish.js +202 -0
  141. package/dist/scripts/design/pipeline/toolkit/publishActions.js +12 -0
  142. package/dist/scripts/design/pipeline/toolkit/reference.js +846 -0
  143. package/dist/scripts/design/pipeline/toolkit/snapshot.js +882 -0
  144. package/dist/scripts/design/pipeline/toolkit/tokens.js +456 -0
  145. package/dist/scripts/design/pipeline/visual-regression.js +137 -0
  146. package/dist/scripts/design/pipeline/write-artifacts.js +61 -0
  147. package/package.json +97 -0
  148. package/schemas/manifest.json +1064 -0
  149. package/templates/README.md +12 -0
  150. 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
+ };