@the-open-engine/zeroshot 6.7.0 → 6.7.2

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 (61) hide show
  1. package/cli/commands/providers.js +4 -1
  2. package/cli/index.js +131 -27
  3. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  4. package/lib/agent-cli-provider/adapters/claude.js +23 -10
  5. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  6. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  7. package/lib/agent-cli-provider/adapters/codex.js +4 -0
  8. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  9. package/lib/agent-cli-provider/adapters/common.d.ts.map +1 -1
  10. package/lib/agent-cli-provider/adapters/common.js +2 -1
  11. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  12. package/lib/agent-cli-provider/contract-options.d.ts.map +1 -1
  13. package/lib/agent-cli-provider/contract-options.js +2 -1
  14. package/lib/agent-cli-provider/contract-options.js.map +1 -1
  15. package/lib/agent-cli-provider/index.d.ts +1 -1
  16. package/lib/agent-cli-provider/index.d.ts.map +1 -1
  17. package/lib/agent-cli-provider/index.js.map +1 -1
  18. package/lib/agent-cli-provider/provider-registry.d.ts +1 -1
  19. package/lib/agent-cli-provider/provider-registry.js +1 -1
  20. package/lib/agent-cli-provider/provider-registry.js.map +1 -1
  21. package/lib/agent-cli-provider/single-agent-runtime.js +6 -2
  22. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  23. package/lib/agent-cli-provider/types.d.ts +3 -1
  24. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  25. package/lib/agent-cli-provider/types.js.map +1 -1
  26. package/lib/git-remote-utils.js +90 -7
  27. package/lib/settings.js +1 -0
  28. package/package.json +6 -15
  29. package/protocol/openengine-cluster/v1/worker.schema.json +47 -0
  30. package/scripts/assert-release-published.js +128 -0
  31. package/scripts/release-dry-run.js +83 -0
  32. package/scripts/release-preflight.js +24 -12
  33. package/scripts/release-recovery.js +149 -0
  34. package/scripts/run-lint-staged-no-stash.js +68 -0
  35. package/scripts/semantic-release-notes.js +44 -0
  36. package/scripts/setup-merge-queue.sh +104 -118
  37. package/src/agent/agent-lifecycle.js +368 -91
  38. package/src/agent/agent-task-executor.js +384 -101
  39. package/src/agent-cli-provider/adapters/claude.ts +29 -11
  40. package/src/agent-cli-provider/adapters/codex.ts +4 -0
  41. package/src/agent-cli-provider/adapters/common.ts +2 -1
  42. package/src/agent-cli-provider/contract-options.ts +2 -1
  43. package/src/agent-cli-provider/index.ts +1 -0
  44. package/src/agent-cli-provider/provider-registry.ts +1 -1
  45. package/src/agent-cli-provider/single-agent-runtime.ts +8 -2
  46. package/src/agent-cli-provider/types.ts +3 -1
  47. package/src/agent-wrapper.js +9 -2
  48. package/src/agents/git-pusher-template.js +94 -19
  49. package/src/attach/socket-discovery.js +9 -19
  50. package/src/attach/socket-paths.js +85 -0
  51. package/src/config-validator.js +10 -11
  52. package/src/isolation-manager.js +164 -74
  53. package/src/orchestrator.js +101 -40
  54. package/task-lib/attachable-watcher.js +13 -10
  55. package/task-lib/commands/kill.js +34 -9
  56. package/task-lib/commands/run.js +17 -2
  57. package/task-lib/commands/status.js +13 -3
  58. package/task-lib/process-termination.js +202 -0
  59. package/task-lib/runner.js +29 -23
  60. package/task-lib/store.js +28 -6
  61. package/task-lib/watcher.js +14 -2
