@the-open-engine/zeroshot 6.7.1 → 6.8.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 (44) hide show
  1. package/cli/index.js +114 -16
  2. package/lib/agent-cli-provider/adapters/common.js +1 -1
  3. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  4. package/lib/agent-cli-provider/adapters/opencode-models.d.ts +7 -0
  5. package/lib/agent-cli-provider/adapters/opencode-models.d.ts.map +1 -0
  6. package/lib/agent-cli-provider/adapters/opencode-models.js +87 -0
  7. package/lib/agent-cli-provider/adapters/opencode-models.js.map +1 -0
  8. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  9. package/lib/agent-cli-provider/adapters/opencode.js +6 -58
  10. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  11. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  12. package/lib/agent-cli-provider/single-agent-runtime.js +27 -13
  13. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  14. package/lib/agent-cli-provider/types.d.ts +1 -0
  15. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  16. package/lib/agent-cli-provider/types.js.map +1 -1
  17. package/lib/git-remote-utils.js +90 -7
  18. package/package.json +6 -15
  19. package/protocol/openengine-cluster/v1/worker.schema.json +47 -0
  20. package/scripts/assert-release-published.js +161 -2
  21. package/scripts/release-dry-run.js +83 -0
  22. package/scripts/release-preflight.js +24 -12
  23. package/scripts/release-recovery.js +149 -0
  24. package/scripts/semantic-release-notes.js +44 -0
  25. package/scripts/setup-merge-queue.sh +104 -118
  26. package/src/agent/agent-task-executor.js +18 -9
  27. package/src/agent-cli-provider/adapters/common.ts +1 -1
  28. package/src/agent-cli-provider/adapters/opencode-models.ts +100 -0
  29. package/src/agent-cli-provider/adapters/opencode.ts +8 -65
  30. package/src/agent-cli-provider/single-agent-runtime.ts +51 -33
  31. package/src/agent-cli-provider/types.ts +1 -0
  32. package/src/agent-wrapper.js +14 -2
  33. package/src/agents/git-pusher-template.js +94 -19
  34. package/src/attach/socket-discovery.js +9 -19
  35. package/src/attach/socket-paths.js +85 -0
  36. package/src/claude-task-runner.js +121 -37
  37. package/src/config-validator.js +2 -2
  38. package/src/isolation-manager.js +164 -74
  39. package/src/orchestrator.js +83 -34
  40. package/src/providers/index.js +5 -0
  41. package/src/task-run-model-args.js +155 -0
  42. package/task-lib/attachable-watcher.js +3 -9
  43. package/task-lib/commands/run.js +17 -2
  44. package/task-lib/runner.js +87 -29
@@ -11,14 +11,9 @@ import {
11
11
  type BuildProviderCommandOptions,
12
12
  type CommandSpec,
13
13
  type ErrorClassification,
14
- type LevelModelSpec,
15
- type LevelOverrides,
16
- type ModelCatalogEntry,
17
- type ModelLevel,
18
14
  type OpencodeCliFeatures,
19
15
  type OutputEvent,
20
16
  type ProviderAdapter,
21
- type ResolvedModelSpec,
22
17
  type WarningMetadata,
23
18
  } from '../types';
