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 +1 -1
- package/server/dist/chat-manager.js +17 -4
- package/server/dist/command-resolver.js +38 -0
- package/server/dist/explore-cwd-manager.js +3 -3
- package/server/dist/project-router.js +24 -0
- package/server/dist/providers/codex-adapter.js +37 -6
- package/server/dist/queue-manager.js +3 -1
- package/server/dist/util/cli-prompt.js +13 -6
package/package.json
CHANGED
|
@@ -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
|
|
347
|
+
return scopedBase;
|
|
345
348
|
const prefix = (0, context_scope_1.buildScopedSystemPromptPrefix)(scope, this._cwd);
|
|
346
349
|
if (!prefix)
|
|
347
|
-
return
|
|
348
|
-
return `${
|
|
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:
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
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('-');
|