@covibes/zeroshot 5.2.1 → 5.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/CHANGELOG.md +174 -189
- package/README.md +226 -195
- package/cli/commands/providers.js +149 -0
- package/cli/index.js +3145 -2366
- package/cli/lib/first-run.js +40 -3
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +24 -78
- package/cluster-templates/base-templates/full-workflow.json +99 -316
- package/cluster-templates/base-templates/single-worker.json +23 -15
- package/cluster-templates/base-templates/worker-validator.json +105 -36
- package/cluster-templates/conductor-bootstrap.json +9 -7
- package/lib/docker-config.js +14 -1
- package/lib/git-remote-utils.js +165 -0
- package/lib/id-detector.js +10 -7
- package/lib/provider-defaults.js +62 -0
- package/lib/provider-detection.js +59 -0
- package/lib/provider-names.js +57 -0
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +298 -15
- package/lib/stream-json-parser.js +4 -238
- package/package.json +27 -6
- package/scripts/setup-merge-queue.sh +170 -0
- package/scripts/validate-templates.js +100 -0
- package/src/agent/agent-config.js +140 -63
- package/src/agent/agent-context-builder.js +336 -165
- package/src/agent/agent-hook-executor.js +337 -67
- package/src/agent/agent-lifecycle.js +386 -287
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +944 -683
- package/src/agent/output-extraction.js +217 -0
- package/src/agent/output-reformatter.js +175 -0
- package/src/agent/schema-utils.js +146 -0
- package/src/agent-wrapper.js +112 -31
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +145 -44
- package/src/config-router.js +13 -13
- package/src/config-validator.js +1049 -563
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +499 -320
- package/src/issue-providers/README.md +305 -0
- package/src/issue-providers/azure-devops-provider.js +273 -0
- package/src/issue-providers/base-provider.js +232 -0
- package/src/issue-providers/github-provider.js +179 -0
- package/src/issue-providers/gitlab-provider.js +241 -0
- package/src/issue-providers/index.js +196 -0
- package/src/issue-providers/jira-provider.js +239 -0
- package/src/ledger.js +50 -11
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1348 -757
- package/src/preflight.js +306 -149
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +73 -0
- package/src/providers/anthropic/index.js +204 -0
- package/src/providers/anthropic/models.js +23 -0
- package/src/providers/anthropic/output-parser.js +177 -0
- package/src/providers/base-provider.js +251 -0
- package/src/providers/capabilities.js +60 -0
- package/src/providers/google/cli-builder.js +55 -0
- package/src/providers/google/index.js +116 -0
- package/src/providers/google/models.js +24 -0
- package/src/providers/google/output-parser.js +101 -0
- package/src/providers/index.js +91 -0
- package/src/providers/openai/cli-builder.js +133 -0
- package/src/providers/openai/index.js +136 -0
- package/src/providers/openai/models.js +21 -0
- package/src/providers/openai/output-parser.js +143 -0
- package/src/providers/opencode/cli-builder.js +42 -0
- package/src/providers/opencode/index.js +103 -0
- package/src/providers/opencode/models.js +55 -0
- package/src/providers/opencode/output-parser.js +122 -0
- package/src/schemas/sub-cluster.js +68 -39
- package/src/status-footer.js +94 -48
- package/src/sub-cluster-wrapper.js +92 -36
- package/src/task-runner.js +8 -6
- package/src/template-resolver.js +12 -9
- package/src/tui/data-poller.js +123 -99
- package/src/tui/formatters.js +4 -3
- package/src/tui/keybindings.js +259 -318
- package/src/tui/layout.js +20 -3
- package/src/tui/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +150 -111
- package/task-lib/claude-recovery.js +156 -0
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +3 -3
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/resume.js +3 -2
- package/task-lib/commands/run.js +12 -3
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +128 -50
- package/task-lib/scheduler.js +3 -3
- package/task-lib/store.js +464 -152
- package/task-lib/tui/formatters.js +94 -45
- package/task-lib/tui/renderer.js +13 -3
- package/task-lib/tui.js +73 -32
- package/task-lib/watcher.js +157 -100
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -103
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue Provider Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages issue provider registration and auto-detection
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const GitHubProvider = require('./github-provider');
|
|
8
|
+
const GitLabProvider = require('./gitlab-provider');
|
|
9
|
+
const JiraProvider = require('./jira-provider');
|
|
10
|
+
const AzureDevOpsProvider = require('./azure-devops-provider');
|
|
11
|
+
const { detectGitContext } = require('../../lib/git-remote-utils');
|
|
12
|
+
|
|
13
|
+
/** @type {Map<string, typeof IssueProvider>} */
|
|
14
|
+
const providers = new Map();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register a provider class
|
|
18
|
+
* @param {typeof IssueProvider} ProviderClass
|
|
19
|
+
*/
|
|
20
|
+
function registerProvider(ProviderClass) {
|
|
21
|
+
if (!ProviderClass.id) {
|
|
22
|
+
throw new Error(`Provider ${ProviderClass.name} must define static id property`);
|
|
23
|
+
}
|
|
24
|
+
providers.set(ProviderClass.id, ProviderClass);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect which provider matches the input
|
|
29
|
+
* Automatically detects provider from git remote when in a git repository
|
|
30
|
+
*
|
|
31
|
+
* @param {string} input - User input (URL, issue key, number)
|
|
32
|
+
* @param {Object} settings - User settings
|
|
33
|
+
* @param {string|null} forceProvider - Force specific provider (from CLI flag)
|
|
34
|
+
* @param {Object|null|undefined} [gitContextOverride] - Override git context (for testing)
|
|
35
|
+
* - undefined: auto-detect from git remote (default)
|
|
36
|
+
* - null: explicitly no git context
|
|
37
|
+
* - object: use provided git context
|
|
38
|
+
* @returns {typeof IssueProvider|null}
|
|
39
|
+
*/
|
|
40
|
+
function detectProvider(input, settings, forceProvider = null, gitContextOverride = undefined) {
|
|
41
|
+
// Force flag takes precedence
|
|
42
|
+
if (forceProvider) {
|
|
43
|
+
return providers.get(forceProvider) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Use provided gitContext or auto-detect
|
|
47
|
+
const gitContext = gitContextOverride === undefined ? detectGitContext() : gitContextOverride;
|
|
48
|
+
|
|
49
|
+
// Auto-detect by checking each provider's detectIdentifier
|
|
50
|
+
for (const ProviderClass of providers.values()) {
|
|
51
|
+
if (ProviderClass.detectIdentifier(input, settings, gitContext)) {
|
|
52
|
+
return ProviderClass;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get provider class by ID
|
|
61
|
+
* @param {string} providerId - Provider identifier
|
|
62
|
+
* @returns {typeof IssueProvider|null}
|
|
63
|
+
*/
|
|
64
|
+
function getProvider(providerId) {
|
|
65
|
+
return providers.get(providerId) || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* List all registered provider IDs
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
function listProviders() {
|
|
73
|
+
return Array.from(providers.keys());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* List providers that support PR/MR creation
|
|
78
|
+
* @returns {string[]}
|
|
79
|
+
*/
|
|
80
|
+
function listPRProviders() {
|
|
81
|
+
return Array.from(providers.values())
|
|
82
|
+
.filter((p) => p.supportsPR())
|
|
83
|
+
.map((p) => p.id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Determine which git platform to create PR/MR on.
|
|
88
|
+
* ALWAYS uses git remote context, NEVER the issue provider.
|
|
89
|
+
*
|
|
90
|
+
* This is the unified replacement for platform-detector.js
|
|
91
|
+
*
|
|
92
|
+
* @param {string} [cwd=process.cwd()] - Working directory
|
|
93
|
+
* @returns {string} Platform ID ('github', 'gitlab', 'azure-devops')
|
|
94
|
+
* @throws {Error} If platform cannot be determined or doesn't support PRs
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* // In a GitHub repo
|
|
98
|
+
* getPlatformForPR() // → 'github'
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // In a GitLab repo with Jira issue
|
|
102
|
+
* getPlatformForPR() // → 'gitlab' (creates GitLab MR, not related to Jira)
|
|
103
|
+
*/
|
|
104
|
+
function getPlatformForPR(cwd = process.cwd()) {
|
|
105
|
+
const gitContext = detectGitContext(cwd);
|
|
106
|
+
|
|
107
|
+
if (!gitContext?.provider) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'Cannot determine git platform for --pr mode. ' +
|
|
110
|
+
'Ensure you are in a git repository with a remote URL from GitHub, GitLab, or Azure DevOps.'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const platform = gitContext.provider;
|
|
115
|
+
const ProviderClass = providers.get(platform);
|
|
116
|
+
|
|
117
|
+
if (!ProviderClass || !ProviderClass.supportsPR()) {
|
|
118
|
+
const supported = listPRProviders().join(', ');
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Platform '${platform}' does not support --pr mode. Supported platforms: ${supported}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return platform;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get PR tool info for a platform
|
|
129
|
+
* @param {string} platform - Platform ID
|
|
130
|
+
* @returns {{ name: string, checkCmd: string, installHint: string, displayName: string }|null}
|
|
131
|
+
*/
|
|
132
|
+
function getPRToolForPlatform(platform) {
|
|
133
|
+
const ProviderClass = providers.get(platform);
|
|
134
|
+
return ProviderClass?.getPRTool() || null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get aggregated settings schema from all issue providers
|
|
139
|
+
* @returns {Object.<string, Object>} Combined settings schema
|
|
140
|
+
*/
|
|
141
|
+
function getIssueProviderSettingsSchema() {
|
|
142
|
+
const schema = {};
|
|
143
|
+
for (const ProviderClass of providers.values()) {
|
|
144
|
+
Object.assign(schema, ProviderClass.getSettingsSchema());
|
|
145
|
+
}
|
|
146
|
+
return schema;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get default values for all issue provider settings
|
|
151
|
+
* @returns {Object.<string, any>} Default values keyed by setting name
|
|
152
|
+
*/
|
|
153
|
+
function getIssueProviderSettingsDefaults() {
|
|
154
|
+
const defaults = {};
|
|
155
|
+
const schema = getIssueProviderSettingsSchema();
|
|
156
|
+
for (const [key, config] of Object.entries(schema)) {
|
|
157
|
+
defaults[key] = config.default;
|
|
158
|
+
}
|
|
159
|
+
return defaults;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate an issue provider setting
|
|
164
|
+
* Delegates to the appropriate provider's validateSetting method
|
|
165
|
+
* @param {string} key - Setting key
|
|
166
|
+
* @param {any} value - Setting value
|
|
167
|
+
* @returns {string|null|undefined} Error message if invalid, null if valid, undefined if not an issue provider setting
|
|
168
|
+
*/
|
|
169
|
+
function validateIssueProviderSetting(key, value) {
|
|
170
|
+
for (const ProviderClass of providers.values()) {
|
|
171
|
+
const result = ProviderClass.validateSetting(key, value);
|
|
172
|
+
if (result !== undefined) {
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return undefined; // Not an issue provider setting
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Auto-register providers on module load
|
|
180
|
+
registerProvider(GitHubProvider);
|
|
181
|
+
registerProvider(GitLabProvider);
|
|
182
|
+
registerProvider(JiraProvider);
|
|
183
|
+
registerProvider(AzureDevOpsProvider);
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
registerProvider,
|
|
187
|
+
detectProvider,
|
|
188
|
+
getProvider,
|
|
189
|
+
listProviders,
|
|
190
|
+
listPRProviders,
|
|
191
|
+
getPlatformForPR,
|
|
192
|
+
getPRToolForPlatform,
|
|
193
|
+
getIssueProviderSettingsSchema,
|
|
194
|
+
getIssueProviderSettingsDefaults,
|
|
195
|
+
validateIssueProviderSetting,
|
|
196
|
+
};
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jira Provider - Fetch issues from Jira via jira CLI (go-jira)
|
|
3
|
+
* Supports both Jira Cloud (*.atlassian.net) and self-hosted (Server/Data Center)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const IssueProvider = require('./base-provider');
|
|
7
|
+
const { execSync } = require('../lib/safe-exec');
|
|
8
|
+
|
|
9
|
+
class JiraProvider extends IssueProvider {
|
|
10
|
+
static id = 'jira';
|
|
11
|
+
static displayName = 'Jira';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Detect Jira issue keys and URLs
|
|
15
|
+
* Matches:
|
|
16
|
+
* - Jira Cloud URLs (*.atlassian.net)
|
|
17
|
+
* - Self-hosted Jira URLs (via jiraInstance setting)
|
|
18
|
+
* - Jira issue keys (KEY-123 format)
|
|
19
|
+
* - Bare numbers when defaultIssueSource=jira (requires jiraProject setting)
|
|
20
|
+
*
|
|
21
|
+
* Note: Jira doesn't use git context since it's not a git hosting platform.
|
|
22
|
+
* Git context will never match 'jira', so this provider only activates via
|
|
23
|
+
* explicit Jira URLs, Jira issue keys, or settings.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} input - Issue identifier (URL, key, or number)
|
|
26
|
+
* @param {Object} settings - User settings
|
|
27
|
+
* @param {Object|null} gitContext - Git context (unused for Jira, but kept for API consistency)
|
|
28
|
+
* @returns {boolean} True if this provider should handle the input
|
|
29
|
+
*/
|
|
30
|
+
static detectIdentifier(input, settings, gitContext = null) {
|
|
31
|
+
// Jira Cloud URLs
|
|
32
|
+
if (/atlassian\.net\/browse\/[A-Z][A-Z0-9]+-\d+/.test(input)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Self-hosted Jira URLs
|
|
37
|
+
if (
|
|
38
|
+
settings.jiraInstance &&
|
|
39
|
+
input.includes(settings.jiraInstance) &&
|
|
40
|
+
/\/browse\/[A-Z][A-Z0-9]+-\d+/.test(input)
|
|
41
|
+
) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Jira issue key pattern (KEY-123)
|
|
46
|
+
if (/^[A-Z][A-Z0-9]+-\d+$/.test(input)) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Bare numbers - use shared priority cascade logic
|
|
51
|
+
// Jira requires jiraProject setting to convert bare numbers to issue keys
|
|
52
|
+
// Note: gitContext check will never match 'jira' since Jira isn't a git host
|
|
53
|
+
return IssueProvider.detectBareNumber(input, settings, gitContext, 'jira', {
|
|
54
|
+
requiredSettings: ['jiraProject'],
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static getRequiredTool() {
|
|
59
|
+
return {
|
|
60
|
+
name: 'jira',
|
|
61
|
+
checkCmd: 'jira version',
|
|
62
|
+
installHint: 'Install jira-cli: brew install go-jira (or https://github.com/go-jira/jira)',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check jira CLI authentication
|
|
68
|
+
* go-jira uses config files (~/.jira.d/) rather than explicit auth commands
|
|
69
|
+
*/
|
|
70
|
+
static checkAuth() {
|
|
71
|
+
try {
|
|
72
|
+
// go-jira uses 'jira session' to verify authentication
|
|
73
|
+
// If not configured, it fails with endpoint/login errors
|
|
74
|
+
execSync('jira session', { encoding: 'utf8', stdio: 'pipe', timeout: 5000 });
|
|
75
|
+
return { authenticated: true, error: null, recovery: [] };
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const stderr = err.stderr || err.message || '';
|
|
78
|
+
|
|
79
|
+
// go-jira has various error patterns for auth issues
|
|
80
|
+
if (
|
|
81
|
+
stderr.includes('endpoint') ||
|
|
82
|
+
stderr.includes('login') ||
|
|
83
|
+
stderr.includes('authentication') ||
|
|
84
|
+
stderr.includes('401')
|
|
85
|
+
) {
|
|
86
|
+
return {
|
|
87
|
+
authenticated: false,
|
|
88
|
+
error: 'Jira CLI not configured',
|
|
89
|
+
recovery: [
|
|
90
|
+
'Create ~/.jira.d/config.yml with your Jira endpoint',
|
|
91
|
+
'See: https://github.com/go-jira/jira#configuration',
|
|
92
|
+
'Example config:\n endpoint: https://yourcompany.atlassian.net\n login: your-email@company.com',
|
|
93
|
+
'Then verify: jira session',
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// If command just doesn't exist or times out, it's likely not configured
|
|
99
|
+
if (stderr.includes('command not found') || stderr.includes('ETIMEDOUT')) {
|
|
100
|
+
return {
|
|
101
|
+
authenticated: false,
|
|
102
|
+
error: 'Jira CLI not configured',
|
|
103
|
+
recovery: [
|
|
104
|
+
'Configure ~/.jira.d/config.yml',
|
|
105
|
+
'See: https://github.com/go-jira/jira#configuration',
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Default: assume not authenticated
|
|
111
|
+
return {
|
|
112
|
+
authenticated: false,
|
|
113
|
+
error: stderr.trim() || 'Jira CLI not configured or authentication failed',
|
|
114
|
+
recovery: [
|
|
115
|
+
'Create ~/.jira.d/config.yml with your Jira endpoint',
|
|
116
|
+
'See: https://github.com/go-jira/jira#configuration',
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Jira-specific settings schema
|
|
124
|
+
*/
|
|
125
|
+
static getSettingsSchema() {
|
|
126
|
+
return {
|
|
127
|
+
jiraInstance: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
nullable: true,
|
|
130
|
+
default: null,
|
|
131
|
+
description: "Self-hosted Jira URL (e.g., 'jira.company.com')",
|
|
132
|
+
},
|
|
133
|
+
jiraProject: {
|
|
134
|
+
type: 'string',
|
|
135
|
+
nullable: true,
|
|
136
|
+
default: null,
|
|
137
|
+
description: "Default Jira project key for bare numbers (e.g., 'PROJ')",
|
|
138
|
+
pattern: /^[A-Z][A-Z0-9]*$/,
|
|
139
|
+
patternMsg: 'jiraProject must be a valid Jira project key (e.g., PROJ, ABC)',
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fetchIssue(identifier, settings) {
|
|
145
|
+
try {
|
|
146
|
+
const issueKey = this._extractIssueKey(identifier, settings);
|
|
147
|
+
|
|
148
|
+
// Fetch issue using jira CLI
|
|
149
|
+
const cmd = `jira issue view ${issueKey} --template json`;
|
|
150
|
+
const output = execSync(cmd, { encoding: 'utf8' });
|
|
151
|
+
const issue = JSON.parse(output);
|
|
152
|
+
|
|
153
|
+
return this._parseIssue(issue);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(`Failed to fetch Jira issue: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Extract Jira issue key from URL or return as-is
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
_extractIssueKey(identifier, settings) {
|
|
164
|
+
// URL format: https://company.atlassian.net/browse/KEY-123
|
|
165
|
+
const urlMatch = identifier.match(/\/browse\/([A-Z][A-Z0-9]+-\d+)/);
|
|
166
|
+
if (urlMatch) {
|
|
167
|
+
return urlMatch[1];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Bare number: construct from jiraProject setting
|
|
171
|
+
if (/^\d+$/.test(identifier) && settings.jiraProject) {
|
|
172
|
+
return `${settings.jiraProject}-${identifier}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Assume it's already a key
|
|
176
|
+
return identifier;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parse Jira issue into standardized InputData format
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
_parseIssue(issue) {
|
|
184
|
+
const fields = issue.fields || {};
|
|
185
|
+
const key = issue.key;
|
|
186
|
+
const summary = fields.summary || '';
|
|
187
|
+
const description = fields.description || '';
|
|
188
|
+
const labels = fields.labels || [];
|
|
189
|
+
const comments = fields.comment?.comments || [];
|
|
190
|
+
|
|
191
|
+
let context = `# Jira Issue ${key}\n\n`;
|
|
192
|
+
context += `## Title\n${summary}\n\n`;
|
|
193
|
+
|
|
194
|
+
if (description) {
|
|
195
|
+
context += `## Description\n${description}\n\n`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (labels.length > 0) {
|
|
199
|
+
context += `## Labels\n`;
|
|
200
|
+
context += labels.map((l) => `- ${l}`).join('\n');
|
|
201
|
+
context += '\n\n';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (comments.length > 0) {
|
|
205
|
+
context += `## Comments\n\n`;
|
|
206
|
+
for (const comment of comments) {
|
|
207
|
+
const author = comment.author?.displayName || comment.author?.name || 'unknown';
|
|
208
|
+
context += `### ${author} (${comment.created})\n`;
|
|
209
|
+
context += `${comment.body}\n\n`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Map Jira labels to GitHub format
|
|
214
|
+
const mappedLabels = labels.map((name) => ({ name }));
|
|
215
|
+
|
|
216
|
+
// Map Jira comments to GitHub format
|
|
217
|
+
const mappedComments = comments.map((comment) => ({
|
|
218
|
+
author: { login: comment.author?.displayName || comment.author?.name || 'unknown' },
|
|
219
|
+
createdAt: comment.created,
|
|
220
|
+
body: comment.body,
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
// Extract issue number from key (KEY-123 → 123)
|
|
224
|
+
const numberMatch = key.match(/-(\d+)$/);
|
|
225
|
+
const number = numberMatch ? parseInt(numberMatch[1], 10) : null;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
number,
|
|
229
|
+
title: summary,
|
|
230
|
+
body: description,
|
|
231
|
+
labels: mappedLabels,
|
|
232
|
+
comments: mappedComments,
|
|
233
|
+
url: fields.self || null,
|
|
234
|
+
context,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = JiraProvider;
|
package/src/ledger.js
CHANGED
|
@@ -15,20 +15,38 @@ const crypto = require('crypto');
|
|
|
15
15
|
class Ledger extends EventEmitter {
|
|
16
16
|
constructor(dbPath = ':memory:') {
|
|
17
17
|
super();
|
|
18
|
-
this.
|
|
18
|
+
this.dbPath = dbPath;
|
|
19
|
+
const busyTimeoutMs = (() => {
|
|
20
|
+
const raw = process.env.ZEROSHOT_SQLITE_BUSY_TIMEOUT_MS;
|
|
21
|
+
if (!raw) return 5000;
|
|
22
|
+
const value = Number(raw);
|
|
23
|
+
return Number.isFinite(value) && value >= 0 ? value : 5000;
|
|
24
|
+
})();
|
|
25
|
+
|
|
26
|
+
this.db = new Database(dbPath, { timeout: busyTimeoutMs });
|
|
19
27
|
this.cache = new Map(); // LRU cache for queries
|
|
20
28
|
this.cacheLimit = 1000;
|
|
21
29
|
this._closed = false; // Track closed state to prevent write-after-close
|
|
30
|
+
this._lastTimestamp = 0;
|
|
22
31
|
this._initSchema();
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
_initSchema() {
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
const journalMode = (process.env.ZEROSHOT_SQLITE_JOURNAL_MODE || 'WAL').trim().toUpperCase();
|
|
36
|
+
// Enable WAL mode for concurrent reads (default), but allow overrides for network filesystems.
|
|
37
|
+
this.db.pragma(`journal_mode = ${journalMode}`);
|
|
28
38
|
// Force synchronous writes so other processes see changes immediately
|
|
29
39
|
this.db.pragma('synchronous = NORMAL');
|
|
30
|
-
//
|
|
31
|
-
|
|
40
|
+
// Autocheckpoint trades latency for WAL growth; 1-page checkpoints are extremely slow on
|
|
41
|
+
// higher-latency disks (common in Kubernetes PVs). Default to SQLite-ish behavior (1000 pages),
|
|
42
|
+
// but allow override for niche correctness/debugging needs.
|
|
43
|
+
const walAutocheckpointPages = (() => {
|
|
44
|
+
const raw = process.env.ZEROSHOT_SQLITE_WAL_AUTOCHECKPOINT_PAGES;
|
|
45
|
+
if (!raw) return 1000;
|
|
46
|
+
const value = Number(raw);
|
|
47
|
+
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 1000;
|
|
48
|
+
})();
|
|
49
|
+
this.db.pragma(`wal_autocheckpoint = ${walAutocheckpointPages}`);
|
|
32
50
|
|
|
33
51
|
// Create messages table
|
|
34
52
|
this.db.exec(`
|
|
@@ -52,6 +70,7 @@ class Ledger extends EventEmitter {
|
|
|
52
70
|
`);
|
|
53
71
|
|
|
54
72
|
this._prepareStatements();
|
|
73
|
+
this._loadLastTimestamp();
|
|
55
74
|
}
|
|
56
75
|
|
|
57
76
|
_prepareStatements() {
|
|
@@ -69,6 +88,13 @@ class Ledger extends EventEmitter {
|
|
|
69
88
|
};
|
|
70
89
|
}
|
|
71
90
|
|
|
91
|
+
_loadLastTimestamp() {
|
|
92
|
+
const row = this.db.prepare('SELECT MAX(timestamp) AS max_timestamp FROM messages').get();
|
|
93
|
+
if (row && Number.isFinite(row.max_timestamp)) {
|
|
94
|
+
this._lastTimestamp = row.max_timestamp;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
/**
|
|
73
99
|
* Append a message to the ledger
|
|
74
100
|
* @param {Object} message - Message object
|
|
@@ -83,7 +109,10 @@ class Ledger extends EventEmitter {
|
|
|
83
109
|
}
|
|
84
110
|
|
|
85
111
|
const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
|
|
86
|
-
const
|
|
112
|
+
const baseTimestamp = Math.max(Date.now(), this._lastTimestamp + 1);
|
|
113
|
+
const requestedTimestamp = typeof message.timestamp === 'number' ? message.timestamp : null;
|
|
114
|
+
const timestamp =
|
|
115
|
+
requestedTimestamp !== null ? Math.max(requestedTimestamp, baseTimestamp) : baseTimestamp;
|
|
87
116
|
|
|
88
117
|
const record = {
|
|
89
118
|
id,
|
|
@@ -113,6 +142,8 @@ class Ledger extends EventEmitter {
|
|
|
113
142
|
// Invalidate cache
|
|
114
143
|
this.cache.clear();
|
|
115
144
|
|
|
145
|
+
this._lastTimestamp = Math.max(this._lastTimestamp, timestamp);
|
|
146
|
+
|
|
116
147
|
// Emit event for subscriptions
|
|
117
148
|
const fullMessage = this._deserializeMessage(record);
|
|
118
149
|
this.emit('message', fullMessage);
|
|
@@ -149,13 +180,13 @@ class Ledger extends EventEmitter {
|
|
|
149
180
|
// Create transaction function - all inserts happen atomically
|
|
150
181
|
const insertMany = this.db.transaction((msgs) => {
|
|
151
182
|
const results = [];
|
|
152
|
-
const baseTimestamp = Date.now();
|
|
183
|
+
const baseTimestamp = Math.max(Date.now(), this._lastTimestamp + 1);
|
|
153
184
|
|
|
154
185
|
for (let i = 0; i < msgs.length; i++) {
|
|
155
186
|
const message = msgs[i];
|
|
156
187
|
const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
|
|
157
188
|
// Use incrementing timestamps to preserve order within batch
|
|
158
|
-
const timestamp =
|
|
189
|
+
const timestamp = baseTimestamp + i;
|
|
159
190
|
|
|
160
191
|
const record = {
|
|
161
192
|
id,
|
|
@@ -184,16 +215,18 @@ class Ledger extends EventEmitter {
|
|
|
184
215
|
results.push(this._deserializeMessage(record));
|
|
185
216
|
}
|
|
186
217
|
|
|
187
|
-
return results;
|
|
218
|
+
return { results, baseTimestamp };
|
|
188
219
|
});
|
|
189
220
|
|
|
190
221
|
try {
|
|
191
222
|
// Execute transaction (atomic - all or nothing)
|
|
192
|
-
const appendedMessages = insertMany(messages);
|
|
223
|
+
const { results: appendedMessages, baseTimestamp } = insertMany(messages);
|
|
193
224
|
|
|
194
225
|
// Invalidate cache
|
|
195
226
|
this.cache.clear();
|
|
196
227
|
|
|
228
|
+
this._lastTimestamp = Math.max(this._lastTimestamp, baseTimestamp + messages.length - 1);
|
|
229
|
+
|
|
197
230
|
// Emit events for subscriptions AFTER transaction commits
|
|
198
231
|
// This ensures listeners see consistent state
|
|
199
232
|
for (const fullMessage of appendedMessages) {
|
|
@@ -248,7 +281,13 @@ class Ledger extends EventEmitter {
|
|
|
248
281
|
params.push(typeof until === 'number' ? until : new Date(until).getTime());
|
|
249
282
|
}
|
|
250
283
|
|
|
251
|
-
|
|
284
|
+
// Defend against prototype pollution affecting default query ordering.
|
|
285
|
+
// Only treat `criteria.order` as set if it's an own property.
|
|
286
|
+
const orderValue = Object.prototype.hasOwnProperty.call(criteria, 'order')
|
|
287
|
+
? criteria.order
|
|
288
|
+
: undefined;
|
|
289
|
+
const direction = String(orderValue ?? 'asc').toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
290
|
+
let sql = `SELECT * FROM messages WHERE ${conditions.join(' AND ')} ORDER BY timestamp ${direction}`;
|
|
252
291
|
|
|
253
292
|
if (limit) {
|
|
254
293
|
sql += ` LIMIT ?`;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe subprocess execution with mandatory timeouts.
|
|
3
|
+
*
|
|
4
|
+
* NEVER use child_process.exec() or execSync() directly.
|
|
5
|
+
* These wrappers enforce timeouts to prevent infinite hangs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { exec: nodeExec, execSync: nodeExecSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
/** Default timeout: 30 seconds */
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Execute command with mandatory timeout.
|
|
15
|
+
* Supports both Promise and callback styles for gradual migration.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} command - Command to execute
|
|
18
|
+
* @param {object} [options] - Options (timeout uses default if not specified)
|
|
19
|
+
* @param {function} [callback] - Optional callback(error, stdout, stderr)
|
|
20
|
+
* @returns {Promise<{stdout: string, stderr: string}>|void} Promise if no callback, void if callback
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // Promise style (preferred)
|
|
24
|
+
* const { stdout } = await exec('ls -la', { timeout: 5000 });
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* // Callback style (for legacy code migration)
|
|
28
|
+
* exec('ls -la', { timeout: 5000 }, (error, stdout) => { ... });
|
|
29
|
+
*/
|
|
30
|
+
function exec(command, optionsOrCallback = {}, callbackArg = null) {
|
|
31
|
+
// Handle overloaded signature: exec(cmd, callback)
|
|
32
|
+
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : callbackArg;
|
|
33
|
+
const options = typeof optionsOrCallback === 'function' ? {} : optionsOrCallback;
|
|
34
|
+
|
|
35
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
36
|
+
|
|
37
|
+
if (timeout <= 0) {
|
|
38
|
+
const err = new Error('exec() timeout must be > 0. Infinite waits are forbidden.');
|
|
39
|
+
if (callback) {
|
|
40
|
+
callback(err);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
return Promise.reject(err);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Callback style
|
|
47
|
+
if (callback) {
|
|
48
|
+
nodeExec(command, { ...options, timeout }, (error, stdout, stderr) => {
|
|
49
|
+
if (error && error.killed && error.signal === 'SIGTERM') {
|
|
50
|
+
error.message = `Command timed out after ${timeout}ms: ${command}`;
|
|
51
|
+
}
|
|
52
|
+
callback(error, stdout, stderr);
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Promise style
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
nodeExec(command, { ...options, timeout }, (error, stdout, stderr) => {
|
|
60
|
+
if (error) {
|
|
61
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
62
|
+
error.message = `Command timed out after ${timeout}ms: ${command}`;
|
|
63
|
+
}
|
|
64
|
+
reject(error);
|
|
65
|
+
} else {
|
|
66
|
+
resolve({ stdout, stderr });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Execute command with mandatory timeout (sync)
|
|
74
|
+
* @param {string} command - Command to execute
|
|
75
|
+
* @param {object} [options] - Options (timeout required or uses default)
|
|
76
|
+
* @returns {string} stdout
|
|
77
|
+
*/
|
|
78
|
+
function execSync(command, options = {}) {
|
|
79
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
80
|
+
|
|
81
|
+
if (timeout <= 0) {
|
|
82
|
+
throw new Error('execSync() timeout must be > 0. Infinite waits are forbidden.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return nodeExecSync(command, { ...options, timeout });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { exec, execSync, DEFAULT_TIMEOUT_MS };
|