@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
package/lib/settings.js CHANGED
@@ -13,6 +13,15 @@ const {
13
13
  normalizeProviderSettings,
14
14
  } = require('./provider-names');
15
15
 
16
+ // Lazy-loaded to avoid circular dependency (issue-providers → settings → issue-providers)
17
+ let _issueProviderFns = null;
18
+ function getIssueProviderFns() {
19
+ if (!_issueProviderFns) {
20
+ _issueProviderFns = require('../src/issue-providers');
21
+ }
22
+ return _issueProviderFns;
23
+ }
24
+
16
25
  /**
17
26
  * Get settings file path (dynamically reads env var for testing)
18
27
  * Using a getter ensures tests can override the path at runtime
@@ -24,6 +33,9 @@ function getSettingsFile() {
24
33
  );
25
34
  }
26
35
 
36
+ // Import provider defaults from separate module to avoid circular dependency
37
+ const { getProviderDefaults, clearProviderDefaultsCache } = require('./provider-defaults');
38
+
27
39
  /**
28
40
  * Model hierarchy for cost ceiling validation
29
41
  * Higher number = more expensive/capable model
@@ -80,30 +92,14 @@ function validateModelAgainstMax(requestedModel, maxModel, minModel = null) {
80
92
  return requestedModel;
81
93
  }
82
94
 
83
- // Default settings
84
- const DEFAULT_SETTINGS = {
95
+ // Default settings (base - without issue provider settings which are added dynamically)
96
+ const DEFAULT_SETTINGS_BASE = {
85
97
  maxModel: 'opus', // Cost ceiling - agents cannot use models above this
86
98
  minModel: null, // Cost floor - agents cannot use models below this (null = no minimum)
87
99
  defaultProvider: 'claude',
88
- providerSettings: {
89
- claude: {
90
- maxLevel: 'level3',
91
- minLevel: 'level1',
92
- defaultLevel: 'level2',
93
- levelOverrides: {},
94
- },
95
- codex: {
96
- maxLevel: 'level3',
97
- minLevel: 'level1',
98
- defaultLevel: 'level2',
99
- levelOverrides: {},
100
- },
101
- gemini: {
102
- maxLevel: 'level3',
103
- minLevel: 'level1',
104
- defaultLevel: 'level2',
105
- levelOverrides: {},
106
- },
100
+ get providerSettings() {
101
+ // Dynamically build from providers on first access
102
+ return getProviderDefaults();
107
103
  },
108
104
  defaultConfig: 'conductor-bootstrap',
109
105
  defaultDocker: false,
@@ -117,7 +113,7 @@ const DEFAULT_SETTINGS = {
117
113
  // Example: 'ccr code' for claude-code-router integration
118
114
  claudeCommand: 'claude',
119
115
  // Docker isolation mounts - preset names or {host, container, readonly?} objects
120
- // Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini
116
+ // Valid presets: gh, git, ssh, aws, azure, kube, terraform, gcloud, claude, codex, gemini, opencode
121
117
  dockerMounts: ['gh', 'git', 'ssh'],
122
118
  // Extra env vars to pass to Docker container (in addition to preset-implied ones)
123
119
  // Supports: VAR (if set), VAR_* (pattern), VAR=value (forced), VAR= (empty)
@@ -125,8 +121,57 @@ const DEFAULT_SETTINGS = {
125
121
  // Container home directory - where $HOME resolves in container paths
126
122
  // Default: /home/node (matches zeroshot-cluster-base image)
127
123
  dockerContainerHome: '/home/node',
124
+ // Issue provider settings - defaultIssueSource is here, others come from providers
125
+ defaultIssueSource: 'github', // 'github' | 'gitlab' | 'jira' | 'azure-devops'
128
126
  };
129
127
 
128
+ // Cache for merged defaults (base + issue provider settings)
129
+ let _defaultSettingsCache = null;
130
+
131
+ /**
132
+ * Get DEFAULT_SETTINGS with issue provider settings merged in
133
+ * Lazy-loaded to avoid circular dependency at module load time
134
+ */
135
+ function getDefaultSettings() {
136
+ if (!_defaultSettingsCache) {
137
+ const issueProviderDefaults = getIssueProviderFns().getIssueProviderSettingsDefaults();
138
+ _defaultSettingsCache = {
139
+ ...DEFAULT_SETTINGS_BASE,
140
+ ...issueProviderDefaults,
141
+ };
142
+ }
143
+ return _defaultSettingsCache;
144
+ }
145
+
146
+ // For backward compatibility, export DEFAULT_SETTINGS as a getter
147
+ const DEFAULT_SETTINGS = new Proxy(DEFAULT_SETTINGS_BASE, {
148
+ get(target, prop) {
149
+ // For known base properties, return from base
150
+ if (prop in target) {
151
+ return target[prop];
152
+ }
153
+ // For issue provider settings, get from merged defaults
154
+ const merged = getDefaultSettings();
155
+ return merged[prop];
156
+ },
157
+ has(target, prop) {
158
+ if (prop in target) return true;
159
+ const merged = getDefaultSettings();
160
+ return prop in merged;
161
+ },
162
+ ownKeys() {
163
+ const merged = getDefaultSettings();
164
+ return Object.keys(merged);
165
+ },
166
+ getOwnPropertyDescriptor(target, prop) {
167
+ const merged = getDefaultSettings();
168
+ if (prop in merged) {
169
+ return { enumerable: true, configurable: true, value: merged[prop] };
170
+ }
171
+ return undefined;
172
+ },
173
+ });
174
+
130
175
  function mapLegacyModelToLevel(model) {
131
176
  switch (model) {
132
177
  case 'haiku':
@@ -141,10 +186,14 @@ function mapLegacyModelToLevel(model) {
141
186
  }
142
187
 
143
188
  function mergeProviderSettings(current, overrides) {
144
- const merged = { ...current };
189
+ // Ensure current has all providers with their defaults
190
+ const providerDefaults = getProviderDefaults();
191
+ const merged = {};
192
+
145
193
  for (const provider of VALID_PROVIDERS) {
146
194
  merged[provider] = {
147
- ...current[provider],
195
+ ...(providerDefaults[provider] || {}),
196
+ ...(current[provider] || {}),
148
197
  ...(overrides?.[provider] || {}),
149
198
  };
150
199
  if (!merged[provider].levelOverrides) {
@@ -202,7 +251,11 @@ function normalizeLoadedSettings(parsed) {
202
251
  function loadSettings() {
203
252
  const settingsFile = getSettingsFile();
204
253
  if (!fs.existsSync(settingsFile)) {
205
- return { ...DEFAULT_SETTINGS };
254
+ // Return a copy with resolved providerSettings
255
+ return {
256
+ ...DEFAULT_SETTINGS,
257
+ providerSettings: getProviderDefaults(),
258
+ };
206
259
  }
207
260
  try {
208
261
  const data = fs.readFileSync(settingsFile, 'utf8');
@@ -210,14 +263,14 @@ function loadSettings() {
210
263
  const merged = { ...DEFAULT_SETTINGS, ...parsed };
211
264
  merged.defaultProvider =
212
265
  normalizeProviderName(merged.defaultProvider) || DEFAULT_SETTINGS.defaultProvider;
213
- merged.providerSettings = mergeProviderSettings(
214
- DEFAULT_SETTINGS.providerSettings,
215
- parsed.providerSettings
216
- );
266
+ merged.providerSettings = mergeProviderSettings(getProviderDefaults(), parsed.providerSettings);
217
267
  return applyLegacyModelBounds(merged);
218
268
  } catch {
219
269
  console.error('Warning: Could not load settings, using defaults');
220
- return { ...DEFAULT_SETTINGS };
270
+ return {
271
+ ...DEFAULT_SETTINGS,
272
+ providerSettings: getProviderDefaults(),
273
+ };
221
274
  }
222
275
  }
223
276
 
@@ -233,6 +286,51 @@ function saveSettings(settings) {
233
286
  fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2), 'utf8');
234
287
  }
235
288
 
289
+ /**
290
+ * Validate claudeCommand setting
291
+ * @returns {string|null} Error message if invalid, null if valid
292
+ */
293
+ function validateClaudeCommand(value) {
294
+ if (typeof value !== 'string') {
295
+ return 'claudeCommand must be a string';
296
+ }
297
+ if (value.trim().length === 0) {
298
+ return 'claudeCommand cannot be empty';
299
+ }
300
+ return null;
301
+ }
302
+
303
+ /**
304
+ * Validate providerSettings structure by delegating to provider implementations
305
+ * @returns {string|null} Error message if invalid, null if valid
306
+ */
307
+ function validateProviderSettings(value) {
308
+ const normalizedSettings = normalizeProviderSettings(value);
309
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
310
+ return 'providerSettings must be an object';
311
+ }
312
+
313
+ // Lazy require to avoid circular dependency
314
+ const { getProvider } = require('../src/providers');
315
+
316
+ for (const [providerName, settings] of Object.entries(normalizedSettings || {})) {
317
+ if (!VALID_PROVIDERS.includes(providerName)) {
318
+ return `Unknown provider in providerSettings: ${providerName}`;
319
+ }
320
+
321
+ // Delegate validation to the provider
322
+ try {
323
+ const provider = getProvider(providerName);
324
+ const error = provider.validateSettings(settings);
325
+ if (error) return error;
326
+ } catch (err) {
327
+ return `Failed to validate ${providerName} settings: ${err.message}`;
328
+ }
329
+ }
330
+
331
+ return null;
332
+ }
333
+
236
334
  /**
237
335
  * Validate a setting value
238
336
  * @returns {string|null} Error message if invalid, null if valid
@@ -255,12 +353,7 @@ function validateSetting(key, value) {
255
353
  }
256
354
 
257
355
  if (key === 'claudeCommand') {
258
- if (typeof value !== 'string') {
259
- return 'claudeCommand must be a string';
260
- }
261
- if (value.trim().length === 0) {
262
- return 'claudeCommand cannot be empty';
263
- }
356
+ return validateClaudeCommand(value);
264
357
  }
265
358
 
266
359
  if (key === 'defaultProvider') {
@@ -271,33 +364,7 @@ function validateSetting(key, value) {
271
364
  }
272
365
 
273
366
  if (key === 'providerSettings') {
274
- const normalizedSettings = normalizeProviderSettings(value);
275
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
276
- return 'providerSettings must be an object';
277
- }
278
- for (const [provider, settings] of Object.entries(normalizedSettings || {})) {
279
- if (!VALID_PROVIDERS.includes(provider)) {
280
- return `Unknown provider in providerSettings: ${provider}`;
281
- }
282
- if (typeof settings !== 'object' || settings === null) {
283
- return `providerSettings.${provider} must be an object`;
284
- }
285
- if (settings.maxLevel && !LEVEL_RANKS[settings.maxLevel]) {
286
- return `Invalid maxLevel for ${provider}: ${settings.maxLevel}`;
287
- }
288
- if (settings.minLevel && !LEVEL_RANKS[settings.minLevel]) {
289
- return `Invalid minLevel for ${provider}: ${settings.minLevel}`;
290
- }
291
- if (settings.defaultLevel && !LEVEL_RANKS[settings.defaultLevel]) {
292
- return `Invalid defaultLevel for ${provider}: ${settings.defaultLevel}`;
293
- }
294
- if (
295
- settings.levelOverrides &&
296
- (typeof settings.levelOverrides !== 'object' || Array.isArray(settings.levelOverrides))
297
- ) {
298
- return `levelOverrides for ${provider} must be an object`;
299
- }
300
- }
367
+ return validateProviderSettings(value);
301
368
  }
302
369
 
303
370
  if (key === 'dockerMounts') {
@@ -308,9 +375,38 @@ function validateSetting(key, value) {
308
375
  return validateEnvPassthrough(value);
309
376
  }
310
377
 
378
+ // Issue source settings validation (grouped for maintainability)
379
+ const issueSourceError = validateIssueSourceSetting(key, value);
380
+ if (issueSourceError !== undefined) {
381
+ return issueSourceError;
382
+ }
383
+
311
384
  return null;
312
385
  }
313
386
 
387
+ /**
388
+ * Validate issue source related settings
389
+ * Delegates to issue providers for provider-specific settings
390
+ * @param {string} key - Setting key
391
+ * @param {any} value - Setting value
392
+ * @returns {string|null|undefined} Error message, null if valid, undefined if not an issue source setting
393
+ */
394
+ function validateIssueSourceSetting(key, value) {
395
+ // defaultIssueSource is handled here (not provider-specific)
396
+ if (key === 'defaultIssueSource') {
397
+ const { listProviders } = getIssueProviderFns();
398
+ const validSources = listProviders();
399
+ if (!validSources.includes(value)) {
400
+ return `Invalid issue source: ${value}. Valid: ${validSources.join(', ')}`;
401
+ }
402
+ return null;
403
+ }
404
+
405
+ // Delegate to issue providers for provider-specific settings
406
+ const { validateIssueProviderSetting } = getIssueProviderFns();
407
+ return validateIssueProviderSetting(key, value);
408
+ }
409
+
314
410
  /**
315
411
  * Coerce value to correct type based on default value type
316
412
  */
@@ -398,6 +494,8 @@ module.exports = {
398
494
  MODEL_HIERARCHY,
399
495
  VALID_MODELS,
400
496
  validateModelAgainstMax,
497
+ // Provider defaults exports
498
+ clearProviderDefaultsCache,
401
499
  // Backward compatibility: SETTINGS_FILE as getter (reads env var dynamically)
402
500
  get SETTINGS_FILE() {
403
501
  return getSettingsFile();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@covibes/zeroshot",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -118,6 +118,8 @@
118
118
  "depcheck": "^1.4.7",
119
119
  "eslint": "^9.39.1",
120
120
  "eslint-config-prettier": "^10.1.8",
121
+ "eslint-plugin-security": "^3.0.1",
122
+ "eslint-plugin-sonarjs": "^3.0.5",
121
123
  "eslint-plugin-unused-imports": "^4.3.0",
122
124
  "husky": "^9.1.7",
123
125
  "jscpd": "^3.5.10",
@@ -131,7 +133,10 @@
131
133
  "unimported": "^1.31.0"
132
134
  },
133
135
  "overrides": {
134
- "xml2js": "^0.5.0"
136
+ "xml2js": "^0.5.0",
137
+ "diff": ">=8.0.3",
138
+ "tar": ">=7.5.3",
139
+ "undici": ">=7.18.2"
135
140
  },
136
141
  "lint-staged": {
137
142
  "*.{js,mjs,cjs}": [
@@ -0,0 +1,170 @@
1
+ #!/bin/bash
2
+ # Setup GitHub merge queue and branch protection for zeroshot
3
+ # Run once after creating the repo or to update settings
4
+
5
+ set -e
6
+
7
+ REPO="covibes/zeroshot"
8
+
9
+ echo "╔═══════════════════════════════════════════════════════════════════╗"
10
+ echo "║ Setting up merge queue for $REPO ║"
11
+ echo "╚═══════════════════════════════════════════════════════════════════╝"
12
+ echo ""
13
+
14
+ # Check gh is authenticated
15
+ if ! gh auth status &>/dev/null; then
16
+ echo "❌ ERROR: Not authenticated with GitHub CLI"
17
+ echo " Run: gh auth login"
18
+ exit 1
19
+ fi
20
+
21
+ # Check we have admin access
22
+ if ! gh api "repos/$REPO" --jq '.permissions.admin' | grep -q true; then
23
+ echo "❌ ERROR: You need admin access to $REPO"
24
+ exit 1
25
+ fi
26
+
27
+ echo "✓ Authenticated with admin access"
28
+ echo ""
29
+
30
+ # ============================================================================
31
+ # Configure 'dev' branch protection (merge target)
32
+ # ============================================================================
33
+
34
+ echo "→ Configuring 'dev' branch protection..."
35
+
36
+ gh api --method PUT "repos/$REPO/branches/dev/protection" \
37
+ --input - <<EOF
38
+ {
39
+ "required_status_checks": {
40
+ "strict": true,
41
+ "contexts": ["check", "install-matrix (ubuntu-latest, 20)", "install-matrix (macos-latest, 20)"]
42
+ },
43
+ "enforce_admins": false,
44
+ "required_pull_request_reviews": null,
45
+ "restrictions": null,
46
+ "allow_force_pushes": false,
47
+ "allow_deletions": false,
48
+ "required_linear_history": true,
49
+ "required_conversation_resolution": false
50
+ }
51
+ EOF
52
+
53
+ echo "✓ 'dev' branch protection configured"
54
+
55
+ # Enable merge queue for dev branch
56
+ echo "→ Enabling merge queue for 'dev' branch..."
57
+
58
+ # Note: Merge queue requires GitHub Enterprise or public repos with Actions
59
+ # Using the ruleset API which supports merge queue
60
+ gh api --method POST "repos/$REPO/rulesets" \
61
+ --input - <<EOF 2>/dev/null || echo " (ruleset may already exist)"
62
+ {
63
+ "name": "dev-merge-queue",
64
+ "target": "branch",
65
+ "enforcement": "active",
66
+ "conditions": {
67
+ "ref_name": {
68
+ "include": ["refs/heads/dev"],
69
+ "exclude": []
70
+ }
71
+ },
72
+ "rules": [
73
+ {
74
+ "type": "merge_queue",
75
+ "parameters": {
76
+ "check_response_timeout_minutes": 30,
77
+ "grouping_strategy": "ALLGREEN",
78
+ "max_entries_to_build": 5,
79
+ "max_entries_to_merge": 5,
80
+ "merge_method": "SQUASH",
81
+ "min_entries_to_merge": 1,
82
+ "min_entries_to_merge_wait_minutes": 1
83
+ }
84
+ }
85
+ ]
86
+ }
87
+ EOF
88
+
89
+ echo "✓ Merge queue enabled for 'dev'"
90
+
91
+ # ============================================================================
92
+ # Configure 'main' branch protection (release branch)
93
+ # ============================================================================
94
+
95
+ echo "→ Configuring 'main' branch protection..."
96
+
97
+ gh api --method PUT "repos/$REPO/branches/main/protection" \
98
+ --input - <<EOF
99
+ {
100
+ "required_status_checks": {
101
+ "strict": true,
102
+ "contexts": ["check", "install-matrix (ubuntu-latest, 20)", "install-matrix (macos-latest, 20)"]
103
+ },
104
+ "enforce_admins": false,
105
+ "required_pull_request_reviews": {
106
+ "required_approving_review_count": 1,
107
+ "dismiss_stale_reviews": true,
108
+ "require_code_owner_reviews": false
109
+ },
110
+ "restrictions": null,
111
+ "allow_force_pushes": false,
112
+ "allow_deletions": false,
113
+ "required_linear_history": true,
114
+ "required_conversation_resolution": false
115
+ }
116
+ EOF
117
+
118
+ echo "✓ 'main' branch protection configured"
119
+
120
+ # ============================================================================
121
+ # Configure repository settings
122
+ # ============================================================================
123
+
124
+ echo "→ Configuring repository settings..."
125
+
126
+ gh api --method PATCH "repos/$REPO" \
127
+ --input - <<EOF
128
+ {
129
+ "allow_squash_merge": true,
130
+ "allow_merge_commit": false,
131
+ "allow_rebase_merge": false,
132
+ "squash_merge_commit_title": "PR_TITLE",
133
+ "squash_merge_commit_message": "PR_BODY",
134
+ "delete_branch_on_merge": true,
135
+ "allow_auto_merge": true
136
+ }
137
+ EOF
138
+
139
+ echo "✓ Repository settings configured"
140
+
141
+ # ============================================================================
142
+ # Summary
143
+ # ============================================================================
144
+
145
+ echo ""
146
+ echo "╔═══════════════════════════════════════════════════════════════════╗"
147
+ echo "║ ✓ Merge queue setup complete! ║"
148
+ echo "╚═══════════════════════════════════════════════════════════════════╝"
149
+ echo ""
150
+ echo "Workflow:"
151
+ echo " feature-branch (local)"
152
+ echo " ↓"
153
+ echo " pre-push hook → lint + typecheck (~5s)"
154
+ echo " ↓"
155
+ echo " push to origin/feature-branch"
156
+ echo " ↓"
157
+ echo " gh pr create --base dev"
158
+ echo " ↓"
159
+ echo " CI runs tests on PR branch"
160
+ echo " ↓"
161
+ echo " gh pr merge --auto --squash → enters merge queue"
162
+ echo " ↓"
163
+ echo " Queue rebases PR on latest dev + runs CI again"
164
+ echo " ↓"
165
+ echo " Merge to dev (only if CI passes on rebased code)"
166
+ echo ""
167
+ echo "Release workflow:"
168
+ echo " gh pr create --base main --head dev --title \"Release\""
169
+ echo " → CI passes → merge → semantic-release publishes"
170
+ echo ""