spec-cat 0.1.21 → 0.1.23

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 (139) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{qt0TxNaY.js → -EMqkm_u.js} +1 -1
  3. package/.output/public/_nuxt/{N3ZxlKm3.js → BDd_kh9e.js} +1 -1
  4. package/.output/public/_nuxt/BJKzOiTU.js +1 -0
  5. package/.output/public/_nuxt/{DqNujSig.js → Bk4uZfKR.js} +1 -1
  6. package/.output/public/_nuxt/C9Qk2Hno.js +1 -0
  7. package/.output/public/_nuxt/CDLI67Cr.js +1 -0
  8. package/.output/public/_nuxt/CLPz4Oer.js +1 -0
  9. package/.output/public/_nuxt/CYVeOpC3.js +1 -0
  10. package/.output/public/_nuxt/CZbP5QXb.js +1 -0
  11. package/.output/public/_nuxt/DQtVbA-s.js +150 -0
  12. package/.output/public/_nuxt/DXWMFGmi.js +1 -0
  13. package/.output/public/_nuxt/DgiJut-o.js +1 -0
  14. package/.output/public/_nuxt/{DUodkQcr.js → FbKhJXKu.js} +3 -3
  15. package/.output/public/_nuxt/builds/latest.json +1 -1
  16. package/.output/public/_nuxt/builds/meta/53b5c409-86a8-4624-8a0f-5bf31ab82deb.json +1 -0
  17. package/.output/public/_nuxt/default.eSO6fRPf.css +1 -0
  18. package/.output/public/_nuxt/entry.BtencOYX.css +1 -0
  19. package/.output/public/_nuxt/useTheme.Dp77PlfC.css +1 -0
  20. package/.output/server/chunks/_/conversationStore.mjs +13 -1
  21. package/.output/server/chunks/_/conversationStore.mjs.map +1 -1
  22. package/.output/server/chunks/_/git.mjs +7 -6
  23. package/.output/server/chunks/_/git.mjs.map +1 -1
  24. package/.output/server/chunks/_/git2.mjs.map +1 -1
  25. package/.output/server/chunks/_/gitApiHelpers.mjs +43 -0
  26. package/.output/server/chunks/_/gitApiHelpers.mjs.map +1 -0
  27. package/.output/server/chunks/_/jobPersister.mjs +326 -0
  28. package/.output/server/chunks/_/jobPersister.mjs.map +1 -0
  29. package/.output/server/chunks/_/jobQueue.mjs +714 -0
  30. package/.output/server/chunks/_/jobQueue.mjs.map +1 -0
  31. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  32. package/.output/server/chunks/build/client.precomputed.mjs.map +1 -1
  33. package/.output/server/chunks/nitro/nitro.mjs +693 -657
  34. package/.output/server/chunks/routes/_ws.mjs +133 -527
  35. package/.output/server/chunks/routes/_ws.mjs.map +1 -1
  36. package/.output/server/chunks/routes/api/git/branch-rename.post.mjs +5 -18
  37. package/.output/server/chunks/routes/api/git/branch-rename.post.mjs.map +1 -1
  38. package/.output/server/chunks/routes/api/git/branch.delete.mjs +5 -18
  39. package/.output/server/chunks/routes/api/git/branch.delete.mjs.map +1 -1
  40. package/.output/server/chunks/routes/api/git/branches.get.mjs +5 -18
  41. package/.output/server/chunks/routes/api/git/branches.get.mjs.map +1 -1
  42. package/.output/server/chunks/routes/api/git/checkout.post.mjs +5 -19
  43. package/.output/server/chunks/routes/api/git/checkout.post.mjs.map +1 -1
  44. package/.output/server/chunks/routes/api/git/cherry-pick.post.mjs +5 -18
  45. package/.output/server/chunks/routes/api/git/cherry-pick.post.mjs.map +1 -1
  46. package/.output/server/chunks/routes/api/git/clean.post.mjs +5 -19
  47. package/.output/server/chunks/routes/api/git/clean.post.mjs.map +1 -1
  48. package/.output/server/chunks/routes/api/git/commit/_id_.get.mjs +23 -26
  49. package/.output/server/chunks/routes/api/git/commit/_id_.get.mjs.map +1 -1
  50. package/.output/server/chunks/routes/api/git/commit.post.mjs +5 -19
  51. package/.output/server/chunks/routes/api/git/commit.post.mjs.map +1 -1
  52. package/.output/server/chunks/routes/api/git/diff.get.mjs +5 -24
  53. package/.output/server/chunks/routes/api/git/diff.get.mjs.map +1 -1
  54. package/.output/server/chunks/routes/api/git/fetch.post.mjs +5 -18
  55. package/.output/server/chunks/routes/api/git/fetch.post.mjs.map +1 -1
  56. package/.output/server/chunks/routes/api/git/file-diff.get.mjs +5 -24
  57. package/.output/server/chunks/routes/api/git/file-diff.get.mjs.map +1 -1
  58. package/.output/server/chunks/routes/api/git/graph.get.mjs +9 -20
  59. package/.output/server/chunks/routes/api/git/graph.get.mjs.map +1 -1
  60. package/.output/server/chunks/routes/api/git/log.get.mjs +5 -24
  61. package/.output/server/chunks/routes/api/git/log.get.mjs.map +1 -1
  62. package/.output/server/chunks/routes/api/git/merge-base.get.mjs +5 -24
  63. package/.output/server/chunks/routes/api/git/merge-base.get.mjs.map +1 -1
  64. package/.output/server/chunks/routes/api/git/merge.post.mjs +5 -18
  65. package/.output/server/chunks/routes/api/git/merge.post.mjs.map +1 -1
  66. package/.output/server/chunks/routes/api/git/pull.post.mjs +14 -26
  67. package/.output/server/chunks/routes/api/git/pull.post.mjs.map +1 -1
  68. package/.output/server/chunks/routes/api/git/push.post.mjs +15 -27
  69. package/.output/server/chunks/routes/api/git/push.post.mjs.map +1 -1
  70. package/.output/server/chunks/routes/api/git/rebase.post.mjs +10 -22
  71. package/.output/server/chunks/routes/api/git/rebase.post.mjs.map +1 -1
  72. package/.output/server/chunks/routes/api/git/remote.delete.mjs +5 -18
  73. package/.output/server/chunks/routes/api/git/remote.delete.mjs.map +1 -1
  74. package/.output/server/chunks/routes/api/git/remote.post.mjs +10 -22
  75. package/.output/server/chunks/routes/api/git/remote.post.mjs.map +1 -1
  76. package/.output/server/chunks/routes/api/git/remote.put.mjs +5 -18
  77. package/.output/server/chunks/routes/api/git/remote.put.mjs.map +1 -1
  78. package/.output/server/chunks/routes/api/git/remotes.get.mjs +5 -18
  79. package/.output/server/chunks/routes/api/git/remotes.get.mjs.map +1 -1
  80. package/.output/server/chunks/routes/api/git/reset.post.mjs +12 -24
  81. package/.output/server/chunks/routes/api/git/reset.post.mjs.map +1 -1
  82. package/.output/server/chunks/routes/api/git/revert.post.mjs +5 -18
  83. package/.output/server/chunks/routes/api/git/revert.post.mjs.map +1 -1
  84. package/.output/server/chunks/routes/api/git/show.get.mjs +5 -24
  85. package/.output/server/chunks/routes/api/git/show.get.mjs.map +1 -1
  86. package/.output/server/chunks/routes/api/git/stage.post.mjs +9 -22
  87. package/.output/server/chunks/routes/api/git/stage.post.mjs.map +1 -1
  88. package/.output/server/chunks/routes/api/git/stash-apply.post.mjs +5 -19
  89. package/.output/server/chunks/routes/api/git/stash-apply.post.mjs.map +1 -1
  90. package/.output/server/chunks/routes/api/git/stash-branch.post.mjs +5 -19
  91. package/.output/server/chunks/routes/api/git/stash-branch.post.mjs.map +1 -1
  92. package/.output/server/chunks/routes/api/git/stash-drop.post.mjs +5 -19
  93. package/.output/server/chunks/routes/api/git/stash-drop.post.mjs.map +1 -1
  94. package/.output/server/chunks/routes/api/git/stash-pop.post.mjs +5 -19
  95. package/.output/server/chunks/routes/api/git/stash-pop.post.mjs.map +1 -1
  96. package/.output/server/chunks/routes/api/git/stash.get.mjs +5 -25
  97. package/.output/server/chunks/routes/api/git/stash.get.mjs.map +1 -1
  98. package/.output/server/chunks/routes/api/git/stash.post.mjs +5 -19
  99. package/.output/server/chunks/routes/api/git/stash.post.mjs.map +1 -1
  100. package/.output/server/chunks/routes/api/git/state.get.mjs +5 -25
  101. package/.output/server/chunks/routes/api/git/state.get.mjs.map +1 -1
  102. package/.output/server/chunks/routes/api/git/status.get.mjs +5 -25
  103. package/.output/server/chunks/routes/api/git/status.get.mjs.map +1 -1
  104. package/.output/server/chunks/routes/api/git/tag/_name_.get.mjs +5 -18
  105. package/.output/server/chunks/routes/api/git/tag/_name_.get.mjs.map +1 -1
  106. package/.output/server/chunks/routes/api/git/tag-push.post.mjs +10 -22
  107. package/.output/server/chunks/routes/api/git/tag-push.post.mjs.map +1 -1
  108. package/.output/server/chunks/routes/api/git/tag.delete.mjs +5 -18
  109. package/.output/server/chunks/routes/api/git/tag.delete.mjs.map +1 -1
  110. package/.output/server/chunks/routes/api/git/tag.post.mjs +17 -29
  111. package/.output/server/chunks/routes/api/git/tag.post.mjs.map +1 -1
  112. package/.output/server/chunks/routes/api/git/unstage.post.mjs +5 -19
  113. package/.output/server/chunks/routes/api/git/unstage.post.mjs.map +1 -1
  114. package/.output/server/chunks/routes/api/index.get.mjs +25 -14
  115. package/.output/server/chunks/routes/api/index.get.mjs.map +1 -1
  116. package/.output/server/chunks/routes/api/index.get2.mjs +14 -141
  117. package/.output/server/chunks/routes/api/index.get2.mjs.map +1 -1
  118. package/.output/server/chunks/routes/api/index.get3.mjs +167 -0
  119. package/.output/server/chunks/routes/api/index.get3.mjs.map +1 -0
  120. package/.output/server/chunks/routes/api/index.post.mjs +77 -116
  121. package/.output/server/chunks/routes/api/index.post.mjs.map +1 -1
  122. package/.output/server/chunks/routes/api/index.post2.mjs +149 -0
  123. package/.output/server/chunks/routes/api/index.post2.mjs.map +1 -0
  124. package/.output/server/chunks/routes/api/jobs/_id/cancel.post.mjs +48 -0
  125. package/.output/server/chunks/routes/api/jobs/_id/cancel.post.mjs.map +1 -0
  126. package/.output/server/chunks/routes/api/jobs/_id_.get.mjs +55 -0
  127. package/.output/server/chunks/routes/api/jobs/_id_.get.mjs.map +1 -0
  128. package/.output/server/chunks/routes/api/repository/status.get.mjs +1 -1
  129. package/.output/server/package.json +1 -1
  130. package/package.json +1 -1
  131. package/.output/public/_nuxt/BIw1AQHU.js +0 -150
  132. package/.output/public/_nuxt/DBtLi_wJ.js +0 -1
  133. package/.output/public/_nuxt/DSDWvT5-.js +0 -1
  134. package/.output/public/_nuxt/K5rMM4le.js +0 -1
  135. package/.output/public/_nuxt/builds/meta/c0768eef-2dd5-410c-8648-edbd9e6c218c.json +0 -1
  136. package/.output/public/_nuxt/ddKcAgQK.js +0 -1
  137. package/.output/public/_nuxt/default.CZoNL3P_.css +0 -1
  138. package/.output/public/_nuxt/entry.DLBgeD7S.css +0 -1
  139. package/.output/public/_nuxt/sxXWehCn.js +0 -1
