astrocode-workflow 0.3.0 → 0.3.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.
Files changed (139) hide show
  1. package/dist/index.js +6 -0
  2. package/dist/shared/metrics.d.ts +66 -0
  3. package/dist/shared/metrics.js +112 -0
  4. package/dist/src/agents/commands.d.ts +9 -0
  5. package/dist/src/agents/commands.js +121 -0
  6. package/dist/src/agents/prompts.d.ts +3 -0
  7. package/dist/src/agents/prompts.js +232 -0
  8. package/dist/src/agents/registry.d.ts +6 -0
  9. package/dist/src/agents/registry.js +242 -0
  10. package/dist/src/agents/types.d.ts +14 -0
  11. package/dist/src/agents/types.js +8 -0
  12. package/dist/src/config/config-handler.d.ts +4 -0
  13. package/dist/src/config/config-handler.js +46 -0
  14. package/dist/src/config/defaults.d.ts +3 -0
  15. package/dist/src/config/defaults.js +3 -0
  16. package/dist/src/config/loader.d.ts +11 -0
  17. package/dist/src/config/loader.js +82 -0
  18. package/dist/src/config/schema.d.ts +194 -0
  19. package/dist/src/config/schema.js +223 -0
  20. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  21. package/dist/src/hooks/continuation-enforcer.js +190 -0
  22. package/dist/src/hooks/inject-provider.d.ts +22 -0
  23. package/dist/src/hooks/inject-provider.js +120 -0
  24. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  25. package/dist/src/hooks/tool-output-truncator.js +57 -0
  26. package/dist/src/index.d.ts +3 -0
  27. package/dist/src/index.js +308 -0
  28. package/dist/src/shared/deep-merge.d.ts +8 -0
  29. package/dist/src/shared/deep-merge.js +25 -0
  30. package/dist/src/shared/hash.d.ts +1 -0
  31. package/dist/src/shared/hash.js +4 -0
  32. package/dist/src/shared/log.d.ts +7 -0
  33. package/dist/src/shared/log.js +24 -0
  34. package/dist/src/shared/metrics.d.ts +66 -0
  35. package/dist/src/shared/metrics.js +112 -0
  36. package/dist/src/shared/model-tuning.d.ts +9 -0
  37. package/dist/src/shared/model-tuning.js +28 -0
  38. package/dist/src/shared/paths.d.ts +19 -0
  39. package/dist/src/shared/paths.js +64 -0
  40. package/dist/src/shared/text.d.ts +4 -0
  41. package/dist/src/shared/text.js +19 -0
  42. package/dist/src/shared/time.d.ts +1 -0
  43. package/dist/src/shared/time.js +3 -0
  44. package/dist/src/state/adapters/index.d.ts +41 -0
  45. package/dist/src/state/adapters/index.js +115 -0
  46. package/dist/src/state/db.d.ts +16 -0
  47. package/dist/src/state/db.js +225 -0
  48. package/dist/src/state/ids.d.ts +8 -0
  49. package/dist/src/state/ids.js +25 -0
  50. package/dist/src/state/repo-lock.d.ts +3 -0
  51. package/dist/src/state/repo-lock.js +29 -0
  52. package/dist/src/state/schema.d.ts +2 -0
  53. package/dist/src/state/schema.js +251 -0
  54. package/dist/src/state/types.d.ts +71 -0
  55. package/dist/src/state/types.js +1 -0
  56. package/dist/src/tools/artifacts.d.ts +18 -0
  57. package/dist/src/tools/artifacts.js +71 -0
  58. package/dist/src/tools/health.d.ts +8 -0
  59. package/dist/src/tools/health.js +119 -0
  60. package/dist/src/tools/index.d.ts +20 -0
  61. package/dist/src/tools/index.js +94 -0
  62. package/dist/src/tools/init.d.ts +17 -0
  63. package/dist/src/tools/init.js +96 -0
  64. package/dist/src/tools/injects.d.ts +53 -0
  65. package/dist/src/tools/injects.js +325 -0
  66. package/dist/src/tools/metrics.d.ts +7 -0
  67. package/dist/src/tools/metrics.js +61 -0
  68. package/dist/src/tools/repair.d.ts +8 -0
  69. package/dist/src/tools/repair.js +25 -0
  70. package/dist/src/tools/reset.d.ts +8 -0
  71. package/dist/src/tools/reset.js +92 -0
  72. package/dist/src/tools/run.d.ts +13 -0
  73. package/dist/src/tools/run.js +54 -0
  74. package/dist/src/tools/spec.d.ts +12 -0
  75. package/dist/src/tools/spec.js +44 -0
  76. package/dist/src/tools/stage.d.ts +23 -0
  77. package/dist/src/tools/stage.js +371 -0
  78. package/dist/src/tools/status.d.ts +8 -0
  79. package/dist/src/tools/status.js +125 -0
  80. package/dist/src/tools/story.d.ts +23 -0
  81. package/dist/src/tools/story.js +85 -0
  82. package/dist/src/tools/workflow.d.ts +13 -0
  83. package/dist/src/tools/workflow.js +355 -0
  84. package/dist/src/ui/inject.d.ts +12 -0
  85. package/dist/src/ui/inject.js +107 -0
  86. package/dist/src/ui/toasts.d.ts +13 -0
  87. package/dist/src/ui/toasts.js +39 -0
  88. package/dist/src/workflow/artifacts.d.ts +24 -0
  89. package/dist/src/workflow/artifacts.js +45 -0
  90. package/dist/src/workflow/baton.d.ts +72 -0
  91. package/dist/src/workflow/baton.js +166 -0
  92. package/dist/src/workflow/context.d.ts +20 -0
  93. package/dist/src/workflow/context.js +113 -0
  94. package/dist/src/workflow/directives.d.ts +39 -0
  95. package/dist/src/workflow/directives.js +137 -0
  96. package/dist/src/workflow/repair.d.ts +8 -0
  97. package/dist/src/workflow/repair.js +99 -0
  98. package/dist/src/workflow/state-machine.d.ts +86 -0
  99. package/dist/src/workflow/state-machine.js +216 -0
  100. package/dist/src/workflow/story-helpers.d.ts +9 -0
  101. package/dist/src/workflow/story-helpers.js +13 -0
  102. package/dist/state/db.d.ts +1 -0
  103. package/dist/state/db.js +9 -0
  104. package/dist/state/repo-lock.d.ts +3 -0
  105. package/dist/state/repo-lock.js +29 -0
  106. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  107. package/dist/test/integration/db-transactions.test.js +126 -0
  108. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  109. package/dist/test/integration/injection-metrics.test.js +129 -0
  110. package/dist/tools/health.d.ts +8 -0
  111. package/dist/tools/health.js +119 -0
  112. package/dist/tools/index.js +9 -0
  113. package/dist/tools/metrics.d.ts +7 -0
  114. package/dist/tools/metrics.js +61 -0
  115. package/dist/tools/reset.d.ts +8 -0
  116. package/dist/tools/reset.js +92 -0
  117. package/dist/tools/workflow.js +210 -215
  118. package/dist/ui/inject.d.ts +6 -0
  119. package/dist/ui/inject.js +86 -67
  120. package/dist/workflow/state-machine.d.ts +32 -32
  121. package/dist/workflow/state-machine.js +85 -170
  122. package/package.json +6 -3
  123. package/src/index.ts +8 -0
  124. package/src/shared/metrics.ts +148 -0
  125. package/src/state/db.ts +10 -1
  126. package/src/state/repo-lock.ts +158 -0
  127. package/src/tools/health.ts +128 -0
  128. package/src/tools/index.ts +12 -3
  129. package/src/tools/init.ts +26 -14
  130. package/src/tools/metrics.ts +71 -0
  131. package/src/tools/repair.ts +21 -8
  132. package/src/tools/reset.ts +100 -0
  133. package/src/tools/stage.ts +12 -0
  134. package/src/tools/status.ts +17 -3
  135. package/src/tools/story.ts +41 -15
  136. package/src/tools/workflow.ts +123 -121
  137. package/src/ui/inject.ts +113 -79
  138. package/src/workflow/state-machine.ts +123 -227
  139. package/src/tools/workflow.ts.backup +0 -681
