@the-open-engine/zeroshot 6.2.0 ā 6.4.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/README.md +141 -474
- package/cli/event-copy.js +12 -0
- package/cli/index.js +314 -96
- package/cli/message-formatters-normal.js +14 -2
- package/cli/message-formatters-watch.js +9 -2
- package/cluster-hooks/block-ask-user-question.py +53 -0
- package/cluster-hooks/block-dangerous-git.py +145 -0
- package/lib/clusters-registry.js +48 -0
- package/lib/compose-utils.js +101 -0
- package/lib/detached-startup.js +12 -1
- package/lib/id-detector.js +8 -9
- package/lib/path-check.js +63 -0
- package/lib/run-mode.js +34 -0
- package/lib/run-plan.js +32 -0
- package/lib/start-cluster.js +58 -28
- package/package.json +7 -6
- package/scripts/check-path.js +19 -0
- package/scripts/validate-templates.js +11 -29
- package/src/agent/agent-hook-executor.js +1 -1
- package/src/agent/agent-lifecycle.js +2 -0
- package/src/agent/agent-task-executor.js +4 -3
- package/src/agent/pr-verification.js +27 -1
- package/src/agents/git-pusher-template.js +121 -4
- package/src/attach/socket-discovery.js +2 -3
- package/src/claude-credentials.js +75 -0
- package/src/claude-task-runner.js +1 -0
- package/src/isolation-manager.js +12 -9
- package/src/issue-providers/README.md +45 -15
- package/src/issue-providers/index.js +2 -0
- package/src/issue-providers/jira-provider.js +8 -1
- package/src/issue-providers/linear-provider.js +405 -0
- package/src/ledger.js +45 -3
- package/src/lib/gc.js +2 -3
- package/src/message-bus.js +22 -2
- package/src/orchestrator.js +271 -144
- package/src/preflight.js +12 -16
- package/src/providers/base-provider.js +7 -6
- package/src/status-footer.js +13 -1
- package/src/template-validation/index.js +3 -0
- package/src/template-validation/report-formatter.js +55 -0
- package/src/worktree-claude-config.js +2 -13
- package/task-lib/runner.js +1 -0
- package/task-lib/watcher.js +1 -0
package/lib/start-cluster.js
CHANGED
|
@@ -3,7 +3,10 @@ const { spawnSync } = require('child_process');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { normalizeProviderName } = require('./provider-names');
|
|
5
5
|
const { getProvider } = require('../src/providers');
|
|
6
|
+
const { detectProvider } = require('../src/issue-providers');
|
|
6
7
|
const TemplateResolver = require('../src/template-resolver');
|
|
8
|
+
const { runModeFromPlan } = require('./run-mode');
|
|
9
|
+
const { resolveRunPlan } = require('./run-plan');
|
|
7
10
|
|
|
8
11
|
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
9
12
|
|
|
@@ -96,35 +99,44 @@ function buildFileInput(file) {
|
|
|
96
99
|
return { file };
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
function detectRunInput(inputArg) {
|
|
100
|
-
const isGitHubUrl = /^https?:\/\/github\.com\/[\w-]+\/[\w-]+\/issues\/\d+/.test(inputArg);
|
|
101
|
-
const isGitLabUrl = /gitlab\.(com|[\w.-]+)\/[\w-]+\/[\w-]+\/-\/issues\/\d+/.test(inputArg);
|
|
102
|
-
const isJiraUrl = /(atlassian\.net|jira\.[\w.-]+)\/browse\/[A-Z][A-Z0-9]+-\d+/.test(inputArg);
|
|
103
|
-
const isAzureUrl =
|
|
104
|
-
/dev\.azure\.com\/.*\/_workitems\/edit\/\d+/.test(inputArg) ||
|
|
105
|
-
/visualstudio\.com\/.*\/_workitems\/edit\/\d+/.test(inputArg);
|
|
106
|
-
const isJiraKey = /^[A-Z][A-Z0-9]+-\d+$/.test(inputArg);
|
|
107
|
-
const isIssueNumber = /^\d+$/.test(inputArg);
|
|
108
|
-
const isRepoIssue = /^[\w-]+\/[\w-]+#\d+$/.test(inputArg);
|
|
102
|
+
function detectRunInput(inputArg, settings = {}, forceProvider = null) {
|
|
109
103
|
const isMarkdownFile = /\.(md|markdown)$/i.test(inputArg);
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
isGitHubUrl ||
|
|
113
|
-
isGitLabUrl ||
|
|
114
|
-
isJiraUrl ||
|
|
115
|
-
isAzureUrl ||
|
|
116
|
-
isJiraKey ||
|
|
117
|
-
isIssueNumber ||
|
|
118
|
-
isRepoIssue
|
|
119
|
-
) {
|
|
120
|
-
return buildIssueInput(inputArg);
|
|
121
|
-
}
|
|
122
104
|
if (isMarkdownFile) {
|
|
123
105
|
return buildFileInput(inputArg);
|
|
124
106
|
}
|
|
107
|
+
|
|
108
|
+
const ProviderClass = detectProvider(inputArg, settings, forceProvider);
|
|
109
|
+
if (ProviderClass) {
|
|
110
|
+
return buildIssueInput(inputArg);
|
|
111
|
+
}
|
|
112
|
+
|
|
125
113
|
return buildTextInput(inputArg);
|
|
126
114
|
}
|
|
127
115
|
|
|
116
|
+
const STDIN_MARKER = '-';
|
|
117
|
+
|
|
118
|
+
function isStdinInput(inputArg) {
|
|
119
|
+
return inputArg === STDIN_MARKER;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function readStdinText(stream = process.stdin) {
|
|
123
|
+
const chunks = [];
|
|
124
|
+
for await (const chunk of stream) {
|
|
125
|
+
chunks.push(chunk);
|
|
126
|
+
}
|
|
127
|
+
return Buffer.concat(chunks.map((chunk) => (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))))
|
|
128
|
+
.toString('utf8')
|
|
129
|
+
.trim();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function encodeStdinEnv(text) {
|
|
133
|
+
return Buffer.from(text, 'utf8').toString('base64');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function decodeStdinEnv(value) {
|
|
137
|
+
return Buffer.from(value, 'base64').toString('utf8');
|
|
138
|
+
}
|
|
139
|
+
|
|
128
140
|
function resolveProviderOverride(options = {}) {
|
|
129
141
|
const override = options.provider || options.envProvider || process.env.ZEROSHOT_PROVIDER;
|
|
130
142
|
if (!override || (typeof override === 'string' && !override.trim())) {
|
|
@@ -187,8 +199,20 @@ function mergeRunOptions(options) {
|
|
|
187
199
|
return envRunOptions ? { ...envRunOptions, ...options } : options;
|
|
188
200
|
}
|
|
189
201
|
|
|
190
|
-
|
|
191
|
-
|
|
202
|
+
// The single producer of the run plan for a cluster start: fold env + settings
|
|
203
|
+
// into the flags, then resolve the canonical isolation/delivery/autoMerge plan.
|
|
204
|
+
// buildStartOptions reads EVERY mode field off this one plan ā no field
|
|
205
|
+
// (isolation, worktree, autoPr, autoMerge, runMode) is derived independently.
|
|
206
|
+
function resolveEffectiveRunPlan(mergedOptions, settings) {
|
|
207
|
+
return resolveRunPlan({
|
|
208
|
+
...mergedOptions,
|
|
209
|
+
docker: anyTruthy(
|
|
210
|
+
mergedOptions.docker,
|
|
211
|
+
process.env.ZEROSHOT_DOCKER === '1',
|
|
212
|
+
settings.defaultDocker
|
|
213
|
+
),
|
|
214
|
+
worktree: anyTruthy(mergedOptions.worktree, process.env.ZEROSHOT_WORKTREE === '1'),
|
|
215
|
+
});
|
|
192
216
|
}
|
|
193
217
|
|
|
194
218
|
function resolveMergeQueue(mergedOptions) {
|
|
@@ -227,14 +251,15 @@ function buildStartOptions({
|
|
|
227
251
|
forceProvider,
|
|
228
252
|
}) {
|
|
229
253
|
const mergedOptions = mergeRunOptions(options);
|
|
254
|
+
const plan = resolveEffectiveRunPlan(mergedOptions, settings);
|
|
230
255
|
return {
|
|
231
256
|
clusterId,
|
|
232
257
|
cwd: resolveTargetCwd(),
|
|
233
|
-
isolation:
|
|
258
|
+
isolation: plan.isolation === 'docker',
|
|
234
259
|
isolationImage: firstTruthy(mergedOptions.dockerImage, process.env.ZEROSHOT_DOCKER_IMAGE),
|
|
235
|
-
worktree:
|
|
236
|
-
autoPr:
|
|
237
|
-
autoMerge:
|
|
260
|
+
worktree: plan.isolation === 'worktree',
|
|
261
|
+
autoPr: plan.delivery !== 'none',
|
|
262
|
+
autoMerge: plan.autoMerge,
|
|
238
263
|
autoPush: process.env.ZEROSHOT_PUSH === '1',
|
|
239
264
|
modelOverride: optionalValue(modelOverride),
|
|
240
265
|
providerOverride: optionalValue(providerOverride),
|
|
@@ -246,6 +271,7 @@ function buildStartOptions({
|
|
|
246
271
|
mergeQueue: resolveMergeQueue(mergedOptions),
|
|
247
272
|
closeIssue: resolveCloseIssue(mergedOptions),
|
|
248
273
|
ship: mergedOptions.ship,
|
|
274
|
+
runMode: runModeFromPlan(plan),
|
|
249
275
|
requiredQualityGates: mergedOptions.requiredQualityGates,
|
|
250
276
|
settings,
|
|
251
277
|
};
|
|
@@ -310,6 +336,10 @@ module.exports = {
|
|
|
310
336
|
buildIssueInput,
|
|
311
337
|
buildFileInput,
|
|
312
338
|
detectRunInput,
|
|
339
|
+
isStdinInput,
|
|
340
|
+
readStdinText,
|
|
341
|
+
encodeStdinEnv,
|
|
342
|
+
decodeStdinEnv,
|
|
313
343
|
resolveProviderOverride,
|
|
314
344
|
resolveConfigPath,
|
|
315
345
|
loadClusterConfig,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-open-engine/zeroshot",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
|
|
5
5
|
"main": "src/orchestrator.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
"pretest": "npm run build:agent-cli-provider",
|
|
16
16
|
"test": "node tests/run-tests.js",
|
|
17
17
|
"test:unit": "node tests/run-tests.js",
|
|
18
|
-
"test:
|
|
19
|
-
"test:
|
|
18
|
+
"test:e2e": "mocha 'tests/e2e/**/*.test.js' --timeout 120000",
|
|
19
|
+
"test:e2e:docker": "bash tests/integration/e2e-isolation-and-auto.test.sh",
|
|
20
|
+
"test:slow": "mocha 'tests/integration/**/*.test.js' 'tests/providers/**/*.test.js' --timeout 180000",
|
|
21
|
+
"test:all": "npm run test && npm run test:e2e && npm run test:slow",
|
|
20
22
|
"test:coverage": "c8 npm run test:unit",
|
|
21
23
|
"test:coverage:report": "c8 --reporter=html npm run test:unit && echo 'Coverage report generated at coverage/index.html'",
|
|
22
|
-
"postinstall": "node scripts/fix-node-pty-permissions.js",
|
|
24
|
+
"postinstall": "node scripts/fix-node-pty-permissions.js && node scripts/check-path.js",
|
|
23
25
|
"start": "node cli/index.js",
|
|
24
26
|
"typecheck": "tsc --noEmit",
|
|
25
27
|
"typecheck:agent-cli-provider": "tsc --project tsconfig.agent-cli-provider.json",
|
|
@@ -94,7 +96,7 @@
|
|
|
94
96
|
"cli/",
|
|
95
97
|
"task-lib/",
|
|
96
98
|
"cluster-templates/",
|
|
97
|
-
"hooks/",
|
|
99
|
+
"cluster-hooks/",
|
|
98
100
|
"docker/",
|
|
99
101
|
"scripts/",
|
|
100
102
|
"README.md",
|
|
@@ -110,7 +112,6 @@
|
|
|
110
112
|
"node-pty": "^1.1.0",
|
|
111
113
|
"omelette": "^0.4.17",
|
|
112
114
|
"pidusage": "^4.0.1",
|
|
113
|
-
"puppeteer": "^24.34.0",
|
|
114
115
|
"proper-lockfile": "^4.1.2"
|
|
115
116
|
},
|
|
116
117
|
"devDependencies": {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Postinstall check: warn if the npm global bin dir isn't on PATH.
|
|
4
|
+
*
|
|
5
|
+
* Runs after `npm install -g` regardless of whether the user can invoke
|
|
6
|
+
* `zeroshot` afterward (this is the only guaranteed-execution point in
|
|
7
|
+
* that failure scenario).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { checkBinDirOnPath, printPathWarning } = require('../lib/path-check');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const { onPath, binDir } = checkBinDirOnPath();
|
|
14
|
+
if (!onPath && binDir) {
|
|
15
|
+
printPathWarning(binDir);
|
|
16
|
+
}
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.warn(`[postinstall] Warning: PATH check failed: ${err.message}`);
|
|
19
|
+
}
|
|
@@ -8,11 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { validateTemplates } = require('../src/template-validation');
|
|
11
|
+
const { validateTemplates, formatValidationReport } = require('../src/template-validation');
|
|
12
12
|
|
|
13
13
|
const TEMPLATES_DIR = path.join(__dirname, '../cluster-templates');
|
|
14
14
|
|
|
15
15
|
function parseArgs(argv) {
|
|
16
|
+
const changedArg = argv.find((arg) => arg.startsWith('--changed='));
|
|
17
|
+
const changedFiles = changedArg
|
|
18
|
+
? new Set(changedArg.split('=').slice(1).join('=').split(',').filter(Boolean))
|
|
19
|
+
: null;
|
|
16
20
|
const simModeArg = argv.find((arg) => arg.startsWith('--sim='));
|
|
17
21
|
const simMode = simModeArg ? simModeArg.split('=')[1] : process.env.ZEROSHOT_TEMPLATE_SIM;
|
|
18
22
|
const randomScopeArg = argv.find((arg) => arg.startsWith('--random-scope='));
|
|
@@ -49,13 +53,16 @@ function parseArgs(argv) {
|
|
|
49
53
|
maxSteps: Number.isFinite(sampleSteps) && sampleSteps > 0 ? sampleSteps : undefined,
|
|
50
54
|
maxScenarioMs: Number.isFinite(sampleMs) && sampleMs > 0 ? sampleMs : undefined,
|
|
51
55
|
},
|
|
56
|
+
changedFiles,
|
|
52
57
|
};
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
async function main() {
|
|
56
61
|
console.log('Validating cluster templates...\n');
|
|
57
62
|
|
|
58
|
-
const { deep, randomSampling, randomScope, randomOptions } = parseArgs(
|
|
63
|
+
const { deep, randomSampling, randomScope, randomOptions, changedFiles } = parseArgs(
|
|
64
|
+
process.argv.slice(2)
|
|
65
|
+
);
|
|
59
66
|
const report = await validateTemplates({
|
|
60
67
|
templatesDir: TEMPLATES_DIR,
|
|
61
68
|
deep,
|
|
@@ -64,33 +71,8 @@ async function main() {
|
|
|
64
71
|
randomOptions,
|
|
65
72
|
});
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for (const { filePath, result } of report.results) {
|
|
70
|
-
const relativePath = path.relative(process.cwd(), filePath);
|
|
71
|
-
|
|
72
|
-
if (!result.valid) {
|
|
73
|
-
hasErrors = true;
|
|
74
|
-
console.error(`\nā ${relativePath}`);
|
|
75
|
-
for (const error of result.errors) {
|
|
76
|
-
console.error(` ERROR: ${error}`);
|
|
77
|
-
}
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (result.warnings.length > 0) {
|
|
82
|
-
console.warn(`\nā ļø ${relativePath}`);
|
|
83
|
-
for (const warning of result.warnings) {
|
|
84
|
-
console.warn(` WARN: ${warning}`);
|
|
85
|
-
}
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
console.log(`ā ${relativePath}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
console.log(`\n${'='.repeat(60)}`);
|
|
93
|
-
console.log(`Validated: ${report.validated} templates, Skipped: ${report.skipped} files`);
|
|
74
|
+
const { lines, hasErrors } = formatValidationReport(report, { changedFiles });
|
|
75
|
+
console.log(lines.join('\n'));
|
|
94
76
|
|
|
95
77
|
if (hasErrors) {
|
|
96
78
|
console.error('\nā VALIDATION FAILED - Fix errors above before merging\n');
|
|
@@ -118,7 +118,7 @@ function buildClaudeEnv(modelSpec, options = {}) {
|
|
|
118
118
|
env.ANTHROPIC_MODEL = modelSpec.model;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
// Activate AskUserQuestion blocking hook (see hooks/block-ask-user-question.py)
|
|
121
|
+
// Activate AskUserQuestion blocking hook (see cluster-hooks/block-ask-user-question.py)
|
|
122
122
|
env.ZEROSHOT_BLOCK_ASK_USER = '1';
|
|
123
123
|
|
|
124
124
|
return env;
|
|
@@ -446,7 +446,7 @@ function ensureAskUserQuestionHook(targetClaudeDir = null) {
|
|
|
446
446
|
}
|
|
447
447
|
|
|
448
448
|
// Copy hook script if not present or outdated
|
|
449
|
-
const hookScriptSrc = path.join(__dirname, '..', '..', 'hooks', hookScriptName);
|
|
449
|
+
const hookScriptSrc = path.join(__dirname, '..', '..', 'cluster-hooks', hookScriptName);
|
|
450
450
|
if (fs.existsSync(hookScriptSrc)) {
|
|
451
451
|
// Always copy to ensure latest version
|
|
452
452
|
fs.copyFileSync(hookScriptSrc, hookScriptDst);
|
|
@@ -524,7 +524,7 @@ function ensureDangerousGitHook(targetClaudeDir = null) {
|
|
|
524
524
|
}
|
|
525
525
|
|
|
526
526
|
// Copy hook script if not present or outdated
|
|
527
|
-
const hookScriptSrc = path.join(__dirname, '..', '..', 'hooks', hookScriptName);
|
|
527
|
+
const hookScriptSrc = path.join(__dirname, '..', '..', 'cluster-hooks', hookScriptName);
|
|
528
528
|
if (fs.existsSync(hookScriptSrc)) {
|
|
529
529
|
// Always copy to ensure latest version
|
|
530
530
|
fs.copyFileSync(hookScriptSrc, hookScriptDst);
|
|
@@ -834,6 +834,7 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv }) {
|
|
|
834
834
|
cwd,
|
|
835
835
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
836
836
|
env: spawnEnv,
|
|
837
|
+
windowsHide: true,
|
|
837
838
|
});
|
|
838
839
|
|
|
839
840
|
// NOTE: Don't emit PROCESS_SPAWNED here - proc.pid is a wrapper that exits immediately.
|
|
@@ -579,7 +579,9 @@ function shouldSkipVerification(adapter) {
|
|
|
579
579
|
return skipVars.some((name) => process.env[name] === '1');
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
-
async function verifyPullRequest({ result, agent }) {
|
|
582
|
+
async function verifyPullRequest({ result, agent, autoMerge }) {
|
|
583
|
+
// Default to true (merge required) so existing callers/tests and --ship semantics stay fail-closed.
|
|
584
|
+
const requireMerge = autoMerge !== false;
|
|
583
585
|
const platform = resolveVerificationPlatform(agent);
|
|
584
586
|
const adapter = getVerificationAdapter(platform);
|
|
585
587
|
const providerName =
|
|
@@ -622,6 +624,30 @@ async function verifyPullRequest({ result, agent }) {
|
|
|
622
624
|
);
|
|
623
625
|
}
|
|
624
626
|
|
|
627
|
+
if (!requireMerge) {
|
|
628
|
+
// --pr mode: the PR/MR was created and verified to exist. It's left OPEN for human
|
|
629
|
+
// review - do NOT poll for merge or treat an unmerged PR as a failure.
|
|
630
|
+
if (adapter.isMerged(prData)) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
`VERIFICATION FAILED: ${adapter.itemName} #${prData.number} is already merged, ` +
|
|
633
|
+
'but this run was started in review mode (autoMerge=false).'
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
agent._log(
|
|
638
|
+
`ā
VERIFICATION PASSED: ${adapter.itemName} #${prData.number} created (open for human review)`
|
|
639
|
+
);
|
|
640
|
+
publishClusterComplete(agent, {
|
|
641
|
+
...buildVerificationPayload({
|
|
642
|
+
platform,
|
|
643
|
+
prData,
|
|
644
|
+
reason: 'git-pusher-complete-verified',
|
|
645
|
+
}),
|
|
646
|
+
merged: adapter.isMerged(prData),
|
|
647
|
+
});
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
625
651
|
if (!adapter.isMerged(prData)) {
|
|
626
652
|
prData = await pollForMerge({ adapter, prData, agent, cwd: agent.workingDirectory });
|
|
627
653
|
}
|
|
@@ -341,7 +341,13 @@ function resolveGitHubConfig(options = {}) {
|
|
|
341
341
|
(parseBool(repoGithub.closeIssue) === true ? 'always' : null) ||
|
|
342
342
|
'never';
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
// --ship (or explicit autoMerge) merges automatically; --pr alone stops after PR creation
|
|
345
|
+
// for human review. Repo settings can opt in to auto-merge when the caller hasn't decided.
|
|
346
|
+
const autoMerge =
|
|
347
|
+
options.autoMerge === true ||
|
|
348
|
+
(options.autoMerge !== false && parseBool(repoGithub.autoMerge) === true);
|
|
349
|
+
|
|
350
|
+
return { prBase, useMergeQueue, closeIssueMode, autoMerge };
|
|
345
351
|
}
|
|
346
352
|
|
|
347
353
|
/**
|
|
@@ -352,7 +358,7 @@ function resolveGitHubConfig(options = {}) {
|
|
|
352
358
|
* @returns {Object|null} Platform configuration or null if unsupported
|
|
353
359
|
*/
|
|
354
360
|
function getPlatformConfig(platform, config = {}) {
|
|
355
|
-
const { prBase, useMergeQueue, closeIssueMode } = config;
|
|
361
|
+
const { prBase, useMergeQueue, closeIssueMode, autoMerge } = config;
|
|
356
362
|
|
|
357
363
|
const PLATFORM_CONFIGS = {
|
|
358
364
|
github: {
|
|
@@ -373,6 +379,7 @@ for i in $(seq 1 90); do if timeout 30 gh pr view --json mergedAt --jq .mergedAt
|
|
|
373
379
|
rebaseBranch: prBase || 'main',
|
|
374
380
|
usesMergeQueue: useMergeQueue,
|
|
375
381
|
closeIssueMode: closeIssueMode || 'never',
|
|
382
|
+
autoMerge: Boolean(autoMerge),
|
|
376
383
|
},
|
|
377
384
|
gitlab: {
|
|
378
385
|
prName: 'MR',
|
|
@@ -384,6 +391,7 @@ for i in $(seq 1 90); do if timeout 30 gh pr view --json mergedAt --jq .mergedAt
|
|
|
384
391
|
prUrlExample: 'https://gitlab.com/owner/repo/-/merge_requests/123',
|
|
385
392
|
outputFields: { urlField: 'mr_url', numberField: 'mr_number', mergedField: 'merged' },
|
|
386
393
|
closeIssueMode: closeIssueMode || 'never',
|
|
394
|
+
autoMerge: Boolean(autoMerge),
|
|
387
395
|
},
|
|
388
396
|
'azure-devops': {
|
|
389
397
|
prName: 'PR',
|
|
@@ -402,6 +410,7 @@ for i in $(seq 1 90); do if timeout 30 gh pr view --json mergedAt --jq .mergedAt
|
|
|
402
410
|
// Azure requires extracting PR ID from create output
|
|
403
411
|
requiresPrIdExtraction: true,
|
|
404
412
|
closeIssueMode: closeIssueMode || 'never',
|
|
413
|
+
autoMerge: Boolean(autoMerge),
|
|
405
414
|
},
|
|
406
415
|
};
|
|
407
416
|
|
|
@@ -414,6 +423,107 @@ for i in $(seq 1 90); do if timeout 30 gh pr view --json mergedAt --jq .mergedAt
|
|
|
414
423
|
*/
|
|
415
424
|
const SUPPORTED_PLATFORMS = ['github', 'gitlab', 'azure-devops'];
|
|
416
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Generate the review-mode prompt (--pr without --ship): create the PR/MR and STOP.
|
|
428
|
+
* No merge step, no issue-closing - the PR is left open for human review.
|
|
429
|
+
* @param {Object} config - Platform configuration from PLATFORM_CONFIGS
|
|
430
|
+
* @returns {string} The complete review-mode prompt
|
|
431
|
+
*/
|
|
432
|
+
function generateReviewModePrompt(config) {
|
|
433
|
+
const { prName, prNameLower, createCmd, prUrlExample, outputFields, requiresPrIdExtraction } =
|
|
434
|
+
config;
|
|
435
|
+
|
|
436
|
+
return `CRITICAL: ALL VALIDATORS APPROVED. YOU ARE A TRANSPORT-ONLY GIT PUSHER.
|
|
437
|
+
|
|
438
|
+
Your job is to preserve validator ownership: stage, commit, push, and create the ${prName} for HUMAN REVIEW.
|
|
439
|
+
|
|
440
|
+
Do NOT edit source files, tests, configs, generated artifacts, or lockfiles.
|
|
441
|
+
Do NOT inspect CI logs to debug product code.
|
|
442
|
+
Do NOT resolve merge conflicts or rebase conflicts.
|
|
443
|
+
Do NOT run implementation/debugging workflows after validators hand off.
|
|
444
|
+
Do NOT merge the ${prName} - it is left OPEN for human review.
|
|
445
|
+
Do NOT close the linked issue - it stays open until a human merges the ${prName}.
|
|
446
|
+
|
|
447
|
+
Allowed after validation:
|
|
448
|
+
- git add/status/commit/push
|
|
449
|
+
- ${createCmd.split(' ').slice(0, 3).join(' ')}
|
|
450
|
+
- status-only commands such as ${prName === 'PR' ? 'gh pr view/gh pr checks' : 'the platform PR/MR status command'}
|
|
451
|
+
|
|
452
|
+
If commit hooks, push, ${prName} creation, or conflict handling requires code changes, STOP and report the blocked state in JSON. The implementation and validator agents must fix code and rerun quality gates.
|
|
453
|
+
|
|
454
|
+
## MANDATORY STEPS - EXECUTE EACH ONE IN ORDER - DO NOT SKIP ANY STEP
|
|
455
|
+
|
|
456
|
+
### STEP 1: Stage ALL changes (MANDATORY)
|
|
457
|
+
\`\`\`bash
|
|
458
|
+
git add -A
|
|
459
|
+
\`\`\`
|
|
460
|
+
Run this command. Do not skip it. If commit fails because hooks/checks fail, do not edit files. Output blocked JSON with the failure summary.
|
|
461
|
+
|
|
462
|
+
### STEP 2: Check what's staged
|
|
463
|
+
\`\`\`bash
|
|
464
|
+
git status
|
|
465
|
+
\`\`\`
|
|
466
|
+
Run this. If nothing to commit, output JSON with ${outputFields.urlField}: null and stop.
|
|
467
|
+
|
|
468
|
+
### STEP 3: Commit the changes (MANDATORY if there are changes)
|
|
469
|
+
\`\`\`bash
|
|
470
|
+
git commit -m "feat: implement #{{issue_number}} - {{issue_title}}"
|
|
471
|
+
\`\`\`
|
|
472
|
+
Run this command. Do not skip it.
|
|
473
|
+
|
|
474
|
+
### STEP 4: Push to origin (MANDATORY)
|
|
475
|
+
\`\`\`bash
|
|
476
|
+
git push -u origin HEAD
|
|
477
|
+
\`\`\`
|
|
478
|
+
Run this. If it fails, do not edit files, rebase, or resolve conflicts. Output blocked JSON with the failure summary.
|
|
479
|
+
|
|
480
|
+
ā ļø AFTER PUSH YOU ARE NOT DONE! CONTINUE TO STEP 5! ā ļø
|
|
481
|
+
|
|
482
|
+
### STEP 5: CREATE THE ${prName.toUpperCase()} (MANDATORY - YOU MUST RUN THIS COMMAND)
|
|
483
|
+
\`\`\`bash
|
|
484
|
+
${createCmd}
|
|
485
|
+
\`\`\`
|
|
486
|
+
šØ YOU MUST RUN \`${createCmd.split(' ').slice(0, 3).join(' ')}\`! Outputting a link is NOT creating a ${prName}! šØ
|
|
487
|
+
The push output shows a "Create a ${prNameLower}" link - IGNORE IT.
|
|
488
|
+
You MUST run the \`${createCmd.split(' ').slice(0, 3).join(' ')}\` command above.${requiresPrIdExtraction ? '' : ` Save the actual ${prName} URL from the output.`}
|
|
489
|
+
|
|
490
|
+
ā ļø AFTER THE ${prName} IS CREATED, YOU ARE DONE. DO NOT MERGE. DO NOT CLOSE THE ISSUE. ā ļø
|
|
491
|
+
|
|
492
|
+
## CRITICAL RULES
|
|
493
|
+
- Execute EVERY step in order (1, 2, 3, 4, 5)
|
|
494
|
+
- Do NOT skip git add -A
|
|
495
|
+
- Do NOT skip git commit
|
|
496
|
+
- Do NOT skip ${createCmd.split(' ').slice(0, 3).join(' ')} - THE TASK IS NOT DONE UNTIL ${prName} EXISTS
|
|
497
|
+
- Do NOT merge the ${prName} - this run is for human review only
|
|
498
|
+
- Do NOT close the issue - it stays open until a human merges the ${prName}
|
|
499
|
+
- Do NOT edit files after validator handoff
|
|
500
|
+
- Do NOT debug product failures after validator handoff
|
|
501
|
+
- If push or ${prName} creation fails, report it instead of fixing code
|
|
502
|
+
- Output JSON as soon as the ${prName} is created (OPEN, unmerged), or a non-code transport failure blocks progress
|
|
503
|
+
- A link from git push is NOT a ${prName} - you must run ${createCmd.split(' ').slice(0, 3).join(' ')}
|
|
504
|
+
|
|
505
|
+
## Final Output
|
|
506
|
+
ONLY after the ${prName} is CREATED (left OPEN for review), output:
|
|
507
|
+
\`\`\`json
|
|
508
|
+
{"${outputFields.urlField}": "${prUrlExample}", "${outputFields.numberField}": 123, "merged": false}
|
|
509
|
+
\`\`\`
|
|
510
|
+
|
|
511
|
+
If truly no changes exist, output:
|
|
512
|
+
\`\`\`json
|
|
513
|
+
{"${outputFields.urlField}": null, "${outputFields.numberField}": null, "merged": false}
|
|
514
|
+
\`\`\`
|
|
515
|
+
|
|
516
|
+
If blocked after creating a ${prName}, output:
|
|
517
|
+
\`\`\`json
|
|
518
|
+
{"${outputFields.urlField}": "${prUrlExample}", "${outputFields.numberField}": 123, "merged": false, "blocked": true, "blocked_reason": "ci_failed: test job failed"}
|
|
519
|
+
\`\`\`
|
|
520
|
+
|
|
521
|
+
If blocked before creating a ${prName}, output:
|
|
522
|
+
\`\`\`json
|
|
523
|
+
{"${outputFields.urlField}": null, "${outputFields.numberField}": null, "merged": false, "blocked": true, "blocked_reason": "commit_failed: pre-commit hook failed"}
|
|
524
|
+
\`\`\``;
|
|
525
|
+
}
|
|
526
|
+
|
|
417
527
|
/**
|
|
418
528
|
* Generate the prompt for a specific platform
|
|
419
529
|
* @param {Object} config - Platform configuration from PLATFORM_CONFIGS
|
|
@@ -432,8 +542,13 @@ function generatePrompt(config) {
|
|
|
432
542
|
rebaseBranch,
|
|
433
543
|
usesMergeQueue,
|
|
434
544
|
closeIssueMode,
|
|
545
|
+
autoMerge,
|
|
435
546
|
} = config;
|
|
436
547
|
|
|
548
|
+
if (!autoMerge) {
|
|
549
|
+
return generateReviewModePrompt(config);
|
|
550
|
+
}
|
|
551
|
+
|
|
437
552
|
// Azure-specific instructions for PR ID extraction
|
|
438
553
|
const azurePrIdNote = requiresPrIdExtraction
|
|
439
554
|
? `\n\nš” IMPORTANT: The output will contain the PR ID. You MUST extract it for the next step.
|
|
@@ -614,6 +729,7 @@ If blocked before creating a ${prName}, output:
|
|
|
614
729
|
* @param {boolean} [options.mergeQueue] - Use GitHub merge queue
|
|
615
730
|
* @param {string} [options.closeIssue] - When to close issue: auto|always|never
|
|
616
731
|
* @param {Array} [options.requiredQualityGates] - Required handoff quality gates
|
|
732
|
+
* @param {boolean} [options.autoMerge] - Merge the PR (--ship). False stops after PR creation (--pr).
|
|
617
733
|
* @returns {Object} Agent configuration object
|
|
618
734
|
* @throws {Error} If platform is not supported
|
|
619
735
|
*/
|
|
@@ -647,8 +763,9 @@ function generateGitPusherAgent(platform, options = {}) {
|
|
|
647
763
|
hooks: {
|
|
648
764
|
onComplete: {
|
|
649
765
|
action: 'verify_pull_request',
|
|
650
|
-
//
|
|
651
|
-
//
|
|
766
|
+
// Verification reads PR data from result.structured_output; autoMerge controls
|
|
767
|
+
// whether an OPEN unmerged PR counts as success (--pr) or must be merged (--ship).
|
|
768
|
+
config: { autoMerge: Boolean(platformConfig.autoMerge) },
|
|
652
769
|
},
|
|
653
770
|
},
|
|
654
771
|
output: {
|
|
@@ -11,10 +11,10 @@ const path = require('path');
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const os = require('os');
|
|
13
13
|
const net = require('net');
|
|
14
|
+
const { readClustersFileSync } = require('../../lib/clusters-registry');
|
|
14
15
|
|
|
15
16
|
const ZEROSHOT_DIR = path.join(os.homedir(), '.zeroshot');
|
|
16
17
|
const SOCKET_DIR = path.join(ZEROSHOT_DIR, 'sockets');
|
|
17
|
-
const CLUSTERS_FILE = path.join(ZEROSHOT_DIR, 'clusters.json');
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Check if an ID is a known cluster by looking up clusters.json
|
|
@@ -24,8 +24,7 @@ const CLUSTERS_FILE = path.join(ZEROSHOT_DIR, 'clusters.json');
|
|
|
24
24
|
*/
|
|
25
25
|
function isKnownCluster(id) {
|
|
26
26
|
try {
|
|
27
|
-
|
|
28
|
-
const clusters = JSON.parse(fs.readFileSync(CLUSTERS_FILE, 'utf8'));
|
|
27
|
+
const clusters = readClustersFileSync(ZEROSHOT_DIR);
|
|
29
28
|
return id in clusters;
|
|
30
29
|
} catch {
|
|
31
30
|
return false;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provisions Claude Code credentials into an isolated CLAUDE_CONFIG_DIR.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code only reads the macOS Keychain when using the DEFAULT config dir.
|
|
5
|
+
* Isolated agent runs (--worktree, --docker) use a custom CLAUDE_CONFIG_DIR, so
|
|
6
|
+
* a Keychain-only (OAuth subscription) login must be materialized to a file or
|
|
7
|
+
* every isolated agent fails auth even though the host is logged in.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { execSync } = require('./lib/safe-exec');
|
|
14
|
+
|
|
15
|
+
const CREDENTIALS_BASENAME = '.credentials.json';
|
|
16
|
+
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the Claude Code OAuth credentials JSON from the macOS Keychain.
|
|
20
|
+
* @returns {string|null} Raw credentials JSON, or null if unavailable/invalid/non-darwin.
|
|
21
|
+
*/
|
|
22
|
+
function readKeychainCredentials() {
|
|
23
|
+
if (os.platform() !== 'darwin') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const output = execSync(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -w`, {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
31
|
+
timeout: 2000,
|
|
32
|
+
});
|
|
33
|
+
const trimmed = output.trim();
|
|
34
|
+
if (!trimmed) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
JSON.parse(trimmed); // reject garbage output that isn't real credentials
|
|
38
|
+
return trimmed;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Copy an existing credentials file into an isolated config dir, or materialize
|
|
46
|
+
* one from the macOS Keychain when no source file exists. Resynced on every call
|
|
47
|
+
* so an expired-then-refreshed OAuth token in the Keychain is picked up per run.
|
|
48
|
+
* @param {{ sourceDir: string, destDir: string }} options
|
|
49
|
+
* @returns {boolean} true if a credentials file was written into destDir
|
|
50
|
+
*/
|
|
51
|
+
function provisionClaudeCredentials({ sourceDir, destDir }) {
|
|
52
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
const destPath = path.join(destDir, CREDENTIALS_BASENAME);
|
|
55
|
+
const sourcePath = path.join(sourceDir, CREDENTIALS_BASENAME);
|
|
56
|
+
|
|
57
|
+
if (fs.existsSync(sourcePath)) {
|
|
58
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
59
|
+
fs.chmodSync(destPath, 0o600);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const keychainCredentials = readKeychainCredentials();
|
|
64
|
+
if (keychainCredentials) {
|
|
65
|
+
fs.writeFileSync(destPath, keychainCredentials, { mode: 0o600 });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
readKeychainCredentials,
|
|
74
|
+
provisionClaudeCredentials,
|
|
75
|
+
};
|