sinapse-ai 1.4.2 → 1.6.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/.codex/agents/snps-orqx.md +1 -1
- package/.sinapse-ai/core/external-executors/delegate-cli.js +585 -0
- package/.sinapse-ai/core/external-executors/index.js +13 -0
- package/.sinapse-ai/core/orchestration/fast-path-gate.js +356 -0
- package/.sinapse-ai/core/orchestration/index.js +23 -0
- package/.sinapse-ai/data/entity-registry.yaml +41 -24
- package/.sinapse-ai/data/registry-update-log.jsonl +10 -0
- package/.sinapse-ai/development/agents/snps-orqx.md +1 -1
- package/.sinapse-ai/install-manifest.yaml +22 -10
- package/.sinapse-ai/product/templates/activation-instructions-inline-greeting.yaml +17 -1
- package/.sinapse-ai/schemas/squad-schema.json +217 -113
- package/bin/sinapse-delegate.js +30 -0
- package/docs/guides/README.md +7 -7
- package/docs/guides/ade-guide.md +16 -16
- package/docs/guides/hooks-two-layers.md +66 -0
- package/docs/pt/README.md +3 -39
- package/docs/pt/community.md +13 -13
- package/docs/pt/guides/ade-guide.md +16 -16
- package/docs/sinapse-agent-flows/README.md +3 -4
- package/docs/sinapse-agent-flows/analyst-system.md +12 -12
- package/docs/sinapse-agent-flows/architect-system.md +16 -16
- package/docs/sinapse-agent-flows/data-engineer-system.md +10 -10
- package/docs/sinapse-agent-flows/dev-system.md +11 -11
- package/docs/sinapse-agent-flows/devops-system.md +5 -5
- package/docs/sinapse-agent-flows/pm-system.md +9 -9
- package/docs/sinapse-agent-flows/qa-system.md +10 -10
- package/docs/sinapse-agent-flows/sm-system.md +10 -10
- package/docs/sinapse-agent-flows/snps-orqx-system.md +9 -9
- package/docs/sinapse-agent-flows/squad-creator-system.md +7 -7
- package/package.json +5 -2
- package/scripts/regenerate-orqx-stubs.ps1 +96 -0
- package/scripts/validate-squad-yaml.js +166 -0
- package/sinapse/agents/sinapse-orqx.md +791 -0
- package/sinapse/agents/snps-orqx.md +1 -1
- package/squads/claude-code-mastery/_deprecated/README.md +42 -0
- package/squads/claude-code-mastery/agents/swarm-orqx.md +3 -1
- package/squads/claude-code-mastery/squad.yaml +46 -49
- package/squads/squad-cybersecurity/squad.yaml +1 -0
- package/squads/squad-finance/agents/cost-optimizer.md +164 -0
- package/squads/squad-finance/agents/fiscal-compliance-br.md +224 -0
- package/squads/squad-finance/agents/forecast-strategist.md +234 -0
- package/squads/squad-finance/squad.yaml +140 -21
- package/squads/squad-storytelling/squad.yaml +1 -0
- /package/squads/claude-code-mastery/{agents → _deprecated}/claude-orqx.md +0 -0
- /package/squads/claude-code-mastery/{agents → _deprecated}/db-sage.md +0 -0
- /package/squads/claude-code-mastery/{agents → _deprecated}/tools-orqx.md +0 -0
|
@@ -40,7 +40,7 @@ Then display:
|
|
|
40
40
|
*help — Show all commands and squad overview
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
After the greeting,
|
|
43
|
+
After the greeting, check if the user already provided briefing/context with the activation. If YES → proceed IMMEDIATELY to the NON-NEGOTIABLE ORCHESTRATION PLAN flow below (Imperator's core function). If NO (bare activation only) → await briefing. On receipt, plan automatically. NEVER ask "do you want me to plan?" — the answer is always YES for Imperator.
|
|
44
44
|
|
|
45
45
|
If the user asks about SINAPSE, how it works, or how to use it, execute the `*onboard` task from `tasks/onboard-user.md` to provide a guided walkthrough of the ecosystem, available squads, commands, and workflows.
|
|
46
46
|
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawn, spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_RUN_DIR = '.sinapse/external-runs';
|
|
6
|
+
const SUPPORTED_SANDBOXES = new Set([
|
|
7
|
+
'read-only',
|
|
8
|
+
'workspace-write',
|
|
9
|
+
'full-auto',
|
|
10
|
+
'danger-full-access',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const PROVIDERS = {
|
|
14
|
+
codex: {
|
|
15
|
+
id: 'codex',
|
|
16
|
+
binary: 'codex',
|
|
17
|
+
buildArgs(options) {
|
|
18
|
+
const args = [];
|
|
19
|
+
|
|
20
|
+
if (options.sandbox === 'danger-full-access') {
|
|
21
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
22
|
+
} else {
|
|
23
|
+
const codexSandbox = options.sandbox === 'full-auto' ? 'workspace-write' : options.sandbox;
|
|
24
|
+
args.push('-a', 'never', '-s', codexSandbox);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (options.model) {
|
|
28
|
+
args.push('-m', options.model);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.profile) {
|
|
32
|
+
args.push('-p', options.profile);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
args.push('exec', '-C', options.workdir, '-o', options.outputPath);
|
|
36
|
+
|
|
37
|
+
for (const image of options.images) {
|
|
38
|
+
args.push('-i', image);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
args.push('-');
|
|
42
|
+
|
|
43
|
+
return args;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
class DelegateCliError extends Error {
|
|
49
|
+
constructor(message, exitCode = 1, cause = null) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'DelegateCliError';
|
|
52
|
+
this.exitCode = exitCode;
|
|
53
|
+
this.cause = cause;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function showHelp(output = process.stdout) {
|
|
58
|
+
output.write(`SINAPSE External Executor Delegation
|
|
59
|
+
|
|
60
|
+
USAGE:
|
|
61
|
+
sinapse-delegate <provider> -t <slug> [-f prompt_file | -p prompt] [options]
|
|
62
|
+
|
|
63
|
+
PROVIDERS:
|
|
64
|
+
codex
|
|
65
|
+
|
|
66
|
+
OPTIONS:
|
|
67
|
+
-t, --task <slug> Stable task/story slug for the run directory
|
|
68
|
+
-f, --prompt-file <path> Prompt file to send to the external executor
|
|
69
|
+
-p, --prompt <text> Inline prompt to send to the external executor
|
|
70
|
+
-d, --workdir <path> Working directory for the executor (default: cwd)
|
|
71
|
+
-m, --model <model> Provider model override
|
|
72
|
+
--profile <name> Provider profile/config override
|
|
73
|
+
--sandbox <mode> read-only | workspace-write | full-auto | danger-full-access
|
|
74
|
+
--run-dir <path> Base run directory (default: .sinapse/external-runs)
|
|
75
|
+
--image <path> Image input for providers that support it (repeatable)
|
|
76
|
+
--allow-dirty Allow delegation with a dirty git worktree
|
|
77
|
+
--skip-git-check Skip git worktree detection and cleanliness check
|
|
78
|
+
--foreground Wait for the executor process and return its exit code
|
|
79
|
+
--dry-run Print the planned run without creating files or spawning
|
|
80
|
+
-h, --help Show this help
|
|
81
|
+
|
|
82
|
+
OUTPUT:
|
|
83
|
+
Prints machine-readable key=value lines: STATUS, RUN_DIR, PID, LOG, OUTPUT, PROMPT, COMMAND.
|
|
84
|
+
`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseArgs(argv) {
|
|
88
|
+
const options = {
|
|
89
|
+
provider: null,
|
|
90
|
+
slug: null,
|
|
91
|
+
prompt: null,
|
|
92
|
+
promptFile: null,
|
|
93
|
+
workdir: process.cwd(),
|
|
94
|
+
model: null,
|
|
95
|
+
profile: null,
|
|
96
|
+
sandbox: 'workspace-write',
|
|
97
|
+
runDirBase: DEFAULT_RUN_DIR,
|
|
98
|
+
images: [],
|
|
99
|
+
allowDirty: false,
|
|
100
|
+
skipGitCheck: false,
|
|
101
|
+
foreground: false,
|
|
102
|
+
dryRun: false,
|
|
103
|
+
help: false,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
107
|
+
const arg = argv[i];
|
|
108
|
+
|
|
109
|
+
if (arg === '--help' || arg === '-h') {
|
|
110
|
+
options.help = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!arg.startsWith('-') && !options.provider) {
|
|
115
|
+
options.provider = arg;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const readValue = (name) => {
|
|
120
|
+
const value = argv[i + 1];
|
|
121
|
+
if (!value || value.startsWith('-')) {
|
|
122
|
+
throw new DelegateCliError(`${name} requires a value`, 2);
|
|
123
|
+
}
|
|
124
|
+
i += 1;
|
|
125
|
+
return value;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
switch (arg) {
|
|
129
|
+
case '-t':
|
|
130
|
+
case '--task':
|
|
131
|
+
options.slug = readValue(arg);
|
|
132
|
+
break;
|
|
133
|
+
case '-f':
|
|
134
|
+
case '--prompt-file':
|
|
135
|
+
options.promptFile = readValue(arg);
|
|
136
|
+
break;
|
|
137
|
+
case '-p':
|
|
138
|
+
case '--prompt':
|
|
139
|
+
options.prompt = readValue(arg);
|
|
140
|
+
break;
|
|
141
|
+
case '-d':
|
|
142
|
+
case '--workdir':
|
|
143
|
+
options.workdir = readValue(arg);
|
|
144
|
+
break;
|
|
145
|
+
case '-m':
|
|
146
|
+
case '--model':
|
|
147
|
+
options.model = readValue(arg);
|
|
148
|
+
break;
|
|
149
|
+
case '--profile':
|
|
150
|
+
options.profile = readValue(arg);
|
|
151
|
+
break;
|
|
152
|
+
case '--sandbox':
|
|
153
|
+
options.sandbox = readValue(arg);
|
|
154
|
+
break;
|
|
155
|
+
case '--run-dir':
|
|
156
|
+
options.runDirBase = readValue(arg);
|
|
157
|
+
break;
|
|
158
|
+
case '--image':
|
|
159
|
+
options.images.push(readValue(arg));
|
|
160
|
+
break;
|
|
161
|
+
case '--allow-dirty':
|
|
162
|
+
options.allowDirty = true;
|
|
163
|
+
break;
|
|
164
|
+
case '--skip-git-check':
|
|
165
|
+
options.skipGitCheck = true;
|
|
166
|
+
break;
|
|
167
|
+
case '--foreground':
|
|
168
|
+
options.foreground = true;
|
|
169
|
+
break;
|
|
170
|
+
case '--dry-run':
|
|
171
|
+
options.dryRun = true;
|
|
172
|
+
break;
|
|
173
|
+
default:
|
|
174
|
+
throw new DelegateCliError(`Unknown option or extra argument: ${arg}`, 2);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return options;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function validateOptions(options) {
|
|
182
|
+
if (options.help) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!options.provider) {
|
|
187
|
+
throw new DelegateCliError('Provider is required. Use: sinapse-delegate <provider> ...', 2);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!PROVIDERS[options.provider]) {
|
|
191
|
+
throw new DelegateCliError(`Unsupported provider: ${options.provider}`, 2);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!options.slug) {
|
|
195
|
+
throw new DelegateCliError('Task slug is required. Use -t <slug>.', 2);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (options.prompt && options.promptFile) {
|
|
199
|
+
throw new DelegateCliError('Use either --prompt or --prompt-file, not both.', 2);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (!options.prompt && !options.promptFile) {
|
|
203
|
+
throw new DelegateCliError('Prompt is required. Use --prompt or --prompt-file.', 2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!SUPPORTED_SANDBOXES.has(options.sandbox)) {
|
|
207
|
+
throw new DelegateCliError(
|
|
208
|
+
`Unsupported sandbox: ${options.sandbox}. Expected one of: ${Array.from(SUPPORTED_SANDBOXES).join(', ')}`,
|
|
209
|
+
2,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function sanitizeSlug(slug) {
|
|
215
|
+
const sanitized = String(slug)
|
|
216
|
+
.trim()
|
|
217
|
+
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
218
|
+
.replace(/^-+|-+$/g, '')
|
|
219
|
+
.slice(0, 80);
|
|
220
|
+
|
|
221
|
+
if (!sanitized) {
|
|
222
|
+
throw new DelegateCliError('Task slug must contain at least one safe character.', 2);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return sanitized;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function formatTimestamp(date = new Date()) {
|
|
229
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
230
|
+
return [
|
|
231
|
+
date.getUTCFullYear(),
|
|
232
|
+
pad(date.getUTCMonth() + 1),
|
|
233
|
+
pad(date.getUTCDate()),
|
|
234
|
+
'-',
|
|
235
|
+
pad(date.getUTCHours()),
|
|
236
|
+
pad(date.getUTCMinutes()),
|
|
237
|
+
pad(date.getUTCSeconds()),
|
|
238
|
+
].join('');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function resolvePathFrom(baseDir, value) {
|
|
242
|
+
return path.resolve(baseDir, value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function loadPrompt(options) {
|
|
246
|
+
if (options.promptFile) {
|
|
247
|
+
const promptFile = path.resolve(options.promptFile);
|
|
248
|
+
let text;
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
text = fs.readFileSync(promptFile, 'utf8');
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error.code === 'ENOENT') {
|
|
254
|
+
throw new DelegateCliError(
|
|
255
|
+
`Prompt file not found: ${promptFile}. Use --prompt for inline text or --prompt-file <path> for an existing file.`,
|
|
256
|
+
2,
|
|
257
|
+
error,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw new DelegateCliError(
|
|
262
|
+
`Could not read prompt file ${promptFile}: ${error.message}`,
|
|
263
|
+
2,
|
|
264
|
+
error,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
text,
|
|
270
|
+
source: promptFile,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
text: options.prompt,
|
|
276
|
+
source: 'inline',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function shellQuote(value) {
|
|
281
|
+
const text = String(value);
|
|
282
|
+
if (/^[A-Za-z0-9_./:=@%+-]+$/.test(text)) {
|
|
283
|
+
return text;
|
|
284
|
+
}
|
|
285
|
+
const singleQuote = String.fromCharCode(39);
|
|
286
|
+
const escapedQuote = singleQuote + '\\' + singleQuote + singleQuote;
|
|
287
|
+
return singleQuote + text.replace(/'/g, escapedQuote) + singleQuote;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function formatCommand(command, args) {
|
|
291
|
+
return [command, ...args].map(shellQuote).join(' ');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function commandExists(command) {
|
|
295
|
+
if (process.platform === 'win32') {
|
|
296
|
+
return spawnSync('where', [command], { stdio: 'ignore' }).status === 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return spawnSync('sh', ['-c', `command -v ${shellQuote(command)} >/dev/null 2>&1`], {
|
|
300
|
+
stdio: 'ignore',
|
|
301
|
+
}).status === 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function assertExecutorAvailable(provider) {
|
|
305
|
+
if (!commandExists(provider.binary)) {
|
|
306
|
+
throw new DelegateCliError(
|
|
307
|
+
`Executor binary not found on PATH: ${provider.binary}`,
|
|
308
|
+
3,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function gitStatus(workdir) {
|
|
314
|
+
const inside = spawnSync('git', ['-C', workdir, 'rev-parse', '--is-inside-work-tree'], {
|
|
315
|
+
encoding: 'utf8',
|
|
316
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (inside.status !== 0 || inside.stdout.trim() !== 'true') {
|
|
320
|
+
return { inside: false, dirty: false, status: '' };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const status = spawnSync('git', ['-C', workdir, 'status', '--porcelain'], {
|
|
324
|
+
encoding: 'utf8',
|
|
325
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (status.status !== 0) {
|
|
329
|
+
throw new DelegateCliError(`Could not read git status: ${status.stderr.trim()}`, 4);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const output = status.stdout.trim();
|
|
333
|
+
return { inside: true, dirty: output.length > 0, status: output };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function assertGitReady(options) {
|
|
337
|
+
if (options.skipGitCheck) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const status = gitStatus(options.workdir);
|
|
342
|
+
if (!status.inside) {
|
|
343
|
+
throw new DelegateCliError(
|
|
344
|
+
'Workdir is not inside a git repository. Use --skip-git-check only for intentional non-git runs.',
|
|
345
|
+
4,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (status.dirty && !options.allowDirty) {
|
|
350
|
+
throw new DelegateCliError(
|
|
351
|
+
'Git worktree is dirty. Commit/stash first or pass --allow-dirty for intentional changes.',
|
|
352
|
+
4,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function createDelegatePlan(rawOptions, date = new Date()) {
|
|
358
|
+
const options = {
|
|
359
|
+
...rawOptions,
|
|
360
|
+
workdir: path.resolve(rawOptions.workdir || process.cwd()),
|
|
361
|
+
images: rawOptions.images || [],
|
|
362
|
+
};
|
|
363
|
+
validateOptions(options);
|
|
364
|
+
|
|
365
|
+
const provider = PROVIDERS[options.provider];
|
|
366
|
+
const slug = sanitizeSlug(options.slug);
|
|
367
|
+
const timestamp = formatTimestamp(date);
|
|
368
|
+
const runDirBase = path.isAbsolute(options.runDirBase)
|
|
369
|
+
? options.runDirBase
|
|
370
|
+
: resolvePathFrom(options.workdir, options.runDirBase);
|
|
371
|
+
const runDir = path.join(runDirBase, `${timestamp}-${slug}`);
|
|
372
|
+
const promptPath = path.join(runDir, 'prompt.md');
|
|
373
|
+
const outputPath = path.join(runDir, 'output.md');
|
|
374
|
+
const logPath = path.join(runDir, `${provider.id}.log`);
|
|
375
|
+
const metadataPath = path.join(runDir, 'metadata.json');
|
|
376
|
+
const commandPath = path.join(runDir, 'command.txt');
|
|
377
|
+
const prompt = loadPrompt(options);
|
|
378
|
+
const providerOptions = {
|
|
379
|
+
...options,
|
|
380
|
+
outputPath,
|
|
381
|
+
images: options.images.map((image) => path.resolve(options.workdir, image)),
|
|
382
|
+
};
|
|
383
|
+
const args = provider.buildArgs(providerOptions);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
provider,
|
|
387
|
+
options,
|
|
388
|
+
slug,
|
|
389
|
+
timestamp,
|
|
390
|
+
runDir,
|
|
391
|
+
promptPath,
|
|
392
|
+
outputPath,
|
|
393
|
+
logPath,
|
|
394
|
+
metadataPath,
|
|
395
|
+
commandPath,
|
|
396
|
+
prompt,
|
|
397
|
+
command: provider.binary,
|
|
398
|
+
args,
|
|
399
|
+
displayCommand: formatCommand(provider.binary, args),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function writeRunFiles(plan, pid = null) {
|
|
404
|
+
fs.mkdirSync(plan.runDir, { recursive: true });
|
|
405
|
+
fs.writeFileSync(plan.promptPath, plan.prompt.text, 'utf8');
|
|
406
|
+
fs.writeFileSync(plan.commandPath, `${plan.displayCommand}\n`, 'utf8');
|
|
407
|
+
|
|
408
|
+
const metadata = {
|
|
409
|
+
provider: plan.provider.id,
|
|
410
|
+
slug: plan.slug,
|
|
411
|
+
created_at: new Date().toISOString(),
|
|
412
|
+
workdir: plan.options.workdir,
|
|
413
|
+
sandbox: plan.options.sandbox,
|
|
414
|
+
model: plan.options.model || null,
|
|
415
|
+
profile: plan.options.profile || null,
|
|
416
|
+
foreground: plan.options.foreground,
|
|
417
|
+
allow_dirty: plan.options.allowDirty,
|
|
418
|
+
skip_git_check: plan.options.skipGitCheck,
|
|
419
|
+
prompt_source: plan.prompt.source,
|
|
420
|
+
run_dir: plan.runDir,
|
|
421
|
+
prompt: plan.promptPath,
|
|
422
|
+
output: plan.outputPath,
|
|
423
|
+
log: plan.logPath,
|
|
424
|
+
command: plan.command,
|
|
425
|
+
args: plan.args,
|
|
426
|
+
pid,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
fs.writeFileSync(plan.metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function spawnExecutor(plan) {
|
|
433
|
+
fs.mkdirSync(path.dirname(plan.logPath), { recursive: true });
|
|
434
|
+
const logFd = fs.openSync(plan.logPath, 'a');
|
|
435
|
+
let logClosed = false;
|
|
436
|
+
let child;
|
|
437
|
+
|
|
438
|
+
const closeLog = () => {
|
|
439
|
+
if (!logClosed) {
|
|
440
|
+
fs.closeSync(logFd);
|
|
441
|
+
logClosed = true;
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const recordSpawnError = (error) => {
|
|
446
|
+
if (!logClosed) {
|
|
447
|
+
fs.writeSync(logFd, `\n[sinapse-delegate] executor error: ${error.message}\n`);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
try {
|
|
452
|
+
child = spawn(plan.command, plan.args, {
|
|
453
|
+
cwd: plan.options.workdir,
|
|
454
|
+
detached: !plan.options.foreground,
|
|
455
|
+
stdio: ['pipe', logFd, logFd],
|
|
456
|
+
env: process.env,
|
|
457
|
+
});
|
|
458
|
+
} catch (error) {
|
|
459
|
+
recordSpawnError(error);
|
|
460
|
+
writeRunFiles(plan, null);
|
|
461
|
+
closeLog();
|
|
462
|
+
return { status: 'failed', pid: null, exitCode: 1, error: error.message };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
writeRunFiles(plan, child.pid || null);
|
|
466
|
+
|
|
467
|
+
const errorResult = new Promise((resolve) => {
|
|
468
|
+
child.once('error', (error) => {
|
|
469
|
+
recordSpawnError(error);
|
|
470
|
+
closeLog();
|
|
471
|
+
resolve({
|
|
472
|
+
status: 'failed',
|
|
473
|
+
pid: child.pid || null,
|
|
474
|
+
exitCode: 1,
|
|
475
|
+
error: error.message,
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (child.stdin) {
|
|
481
|
+
child.stdin.end(plan.prompt.text);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!plan.options.foreground) {
|
|
485
|
+
const immediateError = await Promise.race([
|
|
486
|
+
errorResult,
|
|
487
|
+
new Promise((resolve) => setImmediate(() => resolve(null))),
|
|
488
|
+
]);
|
|
489
|
+
|
|
490
|
+
if (immediateError) {
|
|
491
|
+
return immediateError;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
child.unref();
|
|
495
|
+
closeLog();
|
|
496
|
+
return { status: 'started', pid: child.pid, exitCode: 0 };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const result = await Promise.race([
|
|
500
|
+
errorResult,
|
|
501
|
+
new Promise((resolve) => {
|
|
502
|
+
child.once('close', (code) => {
|
|
503
|
+
closeLog();
|
|
504
|
+
const exitCode = code !== null ? code : 1;
|
|
505
|
+
resolve({
|
|
506
|
+
status: exitCode === 0 ? 'completed' : 'failed',
|
|
507
|
+
pid: child.pid,
|
|
508
|
+
exitCode,
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
}),
|
|
512
|
+
]);
|
|
513
|
+
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function sanitizeResultValue(value) {
|
|
518
|
+
return String(value).replace(/\r?\n/g, ' ');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function printResult(plan, result, output = process.stdout) {
|
|
522
|
+
const lines = [
|
|
523
|
+
`STATUS=${result.status}`,
|
|
524
|
+
`RUN_DIR=${plan.runDir}`,
|
|
525
|
+
result.pid ? `PID=${result.pid}` : null,
|
|
526
|
+
`LOG=${plan.logPath}`,
|
|
527
|
+
`OUTPUT=${plan.outputPath}`,
|
|
528
|
+
`PROMPT=${plan.promptPath}`,
|
|
529
|
+
`COMMAND=${plan.displayCommand}`,
|
|
530
|
+
result.error ? `ERROR=${sanitizeResultValue(result.error)}` : null,
|
|
531
|
+
].filter(Boolean);
|
|
532
|
+
|
|
533
|
+
output.write(`${lines.join('\n')}\n`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async function runCli(argv, output = process.stdout, _errorOutput = process.stderr) {
|
|
537
|
+
const options = parseArgs(argv);
|
|
538
|
+
|
|
539
|
+
if (options.help) {
|
|
540
|
+
showHelp(output);
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const plan = createDelegatePlan(options);
|
|
545
|
+
|
|
546
|
+
if (options.dryRun) {
|
|
547
|
+
printResult(plan, { status: 'dry-run', pid: null }, output);
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
assertExecutorAvailable(plan.provider);
|
|
552
|
+
assertGitReady(plan.options);
|
|
553
|
+
|
|
554
|
+
const result = await spawnExecutor(plan);
|
|
555
|
+
printResult(plan, result, output);
|
|
556
|
+
return result.exitCode;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function main() {
|
|
560
|
+
try {
|
|
561
|
+
const exitCode = await runCli(process.argv.slice(2));
|
|
562
|
+
process.exit(exitCode);
|
|
563
|
+
} catch (error) {
|
|
564
|
+
const exitCode = error.exitCode || 1;
|
|
565
|
+
process.stderr.write(`ERROR=${error.message}\n`);
|
|
566
|
+
process.exit(exitCode);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
module.exports = {
|
|
571
|
+
DEFAULT_RUN_DIR,
|
|
572
|
+
PROVIDERS,
|
|
573
|
+
SUPPORTED_SANDBOXES,
|
|
574
|
+
DelegateCliError,
|
|
575
|
+
parseArgs,
|
|
576
|
+
validateOptions,
|
|
577
|
+
sanitizeSlug,
|
|
578
|
+
formatTimestamp,
|
|
579
|
+
createDelegatePlan,
|
|
580
|
+
formatCommand,
|
|
581
|
+
gitStatus,
|
|
582
|
+
runCli,
|
|
583
|
+
showHelp,
|
|
584
|
+
main,
|
|
585
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const delegateCli = require('./delegate-cli');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
DEFAULT_RUN_DIR: delegateCli.DEFAULT_RUN_DIR,
|
|
5
|
+
PROVIDERS: delegateCli.PROVIDERS,
|
|
6
|
+
SUPPORTED_SANDBOXES: delegateCli.SUPPORTED_SANDBOXES,
|
|
7
|
+
DelegateCliError: delegateCli.DelegateCliError,
|
|
8
|
+
sanitizeSlug: delegateCli.sanitizeSlug,
|
|
9
|
+
formatTimestamp: delegateCli.formatTimestamp,
|
|
10
|
+
createDelegatePlan: delegateCli.createDelegatePlan,
|
|
11
|
+
formatCommand: delegateCli.formatCommand,
|
|
12
|
+
gitStatus: delegateCli.gitStatus,
|
|
13
|
+
};
|