@@ -1,13 +1,15 @@
1
1
  // src/tools/workflow.ts
2
2
  import { tool } from "@opencode-ai/plugin/tool";
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",
@@ -104,6 +106,46 @@ function buildDelegationPrompt(opts) {
104
106
  debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
105
107
  return prompt;
106
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
+ }
107
149
  export function createAstroWorkflowProceedTool(opts) {
108
150
  const { ctx, config, db, agents } = opts;
109
151
  const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
@@ -114,247 +156,200 @@ export function createAstroWorkflowProceedTool(opts) {
114
156
  max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
115
157
  },
116
158
  execute: async ({ mode, max_steps }) => {
117
- const sessionId = ctx.sessionID;
118
- const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
119
- const actions = [];
120
- const warnings = [];
121
- const startedAt = nowISO();
122
- for (let i = 0; i < steps; i++) {
123
- const next = decideNextAction(db, config);
124
- if (next.kind === "idle") {
125
- actions.push("idle: no approved stories");
126
- break;
127
- }
128
- if (next.kind === "start_run") {
129
- // NOTE: createRunForStory owns its own tx (state-machine.ts).
130
- const { run_id } = createRunForStory(db, config, next.story_key);
131
- actions.push(`started run ${run_id} for story ${next.story_key}`);
132
- if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
133
- await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
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");
175
+ break;
134
176
  }
135
- if (sessionId) {
136
- await injectChatPrompt({
137
- ctx,
138
- sessionId,
139
- agent: "Astro",
140
- text: [
141
- `[SYSTEM DIRECTIVE: ASTROCODE — RUN_STARTED]`,
142
- ``,
143
- `Run started: \`${run_id}\``,
144
- `Story: \`${next.story_key}\``,
145
- ``,
146
- `Next: call **astro_workflow_proceed** again to delegate the first stage.`,
147
- ].join("\n"),
148
- });
149
- actions.push(`injected run started message for ${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;
150
184
  }
151
- if (mode === "step")
152
- break;
153
- continue;
154
- }
155
- if (next.kind === "complete_run") {
156
- // NOTE: completeRun owns its own tx (state-machine.ts).
157
- completeRun(db, next.run_id);
158
- actions.push(`completed run ${next.run_id}`);
159
- if (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
160
- await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
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;
161
191
  }
162
- // explicit injection on completeRun (requested)
163
- if (sessionId) {
164
- await injectChatPrompt({
165
- ctx,
166
- sessionId,
167
- agent: "Astro",
168
- text: [
169
- `[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
170
- ``,
171
- `Run \`${next.run_id}\` completed.`,
172
- ``,
173
- `Next: call **astro_workflow_proceed** (mode=step) to start the next approved story (if any).`,
174
- ].join("\n"),
175
- });
176
- actions.push(`injected run completed message for ${next.run_id}`);
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;
177
199
  }
178
- if (mode === "step")
179
- break;
180
- continue;
181
- }
182
- if (next.kind === "delegate_stage") {
183
- const active = getActiveRun(db);
184
- if (!active)
185
- throw new Error("Invariant: delegate_stage but no active run.");
186
- const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
187
- const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
188
- let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
189
- const agentExists = (name) => {
190
- if (agents && agents[name])
191
- return true;
192
- const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
193
- if (knownStageAgents.includes(name))
194
- return true;
195
- return false;
196
- };
197
- if (!agentExists(agentName)) {
198
- const originalAgent = agentName;
199
- console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
200
- agentName = config.agents?.orchestrator_name || "Astro";
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
+ };
201
215
  if (!agentExists(agentName)) {
202
- console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
203
- 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";
204
219
  if (!agentExists(agentName)) {
205
- 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
+ }
206
225
  }
207
226
  }
208
- }
209
- // NOTE: startStage owns its own tx (state-machine.ts).
210
- startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
211
- actions.push(`stage started: ${next.stage_key}`);
212
- if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
213
- await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
214
- }
215
- // ✅ explicit injection on startStage (requested)
216
- if (sessionId) {
217
- await injectChatPrompt({
218
- ctx,
219
- sessionId,
220
- agent: "Astro",
221
- text: [
222
- `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
223
- ``,
224
- `Run: \`${active.run_id}\``,
225
- `Stage: \`${next.stage_key}\``,
226
- `Delegated to: \`${agentName}\``,
227
- ].join("\n"),
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);
228
230
  });
