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.
Files changed (144) hide show
  1. package/dist/index.js +6 -0
  2. package/dist/shared/metrics.d.ts +66 -0
  3. package/dist/shared/metrics.js +112 -0
  4. package/dist/src/agents/commands.d.ts +9 -0
  5. package/dist/src/agents/commands.js +121 -0
  6. package/dist/src/agents/prompts.d.ts +3 -0
  7. package/dist/src/agents/prompts.js +232 -0
  8. package/dist/src/agents/registry.d.ts +6 -0
  9. package/dist/src/agents/registry.js +242 -0
  10. package/dist/src/agents/types.d.ts +14 -0
  11. package/dist/src/agents/types.js +8 -0
  12. package/dist/src/astro/workflow-runner.d.ts +11 -0
  13. package/dist/src/astro/workflow-runner.js +14 -0
  14. package/dist/src/config/config-handler.d.ts +4 -0
  15. package/dist/src/config/config-handler.js +46 -0
  16. package/dist/src/config/defaults.d.ts +3 -0
  17. package/dist/src/config/defaults.js +3 -0
  18. package/dist/src/config/loader.d.ts +11 -0
  19. package/dist/src/config/loader.js +82 -0
  20. package/dist/src/config/schema.d.ts +195 -0
  21. package/dist/src/config/schema.js +224 -0
  22. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  23. package/dist/src/hooks/continuation-enforcer.js +190 -0
  24. package/dist/src/hooks/inject-provider.d.ts +27 -0
  25. package/dist/src/hooks/inject-provider.js +189 -0
  26. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  27. package/dist/src/hooks/tool-output-truncator.js +57 -0
  28. package/dist/src/index.d.ts +3 -0
  29. package/dist/src/index.js +307 -0
  30. package/dist/src/shared/deep-merge.d.ts +8 -0
  31. package/dist/src/shared/deep-merge.js +25 -0
  32. package/dist/src/shared/hash.d.ts +1 -0
  33. package/dist/src/shared/hash.js +4 -0
  34. package/dist/src/shared/log.d.ts +7 -0
  35. package/dist/src/shared/log.js +24 -0
  36. package/dist/src/shared/metrics.d.ts +66 -0
  37. package/dist/src/shared/metrics.js +112 -0
  38. package/dist/src/shared/model-tuning.d.ts +9 -0
  39. package/dist/src/shared/model-tuning.js +28 -0
  40. package/dist/src/shared/paths.d.ts +19 -0
  41. package/dist/src/shared/paths.js +64 -0
  42. package/dist/src/shared/text.d.ts +4 -0
  43. package/dist/src/shared/text.js +19 -0
  44. package/dist/src/shared/time.d.ts +1 -0
  45. package/dist/src/shared/time.js +3 -0
  46. package/dist/src/state/adapters/index.d.ts +41 -0
  47. package/dist/src/state/adapters/index.js +115 -0
  48. package/dist/src/state/db.d.ts +16 -0
  49. package/dist/src/state/db.js +225 -0
  50. package/dist/src/state/ids.d.ts +8 -0
  51. package/dist/src/state/ids.js +25 -0
  52. package/dist/src/state/repo-lock.d.ts +67 -0
  53. package/dist/src/state/repo-lock.js +580 -0
  54. package/dist/src/state/schema.d.ts +2 -0
  55. package/dist/src/state/schema.js +258 -0
  56. package/dist/src/state/types.d.ts +71 -0
  57. package/dist/src/state/types.js +1 -0
  58. package/dist/src/state/workflow-repo-lock.d.ts +23 -0
  59. package/dist/src/state/workflow-repo-lock.js +83 -0
  60. package/dist/src/tools/artifacts.d.ts +18 -0
  61. package/dist/src/tools/artifacts.js +71 -0
  62. package/dist/src/tools/health.d.ts +8 -0
  63. package/dist/src/tools/health.js +88 -0
  64. package/dist/src/tools/index.d.ts +20 -0
  65. package/dist/src/tools/index.js +94 -0
  66. package/dist/src/tools/init.d.ts +17 -0
  67. package/dist/src/tools/init.js +96 -0
  68. package/dist/src/tools/injects.d.ts +53 -0
  69. package/dist/src/tools/injects.js +325 -0
  70. package/dist/src/tools/lock.d.ts +4 -0
  71. package/dist/src/tools/lock.js +78 -0
  72. package/dist/src/tools/metrics.d.ts +7 -0
  73. package/dist/src/tools/metrics.js +61 -0
  74. package/dist/src/tools/repair.d.ts +8 -0
  75. package/dist/src/tools/repair.js +26 -0
  76. package/dist/src/tools/reset.d.ts +8 -0
  77. package/dist/src/tools/reset.js +92 -0
  78. package/dist/src/tools/run.d.ts +13 -0
  79. package/dist/src/tools/run.js +54 -0
  80. package/dist/src/tools/spec.d.ts +12 -0
  81. package/dist/src/tools/spec.js +44 -0
  82. package/dist/src/tools/stage.d.ts +23 -0
  83. package/dist/src/tools/stage.js +371 -0
  84. package/dist/src/tools/status.d.ts +8 -0
  85. package/dist/src/tools/status.js +125 -0
  86. package/dist/src/tools/story.d.ts +23 -0
  87. package/dist/src/tools/story.js +85 -0
  88. package/dist/src/tools/workflow.d.ts +13 -0
  89. package/dist/src/tools/workflow.js +345 -0
  90. package/dist/src/ui/inject.d.ts +12 -0
  91. package/dist/src/ui/inject.js +107 -0
  92. package/dist/src/ui/toasts.d.ts +13 -0
  93. package/dist/src/ui/toasts.js +39 -0
  94. package/dist/src/workflow/artifacts.d.ts +24 -0
  95. package/dist/src/workflow/artifacts.js +45 -0
  96. package/dist/src/workflow/baton.d.ts +72 -0
  97. package/dist/src/workflow/baton.js +166 -0
  98. package/dist/src/workflow/context.d.ts +20 -0
  99. package/dist/src/workflow/context.js +113 -0
  100. package/dist/src/workflow/directives.d.ts +39 -0
  101. package/dist/src/workflow/directives.js +137 -0
  102. package/dist/src/workflow/repair.d.ts +8 -0
  103. package/dist/src/workflow/repair.js +99 -0
  104. package/dist/src/workflow/state-machine.d.ts +86 -0
  105. package/dist/src/workflow/state-machine.js +216 -0
  106. package/dist/src/workflow/story-helpers.d.ts +9 -0
  107. package/dist/src/workflow/story-helpers.js +13 -0
  108. package/dist/state/db.d.ts +1 -0
  109. package/dist/state/db.js +9 -0
  110. package/dist/state/repo-lock.d.ts +3 -0
  111. package/dist/state/repo-lock.js +29 -0
  112. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  113. package/dist/test/integration/db-transactions.test.js +126 -0
  114. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  115. package/dist/test/integration/injection-metrics.test.js +129 -0
  116. package/dist/tools/health.d.ts +8 -0
  117. package/dist/tools/health.js +119 -0
  118. package/dist/tools/index.js +9 -0
  119. package/dist/tools/metrics.d.ts +7 -0
  120. package/dist/tools/metrics.js +61 -0
  121. package/dist/tools/reset.d.ts +8 -0
  122. package/dist/tools/reset.js +92 -0
  123. package/dist/tools/workflow.js +178 -168
  124. package/dist/ui/inject.js +21 -9
  125. package/package.json +6 -4
  126. package/src/astro/workflow-runner.ts +16 -0
  127. package/src/config/schema.ts +1 -0
  128. package/src/hooks/inject-provider.ts +94 -14
  129. package/src/index.ts +7 -0
  130. package/src/shared/metrics.ts +148 -0
  131. package/src/state/db.ts +10 -1
  132. package/src/state/schema.ts +8 -1
  133. package/src/tools/health.ts +99 -0
  134. package/src/tools/index.ts +12 -3
  135. package/src/tools/init.ts +7 -6
  136. package/src/tools/metrics.ts +71 -0
  137. package/src/tools/repair.ts +8 -4
  138. package/src/tools/reset.ts +100 -0
  139. package/src/tools/stage.ts +1 -0
  140. package/src/tools/status.ts +2 -1
  141. package/src/tools/story.ts +1 -0
  142. package/src/tools/workflow.ts +2 -0
  143. package/src/ui/inject.ts +21 -9
  144. package/src/workflow/repair.ts +2 -2
