astrocode-workflow 0.2.0 → 0.2.2

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.
@@ -1,13 +1,15 @@
1
+ // src/tools/workflow.ts
1
2
  import { tool } from "@opencode-ai/plugin/tool";
2
3
  import { withTx } from "../state/db";
3
4
  import { buildContextSnapshot } from "../workflow/context";
4
- import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
5
+ import { decideNextAction, createRunForStory, startStage, completeRun, failRun, getActiveRun, EVENT_TYPES, } from "../workflow/state-machine";
5
6
  import { buildStageDirective, directiveHash } from "../workflow/directives";
6
7
  import { injectChatPrompt } from "../ui/inject";
7
8
  import { nowISO } from "../shared/time";
8
9
  import { newEventId } from "../state/ids";
9
10
  import { debug } from "../shared/log";
10
11
  import { createToastManager } from "../ui/toasts";
12
+ import { acquireRepoLock } from "../state/repo-lock";
11
13
  // Agent name mapping for case-sensitive resolution
12
14
  export const STAGE_TO_AGENT_MAP = {
13
15
  frame: "Frame",
@@ -31,24 +33,20 @@ export function resolveAgentName(stageKey, config, agents, warnings) {
31
33
  // Validate that the agent actually exists in the registry
32
34
  if (agents && !agents[candidate]) {
33
35
  const warning = `Agent "${candidate}" not found in registry for stage "${stageKey}". Falling back to General.`;
34
- if (warnings) {
36
+ if (warnings)
35
37
  warnings.push(warning);
36
- }
37
- else {
38
+ else
38
39
  console.warn(`[Astrocode] ${warning}`);
39
- }
40
40
  candidate = "General";
41
41
  }
42
42
  // Final guard: ensure General exists, fallback to built-in "general" if not
43
43
  if (agents && !agents[candidate]) {
44
44
  const finalWarning = `Critical: General agent not found in registry. Falling back to built-in "general".`;
45
- if (warnings) {
45
+ if (warnings)
46
46
  warnings.push(finalWarning);
47
- }
48
- else {
47
+ else
49
48
  console.warn(`[Astrocode] ${finalWarning}`);
50
- }
51
- return "general"; // built-in, guaranteed by OpenCode
49
+ return "general";
52
50
  }
53
51
  return candidate;
54
52
  }
@@ -84,9 +82,6 @@ function stageConstraints(stage, cfg) {
84
82
  }
85
83
  return common;
86
84
  }
87
- function agentNameForStage(stage, cfg) {
88
- return cfg.agents.stage_agent_names[stage];
89
- }
90
85
  function buildDelegationPrompt(opts) {
91
86
  const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
92
87
  const stageUpper = stage_key.toUpperCase();
@@ -108,10 +103,49 @@ function buildDelegationPrompt(opts) {
108
103
  ``,
109
104
  `Important: do NOT do any stage work yourself in orchestrator mode.`,
110
105
  ].join("\n").trim();
111
- // Debug log the delegation prompt to troubleshoot agent output issues
112
106
  debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
113
107
  return prompt;
114
108
  }