229
- actions.push(`injected stage started message for ${next.stage_key}`);
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
- // Best-effort: continuations table may not exist on older DBs.
255
- try {
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)
256
255
  const h = directiveHash(delegatePrompt);
257
256
  const now = nowISO();
258
257
  if (sessionId) {
258
+ // This assumes continuations table exists in vNext schema.
259
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);
260
260
  }
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;
261
279
  }
262
- catch (e) {
263
- warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
264
- }
265
- // Visible injection so user can see state
266
- if (sessionId) {
267
- await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
268
- const continueMessage = [
269
- `[SYSTEM DIRECTIVE: ASTROCODE AWAIT_STAGE_COMPLETION]`,
270
- ``,
271
- `Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
272
- ``,
273
- `When \`${agentName}\` completes, call:`,
274
- `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
275
- ``,
276
- `Then run **astro_workflow_proceed** again.`,
277
- ].join("\n");
278
- await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
279
- }
280
- actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
281
- // Stop here; subagent needs to run.
282
- break;
283
- }
284
- if (next.kind === "await_stage_completion") {
285
- actions.push(`await stage completion: ${next.stage_key}`);
286
- if (sessionId) {
287
- const context = buildContextSnapshot({
288
- db,
289
- config,
290
- run_id: next.run_id,
291
- next_action: `complete stage ${next.stage_key}`,
292
- });
293
- const prompt = [
294
- `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
295
- ``,
296
- `Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
297
- `If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
298
- ``,
299
- `Context snapshot:`,
300
- context,
301
- ].join("\n").trim();
302
- try {
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();
303
298
  const h = directiveHash(prompt);
304
299
  const now = nowISO();
305
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" });
306
302
  }
307
- catch (e) {
308
- warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
309
- }
310
- await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
303
+ break;
311
304
  }
