astrocode-workflow 0.3.4 → 0.3.5-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/src/astro/workflow-runner.d.ts +15 -0
- package/dist/src/astro/workflow-runner.js +25 -0
- package/dist/src/hooks/inject-provider.d.ts +5 -0
- package/dist/src/hooks/inject-provider.js +10 -0
- package/dist/src/index.js +11 -6
- package/dist/src/state/repo-lock.d.ts +65 -1
- package/dist/src/state/repo-lock.js +568 -17
- package/dist/src/state/workflow-repo-lock.d.ts +16 -0
- package/dist/src/state/workflow-repo-lock.js +50 -0
- package/dist/src/tools/index.js +3 -0
- package/dist/src/tools/lock.d.ts +4 -0
- package/dist/src/tools/lock.js +78 -0
- package/dist/src/tools/repair.js +40 -6
- package/dist/src/tools/status.js +1 -1
- package/dist/src/tools/workflow.js +182 -179
- package/dist/src/workflow/repair.js +2 -2
- package/package.json +1 -1
- package/src/hooks/inject-provider.ts +16 -0
- package/src/index.ts +13 -7
- package/src/state/repo-lock.ts +129 -22
- package/src/state/workflow-repo-lock.ts +1 -1
- package/src/tools/index.ts +3 -0
- package/src/tools/lock.ts +75 -0
- package/src/tools/repair.ts +43 -6
- package/src/workflow/repair.ts +2 -2
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
3
|
+
import { getLockStatus, tryRemoveStaleLock } from "../state/repo-lock";
|
|
4
|
+
export function createAstroLockStatusTool(opts) {
|
|
5
|
+
const { ctx } = opts;
|
|
6
|
+
return tool({
|
|
7
|
+
description: "Check Astrocode lock status and attempt repair. Shows diagnostics (PID, age, session) and can remove stale/dead locks.",
|
|
8
|
+
args: {
|
|
9
|
+
attempt_repair: tool.schema.boolean().default(false).describe("If true, attempt to remove stale or dead locks"),
|
|
10
|
+
},
|
|
11
|
+
execute: async ({ attempt_repair }) => {
|
|
12
|
+
const repoRoot = ctx.directory;
|
|
13
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
14
|
+
const status = getLockStatus(lockPath);
|
|
15
|
+
if (!status.exists) {
|
|
16
|
+
return "✅ No lock file found. Repository is unlocked.";
|
|
17
|
+
}
|
|
18
|
+
const lines = [];
|
|
19
|
+
lines.push("# Astrocode Lock Status");
|
|
20
|
+
lines.push("");
|
|
21
|
+
lines.push("## Lock Details");
|
|
22
|
+
lines.push(`- **Path**: ${status.path}`);
|
|
23
|
+
lines.push(`- **PID**: ${status.pid} (${status.pidAlive ? '🟢 ALIVE' : '🔴 DEAD'})`);
|
|
24
|
+
lines.push(`- **Age**: ${status.ageMs ? Math.floor(status.ageMs / 1000) : '?'}s`);
|
|
25
|
+
lines.push(`- **Status**: ${status.isStale ? '⚠️ STALE' : '✅ FRESH'}`);
|
|
26
|
+
if (status.sessionId)
|
|
27
|
+
lines.push(`- **Session**: ${status.sessionId}`);
|
|
28
|
+
if (status.owner)
|
|
29
|
+
lines.push(`- **Owner**: ${status.owner}`);
|
|
30
|
+
if (status.instanceId)
|
|
31
|
+
lines.push(`- **Instance**: ${status.instanceId.substring(0, 8)}...`);
|
|
32
|
+
if (status.leaseId)
|
|
33
|
+
lines.push(`- **Lease**: ${status.leaseId.substring(0, 8)}...`);
|
|
34
|
+
if (status.createdAt)
|
|
35
|
+
lines.push(`- **Created**: ${status.createdAt}`);
|
|
36
|
+
if (status.updatedAt)
|
|
37
|
+
lines.push(`- **Updated**: ${status.updatedAt}`);
|
|
38
|
+
if (status.repoRoot)
|
|
39
|
+
lines.push(`- **Repo**: ${status.repoRoot}`);
|
|
40
|
+
lines.push(`- **Version**: ${status.version ?? 'unknown'}`);
|
|
41
|
+
lines.push("");
|
|
42
|
+
if (attempt_repair) {
|
|
43
|
+
lines.push("## Repair Attempt");
|
|
44
|
+
const result = tryRemoveStaleLock(lockPath);
|
|
45
|
+
if (result.removed) {
|
|
46
|
+
lines.push(`✅ **Lock removed**: ${result.reason}`);
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push("The repository is now unlocked and ready for use.");
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
lines.push(`⚠️ **Lock NOT removed**: ${result.reason}`);
|
|
52
|
+
lines.push("");
|
|
53
|
+
lines.push("**Recommendations**:");
|
|
54
|
+
lines.push("- If the owning process has crashed, wait 30 seconds for automatic stale detection");
|
|
55
|
+
lines.push("- If the process is still running, wait for it to complete");
|
|
56
|
+
lines.push("- As a last resort, manually stop the process and run this tool again with attempt_repair=true");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
lines.push("## Recommendations");
|
|
61
|
+
if (!status.pidAlive) {
|
|
62
|
+
lines.push("🔧 **Action Required**: Lock belongs to dead process. Run with `attempt_repair=true` to remove it.");
|
|
63
|
+
}
|
|
64
|
+
else if (status.isStale) {
|
|
65
|
+
lines.push("🔧 **Action Suggested**: Lock is stale (not updated recently). Run with `attempt_repair=true` to remove it.");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push("✅ Lock is active and healthy. The owning process is running normally.");
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("If you believe this is incorrect:");
|
|
71
|
+
lines.push("- Wait 30 seconds and check again (automatic stale detection)");
|
|
72
|
+
lines.push("- Run with `attempt_repair=true` only if you're certain the process has crashed");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
package/dist/src/tools/repair.js
CHANGED
|
@@ -1,25 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
3
|
import { withTx } from "../state/db";
|
|
3
4
|
import { repairState, formatRepairReport } from "../workflow/repair";
|
|
4
5
|
import { putArtifact } from "../workflow/artifacts";
|
|
5
6
|
import { nowISO } from "../shared/time";
|
|
7
|
+
import { getLockStatus, tryRemoveStaleLock } from "../state/repo-lock";
|
|
6
8
|
export function createAstroRepairTool(opts) {
|
|
7
9
|
const { ctx, config, db } = opts;
|
|
8
10
|
return tool({
|
|
9
|
-
description: "Repair Astrocode invariants and recover from inconsistent DB state. Writes a repair report artifact.",
|
|
11
|
+
description: "Repair Astrocode invariants and recover from inconsistent DB state. Also checks and repairs lock files. Writes a repair report artifact.",
|
|
10
12
|
args: {
|
|
11
13
|
write_report_artifact: tool.schema.boolean().default(true),
|
|
14
|
+
repair_lock: tool.schema.boolean().default(true).describe("Attempt to remove stale/dead lock files"),
|
|
12
15
|
},
|
|
13
|
-
execute: async ({ write_report_artifact }) => {
|
|
16
|
+
execute: async ({ write_report_artifact, repair_lock }) => {
|
|
14
17
|
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
45
|
const report = withTx(db, () => repairState(db, config));
|
|
16
|
-
const
|
|
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
51
|
if (write_report_artifact) {
|
|
18
52
|
const rel = `.astro/repair/repair_${nowISO().replace(/[:.]/g, "-")}.md`;
|
|
19
|
-
const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content:
|
|
20
|
-
return
|
|
53
|
+
const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content: fullMd, meta: { kind: "repair" } });
|
|
54
|
+
return fullMd + `\n\nReport saved: ${rel} (artifact=${a.artifact_id})`;
|
|
21
55
|
}
|
|
22
|
-
return
|
|
56
|
+
return fullMd;
|
|
23
57
|
},
|
|
24
58
|
});
|
|
25
59
|
}
|
package/dist/src/tools/status.js
CHANGED
|
@@ -31,7 +31,7 @@ function stageIcon(status) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
export function createAstroStatusTool(opts) {
|
|
34
|
-
const { config, db } = opts;
|
|
34
|
+
const { ctx, config, db } = opts;
|
|
35
35
|
return tool({
|
|
36
36
|
description: "Show a compact Astrocode status dashboard: active run/stage, pipeline, story board counts, and next action.",
|
|
37
37
|
args: {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/tools/workflow.ts
|
|
2
2
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
import { withTx } from "../state/db";
|
|
4
5
|
import { buildContextSnapshot } from "../workflow/context";
|
|
5
6
|
import { decideNextAction, createRunForStory, startStage, completeRun, failRun, getActiveRun, EVENT_TYPES, } from "../workflow/state-machine";
|
|
@@ -10,6 +11,7 @@ import { newEventId } from "../state/ids";
|
|
|
10
11
|
import { debug } from "../shared/log";
|
|
11
12
|
import { createToastManager } from "../ui/toasts";
|
|
12
13
|
import { acquireRepoLock } from "../state/repo-lock";
|
|
14
|
+
import { workflowRepoLock } from "../state/workflow-repo-lock";
|
|
13
15
|
// Agent name mapping for case-sensitive resolution
|
|
14
16
|
export const STAGE_TO_AGENT_MAP = {
|
|
15
17
|
frame: "Frame",
|
|
@@ -156,200 +158,201 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
156
158
|
max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
|
|
157
159
|
},
|
|
158
160
|
execute: async ({ mode, max_steps }) => {
|
|
159
|
-
|
|
160
|
-
const lockPath =
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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;
|
|
184
|
-
}
|
|
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")
|
|
161
|
+
const repoRoot = ctx.directory;
|
|
162
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
163
|
+
const sessionId = ctx.sessionID;
|
|
164
|
+
return workflowRepoLock({ acquireRepoLock }, {
|
|
165
|
+
lockPath,
|
|
166
|
+
repoRoot,
|
|
167
|
+
sessionId,
|
|
168
|
+
owner: "astro_workflow_proceed",
|
|
169
|
+
fn: async () => {
|
|
170
|
+
const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
|
|
171
|
+
const actions = [];
|
|
172
|
+
const warnings = [];
|
|
173
|
+
const startedAt = nowISO();
|
|
174
|
+
// Collect UI events emitted inside state-machine functions, then flush AFTER tx.
|
|
175
|
+
const uiEvents = [];
|
|
176
|
+
const emit = (e) => uiEvents.push(e);
|
|
177
|
+
for (let i = 0; i < steps; i++) {
|
|
178
|
+
const next = decideNextAction(db, config);
|
|
179
|
+
if (next.kind === "idle") {
|
|
180
|
+
actions.push("idle: no approved stories");
|
|
189
181
|
break;
|
|
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
|
-
|
|
182
|
+
}
|
|
183
|
+
if (next.kind === "start_run") {
|
|
184
|
+
// SINGLE tx boundary: caller owns tx, state-machine is pure.
|
|
185
|
+
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
186
|
+
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
187
|
+
if (mode === "step")
|
|
188
|
+
break;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (next.kind === "complete_run") {
|
|
192
|
+
withTx(db, () => completeRun(db, next.run_id, emit));
|
|
193
|
+
actions.push(`completed run ${next.run_id}`);
|
|
194
|
+
if (mode === "step")
|
|
195
|
+
break;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (next.kind === "failed") {
|
|
199
|
+
// Ensure DB state reflects failure in one tx; emit UI event.
|
|
200
|
+
withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
|
|
201
|
+
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
202
|
+
if (mode === "step")
|
|
203
|
+
break;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (next.kind === "delegate_stage") {
|
|
207
|
+
const active = getActiveRun(db);
|
|
208
|
+
if (!active)
|
|
209
|
+
throw new Error("Invariant: delegate_stage but no active run.");
|
|
210
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
|
|
211
|
+
const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
|
|
212
|
+
let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
|
|
213
|
+
const agentExists = (name) => {
|
|
214
|
+
if (agents && agents[name])
|
|
215
|
+
return true;
|
|
216
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
217
|
+
if (knownStageAgents.includes(name))
|
|
218
|
+
return true;
|
|
219
|
+
return false;
|
|
220
|
+
};
|
|
219
221
|
if (!agentExists(agentName)) {
|
|
220
|
-
|
|
221
|
-
agentName
|
|
222
|
+
const originalAgent = agentName;
|
|
223
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
224
|
+
agentName = config.agents?.orchestrator_name || "Astro";
|
|
222
225
|
if (!agentExists(agentName)) {
|
|
223
|
-
|
|
226
|
+
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
227
|
+
agentName = "General";
|
|
228
|
+
if (!agentExists(agentName)) {
|
|
229
|
+
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
230
|
+
}
|
|
224
231
|
}
|
|
225
232
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
});
|
|
231
|
-
const context = buildContextSnapshot({
|
|
232
|
-
db,
|
|
233
|
-
config,
|
|
234
|
-
run_id: active.run_id,
|
|
235
|
-
next_action: `delegate stage ${next.stage_key}`,
|
|
236
|
-
});
|
|
237
|
-
const stageDirective = buildStageDirective({
|
|
238
|
-
config,
|
|
239
|
-
stage_key: next.stage_key,
|
|
240
|
-
run_id: active.run_id,
|
|
241
|
-
story_key: run.story_key,
|
|
242
|
-
story_title: story?.title ?? "(missing)",
|
|
243
|
-
stage_agent_name: agentName,
|
|
244
|
-
stage_goal: stageGoal(next.stage_key, config),
|
|
245
|
-
stage_constraints: stageConstraints(next.stage_key, config),
|
|
246
|
-
context_snapshot_md: context,
|
|
247
|
-
}).body;
|
|
248
|
-
const delegatePrompt = buildDelegationPrompt({
|
|
249
|
-
stageDirective,
|
|
250
|
-
run_id: active.run_id,
|
|
251
|
-
stage_key: next.stage_key,
|
|
252
|
-
stage_agent_name: agentName,
|
|
253
|
-
});
|
|
254
|
-
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
255
|
-
const h = directiveHash(delegatePrompt);
|
|
256
|
-
const now = nowISO();
|
|
257
|
-
if (sessionId) {
|
|
258
|
-
// This assumes continuations table exists in vNext schema.
|
|
259
|
-
db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)").run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
|
|
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) {
|
|
233
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
234
|
+
withTx(db, () => {
|
|
235
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
|
|
236
|
+
});
|
|
283
237
|
const context = buildContextSnapshot({
|
|
284
238
|
db,
|
|
285
239
|
config,
|
|
286
|
-
run_id:
|
|
287
|
-
next_action: `
|
|
240
|
+
run_id: active.run_id,
|
|
241
|
+
next_action: `delegate stage ${next.stage_key}`,
|
|
242
|
+
});
|
|
243
|
+
const stageDirective = buildStageDirective({
|
|
244
|
+
config,
|
|
245
|
+
stage_key: next.stage_key,
|
|
246
|
+
run_id: active.run_id,
|
|
247
|
+
story_key: run.story_key,
|
|
248
|
+
story_title: story?.title ?? "(missing)",
|
|
249
|
+
stage_agent_name: agentName,
|
|
250
|
+
stage_goal: stageGoal(next.stage_key, config),
|
|
251
|
+
stage_constraints: stageConstraints(next.stage_key, config),
|
|
252
|
+
context_snapshot_md: context,
|
|
253
|
+
}).body;
|
|
254
|
+
const delegatePrompt = buildDelegationPrompt({
|
|
255
|
+
stageDirective,
|
|
256
|
+
run_id: active.run_id,
|
|
257
|
+
stage_key: next.stage_key,
|
|
258
|
+
stage_agent_name: agentName,
|
|
288
259
|
});
|
|
289
|
-
|
|
290
|
-
|
|
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);
|
|
260
|
+
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
261
|
+
const h = directiveHash(delegatePrompt);
|
|
299
262
|
const now = nowISO();
|
|
300
|
-
|
|
301
|
-
|
|
263
|
+
if (sessionId) {
|
|
264
|
+
// This assumes continuations table exists in vNext schema.
|
|
265
|
+
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);
|
|
266
|
+
}
|
|
267
|
+
// Visible injection so user can see state (awaited)
|
|
268
|
+
if (sessionId) {
|
|
269
|
+
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
270
|
+
const continueMessage = [
|
|
271
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_COMPLETION]`,
|
|
272
|
+
``,
|
|
273
|
+
`Stage \`${next.stage_key}\` delegated to \`${agentName}\`.`,
|
|
274
|
+
``,
|
|
275
|
+
`When \`${agentName}\` completes, call:`,
|
|
276
|
+
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
277
|
+
``,
|
|
278
|
+
`This advances the workflow.`,
|
|
279
|
+
].join("\n");
|
|
280
|
+
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
281
|
+
}
|
|
282
|
+
actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
|
|
283
|
+
// Stop here; subagent needs to run.
|
|
284
|
+
break;
|
|
302
285
|
}
|
|
286
|
+
if (next.kind === "await_stage_completion") {
|
|
287
|
+
actions.push(`await stage completion: ${next.stage_key}`);
|
|
288
|
+
if (sessionId) {
|
|
289
|
+
const context = buildContextSnapshot({
|
|
290
|
+
db,
|
|
291
|
+
config,
|
|
292
|
+
run_id: next.run_id,
|
|
293
|
+
next_action: `complete stage ${next.stage_key}`,
|
|
294
|
+
});
|
|
295
|
+
const prompt = [
|
|
296
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
|
|
297
|
+
``,
|
|
298
|
+
`Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
|
|
299
|
+
`If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
|
|
300
|
+
``,
|
|
301
|
+
`Context snapshot:`,
|
|
302
|
+
context,
|
|
303
|
+
].join("\n").trim();
|
|
304
|
+
const h = directiveHash(prompt);
|
|
305
|
+
const now = nowISO();
|
|
306
|
+
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);
|
|
307
|
+
await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
actions.push(`unhandled next action: ${next.kind}`);
|
|
303
312
|
break;
|
|
304
313
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
agent: "Astro",
|
|
325
|
-
});
|
|
314
|
+
// Flush UI events (toast + prompt) AFTER state transitions
|
|
315
|
+
if (uiEvents.length > 0) {
|
|
316
|
+
for (const e of uiEvents) {
|
|
317
|
+
const msg = buildUiMessage(e);
|
|
318
|
+
if (config.ui.toasts.enabled) {
|
|
319
|
+
await toasts.show({
|
|
320
|
+
title: msg.title,
|
|
321
|
+
message: msg.message,
|
|
322
|
+
variant: msg.variant,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
if (ctx?.sessionID) {
|
|
326
|
+
await injectChatPrompt({
|
|
327
|
+
ctx,
|
|
328
|
+
sessionId: ctx.sessionID,
|
|
329
|
+
text: msg.chatText,
|
|
330
|
+
agent: "Astro",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
326
333
|
}
|
|
334
|
+
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
335
|
+
}
|
|
336
|
+
// Housekeeping event
|
|
337
|
+
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());
|
|
338
|
+
const active = getActiveRun(db);
|
|
339
|
+
const lines = [];
|
|
340
|
+
lines.push(`# astro_workflow_proceed`);
|
|
341
|
+
lines.push(`- mode: ${mode}`);
|
|
342
|
+
lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
|
|
343
|
+
if (active)
|
|
344
|
+
lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
|
|
345
|
+
lines.push(``, `## Actions`);
|
|
346
|
+
for (const a of actions)
|
|
347
|
+
lines.push(`- ${a}`);
|
|
348
|
+
if (warnings.length > 0) {
|
|
349
|
+
lines.push(``, `## Warnings`);
|
|
350
|
+
for (const w of warnings)
|
|
351
|
+
lines.push(`⚠️ ${w}`);
|
|
327
352
|
}
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
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();
|
|
348
|
-
}
|
|
349
|
-
finally {
|
|
350
|
-
// Always release the lock
|
|
351
|
-
repoLock.release();
|
|
352
|
-
}
|
|
353
|
+
return lines.join("\n").trim();
|
|
354
|
+
},
|
|
355
|
+
});
|
|
353
356
|
},
|
|
354
357
|
});
|
|
355
358
|
}
|
|
@@ -43,10 +43,10 @@ export function repairState(db, config) {
|
|
|
43
43
|
.all(active.run_id);
|
|
44
44
|
if (stageRuns.length < pipeline.length) {
|
|
45
45
|
const existingKeys = new Set(stageRuns.map((s) => s.stage_key));
|
|
46
|
-
const insert = db.prepare("INSERT INTO stage_runs (stage_run_id, run_id, stage_key, stage_index, status, updated_at) VALUES (?, ?, ?, ?, 'pending', ?)");
|
|
46
|
+
const insert = db.prepare("INSERT INTO stage_runs (stage_run_id, run_id, stage_key, stage_index, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'pending', ?, ?)");
|
|
47
47
|
pipeline.forEach((key, idx) => {
|
|
48
48
|
if (!existingKeys.has(key)) {
|
|
49
|
-
insert.run(newStageRunId(), active.run_id, key, idx, now);
|
|
49
|
+
insert.run(newStageRunId(), active.run_id, key, idx, now, now);
|
|
50
50
|
push(report, `Inserted missing stage_run ${key} for run ${active.run_id}`);
|
|
51
51
|
}
|
|
52
52
|
});
|
package/package.json
CHANGED
|
@@ -9,6 +9,11 @@ type ChatMessageInput = {
|
|
|
9
9
|
agent: string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
type ToolExecuteAfterInput = {
|
|
13
|
+
tool: string;
|
|
14
|
+
sessionID?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
12
17
|
type RuntimeState = {
|
|
13
18
|
db: SqliteDb | null;
|
|
14
19
|
limitedMode: boolean;
|
|
@@ -156,5 +161,16 @@ export function createInjectProvider(opts: {
|
|
|
156
161
|
// Inject eligible injects before processing the user's message
|
|
157
162
|
await injectEligibleInjects(input.sessionID);
|
|
158
163
|
},
|
|
164
|
+
|
|
165
|
+
async onToolAfter(input: ToolExecuteAfterInput) {
|
|
166
|
+
if (!config.inject?.enabled) return;
|
|
167
|
+
|
|
168
|
+
// Extract sessionID (same pattern as continuation enforcer)
|
|
169
|
+
const sessionId = input.sessionID ?? (ctx as any).sessionID;
|
|
170
|
+
if (!sessionId) return;
|
|
171
|
+
|
|
172
|
+
// Inject eligible injects after tool execution
|
|
173
|
+
await injectEligibleInjects(sessionId);
|
|
174
|
+
},
|
|
159
175
|
};
|
|
160
176
|
}
|