astrocode-workflow 0.4.1 → 0.4.3
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.
- package/dist/src/astro/workflow-runner.d.ts +1 -5
- package/dist/src/astro/workflow-runner.js +6 -17
- package/dist/src/hooks/inject-provider.js +23 -0
- package/dist/src/index.js +0 -6
- package/dist/src/tools/health.js +0 -31
- package/dist/src/tools/index.js +0 -3
- package/dist/src/tools/repair.js +4 -37
- package/dist/src/tools/workflow.js +192 -209
- package/package.json +1 -1
- package/src/astro/workflow-runner.ts +5 -25
- package/src/hooks/inject-provider.ts +24 -0
- package/src/index.ts +0 -7
- package/src/tools/health.ts +0 -29
- package/src/tools/index.ts +2 -5
- package/src/tools/repair.ts +4 -38
- package/src/tools/workflow.ts +25 -44
- package/src/state/repo-lock.ts +0 -706
- package/src/state/workflow-repo-lock.ts +0 -111
- package/src/tools/lock.ts +0 -75
|
@@ -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",
|
|
@@ -113,36 +110,33 @@ function buildUiMessage(e) {
|
|
|
113
110
|
case "stage_started": {
|
|
114
111
|
const agent = e.agent_name ? ` (${e.agent_name})` : "";
|
|
115
112
|
const title = "Astrocode";
|
|
116
|
-
const message =
|
|
113
|
+
const message = `▶ Stage started: ${e.stage_key}${agent}`;
|
|
117
114
|
const chatText = [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`Stage: ${e.stage_key}${agent}`,
|
|
115
|
+
`### ▶ ASTROCODE: STAGE_STARTED`,
|
|
116
|
+
`**Run:** \`${e.run_id}\` `,
|
|
117
|
+
`**Stage:** \`${e.stage_key}\`${agent}`,
|
|
122
118
|
].join("\n");
|
|
123
119
|
return { title, message, variant: "info", chatText };
|
|
124
120
|
}
|
|
125
121
|
case "run_completed": {
|
|
126
122
|
const title = "Astrocode";
|
|
127
|
-
const message =
|
|
123
|
+
const message = `✓ Run completed: ${e.run_id}`;
|
|
128
124
|
const chatText = [
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`Story: ${e.story_key}`,
|
|
125
|
+
`### ✓ ASTROCODE: RUN_COMPLETED`,
|
|
126
|
+
`**Run:** \`${e.run_id}\` `,
|
|
127
|
+
`**Story:** \`${e.story_key}\``,
|
|
133
128
|
].join("\n");
|
|
134
129
|
return { title, message, variant: "success", chatText };
|
|
135
130
|
}
|
|
136
131
|
case "run_failed": {
|
|
137
132
|
const title = "Astrocode";
|
|
138
|
-
const message =
|
|
133
|
+
const message = `✖ Run failed: ${e.run_id} (${e.stage_key})`;
|
|
139
134
|
const chatText = [
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
`Error: ${e.error_text}`,
|
|
135
|
+
`### ✖ ASTROCODE: RUN_FAILED`,
|
|
136
|
+
`**Run:** \`${e.run_id}\` `,
|
|
137
|
+
`**Story:** \`${e.story_key}\` `,
|
|
138
|
+
`**Stage:** \`${e.stage_key}\` `,
|
|
139
|
+
`**Error:** ${e.error_text}`,
|
|
146
140
|
].join("\n");
|
|
147
141
|
return { title, message, variant: "error", chatText };
|
|
148
142
|
}
|
|
@@ -158,202 +152,191 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
158
152
|
max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
|
|
159
153
|
},
|
|
160
154
|
execute: async ({ mode, max_steps }) => {
|
|
161
|
-
const repoRoot = ctx.directory;
|
|
162
|
-
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
163
155
|
const sessionId = ctx.sessionID;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
156
|
+
const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
|
|
157
|
+
const actions = [];
|
|
158
|
+
const warnings = [];
|
|
159
|
+
const startedAt = nowISO();
|
|
160
|
+
// Collect UI events emitted inside state-machine functions, then flush AFTER tx.
|
|
161
|
+
const uiEvents = [];
|
|
162
|
+
const emit = (e) => uiEvents.push(e);
|
|
163
|
+
for (let i = 0; i < steps; i++) {
|
|
164
|
+
const next = decideNextAction(db, config);
|
|
165
|
+
if (next.kind === "idle") {
|
|
166
|
+
actions.push("idle: no approved stories");
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (next.kind === "start_run") {
|
|
170
|
+
// SINGLE tx boundary: caller owns tx, state-machine is pure.
|
|
171
|
+
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
172
|
+
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
173
|
+
if (mode === "step")
|
|
174
|
+
break;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (next.kind === "complete_run") {
|
|
178
|
+
withTx(db, () => completeRun(db, next.run_id, emit));
|
|
179
|
+
actions.push(`completed run ${next.run_id}`);
|
|
180
|
+
if (mode === "step")
|
|
181
|
+
break;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (next.kind === "failed") {
|
|
185
|
+
// Ensure DB state reflects failure in one tx; emit UI event.
|
|
186
|
+
withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
|
|
187
|
+
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
188
|
+
if (mode === "step")
|
|
189
|
+
break;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (next.kind === "delegate_stage") {
|
|
193
|
+
const active = getActiveRun(db);
|
|
194
|
+
if (!active)
|
|
195
|
+
throw new Error("Invariant: delegate_stage but no active run.");
|
|
196
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
|
|
197
|
+
const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
|
|
198
|
+
let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
|
|
199
|
+
const agentExists = (name) => {
|
|
200
|
+
if (agents && agents[name])
|
|
201
|
+
return true;
|
|
202
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
203
|
+
if (knownStageAgents.includes(name))
|
|
204
|
+
return true;
|
|
205
|
+
return false;
|
|
206
|
+
};
|
|
207
|
+
if (!agentExists(agentName)) {
|
|
208
|
+
const originalAgent = agentName;
|
|
209
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
210
|
+
agentName = config.agents?.orchestrator_name || "Astro";
|
|
211
|
+
if (!agentExists(agentName)) {
|
|
212
|
+
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
213
|
+
agentName = "General";
|
|
222
214
|
if (!agentExists(agentName)) {
|
|
223
|
-
|
|
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" });
|
|
215
|
+
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
282
216
|
}
|
|
283
|
-
actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
|
|
284
|
-
// Stop here; subagent needs to run.
|
|
285
|
-
break;
|
|
286
217
|
}
|
|
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
218
|
}
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
219
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
220
|
+
withTx(db, () => {
|
|
221
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
|
|
222
|
+
});
|
|
223
|
+
const context = buildContextSnapshot({
|
|
224
|
+
db,
|
|
225
|
+
config,
|
|
226
|
+
run_id: active.run_id,
|
|
227
|
+
next_action: `delegate stage ${next.stage_key}`,
|
|
228
|
+
});
|
|
229
|
+
const stageDirective = buildStageDirective({
|
|
230
|
+
config,
|
|
231
|
+
stage_key: next.stage_key,
|
|
232
|
+
run_id: active.run_id,
|
|
233
|
+
story_key: run.story_key,
|
|
234
|
+
story_title: story?.title ?? "(missing)",
|
|
235
|
+
stage_agent_name: agentName,
|
|
236
|
+
stage_goal: stageGoal(next.stage_key, config),
|
|
237
|
+
stage_constraints: stageConstraints(next.stage_key, config),
|
|
238
|
+
context_snapshot_md: context,
|
|
239
|
+
}).body;
|
|
240
|
+
const delegatePrompt = buildDelegationPrompt({
|
|
241
|
+
stageDirective,
|
|
242
|
+
run_id: active.run_id,
|
|
243
|
+
stage_key: next.stage_key,
|
|
244
|
+
stage_agent_name: agentName,
|
|
245
|
+
});
|
|
246
|
+
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
247
|
+
const h = directiveHash(delegatePrompt);
|
|
248
|
+
const now = nowISO();
|
|
249
|
+
if (sessionId) {
|
|
250
|
+
// This assumes continuations table exists in vNext schema.
|
|
251
|
+
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
252
|
}
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
253
|
+
// Visible injection so user can see state (awaited)
|
|
254
|
+
if (sessionId) {
|
|
255
|
+
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
256
|
+
const continueMessage = [
|
|
257
|
+
`### ⏳ ASTROCODE: AWAITING COMPLETION`,
|
|
258
|
+
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
259
|
+
``,
|
|
260
|
+
`**Action Required:** When \`${agentName}\` completes, call:`,
|
|
261
|
+
`\`\`\`bash`,
|
|
262
|
+
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="...")`,
|
|
263
|
+
`\`\`\``,
|
|
264
|
+
`*Then run \`astro_workflow_proceed\` to continue.*`,
|
|
265
|
+
].join("\n");
|
|
266
|
+
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
267
|
+
}
|
|
268
|
+
actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
|
|
269
|
+
// Stop here; subagent needs to run.
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
if (next.kind === "await_stage_completion") {
|
|
273
|
+
actions.push(`await stage completion: ${next.stage_key}`);
|
|
274
|
+
if (sessionId) {
|
|
275
|
+
const context = buildContextSnapshot({
|
|
276
|
+
db,
|
|
277
|
+
config,
|
|
278
|
+
run_id: next.run_id,
|
|
279
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
280
|
+
});
|
|
281
|
+
const prompt = [
|
|
282
|
+
`### 📥 ASTROCODE: AWAITING OUTPUT`,
|
|
283
|
+
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
284
|
+
``,
|
|
285
|
+
`**Action Required:** If you have the subagent output, call \`astro_stage_complete\` with \`output_text=the FULL output\`.`,
|
|
286
|
+
``,
|
|
287
|
+
`#### Context Snapshot`,
|
|
288
|
+
context,
|
|
289
|
+
].join("\n").trim();
|
|
290
|
+
const h = directiveHash(prompt);
|
|
291
|
+
const now = nowISO();
|
|
292
|
+
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);
|
|
293
|
+
await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
actions.push(`unhandled next action: ${next.kind}`);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
// Flush UI events (toast + prompt) AFTER state transitions
|
|
301
|
+
if (uiEvents.length > 0) {
|
|
302
|
+
for (const e of uiEvents) {
|
|
303
|
+
const msg = buildUiMessage(e);
|
|
304
|
+
if (config.ui.toasts.enabled) {
|
|
305
|
+
await toasts.show({
|
|
306
|
+
title: msg.title,
|
|
307
|
+
message: msg.message,
|
|
308
|
+
variant: msg.variant,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (ctx?.sessionID) {
|
|
312
|
+
await injectChatPrompt({
|
|
313
|
+
ctx,
|
|
314
|
+
sessionId: ctx.sessionID,
|
|
315
|
+
text: msg.chatText,
|
|
316
|
+
agent: "Astro",
|
|
317
|
+
});
|
|
353
318
|
}
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
}
|
|
319
|
+
}
|
|
320
|
+
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
321
|
+
}
|
|
322
|
+
// Housekeeping event
|
|
323
|
+
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());
|
|
324
|
+
const active = getActiveRun(db);
|
|
325
|
+
const lines = [];
|
|
326
|
+
lines.push(`# astro_workflow_proceed`);
|
|
327
|
+
lines.push(`- mode: ${mode}`);
|
|
328
|
+
lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
|
|
329
|
+
if (active)
|
|
330
|
+
lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
|
|
331
|
+
lines.push(``, `## Actions`);
|
|
332
|
+
for (const a of actions)
|
|
333
|
+
lines.push(`- ${a}`);
|
|
334
|
+
if (warnings.length > 0) {
|
|
335
|
+
lines.push(``, `## Warnings`);
|
|
336
|
+
for (const w of warnings)
|
|
337
|
+
lines.push(`⚠️ ${w}`);
|
|
338
|
+
}
|
|
339
|
+
return lines.join("\n").trim();
|
|
357
340
|
},
|
|
358
341
|
});
|
|
359
342
|
}
|
package/package.json
CHANGED
|
@@ -1,36 +1,16 @@
|
|
|
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
3
|
/**
|
|
6
|
-
*
|
|
4
|
+
* Executes the workflow loop.
|
|
7
5
|
* Everything that mutates the repo (tool calls, steps) runs inside this scope.
|
|
8
6
|
*
|
|
9
7
|
* Replace the internals with your actual astro/opencode driver loop.
|
|
10
8
|
*/
|
|
11
9
|
export async function runAstroWorkflow(opts: {
|
|
12
|
-
lockPath: string;
|
|
13
|
-
repoRoot: string;
|
|
14
|
-
sessionId: string;
|
|
15
|
-
owner?: string;
|
|
16
|
-
|
|
17
|
-
// Hook in your existing workflow engine
|
|
18
10
|
proceedOneStep: () => Promise<{ done: boolean }>;
|
|
19
11
|
}): Promise<void> {
|
|
20
|
-
|
|
21
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
repoRoot: opts.repoRoot,
|
|
25
|
-
sessionId: opts.sessionId,
|
|
26
|
-
owner: opts.owner,
|
|
27
|
-
fn: async () => {
|
|
28
|
-
// ✅ Lock is held ONCE for the entire run. Tool calls can "rattle through".
|
|
29
|
-
while (true) {
|
|
30
|
-
const { done } = await opts.proceedOneStep();
|
|
31
|
-
if (done) return;
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
}
|
|
35
|
-
);
|
|
12
|
+
while (true) {
|
|
13
|
+
const { done } = await opts.proceedOneStep();
|
|
14
|
+
if (done) return;
|
|
15
|
+
}
|
|
36
16
|
}
|
|
@@ -3,6 +3,7 @@ import type { SqliteDb } from "../state/db";
|
|
|
3
3
|
import { selectEligibleInjects } from "../tools/injects";
|
|
4
4
|
import { injectChatPrompt } from "../ui/inject";
|
|
5
5
|
import { nowISO } from "../shared/time";
|
|
6
|
+
import { getActiveRun } from "../workflow/state-machine";
|
|
6
7
|
|
|
7
8
|
type ChatMessageInput = {
|
|
8
9
|
sessionID: string;
|
|
@@ -235,6 +236,29 @@ export function createInjectProvider(opts: {
|
|
|
235
236
|
|
|
236
237
|
// Inject eligible injects after workflow tool execution
|
|
237
238
|
await injectEligibleInjects(sessionId, `tool_after:${input.tool}`);
|
|
239
|
+
|
|
240
|
+
// Inject Workflow Pulse if a run is active
|
|
241
|
+
await maybeInjectWorkflowPulse(sessionId);
|
|
238
242
|
},
|
|
239
243
|
};
|
|
244
|
+
|
|
245
|
+
async function maybeInjectWorkflowPulse(sessionId: string) {
|
|
246
|
+
if (!db) return;
|
|
247
|
+
try {
|
|
248
|
+
const active = getActiveRun(db);
|
|
249
|
+
if (!active) return;
|
|
250
|
+
|
|
251
|
+
const agentSuffix = active.current_stage_key ? ` (Agent: ${active.current_stage_key})` : "";
|
|
252
|
+
const pulseText = `📡 **ASTRO PULSE:** Run \`${active.run_id}\` is \`${active.status}\`${agentSuffix}.`;
|
|
253
|
+
|
|
254
|
+
await injectChatPrompt({
|
|
255
|
+
ctx,
|
|
256
|
+
sessionId,
|
|
257
|
+
text: pulseText,
|
|
258
|
+
agent: "Astro"
|
|
259
|
+
});
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore pulse errors
|
|
262
|
+
}
|
|
263
|
+
}
|
|
240
264
|
}
|
package/src/index.ts
CHANGED
|
@@ -59,13 +59,6 @@ const Astrocode: Plugin = async (ctx) => {
|
|
|
59
59
|
}
|
|
60
60
|
const repoRoot = ctx.directory;
|
|
61
61
|
|
|
62
|
-
// NOTE: Repo locking is handled at the workflow level via workflowRepoLock.
|
|
63
|
-
// The workflow tool correctly acquires and holds the lock for the entire workflow execution.
|
|
64
|
-
// Plugin-level locking is unnecessary and architecturally incorrect since:
|
|
65
|
-
// - The lock would be held for the entire session lifecycle (too long)
|
|
66
|
-
// - Individual tools are designed to be called within workflow context where lock is held
|
|
67
|
-
// - Workflow-level locking with refcounting prevents lock churn during tool execution
|
|
68
|
-
|
|
69
62
|
// Always load config first - this provides defaults even in limited mode
|
|
70
63
|
let pluginConfig: AstrocodeConfig;
|
|
71
64
|
try {
|
package/src/tools/health.ts
CHANGED
|
@@ -25,34 +25,6 @@ export function createAstroHealthTool(opts: { ctx: any; config: AstrocodeConfig;
|
|
|
25
25
|
lines.push(`- Repo: ${repoRoot}`);
|
|
26
26
|
lines.push(`- DB Path: ${fullDbPath}`);
|
|
27
27
|
|
|
28
|
-
// Lock status
|
|
29
|
-
const lockPath = `${repoRoot}/.astro/astro.lock`;
|
|
30
|
-
try {
|
|
31
|
-
if (fs.existsSync(lockPath)) {
|
|
32
|
-
const lockContent = fs.readFileSync(lockPath, "utf8").trim();
|
|
33
|
-
const parts = lockContent.split(" ");
|
|
34
|
-
if (parts.length >= 2) {
|
|
35
|
-
const pid = parseInt(parts[0]);
|
|
36
|
-
const startedAt = parts[1];
|
|
37
|
-
|
|
38
|
-
// Check if PID is still running
|
|
39
|
-
try {
|
|
40
|
-
(process as any).kill(pid, 0); // Signal 0 just checks if process exists
|
|
41
|
-
lines.push(`- Lock: HELD by PID ${pid} (started ${startedAt})`);
|
|
42
|
-
} catch {
|
|
43
|
-
lines.push(`- Lock: STALE (PID ${pid} not running, started ${startedAt})`);
|
|
44
|
-
lines.push(` → Run: rm "${lockPath}"`);
|
|
45
|
-
}
|
|
46
|
-
} else {
|
|
47
|
-
lines.push(`- Lock: MALFORMED (${lockContent})`);
|
|
48
|
-
}
|
|
49
|
-
} else {
|
|
50
|
-
lines.push(`- Lock: NONE (no lock file)`);
|
|
51
|
-
}
|
|
52
|
-
} catch (e) {
|
|
53
|
-
lines.push(`- Lock: ERROR (${String(e)})`);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
28
|
// DB file status
|
|
57
29
|
const dbExists = fs.existsSync(fullDbPath);
|
|
58
30
|
const walExists = fs.existsSync(`${fullDbPath}-wal`);
|
|
@@ -116,7 +88,6 @@ export function createAstroHealthTool(opts: { ctx: any; config: AstrocodeConfig;
|
|
|
116
88
|
lines.push(`## Status`);
|
|
117
89
|
lines.push(`✅ DB accessible`);
|
|
118
90
|
lines.push(`✅ Schema valid`);
|
|
119
|
-
lines.push(`✅ Lock file checked`);
|
|
120
91
|
|
|
121
92
|
if (walExists || shmExists) {
|
|
122
93
|
lines.push(`⚠️ WAL/SHM files present - indicates unclean shutdown or active transaction`);
|
package/src/tools/index.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { createAstroRepairTool } from "./repair";
|
|
|
15
15
|
import { createAstroHealthTool } from "./health";
|
|
16
16
|
import { createAstroResetTool } from "./reset";
|
|
17
17
|
import { createAstroMetricsTool } from "./metrics";
|
|
18
|
-
import { createAstroLockStatusTool } from "./lock";
|
|
19
18
|
|
|
20
19
|
import { AgentConfig } from "@opencode-ai/sdk";
|
|
21
20
|
|
|
@@ -44,8 +43,7 @@ export function createAstroTools(opts: CreateAstroToolsOptions): Record<string,
|
|
|
44
43
|
tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
|
|
45
44
|
tools.astro_health = createAstroHealthTool({ ctx, config, db });
|
|
46
45
|
tools.astro_reset = createAstroResetTool({ ctx, config, db });
|
|
47
|
-
|
|
48
|
-
tools.astro_lock_status = createAstroLockStatusTool({ ctx });
|
|
46
|
+
tools.astro_metrics = createAstroMetricsTool({ ctx, config });
|
|
49
47
|
|
|
50
48
|
// Recovery tool - available even in limited mode to allow DB initialization
|
|
51
49
|
tools.astro_init = createAstroInitTool({ ctx, config, runtime });
|
|
@@ -110,8 +108,7 @@ export function createAstroTools(opts: CreateAstroToolsOptions): Record<string,
|
|
|
110
108
|
["_astro_repair", "astro_repair"],
|
|
111
109
|
["_astro_health", "astro_health"],
|
|
112
110
|
["_astro_reset", "astro_reset"],
|
|
113
|
-
|
|
114
|
-
["_astro_lock_status", "astro_lock_status"],
|
|
111
|
+
["_astro_metrics", "astro_metrics"],
|
|
115
112
|
];
|
|
116
113
|
|
|
117
114
|
// Only add aliases for tools that exist
|