@the-open-engine/zeroshot 6.7.1 → 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.
@@ -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
+ };