specrails-desktop 2.31.2 → 2.33.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/client/dist/assets/{ActivityFeedPage-Cdccp1kl.js → ActivityFeedPage-DaBTALGY.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-DUwwfVOf.js → AgentBrowserCapture-SxwVNrUs.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-B1Fu0f_G.js → AgentModeCodePane-CmLgIoDw.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-DA6wQKKt.js → AgentModeJobsPane-BvP_T32M.js} +1 -1
- package/client/dist/assets/{AgentsPage-CR72XsPj.js → AgentsPage-Bz7uDqvI.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-CPnl0xkM.js → AnalyticsPage-DrNHMTed.js} +1 -1
- package/client/dist/assets/{CodePage-BYY7EF1b.js → CodePage-DZtAK2Ac.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-9PUjsD8C.js → DesktopAnalyticsPage-DrvNiDb0.js} +1 -1
- package/client/dist/assets/{DocsDialog-zngqdI-4.js → DocsDialog-BEQpGC1o.js} +1 -1
- package/client/dist/assets/{DocsPage-CDJH_q9M.js → DocsPage-Btso3Viq.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-drUo56r7.js → InteractiveJobComposer-Ca9-_4VF.js} +1 -1
- package/client/dist/assets/{JobDetailModal-CWmzaRXG.js → JobDetailModal-Brk7fL5A.js} +1 -1
- package/client/dist/assets/{JobDetailPage-xlzBYZEJ.js → JobDetailPage-DiQVcvW7.js} +1 -1
- package/client/dist/assets/{JobsPage-Dcnol1Y6.js → JobsPage-1SvirYQm.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-D3q7EoNM.js → LoopBuilderPage-X_YwJ-Ik.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-Q8osegkj.js → LoopPreviewModal-BeW-cr7A.js} +1 -1
- package/client/dist/assets/{LoopsPage-CATnNd1X.js → LoopsPage-Na9vDt4p.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-DIsnQMVz.js → MinimizedChatsContext-BQQohTp2.js} +1 -1
- package/client/dist/assets/{PluginsPage-BOh6z0zK.js → PluginsPage-B-NLA93L.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-CNcANqEL.js → ProjectSettingsDialog-CQlq1Mf7.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-4EpqyErX.js → TemplatePreviewModal-CPz1o3fP.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-BfV4tnvr.js → TicketDetailModal-DEoAJOuD.js} +3 -3
- package/client/dist/assets/{Trans-cWtExGkC.js → Trans-CpUoGvNy.js} +1 -1
- package/client/dist/assets/agentstudio-BNUth0uj.js +1 -0
- package/client/dist/assets/agentstudio-BNdB5hkr.js +1 -0
- package/client/dist/assets/agentstudio-C7-bt8FO.js +1 -0
- package/client/dist/assets/agentstudio-C8uNchpV.js +1 -0
- package/client/dist/assets/agentstudio-CKabyMlg.js +1 -0
- package/client/dist/assets/agentstudio-CN2pokQu.js +1 -0
- package/client/dist/assets/agentstudio-DR7doNBV.js +1 -0
- package/client/dist/assets/agentstudio-y2dVDXeM.js +1 -0
- package/client/dist/assets/{dist-js-Cn09PP3z.js → dist-js-1QRck05T.js} +1 -1
- package/client/dist/assets/{dist-js-mq5XA-4f.js → dist-js-K9AsZWxz.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-DUyzS4rr.js → formatDistanceToNow-2bGOYubE.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BCIwmmTj.js → getTimezoneOffsetInMilliseconds-B_N-0uSl.js} +1 -1
- package/client/dist/assets/{i18n-D21Pr8Sj.js → i18n-B2SaZsf2.js} +2 -2
- package/client/dist/assets/{index-CzNyrRn5.js → index-Bpzm94S9.js} +17 -17
- package/client/dist/assets/index-ChIuonbC.css +2 -0
- package/client/dist/assets/loops-B2_97D26.js +1 -0
- package/client/dist/assets/loops-BISOkuCu.js +1 -0
- package/client/dist/assets/loops-Bo6w_peZ.js +1 -0
- package/client/dist/assets/loops-BwxCndZk.js +1 -0
- package/client/dist/assets/loops-CD19kEzq.js +1 -0
- package/client/dist/assets/loops-CMMQOw2P.js +1 -0
- package/client/dist/assets/loops-CnxqL8IL.js +1 -0
- package/client/dist/assets/loops-aEb4RxN2.js +1 -0
- package/client/dist/assets/setup-CA9J5hck.js +1 -0
- package/client/dist/assets/setup-CK8IKkeD.js +1 -0
- package/client/dist/assets/setup-Cas_jrnl.js +1 -0
- package/client/dist/assets/setup-ClcYC8KP.js +1 -0
- package/client/dist/assets/setup-CtfFTXdf.js +1 -0
- package/client/dist/assets/setup-D73npsqO.js +1 -0
- package/client/dist/assets/setup-DXw1UgZB.js +1 -0
- package/client/dist/assets/setup-DsKIBie0.js +1 -0
- package/client/dist/assets/{spending-BCyUxoOI.js → spending-B20-uFLQ.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-CkVCDzer.js → useBrowserCaptureSession-DACautTD.js} +1 -1
- package/client/dist/assets/{useDesktop-DCP_MHp9.js → useDesktop-Cwyis9DX.js} +1 -1
- package/client/dist/index.html +14 -14
- package/package.json +1 -1
- package/server/dist/ai-invocations.js +35 -0
- package/server/dist/job-phase-breakdown.js +132 -0
- package/server/dist/loop-executors.js +10 -3
- package/server/dist/loop-factory.js +7 -5
- package/server/dist/loop-graph.js +4 -3
- package/server/dist/loop-run-manager.js +7 -3
- package/server/dist/loop-templates.js +4 -3
- package/server/dist/pr-body.js +13 -0
- package/server/dist/profiles-router.js +24 -1
- package/server/dist/project-router-jobs.js +16 -0
- package/server/dist/queue-manager.js +4 -0
- package/server/dist/rail-isolated-launch.js +15 -0
- package/server/dist/rail-pr-decision.js +10 -0
- package/server/dist/repo-map.js +293 -0
- package/server/dist/spawn-lifecycle.js +1 -1
- package/server/dist/worktree-node-modules.js +158 -0
- package/client/dist/assets/agentstudio-8TQqBenW.js +0 -1
- package/client/dist/assets/agentstudio-B8Z_mmEX.js +0 -1
- package/client/dist/assets/agentstudio-BZR9YmLs.js +0 -1
- package/client/dist/assets/agentstudio-Bge9pAWb.js +0 -1
- package/client/dist/assets/agentstudio-Bz169Vzt.js +0 -1
- package/client/dist/assets/agentstudio-D6L2jpNi.js +0 -1
- package/client/dist/assets/agentstudio-K2-Z7CL0.js +0 -1
- package/client/dist/assets/agentstudio-Zmgzthv8.js +0 -1
- package/client/dist/assets/index-Bz0TeGRs.css +0 -2
- package/client/dist/assets/loops-BzfXWEwO.js +0 -1
- package/client/dist/assets/loops-CPpgMapU.js +0 -1
- package/client/dist/assets/loops-CWWbKuVL.js +0 -1
- package/client/dist/assets/loops-Crc9It8o.js +0 -1
- package/client/dist/assets/loops-DH2hZcbK.js +0 -1
- package/client/dist/assets/loops-DS5TmvIU.js +0 -1
- package/client/dist/assets/loops-P2f0cN8-.js +0 -1
- package/client/dist/assets/loops-fJV-9hH7.js +0 -1
- package/client/dist/assets/setup-BBiHEO7h.js +0 -1
- package/client/dist/assets/setup-BlTWhNIQ.js +0 -1
- package/client/dist/assets/setup-Bp2L_oiP.js +0 -1
- package/client/dist/assets/setup-CInYXp1-.js +0 -1
- package/client/dist/assets/setup-CYH3bP6K.js +0 -1
- package/client/dist/assets/setup-DwcQ921P.js +0 -1
- package/client/dist/assets/setup-PBLjuDSs.js +0 -1
- package/client/dist/assets/setup-bx6FtoBp.js +0 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.computeJobPhaseBreakdown = computeJobPhaseBreakdown;
|
|
4
|
+
const pricing_1 = require("./pricing");
|
|
5
|
+
function computeJobPhaseBreakdown(events, providerId = 'claude') {
|
|
6
|
+
const phases = new Map();
|
|
7
|
+
/** tool_use id → root phase id, for transitive nested-Task attribution. */
|
|
8
|
+
const rootOf = new Map();
|
|
9
|
+
const orchestrator = emptyTotals();
|
|
10
|
+
const unattributed = emptyTotals();
|
|
11
|
+
let sawUsage = false;
|
|
12
|
+
const totalsFor = (parentId) => {
|
|
13
|
+
if (!parentId)
|
|
14
|
+
return orchestrator;
|
|
15
|
+
const rootId = rootOf.get(parentId);
|
|
16
|
+
if (!rootId)
|
|
17
|
+
return unattributed;
|
|
18
|
+
return phases.get(rootId) ?? unattributed;
|
|
19
|
+
};
|
|
20
|
+
for (const ev of events) {
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(ev.payload);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const parentId = typeof parsed.parent_tool_use_id === 'string' ? parsed.parent_tool_use_id : null;
|
|
29
|
+
const message = parsed.message;
|
|
30
|
+
if (parsed.type === 'assistant' && message) {
|
|
31
|
+
// Register Task spawns FIRST so a same-event usage envelope can't
|
|
32
|
+
// attribute to a phase this very event opens.
|
|
33
|
+
for (const block of contentBlocks(message)) {
|
|
34
|
+
if (block.type !== 'tool_use' || block.name !== 'Task' || typeof block.id !== 'string')
|
|
35
|
+
continue;
|
|
36
|
+
const input = (block.input ?? {});
|
|
37
|
+
const rootId = parentId ? (rootOf.get(parentId) ?? null) : null;
|
|
38
|
+
if (rootId) {
|
|
39
|
+
// Nested Task: its events roll up into the ancestor phase.
|
|
40
|
+
rootOf.set(block.id, rootId);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
rootOf.set(block.id, block.id);
|
|
44
|
+
phases.set(block.id, {
|
|
45
|
+
...emptyTotals(),
|
|
46
|
+
agent: typeof input.subagent_type === 'string' ? input.subagent_type : 'subagent',
|
|
47
|
+
toolUseId: block.id,
|
|
48
|
+
description: typeof input.description === 'string' ? input.description : null,
|
|
49
|
+
startedAt: ev.timestamp,
|
|
50
|
+
endedAt: null,
|
|
51
|
+
durationMs: null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const usage = message.usage;
|
|
56
|
+
if (usage && typeof usage === 'object') {
|
|
57
|
+
sawUsage = true;
|
|
58
|
+
const target = totalsFor(parentId);
|
|
59
|
+
const tokensIn = num(usage.input_tokens);
|
|
60
|
+
const tokensOut = num(usage.output_tokens);
|
|
61
|
+
const cacheRead = num(usage.cache_read_input_tokens);
|
|
62
|
+
const cacheCreate = num(usage.cache_creation_input_tokens);
|
|
63
|
+
target.tokensIn += tokensIn;
|
|
64
|
+
target.tokensOut += tokensOut;
|
|
65
|
+
target.tokensCacheRead += cacheRead;
|
|
66
|
+
target.tokensCacheCreate += cacheCreate;
|
|
67
|
+
target.assistantEvents += 1;
|
|
68
|
+
const cost = (0, pricing_1.estimateCostUsd)(providerId, str(message.model), {
|
|
69
|
+
tokens_in: tokensIn,
|
|
70
|
+
tokens_out: tokensOut,
|
|
71
|
+
tokens_cache_read: cacheRead,
|
|
72
|
+
tokens_cache_create: cacheCreate,
|
|
73
|
+
});
|
|
74
|
+
if (cost !== null) {
|
|
75
|
+
target.estimatedCostUsd = (target.estimatedCostUsd ?? 0) + cost;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (parsed.type === 'user' && message) {
|
|
81
|
+
// A tool_result closing a root Task block ends that phase.
|
|
82
|
+
for (const block of contentBlocks(message)) {
|
|
83
|
+
if (block.type !== 'tool_result' || typeof block.tool_use_id !== 'string')
|
|
84
|
+
continue;
|
|
85
|
+
const phase = phases.get(block.tool_use_id);
|
|
86
|
+
if (phase && phase.endedAt === null) {
|
|
87
|
+
phase.endedAt = ev.timestamp;
|
|
88
|
+
phase.durationMs = spanMs(phase.startedAt, ev.timestamp);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!sawUsage)
|
|
94
|
+
return null;
|
|
95
|
+
const hasUnattributed = unattributed.assistantEvents > 0 || unattributed.tokensIn > 0 || unattributed.tokensOut > 0;
|
|
96
|
+
return {
|
|
97
|
+
phases: [...phases.values()],
|
|
98
|
+
orchestrator,
|
|
99
|
+
unattributed: hasUnattributed ? unattributed : null,
|
|
100
|
+
estimated: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ─── Internals ────────────────────────────────────────────────────────────────
|
|
104
|
+
function emptyTotals() {
|
|
105
|
+
return {
|
|
106
|
+
tokensIn: 0,
|
|
107
|
+
tokensOut: 0,
|
|
108
|
+
tokensCacheRead: 0,
|
|
109
|
+
tokensCacheCreate: 0,
|
|
110
|
+
assistantEvents: 0,
|
|
111
|
+
estimatedCostUsd: null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function contentBlocks(message) {
|
|
115
|
+
const content = message.content;
|
|
116
|
+
return Array.isArray(content) ? content : [];
|
|
117
|
+
}
|
|
118
|
+
function num(value) {
|
|
119
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
120
|
+
}
|
|
121
|
+
function str(value) {
|
|
122
|
+
return typeof value === 'string' ? value : null;
|
|
123
|
+
}
|
|
124
|
+
function spanMs(start, end) {
|
|
125
|
+
if (!start || !end)
|
|
126
|
+
return null;
|
|
127
|
+
const a = Date.parse(start);
|
|
128
|
+
const b = Date.parse(end);
|
|
129
|
+
if (Number.isNaN(a) || Number.isNaN(b) || b < a)
|
|
130
|
+
return null;
|
|
131
|
+
return b - a;
|
|
132
|
+
}
|
|
@@ -22,6 +22,7 @@ const spawn_lifecycle_1 = require("./spawn-lifecycle");
|
|
|
22
22
|
const result_event_1 = require("./result-event");
|
|
23
23
|
const loop_decider_1 = require("./loop-decider");
|
|
24
24
|
const rail_isolation_1 = require("./rail-isolation");
|
|
25
|
+
const repo_map_1 = require("./repo-map");
|
|
25
26
|
const feature_flags_1 = require("./feature-flags");
|
|
26
27
|
const runtime_1 = require("./providers/runtime");
|
|
27
28
|
// Per-step wall-clock caps so a single hung step can't block the engine's
|
|
@@ -95,7 +96,10 @@ function runShellCommand(command, cwd, env, timeoutMs, onLine, onSpawn) {
|
|
|
95
96
|
*/
|
|
96
97
|
function aiStepEnv(env, repoDir) {
|
|
97
98
|
const base = repoDir ? { ...env, SPECRAILS_REPO_DIR: repoDir } : env;
|
|
98
|
-
|
|
99
|
+
// Deterministic repo map (zero-AI) so the pipeline's exploration phase
|
|
100
|
+
// starts oriented instead of spending its first turns on `ls`/`find`.
|
|
101
|
+
const withMap = (0, repo_map_1.injectRepoMapEnv)(base, repoDir);
|
|
102
|
+
return (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? { ...withMap, SPECRAILS_GIT_AUTO: 'false' } : withMap;
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
105
|
* Relocated cwd is the workspace; the source repo is reached via the
|
|
@@ -182,7 +186,9 @@ function createLoopExecutors(opts = {}) {
|
|
|
182
186
|
buildOpts,
|
|
183
187
|
cwd,
|
|
184
188
|
env: (0, runtime_1.buildProviderEnv)(adapter, buildOpts, stepEnv),
|
|
185
|
-
|
|
189
|
+
// 0 ⇒ watchdog disabled (factory loops run untimed; the loop's
|
|
190
|
+
// maxIterations / cost cap remain the runaway guards).
|
|
191
|
+
timeoutMs: (aiStepTimeoutMs ?? AI_STEP_TIMEOUT_MS) > 0 ? (aiStepTimeoutMs ?? AI_STEP_TIMEOUT_MS) : undefined,
|
|
186
192
|
onSpawn,
|
|
187
193
|
// Two complementary streams, mirroring QueueManager's contract:
|
|
188
194
|
// • RAW JSONL via onStdoutLine → engine emits parsed `event`s that drive
|
|
@@ -319,7 +325,8 @@ function createLoopExecutors(opts = {}) {
|
|
|
319
325
|
return {
|
|
320
326
|
adapter,
|
|
321
327
|
spec: { binary: adapter.binary, args, cwd, env: (0, runtime_1.buildProviderEnv)(adapter, buildOpts, stepEnv) },
|
|
322
|
-
// The loop's ai-step timeout bounds the WHOLE step, interactive
|
|
328
|
+
// The loop's ai-step timeout bounds the WHOLE step, interactive
|
|
329
|
+
// included. 0 ⇒ unbounded (the engine skips arming the step timer).
|
|
323
330
|
stepTimeoutMs: aiStepTimeoutMs ?? AI_STEP_TIMEOUT_MS,
|
|
324
331
|
};
|
|
325
332
|
},
|
|
@@ -8,12 +8,14 @@ exports.factoryLoopForMode = factoryLoopForMode;
|
|
|
8
8
|
const loop_templates_1 = require("./loop-templates");
|
|
9
9
|
const GREEN_GOAL = 'Stop only when the latest verification step reports {{const:VERIFICATION_PASS}} and the history proves the spec is implemented with all required tests/build checks passing.';
|
|
10
10
|
// Factory loops run the WHOLE architect→developer→reviewer pipeline inside a
|
|
11
|
-
// single AI step (`/specrails:implement` etc.), so
|
|
12
|
-
//
|
|
13
|
-
//
|
|
11
|
+
// single AI step (`/specrails:implement` etc.), so no fixed wall-clock budget is
|
|
12
|
+
// honest — a legit implement outran the old 60-min step cap and got killed
|
|
13
|
+
// mid-run. Built-in loops therefore run UNTIMED (0 = no timeout, both the run
|
|
14
|
+
// deadline and the per-step watchdog); maxIterations and the optional cost cap
|
|
15
|
+
// remain the runaway guards.
|
|
14
16
|
const FACTORY_MAX_ITERATIONS = 12;
|
|
15
|
-
const FACTORY_LOOP_TIMEOUT_MIN =
|
|
16
|
-
const FACTORY_AI_STEP_TIMEOUT_MIN =
|
|
17
|
+
const FACTORY_LOOP_TIMEOUT_MIN = 0;
|
|
18
|
+
const FACTORY_AI_STEP_TIMEOUT_MIN = 0;
|
|
17
19
|
const SDD_QUICK_OPENSPEC_FACTORY = {
|
|
18
20
|
id: 'factory:sdd-quick-openspec',
|
|
19
21
|
name: 'SDD Quick (OpenSpec)',
|
|
@@ -27,7 +27,7 @@ function emptyLoopGraph() {
|
|
|
27
27
|
* - at least one `end` node (the explicit exit),
|
|
28
28
|
* - every edge references existing nodes (no dangling edges),
|
|
29
29
|
* - every node is reachable from `start` (no orphan / disconnected nodes),
|
|
30
|
-
* - a sane config (maxIterations ≥ 1, timeoutMinutes ≥
|
|
30
|
+
* - a sane config (maxIterations ≥ 1, timeoutMinutes ≥ 0 — 0 = no timeout).
|
|
31
31
|
*
|
|
32
32
|
* Returns all errors found (not just the first) so the builder can highlight
|
|
33
33
|
* every problem at once.
|
|
@@ -60,10 +60,11 @@ function validateLoopGraph(graph) {
|
|
|
60
60
|
!Number.isInteger(cfg.maxIterations) ||
|
|
61
61
|
cfg.maxIterations < 1 ||
|
|
62
62
|
typeof cfg.timeoutMinutes !== 'number' ||
|
|
63
|
-
cfg.timeoutMinutes
|
|
63
|
+
!Number.isFinite(cfg.timeoutMinutes) ||
|
|
64
|
+
cfg.timeoutMinutes < 0) {
|
|
64
65
|
errors.push({
|
|
65
66
|
code: 'INVALID_CONFIG',
|
|
66
|
-
message: 'maxIterations and timeoutMinutes
|
|
67
|
+
message: 'maxIterations must be ≥ 1 and timeoutMinutes ≥ 0 (0 = no timeout).',
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
// ── Dangling edges ────────────────────────────────────────────────────────────
|
|
@@ -502,9 +502,13 @@ class LoopRunManager {
|
|
|
502
502
|
const neverAfterDispose = () => new Promise(() => { });
|
|
503
503
|
const runId = req.runId ?? (0, ids_1.newId)();
|
|
504
504
|
const maxIterations = req.graph.config.maxIterations;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
505
|
+
// 0 ⇒ no whole-run deadline (built-in pipeline loops opt out of the wall
|
|
506
|
+
// clock; maxIterations / maxCostUsd remain the runaway guards).
|
|
507
|
+
const deadline = req.graph.config.timeoutMinutes > 0
|
|
508
|
+
? this.now() + req.graph.config.timeoutMinutes * 60_000
|
|
509
|
+
: Infinity;
|
|
510
|
+
// Per-step AI timeout (ms) — undefined falls back to the executor default,
|
|
511
|
+
// 0 disables the per-step watchdog entirely.
|
|
508
512
|
const aiStepTimeoutMs = req.graph.config.aiStepTimeoutMinutes != null
|
|
509
513
|
? req.graph.config.aiStepTimeoutMinutes * 60_000
|
|
510
514
|
: undefined;
|
|
@@ -170,9 +170,10 @@ function opsxLifecycleGraph() {
|
|
|
170
170
|
{ id: 'e-stop', source: 'decide', target: 'archive', branch: 'stop' },
|
|
171
171
|
{ id: 'e-archive', source: 'archive', target: 'done' },
|
|
172
172
|
],
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
173
|
+
// At most 3 full lifecycle passes guard a never-satisfied verify. Like the
|
|
174
|
+
// other built-in loops the run is UNTIMED (0 = no timeout) — apply can
|
|
175
|
+
// implement a whole change and must never be killed mid-flight.
|
|
176
|
+
config: { maxIterations: 3, timeoutMinutes: 0, aiStepTimeoutMinutes: 0 },
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
179
|
exports.LOOP_TEMPLATES = [
|
package/server/dist/pr-body.js
CHANGED
|
@@ -79,8 +79,21 @@ function buildCanonicalPrBody(input) {
|
|
|
79
79
|
const changesSection = buildChangesSection(input.changes ?? null);
|
|
80
80
|
if (changesSection)
|
|
81
81
|
parts.push(changesSection);
|
|
82
|
+
const costLine = buildCostFooter(input.costUsd ?? null);
|
|
83
|
+
if (costLine)
|
|
84
|
+
parts.push(costLine);
|
|
82
85
|
return parts.join('\n\n');
|
|
83
86
|
}
|
|
87
|
+
/** `> Specrails cost: $X.XX` — the transparency footer that keeps AI spend
|
|
88
|
+
* visible at the exact place the work is reviewed. `~` marks a total that
|
|
89
|
+
* includes rate-card estimates (providers without native cost, kill-path
|
|
90
|
+
* reconstructions). */
|
|
91
|
+
function buildCostFooter(cost) {
|
|
92
|
+
if (!cost || !Number.isFinite(cost.totalUsd) || cost.totalUsd <= 0)
|
|
93
|
+
return null;
|
|
94
|
+
const prefix = cost.estimated ? '~' : '';
|
|
95
|
+
return `> Specrails cost: ${prefix}$${cost.totalUsd.toFixed(2)}`;
|
|
96
|
+
}
|
|
84
97
|
function summaryParagraph(input) {
|
|
85
98
|
const n = input.tickets.length;
|
|
86
99
|
const refs = input.tickets.map((t) => displayRef(t)).join(', ');
|
|
@@ -282,7 +282,30 @@ function createProfilesRouter() {
|
|
|
282
282
|
throw err;
|
|
283
283
|
}
|
|
284
284
|
broadcast({ type: 'profile.changed', projectId: project.id, name: 'default' });
|
|
285
|
-
|
|
285
|
+
// Companion `fast` profile (claude only, best-effort): same chain, but
|
|
286
|
+
// the flanking pipeline stages (architect plan / reviewer gate) run on
|
|
287
|
+
// haiku while the developer keeps its migrated model — a one-click way
|
|
288
|
+
// to cut implement wall-clock when the spec doesn't need heavyweight
|
|
289
|
+
// design/review. Skipped silently when 'fast' already exists.
|
|
290
|
+
let fastProfile = null;
|
|
291
|
+
if (isClaude && (0, providers_1.isModelAvailableForAdapter)(adapter, 'haiku')) {
|
|
292
|
+
const candidate = {
|
|
293
|
+
...profile,
|
|
294
|
+
name: 'fast',
|
|
295
|
+
description: 'Speed-first: haiku architect/reviewer, your developer model unchanged. Pick per rail when the spec is routine.',
|
|
296
|
+
agents: profile.agents.map((a) => (a.id === 'sr-architect' || a.id === 'sr-reviewer' ? { ...a, model: 'haiku' } : a)),
|
|
297
|
+
};
|
|
298
|
+
try {
|
|
299
|
+
(0, profile_manager_1.createProfile)(specRoot(project), candidate, provider);
|
|
300
|
+
fastProfile = candidate;
|
|
301
|
+
broadcast({ type: 'profile.changed', projectId: project.id, name: 'fast' });
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
if (!(err instanceof profile_manager_1.ProfileConflictError))
|
|
305
|
+
throw err;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
res.status(201).json({ profile, ...(fastProfile ? { fastProfile } : {}) });
|
|
286
309
|
}
|
|
287
310
|
catch (err) {
|
|
288
311
|
handleError(res, err);
|
|
@@ -10,6 +10,7 @@ const db_1 = require("./db");
|
|
|
10
10
|
const queue_manager_1 = require("./queue-manager");
|
|
11
11
|
const feature_flags_1 = require("./feature-flags");
|
|
12
12
|
const types_1 = require("./types");
|
|
13
|
+
const job_phase_breakdown_1 = require("./job-phase-breakdown");
|
|
13
14
|
const providers_1 = require("./providers");
|
|
14
15
|
const hooks_1 = require("./hooks");
|
|
15
16
|
const explore_smash_1 = require("./explore-smash");
|
|
@@ -589,6 +590,21 @@ function registerJobsRoutes(deps) {
|
|
|
589
590
|
res.json({ jobs });
|
|
590
591
|
}
|
|
591
592
|
});
|
|
593
|
+
// Per-phase cost/usage attribution, computed post-hoc from the persisted
|
|
594
|
+
// raw stream events (claude Task/parent_tool_use_id envelope). Costs are
|
|
595
|
+
// rate-card ESTIMATES and flagged as such; providers whose stream carries
|
|
596
|
+
// no per-event usage (codex/gemini/kimi) return breakdown: null.
|
|
597
|
+
router.get('/:projectId/jobs/:id/phase-breakdown', (req, res) => {
|
|
598
|
+
const { db } = ctx(req);
|
|
599
|
+
const job = db.prepare('SELECT id FROM jobs WHERE id = ?').get(req.params.id);
|
|
600
|
+
if (!job) {
|
|
601
|
+
res.status(404).json({ error: 'Job not found' });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const events = db.prepare("SELECT event_type, payload, timestamp FROM events WHERE job_id = ? AND event_type IN ('assistant', 'user') ORDER BY seq ASC").all(req.params.id);
|
|
605
|
+
const breakdown = (0, job_phase_breakdown_1.computeJobPhaseBreakdown)(events);
|
|
606
|
+
res.json({ jobId: req.params.id, breakdown });
|
|
607
|
+
});
|
|
592
608
|
// Must be registered BEFORE /:projectId/jobs/:id, otherwise Express matches
|
|
593
609
|
// the parameterized route first with id='compare' and this never runs (the
|
|
594
610
|
// Job Comparison feature would always 404).
|
|
@@ -15,6 +15,7 @@ const win_spawn_1 = require("./util/win-spawn");
|
|
|
15
15
|
const types_1 = require("./types");
|
|
16
16
|
const command_resolver_1 = require("./command-resolver");
|
|
17
17
|
const rail_isolation_1 = require("./rail-isolation");
|
|
18
|
+
const repo_map_1 = require("./repo-map");
|
|
18
19
|
const cli_prompt_1 = require("./util/cli-prompt");
|
|
19
20
|
const stream_display_1 = require("./util/stream-display");
|
|
20
21
|
const hooks_1 = require("./hooks");
|
|
@@ -2368,6 +2369,9 @@ class QueueManager {
|
|
|
2368
2369
|
if (profileSnapshotPath) {
|
|
2369
2370
|
spawnEnv = { ...spawnEnv, SPECRAILS_PROFILE_PATH: profileSnapshotPath };
|
|
2370
2371
|
}
|
|
2372
|
+
// Deterministic repo map (zero-AI, best-effort) — orients the pipeline's
|
|
2373
|
+
// exploration phase so it doesn't spend its first turns on `ls`/`find`.
|
|
2374
|
+
spawnEnv = (0, repo_map_1.injectRepoMapEnv)(spawnEnv, execution.repoDir);
|
|
2371
2375
|
// ─── Plugin resolution + snapshot ──────────────────────────────────────
|
|
2372
2376
|
// Active = installed + verify ok; degraded = installed but verify failed
|
|
2373
2377
|
// or timed out. Degraded does NOT block spawn — rail proceeds, UI gets
|
|
@@ -78,6 +78,7 @@ const agent_chat_registry_1 = require("./agent-chat-registry");
|
|
|
78
78
|
const rail_merge_orchestrator_1 = require("./rail-merge-orchestrator");
|
|
79
79
|
const loop_executors_1 = require("./loop-executors");
|
|
80
80
|
const worktree_overlay_1 = require("./worktree-overlay");
|
|
81
|
+
const worktree_node_modules_1 = require("./worktree-node-modules");
|
|
81
82
|
const workspace_resolution_1 = require("./workspace-resolution");
|
|
82
83
|
const feature_flags_1 = require("./feature-flags");
|
|
83
84
|
const file_provenance_1 = require("./file-provenance");
|
|
@@ -690,6 +691,20 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
690
691
|
// and future edits — the spawn proceeds regardless.
|
|
691
692
|
notifyOverlayDegraded(unit.ticketId, [err instanceof Error ? err.message : String(err)]);
|
|
692
693
|
}
|
|
694
|
+
// Warm-dependency reuse: link the base checkout's node_modules trees
|
|
695
|
+
// into the fresh worktree (best-effort; a failure just leaves the run
|
|
696
|
+
// on the legacy cold start). Linked paths join the overlay exclusion
|
|
697
|
+
// list so they inherit the commit-time guarantees.
|
|
698
|
+
try {
|
|
699
|
+
const nm = (io.linkNodeModules ?? worktree_node_modules_1.linkNodeModulesIntoWorktree)(baseRepo, handle.worktreePath);
|
|
700
|
+
if (nm.linked.length > 0)
|
|
701
|
+
overlayExcludes = overlayExcludes.concat(nm.linked);
|
|
702
|
+
if (nm.warnings.length > 0)
|
|
703
|
+
notifyOverlayDegraded(unit.ticketId, nm.warnings);
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
notifyOverlayDegraded(unit.ticketId, [err instanceof Error ? err.message : String(err)]);
|
|
707
|
+
}
|
|
693
708
|
// Code-Explorer provenance: freeze the worktree's pre-run state (HEAD sha
|
|
694
709
|
// + the overlay's untracked set) so the settle diff attributes exactly
|
|
695
710
|
// the run's own writes. Best-effort — a snapshot failure only loses
|
|
@@ -73,6 +73,7 @@ const rail_pr_delivery_1 = require("./rail-pr-delivery");
|
|
|
73
73
|
const rail_worktree_release_1 = require("./rail-worktree-release");
|
|
74
74
|
const pr_naming_1 = require("./pr-naming");
|
|
75
75
|
const pr_body_1 = require("./pr-body");
|
|
76
|
+
const ai_invocations_1 = require("./ai-invocations");
|
|
76
77
|
const jira_db_1 = require("./jira/jira-db");
|
|
77
78
|
const rail_pr_store_1 = require("./rail-pr-store");
|
|
78
79
|
const rail_worktrees_store_1 = require("./rail-worktrees-store");
|
|
@@ -1292,12 +1293,21 @@ async function runCreatePr(deps, row) {
|
|
|
1292
1293
|
catch {
|
|
1293
1294
|
changes = null;
|
|
1294
1295
|
}
|
|
1296
|
+
// Cost footer is best-effort: a lookup failure omits the line, never blocks.
|
|
1297
|
+
let costUsd = null;
|
|
1298
|
+
try {
|
|
1299
|
+
costUsd = (0, ai_invocations_1.sumInvocationCostForRuns)(deps.db, snap.runIds);
|
|
1300
|
+
}
|
|
1301
|
+
catch {
|
|
1302
|
+
costUsd = null;
|
|
1303
|
+
}
|
|
1295
1304
|
const title = (0, pr_naming_1.buildPrTitle)(ticketData, { loopName: row.loop_name });
|
|
1296
1305
|
const body = (0, pr_body_1.buildCanonicalPrBody)({
|
|
1297
1306
|
loopName: row.loop_name,
|
|
1298
1307
|
baseBranch: row.base_branch,
|
|
1299
1308
|
tickets: ticketData.map((t) => ({ ...t, branch: branchFor(t.ticketId) })),
|
|
1300
1309
|
changes,
|
|
1310
|
+
costUsd,
|
|
1301
1311
|
});
|
|
1302
1312
|
let next;
|
|
1303
1313
|
// Underlying git/gh failure detail for a degraded or failed delivery —
|