@@ -4,7 +4,6 @@ import {
4
4
  type ClaudeCliFeatures,
5
5
  type CommandSpec,
6
6
  type ErrorClassification,
7
- InvalidProviderModelError,
8
7
  type LevelModelSpec,
9
8
  type LevelOverrides,
10
9
  type ModelCatalogEntry,
@@ -28,8 +27,23 @@ import { parseClaudeEvent } from './claude-parser';
28
27
 
29
28
  const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {
30
29
  haiku: { rank: 1 },
30
+ 'claude-haiku-4-5': { rank: 1 },
31
+ 'claude-haiku-4-5-20251001': { rank: 1 },
31
32
  sonnet: { rank: 2 },
33
+ 'claude-sonnet-4-5': { rank: 2 },
34
+ 'claude-sonnet-4-5-20250929': { rank: 2 },
35
+ 'claude-sonnet-4-6': { rank: 2 },
36
+ 'claude-sonnet-5': { rank: 2 },
32
37
  opus: { rank: 3 },
38
+ 'claude-opus-4-5': { rank: 3 },
39
+ 'claude-opus-4-5-20251101': { rank: 3 },
40
+ 'claude-opus-4-6': { rank: 3 },
41
+ 'claude-opus-4-7': { rank: 3 },
42
+ 'claude-opus-4-8': { rank: 3 },
43
+ fable: { rank: 3 },
44
+ 'claude-fable-5': { rank: 3 },
45
+ 'claude-mythos-5': { rank: 3 },
46
+ 'claude-mythos-preview': { rank: 3 },
33
47
  };
34
48
 
35
49
  const LEVEL_MAPPING: Readonly<Record<ModelLevel, LevelModelSpec>> = {
@@ -50,6 +64,7 @@ function detectCliFeatures(helpText?: string | null): ClaudeCliFeatures {
50
64
  supportsIncludePartials: unknown ? true : /--include-partial-messages/.test(help),
51
65
  supportsVerbose: unknown ? true : /--verbose/.test(help),
52
66
  supportsModel: unknown ? true : /--model/.test(help),
67
+ supportsEffort: unknown ? true : /--effort/.test(help),
53
68
  unknown,
54
69
  };
55
70
  }
@@ -86,6 +101,9 @@ function addModelArgs(args: string[], options: BuildProviderCommandOptions): voi
86
101
  if (options.modelSpec?.model && features.supportsModel !== false) {
87
102
  args.push('--model', options.modelSpec.model);
88
103
  }
104
+ if (options.modelSpec?.reasoningEffort && features.supportsEffort !== false) {
105
+ args.push('--effort', options.modelSpec.reasoningEffort);
106
+ }
89
107
  }
90
108
 
91
109
  function addAutoApproveArgs(args: string[], options: BuildProviderCommandOptions): void {
@@ -139,6 +157,15 @@ function collectWarnings(options: BuildProviderCommandOptions): WarningMetadata[
139
157
  )
140
158
  );
141
159
  }
160
+ if (options.modelSpec?.reasoningEffort && features.supportsEffort === false) {
161
+ warnings.push(
162
+ warning(
163
+ 'claude',
164
+ 'claude-reasoning',
165
+ 'Claude CLI does not support --effort; skipping reasoningEffort.'
166
+ )
167
+ );
168
+ }
142
169
  return warnings;
143
170
  }
144
171
 
@@ -176,16 +203,7 @@ function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): Resolv
176
203
  }
177
204
 
178
205
  function validateModelId(modelId: string | null | undefined): string | null | undefined {
179
- try {
180
- return validateModelIdFromCatalog('claude', MODEL_CATALOG, modelId);
181
- } catch (error) {
182
- if (modelId && (modelId === 'opus-4.6' || modelId === 'claude-opus-4-6')) {
183
- throw new InvalidProviderModelError(
184
- `Invalid model "${modelId}" for provider "claude". Use canonical model ids: haiku, sonnet, opus.`
185
- );
186
- }
187
- throw error;
188
- }
206
+ return validateModelIdFromCatalog('claude', MODEL_CATALOG, modelId);
189
207
  }
190
208
 
