specrails-desktop 2.7.0 → 2.8.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
|
@@ -29,13 +29,15 @@ function isCodexBetaDisabled() {
|
|
|
29
29
|
const v = process.env.SPECRAILS_CODEX_BETA ?? process.env.SPECRAILS_HUB_CODEX_BETA;
|
|
30
30
|
return v === '0';
|
|
31
31
|
}
|
|
32
|
-
// Gemini is
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
// Gemini is enabled by DEFAULT now that its stream-json schema and the full rails
|
|
33
|
+
// pipeline (architect→developer→reviewer delegation, headless agent loading, the
|
|
34
|
+
// MAX_TURNS resume + reviewer gate) are validated against the live binary
|
|
35
|
+
// (0.46/0.47). Emergency rollback: SPECRAILS_GEMINI_BETA=0 forces it back to
|
|
36
|
+
// "unavailable" without redeploying — parity with codex's SPECRAILS_CODEX_BETA.
|
|
37
|
+
// The adapter is always registered (pricing/getAdapter work); only project
|
|
38
|
+
// selection is gated.
|
|
39
|
+
function isGeminiBetaDisabled() {
|
|
40
|
+
return process.env.SPECRAILS_GEMINI_BETA === '0';
|
|
39
41
|
}
|
|
40
42
|
// Theme allow-list. Mirror of THEME_IDS in `client/src/lib/themes.ts` —
|
|
41
43
|
// kept duplicated to avoid pulling client code into the server bundle.
|
|
@@ -160,10 +162,10 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
160
162
|
const gated = { ...providers };
|
|
161
163
|
if (isCodexBetaDisabled())
|
|
162
164
|
gated.codex = false;
|
|
163
|
-
// Gemini
|
|
164
|
-
//
|
|
165
|
-
if (
|
|
166
|
-
|
|
165
|
+
// Gemini: enabled by default; forced unavailable only when
|
|
166
|
+
// SPECRAILS_GEMINI_BETA=0 (emergency rollback, parity with codex).
|
|
167
|
+
if (isGeminiBetaDisabled())
|
|
168
|
+
gated.gemini = false;
|
|
167
169
|
res.json({ ...gated, tiers });
|
|
168
170
|
});
|
|
169
171
|
router.get('/setup-prerequisites', (req, res) => {
|
|
@@ -231,9 +233,9 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
231
233
|
});
|
|
232
234
|
return;
|
|
233
235
|
}
|
|
234
|
-
if (providers.includes('gemini') &&
|
|
236
|
+
if (providers.includes('gemini') && isGeminiBetaDisabled()) {
|
|
235
237
|
res.status(400).json({
|
|
236
|
-
error: 'Gemini provider is
|
|
238
|
+
error: 'Gemini provider is currently disabled (SPECRAILS_GEMINI_BETA=0). Unset or set to 1 to enable.',
|
|
237
239
|
});
|
|
238
240
|
return;
|
|
239
241
|
}
|
|
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports._GEMINI_MIN_VERSION = exports.geminiAdapter = void 0;
|
|
30
30
|
exports._compareSemver = compareSemver;
|
|
31
31
|
const child_process_1 = require("child_process");
|
|
32
|
+
const gemini_agent_ack_1 = require("./gemini-agent-ack");
|
|
32
33
|
const WHICH_CMD = process.platform === 'win32' ? 'where' : 'which';
|
|
33
34
|
// Floor where `--output-format stream-json` + headless `--resume` are available.
|
|
34
35
|
const GEMINI_MIN_VERSION = '0.11.0';
|
|
@@ -231,4 +232,7 @@ exports.geminiAdapter = {
|
|
|
231
232
|
extractResult: extractGeminiResult,
|
|
232
233
|
baselineAgents: () => ['sr-architect', 'sr-developer', 'sr-reviewer'],
|
|
233
234
|
detectInstalled: detectGeminiInstalled,
|
|
235
|
+
// Pre-acknowledge the project's custom subagents so they load in headless
|
|
236
|
+
// `gemini -p` rail spawns (else invoke_agent reports "Subagent not found").
|
|
237
|
+
prepareHeadlessSpawn: gemini_agent_ack_1.acknowledgeGeminiProjectAgents,
|
|
234
238
|
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Gemini headless subagent pre-acknowledgment.
|
|
3
|
+
//
|
|
4
|
+
// gemini 0.46+ DISCOVERS `<project>/.gemini/agents/*.md` but only ENABLES a
|
|
5
|
+
// project's custom subagents after an interactive "New Agents Discovered →
|
|
6
|
+
// Acknowledge and Enable" prompt. That prompt never fires in headless
|
|
7
|
+
// (`gemini -p`) spawns — which is how the desktop runs every rail — so
|
|
8
|
+
// `invoke_agent sr-architect` returns "Subagent not found" and the implement
|
|
9
|
+
// orchestrator silently falls back to a generic agent (the specialised
|
|
10
|
+
// architect/developer/reviewer personas never run in isolation).
|
|
11
|
+
//
|
|
12
|
+
// specrails-core writes the acknowledgment file at install time; this is the
|
|
13
|
+
// defence-in-depth copy the desktop runs right before a gemini rail spawn, so a
|
|
14
|
+
// project installed with an older core (or whose agents changed since install)
|
|
15
|
+
// is still trusted headless. The file gemini reads is
|
|
16
|
+
// `~/.gemini/acknowledgments/agents.json`, shaped
|
|
17
|
+
// { [projectRoot]: { [agentName]: <sha256-hex of the agent .md file> } }
|
|
18
|
+
// where the hash is sha256 of the FULL agent markdown file (verified empirically
|
|
19
|
+
// against gemini 0.47). Entries are MERGED so other projects (and other agents)
|
|
20
|
+
// survive. Best-effort — callers swallow any error; a failure only means the
|
|
21
|
+
// agents need the one-time interactive acknowledge.
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.acknowledgeGeminiProjectAgents = acknowledgeGeminiProjectAgents;
|
|
24
|
+
const crypto_1 = require("crypto");
|
|
25
|
+
const fs_1 = require("fs");
|
|
26
|
+
const os_1 = require("os");
|
|
27
|
+
const path_1 = require("path");
|
|
28
|
+
function ackFilePath() {
|
|
29
|
+
return (0, path_1.join)((0, os_1.homedir)(), '.gemini', 'acknowledgments', 'agents.json');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pre-acknowledge every `<projectPath>/.gemini/agents/*.md` so gemini loads them
|
|
33
|
+
* in headless mode. No-op when the project has no `.gemini/agents` dir or no
|
|
34
|
+
* agent files. The `projectPath` is the key gemini uses (the spawn cwd / repo
|
|
35
|
+
* root), matching what `specrails-core` writes at install.
|
|
36
|
+
*/
|
|
37
|
+
function acknowledgeGeminiProjectAgents(projectPath) {
|
|
38
|
+
const agentsDir = (0, path_1.join)(projectPath, '.gemini', 'agents');
|
|
39
|
+
if (!(0, fs_1.existsSync)(agentsDir))
|
|
40
|
+
return;
|
|
41
|
+
const agentFiles = (0, fs_1.readdirSync)(agentsDir).filter((f) => f.endsWith('.md') && !f.startsWith('_'));
|
|
42
|
+
if (agentFiles.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
const ackPath = ackFilePath();
|
|
45
|
+
let store = {};
|
|
46
|
+
if ((0, fs_1.existsSync)(ackPath)) {
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(ackPath, 'utf8'));
|
|
49
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
50
|
+
store = parsed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Corrupt/unreadable file — start fresh rather than crash the spawn.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const projectEntry = { ...(store[projectPath] ?? {}) };
|
|
58
|
+
for (const file of agentFiles) {
|
|
59
|
+
const content = (0, fs_1.readFileSync)((0, path_1.join)(agentsDir, file), 'utf8');
|
|
60
|
+
projectEntry[file.slice(0, -3)] = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
61
|
+
}
|
|
62
|
+
store[projectPath] = projectEntry;
|
|
63
|
+
(0, fs_1.mkdirSync)((0, path_1.join)((0, os_1.homedir)(), '.gemini', 'acknowledgments'), { recursive: true });
|
|
64
|
+
(0, fs_1.writeFileSync)(ackPath, `${JSON.stringify(store, null, 2)}\n`);
|
|
65
|
+
}
|
|
@@ -1000,6 +1000,19 @@ class QueueManager {
|
|
|
1000
1000
|
};
|
|
1001
1001
|
}
|
|
1002
1002
|
}
|
|
1003
|
+
// Provider-specific filesystem prep before a headless rail spawn. Gemini
|
|
1004
|
+
// uses this to pre-acknowledge the project's custom subagents so they load
|
|
1005
|
+
// in `gemini -p` mode (else invoke_agent reports "Subagent not found" and the
|
|
1006
|
+
// orchestrator silently falls back to a generic agent). No-op for claude/codex.
|
|
1007
|
+
if (this._cwd) {
|
|
1008
|
+
try {
|
|
1009
|
+
adapter.prepareHeadlessSpawn?.(this._cwd);
|
|
1010
|
+
}
|
|
1011
|
+
catch (err) {
|
|
1012
|
+
/* c8 ignore next -- best-effort prep; a failure is non-fatal */
|
|
1013
|
+
console.warn(`[queue-manager] headless-spawn prep failed: ${err.message}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1003
1016
|
// ─── Interactive ultracode branch ──────────────────────────────────────
|
|
1004
1017
|
// When the launch requested interactive mode AND the command is ultracode
|
|
1005
1018
|
// AND the adapter supports persistent stdin (claude), hand off to a resident
|
|
@@ -9,7 +9,8 @@ exports.extractDisplayText = extractDisplayText;
|
|
|
9
9
|
/**
|
|
10
10
|
* Map a parsed provider stream-json frame to the single display line the Job
|
|
11
11
|
* Detail log shows (or null when the frame carries no user-facing text).
|
|
12
|
-
* Handles
|
|
12
|
+
* Handles Claude `--output-format stream-json`, Codex `exec --json`, and Gemini
|
|
13
|
+
* `--output-format stream-json` frame shapes.
|
|
13
14
|
*/
|
|
14
15
|
function extractDisplayText(event) {
|
|
15
16
|
const type = event.type;
|
|
@@ -22,10 +23,24 @@ function extractDisplayText(event) {
|
|
|
22
23
|
return texts.join('') || null;
|
|
23
24
|
}
|
|
24
25
|
if (type === 'tool_use') {
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
// Claude emits `name`/`input`; Gemini stream-json uses `tool_name`/`parameters`.
|
|
27
|
+
// Tolerating both keeps a single tool_use branch across providers — without
|
|
28
|
+
// this fallback every Gemini tool call renders as `[tool: undefined] {}`.
|
|
29
|
+
const e = event;
|
|
30
|
+
const name = e.name ?? e.tool_name ?? '<unnamed>';
|
|
31
|
+
const input = JSON.stringify(e.input ?? e.parameters ?? {});
|
|
27
32
|
return `[tool: ${name}] ${input.slice(0, 120)}`;
|
|
28
33
|
}
|
|
34
|
+
if (type === 'message') {
|
|
35
|
+
// Gemini stream-json streams assistant text as `message` delta frames whose
|
|
36
|
+
// `content` is a plain string; the `role:"user"` echo carries no display
|
|
37
|
+
// value. Without this branch the Job Detail log drops all Gemini narration.
|
|
38
|
+
const e = event;
|
|
39
|
+
if (e.role !== 'assistant')
|
|
40
|
+
return null;
|
|
41
|
+
const text = e.content ?? '';
|
|
42
|
+
return text.length > 0 ? text : null;
|
|
43
|
+
}
|
|
29
44
|
if (type === 'tool_result' || type === 'system_prompt' || type === 'user' || type === 'system' || type === 'result') {
|
|
30
45
|
return null;
|
|
31
46
|
}
|