astrocode-workflow 0.4.0 → 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.
- package/dist/index.js +6 -0
- package/dist/shared/metrics.d.ts +66 -0
- package/dist/shared/metrics.js +112 -0
- package/dist/src/agents/commands.d.ts +9 -0
- package/dist/src/agents/commands.js +121 -0
- package/dist/src/agents/prompts.d.ts +3 -0
- package/dist/src/agents/prompts.js +232 -0
- package/dist/src/agents/registry.d.ts +6 -0
- package/dist/src/agents/registry.js +242 -0
- package/dist/src/agents/types.d.ts +14 -0
- package/dist/src/agents/types.js +8 -0
- package/dist/src/astro/workflow-runner.d.ts +11 -0
- package/dist/src/astro/workflow-runner.js +14 -0
- package/dist/src/config/config-handler.d.ts +4 -0
- package/dist/src/config/config-handler.js +46 -0
- package/dist/src/config/defaults.d.ts +3 -0
- package/dist/src/config/defaults.js +3 -0
- package/dist/src/config/loader.d.ts +11 -0
- package/dist/src/config/loader.js +82 -0
- package/dist/src/config/schema.d.ts +195 -0
- package/dist/src/config/schema.js +224 -0
- package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
- package/dist/src/hooks/continuation-enforcer.js +190 -0
- package/dist/src/hooks/inject-provider.d.ts +27 -0
- package/dist/src/hooks/inject-provider.js +189 -0
- package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
- package/dist/src/hooks/tool-output-truncator.js +57 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +307 -0
- package/dist/src/shared/deep-merge.d.ts +8 -0
- package/dist/src/shared/deep-merge.js +25 -0
- package/dist/src/shared/hash.d.ts +1 -0
- package/dist/src/shared/hash.js +4 -0
- package/dist/src/shared/log.d.ts +7 -0
- package/dist/src/shared/log.js +24 -0
- package/dist/src/shared/metrics.d.ts +66 -0
- package/dist/src/shared/metrics.js +112 -0
- package/dist/src/shared/model-tuning.d.ts +9 -0
- package/dist/src/shared/model-tuning.js +28 -0
- package/dist/src/shared/paths.d.ts +19 -0
- package/dist/src/shared/paths.js +64 -0
- package/dist/src/shared/text.d.ts +4 -0
- package/dist/src/shared/text.js +19 -0
- package/dist/src/shared/time.d.ts +1 -0
- package/dist/src/shared/time.js +3 -0
- package/dist/src/state/adapters/index.d.ts +41 -0
- package/dist/src/state/adapters/index.js +115 -0
- package/dist/src/state/db.d.ts +16 -0
- package/dist/src/state/db.js +225 -0
- package/dist/src/state/ids.d.ts +8 -0
- package/dist/src/state/ids.js +25 -0
- package/dist/src/state/repo-lock.d.ts +67 -0
- package/dist/src/state/repo-lock.js +580 -0
- package/dist/src/state/schema.d.ts +2 -0
- package/dist/src/state/schema.js +258 -0
- package/dist/src/state/types.d.ts +71 -0
- package/dist/src/state/types.js +1 -0
- package/dist/src/state/workflow-repo-lock.d.ts +23 -0
- package/dist/src/state/workflow-repo-lock.js +83 -0
- package/dist/src/tools/artifacts.d.ts +18 -0
- package/dist/src/tools/artifacts.js +71 -0
- package/dist/src/tools/health.d.ts +8 -0
- package/dist/src/tools/health.js +88 -0
- package/dist/src/tools/index.d.ts +20 -0
- package/dist/src/tools/index.js +94 -0
- package/dist/src/tools/init.d.ts +17 -0
- package/dist/src/tools/init.js +96 -0
- package/dist/src/tools/injects.d.ts +53 -0
- package/dist/src/tools/injects.js +325 -0
- package/dist/src/tools/lock.d.ts +4 -0
- package/dist/src/tools/lock.js +78 -0
- package/dist/src/tools/metrics.d.ts +7 -0
- package/dist/src/tools/metrics.js +61 -0
- package/dist/src/tools/repair.d.ts +8 -0
- package/dist/src/tools/repair.js +26 -0
- package/dist/src/tools/reset.d.ts +8 -0
- package/dist/src/tools/reset.js +92 -0
- package/dist/src/tools/run.d.ts +13 -0
- package/dist/src/tools/run.js +54 -0
- package/dist/src/tools/spec.d.ts +12 -0
- package/dist/src/tools/spec.js +44 -0
- package/dist/src/tools/stage.d.ts +23 -0
- package/dist/src/tools/stage.js +371 -0
- package/dist/src/tools/status.d.ts +8 -0
- package/dist/src/tools/status.js +125 -0
- package/dist/src/tools/story.d.ts +23 -0
- package/dist/src/tools/story.js +85 -0
- package/dist/src/tools/workflow.d.ts +13 -0
- package/dist/src/tools/workflow.js +345 -0
- package/dist/src/ui/inject.d.ts +12 -0
- package/dist/src/ui/inject.js +107 -0
- package/dist/src/ui/toasts.d.ts +13 -0
- package/dist/src/ui/toasts.js +39 -0
- package/dist/src/workflow/artifacts.d.ts +24 -0
- package/dist/src/workflow/artifacts.js +45 -0
- package/dist/src/workflow/baton.d.ts +72 -0
- package/dist/src/workflow/baton.js +166 -0
- package/dist/src/workflow/context.d.ts +20 -0
- package/dist/src/workflow/context.js +113 -0
- package/dist/src/workflow/directives.d.ts +39 -0
- package/dist/src/workflow/directives.js +137 -0
- package/dist/src/workflow/repair.d.ts +8 -0
- package/dist/src/workflow/repair.js +99 -0
- package/dist/src/workflow/state-machine.d.ts +86 -0
- package/dist/src/workflow/state-machine.js +216 -0
- package/dist/src/workflow/story-helpers.d.ts +9 -0
- package/dist/src/workflow/story-helpers.js +13 -0
- package/dist/state/db.d.ts +1 -0
- package/dist/state/db.js +9 -0
- package/dist/state/repo-lock.d.ts +3 -0
- package/dist/state/repo-lock.js +29 -0
- package/dist/test/integration/db-transactions.test.d.ts +1 -0
- package/dist/test/integration/db-transactions.test.js +126 -0
- package/dist/test/integration/injection-metrics.test.d.ts +1 -0
- package/dist/test/integration/injection-metrics.test.js +129 -0
- package/dist/tools/health.d.ts +8 -0
- package/dist/tools/health.js +119 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/metrics.d.ts +7 -0
- package/dist/tools/metrics.js +61 -0
- package/dist/tools/reset.d.ts +8 -0
- package/dist/tools/reset.js +92 -0
- package/dist/tools/workflow.js +178 -168
- package/dist/ui/inject.js +21 -9
- package/package.json +6 -4
- package/src/astro/workflow-runner.ts +16 -0
- package/src/config/schema.ts +1 -0
- package/src/hooks/inject-provider.ts +94 -14
- package/src/index.ts +7 -0
- package/src/shared/metrics.ts +148 -0
- package/src/state/db.ts +10 -1
- package/src/state/schema.ts +8 -1
- package/src/tools/health.ts +99 -0
- package/src/tools/index.ts +12 -3
- package/src/tools/init.ts +7 -6
- package/src/tools/metrics.ts +71 -0
- package/src/tools/repair.ts +8 -4
- package/src/tools/reset.ts +100 -0
- package/src/tools/stage.ts +1 -0
- package/src/tools/status.ts +2 -1
- package/src/tools/story.ts +1 -0
- package/src/tools/workflow.ts +2 -0
- package/src/ui/inject.ts +21 -9
- package/src/workflow/repair.ts +2 -2
package/dist/tools/workflow.js
CHANGED
|
@@ -9,6 +9,7 @@ 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";
|
|
12
13
|
// Agent name mapping for case-sensitive resolution
|
|
13
14
|
export const STAGE_TO_AGENT_MAP = {
|
|
14
15
|
frame: "Frame",
|
|
@@ -155,191 +156,200 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
155
156
|
max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
|
|
156
157
|
},
|
|
157
158
|
execute: async ({ mode, max_steps }) => {
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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")
|
|
159
|
+
// Acquire repo lock to ensure no concurrent workflow operations
|
|
160
|
+
const lockPath = `${ctx.directory}/.astro/astro.lock`;
|
|
161
|
+
const repoLock = acquireRepoLock(lockPath);
|
|
162
|
+
try {
|
|
163
|
+
const sessionId = ctx.sessionID;
|
|
164
|
+
const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
|
|
165
|
+
const actions = [];
|
|
166
|
+
const warnings = [];
|
|
167
|
+
const startedAt = nowISO();
|
|
168
|
+
// Collect UI events emitted inside state-machine functions, then flush AFTER tx.
|
|
169
|
+
const uiEvents = [];
|
|
170
|
+
const emit = (e) => uiEvents.push(e);
|
|
171
|
+
for (let i = 0; i < steps; i++) {
|
|
172
|
+
const next = decideNextAction(db, config);
|
|
173
|
+
if (next.kind === "idle") {
|
|
174
|
+
actions.push("idle: no approved stories");
|
|
184
175
|
break;
|
|
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
|
-
|
|
176
|
+
}
|
|
177
|
+
if (next.kind === "start_run") {
|
|
178
|
+
// SINGLE tx boundary: caller owns tx, state-machine is pure.
|
|
179
|
+
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
180
|
+
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
181
|
+
if (mode === "step")
|
|
182
|
+
break;
|
|
183
|
+
continue;
|
|
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")
|
|
189
|
+
break;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (next.kind === "failed") {
|
|
193
|
+
// Ensure DB state reflects failure in one tx; emit UI event.
|
|
194
|
+
withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
|
|
195
|
+
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
196
|
+
if (mode === "step")
|
|
197
|
+
break;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (next.kind === "delegate_stage") {
|
|
201
|
+
const active = getActiveRun(db);
|
|
202
|
+
if (!active)
|
|
203
|
+
throw new Error("Invariant: delegate_stage but no active run.");
|
|
204
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
|
|
205
|
+
const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
|
|
206
|
+
let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
|
|
207
|
+
const agentExists = (name) => {
|
|
208
|
+
if (agents && agents[name])
|
|
209
|
+
return true;
|
|
210
|
+
const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
|
|
211
|
+
if (knownStageAgents.includes(name))
|
|
212
|
+
return true;
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
214
215
|
if (!agentExists(agentName)) {
|
|
215
|
-
|
|
216
|
-
agentName
|
|
216
|
+
const originalAgent = agentName;
|
|
217
|
+
console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
|
|
218
|
+
agentName = config.agents?.orchestrator_name || "Astro";
|
|
217
219
|
if (!agentExists(agentName)) {
|
|
218
|
-
|
|
220
|
+
console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
|
|
221
|
+
agentName = "General";
|
|
222
|
+
if (!agentExists(agentName)) {
|
|
223
|
+
throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
|
|
224
|
+
}
|
|
219
225
|
}
|
|
220
226
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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) {
|
|
227
|
+
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
228
|
+
withTx(db, () => {
|
|
229
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
|
|
230
|
+
});
|
|
278
231
|
const context = buildContextSnapshot({
|
|
279
232
|
db,
|
|
280
233
|
config,
|
|
281
|
-
run_id:
|
|
282
|
-
next_action: `
|
|
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,
|
|
283
253
|
});
|
|
284
|
-
|
|
285
|
-
|
|
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);
|
|
254
|
+
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
255
|
+
const h = directiveHash(delegatePrompt);
|
|
294
256
|
const now = nowISO();
|
|
295
|
-
|
|
296
|
-
|
|
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) {
|
|
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;
|
|
297
304
|
}
|
|
305
|
+
actions.push(`unhandled next action: ${next.kind}`);
|
|
298
306
|
break;
|
|
299
307
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
agent: "Astro",
|
|
320
|
-
});
|
|
308
|
+
// Flush UI events (toast + prompt) AFTER state transitions
|
|
309
|
+
if (uiEvents.length > 0) {
|
|
310
|
+
for (const e of uiEvents) {
|
|
311
|
+
const msg = buildUiMessage(e);
|
|
312
|
+
if (config.ui.toasts.enabled) {
|
|
313
|
+
await toasts.show({
|
|
314
|
+
title: msg.title,
|
|
315
|
+
message: msg.message,
|
|
316
|
+
variant: msg.variant,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (ctx?.sessionID) {
|
|
320
|
+
await injectChatPrompt({
|
|
321
|
+
ctx,
|
|
322
|
+
sessionId: ctx.sessionID,
|
|
323
|
+
text: msg.chatText,
|
|
324
|
+
agent: "Astro",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
321
327
|
}
|
|
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}`);
|
|
322
346
|
}
|
|
323
|
-
|
|
347
|
+
return lines.join("\n").trim();
|
|
324
348
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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}`);
|
|
349
|
+
finally {
|
|
350
|
+
// Always release the lock
|
|
351
|
+
repoLock.release();
|
|
341
352
|
}
|
|
342
|
-
return lines.join("\n").trim();
|
|
343
353
|
},
|
|
344
354
|
});
|
|
345
355
|
}
|
package/dist/ui/inject.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// src/ui/inject.ts
|
|
2
|
+
import { recordInjection, recordError } from "../shared/metrics";
|
|
1
3
|
const MAX_ATTEMPTS = 4;
|
|
2
4
|
const RETRY_DELAYS_MS = [250, 500, 1000, 2000]; // attempt 1..4
|
|
3
5
|
// Per-session queues so one stuck session doesn't block others
|
|
@@ -17,15 +19,24 @@ function getPromptInvoker(ctx) {
|
|
|
17
19
|
async function tryInjectOnce(item) {
|
|
18
20
|
const { ctx, sessionId, text, agent = "Astro" } = item;
|
|
19
21
|
const prefixedText = `[${agent}]\n\n${text}`;
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
const injectionRecorder = recordInjection({ sessionId, attempts: item.attempts + 1, agent });
|
|
23
|
+
const injectionStart = injectionRecorder.start();
|
|
24
|
+
try {
|
|
25
|
+
const { session, prompt } = getPromptInvoker(ctx);
|
|
26
|
+
// IMPORTANT: force correct `this` binding
|
|
27
|
+
await prompt.call(session, {
|
|
28
|
+
path: { id: sessionId },
|
|
29
|
+
body: {
|
|
30
|
+
parts: [{ type: "text", text: prefixedText }],
|
|
31
|
+
agent,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
injectionRecorder.end(injectionStart, true);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
injectionRecorder.end(injectionStart, false);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
29
40
|
}
|
|
30
41
|
async function runSessionQueue(sessionId) {
|
|
31
42
|
if (running.has(sessionId))
|
|
@@ -48,6 +59,7 @@ async function runSessionQueue(sessionId) {
|
|
|
48
59
|
const delay = RETRY_DELAYS_MS[Math.min(item.attempts - 1, RETRY_DELAYS_MS.length - 1)] ?? 2000;
|
|
49
60
|
if (item.attempts >= MAX_ATTEMPTS) {
|
|
50
61
|
console.warn(`[Astrocode] Injection failed permanently after ${item.attempts} attempts: ${msg}`);
|
|
62
|
+
recordError("injection_failure", `Injection failed after ${item.attempts} attempts: ${msg}`);
|
|
51
63
|
item.reject(err);
|
|
52
64
|
continue;
|
|
53
65
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astrocode-workflow",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsc -p tsconfig.json",
|
|
15
15
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
16
|
-
"clean": "rm -rf dist"
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"test": "vitest",
|
|
18
|
+
"test:run": "vitest run"
|
|
17
19
|
},
|
|
18
20
|
"dependencies": {
|
|
19
21
|
"@opencode-ai/plugin": "^1.1.19",
|
|
20
22
|
"@opencode-ai/sdk": "^1.1.19",
|
|
21
|
-
"astrocode-workflow": "^0.1.51",
|
|
22
23
|
"jsonc-parser": "^3.2.0",
|
|
23
24
|
"zod": "4.1.8"
|
|
24
25
|
},
|
|
@@ -28,6 +29,7 @@
|
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@types/better-sqlite3": "^7.6.12",
|
|
30
31
|
"@types/node": "^20.12.12",
|
|
31
|
-
"typescript": "^5.6.3"
|
|
32
|
+
"typescript": "^5.6.3",
|
|
33
|
+
"vitest": "^1.6.0"
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/astro/workflow-runner.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Executes the workflow loop.
|
|
5
|
+
* Everything that mutates the repo (tool calls, steps) runs inside this scope.
|
|
6
|
+
*
|
|
7
|
+
* Replace the internals with your actual astro/opencode driver loop.
|
|
8
|
+
*/
|
|
9
|
+
export async function runAstroWorkflow(opts: {
|
|
10
|
+
proceedOneStep: () => Promise<{ done: boolean }>;
|
|
11
|
+
}): Promise<void> {
|
|
12
|
+
while (true) {
|
|
13
|
+
const { done } = await opts.proceedOneStep();
|
|
14
|
+
if (done) return;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/config/schema.ts
CHANGED
|
@@ -203,6 +203,7 @@ const InjectSchema = z
|
|
|
203
203
|
scope_allowlist: z.array(z.string()).default(["repo", "global"]),
|
|
204
204
|
type_allowlist: z.array(z.string()).default(["note", "policy"]),
|
|
205
205
|
max_per_turn: z.number().int().positive().default(5),
|
|
206
|
+
auto_approve_queued_stories: z.boolean().default(false).describe("Auto-approve highest priority queued story after workflow tools"),
|
|
206
207
|
})
|
|
207
208
|
.partial()
|
|
208
209
|
.default({});
|
|
@@ -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;
|
|
@@ -24,19 +29,30 @@ export function createInjectProvider(opts: {
|
|
|
24
29
|
const { db } = runtime;
|
|
25
30
|
|
|
26
31
|
// Cache to avoid re-injecting the same injects repeatedly
|
|
32
|
+
// Map of inject_id -> last injected timestamp
|
|
27
33
|
const injectedCache = new Map<string, number>();
|
|
28
34
|
|
|
29
35
|
function shouldSkipInject(injectId: string, nowMs: number): boolean {
|
|
30
36
|
const lastInjected = injectedCache.get(injectId);
|
|
31
37
|
if (!lastInjected) return false;
|
|
32
38
|
|
|
33
|
-
//
|
|
34
|
-
|
|
39
|
+
// REDUCED cooldown from 5 minutes to 1 minute
|
|
40
|
+
// This allows injects to appear more frequently during workflow
|
|
41
|
+
const cooldownMs = 1 * 60 * 1000;
|
|
35
42
|
return nowMs - lastInjected < cooldownMs;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
function markInjected(injectId: string, nowMs: number) {
|
|
39
46
|
injectedCache.set(injectId, nowMs);
|
|
47
|
+
|
|
48
|
+
// Clean up old entries to prevent memory leak
|
|
49
|
+
// Remove entries older than 10 minutes
|
|
50
|
+
const tenMinutesAgo = nowMs - (10 * 60 * 1000);
|
|
51
|
+
for (const [id, timestamp] of injectedCache.entries()) {
|
|
52
|
+
if (timestamp < tenMinutesAgo) {
|
|
53
|
+
injectedCache.delete(id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
40
56
|
}
|
|
41
57
|
|
|
42
58
|
function getInjectionDiagnostics(nowIso: string, scopeAllowlist: string[], typeAllowlist: string[]): any {
|
|
@@ -89,7 +105,7 @@ export function createInjectProvider(opts: {
|
|
|
89
105
|
};
|
|
90
106
|
}
|
|
91
107
|
|
|
92
|
-
async function injectEligibleInjects(sessionId: string) {
|
|
108
|
+
async function injectEligibleInjects(sessionId: string, context?: string) {
|
|
93
109
|
const now = nowISO();
|
|
94
110
|
const nowMs = Date.now();
|
|
95
111
|
|
|
@@ -115,7 +131,7 @@ export function createInjectProvider(opts: {
|
|
|
115
131
|
// Log when no injects are eligible
|
|
116
132
|
if (EMIT_TELEMETRY) {
|
|
117
133
|
// eslint-disable-next-line no-console
|
|
118
|
-
console.log(`[Astrocode:inject] ${now}
|
|
134
|
+
console.log(`[Astrocode:inject] ${now} context=${context ?? 'unknown'} selected=0 injected=0 skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:0}`);
|
|
119
135
|
}
|
|
120
136
|
return;
|
|
121
137
|
}
|
|
@@ -130,21 +146,68 @@ export function createInjectProvider(opts: {
|
|
|
130
146
|
// Format as injection message
|
|
131
147
|
const formattedText = `[Inject: ${inject.title}]\n\n${inject.body_md}`;
|
|
132
148
|
|
|
149
|
+
try {
|
|
150
|
+
await injectChatPrompt({
|
|
151
|
+
ctx,
|
|
152
|
+
sessionId,
|
|
153
|
+
text: formattedText,
|
|
154
|
+
agent: "Astrocode"
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
injected++;
|
|
158
|
+
markInjected(inject.inject_id, nowMs);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
// Log injection failures but don't crash
|
|
161
|
+
// eslint-disable-next-line no-console
|
|
162
|
+
console.error(`[Astrocode:inject] Failed to inject ${inject.inject_id}:`, err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Log diagnostic summary
|
|
167
|
+
if (EMIT_TELEMETRY || injected > 0) {
|
|
168
|
+
// eslint-disable-next-line no-console
|
|
169
|
+
console.log(`[Astrocode:inject] ${now} context=${context ?? 'unknown'} selected=${diagnostics.selected_eligible} injected=${injected} skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:${skippedDeduped}}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Workflow-related tools that should trigger inject + auto-approval
|
|
174
|
+
const WORKFLOW_TOOLS = new Set([
|
|
175
|
+
'astro_workflow_proceed',
|
|
176
|
+
'astro_story_queue',
|
|
177
|
+
'astro_story_approve',
|
|
178
|
+
'astro_stage_start',
|
|
179
|
+
'astro_stage_complete',
|
|
180
|
+
'astro_stage_fail',
|
|
181
|
+
'astro_run_abort',
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
// Auto-approve queued stories if enabled
|
|
185
|
+
async function maybeAutoApprove(sessionId: string) {
|
|
186
|
+
if (!config.inject?.auto_approve_queued_stories) return;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Get all queued stories
|
|
190
|
+
const queued = db.prepare("SELECT story_key, title FROM stories WHERE state='queued' ORDER BY priority DESC, created_at ASC").all() as Array<{ story_key: string; title: string }>;
|
|
191
|
+
|
|
192
|
+
if (queued.length === 0) return;
|
|
193
|
+
|
|
194
|
+
// Auto-approve the highest priority queued story
|
|
195
|
+
const story = queued[0];
|
|
196
|
+
db.prepare("UPDATE stories SET state='approved', updated_at=? WHERE story_key=?").run(nowISO(), story.story_key);
|
|
197
|
+
|
|
198
|
+
// eslint-disable-next-line no-console
|
|
199
|
+
console.log(`[Astrocode:inject] Auto-approved story ${story.story_key}: ${story.title}`);
|
|
200
|
+
|
|
201
|
+
// Inject a notification about the auto-approval
|
|
133
202
|
await injectChatPrompt({
|
|
134
203
|
ctx,
|
|
135
204
|
sessionId,
|
|
136
|
-
text:
|
|
205
|
+
text: `✅ Auto-approved story ${story.story_key}: ${story.title}`,
|
|
137
206
|
agent: "Astrocode"
|
|
138
207
|
});
|
|
139
|
-
|
|
140
|
-
injected++;
|
|
141
|
-
markInjected(inject.inject_id, nowMs);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Log diagnostic summary
|
|
145
|
-
if (EMIT_TELEMETRY) {
|
|
208
|
+
} catch (err) {
|
|
146
209
|
// eslint-disable-next-line no-console
|
|
147
|
-
console.
|
|
210
|
+
console.error(`[Astrocode:inject] Auto-approval failed:`, err);
|
|
148
211
|
}
|
|
149
212
|
}
|
|
150
213
|
|
|
@@ -154,7 +217,24 @@ export function createInjectProvider(opts: {
|
|
|
154
217
|
if (!config.inject?.enabled) return;
|
|
155
218
|
|
|
156
219
|
// Inject eligible injects before processing the user's message
|
|
157
|
-
await injectEligibleInjects(input.sessionID);
|
|
220
|
+
await injectEligibleInjects(input.sessionID, 'chat_message');
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
async onToolAfter(input: ToolExecuteAfterInput) {
|
|
224
|
+
if (!config.inject?.enabled) return;
|
|
225
|
+
|
|
226
|
+
// Only inject after workflow-related tools
|
|
227
|
+
if (!WORKFLOW_TOOLS.has(input.tool)) return;
|
|
228
|
+
|
|
229
|
+
// Extract sessionID (same pattern as continuation enforcer)
|
|
230
|
+
const sessionId = input.sessionID ?? (ctx as any).sessionID;
|
|
231
|
+
if (!sessionId) return;
|
|
232
|
+
|
|
233
|
+
// Auto-approve queued stories if enabled
|
|
234
|
+
await maybeAutoApprove(sessionId);
|
|
235
|
+
|
|
236
|
+
// Inject eligible injects after workflow tool execution
|
|
237
|
+
await injectEligibleInjects(sessionId, `tool_after:${input.tool}`);
|
|
158
238
|
},
|
|
159
239
|
};
|
|
160
240
|
}
|