109
+ function buildUiMessage(e) {
110
+ switch (e.kind) {
111
+ case "stage_started": {
112
+ const agent = e.agent_name ? ` (${e.agent_name})` : "";
113
+ const title = "Astrocode";
114
+ const message = `Stage started: ${e.stage_key}${agent}`;
115
+ const chatText = [
116
+ `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
117
+ ``,
118
+ `Run: ${e.run_id}`,
119
+ `Stage: ${e.stage_key}${agent}`,
120
+ ].join("\n");
121
+ return { title, message, variant: "info", chatText };
122
+ }
123
+ case "run_completed": {
124
+ const title = "Astrocode";
125
+ const message = `Run completed: ${e.run_id}`;
126
+ const chatText = [
127
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
128
+ ``,
129
+ `Run: ${e.run_id}`,
130
+ `Story: ${e.story_key}`,
131
+ ].join("\n");
132
+ return { title, message, variant: "success", chatText };
133
+ }
134
+ case "run_failed": {
135
+ const title = "Astrocode";
136
+ const message = `Run failed: ${e.run_id} (${e.stage_key})`;
137
+ const chatText = [
138
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
139
+ ``,
140
+ `Run: ${e.run_id}`,
141
+ `Story: ${e.story_key}`,
142
+ `Stage: ${e.stage_key}`,
143
+ `Error: ${e.error_text}`,
144
+ ].join("\n");
145
+ return { title, message, variant: "error", chatText };
146
+ }
147
+ }
148
+ }
115
149
  export function createAstroWorkflowProceedTool(opts) {
116
150
  const { ctx, config, db, agents } = opts;
117
151
  const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
@@ -122,219 +156,200 @@ export function createAstroWorkflowProceedTool(opts) {
122
156
  max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
123
157
  },
124
158
  execute: async ({ mode, max_steps }) => {
125
- const sessionId = ctx.sessionID;
126
- const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
127
- const actions = [];
128
- const warnings = [];
129
- const startedAt = nowISO();
130
- for (let i = 0; i < steps; i++) {
131
- const next = decideNextAction(db, config);
132
- if (next.kind === "idle") {
133
- actions.push("idle: no approved stories");
134
- break;
135
- }
136
- if (next.kind === "start_run") {
137
- const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
138
- actions.push(`started run ${run_id} for story ${next.story_key}`);
139
- if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
140
- await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
141
- }
142
- if (mode === "step")
159
+ // Acquire repo lock to ensure no concurrent workflow operations
160
+ const lockPath = `${ctx.directory}/.astro/astro.lock`;
161
+ const repoLock = acquireRepoLock(lockPath);
162
+ try {
163
+ const sessionId = ctx.sessionID;
164
+ const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
165
+ const actions = [];
166
+ const warnings = [];
167
+ const startedAt = nowISO();
168
+ // Collect UI events emitted inside state-machine functions, then flush AFTER tx.
169
+ const uiEvents = [];
170
+ const emit = (e) => uiEvents.push(e);
171
+ for (let i = 0; i < steps; i++) {
172
+ const next = decideNextAction(db, config);
173
+ if (next.kind === "idle") {
174
+ actions.push("idle: no approved stories");
143
175
  break;
144
- continue;
145
- }
146
- if (next.kind === "complete_run") {
147
- withTx(db, () => completeRun(db, next.run_id));
148
- actions.push(`completed run ${next.run_id}`);
149
- if (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
150
- await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
151
176
  }
152
- // Inject continuation directive for workflow resumption
153
- if (sessionId) {
154
- const continueDirective = [
155
- `[SYSTEM DIRECTIVE: ASTROCODE CONTINUE]`,
156
- ``,
157
- `Run ${next.run_id} completed successfully.`,
158
- ``,
159
- `The Clara Forms implementation run has finished all stages. The spec has been analyzed and decomposed into prioritized implementation stories.`,
160
- ``,
161
- `Next actions: Review the generated stories and approve the next one to continue development.`,
162
- ].join("\n");
163
- await injectChatPrompt({
164
- ctx,
165
- sessionId,
166
- text: continueDirective,
167
- agent: "Astro"
168
- });
169
- actions.push(`injected continuation directive for completed run ${next.run_id}`);
177
+ if (next.kind === "start_run") {
178
+ // SINGLE tx boundary: caller owns tx, state-machine is pure.
179
+ const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
180
+ actions.push(`started run ${run_id} for story ${next.story_key}`);
181
+ if (mode === "step")
182
+ break;
183
+ continue;
170
184
  }
171
- // Check for next approved story to start
172
- const nextStory = db.prepare("SELECT story_key, title FROM stories WHERE state = 'approved' ORDER BY priority DESC, created_at ASC LIMIT 1").get();
173
- if (nextStory && sessionId) {
174
- const nextDirective = [
175
- `[SYSTEM DIRECTIVE: ASTROCODE — START_NEXT_STORY]`,
176
- ``,
177
- `The previous run completed successfully. Start the next approved story.`,
178
- ``,
179
- `Next Story: ${nextStory.story_key} — ${nextStory.title}`,
180
- ``,
181
- `Action: Call astro_story_approve with story_key="${nextStory.story_key}" to start it, or select a different story.`,
182
- ].join("\n");
183
- await injectChatPrompt({
184
- ctx,
185
- sessionId,
186
- text: nextDirective,
187
- agent: "Astro"
188
- });
189
- actions.push(`injected directive to start next story ${nextStory.story_key}`);
185
+ if (next.kind === "complete_run") {
186
+ withTx(db, () => completeRun(db, next.run_id, emit));
187
+ actions.push(`completed run ${next.run_id}`);
188
+ if (mode === "step")
189
+ break;
190
+ continue;
190
191
  }
191
- if (mode === "step")
192
- break;
193
- continue;
194
- }
195
- if (next.kind === "delegate_stage") {
196
- const active = getActiveRun(db);
197
- if (!active)
198
- throw new Error("Invariant: delegate_stage but no active run.");
199
- const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
200
- const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
201
- // Mark stage started + set subagent_type to the stage agent.
202
- let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
203
- // Validate agent availability with fallback chain
204
- const systemConfig = config;
205
- // Check both the system config agent map (if present) OR the local agents map passed to the tool
206
- const agentExists = (name) => {
207
- // Check local agents map first (populated from src/index.ts)
208
- if (agents && agents[name]) {
209
- return true;
210
- }
211
- // Check system config agent map
212
- if (systemConfig.agent && systemConfig.agent[name]) {
213
- return true;
214
- }
215
- // For known stage agents, assume they exist (they are system-provided subagents)
216
- const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close"];
217
- if (knownStageAgents.includes(name)) {
218
- return true;
219
- }
220
- return false;
221
- };
222
- if (!agentExists(agentName)) {
223
- const originalAgent = agentName;
224
- console.warn(`[Astrocode] Agent ${agentName} not found in config. Falling back to orchestrator.`);
225
- // First fallback: orchestrator
226
- agentName = config.agents?.orchestrator_name || "Astro";
192
+ if (next.kind === "failed") {
193
+ // Ensure DB state reflects failure in one tx; emit UI event.
194
+ withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
195
+ actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
196
+ if (mode === "step")
197
+ break;
198
+ continue;
199
+ }
200
+ if (next.kind === "delegate_stage") {
201
+ const active = getActiveRun(db);
202
+ if (!active)
203
+ throw new Error("Invariant: delegate_stage but no active run.");
204
+ const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
205
+ const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
206
+ let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
207
+ const agentExists = (name) => {
208
+ if (agents && agents[name])
209
+ return true;
210
+ const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
211
+ if (knownStageAgents.includes(name))
212
+ return true;
213
+ return false;
214
+ };
227
215
  if (!agentExists(agentName)) {
228
- console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
229
- // Final fallback: General (guaranteed to exist)
230
- agentName = "General";
216
+ const originalAgent = agentName;
217
+ console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
218
+ agentName = config.agents?.orchestrator_name || "Astro";
231
219
  if (!agentExists(agentName)) {
232
- throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
220
+ console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
221
+ agentName = "General";
222
+ if (!agentExists(agentName)) {
223
+ throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
224
+ }
233
225
  }
234
226
  }
235
- }
236
- withTx(db, () => {
237
- startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
238
- // Log delegation observability
239
- if (config.debug?.telemetry?.enabled) {
240
- // eslint-disable-next-line no-console
241
- console.log(`[Astrocode:delegate] run_id=${active.run_id} stage=${next.stage_key} agent=${agentName} fallback=${agentName !== resolveAgentName(next.stage_key, config, agents) ? 'yes' : 'no'}`);
227
+ // NOTE: startStage owns its own tx (state-machine.ts).
228
+ withTx(db, () => {
229
+ startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
230
+ });
231
+ const context = buildContextSnapshot({
232
+ db,
233
+ config,
234
+ run_id: active.run_id,
235
+ next_action: `delegate stage ${next.stage_key}`,
236
+ });
237
+ const stageDirective = buildStageDirective({
238
+ config,
239
+ stage_key: next.stage_key,
240
+ run_id: active.run_id,
241
+ story_key: run.story_key,
242
+ story_title: story?.title ?? "(missing)",
243
+ stage_agent_name: agentName,
244
+ stage_goal: stageGoal(next.stage_key, config),
245
+ stage_constraints: stageConstraints(next.stage_key, config),
246
+ context_snapshot_md: context,
247
+ }).body;
248
+ const delegatePrompt = buildDelegationPrompt({
249
+ stageDirective,
250
+ run_id: active.run_id,
251
+ stage_key: next.stage_key,
252
+ stage_agent_name: agentName,
253
+ });
254
+ // Record continuation (best-effort; no tx wrapper needed but safe either way)
255
+ const h = directiveHash(delegatePrompt);
256
+ const now = nowISO();
257
+ if (sessionId) {
258
+ // This assumes continuations table exists in vNext schema.
259
+ db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)").run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
242
260
  }
243
- });
244
- if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
245
- await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
246
- }
247
- const context = buildContextSnapshot({ db, config, run_id: active.run_id, next_action: `delegate stage ${next.stage_key}` });
248
- const stageDirective = buildStageDirective({
249
- config,
250
- stage_key: next.stage_key,
251
- run_id: active.run_id,
252
- story_key: run.story_key,
253
- story_title: story?.title ?? "(missing)",
254
- stage_agent_name: agentName,
255
- stage_goal: stageGoal(next.stage_key, config),
256
- stage_constraints: stageConstraints(next.stage_key, config),
257
- context_snapshot_md: context,
258
- }).body;
259
- const delegatePrompt = buildDelegationPrompt({
260
- stageDirective,
261
- run_id: active.run_id,
262
- stage_key: next.stage_key,
263
- stage_agent_name: agentName,
264
- });
265
- // Record in continuations as a stage directive (dedupe by hash)
266
- const h = directiveHash(delegatePrompt);
267
- const now = nowISO();
268
- if (sessionId) {
269
- db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)").run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
261
+ // Visible injection so user can see state (awaited)
262
+ if (sessionId) {
263
+ await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
264
+ const continueMessage = [
265
+ `[SYSTEM DIRECTIVE: ASTROCODE AWAIT_STAGE_COMPLETION]`,
266
+ ``,
267
+ `Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
268
+ ``,
269
+ `When \`${agentName}\` completes, call:`,
270
+ `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
271
+ ``,
272
+ `This advances the workflow.`,
273
+ ].join("\n");
274
+ await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
275
+ }
276
+ actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
277
+ // Stop here; subagent needs to run.
278
+ break;
270
279
  }
271
- // Visible injection so user can see state
272
- if (sessionId) {
273
- await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
274
- // Inject continuation guidance
275
- const continueMessage = [
276
- `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
277
- ``,
278
- `Stage ${next.stage_key} delegated to ${agentName}.`,
279
- ``,
280
- `When ${agentName} completes, call:`,
281
- `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
282
- ``,
283
- `This advances the workflow.`,
284
- ].join("\n");
285
- await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
280
+ if (next.kind === "await_stage_completion") {
281
+ actions.push(`await stage completion: ${next.stage_key}`);
282
+ if (sessionId) {
283
+ const context = buildContextSnapshot({
284
+ db,
285
+ config,
286
+ run_id: next.run_id,
287
+ next_action: `complete stage ${next.stage_key}`,
288
+ });
289
+ const prompt = [
290
+ `[SYSTEM DIRECTIVE: ASTROCODE AWAIT_STAGE_OUTPUT]`,
291
+ ``,
292
+ `Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
293
+ `If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
294
+ ``,
295
+ `Context snapshot:`,
296
+ context,
297
+ ].join("\n").trim();
298
+ const h = directiveHash(prompt);
299
+ const now = nowISO();
300
+ db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)").run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
301
+ await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
302
+ }
303
+ break;
286
304
  }
287
- actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
288
- // Stop here; subagent needs to run.
305
+ actions.push(`unhandled next action: ${next.kind}`);
289
306
  break;
290
307
  }
291
- if (next.kind === "await_stage_completion") {
292
- actions.push(`await stage completion: ${next.stage_key}`);
293
- // Optionally nudge with a short directive
294
- if (sessionId) {
295
- const context = buildContextSnapshot({ db, config, run_id: next.run_id, next_action: `complete stage ${next.stage_key}` });
296
- const prompt = [
297
- `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
298
- ``,
299
- `Run ${next.run_id} is waiting for stage ${next.stage_key} output.`,
300
- `If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
301
- ``,
302
- `Context snapshot:`,
303
- context,
304
- ].join("\n").trim();
305
- const h = directiveHash(prompt);
306
- const now = nowISO();
307
- db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)").run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
308
- await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
308
+ // Flush UI events (toast + prompt) AFTER state transitions
309
+ if (uiEvents.length > 0) {
310
+ for (const e of uiEvents) {
311
+ const msg = buildUiMessage(e);
312
+ if (config.ui.toasts.enabled) {
313
+ await toasts.show({
314
+ title: msg.title,
315
+ message: msg.message,
316
+ variant: msg.variant,
317
+ });
318
+ }
319
+ if (ctx?.sessionID) {
320
+ await injectChatPrompt({
321
+ ctx,
322
+ sessionId: ctx.sessionID,
323
+ text: msg.chatText,
324
+ agent: "Astro",
325
+ });
326
+ }
309
327
  }
310
- break;
328
+ actions.push(`ui: flushed ${uiEvents.length} event(s)`);
311
329
  }
312
- if (next.kind === "failed") {
313
- actions.push(`failed: ${next.stage_key} ${next.error_text}`);
314
- break;
330
+ // Housekeeping event
331
+ db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)").run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
332
+ const active = getActiveRun(db);
333
+ const lines = [];
334
+ lines.push(`# astro_workflow_proceed`);
335
+ lines.push(`- mode: ${mode}`);
336
+ lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
337
+ if (active)
338
+ lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
339
+ lines.push(``, `## Actions`);
340
+ for (const a of actions)
341
+ lines.push(`- ${a}`);
342
+ if (warnings.length > 0) {
343
+ lines.push(``, `## Warnings`);
344
+ for (const w of warnings)
345
+ lines.push(`⚠️ ${w}`);
315
346
  }
316
- // safety
317
- actions.push(`unhandled next action: ${next.kind}`);
318
- break;
347
+ return lines.join("\n").trim();
319
348
  }
320
- // Housekeeping event
321
- db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)").run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
322
- const active = getActiveRun(db);
323
- const lines = [];
324
- lines.push(`# astro_workflow_proceed`);
325
- lines.push(`- mode: ${mode}`);
326
- lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
327
- if (active)
328
- lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
329
- lines.push(``, `## Actions`);
330
- for (const a of actions)
331
- lines.push(`- ${a}`);
332
- if (warnings.length > 0) {
333
- lines.push(``, `## Warnings`);
334
- for (const w of warnings)
335
- lines.push(`⚠️ ${w}`);
349
+ finally {
350
+ // Always release the lock
351
+ repoLock.release();
336
352
  }
337
- return lines.join("\n").trim();
338
353
  },
339
354
  });
340
355
  }
@@ -1,20 +1,12 @@
1
- type QueueItem = {
1
+ /**
2
+ * Inject a visible prompt into the conversation.
3
+ * - Deterministic ordering per session
4
+ * - Correct SDK binding (prevents `this._client` undefined)
5
+ * - Awaitable: resolves when delivered, rejects after max retries
6
+ */
7
+ export declare function injectChatPrompt(opts: {
2
8
  ctx: any;
3
- sessionId: string;
9
+ sessionId?: string;
4
10
  text: string;
5
11
  agent?: string;
6
- };
7
- /**
8
- * Enqueue an injection and ensure the worker is running.
9
- * Does NOT wait for delivery — use `flushChatPrompts()` to wait.
10
- */
11
- export declare function enqueueChatPrompt(opts: QueueItem): void;
12
- /**
13
- * Wait until all queued injections have been processed (sent or exhausted retries).
14
- */
15
- export declare function flushChatPrompts(): Promise<void>;
16
- /**
17
- * Deterministic helper: enqueue + flush (recommended for stage boundaries).
18
- */
19
- export declare function injectChatPrompt(opts: QueueItem): Promise<void>;
20
- export {};
12
+ }): Promise<void>;