@@ -0,0 +1,714 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { e as getProjectDir } from '../nitro/nitro.mjs';
3
+ import { exec } from 'node:child_process';
4
+ import { promisify } from 'node:util';
5
+ import { join } from 'node:path';
6
+ import { r as resolveServerProviderSelection, a as guardProviderCapability } from './aiProviderSelection.mjs';
7
+ import { s as streamChatWithProvider, h as hasCodexPermissionError, a as hasCodexMissingRolloutPathError, b as summarizeProviderProcessError } from './providerProcessError.mjs';
8
+ import { g as getProvider } from './aiProviderRegistry.mjs';
9
+ import { n as normalizeToolName, e as extractPermissionRequestFromProcessOutput, a as normalizeTools, c as checkForPermissionRequest, i as isRenderableEvent } from './uiAdapter.mjs';
10
+
11
+ var __defProp$1 = Object.defineProperty;
12
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
13
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, key + "" , value);
14
+ class ConversationEventBus {
15
+ constructor() {
16
+ __publicField$1(this, "subscribers", /* @__PURE__ */ new Map());
17
+ }
18
+ /**
19
+ * Emit an event to all subscribers of a conversation.
20
+ */
21
+ emit(conversationId, event) {
22
+ const subs = this.subscribers.get(conversationId);
23
+ if (!subs) return;
24
+ for (const callback of subs) {
25
+ try {
26
+ callback(event);
27
+ } catch (err) {
28
+ console.error("[EventBus] Subscriber error:", err);
29
+ }
30
+ }
31
+ }
32
+ /**
33
+ * Subscribe to events for a conversation.
34
+ * Returns an unsubscribe function.
35
+ */
36
+ subscribe(conversationId, callback) {
37
+ let subs = this.subscribers.get(conversationId);
38
+ if (!subs) {
39
+ subs = /* @__PURE__ */ new Set();
40
+ this.subscribers.set(conversationId, subs);
41
+ }
42
+ subs.add(callback);
43
+ return () => {
44
+ subs.delete(callback);
45
+ if (subs.size === 0) {
46
+ this.subscribers.delete(conversationId);
47
+ }
48
+ };
49
+ }
50
+ /**
51
+ * Check if a conversation has any active subscribers.
52
+ */
53
+ hasSubscribers(conversationId) {
54
+ const subs = this.subscribers.get(conversationId);
55
+ return !!subs && subs.size > 0;
56
+ }
57
+ }
58
+ const eventBus = new ConversationEventBus();
59
+ const GLOBAL_CHANNEL = "__global__";
60
+
61
+ const execAsync = promisify(exec);
62
+ const WORKTREE_PREFIX = "/tmp/sc-";
63
+ async function ensureChatWorktree(projectDir, worktreePath, knownBranch) {
64
+ if (!worktreePath.startsWith(WORKTREE_PREFIX)) {
65
+ return { recovered: false };
66
+ }
67
+ if (existsSync(worktreePath)) {
68
+ return { recovered: false };
69
+ }
70
+ let branchName;
71
+ if (knownBranch) {
72
+ branchName = knownBranch;
73
+ } else {
74
+ const convId = worktreePath.slice(WORKTREE_PREFIX.length);
75
+ if (!convId) {
76
+ return { recovered: false, error: `Cannot derive branch name from worktree path: ${worktreePath}` };
77
+ }
78
+ branchName = `sc/${convId}`;
79
+ }
80
+ try {
81
+ await execAsync("git worktree prune", { cwd: projectDir });
82
+ try {
83
+ await execAsync(`git rev-parse --verify "${branchName}"`, { cwd: projectDir });
84
+ } catch {
85
+ return { recovered: false, error: `Branch "${branchName}" no longer exists` };
86
+ }
87
+ await execAsync(`git worktree add "${worktreePath}" "${branchName}"`, { cwd: projectDir });
88
+ console.log(`[ensureChatWorktree] Recovered worktree: ${worktreePath} \u2192 ${branchName}`);
89
+ return { recovered: true };
90
+ } catch (err) {
91
+ const message = err instanceof Error ? err.message : String(err);
92
+ console.error(`[ensureChatWorktree] Recovery failed for ${worktreePath}:`, message);
93
+ return { recovered: false, error: `Worktree recovery failed: ${message}` };
94
+ }
95
+ }
96
+
97
+ async function loadSpecContext(projectDir, featureId) {
98
+ const featurePath = join(projectDir, "specs", featureId);
99
+ const specPath = join(featurePath, "spec.md");
100
+ if (!existsSync(specPath)) {
101
+ return null;
102
+ }
103
+ const sections = [];
104
+ sections.push(`# Feature Context: ${featureId}`);
105
+ sections.push("");
106
+ sections.push("You are working on the feature described in the following specification files.");
107
+ sections.push("You MUST read these files first to understand the feature requirements:");
108
+ sections.push("");
109
+ sections.push(`- specs/${featureId}/spec.md`);
110
+ const planPath = join(featurePath, "plan.md");
111
+ if (existsSync(planPath)) {
112
+ sections.push(`- specs/${featureId}/plan.md`);
113
+ }
114
+ const tasksPath = join(featurePath, "tasks.md");
115
+ if (existsSync(tasksPath)) {
116
+ sections.push(`- specs/${featureId}/tasks.md`);
117
+ }
118
+ sections.push("");
119
+ sections.push("## Spec-Driven Workflow (MANDATORY)");
120
+ sections.push("");
121
+ sections.push("This chat is linked to a feature spec. You MUST follow the spec-driven workflow:");
122
+ sections.push("");
123
+ sections.push("1. **Read the spec files above** before doing anything else.");
124
+ sections.push("2. **When the user requests changes or new functionality:**");
125
+ sections.push(" - First, update the relevant spec file(s) (spec.md, plan.md, tasks.md) to reflect the new requirements.");
126
+ sections.push(" - Then, implement the code changes according to the updated spec.");
127
+ sections.push(" - Never skip the spec update step. The spec is the source of truth.");
128
+ sections.push("3. **When the user requests a bug fix:**");
129
+ sections.push(" - Check if the bug contradicts the spec. If so, fix the code to match the spec.");
130
+ sections.push(" - If the spec itself is wrong, update the spec first, then fix the code.");
131
+ sections.push("4. **FR Traceability:** Every change must be traceable from spec \u2192 plan \u2192 task \u2192 implementation.");
132
+ return sections.join("\n");
133
+ }
134
+
135
+ function isApprovalMode(mode) {
136
+ return mode === "ask" || mode === "plan";
137
+ }
138
+ function deriveApprovalRequestFromEvent(event, approvedTools, providerId, mode) {
139
+ if (!isApprovalMode(mode)) return null;
140
+ return checkForPermissionRequest(event, approvedTools, providerId);
141
+ }
142
+ function deriveApprovalRequestFromProcessOutput(nonJsonOutput, mode) {
143
+ if (!isApprovalMode(mode)) return null;
144
+ const inferred = extractPermissionRequestFromProcessOutput(nonJsonOutput);
145
+ if (!inferred) return null;
146
+ const tools = normalizeTools(inferred.tools);
147
+ return {
148
+ type: "permission_request",
149
+ tool: tools[0] || "Permission",
150
+ tools,
151
+ description: inferred.description
152
+ };
153
+ }
154
+ function approveTools(approvedTools, tools) {
155
+ for (const tool of tools) {
156
+ const normalized = normalizeToolName(tool);
157
+ if (normalized) {
158
+ approvedTools.add(normalized);
159
+ }
160
+ }
161
+ }
162
+
163
+ var __defProp = Object.defineProperty;
164
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
165
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
166
+ const MAX_ATTACHMENT_COUNT = 4;
167
+ const MAX_ATTACHMENT_SIZE_BYTES = 5 * 1024 * 1024;
168
+ const SPECKIT_AUTONOMY_DIRECTIVE = [
169
+ "Speckit Execution Mode (MANDATORY):",
170
+ "- Do not ask the user for confirmation, follow-up, or permission to proceed.",
171
+ '- Do not end with questions like "Would you like me to..." or "Shall I...".',
172
+ "- For remediation and traceability gaps, directly edit the relevant spec files now (spec.md, plan.md, tasks.md) when writable.",
173
+ "- Prefer concrete file edits over recommendations; provide a brief change summary after edits are complete.",
174
+ "- Only stop without edits if blocked by a hard constraint (missing files, permission failure), and report the blocker explicitly."
175
+ ].join("\n");
176
+ function isSpeckitCommand(message) {
177
+ return message.trim().startsWith("/speckit.");
178
+ }
179
+ function killProc(proc) {
180
+ try {
181
+ proc.kill();
182
+ } catch {
183
+ }
184
+ }
185
+ function normalizeImageAttachments(attachments) {
186
+ if (!Array.isArray(attachments)) return [];
187
+ return attachments.slice(0, MAX_ATTACHMENT_COUNT).map((item) => {
188
+ if (!item || typeof item !== "object") return null;
189
+ const record = item;
190
+ const id = typeof record.id === "string" ? record.id : "";
191
+ const name = typeof record.name === "string" ? record.name : "image";
192
+ const mimeType = typeof record.mimeType === "string" ? record.mimeType : "";
193
+ const size = typeof record.size === "number" ? record.size : 0;
194
+ const dataUrl = typeof record.dataUrl === "string" ? record.dataUrl : "";
195
+ if (!id || !mimeType.startsWith("image/") || size <= 0 || size > MAX_ATTACHMENT_SIZE_BYTES || !dataUrl.startsWith("data:image/")) {
196
+ return null;
197
+ }
198
+ return { id, name, mimeType, size, dataUrl };
199
+ }).filter((entry) => entry !== null);
200
+ }
201
+ function buildProviderMessage(baseMessage, attachments) {
202
+ if (attachments.length === 0) return baseMessage;
203
+ const lines = [];
204
+ if (baseMessage.trim().length > 0) {
205
+ lines.push(baseMessage);
206
+ lines.push("");
207
+ } else {
208
+ lines.push("User sent image attachments without additional text.");
209
+ lines.push("");
210
+ }
211
+ lines.push("Attached images (data URLs):");
212
+ attachments.forEach((attachment, index) => {
213
+ lines.push(`${index + 1}. ${attachment.name} (${attachment.mimeType}, ${attachment.size} bytes)`);
214
+ lines.push(attachment.dataUrl);
215
+ lines.push("");
216
+ });
217
+ lines.push("Use the attached images as part of your answer.");
218
+ return lines.join("\n");
219
+ }
220
+ function generateJobId() {
221
+ return `job-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`;
222
+ }
223
+ class ChatJobQueue {
224
+ constructor() {
225
+ __publicField(this, "jobs", /* @__PURE__ */ new Map());
226
+ __publicField(this, "jobProcessStates", /* @__PURE__ */ new Map());
227
+ __publicField(this, "conversationStates", /* @__PURE__ */ new Map());
228
+ }
229
+ getConversationState(conversationId) {
230
+ let state = this.conversationStates.get(conversationId);
231
+ if (!state) {
232
+ state = {
233
+ approvedTools: /* @__PURE__ */ new Set(),
234
+ providerSessionId: null,
235
+ activeJobId: null
236
+ };
237
+ this.conversationStates.set(conversationId, state);
238
+ }
239
+ return state;
240
+ }
241
+ emitAndBuffer(job, event) {
242
+ job.events.push(event);
243
+ eventBus.emit(job.conversationId, event);
244
+ }
245
+ emitGlobal(event) {
246
+ eventBus.emit(GLOBAL_CHANNEL, event);
247
+ }
248
+ setJobStatus(job, status) {
249
+ const prev = job.status;
250
+ job.status = status;
251
+ if (prev !== status && (status === "done" || status === "error")) {
252
+ this.emitGlobal({
253
+ type: "notification",
254
+ notificationEvent: "job_completed",
255
+ jobId: job.id,
256
+ conversationId: job.conversationId,
257
+ source: job.source,
258
+ status
259
+ });
260
+ }
261
+ }
262
+ // ── Public API ─────────────────────────────────────
263
+ /**
264
+ * Submit a new job for provider execution.
265
+ * Returns the job ID.
266
+ */
267
+ submit(msg, source = "user") {
268
+ const conversationId = msg.conversationId;
269
+ const convState = this.getConversationState(conversationId);
270
+ const speckitCommand = isSpeckitCommand(msg.message);
271
+ if (convState.activeJobId) {
272
+ const prevState = this.jobProcessStates.get(convState.activeJobId);
273
+ if (prevState == null ? void 0 : prevState.proc) {
274
+ prevState.procGeneration++;
275
+ killProc(prevState.proc);
276
+ prevState.proc = null;
277
+ }
278
+ const prevJob = this.jobs.get(convState.activeJobId);
279
+ if (prevJob && (prevJob.status === "running" || prevJob.status === "waiting_permission")) {
280
+ this.setJobStatus(prevJob, "done");
281
+ }
282
+ }
283
+ if (speckitCommand) {
284
+ console.log("[JobQueue] Speckit command detected \u2014 auto-resetting context:", conversationId);
285
+ convState.providerSessionId = null;
286
+ convState.approvedTools.clear();
287
+ } else if (msg.sessionId) {
288
+ convState.providerSessionId = msg.sessionId;
289
+ } else {
290
+ convState.approvedTools.clear();
291
+ convState.providerSessionId = null;
292
+ }
293
+ const job = {
294
+ id: generateJobId(),
295
+ conversationId,
296
+ message: msg,
297
+ source,
298
+ status: "queued",
299
+ events: [],
300
+ createdAt: Date.now()
301
+ };
302
+ const procState = {
303
+ proc: null,
304
+ procGeneration: 0,
305
+ pendingTools: []
306
+ };
307
+ this.jobs.set(job.id, job);
308
+ this.jobProcessStates.set(job.id, procState);
309
+ convState.activeJobId = job.id;
310
+ console.log("[JobQueue] Job submitted:", {
311
+ jobId: job.id,
312
+ conversationId,
313
+ source,
314
+ hasSessionId: !!msg.sessionId,
315
+ sessionId: convState.providerSessionId,
316
+ approvedTools: Array.from(convState.approvedTools),
317
+ isSpeckitCommand: speckitCommand
318
+ });
319
+ this.emitGlobal({
320
+ type: "notification",
321
+ notificationEvent: "job_created",
322
+ jobId: job.id,
323
+ conversationId,
324
+ source,
325
+ message: msg.message
326
+ });
327
+ this.runProvider(job, false, false);
328
+ return job.id;
329
+ }
330
+ /**
331
+ * Handle permission response for the active job.
332
+ */
333
+ respondToPermission(conversationId, allow) {
334
+ const convState = this.getConversationState(conversationId);
335
+ if (!convState.activeJobId) return;
336
+ const job = this.jobs.get(convState.activeJobId);
337
+ const procState = this.jobProcessStates.get(convState.activeJobId);
338
+ if (!job || !procState) return;
339
+ console.log("[JobQueue] Permission response:", {
340
+ jobId: job.id,
341
+ allow,
342
+ pendingTools: procState.pendingTools,
343
+ approvedTools: Array.from(convState.approvedTools),
344
+ sessionId: convState.providerSessionId,
345
+ providerId: job.message.providerId,
346
+ providerModelKey: job.message.providerModelKey
347
+ });
348
+ if (allow) {
349
+ approveTools(convState.approvedTools, procState.pendingTools);
350
+ console.log("[JobQueue] Tools approved:", procState.pendingTools, "- Total:", Array.from(convState.approvedTools));
351
+ procState.pendingTools = [];
352
+ job.status = "running";
353
+ const resumeMode = job.message.permissionMode || "ask";
354
+ job.message = { ...job.message, permissionMode: resumeMode };
355
+ this.runProvider(job, false, false);
356
+ } else {
357
+ procState.pendingTools = [];
358
+ this.setJobStatus(job, "done");
359
+ this.emitAndBuffer(job, { type: "done", requestId: "denied", denied: true });
360
+ }
361
+ }
362
+ /**
363
+ * Abort the active job for a conversation.
364
+ */
365
+ abort(conversationId) {
366
+ const convState = this.conversationStates.get(conversationId);
367
+ if (!(convState == null ? void 0 : convState.activeJobId)) return;
368
+ const job = this.jobs.get(convState.activeJobId);
369
+ const procState = this.jobProcessStates.get(convState.activeJobId);
370
+ if (!job || !procState) return;
371
+ console.log("[JobQueue] Abort:", job.id);
372
+ if (procState.proc) {
373
+ procState.procGeneration++;
374
+ killProc(procState.proc);
375
+ procState.proc = null;
376
+ }
377
+ procState.pendingTools = [];
378
+ this.setJobStatus(job, "done");
379
+ }
380
+ /**
381
+ * Reset provider session state for a conversation.
382
+ */
383
+ resetContext(conversationId) {
384
+ const convState = this.conversationStates.get(conversationId);
385
+ if (!convState) return;
386
+ console.log("[JobQueue] Reset context:", conversationId);
387
+ if (convState.activeJobId) {
388
+ const procState = this.jobProcessStates.get(convState.activeJobId);
389
+ if (procState == null ? void 0 : procState.proc) {
390
+ procState.procGeneration++;
391
+ killProc(procState.proc);
392
+ procState.proc = null;
393
+ }
394
+ }
395
+ convState.providerSessionId = null;
396
+ convState.approvedTools.clear();
397
+ convState.activeJobId = null;
398
+ }
399
+ /**
400
+ * Clean up on peer disconnect.
401
+ * Jobs are NOT aborted — they run to completion so that a reconnecting
402
+ * client (e.g. after browser refresh) can subscribe and replay events.
403
+ */
404
+ cleanup(_conversationId) {
405
+ }
406
+ getJob(id) {
407
+ return this.jobs.get(id);
408
+ }
409
+ getActiveJob(conversationId) {
410
+ const convState = this.conversationStates.get(conversationId);
411
+ if (!(convState == null ? void 0 : convState.activeJobId)) return void 0;
412
+ return this.jobs.get(convState.activeJobId);
413
+ }
414
+ listJobs(conversationId) {
415
+ return Array.from(this.jobs.values()).filter((j) => j.conversationId === conversationId);
416
+ }
417
+ listAllJobs() {
418
+ return Array.from(this.jobs.values());
419
+ }
420
+ // ── Provider Execution ─────────────────────────────
421
+ async runProvider(job, isRetry, forceEphemeral) {
422
+ const msg = job.message;
423
+ const convState = this.getConversationState(job.conversationId);
424
+ const procState = this.jobProcessStates.get(job.id);
425
+ if (!procState) return;
426
+ job.status = "running";
427
+ const requestedSelection = msg.providerId ? { providerId: msg.providerId, modelKey: msg.providerModelKey || "" } : { providerId: "claude", modelKey: msg.providerModelKey || "" };
428
+ const selection = await resolveServerProviderSelection(requestedSelection);
429
+ const provider = getProvider(selection.providerId);
430
+ if (!provider) {
431
+ this.emitAndBuffer(job, {
432
+ type: "error",
433
+ error: `Provider "${selection.providerId}" is not registered`,
434
+ requestId: msg.requestId
435
+ });
436
+ this.setJobStatus(job, "error");
437
+ return;
438
+ }
439
+ const providerGuard = await guardProviderCapability(
440
+ selection,
441
+ "streaming",
442
+ "Choose a provider with streaming capability in Settings."
443
+ );
444
+ if ("failure" in providerGuard) {
445
+ this.emitAndBuffer(job, {
446
+ type: "error",
447
+ error: providerGuard.failure.error,
448
+ requestId: msg.requestId
449
+ });
450
+ this.setJobStatus(job, "error");
451
+ return;
452
+ }
453
+ const projectDir = getProjectDir();
454
+ const workingDirectory = msg.cwd || projectDir;
455
+ const mode = msg.permissionMode || "ask";
456
+ if (mode === "ask" || mode === "plan") {
457
+ const permissionGuard = await guardProviderCapability(
458
+ selection,
459
+ "permissions",
460
+ "Use auto/bypass permission mode or choose a provider that supports permission prompts."
461
+ );
462
+ if ("failure" in permissionGuard) {
463
+ this.emitAndBuffer(job, {
464
+ type: "error",
465
+ error: permissionGuard.failure.error,
466
+ requestId: msg.requestId
467
+ });
468
+ this.setJobStatus(job, "error");
469
+ return;
470
+ }
471
+ }
472
+ if (workingDirectory.startsWith("/tmp/sc-") && !existsSync(workingDirectory)) {
473
+ const result = await ensureChatWorktree(projectDir, workingDirectory, msg.worktreeBranch);
474
+ if (result.recovered) {
475
+ this.emitAndBuffer(job, { type: "worktree_recovered" });
476
+ } else if (result.error) {
477
+ this.emitAndBuffer(job, {
478
+ type: "error",
479
+ error: `Worktree recovery failed: ${result.error}`,
480
+ requestId: msg.requestId
481
+ });
482
+ this.setJobStatus(job, "error");
483
+ return;
484
+ }
485
+ }
486
+ const usedResumeFlag = !isRetry && !!convState.providerSessionId;
487
+ const resumeSessionId = usedResumeFlag ? convState.providerSessionId : void 0;
488
+ let systemPrompt;
489
+ const speckitCommand = isSpeckitCommand(msg.message);
490
+ if (msg.featureId && !usedResumeFlag) {
491
+ try {
492
+ const specContext = await loadSpecContext(projectDir, msg.featureId);
493
+ if (specContext) {
494
+ systemPrompt = specContext;
495
+ }
496
+ } catch (error) {
497
+ console.error("[JobQueue] Failed to load spec context:", error);
498
+ }
499
+ }
500
+ if (speckitCommand && !usedResumeFlag) {
501
+ systemPrompt = systemPrompt ? `${systemPrompt}
502
+
503
+ ${SPECKIT_AUTONOMY_DIRECTIVE}` : SPECKIT_AUTONOMY_DIRECTIVE;
504
+ }
505
+ console.log("[JobQueue] Running provider:", selection.providerId, selection.modelKey, isRetry ? "(retry)" : "");
506
+ const generation = procState.procGeneration;
507
+ let permissionRequested = false;
508
+ let emittedRenderableContent = false;
509
+ let emittedTerminalErrorEvent = false;
510
+ const attachments = msg.attachments || [];
511
+ const providerMessage = buildProviderMessage(msg.message, attachments);
512
+ try {
513
+ procState.proc = await streamChatWithProvider(
514
+ {
515
+ message: providerMessage,
516
+ selection,
517
+ cwd: workingDirectory,
518
+ permissionMode: mode,
519
+ approvedTools: Array.from(convState.approvedTools),
520
+ resumeSessionId,
521
+ systemPrompt,
522
+ ephemeral: forceEphemeral && selection.providerId === "codex"
523
+ },
524
+ {
525
+ onProviderJson: (parsed) => {
526
+ var _a;
527
+ const events = provider.toCanonicalEvents(parsed);
528
+ for (const event of events) {
529
+ if (event.sessionId) {
530
+ convState.providerSessionId = event.sessionId;
531
+ }
532
+ if (isRenderableEvent(event)) {
533
+ emittedRenderableContent = true;
534
+ }
535
+ if (event.type === "error" || event.type === "turn_result" && event.subtype !== "success") {
536
+ emittedTerminalErrorEvent = true;
537
+ }
538
+ if ((mode === "ask" || mode === "plan") && !permissionRequested) {
539
+ const permRequest = deriveApprovalRequestFromEvent(
540
+ event,
541
+ convState.approvedTools,
542
+ selection.providerId,
543
+ mode
544
+ );
545
+ if (permRequest) {
546
+ permissionRequested = true;
547
+ procState.pendingTools = permRequest.tools || [permRequest.tool];
548
+ job.status = "waiting_permission";
549
+ this.emitAndBuffer(job, {
550
+ type: "permission_request",
551
+ tool: permRequest.tool,
552
+ tools: procState.pendingTools,
553
+ description: permRequest.description || `Permission required: ${permRequest.tool}`
554
+ });
555
+ (_a = procState.proc) == null ? void 0 : _a.kill();
556
+ return;
557
+ }
558
+ }
559
+ this.emitAndBuffer(job, { type: "ui_event", event });
560
+ }
561
+ },
562
+ onClose: ({ exitCode, signal, nonJsonOutput }) => {
563
+ if (procState.procGeneration !== generation) {
564
+ return;
565
+ }
566
+ try {
567
+ if (!permissionRequested) {
568
+ if (exitCode !== 0 && exitCode !== null) {
569
+ console.error("[JobQueue] Provider exited unexpectedly", {
570
+ providerId: selection.providerId,
571
+ modelKey: selection.modelKey,
572
+ permissionMode: mode,
573
+ requestId: msg.requestId,
574
+ exitCode,
575
+ signal,
576
+ nonJsonOutput: nonJsonOutput.slice(-25)
577
+ });
578
+ const inferred = deriveApprovalRequestFromProcessOutput(nonJsonOutput, mode);
579
+ if (inferred) {
580
+ permissionRequested = true;
581
+ procState.pendingTools = inferred.tools || [inferred.tool];
582
+ job.status = "waiting_permission";
583
+ this.emitAndBuffer(job, {
584
+ type: "permission_request",
585
+ tool: inferred.tool,
586
+ tools: procState.pendingTools,
587
+ description: inferred.description
588
+ });
589
+ procState.proc = null;
590
+ return;
591
+ }
592
+ const hasPermissionError = hasCodexPermissionError(nonJsonOutput);
593
+ const missingRolloutPath = hasCodexMissingRolloutPathError(nonJsonOutput);
594
+ if (missingRolloutPath && !hasPermissionError && !isRetry) {
595
+ this.emitAndBuffer(job, {
596
+ type: "session_reset",
597
+ reason: "Codex session state was missing rollout data. Retrying with a fresh ephemeral session."
598
+ });
599
+ convState.providerSessionId = null;
600
+ procState.proc = null;
601
+ this.runProvider(job, true, true);
602
+ return;
603
+ }
604
+ if (usedResumeFlag && !isRetry) {
605
+ const retryWithEphemeral = selection.providerId === "codex";
606
+ this.emitAndBuffer(job, {
607
+ type: "session_reset",
608
+ reason: retryWithEphemeral ? `Session resume failed (exit code ${exitCode}). Retrying with a fresh ephemeral session.` : `Session resume failed (exit code ${exitCode}). Retrying with a fresh session.`
609
+ });
610
+ convState.providerSessionId = null;
611
+ procState.proc = null;
612
+ this.runProvider(job, true, retryWithEphemeral);
613
+ return;
614
+ }
615
+ const summary = summarizeProviderProcessError(nonJsonOutput, 700);
616
+ if (!summary && emittedTerminalErrorEvent && emittedRenderableContent) {
617
+ this.emitAndBuffer(job, { type: "done", requestId: msg.requestId });
618
+ this.setJobStatus(job, "done");
619
+ return;
620
+ }
621
+ const details = summary ? ` \u2014 ${summary}` : "";
622
+ this.emitAndBuffer(job, {
623
+ type: "error",
624
+ error: `Provider process exited unexpectedly (code: ${exitCode}${signal ? ", signal: " + signal : ""})${details}`,
625
+ requestId: msg.requestId
626
+ });
627
+ this.setJobStatus(job, "error");
628
+ } else if (exitCode === null && signal) {
629
+ const summary = summarizeProviderProcessError(nonJsonOutput, 700);
630
+ const details = summary ? ` \u2014 ${summary}` : "";
631
+ this.emitAndBuffer(job, {
632
+ type: "error",
633
+ error: `Provider process was killed by signal ${signal}${details}`,
634
+ requestId: msg.requestId
635
+ });
636
+ this.setJobStatus(job, "error");
637
+ console.error("[JobQueue] Provider killed by signal", {
638
+ providerId: selection.providerId,
639
+ modelKey: selection.modelKey,
640
+ permissionMode: mode,
641
+ requestId: msg.requestId,
642
+ signal,
643
+ nonJsonOutput: nonJsonOutput.slice(-25)
644
+ });
645
+ } else {
646
+ if (!emittedRenderableContent) {
647
+ const summary = summarizeProviderProcessError(nonJsonOutput, 700);
648
+ const fallbackText = summary ? `Provider returned no structured response.
649
+
650
+ Raw output:
651
+ ${summary}` : "Provider completed without returning visible response content.";
652
+ this.emitAssistantText(job, fallbackText, convState.providerSessionId);
653
+ }
654
+ this.emitAndBuffer(job, { type: "done", requestId: msg.requestId });
655
+ this.setJobStatus(job, "done");
656
+ }
657
+ }
658
+ } finally {
659
+ procState.proc = null;
660
+ }
661
+ },
662
+ onError: (error) => {
663
+ try {
664
+ this.emitAndBuffer(job, {
665
+ type: "error",
666
+ error: `Provider process error: ${error.message}`,
667
+ requestId: msg.requestId
668
+ });
669
+ this.setJobStatus(job, "error");
670
+ } finally {
671
+ procState.pendingTools = [];
672
+ procState.proc = null;
673
+ }
674
+ }
675
+ }
676
+ );
677
+ } catch (error) {
678
+ const errorMsg = error instanceof Error ? error.message : "Failed to start provider process";
679
+ this.emitAndBuffer(job, {
680
+ type: "error",
681
+ error: errorMsg,
682
+ requestId: msg.requestId
683
+ });
684
+ this.setJobStatus(job, "error");
685
+ procState.pendingTools = [];
686
+ procState.proc = null;
687
+ }
688
+ }
689
+ emitAssistantText(job, text, sessionId) {
690
+ const blockId = `blk-${Date.now()}`;
691
+ this.emitAndBuffer(job, {
692
+ type: "ui_event",
693
+ event: {
694
+ type: "block_start",
695
+ sessionId: sessionId || void 0,
696
+ blockId,
697
+ blockType: "text",
698
+ text
699
+ }
700
+ });
701
+ this.emitAndBuffer(job, {
702
+ type: "ui_event",
703
+ event: {
704
+ type: "block_end",
705
+ sessionId: sessionId || void 0,
706
+ blockId: ""
707
+ }
708
+ });
709
+ }
710
+ }
711
+ const jobQueue = new ChatJobQueue();
712
+
713
+ export { GLOBAL_CHANNEL as G, eventBus as e, jobQueue as j, normalizeImageAttachments as n };
714
+ //# sourceMappingURL=jobQueue.mjs.map