@covibes/zeroshot 5.3.0 → 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/README.md +94 -14
- package/cli/commands/providers.js +8 -9
- package/cli/index.js +3032 -2409
- package/cli/message-formatters-normal.js +28 -6
- package/cluster-templates/base-templates/debug-workflow.json +1 -1
- package/cluster-templates/base-templates/full-workflow.json +72 -188
- package/cluster-templates/base-templates/worker-validator.json +59 -3
- package/cluster-templates/conductor-bootstrap.json +4 -4
- package/lib/docker-config.js +8 -0
- 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-names.js +2 -1
- package/lib/settings/claude-auth.js +78 -0
- package/lib/settings.js +161 -63
- package/package.json +7 -2
- package/scripts/setup-merge-queue.sh +170 -0
- package/src/agent/agent-config.js +135 -82
- package/src/agent/agent-context-builder.js +297 -188
- package/src/agent/agent-hook-executor.js +310 -113
- package/src/agent/agent-lifecycle.js +385 -325
- package/src/agent/agent-stuck-detector.js +7 -7
- package/src/agent/agent-task-executor.js +824 -565
- package/src/agent/output-extraction.js +41 -24
- package/src/agent/output-reformatter.js +1 -1
- package/src/agent/schema-utils.js +108 -73
- package/src/agent-wrapper.js +10 -1
- package/src/agents/git-pusher-template.js +285 -0
- package/src/claude-task-runner.js +85 -34
- package/src/config-validator.js +922 -657
- package/src/input-helpers.js +65 -0
- package/src/isolation-manager.js +289 -199
- 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 +22 -5
- package/src/lib/safe-exec.js +88 -0
- package/src/orchestrator.js +1107 -811
- package/src/preflight.js +313 -159
- package/src/process-metrics.js +98 -56
- package/src/providers/anthropic/cli-builder.js +53 -25
- package/src/providers/anthropic/index.js +72 -2
- package/src/providers/anthropic/output-parser.js +32 -14
- package/src/providers/base-provider.js +70 -0
- package/src/providers/capabilities.js +9 -0
- package/src/providers/google/output-parser.js +47 -38
- package/src/providers/index.js +19 -3
- package/src/providers/openai/cli-builder.js +14 -3
- package/src/providers/openai/index.js +1 -0
- package/src/providers/openai/output-parser.js +44 -30
- 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 +76 -35
- 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/renderer.js +11 -21
- package/task-lib/attachable-watcher.js +118 -81
- package/task-lib/claude-recovery.js +61 -24
- package/task-lib/commands/episodes.js +105 -0
- package/task-lib/commands/list.js +2 -2
- package/task-lib/commands/logs.js +231 -189
- package/task-lib/commands/schedules.js +111 -61
- package/task-lib/config.js +0 -2
- package/task-lib/runner.js +84 -27
- package/task-lib/scheduler.js +1 -1
- package/task-lib/store.js +467 -168
- 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 +128 -90
- package/src/agents/git-pusher-agent.json +0 -20
- package/src/github.js +0 -139
|
@@ -27,10 +27,24 @@ const { getProvider, parseChunkWithProvider } = require('../providers');
|
|
|
27
27
|
*/
|
|
28
28
|
function stripTimestamp(line) {
|
|
29
29
|
if (!line || typeof line !== 'string') return '';
|
|
30
|
-
|
|
30
|
+
let trimmed = line.trim().replace(/\r$/, '');
|
|
31
31
|
if (!trimmed) return '';
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
|
|
33
|
+
const tsMatch = trimmed.match(/^\[(\d{13})\](.*)$/);
|
|
34
|
+
if (tsMatch) trimmed = (tsMatch[2] || '').trimStart();
|
|
35
|
+
|
|
36
|
+
// In cluster logs, lines are often prefixed like:
|
|
37
|
+
// "validator | {json...}"
|
|
38
|
+
// Strip the "<agent> | " prefix so we can JSON.parse the event line.
|
|
39
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
40
|
+
const pipeMatch = trimmed.match(/^[^|]{1,40}\|\s*(.*)$/);
|
|
41
|
+
if (pipeMatch) {
|
|
42
|
+
const afterPipe = (pipeMatch[1] || '').trimStart();
|
|
43
|
+
if (afterPipe.startsWith('{') || afterPipe.startsWith('[')) return afterPipe;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return trimmed;
|
|
34
48
|
}
|
|
35
49
|
|
|
36
50
|
/**
|
|
@@ -49,27 +63,8 @@ function extractFromResultWrapper(output) {
|
|
|
49
63
|
|
|
50
64
|
try {
|
|
51
65
|
const obj = JSON.parse(content);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (obj.type !== 'result') continue;
|
|
55
|
-
|
|
56
|
-
// Check structured_output first (standard CLI format)
|
|
57
|
-
if (obj.structured_output && typeof obj.structured_output === 'object') {
|
|
58
|
-
return obj.structured_output;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Check result field - can be object or string
|
|
62
|
-
if (obj.result) {
|
|
63
|
-
if (typeof obj.result === 'object') {
|
|
64
|
-
return obj.result;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Result is string - might contain markdown-wrapped JSON
|
|
68
|
-
if (typeof obj.result === 'string') {
|
|
69
|
-
const extracted = extractFromMarkdown(obj.result) || extractDirectJson(obj.result);
|
|
70
|
-
if (extracted) return extracted;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
66
|
+
const extracted = extractResultContent(obj);
|
|
67
|
+
if (extracted) return extracted;
|
|
73
68
|
} catch {
|
|
74
69
|
// Not valid JSON, continue to next line
|
|
75
70
|
}
|
|
@@ -78,6 +73,28 @@ function extractFromResultWrapper(output) {
|
|
|
78
73
|
return null;
|
|
79
74
|
}
|
|
80
75
|
|
|
76
|
+
function extractResultContent(obj) {
|
|
77
|
+
// Must be type:result WITH actual content
|
|
78
|
+
if (obj?.type !== 'result') return null;
|
|
79
|
+
|
|
80
|
+
// Check structured_output first (standard CLI format)
|
|
81
|
+
if (obj.structured_output && typeof obj.structured_output === 'object') {
|
|
82
|
+
return obj.structured_output;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check result field - can be object or string
|
|
86
|
+
if (!obj.result) return null;
|
|
87
|
+
|
|
88
|
+
if (typeof obj.result === 'object') {
|
|
89
|
+
return obj.result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof obj.result !== 'string') return null;
|
|
93
|
+
|
|
94
|
+
// Result is string - might contain markdown-wrapped JSON
|
|
95
|
+
return extractFromMarkdown(obj.result) || extractDirectJson(obj.result);
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
/**
|
|
82
99
|
* Strategy 2: Extract from accumulated text events
|
|
83
100
|
* Handles non-Claude providers where JSON is in text content
|
|
@@ -66,7 +66,7 @@ Fix this issue in your response.`;
|
|
|
66
66
|
* @param {Object} options
|
|
67
67
|
* @param {string} options.rawOutput - The non-JSON output to reformat
|
|
68
68
|
* @param {Object} options.schema - Target JSON schema
|
|
69
|
-
* @param {string} options.providerName - Provider name (claude, codex, gemini)
|
|
69
|
+
* @param {string} options.providerName - Provider name (claude, codex, gemini, opencode)
|
|
70
70
|
* @param {number} [options.maxAttempts=3] - Maximum reformatting attempts
|
|
71
71
|
* @param {Function} [options.onAttempt] - Callback for each attempt (attempt, error)
|
|
72
72
|
* @returns {Promise<Object>} The reformatted JSON object
|
|
@@ -7,6 +7,38 @@
|
|
|
7
7
|
* SOLUTION: Normalize enum values BEFORE validation. Provider-agnostic.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
const ENUM_VARIATIONS = {
|
|
11
|
+
// taskType variations
|
|
12
|
+
BUG: 'DEBUG',
|
|
13
|
+
FIX: 'DEBUG',
|
|
14
|
+
BUGFIX: 'DEBUG',
|
|
15
|
+
BUG_FIX: 'DEBUG',
|
|
16
|
+
INVESTIGATE: 'DEBUG',
|
|
17
|
+
TROUBLESHOOT: 'DEBUG',
|
|
18
|
+
IMPLEMENT: 'TASK',
|
|
19
|
+
BUILD: 'TASK',
|
|
20
|
+
CREATE: 'TASK',
|
|
21
|
+
ADD: 'TASK',
|
|
22
|
+
FEATURE: 'TASK',
|
|
23
|
+
QUESTION: 'INQUIRY',
|
|
24
|
+
ASK: 'INQUIRY',
|
|
25
|
+
EXPLORE: 'INQUIRY',
|
|
26
|
+
RESEARCH: 'INQUIRY',
|
|
27
|
+
UNDERSTAND: 'INQUIRY',
|
|
28
|
+
// complexity variations
|
|
29
|
+
EASY: 'TRIVIAL',
|
|
30
|
+
BASIC: 'SIMPLE',
|
|
31
|
+
MINOR: 'SIMPLE',
|
|
32
|
+
MODERATE: 'STANDARD',
|
|
33
|
+
MEDIUM: 'STANDARD',
|
|
34
|
+
NORMAL: 'STANDARD',
|
|
35
|
+
HARD: 'STANDARD',
|
|
36
|
+
COMPLEX: 'CRITICAL',
|
|
37
|
+
RISKY: 'CRITICAL',
|
|
38
|
+
HIGH_RISK: 'CRITICAL',
|
|
39
|
+
DANGEROUS: 'CRITICAL',
|
|
40
|
+
};
|
|
41
|
+
|
|
10
42
|
/**
|
|
11
43
|
* Normalize enum values in parsed JSON to match schema definitions.
|
|
12
44
|
*
|
|
@@ -25,87 +57,90 @@ function normalizeEnumValues(result, schema) {
|
|
|
25
57
|
}
|
|
26
58
|
|
|
27
59
|
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const matchCount = parts.filter((p) => propSchema.enum.includes(p)).length;
|
|
36
|
-
if (matchCount >= 2) {
|
|
37
|
-
// Model copied the format - pick the first valid option and warn
|
|
38
|
-
const firstValid = parts.find((p) => propSchema.enum.includes(p));
|
|
39
|
-
if (firstValid) {
|
|
40
|
-
console.warn(
|
|
41
|
-
`⚠️ Model copied enum format instead of choosing. Field "${key}" had "${result[key]}", using "${firstValid}"`
|
|
42
|
-
);
|
|
43
|
-
value = firstValid;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Find exact match (case-insensitive)
|
|
49
|
-
const match = propSchema.enum.find((e) => e.toUpperCase() === value);
|
|
50
|
-
if (match) {
|
|
51
|
-
result[key] = match;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Common variations mapping
|
|
56
|
-
const variations = {
|
|
57
|
-
// taskType variations
|
|
58
|
-
BUG: 'DEBUG',
|
|
59
|
-
FIX: 'DEBUG',
|
|
60
|
-
BUGFIX: 'DEBUG',
|
|
61
|
-
BUG_FIX: 'DEBUG',
|
|
62
|
-
INVESTIGATE: 'DEBUG',
|
|
63
|
-
TROUBLESHOOT: 'DEBUG',
|
|
64
|
-
IMPLEMENT: 'TASK',
|
|
65
|
-
BUILD: 'TASK',
|
|
66
|
-
CREATE: 'TASK',
|
|
67
|
-
ADD: 'TASK',
|
|
68
|
-
FEATURE: 'TASK',
|
|
69
|
-
QUESTION: 'INQUIRY',
|
|
70
|
-
ASK: 'INQUIRY',
|
|
71
|
-
EXPLORE: 'INQUIRY',
|
|
72
|
-
RESEARCH: 'INQUIRY',
|
|
73
|
-
UNDERSTAND: 'INQUIRY',
|
|
74
|
-
// complexity variations
|
|
75
|
-
EASY: 'TRIVIAL',
|
|
76
|
-
BASIC: 'SIMPLE',
|
|
77
|
-
MINOR: 'SIMPLE',
|
|
78
|
-
MODERATE: 'STANDARD',
|
|
79
|
-
MEDIUM: 'STANDARD',
|
|
80
|
-
NORMAL: 'STANDARD',
|
|
81
|
-
HARD: 'STANDARD',
|
|
82
|
-
COMPLEX: 'CRITICAL',
|
|
83
|
-
RISKY: 'CRITICAL',
|
|
84
|
-
HIGH_RISK: 'CRITICAL',
|
|
85
|
-
DANGEROUS: 'CRITICAL',
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
if (variations[value] && propSchema.enum.includes(variations[value])) {
|
|
89
|
-
result[key] = variations[value];
|
|
90
|
-
}
|
|
60
|
+
const matched = normalizeEnumValue({
|
|
61
|
+
result,
|
|
62
|
+
key,
|
|
63
|
+
propSchema,
|
|
64
|
+
});
|
|
65
|
+
if (matched) {
|
|
66
|
+
continue;
|
|
91
67
|
}
|
|
92
68
|
|
|
93
69
|
// Recursively handle nested objects
|
|
94
|
-
|
|
95
|
-
normalizeEnumValues(result[key], propSchema);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Handle arrays of objects
|
|
99
|
-
if (propSchema.type === 'array' && propSchema.items?.properties && Array.isArray(result[key])) {
|
|
100
|
-
for (const item of result[key]) {
|
|
101
|
-
normalizeEnumValues(item, propSchema.items);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
70
|
+
normalizeNestedValues(result, propSchema, key);
|
|
104
71
|
}
|
|
105
72
|
|
|
106
73
|
return result;
|
|
107
74
|
}
|
|
108
75
|
|
|
76
|
+
function normalizeEnumValue({ result, key, propSchema }) {
|
|
77
|
+
if (!propSchema.enum || typeof result[key] !== 'string') {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const rawValue = result[key];
|
|
82
|
+
let value = rawValue.trim().toUpperCase();
|
|
83
|
+
value = normalizeEnumCopyValue(value, propSchema.enum, key, rawValue);
|
|
84
|
+
|
|
85
|
+
// Find exact match (case-insensitive)
|
|
86
|
+
const match = findEnumMatch(propSchema.enum, value);
|
|
87
|
+
if (match) {
|
|
88
|
+
result[key] = match;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Common variations mapping
|
|
93
|
+
const variation = ENUM_VARIATIONS[value];
|
|
94
|
+
if (variation && propSchema.enum.includes(variation)) {
|
|
95
|
+
result[key] = variation;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeEnumCopyValue(value, enumValues, key, rawValue) {
|
|
102
|
+
// DETECT: Model copied the enum list instead of choosing (e.g., "TRIVIAL|SIMPLE|STANDARD")
|
|
103
|
+
if (!value.includes('|')) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parts = value.split('|').map((p) => p.trim());
|
|
108
|
+
// Check if this looks like the enum list was copied verbatim
|
|
109
|
+
const matchCount = parts.filter((p) => enumValues.includes(p)).length;
|
|
110
|
+
if (matchCount < 2) {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Model copied the format - pick the first valid option and warn
|
|
115
|
+
const firstValid = parts.find((p) => enumValues.includes(p));
|
|
116
|
+
if (!firstValid) {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.warn(
|
|
121
|
+
`⚠️ Model copied enum format instead of choosing. Field "${key}" had "${rawValue}", using "${firstValid}"`
|
|
122
|
+
);
|
|
123
|
+
return firstValid;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findEnumMatch(enumValues, value) {
|
|
127
|
+
return enumValues.find((entry) => entry.toUpperCase() === value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeNestedValues(result, propSchema, key) {
|
|
131
|
+
// Recursively handle nested objects
|
|
132
|
+
if (propSchema.type === 'object' && propSchema.properties && result[key]) {
|
|
133
|
+
normalizeEnumValues(result[key], propSchema);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle arrays of objects
|
|
137
|
+
if (propSchema.type === 'array' && propSchema.items?.properties && Array.isArray(result[key])) {
|
|
138
|
+
for (const item of result[key]) {
|
|
139
|
+
normalizeEnumValues(item, propSchema.items);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
109
144
|
module.exports = {
|
|
110
145
|
normalizeEnumValues,
|
|
111
146
|
};
|
package/src/agent-wrapper.js
CHANGED
|
@@ -71,6 +71,8 @@ class AgentWrapper {
|
|
|
71
71
|
this.unsubscribe = null;
|
|
72
72
|
/** @type {number | null} */
|
|
73
73
|
this.lastTaskEndTime = null; // Track when last task completed (for context filtering)
|
|
74
|
+
/** @type {number | null} */
|
|
75
|
+
this.lastAgentStartTime = null; // Track when agent last began executing (for context filtering)
|
|
74
76
|
|
|
75
77
|
// LIVENESS DETECTION - Track output freshness to detect stuck agents
|
|
76
78
|
/** @type {number | null} */
|
|
@@ -404,7 +406,8 @@ class AgentWrapper {
|
|
|
404
406
|
* @private
|
|
405
407
|
*/
|
|
406
408
|
_buildContext(triggeringMessage) {
|
|
407
|
-
|
|
409
|
+
const previousAgentStart = this.lastAgentStartTime;
|
|
410
|
+
const context = buildContext({
|
|
408
411
|
id: this.id,
|
|
409
412
|
role: this.role,
|
|
410
413
|
iteration: this.iteration,
|
|
@@ -412,12 +415,18 @@ class AgentWrapper {
|
|
|
412
415
|
messageBus: this.messageBus,
|
|
413
416
|
cluster: this.cluster,
|
|
414
417
|
lastTaskEndTime: this.lastTaskEndTime,
|
|
418
|
+
lastAgentStartTime: previousAgentStart,
|
|
415
419
|
triggeringMessage,
|
|
416
420
|
selectedPrompt: this._selectPrompt(),
|
|
417
421
|
// Pass isolation state for conditional git restriction
|
|
418
422
|
worktree: this.worktree,
|
|
419
423
|
isolation: this.isolation,
|
|
420
424
|
});
|
|
425
|
+
|
|
426
|
+
// Record when this iteration started so future "since: last_agent_start" filters work
|
|
427
|
+
this.lastAgentStartTime = Date.now();
|
|
428
|
+
|
|
429
|
+
return context;
|
|
421
430
|
}
|
|
422
431
|
|
|
423
432
|
/**
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Pusher Agent Template
|
|
3
|
+
*
|
|
4
|
+
* Generates platform-specific git-pusher agent configurations.
|
|
5
|
+
* Eliminates duplication across github/gitlab/azure JSON files.
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth for:
|
|
8
|
+
* - Trigger logic (validation consensus detection)
|
|
9
|
+
* - Agent structure (id, role, modelLevel, output)
|
|
10
|
+
* - Prompt template with platform-specific commands
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shared trigger logic for detecting when all validators have approved.
|
|
15
|
+
* This is the SINGLE source of truth - no more duplicating across 3 JSON files.
|
|
16
|
+
*/
|
|
17
|
+
const SHARED_TRIGGER_SCRIPT = `const validators = cluster.getAgentsByRole('validator');
|
|
18
|
+
const lastPush = ledger.findLast({ topic: 'IMPLEMENTATION_READY' });
|
|
19
|
+
if (!lastPush) return false;
|
|
20
|
+
if (validators.length === 0) return true;
|
|
21
|
+
const results = ledger.query({ topic: 'VALIDATION_RESULT', since: lastPush.timestamp });
|
|
22
|
+
if (results.length < validators.length) return false;
|
|
23
|
+
const allApproved = results.every(r => r.content?.data?.approved === 'true' || r.content?.data?.approved === true);
|
|
24
|
+
if (!allApproved) return false;
|
|
25
|
+
const hasRealEvidence = results.every(r => {
|
|
26
|
+
const criteria = r.content?.data?.criteriaResults || [];
|
|
27
|
+
return criteria.every(c => {
|
|
28
|
+
return c.evidence?.command && typeof c.evidence?.exitCode === 'number' && c.evidence?.output?.length > 10;
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
return hasRealEvidence;`;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Platform-specific CLI commands and terminology
|
|
35
|
+
*/
|
|
36
|
+
const PLATFORM_CONFIGS = {
|
|
37
|
+
github: {
|
|
38
|
+
prName: 'PR',
|
|
39
|
+
prNameLower: 'pull request',
|
|
40
|
+
createCmd: 'gh pr create --title "feat: {{issue_title}}" --body "Closes #{{issue_number}}"',
|
|
41
|
+
mergeCmd: 'gh pr merge --merge --auto',
|
|
42
|
+
mergeFallbackCmd: 'gh pr merge --merge',
|
|
43
|
+
prUrlExample: 'https://github.com/owner/repo/pull/123',
|
|
44
|
+
outputFields: { urlField: 'pr_url', numberField: 'pr_number', mergedField: 'merged' },
|
|
45
|
+
},
|
|
46
|
+
gitlab: {
|
|
47
|
+
prName: 'MR',
|
|
48
|
+
prNameLower: 'merge request',
|
|
49
|
+
createCmd:
|
|
50
|
+
'glab mr create --title "feat: {{issue_title}}" --description "Closes #{{issue_number}}"',
|
|
51
|
+
mergeCmd: 'glab mr merge --auto-merge',
|
|
52
|
+
mergeFallbackCmd: 'glab mr merge',
|
|
53
|
+
prUrlExample: 'https://gitlab.com/owner/repo/-/merge_requests/123',
|
|
54
|
+
outputFields: { urlField: 'mr_url', numberField: 'mr_number', mergedField: 'merged' },
|
|
55
|
+
},
|
|
56
|
+
'azure-devops': {
|
|
57
|
+
prName: 'PR',
|
|
58
|
+
prNameLower: 'pull request',
|
|
59
|
+
createCmd:
|
|
60
|
+
'az repos pr create --title "feat: {{issue_title}}" --description "Closes #{{issue_number}}"',
|
|
61
|
+
mergeCmd: 'az repos pr update --id <PR_ID> --auto-complete true',
|
|
62
|
+
mergeFallbackCmd: 'az repos pr update --id <PR_ID> --status completed',
|
|
63
|
+
prUrlExample: 'https://dev.azure.com/org/project/_git/repo/pullrequest/123',
|
|
64
|
+
outputFields: {
|
|
65
|
+
urlField: 'pr_url',
|
|
66
|
+
numberField: 'pr_number',
|
|
67
|
+
mergedField: 'merged',
|
|
68
|
+
autoCompleteField: 'auto_complete',
|
|
69
|
+
},
|
|
70
|
+
// Azure requires extracting PR ID from create output
|
|
71
|
+
requiresPrIdExtraction: true,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate the prompt for a specific platform
|
|
77
|
+
* @param {Object} config - Platform configuration from PLATFORM_CONFIGS
|
|
78
|
+
* @returns {string} The complete prompt with platform-specific commands
|
|
79
|
+
*/
|
|
80
|
+
function generatePrompt(config) {
|
|
81
|
+
const {
|
|
82
|
+
prName,
|
|
83
|
+
prNameLower,
|
|
84
|
+
createCmd,
|
|
85
|
+
mergeCmd,
|
|
86
|
+
mergeFallbackCmd,
|
|
87
|
+
prUrlExample,
|
|
88
|
+
outputFields,
|
|
89
|
+
requiresPrIdExtraction,
|
|
90
|
+
} = config;
|
|
91
|
+
|
|
92
|
+
// Azure-specific instructions for PR ID extraction
|
|
93
|
+
const azurePrIdNote = requiresPrIdExtraction
|
|
94
|
+
? `\n\n💡 IMPORTANT: The output will contain the PR ID. You MUST extract it for the next step.
|
|
95
|
+
Look for output like: "Created PR 123" or parse the URL for the PR number.
|
|
96
|
+
Save the PR ID to a variable for step 6.`
|
|
97
|
+
: '';
|
|
98
|
+
|
|
99
|
+
// Azure uses different merge terminology
|
|
100
|
+
const mergeDescription = requiresPrIdExtraction
|
|
101
|
+
? 'SET AUTO-COMPLETE (MANDATORY - THIS IS NOT OPTIONAL)'
|
|
102
|
+
: `MERGE THE ${prName} (MANDATORY - THIS IS NOT OPTIONAL)`;
|
|
103
|
+
|
|
104
|
+
const mergeExplanation = requiresPrIdExtraction
|
|
105
|
+
? `Replace <PR_ID> with the actual PR number from step 5.
|
|
106
|
+
This enables auto-complete (auto-merge when CI passes).
|
|
107
|
+
|
|
108
|
+
If auto-complete is not available or you need to merge immediately:`
|
|
109
|
+
: `This sets auto-merge. If it fails (e.g., no auto-merge enabled), try:`;
|
|
110
|
+
|
|
111
|
+
const postMergeStatus = requiresPrIdExtraction
|
|
112
|
+
? 'PR IS CREATED AND AUTO-COMPLETE IS SET'
|
|
113
|
+
: `${prName} IS MERGED`;
|
|
114
|
+
|
|
115
|
+
const finalOutputNote = requiresPrIdExtraction
|
|
116
|
+
? `ONLY after the PR is created and auto-complete is set, output:
|
|
117
|
+
\`\`\`json
|
|
118
|
+
{"${outputFields.urlField}": "${prUrlExample}", "${outputFields.numberField}": 123, "merged": false, "auto_complete": true}
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
If truly no changes exist, output:
|
|
122
|
+
\`\`\`json
|
|
123
|
+
{"${outputFields.urlField}": null, "${outputFields.numberField}": null, "merged": false, "auto_complete": false}
|
|
124
|
+
\`\`\``
|
|
125
|
+
: `ONLY after the ${prName} is MERGED, output:
|
|
126
|
+
\`\`\`json
|
|
127
|
+
{"${outputFields.urlField}": "${prUrlExample}", "${outputFields.numberField}": 123, "merged": true}
|
|
128
|
+
\`\`\`
|
|
129
|
+
|
|
130
|
+
If truly no changes exist, output:
|
|
131
|
+
\`\`\`json
|
|
132
|
+
{"${outputFields.urlField}": null, "${outputFields.numberField}": null, "merged": false}
|
|
133
|
+
\`\`\``;
|
|
134
|
+
|
|
135
|
+
return `🚨 CRITICAL: ALL VALIDATORS APPROVED. YOU MUST CREATE A ${prName} AND GET IT MERGED. DO NOT STOP UNTIL THE ${prName} IS MERGED. 🚨
|
|
136
|
+
|
|
137
|
+
## MANDATORY STEPS - EXECUTE EACH ONE IN ORDER - DO NOT SKIP ANY STEP
|
|
138
|
+
|
|
139
|
+
### STEP 1: Stage ALL changes (MANDATORY)
|
|
140
|
+
\`\`\`bash
|
|
141
|
+
git add -A
|
|
142
|
+
\`\`\`
|
|
143
|
+
Run this command. Do not skip it.
|
|
144
|
+
|
|
145
|
+
### STEP 2: Check what's staged
|
|
146
|
+
\`\`\`bash
|
|
147
|
+
git status
|
|
148
|
+
\`\`\`
|
|
149
|
+
Run this. If nothing to commit, output JSON with ${outputFields.urlField}: null and stop.
|
|
150
|
+
|
|
151
|
+
### STEP 3: Commit the changes (MANDATORY if there are changes)
|
|
152
|
+
\`\`\`bash
|
|
153
|
+
git commit -m "feat: implement #{{issue_number}} - {{issue_title}}"
|
|
154
|
+
\`\`\`
|
|
155
|
+
Run this command. Do not skip it.
|
|
156
|
+
|
|
157
|
+
### STEP 4: Push to origin (MANDATORY)
|
|
158
|
+
\`\`\`bash
|
|
159
|
+
git push -u origin HEAD
|
|
160
|
+
\`\`\`
|
|
161
|
+
Run this. If it fails, check the error and fix it.
|
|
162
|
+
|
|
163
|
+
⚠️ AFTER PUSH YOU ARE NOT DONE! CONTINUE TO STEP 5! ⚠️
|
|
164
|
+
|
|
165
|
+
### STEP 5: CREATE THE ${prName.toUpperCase()} (MANDATORY - YOU MUST RUN THIS COMMAND)
|
|
166
|
+
\`\`\`bash
|
|
167
|
+
${createCmd}
|
|
168
|
+
\`\`\`
|
|
169
|
+
🚨 YOU MUST RUN \`${createCmd.split(' ').slice(0, 3).join(' ')}\`! Outputting a link is NOT creating a ${prName}! 🚨
|
|
170
|
+
The push output shows a "Create a ${prNameLower}" link - IGNORE IT.
|
|
171
|
+
You MUST run the \`${createCmd.split(' ').slice(0, 3).join(' ')}\` command above.${requiresPrIdExtraction ? '' : ` Save the actual ${prName} URL from the output.`}${azurePrIdNote}
|
|
172
|
+
|
|
173
|
+
⚠️ AFTER ${prName} CREATION YOU ARE NOT DONE! CONTINUE TO STEP 6! ⚠️
|
|
174
|
+
|
|
175
|
+
### STEP 6: ${mergeDescription}
|
|
176
|
+
\`\`\`bash
|
|
177
|
+
${mergeCmd}
|
|
178
|
+
\`\`\`
|
|
179
|
+
${mergeExplanation}
|
|
180
|
+
\`\`\`bash
|
|
181
|
+
${mergeFallbackCmd}
|
|
182
|
+
\`\`\`
|
|
183
|
+
|
|
184
|
+
🚨 IF MERGE FAILS DUE TO CONFLICTS - YOU MUST RESOLVE THEM:
|
|
185
|
+
a) Pull latest main and rebase:
|
|
186
|
+
\`\`\`bash
|
|
187
|
+
git fetch origin main
|
|
188
|
+
git rebase origin/main
|
|
189
|
+
\`\`\`
|
|
190
|
+
b) If conflicts appear - RESOLVE THEM IMMEDIATELY:
|
|
191
|
+
- Read the conflicting files
|
|
192
|
+
- Make intelligent decisions about what code to keep
|
|
193
|
+
- Edit the files to resolve conflicts
|
|
194
|
+
- \`git add <resolved-files>\`
|
|
195
|
+
- \`git rebase --continue\`
|
|
196
|
+
c) Force push the resolved branch:
|
|
197
|
+
\`\`\`bash
|
|
198
|
+
git push --force-with-lease
|
|
199
|
+
\`\`\`
|
|
200
|
+
d) Retry merge:
|
|
201
|
+
\`\`\`bash
|
|
202
|
+
${mergeFallbackCmd}
|
|
203
|
+
\`\`\`
|
|
204
|
+
|
|
205
|
+
REPEAT UNTIL MERGED. DO NOT GIVE UP. DO NOT SKIP. THE ${prName} MUST BE ${requiresPrIdExtraction ? 'SET TO AUTO-COMPLETE' : 'MERGED'}.
|
|
206
|
+
If merge is blocked by CI, wait and retry. ${requiresPrIdExtraction ? 'The auto-complete will merge when CI passes.' : 'If blocked by reviews, set auto-merge.'}
|
|
207
|
+
|
|
208
|
+
## CRITICAL RULES
|
|
209
|
+
- Execute EVERY step in order (1, 2, 3, 4, 5, 6)
|
|
210
|
+
- Do NOT skip git add -A
|
|
211
|
+
- Do NOT skip git commit
|
|
212
|
+
- Do NOT skip ${createCmd.split(' ').slice(0, 3).join(' ')} - THE TASK IS NOT DONE UNTIL ${prName} EXISTS
|
|
213
|
+
- Do NOT skip ${mergeCmd.split(' ').slice(0, 4).join(' ')} - THE TASK IS NOT DONE UNTIL ${postMergeStatus}${requiresPrIdExtraction ? '\n- MUST extract PR ID from step 5 output to use in step 6' : ''}
|
|
214
|
+
- If push fails, debug and fix it
|
|
215
|
+
- If ${prName} creation fails, debug and fix it
|
|
216
|
+
- If ${requiresPrIdExtraction ? 'auto-complete' : 'merge'} fails, debug and fix it
|
|
217
|
+
- DO NOT OUTPUT JSON UNTIL ${postMergeStatus}
|
|
218
|
+
- A link from git push is NOT a ${prName} - you must run ${createCmd.split(' ').slice(0, 3).join(' ')}
|
|
219
|
+
|
|
220
|
+
## Final Output
|
|
221
|
+
${finalOutputNote}`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Generate a git-pusher agent configuration for a specific platform
|
|
226
|
+
*
|
|
227
|
+
* @param {string} platform - Platform ID ('github', 'gitlab', 'azure-devops')
|
|
228
|
+
* @returns {Object} Agent configuration object
|
|
229
|
+
* @throws {Error} If platform is not supported
|
|
230
|
+
*/
|
|
231
|
+
function generateGitPusherAgent(platform) {
|
|
232
|
+
const config = PLATFORM_CONFIGS[platform];
|
|
233
|
+
|
|
234
|
+
if (!config) {
|
|
235
|
+
const supported = Object.keys(PLATFORM_CONFIGS).join(', ');
|
|
236
|
+
throw new Error(`Unsupported platform '${platform}'. Supported: ${supported}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
id: 'git-pusher',
|
|
241
|
+
role: 'completion-detector',
|
|
242
|
+
modelLevel: 'level2',
|
|
243
|
+
triggers: [
|
|
244
|
+
{
|
|
245
|
+
topic: 'VALIDATION_RESULT',
|
|
246
|
+
logic: {
|
|
247
|
+
engine: 'javascript',
|
|
248
|
+
script: SHARED_TRIGGER_SCRIPT,
|
|
249
|
+
},
|
|
250
|
+
action: 'execute_task',
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
prompt: generatePrompt(config),
|
|
254
|
+
output: {
|
|
255
|
+
topic: 'PR_CREATED',
|
|
256
|
+
publishAfter: 'CLUSTER_COMPLETE',
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get list of supported platforms for git-pusher
|
|
263
|
+
* @returns {string[]} Array of platform IDs
|
|
264
|
+
*/
|
|
265
|
+
function getSupportedPlatforms() {
|
|
266
|
+
return Object.keys(PLATFORM_CONFIGS);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check if a platform supports git-pusher (PR/MR creation)
|
|
271
|
+
* @param {string} platform - Platform ID
|
|
272
|
+
* @returns {boolean}
|
|
273
|
+
*/
|
|
274
|
+
function isPlatformSupported(platform) {
|
|
275
|
+
return platform in PLATFORM_CONFIGS;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
generateGitPusherAgent,
|
|
280
|
+
getSupportedPlatforms,
|
|
281
|
+
isPlatformSupported,
|
|
282
|
+
// Export for testing
|
|
283
|
+
SHARED_TRIGGER_SCRIPT,
|
|
284
|
+
PLATFORM_CONFIGS,
|
|
285
|
+
};
|