astrocode-workflow 0.4.1 → 0.4.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,14 +1,10 @@
1
1
  /**
2
- * This is the only place you should hold the repo lock.
2
+ * Executes the workflow loop.
3
3
  * Everything that mutates the repo (tool calls, steps) runs inside this scope.
4
4
  *
5
5
  * Replace the internals with your actual astro/opencode driver loop.
6
6
  */
7
7
  export declare function runAstroWorkflow(opts: {
8
- lockPath: string;
9
- repoRoot: string;
10
- sessionId: string;
11
- owner?: string;
12
8
  proceedOneStep: () => Promise<{
13
9
  done: boolean;
14
10
  }>;
@@ -1,25 +1,14 @@
1
1
  // src/astro/workflow-runner.ts
2
- import { acquireRepoLock } from "../state/repo-lock";
3
- import { workflowRepoLock } from "../state/workflow-repo-lock";
4
2
  /**
5
- * This is the only place you should hold the repo lock.
3
+ * Executes the workflow loop.
6
4
  * Everything that mutates the repo (tool calls, steps) runs inside this scope.
7
5
  *
8
6
  * Replace the internals with your actual astro/opencode driver loop.
9
7
  */
10
8
  export async function runAstroWorkflow(opts) {
11
- await workflowRepoLock({ acquireRepoLock }, {
12
- lockPath: opts.lockPath,
13
- repoRoot: opts.repoRoot,
14
- sessionId: opts.sessionId,
15
- owner: opts.owner,
16
- fn: async () => {
17
- // ✅ Lock is held ONCE for the entire run. Tool calls can "rattle through".
18
- while (true) {
19
- const { done } = await opts.proceedOneStep();
20
- if (done)
21
- return;
22
- }
23
- },
24
- });
9
+ while (true) {
10
+ const { done } = await opts.proceedOneStep();
11
+ if (done)
12
+ return;
13
+ }
25
14
  }
package/dist/src/index.js CHANGED
@@ -37,12 +37,6 @@ const Astrocode = async (ctx) => {
37
37
  throw new Error("Astrocode requires ctx.directory to be a string repo root.");
38
38
  }
39
39
  const repoRoot = ctx.directory;
40
- // NOTE: Repo locking is handled at the workflow level via workflowRepoLock.
41
- // The workflow tool correctly acquires and holds the lock for the entire workflow execution.
42
- // Plugin-level locking is unnecessary and architecturally incorrect since:
43
- // - The lock would be held for the entire session lifecycle (too long)
44
- // - Individual tools are designed to be called within workflow context where lock is held
45
- // - Workflow-level locking with refcounting prevents lock churn during tool execution
46
40
  // Always load config first - this provides defaults even in limited mode
47
41
  let pluginConfig;
48
42
  try {
@@ -19,36 +19,6 @@ export function createAstroHealthTool(opts) {
19
19
  lines.push(`- PID: ${process.pid || "unknown"}`);
20
20
  lines.push(`- Repo: ${repoRoot}`);
21
21
  lines.push(`- DB Path: ${fullDbPath}`);
22
- // Lock status
23
- const lockPath = `${repoRoot}/.astro/astro.lock`;
24
- try {
25
- if (fs.existsSync(lockPath)) {
26
- const lockContent = fs.readFileSync(lockPath, "utf8").trim();
27
- const parts = lockContent.split(" ");
28
- if (parts.length >= 2) {
29
- const pid = parseInt(parts[0]);
30
- const startedAt = parts[1];
31
- // Check if PID is still running
32
- try {
33
- process.kill(pid, 0); // Signal 0 just checks if process exists
34
- lines.push(`- Lock: HELD by PID ${pid} (started ${startedAt})`);
35
- }
36
- catch {
37
- lines.push(`- Lock: STALE (PID ${pid} not running, started ${startedAt})`);
38
- lines.push(` → Run: rm "${lockPath}"`);
39
- }
40
- }
41
- else {
42
- lines.push(`- Lock: MALFORMED (${lockContent})`);
43
- }
44
- }
45
- else {
46
- lines.push(`- Lock: NONE (no lock file)`);
47
- }
48
- }
49
- catch (e) {
50
- lines.push(`- Lock: ERROR (${String(e)})`);
51
- }
52
22
  // DB file status
53
23
  const dbExists = fs.existsSync(fullDbPath);
54
24
  const walExists = fs.existsSync(`${fullDbPath}-wal`);
@@ -109,7 +79,6 @@ export function createAstroHealthTool(opts) {
109
79
  lines.push(`## Status`);
110
80
  lines.push(`✅ DB accessible`);
111
81
  lines.push(`✅ Schema valid`);
112
- lines.push(`✅ Lock file checked`);
113
82
  if (walExists || shmExists) {
114
83
  lines.push(`⚠️ WAL/SHM files present - indicates unclean shutdown or active transaction`);
115
84
  }
@@ -11,7 +11,6 @@ import { createAstroRepairTool } from "./repair";
11
11
  import { createAstroHealthTool } from "./health";
12
12
  import { createAstroResetTool } from "./reset";
13
13
  import { createAstroMetricsTool } from "./metrics";
14
- import { createAstroLockStatusTool } from "./lock";
15
14
  export function createAstroTools(opts) {
16
15
  const { ctx, config, agents, runtime } = opts;
17
16
  const { db } = runtime;
@@ -23,7 +22,6 @@ export function createAstroTools(opts) {
23
22
  tools.astro_health = createAstroHealthTool({ ctx, config, db });
24
23
  tools.astro_reset = createAstroResetTool({ ctx, config, db });
25
24
  tools.astro_metrics = createAstroMetricsTool({ ctx, config });
26
- tools.astro_lock_status = createAstroLockStatusTool({ ctx });
27
25
  // Recovery tool - available even in limited mode to allow DB initialization
28
26
  tools.astro_init = createAstroInitTool({ ctx, config, runtime });
29
27
  // Database-dependent tools
@@ -85,7 +83,6 @@ export function createAstroTools(opts) {
85
83
  ["_astro_health", "astro_health"],
86
84
  ["_astro_reset", "astro_reset"],
87
85
  ["_astro_metrics", "astro_metrics"],
88
- ["_astro_lock_status", "astro_lock_status"],
89
86
  ];
90
87
  // Only add aliases for tools that exist
91
88
  for (const [alias, target] of aliases) {
@@ -1,53 +1,20 @@
1
- import path from "node:path";
2
1
  import { tool } from "@opencode-ai/plugin/tool";
3
2
  import { withTx } from "../state/db";
4
3
  import { repairState, formatRepairReport } from "../workflow/repair";
5
4
  import { putArtifact } from "../workflow/artifacts";
6
5
  import { nowISO } from "../shared/time";
7
- import { getLockStatus, tryRemoveStaleLock } from "../state/repo-lock";
8
6
  export function createAstroRepairTool(opts) {
9
7
  const { ctx, config, db } = opts;
10
8
  return tool({
11
- description: "Repair Astrocode invariants and recover from inconsistent DB state. Also checks and repairs lock files. Writes a repair report artifact.",
9
+ description: "Repair Astrocode invariants and recover from inconsistent DB state. Writes a repair report artifact.",
12
10
  args: {
13
11
  write_report_artifact: tool.schema.boolean().default(true),
14
- repair_lock: tool.schema.boolean().default(true).describe("Attempt to remove stale/dead lock files"),
15
12
  },
16
- execute: async ({ write_report_artifact, repair_lock }) => {
13
+ execute: async ({ write_report_artifact }) => {
17
14
  const repoRoot = ctx.directory;
18
- const lockPath = path.join(repoRoot, ".astro", "astro.lock");
19
- // First, check and repair lock if requested
20
- const lockLines = [];
21
- const lockStatus = getLockStatus(lockPath);
22
- if (lockStatus.exists) {
23
- lockLines.push("## Lock Status");
24
- lockLines.push(`- Lock found: ${lockPath}`);
25
- lockLines.push(`- PID: ${lockStatus.pid} (${lockStatus.pidAlive ? 'alive' : 'dead'})`);
26
- lockLines.push(`- Age: ${lockStatus.ageMs ? Math.floor(lockStatus.ageMs / 1000) : '?'}s`);
27
- lockLines.push(`- Status: ${lockStatus.isStale ? 'stale' : 'fresh'}`);
28
- if (repair_lock) {
29
- const result = tryRemoveStaleLock(lockPath);
30
- if (result.removed) {
31
- lockLines.push(`- **Removed**: ${result.reason}`);
32
- }
33
- else {
34
- lockLines.push(`- **Not removed**: ${result.reason}`);
35
- }
36
- }
37
- else {
38
- if (!lockStatus.pidAlive || lockStatus.isStale) {
39
- lockLines.push(`- **Recommendation**: Run with repair_lock=true to remove this ${!lockStatus.pidAlive ? 'dead' : 'stale'} lock`);
40
- }
41
- }
42
- lockLines.push("");
43
- }
44
- // Then repair database state
15
+ // Repair database state
45
16
  const report = withTx(db, () => repairState(db, config));
46
- const dbMd = formatRepairReport(report);
47
- // Combine lock and DB repair
48
- const fullMd = lockLines.length > 0
49
- ? `# Astrocode Repair Report\n\n${lockLines.join("\n")}\n${dbMd.replace(/^# Astrocode repair report\n*/i, "")}`
50
- : dbMd;
17
+ const fullMd = formatRepairReport(report);
51
18
  if (write_report_artifact) {
52
19
  const rel = `.astro/repair/repair_${nowISO().replace(/[:.]/g, "-")}.md`;
53
20
  const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content: fullMd, meta: { kind: "repair" } });
@@ -1,6 +1,5 @@
1
1
  // src/tools/workflow.ts
2
2
  import { tool } from "@opencode-ai/plugin/tool";
3
- import path from "node:path";
4
3
  import { withTx } from "../state/db";
5
4
  import { buildContextSnapshot } from "../workflow/context";
6
5
  import { decideNextAction, createRunForStory, startStage, completeRun, failRun, getActiveRun, EVENT_TYPES, } from "../workflow/state-machine";
@@ -10,8 +9,6 @@ import { nowISO } from "../shared/time";
10
9
  import { newEventId } from "../state/ids";
11
10
  import { debug } from "../shared/log";
12
11
  import { createToastManager } from "../ui/toasts";
13
- import { acquireRepoLock } from "../state/repo-lock";
14
- import { workflowRepoLock } from "../state/workflow-repo-lock";
15
12
  // Agent name mapping for case-sensitive resolution
16
13
  export const STAGE_TO_AGENT_MAP = {
17
14
  frame: "Frame",
@@ -158,202 +155,191 @@ export function createAstroWorkflowProceedTool(opts) {
158
155
  max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
159
156
  },
160
157
  execute: async ({ mode, max_steps }) => {
161
- const repoRoot = ctx.directory;
162
- const lockPath = path.join(repoRoot, ".astro", "astro.lock");
163
158
  const sessionId = ctx.sessionID;
164
- return workflowRepoLock({ acquireRepoLock }, {
165
- lockPath,
166
- repoRoot,
167
- sessionId,
168
- owner: "astro_workflow_proceed",
169
- advisory: true, // Advisory mode: warn instead of blocking on lock contention
170
- fn: async () => {
171
- const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
172
- const actions = [];
173
- const warnings = [];
174
- const startedAt = nowISO();
175
- // Collect UI events emitted inside state-machine functions, then flush AFTER tx.
176
- const uiEvents = [];
177
- const emit = (e) => uiEvents.push(e);
178
- for (let i = 0; i < steps; i++) {
179
- const next = decideNextAction(db, config);
180
- if (next.kind === "idle") {
181
- actions.push("idle: no approved stories");
182
- break;
183
- }
184
- if (next.kind === "start_run") {
185
- // SINGLE tx boundary: caller owns tx, state-machine is pure.
186
- const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
187
- actions.push(`started run ${run_id} for story ${next.story_key}`);
188
- if (mode === "step")
189
- break;
190
- continue;
191
- }
192
- if (next.kind === "complete_run") {
193
- withTx(db, () => completeRun(db, next.run_id, emit));
194
- actions.push(`completed run ${next.run_id}`);
195
- if (mode === "step")
196
- break;
197
- continue;
198
- }
199
- if (next.kind === "failed") {
200
- // Ensure DB state reflects failure in one tx; emit UI event.
201
- withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
202
- actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
203
- if (mode === "step")
204
- break;
205
- continue;
206
- }
207
- if (next.kind === "delegate_stage") {
208
- const active = getActiveRun(db);
209
- if (!active)
210
- throw new Error("Invariant: delegate_stage but no active run.");
211
- const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
212
- const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
213
- let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
214
- const agentExists = (name) => {
215
- if (agents && agents[name])
216
- return true;
217
- const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
218
- if (knownStageAgents.includes(name))
219
- return true;
220
- return false;
221
- };
159
+ const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
160
+ const actions = [];
161
+ const warnings = [];
162
+ const startedAt = nowISO();
163
+ // Collect UI events emitted inside state-machine functions, then flush AFTER tx.
164
+ const uiEvents = [];
165
+ const emit = (e) => uiEvents.push(e);
166
+ for (let i = 0; i < steps; i++) {
167
+ const next = decideNextAction(db, config);
168
+ if (next.kind === "idle") {
169
+ actions.push("idle: no approved stories");
170
+ break;
171
+ }
172
+ if (next.kind === "start_run") {
173
+ // SINGLE tx boundary: caller owns tx, state-machine is pure.
174
+ const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
175
+ actions.push(`started run ${run_id} for story ${next.story_key}`);
176
+ if (mode === "step")
177
+ break;
178
+ continue;
179
+ }
180
+ if (next.kind === "complete_run") {
181
+ withTx(db, () => completeRun(db, next.run_id, emit));
182
+ actions.push(`completed run ${next.run_id}`);
183
+ if (mode === "step")
184
+ break;
185
+ continue;
186
+ }
187
+ if (next.kind === "failed") {
188
+ // Ensure DB state reflects failure in one tx; emit UI event.
189
+ withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
190
+ actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
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
+ let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
202
+ const agentExists = (name) => {
203
+ if (agents && agents[name])
204
+ return true;
205
+ const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
206
+ if (knownStageAgents.includes(name))
207
+ return true;
208
+ return false;
209
+ };
210
+ if (!agentExists(agentName)) {
211
+ const originalAgent = agentName;
212
+ console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
213
+ agentName = config.agents?.orchestrator_name || "Astro";
214
+ if (!agentExists(agentName)) {
215
+ console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
216
+ agentName = "General";
222
217
  if (!agentExists(agentName)) {
223
- const originalAgent = agentName;
224
- console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
225
- agentName = config.agents?.orchestrator_name || "Astro";
226
- if (!agentExists(agentName)) {
227
- console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
228
- agentName = "General";
229
- if (!agentExists(agentName)) {
230
- throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
231
- }
232
- }
233
- }
234
- // NOTE: startStage owns its own tx (state-machine.ts).
235
- withTx(db, () => {
236
- startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
237
- });
238
- const context = buildContextSnapshot({
239
- db,
240
- config,
241
- run_id: active.run_id,
242
- next_action: `delegate stage ${next.stage_key}`,
243
- });
244
- const stageDirective = buildStageDirective({
245
- config,
246
- stage_key: next.stage_key,
247
- run_id: active.run_id,
248
- story_key: run.story_key,
249
- story_title: story?.title ?? "(missing)",
250
- stage_agent_name: agentName,
251
- stage_goal: stageGoal(next.stage_key, config),
252
- stage_constraints: stageConstraints(next.stage_key, config),
253
- context_snapshot_md: context,
254
- }).body;
255
- const delegatePrompt = buildDelegationPrompt({
256
- stageDirective,
257
- run_id: active.run_id,
258
- stage_key: next.stage_key,
259
- stage_agent_name: agentName,
260
- });
261
- // Record continuation (best-effort; no tx wrapper needed but safe either way)
262
- const h = directiveHash(delegatePrompt);
263
- const now = nowISO();
264
- if (sessionId) {
265
- // This assumes continuations table exists in vNext schema.
266
- 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);
267
- }
268
- // Visible injection so user can see state (awaited)
269
- if (sessionId) {
270
- await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
271
- const continueMessage = [
272
- `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
273
- ``,
274
- `Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
275
- ``,
276
- `When \`${agentName}\` completes, call:`,
277
- `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
278
- ``,
279
- `This advances the workflow.`,
280
- ].join("\n");
281
- await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
218
+ throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
282
219
  }
283
- actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
284
- // Stop here; subagent needs to run.
285
- break;
286
220
  }
287
- if (next.kind === "await_stage_completion") {
288
- actions.push(`await stage completion: ${next.stage_key}`);
289
- if (sessionId) {
290
- const context = buildContextSnapshot({
291
- db,
292
- config,
293
- run_id: next.run_id,
294
- next_action: `complete stage ${next.stage_key}`,
295
- });
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" });
309
- }
310
- break;
311
- }
312
- actions.push(`unhandled next action: ${next.kind}`);
313
- break;
314
221
  }
315
- // Flush UI events (toast + prompt) AFTER state transitions
316
- if (uiEvents.length > 0) {
317
- for (const e of uiEvents) {
318
- const msg = buildUiMessage(e);
319
- if (config.ui.toasts.enabled) {
320
- await toasts.show({
321
- title: msg.title,
322
- message: msg.message,
323
- variant: msg.variant,
324
- });
325
- }
326
- if (ctx?.sessionID) {
327
- await injectChatPrompt({
328
- ctx,
329
- sessionId: ctx.sessionID,
330
- text: msg.chatText,
331
- agent: "Astro",
332
- });
333
- }
334
- }
335
- actions.push(`ui: flushed ${uiEvents.length} event(s)`);
222
+ // NOTE: startStage owns its own tx (state-machine.ts).
223
+ withTx(db, () => {
224
+ startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
225
+ });
226
+ const context = buildContextSnapshot({
227
+ db,
228
+ config,
229
+ run_id: active.run_id,
230
+ next_action: `delegate stage ${next.stage_key}`,
231
+ });
232
+ const stageDirective = buildStageDirective({
233
+ config,
234
+ stage_key: next.stage_key,
235
+ run_id: active.run_id,
236
+ story_key: run.story_key,
237
+ story_title: story?.title ?? "(missing)",
238
+ stage_agent_name: agentName,
239
+ stage_goal: stageGoal(next.stage_key, config),
240
+ stage_constraints: stageConstraints(next.stage_key, config),
241
+ context_snapshot_md: context,
242
+ }).body;
243
+ const delegatePrompt = buildDelegationPrompt({
244
+ stageDirective,
245
+ run_id: active.run_id,
246
+ stage_key: next.stage_key,
247
+ stage_agent_name: agentName,
248
+ });
249
+ // Record continuation (best-effort; no tx wrapper needed but safe either way)
250
+ const h = directiveHash(delegatePrompt);
251
+ const now = nowISO();
252
+ if (sessionId) {
253
+ // This assumes continuations table exists in vNext schema.
254
+ 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);
336
255
  }
337
- // Housekeeping event
338
- 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());
339
- const active = getActiveRun(db);
340
- const lines = [];
341
- lines.push(`# astro_workflow_proceed`);
342
- lines.push(`- mode: ${mode}`);
343
- lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
344
- if (active)
345
- lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
346
- lines.push(``, `## Actions`);
347
- for (const a of actions)
348
- lines.push(`- ${a}`);
349
- if (warnings.length > 0) {
350
- lines.push(``, `## Warnings`);
351
- for (const w of warnings)
352
- lines.push(`⚠️ ${w}`);
256
+ // Visible injection so user can see state (awaited)
257
+ if (sessionId) {
258
+ await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
259
+ const continueMessage = [
260
+ `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
261
+ ``,
262
+ `Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
263
+ ``,
264
+ `When \`${agentName}\` completes, call:`,
265
+ `astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
266
+ ``,
267
+ `This advances the workflow.`,
268
+ ].join("\n");
269
+ await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
270
+ }
271
+ actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
272
+ // Stop here; subagent needs to run.
273
+ break;
274
+ }
275
+ if (next.kind === "await_stage_completion") {
276
+ actions.push(`await stage completion: ${next.stage_key}`);
277
+ if (sessionId) {
278
+ const context = buildContextSnapshot({
279
+ db,
280
+ config,
281
+ run_id: next.run_id,
282
+ next_action: `complete stage ${next.stage_key}`,
283
+ });
284
+ const prompt = [
285
+ `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
286
+ ``,
287
+ `Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
288
+ `If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
289
+ ``,
290
+ `Context snapshot:`,
291
+ context,
292
+ ].join("\n").trim();
293
+ const h = directiveHash(prompt);
294
+ const now = nowISO();
295
+ 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);
296
+ await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
297
+ }
298
+ break;
299
+ }
300
+ actions.push(`unhandled next action: ${next.kind}`);
301
+ break;
302
+ }
303
+ // Flush UI events (toast + prompt) AFTER state transitions
304
+ if (uiEvents.length > 0) {
305
+ for (const e of uiEvents) {
306
+ const msg = buildUiMessage(e);
307
+ if (config.ui.toasts.enabled) {
308
+ await toasts.show({
309
+ title: msg.title,
310
+ message: msg.message,
311
+ variant: msg.variant,
312
+ });
313
+ }
314
+ if (ctx?.sessionID) {
315
+ await injectChatPrompt({
316
+ ctx,
317
+ sessionId: ctx.sessionID,
318
+ text: msg.chatText,
319
+ agent: "Astro",
320
+ });
353
321
  }
354
- return lines.join("\n").trim();
355
- },
356
- });
322
+ }
323
+ actions.push(`ui: flushed ${uiEvents.length} event(s)`);
324
+ }
325
+ // Housekeeping event
326
+ 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());
327
+ const active = getActiveRun(db);
328
+ const lines = [];
329
+ lines.push(`# astro_workflow_proceed`);
330
+ lines.push(`- mode: ${mode}`);
331
+ lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
332
+ if (active)
333
+ lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
334
+ lines.push(``, `## Actions`);
335
+ for (const a of actions)
336
+ lines.push(`- ${a}`);
337
+ if (warnings.length > 0) {
338
+ lines.push(``, `## Warnings`);
339
+ for (const w of warnings)
340
+ lines.push(`⚠️ ${w}`);
341
+ }
342
+ return lines.join("\n").trim();
357
343
  },
358
344
  });
359
345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrocode-workflow",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",