specrails-hub 1.25.4 → 1.26.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-hub",
3
- "version": "1.25.4",
3
+ "version": "1.26.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -90,9 +90,21 @@ class ProjectRegistry {
90
90
  }
91
91
  }
92
92
  };
93
+ // Per-project zombie timeout (stored in queue_state)
94
+ let projectZombieTimeout;
95
+ try {
96
+ const row = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.zombie_timeout_ms'`).get();
97
+ if (row) {
98
+ const parsed = parseInt(row.value, 10);
99
+ if (!isNaN(parsed) && parsed > 0)
100
+ projectZombieTimeout = parsed;
101
+ }
102
+ }
103
+ catch { /* queue_state table may not exist yet */ }
93
104
  const webhookManager = this._webhookManager;
94
105
  const railJobs = new Map();
95
106
  const queueManager = new queue_manager_1.QueueManager(boundBroadcast, db, undefined, project.path, {
107
+ zombieTimeoutMs: projectZombieTimeout,
96
108
  provider: project.provider ?? 'claude',
97
109
  getCostAlertThreshold: () => {
98
110
  const val = (0, hub_db_1.getHubSetting)(this._hubDb, 'cost_alert_threshold_usd');
@@ -454,7 +454,9 @@ function createProjectRouter(registry) {
454
454
  const config = (0, config_1.getConfig)(project.path, db, project.name);
455
455
  const dailyBudgetRaw = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.daily_budget_usd'`).get()?.value;
456
456
  const dailyBudgetUsd = dailyBudgetRaw != null ? parseFloat(dailyBudgetRaw) : null;
457
- res.json({ ...config, dailyBudgetUsd });
457
+ const zombieTimeoutRaw = db.prepare(`SELECT value FROM queue_state WHERE key = 'config.zombie_timeout_ms'`).get()?.value;
458
+ const zombieTimeoutMs = zombieTimeoutRaw != null ? parseInt(zombieTimeoutRaw, 10) : null;
459
+ res.json({ ...config, dailyBudgetUsd, zombieTimeoutMs });
458
460
  }
459
461
  catch (err) {
460
462
  console.error('[project-router] config error:', err);
@@ -462,8 +464,8 @@ function createProjectRouter(registry) {
462
464
  }
463
465
  });
464
466
  router.post('/:projectId/config', (req, res) => {
465
- const { active, labelFilter, dailyBudgetUsd } = req.body ?? {};
466
- const { db } = ctx(req);
467
+ const { active, labelFilter, dailyBudgetUsd, zombieTimeoutMs } = req.body ?? {};
468
+ const { db, queueManager } = ctx(req);
467
469
  try {
468
470
  if (active !== undefined) {
469
471
  db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.active_tracker', ?)`).run(active ?? '');
@@ -479,6 +481,15 @@ function createProjectRouter(registry) {
479
481
  db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.daily_budget_usd', ?)`).run(String(dailyBudgetUsd));
480
482
  }
481
483
  }
484
+ if (zombieTimeoutMs !== undefined) {
485
+ if (zombieTimeoutMs === null) {
486
+ db.prepare(`DELETE FROM queue_state WHERE key = 'config.zombie_timeout_ms'`).run();
487
+ }
488
+ else if (typeof zombieTimeoutMs === 'number' && zombieTimeoutMs > 0) {
489
+ db.prepare(`INSERT OR REPLACE INTO queue_state (key, value) VALUES ('config.zombie_timeout_ms', ?)`).run(String(zombieTimeoutMs));
490
+ }
491
+ queueManager.setZombieTimeout(typeof zombieTimeoutMs === 'number' && zombieTimeoutMs > 0 ? zombieTimeoutMs : queue_manager_1.DEFAULT_ZOMBIE_TIMEOUT_MS);
492
+ }
482
493
  res.json({ ok: true });
483
494
  }
484
495
  catch (err) {
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.QueueManager = exports.JobAlreadyTerminalError = exports.JobNotFoundError = exports.CodexNotFoundError = exports.ClaudeNotFoundError = void 0;
6
+ exports.QueueManager = exports.JobAlreadyTerminalError = exports.JobNotFoundError = exports.CodexNotFoundError = exports.ClaudeNotFoundError = exports.DEFAULT_ZOMBIE_TIMEOUT_MS = void 0;
7
7
  const child_process_1 = require("child_process");
8
8
  const readline_1 = require("readline");
9
9
  const uuid_1 = require("uuid");
@@ -14,7 +14,7 @@ const hooks_1 = require("./hooks");
14
14
  const db_1 = require("./db");
15
15
  const LOG_BUFFER_MAX = 5000;
16
16
  const LOG_BUFFER_DROP = 1000;
17
- const DEFAULT_ZOMBIE_TIMEOUT_MS = 300_000; // 5 minutes
17
+ exports.DEFAULT_ZOMBIE_TIMEOUT_MS = 1_800_000; // 30 minutes
18
18
  // ─── Error classes ────────────────────────────────────────────────────────────
19
19
  class ClaudeNotFoundError extends Error {
20
20
  constructor() {
@@ -127,7 +127,7 @@ class QueueManager {
127
127
  ? parseInt(process.env.WM_ZOMBIE_TIMEOUT_MS, 10)
128
128
  : null;
129
129
  this._zombieTimeoutMs = options?.zombieTimeoutMs
130
- ?? (envTimeout !== null && !isNaN(envTimeout) ? envTimeout : DEFAULT_ZOMBIE_TIMEOUT_MS);
130
+ ?? (envTimeout !== null && !isNaN(envTimeout) ? envTimeout : exports.DEFAULT_ZOMBIE_TIMEOUT_MS);
131
131
  if (this._db) {
132
132
  this._restoreFromDb();
133
133
  }
@@ -135,6 +135,13 @@ class QueueManager {
135
135
  setCommands(commands) {
136
136
  this._commands = commands;
137
137
  }
138
+ setZombieTimeout(ms) {
139
+ this._zombieTimeoutMs = ms;
140
+ // If a job is currently running, reset the timer with the new value
141
+ if (this._activeJobId) {
142
+ this._resetZombieTimer();
143
+ }
144
+ }
138
145
  // ─── Public API ─────────────────────────────────────────────────────────────
139
146
  enqueue(command, priorityOrOpts, opts) {
140
147
  // Support both: enqueue(cmd, priority, opts) and enqueue(cmd, opts)
@@ -355,7 +362,15 @@ class QueueManager {
355
362
  // Headless mode: when --yes is in the command, instruct Claude to auto-proceed
356
363
  // (stdin is ignored in spawned processes, so no user confirmation is possible)
357
364
  if (job.command.includes('--yes')) {
358
- systemAppend += '\n\nIMPORTANT: This command is running in headless/unattended mode (--yes flag). Do NOT wait for user confirmation at any step. Auto-proceed with "yes" for all confirmation prompts. Skip any "Wait for user confirmation" instructions.';
365
+ systemAppend += '\n\nCRITICAL FULLY AUTONOMOUS MODE (--yes flag):\n' +
366
+ 'This pipeline is running headless with NO human operator. stdin is disconnected — nobody can reply.\n' +
367
+ '- NEVER ask for approval, confirmation, review, or feedback. There is nobody to answer.\n' +
368
+ '- NEVER output prompts like "Reply with approved", "Do you want to proceed?", "Please confirm", or "Ready for review".\n' +
369
+ '- NEVER stop between pipeline phases to wait for input. Run ALL phases end-to-end without pausing.\n' +
370
+ '- When there are multiple options or decisions, always choose the RECOMMENDED option and proceed.\n' +
371
+ '- Auto-approve all proposals, designs, and artifacts. Treat everything as "approved" by default.\n' +
372
+ '- Skip any instructions that say "wait for user", "present for review", or "ask the user".\n' +
373
+ '- The pipeline must complete fully from start to finish in a single uninterrupted run.';
359
374
  }
360
375
  // Local ticket store: implement/batch-implement jobs must read specs from
361
376
  // .specrails/local-tickets.json — never from external trackers like Jira/Linear.