@kbediako/codex-orchestrator 0.1.1 → 0.1.3

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 (58) hide show
  1. package/README.md +11 -8
  2. package/dist/bin/codex-orchestrator.js +245 -121
  3. package/dist/orchestrator/src/cli/config/userConfig.js +86 -12
  4. package/dist/orchestrator/src/cli/devtoolsSetup.js +66 -0
  5. package/dist/orchestrator/src/cli/doctor.js +46 -21
  6. package/dist/orchestrator/src/cli/exec/context.js +5 -2
  7. package/dist/orchestrator/src/cli/exec/learning.js +5 -3
  8. package/dist/orchestrator/src/cli/exec/stageRunner.js +1 -1
  9. package/dist/orchestrator/src/cli/exec/summary.js +1 -1
  10. package/dist/orchestrator/src/cli/orchestrator.js +16 -7
  11. package/dist/orchestrator/src/cli/pipelines/index.js +13 -24
  12. package/dist/orchestrator/src/cli/rlm/prompt.js +31 -0
  13. package/dist/orchestrator/src/cli/rlm/runner.js +177 -0
  14. package/dist/orchestrator/src/cli/rlm/types.js +1 -0
  15. package/dist/orchestrator/src/cli/rlm/validator.js +159 -0
  16. package/dist/orchestrator/src/cli/rlmRunner.js +417 -0
  17. package/dist/orchestrator/src/cli/run/environment.js +4 -11
  18. package/dist/orchestrator/src/cli/run/manifest.js +7 -1
  19. package/dist/orchestrator/src/cli/services/commandRunner.js +1 -1
  20. package/dist/orchestrator/src/cli/services/controlPlaneService.js +3 -1
  21. package/dist/orchestrator/src/cli/services/execRuntime.js +1 -2
  22. package/dist/orchestrator/src/cli/services/pipelineResolver.js +33 -2
  23. package/dist/orchestrator/src/cli/services/runPreparation.js +7 -1
  24. package/dist/orchestrator/src/cli/services/schedulerService.js +1 -1
  25. package/dist/orchestrator/src/cli/utils/devtools.js +178 -0
  26. package/dist/orchestrator/src/cli/utils/specGuardRunner.js +3 -1
  27. package/dist/orchestrator/src/cli/utils/strings.js +8 -6
  28. package/dist/orchestrator/src/persistence/ExperienceStore.js +6 -16
  29. package/dist/orchestrator/src/persistence/TaskStateStore.js +1 -1
  30. package/dist/orchestrator/src/persistence/sanitizeIdentifier.js +1 -1
  31. package/dist/packages/orchestrator/src/exec/stdio.js +112 -0
  32. package/dist/packages/orchestrator/src/exec/unified-exec.js +1 -1
  33. package/dist/packages/orchestrator/src/index.js +1 -0
  34. package/dist/packages/shared/design-artifacts/writer.js +4 -14
  35. package/dist/packages/shared/streams/stdio.js +2 -112
  36. package/dist/packages/shared/utils/strings.js +17 -0
  37. package/dist/scripts/design/pipeline/advanced-assets.js +1 -1
  38. package/dist/scripts/design/pipeline/context.js +5 -5
  39. package/dist/scripts/design/pipeline/extract.js +9 -6
  40. package/dist/scripts/design/pipeline/{optionalDeps.js → optional-deps.js} +49 -38
  41. package/dist/scripts/design/pipeline/permit.js +59 -0
  42. package/dist/scripts/design/pipeline/toolkit/common.js +18 -32
  43. package/dist/scripts/design/pipeline/toolkit/reference.js +1 -1
  44. package/dist/scripts/design/pipeline/toolkit/snapshot.js +1 -1
  45. package/dist/scripts/design/pipeline/visual-regression.js +2 -11
  46. package/dist/scripts/lib/cli-args.js +53 -0
  47. package/dist/scripts/lib/docs-helpers.js +111 -0
  48. package/dist/scripts/lib/npm-pack.js +20 -0
  49. package/dist/scripts/lib/run-manifests.js +160 -0
  50. package/package.json +17 -6
  51. package/dist/orchestrator/src/cli/pipelines/defaultDiagnostics.js +0 -32
  52. package/dist/orchestrator/src/cli/pipelines/designReference.js +0 -72
  53. package/dist/orchestrator/src/cli/pipelines/hiFiDesignToolkit.js +0 -71
  54. package/dist/orchestrator/src/cli/utils/jsonlWriter.js +0 -10
  55. package/dist/orchestrator/src/control-plane/index.js +0 -3
  56. package/dist/orchestrator/src/persistence/identifierGuards.js +0 -1
  57. package/dist/orchestrator/src/persistence/writeAtomicFile.js +0 -4
  58. package/dist/orchestrator/src/scheduler/index.js +0 -1
