astrocode-workflow 0.2.2 → 0.3.1
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/index.js +0 -6
- package/dist/tools/workflow.js +168 -178
- package/package.json +1 -1
- package/src/index.ts +0 -8
- package/src/tools/workflow.ts +2 -12
- package/dist/state/repo-lock.d.ts +0 -3
- package/dist/state/repo-lock.js +0 -29
- package/src/state/repo-lock.ts +0 -26
package/dist/index.js
CHANGED
|
@@ -9,7 +9,6 @@ import { createInjectProvider } from "./hooks/inject-provider";
|
|
|
9
9
|
import { createToastManager } from "./ui/toasts";
|
|
10
10
|
import { createAstroAgents } from "./agents/registry";
|
|
11
11
|
import { info, warn } from "./shared/log";
|
|
12
|
-
import { acquireRepoLock } from "./state/repo-lock";
|
|
13
12
|
// Safe config cloning with structuredClone preference (fallback for older Node versions)
|
|
14
13
|
// CONTRACT: Config is guaranteed JSON-serializable (enforced by loadAstrocodeConfig validation)
|
|
15
14
|
const cloneConfig = (v) => {
|
|
@@ -38,9 +37,6 @@ const Astrocode = async (ctx) => {
|
|
|
38
37
|
throw new Error("Astrocode requires ctx.directory to be a string repo root.");
|
|
39
38
|
}
|
|
40
39
|
const repoRoot = ctx.directory;
|
|
41
|
-
// Acquire exclusive repo lock to prevent multiple processes from corrupting the database
|
|
42
|
-
const lockPath = `${repoRoot}/.astro/astro.lock`;
|
|
43
|
-
const repoLock = acquireRepoLock(lockPath);
|
|
44
40
|
// Always load config first - this provides defaults even in limited mode
|
|
45
41
|
let pluginConfig;
|
|
46
42
|
try {
|
|
@@ -284,8 +280,6 @@ const Astrocode = async (ctx) => {
|
|
|
284
280
|
},
|
|
285
281
|
// Best-effort cleanup
|
|
286
282
|
close: async () => {
|
|
287
|
-
// Release repo lock first (important for process termination)
|
|
288
|
-
repoLock.release();
|
|
289
283
|
if (db && typeof db.close === "function") {
|
|
290
284
|
try {
|
|
291
285
|
db.close();
|
package/dist/tools/workflow.js
CHANGED
|
@@ -9,7 +9,6 @@ import { nowISO } from "../shared/time";
|
|
|
9
9
|
import { newEventId } from "../state/ids";
|
|
10
10
|
import { debug } from "../shared/log";
|
|
11
11
|
import { createToastManager } from "../ui/toasts";
|
|
12
|
-
import { acquireRepoLock } from "../state/repo-lock";
|
|
13
12
|
// Agent name mapping for case-sensitive resolution
|
|
14
13
|
export const STAGE_TO_AGENT_MAP = {
|
|
15
14
|
frame: "Frame",
|
|
@@ -156,200 +155,191 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
156
155
|
max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
|
|
157
156
|
},
|
|
158
157
|
execute: async ({ mode, max_steps }) => {
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
158
|
+
const sessionId = ctx.sessionID;
|
|
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")
|
|
175
177
|
break;
|
|
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
|
-
return true;
|
|
213
|
-
return false;
|
|
214
|
-
};
|
|
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";
|
|
215
214
|
if (!agentExists(agentName)) {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
agentName = config.agents?.orchestrator_name || "Astro";
|
|
215
|
+
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
216
|
+
agentName = "General";
|
|
219
217
|
if (!agentExists(agentName)) {
|
|
220
|
-
|
|
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
|
-
}
|
|
218
|
+
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
225
219
|
}
|
|
226
220
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
});
|
|
221
|
+
}
|
|
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);
|
|
255
|
+
}
|
|
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) {
|
|
231
278
|
const context = buildContextSnapshot({
|
|
232
279
|
db,
|
|
233
280
|
config,
|
|
234
|
-
run_id:
|
|
235
|
-
next_action: `
|
|
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,
|
|
281
|
+
run_id: next.run_id,
|
|
282
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
253
283
|
});
|
|
254
|
-
|
|
255
|
-
|
|
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);
|
|
256
294
|
const now = nowISO();
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
}
|
|
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;
|
|
279
|
-
}
|
|
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;
|
|
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" });
|
|
304
297
|
}
|
|
305
|
-
actions.push(`unhandled next action: ${next.kind}`);
|
|
306
298
|
break;
|
|
307
299
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
});
|
|
327
321
|
}
|
|
328
|
-
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
329
|
-
}
|
|
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}`);
|
|
346
322
|
}
|
|
347
|
-
|
|
323
|
+
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
348
324
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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}`);
|
|
352
341
|
}
|
|
342
|
+
return lines.join("\n").trim();
|
|
353
343
|
},
|
|
354
344
|
});
|
|
355
345
|
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { createToastManager, type ToastOptions } from "./ui/toasts";
|
|
|
13
13
|
import { createAstroAgents } from "./agents/registry";
|
|
14
14
|
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
15
15
|
import { info, warn } from "./shared/log";
|
|
16
|
-
import { acquireRepoLock } from "./state/repo-lock";
|
|
17
16
|
|
|
18
17
|
// Type definitions for plugin components
|
|
19
18
|
type ConfigHandler = (config: Record<string, any>) => Promise<void>;
|
|
@@ -59,10 +58,6 @@ const Astrocode: Plugin = async (ctx) => {
|
|
|
59
58
|
}
|
|
60
59
|
const repoRoot = ctx.directory;
|
|
61
60
|
|
|
62
|
-
// Acquire exclusive repo lock to prevent multiple processes from corrupting the database
|
|
63
|
-
const lockPath = `${repoRoot}/.astro/astro.lock`;
|
|
64
|
-
const repoLock = acquireRepoLock(lockPath);
|
|
65
|
-
|
|
66
61
|
// Always load config first - this provides defaults even in limited mode
|
|
67
62
|
let pluginConfig: AstrocodeConfig;
|
|
68
63
|
try {
|
|
@@ -330,9 +325,6 @@ const Astrocode: Plugin = async (ctx) => {
|
|
|
330
325
|
|
|
331
326
|
// Best-effort cleanup
|
|
332
327
|
close: async () => {
|
|
333
|
-
// Release repo lock first (important for process termination)
|
|
334
|
-
repoLock.release();
|
|
335
|
-
|
|
336
328
|
if (db && typeof db.close === "function") {
|
|
337
329
|
try {
|
|
338
330
|
db.close();
|
package/src/tools/workflow.ts
CHANGED
|
@@ -22,7 +22,6 @@ import { newEventId } from "../state/ids";
|
|
|
22
22
|
import { debug } from "../shared/log";
|
|
23
23
|
import { createToastManager } from "../ui/toasts";
|
|
24
24
|
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
25
|
-
import { acquireRepoLock } from "../state/repo-lock";
|
|
26
25
|
|
|
27
26
|
// Agent name mapping for case-sensitive resolution
|
|
28
27
|
export const STAGE_TO_AGENT_MAP: Record<string, string> = {
|
|
@@ -185,13 +184,8 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
185
184
|
max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
|
|
186
185
|
},
|
|
187
186
|
execute: async ({ mode, max_steps }) => {
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
const repoLock = acquireRepoLock(lockPath);
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
194
|
-
const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
|
|
187
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
188
|
+
const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
|
|
195
189
|
|
|
196
190
|
const actions: string[] = [];
|
|
197
191
|
const warnings: string[] = [];
|
|
@@ -414,10 +408,6 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
414
408
|
}
|
|
415
409
|
|
|
416
410
|
return lines.join("\n").trim();
|
|
417
|
-
} finally {
|
|
418
|
-
// Always release the lock
|
|
419
|
-
repoLock.release();
|
|
420
|
-
}
|
|
421
411
|
},
|
|
422
412
|
});
|
|
423
413
|
}
|
package/dist/state/repo-lock.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// src/state/repo-lock.ts
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
export function acquireRepoLock(lockPath) {
|
|
5
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
6
|
-
let fd;
|
|
7
|
-
try {
|
|
8
|
-
fd = fs.openSync(lockPath, "wx"); // exclusive create
|
|
9
|
-
}
|
|
10
|
-
catch (e) {
|
|
11
|
-
const msg = e?.code === "EEXIST"
|
|
12
|
-
? `Astrocode lock is already held (${lockPath}). Another opencode process is running in this repo.`
|
|
13
|
-
: `Failed to acquire lock (${lockPath}): ${e?.message ?? String(e)}`;
|
|
14
|
-
throw new Error(msg);
|
|
15
|
-
}
|
|
16
|
-
fs.writeFileSync(fd, `${process.pid}\n`, "utf8");
|
|
17
|
-
return {
|
|
18
|
-
release: () => {
|
|
19
|
-
try {
|
|
20
|
-
fs.closeSync(fd);
|
|
21
|
-
}
|
|
22
|
-
catch { }
|
|
23
|
-
try {
|
|
24
|
-
fs.unlinkSync(lockPath);
|
|
25
|
-
}
|
|
26
|
-
catch { }
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
package/src/state/repo-lock.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// src/state/repo-lock.ts
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
export function acquireRepoLock(lockPath: string): { release: () => void } {
|
|
6
|
-
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
7
|
-
|
|
8
|
-
let fd: number;
|
|
9
|
-
try {
|
|
10
|
-
fd = fs.openSync(lockPath, "wx"); // exclusive create
|
|
11
|
-
} catch (e: any) {
|
|
12
|
-
const msg = e?.code === "EEXIST"
|
|
13
|
-
? `Astrocode lock is already held (${lockPath}). Another opencode process is running in this repo.`
|
|
14
|
-
: `Failed to acquire lock (${lockPath}): ${e?.message ?? String(e)}`;
|
|
15
|
-
throw new Error(msg);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
fs.writeFileSync(fd, `${(process as any).pid}\n`, "utf8");
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
release: () => {
|
|
22
|
-
try { fs.closeSync(fd); } catch {}
|
|
23
|
-
try { fs.unlinkSync(lockPath); } catch {}
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|