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.
Files changed (100) hide show
  1. package/client/dist/assets/{ActivityFeedPage-Cdccp1kl.js → ActivityFeedPage-DaBTALGY.js} +1 -1
  2. package/client/dist/assets/{AgentBrowserCapture-DUwwfVOf.js → AgentBrowserCapture-SxwVNrUs.js} +1 -1
  3. package/client/dist/assets/{AgentModeCodePane-B1Fu0f_G.js → AgentModeCodePane-CmLgIoDw.js} +2 -2
  4. package/client/dist/assets/{AgentModeJobsPane-DA6wQKKt.js → AgentModeJobsPane-BvP_T32M.js} +1 -1
  5. package/client/dist/assets/{AgentsPage-CR72XsPj.js → AgentsPage-Bz7uDqvI.js} +1 -1
  6. package/client/dist/assets/{AnalyticsPage-CPnl0xkM.js → AnalyticsPage-DrNHMTed.js} +1 -1
  7. package/client/dist/assets/{CodePage-BYY7EF1b.js → CodePage-DZtAK2Ac.js} +1 -1
  8. package/client/dist/assets/{DesktopAnalyticsPage-9PUjsD8C.js → DesktopAnalyticsPage-DrvNiDb0.js} +1 -1
  9. package/client/dist/assets/{DocsDialog-zngqdI-4.js → DocsDialog-BEQpGC1o.js} +1 -1
  10. package/client/dist/assets/{DocsPage-CDJH_q9M.js → DocsPage-Btso3Viq.js} +1 -1
  11. package/client/dist/assets/{InteractiveJobComposer-drUo56r7.js → InteractiveJobComposer-Ca9-_4VF.js} +1 -1
  12. package/client/dist/assets/{JobDetailModal-CWmzaRXG.js → JobDetailModal-Brk7fL5A.js} +1 -1
  13. package/client/dist/assets/{JobDetailPage-xlzBYZEJ.js → JobDetailPage-DiQVcvW7.js} +1 -1
  14. package/client/dist/assets/{JobsPage-Dcnol1Y6.js → JobsPage-1SvirYQm.js} +1 -1
  15. package/client/dist/assets/{LoopBuilderPage-D3q7EoNM.js → LoopBuilderPage-X_YwJ-Ik.js} +1 -1
  16. package/client/dist/assets/{LoopPreviewModal-Q8osegkj.js → LoopPreviewModal-BeW-cr7A.js} +1 -1
  17. package/client/dist/assets/{LoopsPage-CATnNd1X.js → LoopsPage-Na9vDt4p.js} +1 -1
  18. package/client/dist/assets/{MinimizedChatsContext-DIsnQMVz.js → MinimizedChatsContext-BQQohTp2.js} +1 -1
  19. package/client/dist/assets/{PluginsPage-BOh6z0zK.js → PluginsPage-B-NLA93L.js} +1 -1
  20. package/client/dist/assets/{ProjectSettingsDialog-CNcANqEL.js → ProjectSettingsDialog-CQlq1Mf7.js} +1 -1
  21. package/client/dist/assets/{TemplatePreviewModal-4EpqyErX.js → TemplatePreviewModal-CPz1o3fP.js} +1 -1
  22. package/client/dist/assets/{TicketDetailModal-BfV4tnvr.js → TicketDetailModal-DEoAJOuD.js} +3 -3
  23. package/client/dist/assets/{Trans-cWtExGkC.js → Trans-CpUoGvNy.js} +1 -1
  24. package/client/dist/assets/agentstudio-BNUth0uj.js +1 -0
  25. package/client/dist/assets/agentstudio-BNdB5hkr.js +1 -0
  26. package/client/dist/assets/agentstudio-C7-bt8FO.js +1 -0
  27. package/client/dist/assets/agentstudio-C8uNchpV.js +1 -0
  28. package/client/dist/assets/agentstudio-CKabyMlg.js +1 -0
  29. package/client/dist/assets/agentstudio-CN2pokQu.js +1 -0
  30. package/client/dist/assets/agentstudio-DR7doNBV.js +1 -0
  31. package/client/dist/assets/agentstudio-y2dVDXeM.js +1 -0
  32. package/client/dist/assets/{dist-js-Cn09PP3z.js → dist-js-1QRck05T.js} +1 -1
  33. package/client/dist/assets/{dist-js-mq5XA-4f.js → dist-js-K9AsZWxz.js} +1 -1
  34. package/client/dist/assets/{formatDistanceToNow-DUyzS4rr.js → formatDistanceToNow-2bGOYubE.js} +1 -1
  35. package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BCIwmmTj.js → getTimezoneOffsetInMilliseconds-B_N-0uSl.js} +1 -1
  36. package/client/dist/assets/{i18n-D21Pr8Sj.js → i18n-B2SaZsf2.js} +2 -2
  37. package/client/dist/assets/{index-CzNyrRn5.js → index-Bpzm94S9.js} +17 -17
  38. package/client/dist/assets/index-ChIuonbC.css +2 -0
  39. package/client/dist/assets/loops-B2_97D26.js +1 -0
  40. package/client/dist/assets/loops-BISOkuCu.js +1 -0
  41. package/client/dist/assets/loops-Bo6w_peZ.js +1 -0
  42. package/client/dist/assets/loops-BwxCndZk.js +1 -0
  43. package/client/dist/assets/loops-CD19kEzq.js +1 -0
  44. package/client/dist/assets/loops-CMMQOw2P.js +1 -0
  45. package/client/dist/assets/loops-CnxqL8IL.js +1 -0
  46. package/client/dist/assets/loops-aEb4RxN2.js +1 -0
  47. package/client/dist/assets/setup-CA9J5hck.js +1 -0
  48. package/client/dist/assets/setup-CK8IKkeD.js +1 -0
  49. package/client/dist/assets/setup-Cas_jrnl.js +1 -0
  50. package/client/dist/assets/setup-ClcYC8KP.js +1 -0
  51. package/client/dist/assets/setup-CtfFTXdf.js +1 -0
  52. package/client/dist/assets/setup-D73npsqO.js +1 -0
  53. package/client/dist/assets/setup-DXw1UgZB.js +1 -0
  54. package/client/dist/assets/setup-DsKIBie0.js +1 -0
  55. package/client/dist/assets/{spending-BCyUxoOI.js → spending-B20-uFLQ.js} +1 -1
  56. package/client/dist/assets/{useBrowserCaptureSession-CkVCDzer.js → useBrowserCaptureSession-DACautTD.js} +1 -1
  57. package/client/dist/assets/{useDesktop-DCP_MHp9.js → useDesktop-Cwyis9DX.js} +1 -1
  58. package/client/dist/index.html +14 -14
  59. package/package.json +1 -1
  60. package/server/dist/ai-invocations.js +35 -0
  61. package/server/dist/job-phase-breakdown.js +132 -0
  62. package/server/dist/loop-executors.js +10 -3
  63. package/server/dist/loop-factory.js +7 -5
  64. package/server/dist/loop-graph.js +4 -3
  65. package/server/dist/loop-run-manager.js +7 -3
  66. package/server/dist/loop-templates.js +4 -3
  67. package/server/dist/pr-body.js +13 -0
  68. package/server/dist/profiles-router.js +24 -1
  69. package/server/dist/project-router-jobs.js +16 -0
  70. package/server/dist/queue-manager.js +4 -0
  71. package/server/dist/rail-isolated-launch.js +15 -0
  72. package/server/dist/rail-pr-decision.js +10 -0
  73. package/server/dist/repo-map.js +293 -0
  74. package/server/dist/spawn-lifecycle.js +1 -1
  75. package/server/dist/worktree-node-modules.js +158 -0
  76. package/client/dist/assets/agentstudio-8TQqBenW.js +0 -1
  77. package/client/dist/assets/agentstudio-B8Z_mmEX.js +0 -1
  78. package/client/dist/assets/agentstudio-BZR9YmLs.js +0 -1
  79. package/client/dist/assets/agentstudio-Bge9pAWb.js +0 -1
  80. package/client/dist/assets/agentstudio-Bz169Vzt.js +0 -1
  81. package/client/dist/assets/agentstudio-D6L2jpNi.js +0 -1
  82. package/client/dist/assets/agentstudio-K2-Z7CL0.js +0 -1
  83. package/client/dist/assets/agentstudio-Zmgzthv8.js +0 -1
  84. package/client/dist/assets/index-Bz0TeGRs.css +0 -2
  85. package/client/dist/assets/loops-BzfXWEwO.js +0 -1
  86. package/client/dist/assets/loops-CPpgMapU.js +0 -1
  87. package/client/dist/assets/loops-CWWbKuVL.js +0 -1
  88. package/client/dist/assets/loops-Crc9It8o.js +0 -1
  89. package/client/dist/assets/loops-DH2hZcbK.js +0 -1
  90. package/client/dist/assets/loops-DS5TmvIU.js +0 -1
  91. package/client/dist/assets/loops-P2f0cN8-.js +0 -1
  92. package/client/dist/assets/loops-fJV-9hH7.js +0 -1
  93. package/client/dist/assets/setup-BBiHEO7h.js +0 -1
  94. package/client/dist/assets/setup-BlTWhNIQ.js +0 -1
  95. package/client/dist/assets/setup-Bp2L_oiP.js +0 -1
  96. package/client/dist/assets/setup-CInYXp1-.js +0 -1
  97. package/client/dist/assets/setup-CYH3bP6K.js +0 -1
  98. package/client/dist/assets/setup-DwcQ921P.js +0 -1
  99. package/client/dist/assets/setup-PBLjuDSs.js +0 -1
  100. 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