@@ -1,8 +1,12 @@
1
1
  import process from 'node:process';
2
- import { loadUserConfig } from '../config/userConfig.js';
2
+ import { loadPackageConfig, loadUserConfig } from '../config/userConfig.js';
3
3
  import { resolvePipeline } from '../pipelines/index.js';
4
4
  import { loadDesignConfig, shouldActivateDesignPipeline, designPipelineId } from '../../../../packages/shared/config/index.js';
5
5
  import { logger } from '../../logger.js';
6
+ const DEVTOOLS_PIPELINE_ALIASES = new Map([
7
+ ['implementation-gate-devtools', 'implementation-gate'],
8
+ ['frontend-testing-devtools', 'frontend-testing']
9
+ ]);
6
10
  export class PipelineResolver {
7
11
  async loadDesignConfig(rootDir) {
8
12
  const designConfig = await loadDesignConfig({ rootDir });
@@ -19,9 +23,15 @@ export class PipelineResolver {
19
23
  logger.info(`PipelineResolver.resolve loaded design config from ${designConfig.path}`);
20
24
  const userConfig = await loadUserConfig(env);
21
25
  logger.info(`PipelineResolver.resolve loaded user config`);
22
- const requestedPipelineId = options.pipelineId ??
26
+ const pipelineCandidate = options.pipelineId ??
23
27
  (shouldActivateDesignPipeline(designConfig) ? designPipelineId(designConfig) : undefined);
28
+ const resolvedAlias = this.resolvePipelineAlias(pipelineCandidate);
29
+ const requestedPipelineId = resolvedAlias.pipelineId;
24
30
  const envOverrides = this.resolveDesignEnvOverrides(designConfig, requestedPipelineId);
31
+ if (resolvedAlias.devtoolsRequested) {
32
+ envOverrides.CODEX_REVIEW_DEVTOOLS = '1';
33
+ logger.warn(`[pipeline] ${resolvedAlias.aliasId} is deprecated; use ${requestedPipelineId} with CODEX_REVIEW_DEVTOOLS=1.`);
34
+ }
25
35
  try {
26
36
  const { pipeline, source } = resolvePipeline(env, {
27
37
  pipelineId: requestedPipelineId,
@@ -31,6 +41,17 @@ export class PipelineResolver {
31
41
  return { pipeline, userConfig, designConfig, source, envOverrides };
32
42
  }
33
43
  catch (error) {
44
+ if (requestedPipelineId === 'rlm' && userConfig?.source === 'repo') {
45
+ const packageConfig = await loadPackageConfig(env);
46
+ if (packageConfig) {
47
+ const { pipeline, source } = resolvePipeline(env, {
48
+ pipelineId: requestedPipelineId,
49
+ config: packageConfig
50
+ });
51
+ logger.info(`PipelineResolver.resolve selected package pipeline ${pipeline.id}`);
52
+ return { pipeline, userConfig: packageConfig, designConfig, source, envOverrides };
53
+ }
54
+ }
34
55
  logger.error(`PipelineResolver.resolve failed for ${requestedPipelineId ?? '<default>'}: ${error.message}`);
35
56
  throw error;
36
57
  }
@@ -44,4 +65,14 @@ export class PipelineResolver {
44
65
  }
45
66
  return envOverrides;
46
67
  }
68
+ resolvePipelineAlias(pipelineId) {
69
+ if (!pipelineId) {
70
+ return { pipelineId, devtoolsRequested: false };
71
+ }
72
+ const target = DEVTOOLS_PIPELINE_ALIASES.get(pipelineId);
73
+ if (!target) {
74
+ return { pipelineId, devtoolsRequested: false };
75
+ }
76
+ return { pipelineId: target, devtoolsRequested: true, aliasId: pipelineId };
77
+ }
47
78
  }
@@ -62,11 +62,17 @@ export async function prepareRun(options) {
62
62
  planPreview
63
63
  };
64
64
  }
65
- export function resolvePipelineForResume(env, manifest, config) {
65
+ export function resolvePipelineForResume(env, manifest, config, fallbackConfig = null) {
66
66
  const existing = findPipeline(config ?? null, manifest.pipeline_id);
67
67
  if (existing) {
68
68
  return existing;
69
69
  }
70
+ if (manifest.pipeline_id === 'rlm' && fallbackConfig) {
71
+ const fallback = findPipeline(fallbackConfig, manifest.pipeline_id);
72
+ if (fallback) {
73
+ return fallback;
74
+ }
75
+ }
70
76
  const { pipeline } = resolvePipeline(env, { pipelineId: manifest.pipeline_id, config });
71
77
  return pipeline;
72
78
  }
@@ -1,6 +1,6 @@
1
1
  import { sanitizeTaskId } from '../run/environment.js';
2
2
  import { persistManifest } from '../run/manifestPersister.js';
3
- import { buildSchedulerRunSummary, createSchedulerPlan, finalizeSchedulerPlan, serializeSchedulerPlan } from '../../scheduler/index.js';
3
+ import { buildSchedulerRunSummary, createSchedulerPlan, finalizeSchedulerPlan, serializeSchedulerPlan } from '../../scheduler/plan.js';
4
4
  import { isoTimestamp } from '../utils/time.js';
5
5
  export class SchedulerService {
6
6
  now;
@@ -1,5 +1,29 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import process from 'node:process';
1
5
  import { EnvUtils } from '../../../../packages/shared/config/env.js';
6
+ export const DEVTOOLS_SKILL_NAME = 'chrome-devtools';
2
7
  export const DEVTOOLS_CONFIG_OVERRIDE = 'mcp_servers.chrome-devtools.enabled=true';
8
+ const DEVTOOLS_CONFIG_FILENAME = 'config.toml';
9
+ const DEVTOOLS_MCP_COMMAND = [
10
+ 'mcp',
11
+ 'add',
12
+ DEVTOOLS_SKILL_NAME,
13
+ '--',
14
+ 'npx',
15
+ '-y',
16
+ 'chrome-devtools-mcp@latest',
17
+ '--categoryEmulation',
18
+ '--categoryPerformance',
19
+ '--categoryNetwork'
20
+ ];
21
+ const DEVTOOLS_CONFIG_SNIPPET = [
22
+ '[mcp_servers.chrome-devtools]',
23
+ 'command = "npx"',
24
+ 'args = ["-y", "chrome-devtools-mcp@latest", "--categoryEmulation", "--categoryPerformance", "--categoryNetwork"]',
25
+ 'enabled = false'
26
+ ].join('\n');
3
27
  export function isDevtoolsEnabled(env = process.env) {
4
28
  const raw = env.CODEX_REVIEW_DEVTOOLS;
5
29
  if (!raw) {
@@ -7,12 +31,166 @@ export function isDevtoolsEnabled(env = process.env) {
7
31
  }
8
32
  return EnvUtils.isTrue(raw.trim().toLowerCase());
9
33
  }
34
+ export function resolveCodexHome(env = process.env) {
35
+ const override = env.CODEX_HOME?.trim();
36
+ if (override) {
37
+ return override;
38
+ }
39
+ return join(homedir(), '.codex');
40
+ }
41
+ export function resolveCodexConfigPath(env = process.env) {
42
+ return join(resolveCodexHome(env), DEVTOOLS_CONFIG_FILENAME);
43
+ }
44
+ export function resolveDevtoolsReadiness(env = process.env) {
45
+ const codexHome = resolveCodexHome(env);
46
+ const skillPath = join(codexHome, 'skills', DEVTOOLS_SKILL_NAME, 'SKILL.md');
47
+ const skillInstalled = existsSync(skillPath);
48
+ const config = inspectDevtoolsConfig(env);
49
+ const configReady = config.status === 'ok';
50
+ let status;
51
+ if (config.status === 'invalid') {
52
+ status = 'invalid-config';
53
+ }
54
+ else if (!skillInstalled && !configReady) {
55
+ status = 'missing-both';
56
+ }
57
+ else if (!skillInstalled) {
58
+ status = 'missing-skill';
59
+ }
60
+ else if (!configReady) {
61
+ status = 'missing-config';
62
+ }
63
+ else {
64
+ status = 'ok';
65
+ }
66
+ return {
67
+ status,
68
+ skill: {
69
+ status: skillInstalled ? 'ok' : 'missing',
70
+ path: skillPath
71
+ },
72
+ config
73
+ };
74
+ }
75
+ export function buildDevtoolsSetupPlan(env = process.env) {
76
+ const codexHome = resolveCodexHome(env);
77
+ const configPath = resolveCodexConfigPath(env);
78
+ const args = [...DEVTOOLS_MCP_COMMAND];
79
+ return {
80
+ codexHome,
81
+ configPath,
82
+ command: 'codex',
83
+ args,
84
+ commandLine: ['codex', ...args].join(' '),
85
+ configSnippet: DEVTOOLS_CONFIG_SNIPPET
86
+ };
87
+ }
10
88
  export function resolveCodexCommand(args, env = process.env) {
11
89
  if (!isDevtoolsEnabled(env)) {
12
90
  return { command: 'codex', args };
13
91
  }
92
+ const readiness = resolveDevtoolsReadiness(env);
93
+ if (readiness.status !== 'ok') {
94
+ throw new Error(formatDevtoolsPreflightError(readiness));
95
+ }
14
96
  return {
15
97
  command: 'codex',
16
98
  args: ['-c', DEVTOOLS_CONFIG_OVERRIDE, ...args]
17
99
  };
18
100
  }
101
+ export function formatDevtoolsPreflightError(readiness) {
102
+ const lines = ['DevTools MCP is not ready for this run.'];
103
+ lines.push(`- Skill: ${readiness.skill.status} (${readiness.skill.path})`);
104
+ const configStatus = readiness.config.status === 'invalid'
105
+ ? `invalid (${readiness.config.path})`
106
+ : `${readiness.config.status} (${readiness.config.path})`;
107
+ lines.push(`- Config: ${configStatus}`);
108
+ if (readiness.config.detail) {
109
+ lines.push(` detail: ${readiness.config.detail}`);
110
+ }
111
+ if (readiness.config.error) {
112
+ lines.push(` error: ${readiness.config.error}`);
113
+ }
114
+ lines.push('Run `codex-orchestrator doctor --format json` for details.');
115
+ lines.push('Run `codex-orchestrator devtools setup` to configure the MCP server.');
116
+ return lines.join('\n');
117
+ }
118
+ function inspectDevtoolsConfig(env = process.env) {
119
+ const configPath = resolveCodexConfigPath(env);
120
+ if (!existsSync(configPath)) {
121
+ return { status: 'missing', path: configPath, detail: 'config.toml not found' };
122
+ }
123
+ let raw;
124
+ try {
125
+ raw = readFileSync(configPath, 'utf8');
126
+ }
127
+ catch (error) {
128
+ return {
129
+ status: 'invalid',
130
+ path: configPath,
131
+ error: error instanceof Error ? error.message : String(error)
132
+ };
133
+ }
134
+ const hasEntry = hasDevtoolsConfigEntry(raw);
135
+ if (hasEntry) {
136
+ return { status: 'ok', path: configPath };
137
+ }
138
+ return {
139
+ status: 'missing',
140
+ path: configPath,
141
+ detail: 'chrome-devtools entry not found'
142
+ };
143
+ }
144
+ function hasDevtoolsConfigEntry(raw) {
145
+ const lines = raw.split('\n');
146
+ let currentTable = null;
147
+ for (const line of lines) {
148
+ const trimmed = stripTomlComment(line).trim();
149
+ if (!trimmed) {
150
+ continue;
151
+ }
152
+ const tableMatch = trimmed.match(/^\[(.+)\]$/);
153
+ if (tableMatch) {
154
+ currentTable = tableMatch[1]?.trim() ?? null;
155
+ if (currentTable === 'mcp_servers.chrome-devtools' ||
156
+ currentTable === 'mcp_servers."chrome-devtools"' ||
157
+ currentTable === "mcp_servers.'chrome-devtools'") {
158
+ return true;
159
+ }
160
+ continue;
161
+ }
162
+ if (trimmed.startsWith('mcp_servers.')) {
163
+ if (trimmed.startsWith('mcp_servers."chrome-devtools".')) {
164
+ return true;
165
+ }
166
+ if (trimmed.startsWith("mcp_servers.'chrome-devtools'.")) {
167
+ return true;
168
+ }
169
+ if (trimmed.startsWith('mcp_servers.chrome-devtools.')) {
170
+ return true;
171
+ }
172
+ if (trimmed.startsWith('mcp_servers."chrome-devtools"=')) {
173
+ return true;
174
+ }
175
+ if (trimmed.startsWith("mcp_servers.'chrome-devtools'=")) {
176
+ return true;
177
+ }
178
+ if (trimmed.startsWith('mcp_servers.chrome-devtools=')) {
179
+ return true;
180
+ }
181
+ }
182
+ if (currentTable === 'mcp_servers') {
183
+ if (/^"?chrome-devtools"?\s*=/.test(trimmed)) {
184
+ return true;
185
+ }
186
+ }
187
+ }
188
+ return false;
189
+ }
190
+ function stripTomlComment(line) {
191
+ const index = line.indexOf('#');
192
+ if (index === -1) {
193
+ return line;
194
+ }
195
+ return line.slice(0, index);
196
+ }
@@ -3,7 +3,9 @@ import { existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import process from 'node:process';
5
5
  import { logger } from '../../logger.js';
6
- const specGuardPath = join(process.cwd(), 'scripts', 'spec-guard.mjs');
6
+ import { resolveEnvironmentPaths } from '../../../../scripts/lib/run-manifests.js';
7
+ const { repoRoot } = resolveEnvironmentPaths();
8
+ const specGuardPath = join(repoRoot, 'scripts', 'spec-guard.mjs');
7
9
  if (!existsSync(specGuardPath)) {
8
10
  logger.warn(`[spec-guard] skipped: ${specGuardPath} not found`);
9
11
  process.exit(0);
@@ -1,8 +1,10 @@
1
+ import { slugify as sharedSlugify } from '../../../../packages/shared/utils/strings.js';
1
2
  export function slugify(value, fallback = 'command') {
2
- const cleaned = value.trim().replace(/[^a-zA-Z0-9]+/g, '-').replace(/-+/g, '-');
3
- const normalized = cleaned.replace(/^-|-$/g, '');
4
- if (!normalized) {
5
- return fallback;
6
- }
7
- return normalized.slice(0, 80);
3
+ return sharedSlugify(value, {
4
+ fallback,
5
+ maxLength: 80,
6
+ lowercase: false,
7
+ pattern: /[^a-zA-Z0-9]+/g,
8
+ collapseDashes: true
9
+ });
8
10
  }
@@ -1,9 +1,10 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { readFile, mkdir, rm, readdir } from 'node:fs/promises';
2
+ import { readFile, mkdir, rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
+ import { listDirectories, resolveEnvironmentPaths } from '../../../scripts/lib/run-manifests.js';
4
5
  import { acquireLockWithRetry } from './lockFile.js';
5
6
  import { sanitizeTaskId } from './sanitizeTaskId.js';
6
- import { writeAtomicFile } from './writeAtomicFile.js';
7
+ import { writeAtomicFile } from '../utils/atomicWrite.js';
7
8
  export class ExperienceStoreLockError extends Error {
8
9
  taskId;
9
10
  constructor(message, taskId) {
@@ -21,8 +22,9 @@ export class ExperienceStore {
21
22
  lockRetry;
22
23
  now;
23
24
  constructor(options = {}) {
24
- this.outDir = options.outDir ?? join(process.cwd(), 'out');
25
- this.runsDir = options.runsDir ?? join(process.cwd(), '.runs');
25
+ const envPaths = resolveEnvironmentPaths();
26
+ this.outDir = options.outDir ?? envPaths.outRoot;
27
+ this.runsDir = options.runsDir ?? envPaths.runsRoot;
26
28
  this.maxWords = Math.max(1, options.maxSummaryWords ?? DEFAULT_MAX_WORDS);
27
29
  const defaults = {
28
30
  maxAttempts: 5,
@@ -183,18 +185,6 @@ export class ExperienceStore {
183
185
  return `exp-${Date.now().toString(36)}-${suffix}`;
184
186
  }
185
187
  }
186
- async function listDirectories(path) {
187
- try {
188
- const entries = await readdir(path, { withFileTypes: true });
189
- return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
190
- }
191
- catch (error) {
192
- if (error.code === 'ENOENT') {
193
- return [];
194
- }
195
- throw error;
196
- }
197
- }
198
188
  function truncateSummary(value, maxWords) {
199
189
  const tokens = value.trim().split(/\s+/u).filter(Boolean);
200
190
  if (tokens.length <= maxWords) {
@@ -2,7 +2,7 @@ import { mkdir, readFile, rm } from 'node:fs/promises';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { acquireLockWithRetry } from './lockFile.js';
4
4
  import { sanitizeTaskId } from './sanitizeTaskId.js';
5
- import { writeAtomicFile } from './writeAtomicFile.js';
5
+ import { writeAtomicFile } from '../utils/atomicWrite.js';
6
6
  export class TaskStateStoreLockError extends Error {
7
7
  taskId;
8
8
  constructor(message, taskId) {
@@ -1,4 +1,4 @@
1
- import { WINDOWS_FORBIDDEN_CHARACTERS } from './identifierGuards.js';
1
+ const WINDOWS_FORBIDDEN_CHARACTERS = new Set(['<', '>', ':', '"', '|', '?', '*']);
2
2
  export function sanitizeIdentifier(kind, value) {
3
3
  const label = kind === 'task' ? 'task' : 'run';
4
4
  if (!value) {
@@ -0,0 +1,112 @@
1
+ import { Buffer } from 'node:buffer';
2
+ /**
3
+ * Creates a tracker that sequences stdout/stderr chunks while maintaining
4
+ * bounded in-memory buffers for each stream.
5
+ */
6
+ export function createStdioTracker(options = {}) {
7
+ const encoding = options.encoding ?? 'utf-8';
8
+ const maxBufferBytes = options.maxBufferBytes ?? 64 * 1024;
9
+ const now = options.now ?? (() => new Date());
10
+ let sequence = Math.max(0, options.startSequence ?? 0);
11
+ const buffers = {
12
+ stdout: createInternalBuffer(),
13
+ stderr: createInternalBuffer()
14
+ };
15
+ const push = (stream, chunk) => {
16
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
17
+ const target = buffers[stream];
18
+ appendWithLimit(target, buffer, maxBufferBytes);
19
+ const chunkRecord = {
20
+ sequence: ++sequence,
21
+ stream,
22
+ bytes: buffer.byteLength,
23
+ data: buffer.toString(encoding),
24
+ timestamp: now().toISOString()
25
+ };
26
+ return chunkRecord;
27
+ };
28
+ const getBuffered = (stream) => {
29
+ const target = buffers[stream];
30
+ if (target.byteLength === 0) {
31
+ return '';
32
+ }
33
+ const activeChunks = snapshotChunks(target);
34
+ if (activeChunks.length === 0) {
35
+ return '';
36
+ }
37
+ if (activeChunks.length === 1) {
38
+ return activeChunks[0].toString(encoding);
39
+ }
40
+ return Buffer.concat(activeChunks, target.byteLength).toString(encoding);
41
+ };
42
+ const getBufferedBytes = (stream) => buffers[stream].byteLength;
43
+ const reset = () => {
44
+ buffers.stdout = createInternalBuffer();
45
+ buffers.stderr = createInternalBuffer();
46
+ sequence = Math.max(0, options.startSequence ?? 0);
47
+ };
48
+ return {
49
+ push,
50
+ getBuffered,
51
+ getBufferedBytes,
52
+ reset
53
+ };
54
+ }
55
+ function createInternalBuffer() {
56
+ return { chunks: [], start: 0, byteLength: 0 };
57
+ }
58
+ function snapshotChunks(buffer) {
59
+ if (buffer.start === 0) {
60
+ return buffer.chunks;
61
+ }
62
+ return buffer.chunks.slice(buffer.start);
63
+ }
64
+ function appendWithLimit(target, incoming, limit) {
65
+ if (limit <= 0) {
66
+ target.chunks = [];
67
+ target.start = 0;
68
+ target.byteLength = 0;
69
+ return;
70
+ }
71
+ if (!incoming || incoming.byteLength === 0) {
72
+ trimExcess(target, limit);
73
+ return;
74
+ }
75
+ if (incoming.byteLength >= limit) {
76
+ const trimmed = Buffer.from(incoming.subarray(incoming.byteLength - limit));
77
+ target.chunks = [trimmed];
78
+ target.start = 0;
79
+ target.byteLength = trimmed.byteLength;
80
+ return;
81
+ }
82
+ target.chunks.push(incoming);
83
+ target.byteLength += incoming.byteLength;
84
+ trimExcess(target, limit);
85
+ }
86
+ function trimExcess(target, limit) {
87
+ if (target.byteLength <= limit) {
88
+ return;
89
+ }
90
+ let startIndex = target.start;
91
+ const chunks = target.chunks;
92
+ while (target.byteLength > limit && startIndex < chunks.length) {
93
+ const head = chunks[startIndex];
94
+ if (!head) {
95
+ break;
96
+ }
97
+ const overflow = target.byteLength - limit;
98
+ if (head.byteLength <= overflow) {
99
+ target.byteLength -= head.byteLength;
100
+ startIndex += 1;
101
+ continue;
102
+ }
103
+ chunks[startIndex] = head.subarray(overflow);
104
+ target.byteLength -= overflow;
105
+ break;
106
+ }
107
+ target.start = startIndex;
108
+ if (target.start > 0 && target.start * 2 >= chunks.length) {
109
+ target.chunks = chunks.slice(target.start);
110
+ target.start = 0;
111
+ }
112
+ }
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { ToolInvocationFailedError } from '../tool-orchestrator.js';
4
- import { createStdioTracker } from '../../../shared/streams/stdio.js';
4
+ import { createStdioTracker } from './stdio.js';
5
5
  export class UnifiedExecRunner {
6
6
  orchestrator;
7
7
  sessionManager;
@@ -1,3 +1,4 @@
1
1
  export { ToolOrchestrator, SandboxRetryableError, ApprovalRequiredError, ApprovalDeniedError, ToolInvocationFailedError } from './tool-orchestrator.js';
2
2
  export { ExecSessionManager } from './exec/session-manager.js';
3
3
  export { UnifiedExecRunner } from './exec/unified-exec.js';
4
+ export { RemoteExecHandleService } from './exec/handle-service.js';
@@ -1,6 +1,8 @@
1
- import { mkdir, rename, writeFile } from 'node:fs/promises';
2
- import { dirname, join, relative as relativePath } from 'node:path';
1
+ import { join, relative as relativePath } from 'node:path';
3
2
  import { persistDesignManifest } from '../manifest/writer.js';
3
+ import { writeJsonAtomic } from '../../../orchestrator/src/cli/utils/fs.js';
4
+ import { sanitizeTaskId } from '../../../orchestrator/src/persistence/sanitizeTaskId.js';
5
+ import { sanitizeRunId } from '../../../orchestrator/src/persistence/sanitizeRunId.js';
4
6
  export async function writeDesignSummary(options) {
5
7
  const now = options.now ?? new Date();
6
8
  const context = options.context;
@@ -207,15 +209,3 @@ function accumulateNumericTotals(target, incoming) {
207
209
  }
208
210
  }
209
211
  }
210
- function sanitizeTaskId(value) {
211
- return value.replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase();
212
- }
213
- function sanitizeRunId(value) {
214
- return value.replace(/[:]/g, '-');
215
- }
216
- async function writeJsonAtomic(targetPath, payload) {
217
- const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
218
- await mkdir(dirname(targetPath), { recursive: true });
219
- await writeFile(tmpPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
220
- await rename(tmpPath, targetPath);
221
- }
@@ -1,112 +1,2 @@
1
- import { Buffer } from 'node:buffer';
2
- /**
3
- * Creates a tracker that sequences stdout/stderr chunks while maintaining
4
- * bounded in-memory buffers for each stream.
5
- */
6
- export function createStdioTracker(options = {}) {
7
- const encoding = options.encoding ?? 'utf-8';
8
- const maxBufferBytes = options.maxBufferBytes ?? 64 * 1024;
9
- const now = options.now ?? (() => new Date());
10
- let sequence = Math.max(0, options.startSequence ?? 0);
11
- const buffers = {
12
- stdout: createInternalBuffer(),
13
- stderr: createInternalBuffer()
14
- };
15
- const push = (stream, chunk) => {
16
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
17
- const target = buffers[stream];
18
- appendWithLimit(target, buffer, maxBufferBytes);
19
- const chunkRecord = {
20
- sequence: ++sequence,
21
- stream,
22
- bytes: buffer.byteLength,
23
- data: buffer.toString(encoding),
24
- timestamp: now().toISOString()
25
- };
26
- return chunkRecord;
27
- };
28
- const getBuffered = (stream) => {
29
- const target = buffers[stream];
30
- if (target.byteLength === 0) {
31
- return '';
32
- }
33
- const activeChunks = snapshotChunks(target);
34
- if (activeChunks.length === 0) {
35
- return '';
36
- }
37
- if (activeChunks.length === 1) {
38
- return activeChunks[0].toString(encoding);
39
- }
40
- return Buffer.concat(activeChunks, target.byteLength).toString(encoding);
41
- };
42
- const getBufferedBytes = (stream) => buffers[stream].byteLength;
43
- const reset = () => {
44
- buffers.stdout = createInternalBuffer();
45
- buffers.stderr = createInternalBuffer();
46
- sequence = Math.max(0, options.startSequence ?? 0);
47
- };
48
- return {
49
- push,
50
- getBuffered,
51
- getBufferedBytes,
52
- reset
53
- };
54
- }
55
- function createInternalBuffer() {
56
- return { chunks: [], start: 0, byteLength: 0 };
57
- }
58
- function snapshotChunks(buffer) {
59
- if (buffer.start === 0) {
60
- return buffer.chunks;
61
- }
62
- return buffer.chunks.slice(buffer.start);
63
- }
64
- function appendWithLimit(target, incoming, limit) {
65
- if (limit <= 0) {
66
- target.chunks = [];
67
- target.start = 0;
68
- target.byteLength = 0;
69
- return;
70
- }
71
- if (!incoming || incoming.byteLength === 0) {
72
- trimExcess(target, limit);
73
- return;
74
- }
75
- if (incoming.byteLength >= limit) {
76
- const trimmed = Buffer.from(incoming.subarray(incoming.byteLength - limit));
77
- target.chunks = [trimmed];
78
- target.start = 0;
79
- target.byteLength = trimmed.byteLength;
80
- return;
81
- }
82
- target.chunks.push(incoming);
83
- target.byteLength += incoming.byteLength;
84
- trimExcess(target, limit);
85
- }
86
- function trimExcess(target, limit) {
87
- if (target.byteLength <= limit) {
88
- return;
89
- }
90
- let startIndex = target.start;
91
- const chunks = target.chunks;
92
- while (target.byteLength > limit && startIndex < chunks.length) {
93
- const head = chunks[startIndex];
94
- if (!head) {
95
- break;
96
- }
97
- const overflow = target.byteLength - limit;
98
- if (head.byteLength <= overflow) {
99
- target.byteLength -= head.byteLength;
100
- startIndex += 1;
101
- continue;
102
- }
103
- chunks[startIndex] = head.subarray(overflow);
104
- target.byteLength -= overflow;
105
- break;
106
- }
107
- target.start = startIndex;
108
- if (target.start > 0 && target.start * 2 >= chunks.length) {
109
- target.chunks = chunks.slice(target.start);
110
- target.start = 0;
111
- }
112
- }
1
+ // Deprecated shim: keep exports stable while stdio tracking moves into packages/orchestrator.
2
+ export * from '../../orchestrator/src/exec/stdio.js';
@@ -0,0 +1,17 @@
1
+ export function slugify(value, options = {}) {
2
+ const fallback = typeof options.fallback === 'string' ? options.fallback : 'command';
3
+ const maxLength = Number.isFinite(options.maxLength)
4
+ ? Math.max(1, Math.floor(options.maxLength))
5
+ : 80;
6
+ const lowercase = options.lowercase ?? false;
7
+ const pattern = options.pattern ?? /[^a-zA-Z0-9]+/g;
8
+ const collapseDashes = options.collapseDashes ?? true;
9
+ const base = lowercase ? value.toLowerCase() : value;
10
+ const cleaned = base.trim().replace(pattern, '-');
11
+ const collapsed = collapseDashes ? cleaned.replace(/-+/g, '-') : cleaned;
12
+ const normalized = collapsed.replace(/^-+|-+$/g, '');
13
+ if (!normalized) {
14
+ return fallback;
15
+ }
16
+ return normalized.slice(0, maxLength);
17
+ }
@@ -8,7 +8,7 @@ import { loadDesignContext } from './context.js';
8
8
  import { appendApprovals, appendArtifacts, ensureToolkitState, loadDesignRunState, saveDesignRunState, upsertStage } from './state.js';
9
9
  import { stageArtifacts } from '../../../orchestrator/src/persistence/ArtifactStager.js';
10
10
  import { runDefaultInteractions } from './toolkit/snapshot.js';
11
- import { loadPlaywright } from './optionalDeps.js';
11
+ import { loadPlaywright } from './optional-deps.js';
12
12
  const execFileAsync = promisify(execFile);
13
13
  const MOTION_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
14
14
  const DEFAULT_CAPTURE_SECONDS = 12;