191
209
  function classifyError(error: unknown): ErrorClassification {
@@ -27,6 +27,10 @@ import { parseCodexEvent } from './codex-parser';
27
27
  const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {
28
28
  'gpt-5.4': { rank: 2 },
29
29
  'gpt-5.5': { rank: 3 },
30
+ 'gpt-5.6': { rank: 3 },
31
+ 'gpt-5.6-sol': { rank: 3 },
32
+ 'gpt-5.6-terra': { rank: 2 },
33
+ 'gpt-5.6-luna': { rank: 1 },
30
34
  };
31
35
 
32
36
  const LEVEL_MAPPING: Readonly<Record<ModelLevel, LevelModelSpec>> = {
@@ -100,8 +100,9 @@ export function validateModelIdFromCatalog(
100
100
  ): string | null | undefined {
101
101
  if (!modelId) return modelId;
102
102
  if (catalog[modelId] !== undefined) return modelId;
103
+ const validModels = Object.keys(catalog).join(', ');
103
104
  throw new InvalidProviderModelError(
104
- `Invalid model "${modelId}" for provider "${provider}". Use a model listed in provider settings/catalog.`
105
+ `Invalid model "${modelId}" for provider "${provider}". Valid models: ${validModels}.`
105
106
  );
106
107
  }
107
108
 
@@ -13,7 +13,7 @@ import type {
13
13
 
14
14
  const OUTPUT_FORMATS: readonly OutputFormat[] = ['text', 'json', 'stream-json'];
15
15
  const MODEL_LEVELS: readonly ModelLevel[] = ['level1', 'level2', 'level3'];
16
- const REASONING_EFFORTS: readonly ReasoningEffort[] = ['low', 'medium', 'high', 'xhigh'];
16
+ const REASONING_EFFORTS: readonly ReasoningEffort[] = ['low', 'medium', 'high', 'xhigh', 'max'];
17
17
  const CLI_FEATURE_FIELDS = [
18
18
  'supportsOutputFormat',
19
19
  'supportsStreamJson',
@@ -22,6 +22,7 @@ const CLI_FEATURE_FIELDS = [
22
22
  'supportsIncludePartials',
23
23
  'supportsVerbose',
24
24
  'supportsModel',
25
+ 'supportsEffort',
25
26
  'supportsJson',
26
27
  'supportsOutputSchema',
27
28
  'supportsDir',
@@ -91,6 +91,7 @@ export type {
91
91
  ProviderId,
92
92
  ProviderRegistryEntry,
93
93
  RedactionMetadata,
94
+ ReasoningEffort,
94
95
  ResolvedGatewayBuildOptions,
95
96
  ResolvedModelSpec,
96
97
  ResultEvent,
@@ -160,7 +160,7 @@ export const providerRegistry = [
160
160
  capabilities: {
161
161
  ...STANDARD_CAPABILITIES,
162
162
  jsonSchema: true,
163
- reasoningEffort: false,
163
+ reasoningEffort: true,
164
164
  },
165
165
  docs: {
166
166
  label: 'Claude',
@@ -61,7 +61,7 @@ type MutableModelSpec = {
61
61
  };
62
62
 
63
63
  const MODEL_LEVELS: readonly ModelLevel[] = ['level1', 'level2', 'level3'];
64
- const REASONING_EFFORTS: readonly ReasoningEffort[] = ['low', 'medium', 'high', 'xhigh'];
64
+ const REASONING_EFFORTS: readonly ReasoningEffort[] = ['low', 'medium', 'high', 'xhigh', 'max'];
65
65
  const settingsModule: unknown = require('../../lib/settings');
66
66
  const providerDetectionModule: unknown = require('../../lib/provider-detection');
67
67
  const claudeAuthModule: unknown = require('../../lib/settings/claude-auth');
@@ -420,7 +420,13 @@ function optionalModelLevel(value: unknown, field: string): ModelLevel | undefin
420
420
 
421
421
  function optionalReasoningEffort(value: unknown, field: string): ReasoningEffort | undefined {
422
422
  if (value === undefined) return undefined;
423
- if (value === 'low' || value === 'medium' || value === 'high' || value === 'xhigh') {
423
+ if (
424
+ value === 'low' ||
425
+ value === 'medium' ||
426
+ value === 'high' ||
427
+ value === 'xhigh' ||
428
+ value === 'max'
429
+ ) {
424
430
  return value;
425
431
  }
426
432
  throw new Error(`${field} must be one of: ${REASONING_EFFORTS.join(', ')}.`);
@@ -15,7 +15,7 @@ export type ProviderId = (typeof providerIds)[number];
15
15
  export type ProviderAlias = (typeof providerAliases)[number];
16
16
  export type KnownProviderName = ProviderId | ProviderAlias;
17
17
  export type ModelLevel = 'level1' | 'level2' | 'level3';
18
- export type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh';
18
+ export type ReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
19
19
  export type OutputFormat = 'text' | 'json' | 'stream-json';
20
20
  export type {
21
21
  ProviderCapabilities,
@@ -95,6 +95,7 @@ export interface ClaudeCliFeatures extends BaseCliFeatures {
95
95
  readonly supportsIncludePartials: boolean;
96
96
  readonly supportsVerbose: boolean;
97
97
  readonly supportsModel: boolean;
98
+ readonly supportsEffort: boolean;
98
99
  }
99
100
 
100
101
  export interface CodexCliFeatures extends BaseCliFeatures {
@@ -186,6 +187,7 @@ export interface CliFeatureOverrides {
186
187
  readonly supportsIncludePartials?: boolean;
187
188
  readonly supportsVerbose?: boolean;
188
189
  readonly supportsModel?: boolean;
190
+ readonly supportsEffort?: boolean;
189
191
  readonly supportsJson?: boolean;
190
192
  readonly supportsOutputSchema?: boolean;
191
193
  readonly supportsDir?: boolean;
@@ -81,8 +81,15 @@ class AgentWrapper {
81
81
  // LIVENESS DETECTION - Track output freshness to detect stuck agents
82
82
  /** @type {number | null} */
83
83
  this.lastOutputTime = null; // Timestamp of last output received
84
+ /** @type {number | null} */
85
+ this.taskStartedAt = null; // Timestamp used for absolute task timeout
84
86
  /** @type {NodeJS.Timeout | null} */
85
87
  this.livenessCheckInterval = null; // Interval for health checks
88
+ this.consecutiveStaleWarnings = 0;
89
+ this.livenessTerminationStarted = false;
90
+ this.livenessTerminationContext = null;
91
+ this.livenessTerminationAttempts = 0;
92
+ this.livenessTerminationRetryAt = 0;
86
93
  this.staleDuration = normalizedConfig.staleDuration;
87
94
  this.enableLivenessCheck = normalizedConfig.enableLivenessCheck;
88
95
 
@@ -499,8 +506,8 @@ class AgentWrapper {
499
506
  * Kill current task
500
507
  * @private
501
508
  */
502
- _killTask() {
503
- return killTask(this);
509
+ _killTask(reason) {
510
+ return killTask(this, reason);
504
511
  }
505
512
 
506
513
  /**
@@ -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);
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Deterministic attach socket paths.
3
+ *
4
+ * Unix-domain sockets have a small path budget (104 bytes on macOS). Keep the
5
+ * live socket namespace independent from HOME while retaining one namespace
6
+ * per OS user and Zeroshot home.
7
+ */
8
+
9
+ const crypto = require('crypto');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+ const path = require('path');
13
+
14
+ const SOCKET_ROOT = process.platform === 'win32' ? null : '/tmp';
15
+ const SOCKET_DIR_MODE = 0o700;
16
+
17
+ function shortHash(value) {
18
+ return crypto.createHash('sha256').update(value).digest('hex').slice(0, 16);
19
+ }
20
+
21
+ function userNamespace() {
22
+ if (typeof process.getuid === 'function') {
23
+ return String(process.getuid());
24
+ }
25
+ return shortHash(os.userInfo().username);
26
+ }
27
+
28
+ function resolveHomeDir(env = process.env) {
29
+ return env.ZEROSHOT_HOME || env.HOME || env.USERPROFILE || os.homedir();
30
+ }
31
+
32
+ function getSocketDir(homeDir = resolveHomeDir()) {
33
+ if (process.platform === 'win32') {
34
+ return path.join(homeDir, '.zeroshot', 'sockets');
35
+ }
36
+ return path.join(SOCKET_ROOT, `zeroshot-${userNamespace()}-${shortHash(homeDir)}`);
37
+ }
38
+
39
+ function assertSafeSocketDir(socketDir) {
40
+ const stat = fs.lstatSync(socketDir);
41
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
42
+ throw new Error(`Attach socket path is not a directory: ${socketDir}`);
43
+ }
44
+ if (typeof process.getuid === 'function' && stat.uid !== process.getuid()) {
45
+ throw new Error(`Attach socket directory is not owned by the current user: ${socketDir}`);
46
+ }
47
+ }
48
+
49
+ function ensureOwnedDirectory(socketDir) {
50
+ fs.mkdirSync(socketDir, { recursive: true, mode: SOCKET_DIR_MODE });
51
+ assertSafeSocketDir(socketDir);
52
+ if (process.platform !== 'win32') {
53
+ fs.chmodSync(socketDir, SOCKET_DIR_MODE);
54
+ }
55
+ return socketDir;
56
+ }
57
+
58
+ function ensureSocketDir(homeDir = resolveHomeDir()) {
59
+ const socketDir = getSocketDir(homeDir);
60
+ return ensureOwnedDirectory(socketDir);
61
+ }
62
+
63
+ function getTaskSocketPath(taskId, homeDir = resolveHomeDir()) {
64
+ return path.join(ensureSocketDir(homeDir), `${taskId}.sock`);
65
+ }
66
+
67
+ function getAgentSocketPath(clusterId, agentId, homeDir = resolveHomeDir()) {
68
+ const clusterDir = path.join(ensureSocketDir(homeDir), clusterId);
69
+ ensureOwnedDirectory(clusterDir);
70
+ return path.join(clusterDir, `${agentId}.sock`);
71
+ }
72
+
73
+ function getClusterSocketPath(clusterId, homeDir = resolveHomeDir()) {
74
+ return path.join(ensureSocketDir(homeDir), `${clusterId}.sock`);
75
+ }
76
+
77
+ module.exports = {
78
+ SOCKET_DIR_MODE,
79
+ resolveHomeDir,
80
+ getSocketDir,
81
+ ensureSocketDir,
82
+ getTaskSocketPath,
83
+ getAgentSocketPath,
84
+ getClusterSocketPath,
85
+ };
@@ -21,6 +21,9 @@ const { getProvider } = require('./providers');
21
21
  const { CAPABILITIES } = require('./providers/capabilities');
22
22
  const { GUIDANCE_TOPICS } = require('./guidance-topics');
23
23
 
24
+ const REASONING_EFFORTS = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
25
+ const REASONING_EFFORT_LABEL = REASONING_EFFORTS.join('|');
26
+
24
27
  const HOOK_ACTION_TOPIC_CONTRACTS = Object.freeze({
25
28
  verify_pull_request: Object.freeze([
26
29
  { topic: 'CLUSTER_COMPLETE', keys: null },
@@ -2054,14 +2057,13 @@ function validateProviderSettings(provider, providerSettings) {
2054
2057
  providerModule.validateModelId(override.model);
2055
2058
  }
2056
2059
  if (override?.reasoningEffort && !providerSupportsCapability(provider, 'reasoningEffort')) {
2057
- throw new Error(`reasoningEffort overrides are only supported for Codex and Opencode`);
2060
+ throw new Error(
2061
+ `reasoningEffort overrides are only supported for Claude, Codex, and Opencode`
2062
+ );
2058
2063
  }
2059
- if (
2060
- override?.reasoningEffort &&
2061
- !['low', 'medium', 'high', 'xhigh'].includes(override.reasoningEffort)
2062
- ) {
2064
+ if (override?.reasoningEffort && !REASONING_EFFORTS.includes(override.reasoningEffort)) {
2063
2065
  throw new Error(
2064
- `Invalid reasoningEffort "${override.reasoningEffort}" (low|medium|high|xhigh)`
2066
+ `Invalid reasoningEffort "${override.reasoningEffort}" (${REASONING_EFFORT_LABEL})`
2065
2067
  );
2066
2068
  }
2067
2069
  }
@@ -2172,12 +2174,9 @@ function validateModelRulesSupport(agent, provider, providerModule, levels, warn
2172
2174
  function validateReasoningEffortSupport(agent, provider, warnings) {
2173
2175
  if (agent.reasoningEffort && !providerSupportsCapability(provider, 'reasoningEffort')) {
2174
2176
  warnings.push(`Agent "${agent.id}" sets reasoningEffort but ${provider} does not support it`);
2175
- } else if (
2176
- agent.reasoningEffort &&
2177
- !['low', 'medium', 'high', 'xhigh'].includes(agent.reasoningEffort)
2178
- ) {
2177
+ } else if (agent.reasoningEffort && !REASONING_EFFORTS.includes(agent.reasoningEffort)) {
2179
2178
  warnings.push(
2180
- `Agent "${agent.id}" has invalid reasoningEffort "${agent.reasoningEffort}" (low|medium|high|xhigh)`
2179
+ `Agent "${agent.id}" has invalid reasoningEffort "${agent.reasoningEffort}" (${REASONING_EFFORT_LABEL})`
2181
2180
  );
2182
2181
  }
2183
2182
  }