- return (0, rail_isolation_1.isRailPrDeliveryEnabled)() ? { ...base, SPECRAILS_GIT_AUTO: 'false' } : base;
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
- timeoutMs: aiStepTimeoutMs ?? AI_STEP_TIMEOUT_MS,
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 included.
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 they need far more headroom
12
- // than the engine defaults (loop 30 min / step 15 min): a real implement can run
13
- // for a long time. 360 min loop deadline, 60 min per AI step.
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 = 360;
16
- const FACTORY_AI_STEP_TIMEOUT_MIN = 60;
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 ≥ 1).
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 < 1) {
63
+ !Number.isFinite(cfg.timeoutMinutes) ||
64
+ cfg.timeoutMinutes < 0) {
64
65
  errors.push({
65
66
  code: 'INVALID_CONFIG',
66
- message: 'maxIterations and timeoutMinutes must both be ≥ 1.',
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
- const timeoutMs = req.graph.config.timeoutMinutes * 60_000;
506
- const deadline = this.now() + timeoutMs;
507
- // Per-step AI timeout (ms) — undefined falls back to the executor default.
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
- // Conservative bounds so a never-satisfied verify can't spin: at most 3 full
174
- // lifecycle passes; per-step cap raised (apply can implement a whole change).
175
- config: { maxIterations: 3, timeoutMinutes: 180, aiStepTimeoutMinutes: 45 },
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 = [
@@ -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
- res.status(201).json({ profile });
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 —