specrails-hub 1.55.0 → 1.55.1

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.55.0",
3
+ "version": "1.55.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -340,12 +340,15 @@ class ChatManager {
340
340
  `When creating or updating tickets, write directly to this JSON file.\n\n` +
341
341
  `IMPORTANT: Be efficient. Minimize tool calls. Only read files that are directly relevant. ` +
342
342
  `Do not explore broadly — focus on the specific task.`;
343
+ const scopedBase = `${base}\n\n` +
344
+ `When "Specrails Tickets" or "OpenSpec Specs" sections are present below, treat them as authoritative project context. ` +
345
+ `For roadmap-style requests like "suggest the next best spec", ground the answer in that context, avoid duplicates, and propose one concrete next spec instead of generic directions.`;
343
346
  if (!scope || !this._cwd)
344
- return base;
347
+ return scopedBase;
345
348
  const prefix = (0, context_scope_1.buildScopedSystemPromptPrefix)(scope, this._cwd);
346
349
  if (!prefix)
347
- return base;
348
- return `${base}\n\n${prefix}`;
350
+ return scopedBase;
351
+ return `${scopedBase}\n\n${prefix}`;
349
352
  }
350
353
  isActive(conversationId) {
351
354
  return this._activeProcesses.has(conversationId);
@@ -434,8 +437,18 @@ class ChatManager {
434
437
  const scopeFlags = conversationScope && this._adapter.id === 'claude'
435
438
  ? (0, context_scope_1.toolFlagsForScope)(conversationScope).args
436
439
  : [];
440
+ let promptForAdapter = resolvedText;
441
+ if (conversation.kind === 'explore' && this._adapter.id === 'codex' && conversationScope && this._cwd) {
442
+ const scopedContext = (0, context_scope_1.buildScopedSystemPromptPrefix)(conversationScope, this._cwd);
443
+ if (scopedContext) {
444
+ promptForAdapter =
445
+ `Project context selected in Add Spec. Use it to avoid duplicate specs and to make project-specific recommendations.\n\n` +
446
+ `${scopedContext}\n\n` +
447
+ `## User turn\n\n${resolvedText}`;
448
+ }
449
+ }
437
450
  let args = this._adapter.buildArgs(action, {
438
- prompt: resolvedText,
451
+ prompt: promptForAdapter,
439
452
  systemPrompt,
440
453
  model,
441
454
  sessionId: conversation.session_id ?? undefined,
@@ -18,6 +18,41 @@ function findHubRoot() {
18
18
  return null;
19
19
  }
20
20
  const HUB_ROOT = findHubRoot();
21
+ function builtInCommand(commandPath, commandArgs) {
22
+ if (commandPath !== 'specrails:explore-spec')
23
+ return null;
24
+ return `You are a senior product engineer helping the user shape one backlog spec inside specrails-hub's Explore Spec experience.
25
+
26
+ Do not use any local skill, slash-command workflow, or repository-change workflow. Stay inside this chat and shape the draft ticket only. Do not inspect active change folders unless the user explicitly asks about them, and do not create or modify files. The hub commits the final ticket only when the user clicks Create Spec.
27
+
28
+ Your job is to maintain a live draft. After every assistant turn that changes draft state, end your message with a fenced \`spec-draft\` JSON block. The visible prose should match the user's language. Draft fields must be written in English.
29
+
30
+ Use this draft schema. Omit fields you do not want to update:
31
+
32
+ \`\`\`spec-draft
33
+ {
34
+ "title": "Concise, action-oriented title",
35
+ "description": "## Problem Statement\\n2-3 sentences.\\n\\n## Proposed Solution\\n3-5 sentences.\\n\\n## Out of Scope\\n- bullet\\n\\n## Technical Considerations\\n- bullet\\n\\n## Estimated Complexity\\nMedium - one sentence justification.",
36
+ "labels": ["short-label"],
37
+ "priority": "low | medium | high | critical",
38
+ "acceptanceCriteria": ["Short, testable criterion"],
39
+ "chips": ["Up to 3 short replies"],
40
+ "ready": false
41
+ }
42
+ \`\`\`
43
+
44
+ Rules:
45
+ - Ask only the clarifying questions genuinely needed to make the spec concrete.
46
+ - Keep visible replies brief.
47
+ - Set \`ready: true\` only when the draft has a title, description, acceptance criteria, and no outstanding clarifying question.
48
+ - Never call \`/specrails:propose-spec\`, \`/specrails:implement\`, or any slash command with side effects.
49
+
50
+ The user's idea follows below. Begin the Explore Spec conversation.
51
+
52
+ ---
53
+
54
+ ${commandArgs}`.trim();
55
+ }
21
56
  /**
22
57
  * Try to find a command/skill .md file for the given command path parts
23
58
  * within the given base directory. Returns the resolved path or null.
@@ -49,6 +84,9 @@ function resolveCommand(command, cwd) {
49
84
  const commandPath = match[1];
50
85
  const commandArgs = match[2].trim();
51
86
  const parts = commandPath.split(':');
87
+ const builtIn = builtInCommand(commandPath, commandArgs);
88
+ if (builtIn)
89
+ return builtIn;
52
90
  // 1. Check the project directory
53
91
  let resolvedPath = findCommandFile(cwd, parts);
54
92
  // 2. Fallback: check the hub's own directory
@@ -46,9 +46,9 @@ invoke ticket-creation slash commands.
46
46
 
47
47
  ## What you must do
48
48
 
49
- - Act as an interactive thinking partner, same stance as
50
- \`/specrails:explore-spec\`: investigate just enough, ask only the
51
- questions you need, surface trade-offs, propose a concrete shape.
49
+ - Act as an interactive thinking partner for this ticket draft: investigate
50
+ just enough, ask only the questions you need, surface trade-offs, propose a
51
+ concrete shape.
52
52
  - Maintain the structured live draft via fenced \`spec-draft\` JSON blocks at
53
53
  the end of every turn that updates draft state. The hub parses these blocks
54
54
  and updates the user's draft pane; the block itself is stripped from the
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.stripSpecMetadataSections = stripSpecMetadataSections;
40
40
  exports.extractShortSummary = extractShortSummary;
41
+ exports.deriveFallbackShortSummary = deriveFallbackShortSummary;
41
42
  exports.formatDescriptionWithCriteria = formatDescriptionWithCriteria;
42
43
  exports.resolveDefaultSpecModel = resolveDefaultSpecModel;
43
44
  exports.createProjectRouter = createProjectRouter;
@@ -242,6 +243,22 @@ function extractShortSummary(buffer) {
242
243
  const body = m[1].trim();
243
244
  return body.length > 0 ? body : null;
244
245
  }
246
+ function deriveFallbackShortSummary(title, description) {
247
+ const plain = description
248
+ .replace(/```[\s\S]*?```/g, ' ')
249
+ .replace(/^#{1,6}\s+.*$/gm, ' ')
250
+ .replace(/^\s*[-*]\s+/gm, '')
251
+ .replace(/\[[^\]]+\]\([^)]+\)/g, (m) => m.match(/\[([^\]]+)\]/)?.[1] ?? '')
252
+ .replace(/[`*_>]/g, '')
253
+ .replace(/\s+/g, ' ')
254
+ .trim();
255
+ const source = plain || title.trim();
256
+ if (!source)
257
+ return null;
258
+ const sentence = source.match(/^(.{24,}?[.!?])(?:\s|$)/)?.[1] ?? source;
259
+ const capped = sentence.length > 160 ? `${sentence.slice(0, 157).trimEnd()}...` : sentence;
260
+ return (0, ticket_store_1.clampShortSummary)(capped);
261
+ }
245
262
  /**
246
263
  * Fold an `acceptanceCriteria` array into a ticket description body, writing
247
264
  * (or replacing) a `## Acceptance Criteria` section.
@@ -1757,11 +1774,15 @@ function createProjectRouter(registry) {
1757
1774
  const dedupRule = quickScope.specrails
1758
1775
  ? `- The "Specrails Tickets" section above lists every ticket already in the backlog. Do NOT propose a duplicate or a near-duplicate of any of them. If the user's idea is already covered by an existing ticket, say so in "Problem Statement" and pick a *different* angle / sub-feature / next step that builds on the existing one — do not repeat it.\n`
1759
1776
  : '';
1777
+ const backlogRecommendationRule = quickScope.specrails
1778
+ ? `- If the user's idea asks for the "next best spec" or a backlog recommendation, use the existing tickets and OpenSpec context to choose one concrete next spec. Do not respond with generic product directions.\n`
1779
+ : '';
1760
1780
  let baseSystemPrompt = `You are a senior product engineer generating a structured spec proposal.\n\n` +
1761
1781
  (specsPrefix ? `${specsPrefix}\n\n` : '') +
1762
1782
  `RULES:\n` +
1763
1783
  `${codebaseRule}\n` +
1764
1784
  dedupRule +
1785
+ backlogRecommendationRule +
1765
1786
  `- Do NOT create files, tickets, or issues.\n` +
1766
1787
  `- Output ONLY the structured markdown below. No preamble, no explanation.\n\n` +
1767
1788
  `REQUIRED FORMAT:\n` +
@@ -2235,6 +2256,9 @@ function createProjectRouter(registry) {
2235
2256
  .trim();
2236
2257
  }
2237
2258
  }
2259
+ if (bodyShortSummary === null) {
2260
+ bodyShortSummary = deriveFallbackShortSummary(rawTitle, descriptionForStore);
2261
+ }
2238
2262
  try {
2239
2263
  const filePath = ticketPath(req);
2240
2264
  const now = new Date().toISOString();
@@ -29,6 +29,7 @@ const CODEX_MODELS = [
29
29
  { value: 'gpt-5.3-codex', label: 'GPT-5.3 Codex' },
30
30
  ];
31
31
  const SANDBOX_FLAGS = ['--sandbox', 'workspace-write'];
32
+ const RAIL_SANDBOX_FLAGS = ['--sandbox', 'danger-full-access'];
32
33
  // `codex exec resume` does NOT accept `--sandbox` (the flag only exists on
33
34
  // `codex exec`); pass the policy as a `-c` config override instead so the
34
35
  // resumed session honours workspace-write even when the per-project
@@ -44,7 +45,21 @@ function fold(systemPrompt, prompt) {
44
45
  function buildCodexArgs(action, opts) {
45
46
  const args = [];
46
47
  switch (action) {
47
- case 'chat-turn':
48
+ case 'chat-turn': {
49
+ // chat-turn (Explore) spawns codex from the hub-managed explore-cwd,
50
+ // which already ships an AGENTS.md with the Explore stance. Folding the
51
+ // hub's system prompt into the positional argv would double-inject the
52
+ // framing AND, because the user message in Explore is often very short
53
+ // ("quiero hacer un tetris"), the long system text dominates the prompt
54
+ // and codex responds to the system instructions instead of the user.
55
+ // Trust AGENTS.md and pass only the user prompt.
56
+ args.push('exec', '--json', ...SANDBOX_FLAGS, SKIP_GIT_CHECK);
57
+ args.push(opts.prompt);
58
+ args.push('--model', opts.model);
59
+ if (opts.extraArgs)
60
+ args.push(...opts.extraArgs);
61
+ return args;
62
+ }
48
63
  case 'spec-gen':
49
64
  case 'agent-refine':
50
65
  case 'auto-title':
@@ -56,7 +71,21 @@ function buildCodexArgs(action, opts) {
56
71
  args.push(...opts.extraArgs);
57
72
  return args;
58
73
  }
59
- case 'chat-resume':
74
+ case 'chat-resume': {
75
+ if (!opts.sessionId) {
76
+ throw new Error(`${action} requires sessionId`);
77
+ }
78
+ // See chat-turn note: AGENTS.md in explore-cwd carries the Explore
79
+ // framing; the per-turn argv must stay user-text-only so codex doesn't
80
+ // mistake the system prompt for the user request.
81
+ args.push('exec', 'resume', '--json', ...SANDBOX_RESUME_FLAGS, SKIP_GIT_CHECK);
82
+ args.push(opts.sessionId);
83
+ args.push(opts.prompt);
84
+ args.push('--model', opts.model);
85
+ if (opts.extraArgs)
86
+ args.push(...opts.extraArgs);
87
+ return args;
88
+ }
60
89
  case 'setup-enrich-resume': {
61
90
  if (!opts.sessionId) {
62
91
  throw new Error(`${action} requires sessionId`);
@@ -70,10 +99,12 @@ function buildCodexArgs(action, opts) {
70
99
  return args;
71
100
  }
72
101
  case 'rail-job': {
73
- // Rail-job runs in the project root (a git repo). Sandbox still
74
- // workspace-write so codex can apply file changes; skip-git-check kept
75
- // for symmetry harmless when a git repo IS present.
76
- args.push('exec', '--json', ...SANDBOX_FLAGS, SKIP_GIT_CHECK);
102
+ // Rail jobs are headless implementation pipelines. They must run repo
103
+ // inspection, edits, tests, and git probes without interactive approval.
104
+ // On Windows, Codex's workspace-write sandbox can fail before the first
105
+ // shell command with `windows sandbox: spawn setup refresh`; full access
106
+ // matches the existing fully-autonomous rail contract.
107
+ args.push('exec', '--json', ...RAIL_SANDBOX_FLAGS, SKIP_GIT_CHECK);
77
108
  args.push(fold(opts.systemPrompt, opts.prompt));
78
109
  args.push('--model', opts.model);
79
110
  if (opts.extraArgs)
@@ -543,7 +543,9 @@ class QueueManager {
543
543
  if (/\/(specrails|sr):(implement|batch-implement)\b/.test(commandToRun)) {
544
544
  systemAppend += '\n\nIMPORTANT: The ticket/spec data for this project is stored locally in .specrails/local-tickets.json. ' +
545
545
  'You MUST read specs from this file. Do NOT attempt to fetch tickets from Jira, Linear, GitHub Issues, or any other external tracker. ' +
546
- 'The #<id> references in the command correspond to ticket IDs inside .specrails/local-tickets.json.';
546
+ 'The #<id> references in the command correspond to ticket IDs inside .specrails/local-tickets.json. ' +
547
+ 'Do NOT require jq to inspect this file; on Windows or when jq is unavailable, use PowerShell (`Get-Content .specrails/local-tickets.json -Raw | ConvertFrom-Json`) or Node.js built-ins. ' +
548
+ 'When running tests, use the project-defined scripts and package manager commands as-is; do NOT add Jest-only flags such as --runInBand to Vitest commands.';
547
549
  const attachmentContext = this._buildImplementAttachmentContext(commandToRun);
548
550
  if (attachmentContext) {
549
551
  systemAppend += attachmentContext;
@@ -56,19 +56,25 @@ function transformClaudeArgsForWindows(args) {
56
56
  }
57
57
  // Codex `exec` flags we currently use that take a value (rest are
58
58
  // boolean). Update if we ever pass new value-bearing flags.
59
- const CODEX_EXEC_VALUE_FLAGS = new Set(['--model']);
59
+ const CODEX_EXEC_VALUE_FLAGS = new Set(['--model', '--sandbox', '-c']);
60
60
  function transformCodexArgsForWindows(args) {
61
- // Expected shape: `exec [...flags] <prompt> [...flags]`.
61
+ // Expected shapes:
62
+ // exec [...flags] <prompt> [...flags]
63
+ // exec resume [...flags] <sessionId> <prompt> [...flags]
62
64
  if (args.length === 0 || args[0] !== 'exec') {
63
65
  return { args, stdinPayload: null };
64
66
  }
65
67
  const out = ['exec'];
68
+ const isResume = args[1] === 'resume';
69
+ if (isResume)
70
+ out.push('resume');
66
71
  let stdin = null;
67
72
  let promptReplacedIdx = -1;
68
- let i = 1;
73
+ let positionalCount = 0;
74
+ let i = isResume ? 2 : 1;
69
75
  while (i < args.length) {
70
76
  const a = args[i];
71
- if (a.startsWith('--')) {
77
+ if (a.startsWith('-') && a !== '-') {
72
78
  out.push(a);
73
79
  if (CODEX_EXEC_VALUE_FLAGS.has(a) && i + 1 < args.length) {
74
80
  out.push(args[i + 1]);
@@ -78,8 +84,9 @@ function transformCodexArgsForWindows(args) {
78
84
  i += 1;
79
85
  continue;
80
86
  }
81
- // First non-flag positional is the prompt.
82
- if (stdin === null) {
87
+ positionalCount += 1;
88
+ const isPrompt = isResume ? positionalCount === 2 : positionalCount === 1;
89
+ if (isPrompt && stdin === null) {
83
90
  stdin = a;
84
91
  promptReplacedIdx = out.length;
85
92
  out.push('-');