@@ -0,0 +1,345 @@
1
+ // src/tools/workflow.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import { withTx } from "../state/db";
4
+ import { buildContextSnapshot } from "../workflow/context";
5
+ import { decideNextAction, createRunForStory, startStage, completeRun, failRun, getActiveRun, EVENT_TYPES, } from "../workflow/state-machine";
6
+ import { buildStageDirective, directiveHash } from "../workflow/directives";
7
+ import { injectChatPrompt } from "../ui/inject";
8
+ import { nowISO } from "../shared/time";
9
+ import { newEventId } from "../state/ids";
10
+ import { debug } from "../shared/log";
11
+ import { createToastManager } from "../ui/toasts";
12
+ // Agent name mapping for case-sensitive resolution
13
+ export const STAGE_TO_AGENT_MAP = {
14
+ frame: "Frame",
15
+ plan: "Plan",
16
+ spec: "Spec",
17
+ implement: "Implement",
18
+ review: "Review",
19
+ verify: "Verify",
20
+ close: "Close"
21
+ };
22
+ export function resolveAgentName(stageKey, config, agents, warnings) {
23
+ // Use configurable agent names from config, fallback to hardcoded map, then General
24
+ const agentNames = config.agents?.stage_agent_names;
25
+ let candidate;
26
+ if (agentNames && agentNames[stageKey]) {
27
+ candidate = agentNames[stageKey];
28
+ }
29
+ else {
30
+ candidate = STAGE_TO_AGENT_MAP[stageKey] || "General";
31
+ }
32
+ // Validate that the agent actually exists in the registry
33
+ if (agents && !agents[candidate]) {
34
+ const warning = `Agent "${candidate}" not found in registry for stage "${stageKey}". Falling back to General.`;
35
+ if (warnings)
36
+ warnings.push(warning);
37
+ else
38
+ console.warn(`[Astrocode] ${warning}`);
39
+ candidate = "General";
40
+ }
41
+ // Final guard: ensure General exists, fallback to built-in "general" if not
42
+ if (agents && !agents[candidate]) {
43
+ const finalWarning = `Critical: General agent not found in registry. Falling back to built-in "general".`;
44
+ if (warnings)
45
+ warnings.push(finalWarning);
46
+ else
47
+ console.warn(`[Astrocode] ${finalWarning}`);
48
+ return "general";
49
+ }
50
+ return candidate;
51
+ }
52
+ function stageGoal(stage, cfg) {
53
+ switch (stage) {
54
+ case "frame":
55
+ return "Define scope, constraints, and an unambiguous Definition of Done.";
56
+ case "plan":
57
+ return `Create 50-200 detailed implementation stories, each focused on a specific, implementable task. Break down every component into separate stories with clear acceptance criteria.`;
58
+ case "spec":
59
+ return "Produce minimal spec/contract: interfaces, invariants, acceptance checks.";
60
+ case "implement":
61
+ return "Implement the spec with minimal changes, referencing diffs and evidence as artifacts.";
62
+ case "review":
63
+ return "Review implementation for correctness, risks, and alignment with spec.";
64
+ case "verify":
65
+ return "Run verification commands and produce evidence artifacts.";
66
+ case "close":
67
+ return "Summarize outcome and confirm acceptance criteria, leaving clear breadcrumbs.";
68
+ }
69
+ }
70
+ function stageConstraints(stage, cfg) {
71
+ const common = [
72
+ "Do not narrate prompts.",
73
+ "Keep baton markdown short and structured.",
74
+ "If blocked: ask exactly ONE question and stop.",
75
+ ];
76
+ if (stage === "plan") {
77
+ common.push(`Create 50-200 stories; each story must be implementable in 2-8 hours with clear acceptance criteria.`);
78
+ }
79
+ if (stage === "verify" && cfg.workflow.evidence_required.verify) {
80
+ common.push("Evidence required: ASTRO JSON must include evidence[] paths.");
81
+ }
82
+ return common;
83
+ }
84
+ function buildDelegationPrompt(opts) {
85
+ const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
86
+ const stageUpper = stage_key.toUpperCase();
87
+ const prompt = [
88
+ `[SYSTEM DIRECTIVE: ASTROCODE — DELEGATE_STAGE_${stageUpper}]`,
89
+ ``,
90
+ `Do this now, in order:`,
91
+ `1) Call the **task** tool to delegate to subagent \`${stage_agent_name}\`.`,
92
+ ` - Pass this exact prompt text to the subagent (copy/paste):`,
93
+ ``,
94
+ stageDirective,
95
+ ``,
96
+ `2) When the subagent returns, immediately call **astro_stage_complete** with:`,
97
+ ` - run_id = "${run_id}"`,
98
+ ` - stage_key = "${stage_key}"`,
99
+ ` - output_text = (the FULL subagent response text)`,
100
+ ``,
101
+ `3) Then call **astro_workflow_proceed** again (mode=step).`,
102
+ ``,
103
+ `Important: do NOT do any stage work yourself in orchestrator mode.`,
104
+ ].join("\n").trim();
105
+ debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
106
+ return prompt;
107
+ }
108
+ function buildUiMessage(e) {
109
+ switch (e.kind) {
110
+ case "stage_started": {
111
+ const agent = e.agent_name ? ` (${e.agent_name})` : "";
112
+ const title = "Astrocode";
113
+ const message = `Stage started: ${e.stage_key}${agent}`;
114
+ const chatText = [
115
+ `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
116
+ ``,
117
+ `Run: ${e.run_id}`,
118
+ `Stage: ${e.stage_key}${agent}`,
119
+ ].join("\n");
120
+ return { title, message, variant: "info", chatText };
121
+ }
122
+ case "run_completed": {
123
+ const title = "Astrocode";
124
+ const message = `Run completed: ${e.run_id}`;
125
+ const chatText = [
126
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
127
+ ``,
128
+ `Run: ${e.run_id}`,
129
+ `Story: ${e.story_key}`,
130
+ ].join("\n");
131
+ return { title, message, variant: "success", chatText };
132
+ }
133
+ case "run_failed": {
134
+ const title = "Astrocode";
135
+ const message = `Run failed: ${e.run_id} (${e.stage_key})`;
136
+ const chatText = [
137
+ `[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
138
+ ``,
139
+ `Run: ${e.run_id}`,
140
+ `Story: ${e.story_key}`,
141
+ `Stage: ${e.stage_key}`,
142
+ `Error: ${e.error_text}`,
143
+ ].join("\n");
144
+ return { title, message, variant: "error", chatText };
145
+ }
146
+ }
147
+ }
148
+ export function createAstroWorkflowProceedTool(opts) {
149
+ const { ctx, config, db, agents } = opts;
150
+ const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
151
+ return tool({
152
+ description: "Deterministic harness: advances the DB-driven pipeline by one step (or loops bounded). Stops when LLM work is required (delegation/await).",
153
+ args: {
154
+ mode: tool.schema.enum(["step", "loop"]).default(config.workflow.default_mode),
155
+ max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
156
+ },
157
+ execute: async ({ mode, max_steps }) => {
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")
177
+ break;
178
+ continue;
179
+ }
180
+ if (next.kind === "complete_run") {
181
+ withTx(db, () => completeRun(db, next.run_id, emit));
182
+ actions.push(`completed run ${next.run_id}`);
183
+ if (mode === "step")
184
+ break;
185
+ continue;
186
+ }
187
+ if (next.kind === "failed") {
188
+ // Ensure DB state reflects failure in one tx; emit UI event.
189
+ withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
190
+ actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
191
+ if (mode === "step")
192
+ break;
193
+ continue;
194
+ }
195
+ if (next.kind === "delegate_stage") {
196
+ const active = getActiveRun(db);
197
+ if (!active)
198
+ throw new Error("Invariant: delegate_stage but no active run.");
199
+ const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
200
+ const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
201
+ let agentName = resolveAgentName(next.stage_key, config, agents, warnings);
202
+ const agentExists = (name) => {
203
+ if (agents && agents[name])
204
+ return true;
205
+ const knownStageAgents = ["Frame", "Plan", "Spec", "Implement", "Review", "Verify", "Close", "General", "Astro", "general"];
206
+ if (knownStageAgents.includes(name))
207
+ return true;
208
+ return false;
209
+ };
210
+ if (!agentExists(agentName)) {
211
+ const originalAgent = agentName;
212
+ console.warn(`[Astrocode] Agent ${agentName} not found. Falling back to orchestrator.`);
213
+ agentName = config.agents?.orchestrator_name || "Astro";
214
+ if (!agentExists(agentName)) {
215
+ console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
216
+ agentName = "General";
217
+ if (!agentExists(agentName)) {
218
+ throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
219
+ }
220
+ }
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) {
278
+ const context = buildContextSnapshot({
279
+ db,
280
+ config,
281
+ run_id: next.run_id,
282
+ next_action: `complete stage ${next.stage_key}`,
283
+ });
284
+ const prompt = [
285
+ `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
286
+ ``,
287
+ `Run \`${next.run_id}\` is waiting for stage \`${next.stage_key}\` output.`,
288
+ `If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
289
+ ``,
290
+ `Context snapshot:`,
291
+ context,
292
+ ].join("\n").trim();
293
+ const h = directiveHash(prompt);
294
+ const now = nowISO();
295
+ db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)").run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
296
+ await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
297
+ }
298
+ break;
299
+ }
300
+ actions.push(`unhandled next action: ${next.kind}`);
301
+ break;
302
+ }
303
+ // Flush UI events (toast + prompt) AFTER state transitions
304
+ if (uiEvents.length > 0) {
305
+ for (const e of uiEvents) {
306
+ const msg = buildUiMessage(e);
307
+ if (config.ui.toasts.enabled) {
308
+ await toasts.show({
309
+ title: msg.title,
310
+ message: msg.message,
311
+ variant: msg.variant,
312
+ });
313
+ }
314
+ if (ctx?.sessionID) {
315
+ await injectChatPrompt({
316
+ ctx,
317
+ sessionId: ctx.sessionID,
318
+ text: msg.chatText,
319
+ agent: "Astro",
320
+ });
321
+ }
322
+ }
323
+ actions.push(`ui: flushed ${uiEvents.length} event(s)`);
324
+ }
325
+ // Housekeeping event
326
+ db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)").run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
327
+ const active = getActiveRun(db);
328
+ const lines = [];
329
+ lines.push(`# astro_workflow_proceed`);
330
+ lines.push(`- mode: ${mode}`);
331
+ lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
332
+ if (active)
333
+ lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
334
+ lines.push(``, `## Actions`);
335
+ for (const a of actions)
336
+ lines.push(`- ${a}`);
337
+ if (warnings.length > 0) {
338
+ lines.push(``, `## Warnings`);
339
+ for (const w of warnings)
340
+ lines.push(`⚠️ ${w}`);
341
+ }
342
+ return lines.join("\n").trim();
343
+ },
344
+ });
345
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Inject a visible prompt into the conversation.
3
+ * - Deterministic ordering per session
4
+ * - Correct SDK binding (prevents `this._client` undefined)
5
+ * - Awaitable: resolves when delivered, rejects after max retries
6
+ */
7
+ export declare function injectChatPrompt(opts: {
8
+ ctx: any;
9
+ sessionId?: string;
10
+ text: string;
11
+ agent?: string;
12
+ }): Promise<void>;
@@ -0,0 +1,107 @@
1
+ // src/ui/inject.ts
2
+ import { recordInjection, recordError } from "../shared/metrics";
3
+ const MAX_ATTEMPTS = 4;
4
+ const RETRY_DELAYS_MS = [250, 500, 1000, 2000]; // attempt 1..4
5
+ // Per-session queues so one stuck session doesn't block others
6
+ const queues = new Map();
7
+ const running = new Set();
8
+ function sleep(ms) {
9
+ return new Promise((r) => setTimeout(r, ms));
10
+ }
11
+ function getPromptInvoker(ctx) {
12
+ const session = ctx?.client?.session;
13
+ const prompt = session?.prompt;
14
+ if (!session || typeof prompt !== "function") {
15
+ throw new Error("API not available (ctx.client.session.prompt)");
16
+ }
17
+ return { session, prompt };
18
+ }
19
+ async function tryInjectOnce(item) {
20
+ const { ctx, sessionId, text, agent = "Astro" } = item;
21
+ const prefixedText = `[${agent}]\n\n${text}`;
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
+ }
40
+ }
41
+ async function runSessionQueue(sessionId) {
42
+ if (running.has(sessionId))
43
+ return;
44
+ running.add(sessionId);
45
+ try {
46
+ // eslint-disable-next-line no-constant-condition
47
+ while (true) {
48
+ const q = queues.get(sessionId);
49
+ if (!q || q.length === 0)
50
+ break;
51
+ const item = q.shift();
52
+ try {
53
+ await tryInjectOnce(item);
54
+ item.resolve();
55
+ }
56
+ catch (err) {
57
+ item.attempts += 1;
58
+ const msg = err instanceof Error ? err.message : String(err);
59
+ const delay = RETRY_DELAYS_MS[Math.min(item.attempts - 1, RETRY_DELAYS_MS.length - 1)] ?? 2000;
60
+ if (item.attempts >= MAX_ATTEMPTS) {
61
+ console.warn(`[Astrocode] Injection failed permanently after ${item.attempts} attempts: ${msg}`);
62
+ recordError("injection_failure", `Injection failed after ${item.attempts} attempts: ${msg}`);
63
+ item.reject(err);
64
+ continue;
65
+ }
66
+ console.warn(`[Astrocode] Injection attempt ${item.attempts}/${MAX_ATTEMPTS} failed: ${msg}; retrying in ${delay}ms`);
67
+ await sleep(delay);
68
+ // Requeue at front to preserve order (and avoid starving later messages)
69
+ const q2 = queues.get(sessionId) ?? [];
70
+ q2.unshift(item);
71
+ queues.set(sessionId, q2);
72
+ }
73
+ }
74
+ }
75
+ finally {
76
+ running.delete(sessionId);
77
+ }
78
+ }
79
+ /**
80
+ * Inject a visible prompt into the conversation.
81
+ * - Deterministic ordering per session
82
+ * - Correct SDK binding (prevents `this._client` undefined)
83
+ * - Awaitable: resolves when delivered, rejects after max retries
84
+ */
85
+ export async function injectChatPrompt(opts) {
86
+ const sessionId = opts.sessionId ?? opts.ctx?.sessionID;
87
+ if (!sessionId) {
88
+ console.warn("[Astrocode] Skipping injection: No sessionId provided");
89
+ return;
90
+ }
91
+ return new Promise((resolve, reject) => {
92
+ const item = {
93
+ ctx: opts.ctx,
94
+ sessionId,
95
+ text: opts.text,
96
+ agent: opts.agent,
97
+ attempts: 0,
98
+ resolve,
99
+ reject,
100
+ };
101
+ const q = queues.get(sessionId) ?? [];
102
+ q.push(item);
103
+ queues.set(sessionId, q);
104
+ // Fire worker (don't await here; caller awaits the returned Promise)
105
+ void runSessionQueue(sessionId);
106
+ });
107
+ }
@@ -0,0 +1,13 @@
1
+ export type ToastVariant = "info" | "success" | "warning" | "error";
2
+ export type ToastOptions = {
3
+ title: string;
4
+ message: string;
5
+ variant?: ToastVariant;
6
+ durationMs?: number;
7
+ };
8
+ export declare function createToastManager(opts: {
9
+ ctx: any;
10
+ throttleMs: number;
11
+ }): {
12
+ show: (toast: ToastOptions) => Promise<void>;
13
+ };
@@ -0,0 +1,39 @@
1
+ export function createToastManager(opts) {
2
+ const { ctx, throttleMs } = opts;
3
+ let lastAt = 0;
4
+ async function show(toast) {
5
+ const now = Date.now();
6
+ if (now - lastAt < throttleMs)
7
+ return;
8
+ lastAt = now;
9
+ const tui = ctx?.client?.tui;
10
+ if (tui?.showToast) {
11
+ try {
12
+ await tui.showToast({
13
+ title: toast.title,
14
+ message: toast.message,
15
+ variant: toast.variant ?? "info",
16
+ durationMs: toast.durationMs ?? 2500,
17
+ });
18
+ return;
19
+ }
20
+ catch {
21
+ // fall through
22
+ }
23
+ }
24
+ // Fallback: visible chat prompt
25
+ try {
26
+ const sessionId = ctx?.sessionID;
27
+ if (!sessionId)
28
+ return;
29
+ await ctx.client.session.prompt({
30
+ path: { id: sessionId },
31
+ body: { parts: [{ type: "text", text: `[ASTRO TOAST] ${toast.title}: ${toast.message}` }] },
32
+ });
33
+ }
34
+ catch {
35
+ // ignore
36
+ }
37
+ }
38
+ return { show };
39
+ }
@@ -0,0 +1,24 @@
1
+ import type { SqliteDb } from "../state/db";
2
+ export type ArtifactType = "baton" | "summary" | "evidence" | "diff" | "log" | "commit" | "tool_output" | "snapshot" | "spec";
3
+ export type PutArtifactOpts = {
4
+ repoRoot: string;
5
+ db: SqliteDb;
6
+ run_id?: string | null;
7
+ stage_key?: string | null;
8
+ type: ArtifactType | string;
9
+ rel_path: string;
10
+ content: string | Buffer;
11
+ meta?: Record<string, unknown>;
12
+ };
13
+ export declare function writeFileSafe(repoRoot: string, relPath: string, content: string | Buffer): void;
14
+ export declare function putArtifact(opts: PutArtifactOpts): {
15
+ artifact_id: string;
16
+ sha256: string;
17
+ abs_path: string;
18
+ };
19
+ export declare function listArtifacts(db: SqliteDb, filters?: {
20
+ run_id?: string;
21
+ stage_key?: string;
22
+ type?: string;
23
+ }): any[];
24
+ export declare function getArtifact(db: SqliteDb, artifact_id: string): any | null;
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { newArtifactId } from "../state/ids";
4
+ import { nowISO } from "../shared/time";
5
+ import { sha256Hex } from "../shared/hash";
6
+ import { assertInsideAstro, ensureDir, toPosix } from "../shared/paths";
7
+ export function writeFileSafe(repoRoot, relPath, content) {
8
+ const abs = path.join(repoRoot, relPath);
9
+ assertInsideAstro(repoRoot, abs);
10
+ ensureDir(path.dirname(abs));
11
+ fs.writeFileSync(abs, content);
12
+ }
13
+ export function putArtifact(opts) {
14
+ const { repoRoot, db, run_id = null, stage_key = null, type, rel_path, content } = opts;
15
+ const artifact_id = newArtifactId();
16
+ const abs_path = path.join(repoRoot, rel_path);
17
+ writeFileSafe(repoRoot, rel_path, content);
18
+ const sha256 = sha256Hex(content instanceof Buffer ? content : Buffer.from(content, "utf-8"));
19
+ const created_at = nowISO();
20
+ const meta_json = JSON.stringify(opts.meta ?? {});
21
+ db.prepare("INSERT INTO artifacts (artifact_id, run_id, stage_key, type, path, sha256, meta_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(artifact_id, run_id, stage_key, type, toPosix(rel_path), sha256, meta_json, created_at);
22
+ return { artifact_id, sha256, abs_path };
23
+ }
24
+ export function listArtifacts(db, filters) {
25
+ const where = [];
26
+ const params = [];
27
+ if (filters?.run_id) {
28
+ where.push("run_id = ?");
29
+ params.push(filters.run_id);
30
+ }
31
+ if (filters?.stage_key) {
32
+ where.push("stage_key = ?");
33
+ params.push(filters.stage_key);
34
+ }
35
+ if (filters?.type) {
36
+ where.push("type = ?");
37
+ params.push(filters.type);
38
+ }
39
+ const sql = `SELECT * FROM artifacts ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY created_at DESC LIMIT 200`;
40
+ return db.prepare(sql).all(...params);
41
+ }
42
+ export function getArtifact(db, artifact_id) {
43
+ const row = db.prepare("SELECT * FROM artifacts WHERE artifact_id = ?").get(artifact_id);
44
+ return row ?? null;
45
+ }
@@ -0,0 +1,72 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import { z } from "zod";
3
+ export declare const ASTRO_JSON_BEGIN = "<!-- ASTRO_JSON_BEGIN -->";
4
+ export declare const ASTRO_JSON_END = "<!-- ASTRO_JSON_END -->";
5
+ export declare const StageKeySchema: z.ZodEnum<{
6
+ frame: "frame";
7
+ plan: "plan";
8
+ spec: "spec";
9
+ implement: "implement";
10
+ review: "review";
11
+ verify: "verify";
12
+ close: "close";
13
+ }>;
14
+ export declare const AstroJsonSchema: z.ZodObject<{
15
+ schema_version: z.ZodDefault<z.ZodNumber>;
16
+ run_id: z.ZodOptional<z.ZodString>;
17
+ story_key: z.ZodOptional<z.ZodString>;
18
+ stage_key: z.ZodEnum<{
19
+ frame: "frame";
20
+ plan: "plan";
21
+ spec: "spec";
22
+ implement: "implement";
23
+ review: "review";
24
+ verify: "verify";
25
+ close: "close";
26
+ }>;
27
+ status: z.ZodDefault<z.ZodEnum<{
28
+ blocked: "blocked";
29
+ failed: "failed";
30
+ ok: "ok";
31
+ }>>;
32
+ summary: z.ZodDefault<z.ZodString>;
33
+ decisions: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
+ next_actions: z.ZodDefault<z.ZodArray<z.ZodString>>;
35
+ tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
36
+ title: z.ZodString;
37
+ description: z.ZodOptional<z.ZodString>;
38
+ complexity: z.ZodOptional<z.ZodNumber>;
39
+ subtasks: z.ZodOptional<z.ZodArray<z.ZodString>>;
40
+ }, z.core.$strip>>>;
41
+ files: z.ZodDefault<z.ZodArray<z.ZodObject<{
42
+ path: z.ZodString;
43
+ kind: z.ZodDefault<z.ZodString>;
44
+ notes: z.ZodOptional<z.ZodString>;
45
+ }, z.core.$strip>>>;
46
+ evidence: z.ZodDefault<z.ZodArray<z.ZodObject<{
47
+ path: z.ZodString;
48
+ kind: z.ZodDefault<z.ZodString>;
49
+ notes: z.ZodOptional<z.ZodString>;
50
+ }, z.core.$strip>>>;
51
+ new_stories: z.ZodDefault<z.ZodArray<z.ZodObject<{
52
+ title: z.ZodString;
53
+ body_md: z.ZodOptional<z.ZodString>;
54
+ priority: z.ZodOptional<z.ZodNumber>;
55
+ }, z.core.$strip>>>;
56
+ questions: z.ZodDefault<z.ZodArray<z.ZodString>>;
57
+ metrics: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
58
+ }, z.core.$strip>;
59
+ export type AstroJson = z.infer<typeof AstroJsonSchema>;
60
+ export type ParsedStageOutput = {
61
+ baton_md: string;
62
+ astro_json: AstroJson | null;
63
+ astro_json_raw: string | null;
64
+ error: string | null;
65
+ };
66
+ export declare function parseStageOutputText(text: string): ParsedStageOutput;
67
+ export declare function buildBatonSummary(opts: {
68
+ config: AstrocodeConfig;
69
+ stage_key: string;
70
+ astro_json: AstroJson | null;
71
+ baton_md: string;
72
+ }): string;