@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.
Files changed (82) hide show
  1. package/README.md +94 -14
  2. package/cli/commands/providers.js +8 -9
  3. package/cli/index.js +3032 -2409
  4. package/cli/message-formatters-normal.js +28 -6
  5. package/cluster-templates/base-templates/debug-workflow.json +1 -1
  6. package/cluster-templates/base-templates/full-workflow.json +72 -188
  7. package/cluster-templates/base-templates/worker-validator.json +59 -3
  8. package/cluster-templates/conductor-bootstrap.json +4 -4
  9. package/lib/docker-config.js +8 -0
  10. package/lib/git-remote-utils.js +165 -0
  11. package/lib/id-detector.js +10 -7
  12. package/lib/provider-defaults.js +62 -0
  13. package/lib/provider-names.js +2 -1
  14. package/lib/settings/claude-auth.js +78 -0
  15. package/lib/settings.js +161 -63
  16. package/package.json +7 -2
  17. package/scripts/setup-merge-queue.sh +170 -0
  18. package/src/agent/agent-config.js +135 -82
  19. package/src/agent/agent-context-builder.js +297 -188
  20. package/src/agent/agent-hook-executor.js +310 -113
  21. package/src/agent/agent-lifecycle.js +385 -325
  22. package/src/agent/agent-stuck-detector.js +7 -7
  23. package/src/agent/agent-task-executor.js +824 -565
  24. package/src/agent/output-extraction.js +41 -24
  25. package/src/agent/output-reformatter.js +1 -1
  26. package/src/agent/schema-utils.js +108 -73
  27. package/src/agent-wrapper.js +10 -1
  28. package/src/agents/git-pusher-template.js +285 -0
  29. package/src/claude-task-runner.js +85 -34
  30. package/src/config-validator.js +922 -657
  31. package/src/input-helpers.js +65 -0
  32. package/src/isolation-manager.js +289 -199
  33. package/src/issue-providers/README.md +305 -0
  34. package/src/issue-providers/azure-devops-provider.js +273 -0
  35. package/src/issue-providers/base-provider.js +232 -0
  36. package/src/issue-providers/github-provider.js +179 -0
  37. package/src/issue-providers/gitlab-provider.js +241 -0
  38. package/src/issue-providers/index.js +196 -0
  39. package/src/issue-providers/jira-provider.js +239 -0
  40. package/src/ledger.js +22 -5
  41. package/src/lib/safe-exec.js +88 -0
  42. package/src/orchestrator.js +1107 -811
  43. package/src/preflight.js +313 -159
  44. package/src/process-metrics.js +98 -56
  45. package/src/providers/anthropic/cli-builder.js +53 -25
  46. package/src/providers/anthropic/index.js +72 -2
  47. package/src/providers/anthropic/output-parser.js +32 -14
  48. package/src/providers/base-provider.js +70 -0
  49. package/src/providers/capabilities.js +9 -0
  50. package/src/providers/google/output-parser.js +47 -38
  51. package/src/providers/index.js +19 -3
  52. package/src/providers/openai/cli-builder.js +14 -3
  53. package/src/providers/openai/index.js +1 -0
  54. package/src/providers/openai/output-parser.js +44 -30
  55. package/src/providers/opencode/cli-builder.js +42 -0
  56. package/src/providers/opencode/index.js +103 -0
  57. package/src/providers/opencode/models.js +55 -0
  58. package/src/providers/opencode/output-parser.js +122 -0
  59. package/src/schemas/sub-cluster.js +68 -39
  60. package/src/status-footer.js +94 -48
  61. package/src/sub-cluster-wrapper.js +76 -35
  62. package/src/template-resolver.js +12 -9
  63. package/src/tui/data-poller.js +123 -99
  64. package/src/tui/formatters.js +4 -3
  65. package/src/tui/keybindings.js +259 -318
  66. package/src/tui/renderer.js +11 -21
  67. package/task-lib/attachable-watcher.js +118 -81
  68. package/task-lib/claude-recovery.js +61 -24
  69. package/task-lib/commands/episodes.js +105 -0
  70. package/task-lib/commands/list.js +2 -2
  71. package/task-lib/commands/logs.js +231 -189
  72. package/task-lib/commands/schedules.js +111 -61
  73. package/task-lib/config.js +0 -2
  74. package/task-lib/runner.js +84 -27
  75. package/task-lib/scheduler.js +1 -1
  76. package/task-lib/store.js +467 -168
  77. package/task-lib/tui/formatters.js +94 -45
  78. package/task-lib/tui/renderer.js +13 -3
  79. package/task-lib/tui.js +73 -32
  80. package/task-lib/watcher.js +128 -90
  81. package/src/agents/git-pusher-agent.json +0 -20
  82. 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
- const trimmed = line.trim().replace(/\r$/, '');
30
+ let trimmed = line.trim().replace(/\r$/, '');
31
31
  if (!trimmed) return '';
32
- const match = trimmed.match(/^\[(\d{13})\](.*)$/);
33
- return match ? match[2] : trimmed;
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
- // Must be type:result WITH actual content
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
- if (propSchema.enum && typeof result[key] === 'string') {
29
- let value = result[key].trim().toUpperCase();
30
-
31
- // DETECT: Model copied the enum list instead of choosing (e.g., "TRIVIAL|SIMPLE|STANDARD")
32
- if (value.includes('|')) {
33
- const parts = value.split('|').map((p) => p.trim());
34
- // Check if this looks like the enum list was copied verbatim
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
- if (propSchema.type === 'object' && propSchema.properties && result[key]) {
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
  };
@@ -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
- return buildContext({
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
+ };