305
+ actions.push(`unhandled next action: ${next.kind}`);
312
306
  break;
313
307
  }
314
- if (next.kind === "failed") {
315
- actions.push(`failed: ${next.stage_key} ${next.error_text}`);
316
- if (sessionId) {
317
- await injectChatPrompt({
318
- ctx,
319
- sessionId,
320
- agent: "Astro",
321
- text: [
322
- `[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
323
- ``,
324
- `Run \`${next.run_id}\` failed at stage \`${next.stage_key}\`.`,
325
- `Error: ${next.error_text}`,
326
- ].join("\n"),
327
- });
328
- actions.push(`injected run failed message for ${next.run_id}`);
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
+ }
329
327
  }
330
- break;
328
+ actions.push(`ui: flushed ${uiEvents.length} event(s)`);
331
329
  }
332
- actions.push(`unhandled next action: ${next.kind}`);
333
- break;
334
- }
335
- // Housekeeping event (best-effort)
336
- try {
330
+ // Housekeeping event
337
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}`);
346
+ }
347
+ return lines.join("\n").trim();
338
348
  }
339
- catch (e) {
340
- warnings.push(`workflow.proceed event insert failed (non-fatal): ${String(e)}`);
341
- }
342
- const active = getActiveRun(db);
343
- const lines = [];
344
- lines.push(`# astro_workflow_proceed`);
345
- lines.push(`- mode: ${mode}`);
346
- lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
347
- if (active)
348
- lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
349
- lines.push(``, `## Actions`);
350
- for (const a of actions)
351
- lines.push(`- ${a}`);
352
- if (warnings.length > 0) {
353
- lines.push(``, `## Warnings`);
354
- for (const w of warnings)
355
- lines.push(`⚠️ ${w}`);
349
+ finally {
350
+ // Always release the lock
351
+ repoLock.release();
356
352
  }
357
- return lines.join("\n").trim();
358
353
  },
359
354
  });
360
355
  }
@@ -1,3 +1,9 @@
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
+ */
1
7
  export declare function injectChatPrompt(opts: {
2
8
  ctx: any;
3
9
  sessionId?: string;