@the-open-engine/zeroshot 6.1.0 → 6.3.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/commands/cmdproof.js +180 -6
- package/cli/event-copy.js +12 -0
- package/cli/index.js +311 -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 +9 -0
- package/lib/id-detector.js +8 -9
- package/lib/path-check.js +63 -0
- package/lib/run-mode.js +22 -0
- package/lib/start-cluster.js +39 -23
- package/package.json +3 -4
- 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-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 +42 -3
- package/src/lib/gc.js +2 -3
- package/src/message-bus.js +10 -0
- package/src/orchestrator.js +264 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@the-open-engine/zeroshot",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
|
|
5
5
|
"main": "src/orchestrator.js",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"test:all": "npm run test && npm run test:slow",
|
|
20
20
|
"test:coverage": "c8 npm run test:unit",
|
|
21
21
|
"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",
|
|
22
|
+
"postinstall": "node scripts/fix-node-pty-permissions.js && node scripts/check-path.js",
|
|
23
23
|
"start": "node cli/index.js",
|
|
24
24
|
"typecheck": "tsc --noEmit",
|
|
25
25
|
"typecheck:agent-cli-provider": "tsc --project tsconfig.agent-cli-provider.json",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"cli/",
|
|
95
95
|
"task-lib/",
|
|
96
96
|
"cluster-templates/",
|
|
97
|
-
"hooks/",
|
|
97
|
+
"cluster-hooks/",
|
|
98
98
|
"docker/",
|
|
99
99
|
"scripts/",
|
|
100
100
|
"README.md",
|
|
@@ -110,7 +110,6 @@
|
|
|
110
110
|
"node-pty": "^1.1.0",
|
|
111
111
|
"omelette": "^0.4.17",
|
|
112
112
|
"pidusage": "^4.0.1",
|
|
113
|
-
"puppeteer": "^24.34.0",
|
|
114
113
|
"proper-lockfile": "^4.1.2"
|
|
115
114
|
},
|
|
116
115
|
"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
|
+
};
|
package/src/isolation-manager.js
CHANGED
|
@@ -20,6 +20,7 @@ const { normalizeProviderName } = require('../lib/provider-names');
|
|
|
20
20
|
const { resolveMounts, resolveEnvs, expandEnvPatterns } = require('../lib/docker-config');
|
|
21
21
|
const { getProvider } = require('./providers');
|
|
22
22
|
const { readRepoSettings } = require('../lib/repo-settings');
|
|
23
|
+
const { provisionClaudeCredentials } = require('./claude-credentials');
|
|
23
24
|
|
|
24
25
|
const DEFAULT_WORKTREE_SETUP_TIMEOUT_MS = 15 * 60 * 1000;
|
|
25
26
|
|
|
@@ -1036,14 +1037,12 @@ class IsolationManager {
|
|
|
1036
1037
|
const projectsDir = path.join(configDir, 'projects');
|
|
1037
1038
|
fs.mkdirSync(projectsDir, { recursive: true });
|
|
1038
1039
|
|
|
1039
|
-
// Copy
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
fs.copyFileSync(credentialsFile, path.join(configDir, '.credentials.json'));
|
|
1043
|
-
}
|
|
1040
|
+
// Copy credentials file, or materialize from macOS Keychain if none exists
|
|
1041
|
+
// (essential for auth; see src/claude-credentials.js)
|
|
1042
|
+
provisionClaudeCredentials({ sourceDir, destDir: configDir });
|
|
1044
1043
|
|
|
1045
1044
|
// Copy hook script to block AskUserQuestion (CRITICAL for autonomous execution)
|
|
1046
|
-
const hookScriptSrc = path.join(__dirname, '..', 'hooks', 'block-ask-user-question.py');
|
|
1045
|
+
const hookScriptSrc = path.join(__dirname, '..', 'cluster-hooks', 'block-ask-user-question.py');
|
|
1047
1046
|
const hookScriptDst = path.join(hooksDir, 'block-ask-user-question.py');
|
|
1048
1047
|
if (fs.existsSync(hookScriptSrc)) {
|
|
1049
1048
|
fs.copyFileSync(hookScriptSrc, hookScriptDst);
|
|
@@ -1651,10 +1650,14 @@ class IsolationManager {
|
|
|
1651
1650
|
// Tear down any Docker Compose services that may have been started in this worktree.
|
|
1652
1651
|
// Without this, containers keep running with host port mappings after the worktree is deleted,
|
|
1653
1652
|
// blocking port allocation for the main project or other worktrees.
|
|
1654
|
-
|
|
1655
|
-
|
|
1653
|
+
// NEVER pass --volumes (irreversible data loss) and NEVER tear down a pinned/shared
|
|
1654
|
+
// Compose project — only a project scoped to the worktree directory basename, which is
|
|
1655
|
+
// the only kind zeroshot could itself have created, is touched.
|
|
1656
|
+
const { resolveWorktreeComposeTeardown } = require('../lib/compose-utils');
|
|
1657
|
+
const teardown = resolveWorktreeComposeTeardown(worktreeInfo.path);
|
|
1658
|
+
if (teardown.shouldTeardown) {
|
|
1656
1659
|
try {
|
|
1657
|
-
runSync('docker',
|
|
1660
|
+
runSync('docker', teardown.args, {
|
|
1658
1661
|
cwd: worktreeInfo.path,
|
|
1659
1662
|
encoding: 'utf8',
|
|
1660
1663
|
stdio: 'pipe',
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
# Issue Providers
|
|
2
2
|
|
|
3
|
-
Multi-platform issue support for Zeroshot. Fetch issues from GitHub, GitLab, Jira,
|
|
3
|
+
Multi-platform issue support for Zeroshot. Fetch issues from GitHub, GitLab, Jira, Azure DevOps, and Linear.
|
|
4
4
|
|
|
5
5
|
## Supported Providers
|
|
6
6
|
|
|
7
|
-
| Provider | CLI Tool
|
|
8
|
-
| ---------------- |
|
|
9
|
-
| **GitHub** | `gh`
|
|
10
|
-
| **GitLab** | `glab`
|
|
11
|
-
| **Jira** | `jira`
|
|
12
|
-
| **Azure DevOps** | `az`
|
|
7
|
+
| Provider | CLI Tool | URL Pattern | Issue Key Format |
|
|
8
|
+
| ---------------- | ---------------------- | -------------------------------------------- | --------------------- |
|
|
9
|
+
| **GitHub** | `gh` | `github.com/org/repo/issues/123` | `123`, `org/repo#123` |
|
|
10
|
+
| **GitLab** | `glab` | `gitlab.com/org/repo/-/issues/123` | `123`, `org/repo#123` |
|
|
11
|
+
| **Jira** | `jira` | `*.atlassian.net/browse/KEY-123` | `KEY-123` |
|
|
12
|
+
| **Azure DevOps** | `az` | `dev.azure.com/org/proj/_workitems/edit/123` | `123` |
|
|
13
|
+
| **Linear** | _(none — GraphQL API)_ | `linear.app/ws/issue/ENG-42` | `ENG-42` |
|
|
13
14
|
|
|
14
15
|
## Quick Start
|
|
15
16
|
|
|
@@ -30,6 +31,11 @@ zeroshot run https://company.atlassian.net/browse/PROJ-123
|
|
|
30
31
|
# Azure DevOps
|
|
31
32
|
zeroshot run https://dev.azure.com/org/project/_workitems/edit/123
|
|
32
33
|
zeroshot run 123 --devops
|
|
34
|
+
|
|
35
|
+
# Linear
|
|
36
|
+
zeroshot run ENG-42
|
|
37
|
+
zeroshot run https://linear.app/workspace/issue/ENG-42/some-title
|
|
38
|
+
zeroshot run 42 --linear
|
|
33
39
|
```
|
|
34
40
|
|
|
35
41
|
## Automatic Git Remote Detection
|
|
@@ -59,6 +65,7 @@ Override auto-detection with explicit provider flags:
|
|
|
59
65
|
-L, --gitlab # Force GitLab as issue source
|
|
60
66
|
-J, --jira # Force Jira as issue source
|
|
61
67
|
-D, --devops # Force Azure DevOps as issue source
|
|
68
|
+
-N, --linear # Force Linear as issue source
|
|
62
69
|
```
|
|
63
70
|
|
|
64
71
|
**Example:**
|
|
@@ -100,18 +107,24 @@ zeroshot settings set jiraProject MYPROJECT # Default project for bare numbers
|
|
|
100
107
|
# Azure DevOps configuration
|
|
101
108
|
zeroshot settings set azureOrg mycompany
|
|
102
109
|
zeroshot settings set azureProject myproject # Default project for bare numbers
|
|
110
|
+
|
|
111
|
+
# Linear configuration
|
|
112
|
+
zeroshot settings set linearApiKey lin_api_... # API key (falls back to LINEAR_API_KEY env var)
|
|
113
|
+
zeroshot settings set linearTeam ENG # Default team key for bare numbers (auto-derived on first use if unset)
|
|
103
114
|
```
|
|
104
115
|
|
|
105
116
|
### Settings Reference
|
|
106
117
|
|
|
107
|
-
| Setting | Type | Default | Description
|
|
108
|
-
| -------------------- | ------ | ------- |
|
|
109
|
-
| `defaultIssueSource` | string | github | Provider for bare numbers (123)
|
|
110
|
-
| `gitlabInstance` | string | null | Self-hosted GitLab URL
|
|
111
|
-
| `jiraInstance` | string | null | Self-hosted Jira URL
|
|
112
|
-
| `jiraProject` | string | null | Default Jira project key for bare numbers
|
|
113
|
-
| `azureOrg` | string | null | Azure DevOps organization name
|
|
114
|
-
| `azureProject` | string | null | Azure DevOps project name for bare numbers
|
|
118
|
+
| Setting | Type | Default | Description |
|
|
119
|
+
| -------------------- | ------ | ------- | --------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `defaultIssueSource` | string | github | Provider for bare numbers (123) |
|
|
121
|
+
| `gitlabInstance` | string | null | Self-hosted GitLab URL |
|
|
122
|
+
| `jiraInstance` | string | null | Self-hosted Jira URL |
|
|
123
|
+
| `jiraProject` | string | null | Default Jira project key for bare numbers |
|
|
124
|
+
| `azureOrg` | string | null | Azure DevOps organization name |
|
|
125
|
+
| `azureProject` | string | null | Azure DevOps project name for bare numbers |
|
|
126
|
+
| `linearApiKey` | string | null | Linear personal API key (falls back to `LINEAR_API_KEY` env var) |
|
|
127
|
+
| `linearTeam` | string | null | Default Linear team key for bare numbers (auto-derived from the workspace if unset and unambiguous) |
|
|
115
128
|
|
|
116
129
|
## CLI Tool Setup
|
|
117
130
|
|
|
@@ -166,6 +179,23 @@ az login
|
|
|
166
179
|
az devops configure --defaults organization=https://dev.azure.com/yourorg
|
|
167
180
|
```
|
|
168
181
|
|
|
182
|
+
### Linear
|
|
183
|
+
|
|
184
|
+
No CLI install needed — Linear is accessed directly via its GraphQL API.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Create a personal API key at https://linear.app/settings/api
|
|
188
|
+
zeroshot settings set linearApiKey lin_api_...
|
|
189
|
+
|
|
190
|
+
# Or, as a fallback, export it as an env var (checked if linearApiKey is unset):
|
|
191
|
+
export LINEAR_API_KEY=lin_api_...
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Note: `KEY-NUMBER` input (e.g. `ENG-42`) is ambiguous between Jira and Linear.
|
|
195
|
+
It's routed to Linear automatically when Linear is configured
|
|
196
|
+
(`linearApiKey`/`linearTeam`) and Jira isn't; otherwise Jira wins. Use
|
|
197
|
+
`--linear`/`--jira` to force a specific provider.
|
|
198
|
+
|
|
169
199
|
## Self-Hosted Instances
|
|
170
200
|
|
|
171
201
|
### GitLab Self-Hosted
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
const GitHubProvider = require('./github-provider');
|
|
8
8
|
const GitLabProvider = require('./gitlab-provider');
|
|
9
9
|
const JiraProvider = require('./jira-provider');
|
|
10
|
+
const LinearProvider = require('./linear-provider');
|
|
10
11
|
const AzureDevOpsProvider = require('./azure-devops-provider');
|
|
11
12
|
const { detectGitContext } = require('../../lib/git-remote-utils');
|
|
12
13
|
|
|
@@ -180,6 +181,7 @@ function validateIssueProviderSetting(key, value) {
|
|
|
180
181
|
registerProvider(GitHubProvider);
|
|
181
182
|
registerProvider(GitLabProvider);
|
|
182
183
|
registerProvider(JiraProvider);
|
|
184
|
+
registerProvider(LinearProvider);
|
|
183
185
|
registerProvider(AzureDevOpsProvider);
|
|
184
186
|
|
|
185
187
|
module.exports = {
|