24
19
  import {
@@ -26,55 +21,16 @@ import {
26
21
  commandSpec,
27
22
  createParserState,
28
23
  optionFeatures,
29
- resolveModelSpecWithConfig,
30
24
  unsupportedSessionControlWarnings,
31
- validateModelIdFromCatalog,
32
25
  warning,
33
26
  } from './common';
34
-
35
- const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {
36
- 'opencode/big-pickle': { rank: 1 },
37
- 'opencode/glm-4.7-free': { rank: 1 },
38
- 'opencode/gpt-5-nano': { rank: 1 },
39
- 'opencode/grok-code': { rank: 1 },
40
- 'opencode/minimax-m2.1-free': { rank: 1 },
41
- 'google/gemini-1.5-flash': { rank: 1 },
42
- 'google/gemini-1.5-flash-8b': { rank: 1 },
43
- 'google/gemini-1.5-pro': { rank: 1 },
44
- 'google/gemini-2.0-flash': { rank: 1 },
45
- 'google/gemini-2.0-flash-lite': { rank: 1 },
46
- 'google/gemini-2.5-flash': { rank: 1 },
47
- 'google/gemini-2.5-flash-image': { rank: 1 },
48
- 'google/gemini-2.5-flash-image-preview': { rank: 1 },
49
- 'google/gemini-2.5-flash-lite': { rank: 1 },
50
- 'google/gemini-2.5-flash-lite-preview-06-17': { rank: 1 },
51
- 'google/gemini-2.5-flash-lite-preview-09-2025': { rank: 1 },
52
- 'google/gemini-2.5-flash-preview-04-17': { rank: 1 },
53
- 'google/gemini-2.5-flash-preview-05-20': { rank: 1 },
54
- 'google/gemini-2.5-flash-preview-09-2025': { rank: 1 },
55
- 'google/gemini-2.5-flash-preview-tts': { rank: 1 },
56
- 'google/gemini-2.5-pro': { rank: 1 },
57
- 'google/gemini-2.5-pro-preview-05-06': { rank: 1 },
58
- 'google/gemini-2.5-pro-preview-06-05': { rank: 1 },
59
- 'google/gemini-2.5-pro-preview-tts': { rank: 1 },
60
- 'google/gemini-3-flash-preview': { rank: 1 },
61
- 'google/gemini-3-pro-preview': { rank: 1 },
62
- 'google/gemini-embedding-001': { rank: 1 },
63
- 'google/gemini-flash-latest': { rank: 1 },
64
- 'google/gemini-flash-lite-latest': { rank: 1 },
65
- 'google/gemini-live-2.5-flash': { rank: 1 },
66
- 'google/gemini-live-2.5-flash-preview-native-audio': { rank: 1 },
67
- 'openai/gpt-5.1-codex-max': { rank: 1 },
68
- 'openai/gpt-5.1-codex-mini': { rank: 1 },
69
- 'openai/gpt-5.2': { rank: 1 },
70
- 'openai/gpt-5.2-codex': { rank: 1 },
71
- };
72
-
73
- const LEVEL_MAPPING: Readonly<Record<ModelLevel, LevelModelSpec>> = {
74
- level1: { rank: 1, model: null, reasoningEffort: 'low' },
75
- level2: { rank: 2, model: null, reasoningEffort: 'medium' },
76
- level3: { rank: 3, model: null, reasoningEffort: 'high' },
77
- };
27
+ import {
28
+ LEVEL_MAPPING,
29
+ MODEL_CATALOG,
30
+ resolveModelSpec,
31
+ validateConfiguredModelId,
32
+ validateModelId,
33
+ } from './opencode-models';
78
34
 
79
35
  function detectCliFeatures(helpText?: string | null): OpencodeCliFeatures {
80
36
  const help = helpText ?? '';
@@ -244,20 +200,6 @@ function parseEvent(line: string): OutputEvent | null {
244
200
  return null;
245
201
  }
246
202
 
247
- function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
248
- return resolveModelSpecWithConfig({
249
- mapping: LEVEL_MAPPING,
250
- defaultLevel: 'level2',
251
- level,
252
- overrides,
253
- validateModelId,
254
- });
255
- }
256
-
257
- function validateModelId(modelId: string | null | undefined): string | null | undefined {
258
- return validateModelIdFromCatalog('opencode', MODEL_CATALOG, modelId);
259
- }
260
-
261
203
  function classifyError(error: unknown): ErrorClassification {
262
204
  return classifyBaseProviderError(error, [], []);
263
205
  }
@@ -285,5 +227,6 @@ export const opencodeAdapter: ProviderAdapter = {
285
227
  createParserState: () => createParserState('opencode'),
286
228
  resolveModelSpec,
287
229
  validateModelId,
230
+ validateConfiguredModelId,
288
231
  classifyError,
289
232
  };
@@ -34,6 +34,11 @@ interface RuntimeProviderSettings {
34
34
  readonly gateway?: GatewayBuildOptions;
35
35
  }
36
36
 
37
+ interface RuntimeCommandContext {
38
+ readonly cliFeatures: CliFeatureOverrides;
39
+ readonly authEnv: Readonly<Record<string, string>>;
40
+ }
41
+
37
42
  export interface SingleAgentProviderCommandInput {
38
43
  readonly provider?: string | null;
39
44
  readonly context: string;
@@ -62,6 +67,7 @@ type MutableModelSpec = {
62
67
 
63
68
  const MODEL_LEVELS: readonly ModelLevel[] = ['level1', 'level2', 'level3'];
64
69
  const REASONING_EFFORTS: readonly ReasoningEffort[] = ['low', 'medium', 'high', 'xhigh', 'max'];
70
+ const LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV = 'ZEROSHOT_ISOLATED_PROVIDER_SETTINGS_JSON';
65
71
  const settingsModule: unknown = require('../../lib/settings');
66
72
  const providerDetectionModule: unknown = require('../../lib/provider-detection');
67
73
  const claudeAuthModule: unknown = require('../../lib/settings/claude-auth');
@@ -76,6 +82,7 @@ const resolveClaudeAuthFn = moduleFunction(claudeAuthModule, 'resolveClaudeAuth'
76
82
  export function prepareSingleAgentProviderCommand(
77
83
  input: SingleAgentProviderCommandInput
78
84
  ): PreparedSingleAgentProviderCommand {
85
+ rejectCallerSuppliedModelProvenance(input);
79
86
  const baseOptions = input.options ?? {};
80
87
  const settings = loadRuntimeSettings();
81
88
  const adapter = adapterForRuntimeInput(input.provider, settings);
@@ -86,7 +93,10 @@ export function prepareSingleAgentProviderCommand(
86
93
  );
87
94
  const cliFeatures = resolveRuntimeCliFeatures(adapter.id, baseOptions.cliFeatures);
88
95
  const authEnv = baseOptions.authEnv ?? resolveRuntimeAuthEnv(adapter.id, settings);
89
- const options = buildRuntimeOptions(baseOptions, adapter, providerSettings, cliFeatures, authEnv);
96
+ const options = buildRuntimeOptions(baseOptions, adapter, providerSettings, {
97
+ cliFeatures,
98
+ authEnv,
99
+ });
90
100
  return {
91
101
  adapter,
92
102
  options,
@@ -128,8 +138,7 @@ function mergeAcpFailClosedCliFeatures(
128
138
  ...detected,
129
139
  ...overrides,
130
140
  supportsAcpStdio: detected.supportsAcpStdio && overrides.supportsAcpStdio !== false,
131
- supportsPromptImages:
132
- detected.supportsPromptImages && overrides.supportsPromptImages !== false,
141
+ supportsPromptImages: detected.supportsPromptImages && overrides.supportsPromptImages !== false,
133
142
  supportsLoadSession: detected.supportsLoadSession && overrides.supportsLoadSession !== false,
134
143
  supportsSessionCancel:
135
144
  detected.supportsSessionCancel && overrides.supportsSessionCancel !== false,
@@ -162,7 +171,9 @@ export function probeRuntimeProviderCli(provider: string): RuntimeProviderProbe
162
171
  }
163
172
 
164
173
  const helpText = stringResult(getHelpOutputFn(helpCommand.command, helpCommand.args)).trim();
165
- const versionText = stringResult(getVersionOutputFn(helpCommand.command, helpCommand.args)).trim();
174
+ const versionText = stringResult(
175
+ getVersionOutputFn(helpCommand.command, helpCommand.args)
176
+ ).trim();
166
177
  const availabilityProbe = getProviderRegistryEntry(adapter.id).availabilityProbe ?? 'command';
167
178
 
168
179
  return {
@@ -177,8 +188,7 @@ function buildRuntimeOptions(
177
188
  baseOptions: BuildProviderCommandOptions,
178
189
  adapter: ProviderAdapter,
179
190
  providerSettings: RuntimeProviderSettings,
180
- cliFeatures: CliFeatureOverrides,
181
- authEnv: Readonly<Record<string, string>>
191
+ runtime: RuntimeCommandContext
182
192
  ): BuildProviderCommandOptions {
183
193
  const modelSpec = resolveRuntimeModelSpec(adapter, baseOptions.modelSpec, providerSettings);
184
194
  const gateway = resolveRuntimeGatewayOptions(
@@ -191,16 +201,16 @@ function buildRuntimeOptions(
191
201
  ...baseOptions,
192
202
  modelSpec,
193
203
  ...(gateway === undefined ? {} : { gateway }),
194
- cliFeatures,
204
+ cliFeatures: runtime.cliFeatures,
195
205
  };
196
206
  if (baseOptions.jsonSchema && !supportsProviderCapability(adapter.id, 'jsonSchema')) {
197
- if (!shouldIncludeAuthEnv(baseOptions, authEnv)) {
207
+ if (!shouldIncludeAuthEnv(baseOptions, runtime.authEnv)) {
198
208
  return { ...resolved, strictSchema: false };
199
209
  }
200
- return { ...resolved, authEnv, strictSchema: false };
210
+ return { ...resolved, authEnv: runtime.authEnv, strictSchema: false };
201
211
  }
202
- if (!shouldIncludeAuthEnv(baseOptions, authEnv)) return resolved;
203
- return { ...resolved, authEnv };
212
+ if (!shouldIncludeAuthEnv(baseOptions, runtime.authEnv)) return resolved;
213
+ return { ...resolved, authEnv: runtime.authEnv };
204
214
  }
205
215
 
206
216
  function resolveRuntimeGatewayOptions(
@@ -218,23 +228,19 @@ function resolveRuntimeGatewayOptions(
218
228
  ? settingsGateway.headers
219
229
  : { ...(settingsGateway.headers ?? {}), ...requestGateway.headers };
220
230
  const mergedGateway: GatewayBuildOptions = {
221
- ...(requestGateway.baseUrl ?? settingsGateway.baseUrl
231
+ ...((requestGateway.baseUrl ?? settingsGateway.baseUrl)
222
232
  ? { baseUrl: requestGateway.baseUrl ?? settingsGateway.baseUrl }
223
233
  : {}),
224
- ...(requestGateway.apiKey ?? settingsGateway.apiKey
234
+ ...((requestGateway.apiKey ?? settingsGateway.apiKey)
225
235
  ? { apiKey: requestGateway.apiKey ?? settingsGateway.apiKey }
226
236
  : {}),
227
237
  ...(mergedHeaders === undefined ? {} : { headers: mergedHeaders }),
228
238
  model: requestGateway.model ?? modelSpec.model ?? settingsGateway.model ?? null,
229
- ...(requestGateway.toolPolicy ?? settingsGateway.toolPolicy
239
+ ...((requestGateway.toolPolicy ?? settingsGateway.toolPolicy)
230
240
  ? { toolPolicy: requestGateway.toolPolicy ?? settingsGateway.toolPolicy }
231
241
  : {}),
232
242
  };
233
- return resolveGatewayConfiguration(
234
- mergedGateway,
235
- 'options.gateway',
236
- cwd
237
- );
243
+ return resolveGatewayConfiguration(mergedGateway, 'options.gateway', cwd);
238
244
  }
239
245
 
240
246
  function shouldIncludeAuthEnv(
@@ -286,7 +292,8 @@ function adapterForRuntimeInput(
286
292
  provider: string | null | undefined,
287
293
  settings: Record<string, unknown>
288
294
  ): ProviderAdapter {
289
- const configured = provider ?? optionalString(settings.defaultProvider, 'settings.defaultProvider');
295
+ const configured =
296
+ provider ?? optionalString(settings.defaultProvider, 'settings.defaultProvider');
290
297
  return getProviderAdapter(configured ?? 'claude');
291
298
  }
292
299
 
@@ -309,16 +316,14 @@ function runtimeProviderSettings(
309
316
  );
310
317
  const gateway =
311
318
  provider === 'gateway'
312
- ? normalizeGatewayBuildOptions(
313
- providerSettings,
314
- 'settings.providerSettings.gateway',
315
- cwd
316
- )
319
+ ? normalizeGatewayBuildOptions(providerSettings, 'settings.providerSettings.gateway', cwd)
317
320
  : undefined;
318
321
  if (defaultLevel === undefined) {
319
322
  return gateway === undefined ? { levelOverrides } : { levelOverrides, gateway };
320
323
  }
321
- return gateway === undefined ? { defaultLevel, levelOverrides } : { defaultLevel, levelOverrides, gateway };
324
+ return gateway === undefined
325
+ ? { defaultLevel, levelOverrides }
326
+ : { defaultLevel, levelOverrides, gateway };
322
327
  }
323
328
 
324
329
  function runtimeHelpCommand(provider: ProviderId): CommandParts {
@@ -333,7 +338,11 @@ function probeGatewayProvider(adapter: ProviderAdapter): RuntimeProviderProbe {
333
338
  try {
334
339
  const settings = loadRuntimeSettings();
335
340
  const providerSettings = runtimeProviderSettings(settings, 'gateway', process.cwd());
336
- resolveGatewayConfiguration(providerSettings.gateway, 'settings.providerSettings.gateway', process.cwd());
341
+ resolveGatewayConfiguration(
342
+ providerSettings.gateway,
343
+ 'settings.providerSettings.gateway',
344
+ process.cwd()
345
+ );
337
346
  return {
338
347
  available: true,
339
348
  helpText: 'Bundled gateway runner',
@@ -351,8 +360,20 @@ function probeGatewayProvider(adapter: ProviderAdapter): RuntimeProviderProbe {
351
360
  }
352
361
 
353
362
  function loadRuntimeSettings(): Record<string, unknown> {
354
- const settings = loadSettingsFn();
355
- return requiredRecord(settings, 'loadSettings');
363
+ if (Object.prototype.hasOwnProperty.call(process.env, LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV)) {
364
+ throw new Error(
365
+ `${LEGACY_ISOLATED_PROVIDER_SETTINGS_ENV} is not a trusted settings channel; use the settings file.`
366
+ );
367
+ }
368
+ return requiredRecord(loadSettingsFn(), 'loadSettings');
369
+ }
370
+
371
+ function rejectCallerSuppliedModelProvenance(input: SingleAgentProviderCommandInput): void {
372
+ if (Object.prototype.hasOwnProperty.call(input, 'modelSpecSource')) {
373
+ throw new Error(
374
+ 'modelSpecSource is not accepted at the child provider boundary; use modelLevel and effective provider settings.'
375
+ );
376
+ }
356
377
  }
357
378
 
358
379
  function moduleFunction(moduleValue: unknown, field: string): UnknownFunction {
@@ -456,10 +477,7 @@ function requiredRecord(value: unknown, field: string): Record<string, unknown>
456
477
  throw new Error(`${field} must be an object.`);
457
478
  }
458
479
 
459
- function stringRecordFromUnknown(
460
- value: unknown,
461
- field: string
462
- ): Readonly<Record<string, string>> {
480
+ function stringRecordFromUnknown(value: unknown, field: string): Readonly<Record<string, string>> {
463
481
  if (value === undefined || value === null) return {};
464
482
  const record = requiredRecord(value, field);
465
483
  const result: Record<string, string> = {};
@@ -367,6 +367,7 @@ export interface ProviderAdapter {
367
367
  createParserState(): ProviderParserState;
368
368
  resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec;
369
369
  validateModelId(modelId: string | null | undefined): string | null | undefined;
370
+ validateConfiguredModelId?(modelId: string | null | undefined): string | null | undefined;
370
371
  classifyError(error: unknown): ErrorClassification;
371
372
  }
372
373
 
@@ -103,10 +103,11 @@ class AgentWrapper {
103
103
  const taskRunner = options.taskRunner;
104
104
  this.mockSpawnFn = (args, { context }) => {
105
105
  const spec = this._resolveModelSpec();
106
+ const source = this._resolveModelSpecSource();
106
107
  return taskRunner.run(context, {
107
108
  agentId: this.id,
108
- model: this._selectModel(),
109
- modelSpec: spec,
109
+ ...(source === 'direct' ? { model: spec.model } : { modelLevel: spec.level }),
110
+ ...(spec.reasoningEffort ? { reasoningEffort: spec.reasoningEffort } : {}),
110
111
  provider: this._resolveProvider(),
111
112
  cwd: this.config.cwd || process.cwd(),
112
113
  worktreePath: this.worktree?.path || null,
@@ -222,6 +223,17 @@ class AgentWrapper {
222
223
  return applyReasoningOverride({ ...spec, level }, this.config.reasoningEffort);
223
224
  }
224
225
 
226
+ _resolveModelSpecSource() {
227
+ if (this.modelConfig.type === 'rules') {
228
+ for (const rule of this.modelConfig.rules) {
229
+ if (this._matchesIterationRange(rule.iterations)) {
230
+ return rule.model ? 'direct' : 'provider-level';
231
+ }
232
+ }
233
+ }
234
+ return this.modelConfig.model ? 'direct' : 'provider-level';
235
+ }
236
+
225
237
  /**
226
238
  * Publish a message to the message bus, always including sender_model
227
239
  * @private
@@ -275,6 +275,7 @@ const hasSufficientEvidence = latestValidatorMessages.every((r) => {
275
275
  return hasSufficientEvidence;`;
276
276
 
277
277
  const { readRepoSettings } = require('../../lib/repo-settings');
278
+ const { normalizeGitRemoteName, quoteShellArgument } = require('../../lib/git-remote-utils');
278
279
  const { resolveRequiredQualityGates } = require('../quality-gates');
279
280
 
280
281
  function getSafeBranchName(value) {
@@ -313,6 +314,35 @@ function normalizeCloseIssueMode(value) {
313
314
  return null;
314
315
  }
315
316
 
317
+ function normalizeIssueNumber(value) {
318
+ const candidate = typeof value === 'number' ? String(value) : value;
319
+ if (typeof candidate !== 'string') return 'unknown';
320
+ const trimmed = candidate.trim();
321
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed) ? trimmed : 'unknown';
322
+ }
323
+
324
+ function normalizeIssueTitle(value) {
325
+ if (typeof value !== 'string' || value.trim() === '') return 'Implementation';
326
+ const normalized = [...value]
327
+ .map((character) => {
328
+ const codePoint = character.codePointAt(0);
329
+ return codePoint < 0x20 || codePoint === 0x7f ? ' ' : character;
330
+ })
331
+ .join('')
332
+ .trim();
333
+ return normalized || 'Implementation';
334
+ }
335
+
336
+ function resolveIssueContext(options) {
337
+ const issueNumber = normalizeIssueNumber(options.issueNumber);
338
+ const issueTitle = normalizeIssueTitle(options.issueTitle);
339
+ const issueReference =
340
+ options.includeIssueReference === false || issueNumber === 'unknown'
341
+ ? ''
342
+ : `Closes #${issueNumber}`;
343
+ return { issueNumber, issueTitle, issueReference };
344
+ }
345
+
316
346
  /**
317
347
  * Resolve GitHub configuration from CLI options and repo settings.
318
348
  * Priority: CLI options > repo settings (.zeroshot/settings.json) > defaults
@@ -321,12 +351,20 @@ function normalizeCloseIssueMode(value) {
321
351
  * @param {string} [options.prBase] - Target branch for PRs
322
352
  * @param {boolean} [options.mergeQueue] - Use GitHub merge queue
323
353
  * @param {string} [options.closeIssue] - When to close issue: auto|always|never
354
+ * @param {string} [options.gitRemote=origin] - Remote to push the implementation branch to
355
+ * @param {string|number} [options.issueNumber] - Typed issue identifier for prompt commands
356
+ * @param {string} [options.issueTitle] - Typed issue title for prompt commands
357
+ * @param {boolean} [options.includeIssueReference] - Include the closing reference in PR text
324
358
  * @returns {Object} Resolved configuration
325
359
  */
326
360
  function resolveGitHubConfig(options = {}) {
327
361
  const repoSettingsResult = readRepoSettings(options.cwd || process.cwd());
328
362
  const repoSettings = repoSettingsResult.settings || {};
329
363
  const repoGithub = repoSettings.github || {};
364
+ const gitRemote = normalizeGitRemoteName(options.gitRemote ?? 'origin');
365
+ if (!gitRemote) {
366
+ throw new Error(`Invalid git remote name '${options.gitRemote}'`);
367
+ }
330
368
 
331
369
  // CLI options override repo settings
332
370
  const prBase = getSafeBranchName(options.prBase) || getSafeBranchName(repoGithub.prBase);
@@ -347,7 +385,14 @@ function resolveGitHubConfig(options = {}) {
347
385
  options.autoMerge === true ||
348
386
  (options.autoMerge !== false && parseBool(repoGithub.autoMerge) === true);
349
387
 
350
- return { prBase, useMergeQueue, closeIssueMode, autoMerge };
388
+ return {
389
+ prBase,
390
+ useMergeQueue,
391
+ closeIssueMode,
392
+ autoMerge,
393
+ gitRemote,
394
+ issueContext: resolveIssueContext(options),
395
+ };
351
396
  }
352
397
 
353
398
  /**
@@ -358,13 +403,16 @@ function resolveGitHubConfig(options = {}) {
358
403
  * @returns {Object|null} Platform configuration or null if unsupported
359
404
  */
360
405
  function getPlatformConfig(platform, config = {}) {
361
- const { prBase, useMergeQueue, closeIssueMode, autoMerge } = config;
406
+ const { prBase, useMergeQueue, closeIssueMode, autoMerge, gitRemote } = config;
407
+ const issueContext = config.issueContext || resolveIssueContext({});
408
+ const issueTitleArgument = quoteShellArgument(`feat: ${issueContext.issueTitle}`);
409
+ const issueReferenceArgument = quoteShellArgument(issueContext.issueReference);
362
410
 
363
411
  const PLATFORM_CONFIGS = {
364
412
  github: {
365
413
  prName: 'PR',
366
414
  prNameLower: 'pull request',
367
- createCmd: `gh pr create${prBase ? ` --base ${prBase}` : ''} --title "feat: {{issue_title}}" --body "Closes #{{issue_number}}"`,
415
+ createCmd: `gh pr create${prBase ? ` --base ${prBase}` : ''} --title ${issueTitleArgument} --body ${issueReferenceArgument}`,
368
416
  mergeCmd: useMergeQueue
369
417
  ? `PR_ID="$(timeout 30 gh pr view --json id --jq .id)"
370
418
  gh api graphql -f query='mutation($id:ID!){enqueuePullRequest(input:{pullRequestId:$id}){mergeQueueEntry{state}}}' -f id="$PR_ID"
@@ -380,24 +428,26 @@ for i in $(seq 1 90); do if timeout 30 gh pr view --json mergedAt --jq .mergedAt
380
428
  usesMergeQueue: useMergeQueue,
381
429
  closeIssueMode: closeIssueMode || 'never',
382
430
  autoMerge: Boolean(autoMerge),
431
+ gitRemote,
432
+ issueContext,
383
433
  },
384
434
  gitlab: {
385
435
  prName: 'MR',
386
436
  prNameLower: 'merge request',
387
- createCmd:
388
- 'glab mr create --title "feat: {{issue_title}}" --description "Closes #{{issue_number}}"',
437
+ createCmd: `glab mr create --title ${issueTitleArgument} --description ${issueReferenceArgument}`,
389
438
  mergeCmd: 'glab mr merge --auto-merge',
390
439
  mergeFallbackCmd: 'glab mr merge',
391
440
  prUrlExample: 'https://gitlab.com/owner/repo/-/merge_requests/123',
392
441
  outputFields: { urlField: 'mr_url', numberField: 'mr_number', mergedField: 'merged' },
393
442
  closeIssueMode: closeIssueMode || 'never',
394
443
  autoMerge: Boolean(autoMerge),
444
+ gitRemote,
445
+ issueContext,
395
446
  },
396
447
  'azure-devops': {
397
448
  prName: 'PR',
398
449
  prNameLower: 'pull request',
399
- createCmd:
400
- 'az repos pr create --title "feat: {{issue_title}}" --description "Closes #{{issue_number}}"',
450
+ createCmd: `az repos pr create --title ${issueTitleArgument} --description ${issueReferenceArgument}`,
401
451
  mergeCmd: 'az repos pr update --id <PR_ID> --auto-complete true',
402
452
  mergeFallbackCmd: 'az repos pr update --id <PR_ID> --status completed',
403
453
  prUrlExample: 'https://dev.azure.com/org/project/_git/repo/pullrequest/123',
@@ -411,6 +461,8 @@ for i in $(seq 1 90); do if timeout 30 gh pr view --json mergedAt --jq .mergedAt
411
461
  requiresPrIdExtraction: true,
412
462
  closeIssueMode: closeIssueMode || 'never',
413
463
  autoMerge: Boolean(autoMerge),
464
+ gitRemote,
465
+ issueContext,
414
466
  },
415
467
  };
416
468
 
@@ -430,8 +482,20 @@ const SUPPORTED_PLATFORMS = ['github', 'gitlab', 'azure-devops'];
430
482
  * @returns {string} The complete review-mode prompt
431
483
  */
432
484
  function generateReviewModePrompt(config) {
433
- const { prName, prNameLower, createCmd, prUrlExample, outputFields, requiresPrIdExtraction } =
434
- config;
485
+ const {
486
+ prName,
487
+ prNameLower,
488
+ createCmd,
489
+ prUrlExample,
490
+ outputFields,
491
+ requiresPrIdExtraction,
492
+ gitRemote,
493
+ issueContext,
494
+ } = config;
495
+ const gitRemoteArgument = quoteShellArgument(gitRemote);
496
+ const commitMessageArgument = quoteShellArgument(
497
+ `feat: implement #${issueContext.issueNumber} - ${issueContext.issueTitle}`
498
+ );
435
499
 
436
500
  return `CRITICAL: ALL VALIDATORS APPROVED. YOU ARE A TRANSPORT-ONLY GIT PUSHER.
437
501
 
@@ -467,13 +531,13 @@ Run this. If nothing to commit, output JSON with ${outputFields.urlField}: null
467
531
 
468
532
  ### STEP 3: Commit the changes (MANDATORY if there are changes)
469
533
  \`\`\`bash
470
- git commit -m "feat: implement #{{issue_number}} - {{issue_title}}"
534
+ git commit -m ${commitMessageArgument}
471
535
  \`\`\`
472
536
  Run this command. Do not skip it.
473
537
 
474
- ### STEP 4: Push to origin (MANDATORY)
538
+ ### STEP 4: Push to ${gitRemote} (MANDATORY)
475
539
  \`\`\`bash
476
- git push -u origin HEAD
540
+ git push -u -- ${gitRemoteArgument} HEAD
477
541
  \`\`\`
478
542
  Run this. If it fails, do not edit files, rebase, or resolve conflicts. Output blocked JSON with the failure summary.
479
543
 
@@ -543,7 +607,14 @@ function generatePrompt(config) {
543
607
  usesMergeQueue,
544
608
  closeIssueMode,
545
609
  autoMerge,
610
+ gitRemote,
611
+ issueContext,
546
612
  } = config;
613
+ const gitRemoteArgument = quoteShellArgument(gitRemote);
614
+ const issueNumberArgument = quoteShellArgument(issueContext.issueNumber);
615
+ const commitMessageArgument = quoteShellArgument(
616
+ `feat: implement #${issueContext.issueNumber} - ${issueContext.issueTitle}`
617
+ );
547
618
 
548
619
  if (!autoMerge) {
549
620
  return generateReviewModePrompt(config);
@@ -627,13 +698,13 @@ Run this. If nothing to commit, output JSON with ${outputFields.urlField}: null
627
698
 
628
699
  ### STEP 3: Commit the changes (MANDATORY if there are changes)
629
700
  \`\`\`bash
630
- git commit -m "feat: implement #{{issue_number}} - {{issue_title}}"
701
+ git commit -m ${commitMessageArgument}
631
702
  \`\`\`
632
703
  Run this command. Do not skip it.
633
704
 
634
- ### STEP 4: Push to origin (MANDATORY)
705
+ ### STEP 4: Push to ${gitRemote} (MANDATORY)
635
706
  \`\`\`bash
636
- git push -u origin HEAD
707
+ git push -u -- ${gitRemoteArgument} HEAD
637
708
  \`\`\`
638
709
  Run this. If it fails, do not edit files, rebase, or resolve conflicts. Output blocked JSON with the failure summary.
639
710
 
@@ -665,8 +736,8 @@ ${
665
736
  closeIssueMode !== 'never'
666
737
  ? `### STEP 7: Close the issue (MANDATORY)
667
738
  \`\`\`bash
668
- if [ "{{issue_number}}" != "unknown" ]; then
669
- ISSUE_STATE="$(gh issue view {{issue_number}} --json state --jq .state 2>/dev/null || true)"
739
+ if [ ${issueNumberArgument} != "unknown" ]; then
740
+ ISSUE_STATE="$(gh issue view ${issueNumberArgument} --json state --jq .state 2>/dev/null || true)"
670
741
  if [ "$ISSUE_STATE" = "OPEN" ]; then
671
742
  BASE_BRANCH="${rebaseBranch || 'main'}"
672
743
  DEFAULT_BRANCH="$(gh repo view --json defaultBranchRef --jq .defaultBranchRef.name 2>/dev/null || true)"
@@ -682,9 +753,9 @@ if [ "{{issue_number}}" != "unknown" ]; then
682
753
  if [ "$SHOULD_CLOSE" = "1" ]; then
683
754
  PR_URL="$(gh pr view --json url --jq .url 2>/dev/null || true)"
684
755
  if [ -n "$PR_URL" ]; then
685
- gh issue close {{issue_number}} --comment "Implemented in $PR_URL"
756
+ gh issue close ${issueNumberArgument} --comment "Implemented in $PR_URL"
686
757
  else
687
- gh issue close {{issue_number}} --comment "Implemented"
758
+ gh issue close ${issueNumberArgument} --comment "Implemented"
688
759
  fi
689
760
  fi
690
761
  fi
@@ -728,6 +799,10 @@ If blocked before creating a ${prName}, output:
728
799
  * @param {string} [options.prBase] - Target branch for PRs
729
800
  * @param {boolean} [options.mergeQueue] - Use GitHub merge queue
730
801
  * @param {string} [options.closeIssue] - When to close issue: auto|always|never
802
+ * @param {string} [options.gitRemote=origin] - Remote to push the implementation branch to
803
+ * @param {string|number} [options.issueNumber] - Typed issue identifier for prompt commands
804
+ * @param {string} [options.issueTitle] - Typed issue title for prompt commands
805
+ * @param {boolean} [options.includeIssueReference] - Include the closing reference in PR text
731
806
  * @param {Array} [options.requiredQualityGates] - Required handoff quality gates
732
807
  * @param {boolean} [options.autoMerge] - Merge the PR (--ship). False stops after PR creation (--pr).
733
808
  * @returns {Object} Agent configuration object
@@ -1,20 +1,18 @@
1
1
  /**
2
2
  * Socket Discovery - Utilities for socket path management
3
3
  *
4
- * Socket locations:
5
- * - Tasks: ~/.zeroshot/sockets/task-<id>.sock
6
- * - Clusters: ~/.zeroshot/sockets/cluster-<id>.sock (cluster-level, future)
7
- * - Agents: ~/.zeroshot/sockets/cluster-<id>/<agent-id>.sock
4
+ * Socket locations are allocated by socket-paths.js under a short, per-user
5
+ * runtime directory so long HOME paths cannot exceed Unix socket limits.
8
6
  */
9
7
 
10
8
  const path = require('path');
11
9
  const fs = require('fs');
12
- const os = require('os');
13
10
  const net = require('net');
14
11
  const { readClustersFileSync } = require('../../lib/clusters-registry');
12
+ const socketPaths = require('./socket-paths');
15
13
 
16
- const ZEROSHOT_DIR = path.join(os.homedir(), '.zeroshot');
17
- const SOCKET_DIR = path.join(ZEROSHOT_DIR, 'sockets');
14
+ const ZEROSHOT_DIR = path.join(socketPaths.resolveHomeDir(), '.zeroshot');
15
+ const SOCKET_DIR = socketPaths.getSocketDir();
18
16
 
19
17
  /**
20
18
  * Check if an ID is a known cluster by looking up clusters.json
@@ -35,9 +33,7 @@ function isKnownCluster(id) {
35
33
  * Ensure socket directory exists
36
34
  */
37
35
  function ensureSocketDir() {
38
- if (!fs.existsSync(SOCKET_DIR)) {
39
- fs.mkdirSync(SOCKET_DIR, { recursive: true });
40
- }
36
+ socketPaths.ensureSocketDir();
41
37
  }
42
38
 
43
39
  /**
@@ -46,8 +42,7 @@ function ensureSocketDir() {
46
42
  * @returns {string} - Socket path
47
43
  */
48
44
  function getTaskSocketPath(taskId) {
49
- ensureSocketDir();
50
- return path.join(SOCKET_DIR, `${taskId}.sock`);
45
+ return socketPaths.getTaskSocketPath(taskId);
51
46
  }
52
47
 
53
48
  /**
@@ -57,11 +52,7 @@ function getTaskSocketPath(taskId) {
57
52
  * @returns {string} - Socket path
58
53
  */
59
54
  function getAgentSocketPath(clusterId, agentId) {
60
- const clusterDir = path.join(SOCKET_DIR, clusterId);
61
- if (!fs.existsSync(clusterDir)) {
62
- fs.mkdirSync(clusterDir, { recursive: true });
63
- }
64
- return path.join(clusterDir, `${agentId}.sock`);
55
+ return socketPaths.getAgentSocketPath(clusterId, agentId);
65
56
  }
66
57
 
67
58
  /**
@@ -81,8 +72,7 @@ function getSocketPath(id, agentId = null) {
81
72
  return getAgentSocketPath(id, agentId);
82
73
  }
83
74
  // Cluster-level socket (future use)
84
- ensureSocketDir();
85
- return path.join(SOCKET_DIR, `${id}.sock`);
75
+ return socketPaths.getClusterSocketPath(id);
86
76
  }
87
77
  // Unknown format, treat as task
88
78
  return getTaskSocketPath(id);