astrocode-workflow 0.0.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.
Files changed (133) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +85 -0
  3. package/dist/agents/commands.d.ts +9 -0
  4. package/dist/agents/commands.js +121 -0
  5. package/dist/agents/prompts.d.ts +2 -0
  6. package/dist/agents/prompts.js +27 -0
  7. package/dist/agents/registry.d.ts +6 -0
  8. package/dist/agents/registry.js +223 -0
  9. package/dist/agents/types.d.ts +14 -0
  10. package/dist/agents/types.js +8 -0
  11. package/dist/config/config-handler.d.ts +4 -0
  12. package/dist/config/config-handler.js +46 -0
  13. package/dist/config/defaults.d.ts +3 -0
  14. package/dist/config/defaults.js +3 -0
  15. package/dist/config/loader.d.ts +11 -0
  16. package/dist/config/loader.js +48 -0
  17. package/dist/config/schema.d.ts +176 -0
  18. package/dist/config/schema.js +198 -0
  19. package/dist/hooks/continuation-enforcer.d.ts +26 -0
  20. package/dist/hooks/continuation-enforcer.js +166 -0
  21. package/dist/hooks/tool-output-truncator.d.ts +17 -0
  22. package/dist/hooks/tool-output-truncator.js +56 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +108 -0
  25. package/dist/shared/deep-merge.d.ts +8 -0
  26. package/dist/shared/deep-merge.js +25 -0
  27. package/dist/shared/hash.d.ts +1 -0
  28. package/dist/shared/hash.js +4 -0
  29. package/dist/shared/log.d.ts +7 -0
  30. package/dist/shared/log.js +24 -0
  31. package/dist/shared/model-tuning.d.ts +9 -0
  32. package/dist/shared/model-tuning.js +28 -0
  33. package/dist/shared/paths.d.ts +19 -0
  34. package/dist/shared/paths.js +51 -0
  35. package/dist/shared/text.d.ts +4 -0
  36. package/dist/shared/text.js +19 -0
  37. package/dist/shared/time.d.ts +1 -0
  38. package/dist/shared/time.js +3 -0
  39. package/dist/state/adapters/index.d.ts +39 -0
  40. package/dist/state/adapters/index.js +119 -0
  41. package/dist/state/db.d.ts +17 -0
  42. package/dist/state/db.js +83 -0
  43. package/dist/state/ids.d.ts +8 -0
  44. package/dist/state/ids.js +25 -0
  45. package/dist/state/schema.d.ts +2 -0
  46. package/dist/state/schema.js +247 -0
  47. package/dist/state/types.d.ts +70 -0
  48. package/dist/state/types.js +1 -0
  49. package/dist/tools/artifacts.d.ts +18 -0
  50. package/dist/tools/artifacts.js +71 -0
  51. package/dist/tools/index.d.ts +8 -0
  52. package/dist/tools/index.js +100 -0
  53. package/dist/tools/init.d.ts +8 -0
  54. package/dist/tools/init.js +41 -0
  55. package/dist/tools/injects.d.ts +23 -0
  56. package/dist/tools/injects.js +99 -0
  57. package/dist/tools/repair.d.ts +8 -0
  58. package/dist/tools/repair.js +25 -0
  59. package/dist/tools/run.d.ts +13 -0
  60. package/dist/tools/run.js +54 -0
  61. package/dist/tools/spec.d.ts +13 -0
  62. package/dist/tools/spec.js +41 -0
  63. package/dist/tools/stage.d.ts +23 -0
  64. package/dist/tools/stage.js +284 -0
  65. package/dist/tools/status.d.ts +8 -0
  66. package/dist/tools/status.js +107 -0
  67. package/dist/tools/story.d.ts +23 -0
  68. package/dist/tools/story.js +85 -0
  69. package/dist/tools/workflow.d.ts +8 -0
  70. package/dist/tools/workflow.js +197 -0
  71. package/dist/ui/inject.d.ts +5 -0
  72. package/dist/ui/inject.js +9 -0
  73. package/dist/ui/toasts.d.ts +13 -0
  74. package/dist/ui/toasts.js +39 -0
  75. package/dist/workflow/artifacts.d.ts +24 -0
  76. package/dist/workflow/artifacts.js +45 -0
  77. package/dist/workflow/baton.d.ts +66 -0
  78. package/dist/workflow/baton.js +101 -0
  79. package/dist/workflow/context.d.ts +12 -0
  80. package/dist/workflow/context.js +67 -0
  81. package/dist/workflow/directives.d.ts +37 -0
  82. package/dist/workflow/directives.js +111 -0
  83. package/dist/workflow/repair.d.ts +8 -0
  84. package/dist/workflow/repair.js +99 -0
  85. package/dist/workflow/state-machine.d.ts +43 -0
  86. package/dist/workflow/state-machine.js +127 -0
  87. package/dist/workflow/story-helpers.d.ts +9 -0
  88. package/dist/workflow/story-helpers.js +13 -0
  89. package/package.json +32 -0
  90. package/src/agents/commands.ts +137 -0
  91. package/src/agents/prompts.ts +28 -0
  92. package/src/agents/registry.ts +310 -0
  93. package/src/agents/types.ts +31 -0
  94. package/src/config/config-handler.ts +48 -0
  95. package/src/config/defaults.ts +4 -0
  96. package/src/config/loader.ts +55 -0
  97. package/src/config/schema.ts +236 -0
  98. package/src/hooks/continuation-enforcer.ts +217 -0
  99. package/src/hooks/tool-output-truncator.ts +82 -0
  100. package/src/index.ts +131 -0
  101. package/src/shared/deep-merge.ts +28 -0
  102. package/src/shared/hash.ts +5 -0
  103. package/src/shared/log.ts +30 -0
  104. package/src/shared/model-tuning.ts +48 -0
  105. package/src/shared/paths.ts +70 -0
  106. package/src/shared/text.ts +20 -0
  107. package/src/shared/time.ts +3 -0
  108. package/src/shims.node.d.ts +20 -0
  109. package/src/state/adapters/index.ts +155 -0
  110. package/src/state/db.ts +105 -0
  111. package/src/state/ids.ts +33 -0
  112. package/src/state/schema.ts +249 -0
  113. package/src/state/types.ts +76 -0
  114. package/src/tools/artifacts.ts +83 -0
  115. package/src/tools/index.ts +111 -0
  116. package/src/tools/init.ts +50 -0
  117. package/src/tools/injects.ts +108 -0
  118. package/src/tools/repair.ts +31 -0
  119. package/src/tools/run.ts +62 -0
  120. package/src/tools/spec.ts +50 -0
  121. package/src/tools/stage.ts +361 -0
  122. package/src/tools/status.ts +119 -0
  123. package/src/tools/story.ts +106 -0
  124. package/src/tools/workflow.ts +241 -0
  125. package/src/ui/inject.ts +13 -0
  126. package/src/ui/toasts.ts +48 -0
  127. package/src/workflow/artifacts.ts +69 -0
  128. package/src/workflow/baton.ts +141 -0
  129. package/src/workflow/context.ts +86 -0
  130. package/src/workflow/directives.ts +170 -0
  131. package/src/workflow/repair.ts +138 -0
  132. package/src/workflow/state-machine.ts +194 -0
  133. package/src/workflow/story-helpers.ts +18 -0
@@ -0,0 +1,107 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { decideNextAction, getActiveRun, getStageRuns, getStory } from "../workflow/state-machine";
3
+ function statusIcon(status) {
4
+ switch (status) {
5
+ case "running":
6
+ return "🟦";
7
+ case "completed":
8
+ return "✅";
9
+ case "failed":
10
+ return "⛔";
11
+ case "aborted":
12
+ return "🛑";
13
+ case "created":
14
+ return "🆕";
15
+ default:
16
+ return "⬜";
17
+ }
18
+ }
19
+ function stageIcon(status) {
20
+ switch (status) {
21
+ case "completed":
22
+ return "✅";
23
+ case "running":
24
+ return "🟦";
25
+ case "failed":
26
+ return "⛔";
27
+ case "skipped":
28
+ return "⏭️";
29
+ default:
30
+ return "⬜";
31
+ }
32
+ }
33
+ export function createAstroStatusTool(opts) {
34
+ const { config, db } = opts;
35
+ return tool({
36
+ description: "Show a compact Astrocode status dashboard: active run/stage, pipeline, story board counts, and next action.",
37
+ args: {
38
+ include_board: tool.schema.boolean().default(true),
39
+ include_recent_events: tool.schema.boolean().default(false),
40
+ },
41
+ execute: async ({ include_board, include_recent_events }) => {
42
+ // Check if database is available
43
+ if (!db) {
44
+ return "🔄 Astrocode Status\n\n⚠️ Limited Mode: Database not available\nAstrocode is running with reduced functionality";
45
+ }
46
+ const active = getActiveRun(db);
47
+ const lines = [];
48
+ lines.push(`# Astrocode Status`);
49
+ if (!active) {
50
+ lines.push(`- Active run: *(none)*`);
51
+ const next = decideNextAction(db, config);
52
+ if (next.kind === "idle")
53
+ lines.push(`- Next: ${next.reason === "no_approved_stories" ? "No approved stories." : "Idle."}`);
54
+ if (include_board) {
55
+ const counts = db
56
+ .prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state")
57
+ .all();
58
+ lines.push(``, `## Story board`);
59
+ for (const row of counts)
60
+ lines.push(`- ${row.state}: ${row.c}`);
61
+ }
62
+ return lines.join("\n");
63
+ }
64
+ const story = getStory(db, active.story_key);
65
+ const stageRuns = getStageRuns(db, active.run_id);
66
+ lines.push(`- Active run: \`${active.run_id}\` ${statusIcon(active.status)} **${active.status}**`);
67
+ lines.push(`- Story: \`${active.story_key}\` — ${story?.title ?? "(missing)"} (${story?.state ?? "?"})`);
68
+ lines.push(`- Current stage: \`${active.current_stage_key ?? "?"}\``);
69
+ lines.push(``, `## Pipeline`);
70
+ for (const s of stageRuns) {
71
+ lines.push(`- ${stageIcon(s.status)} \`${s.stage_key}\` (${s.status})`);
72
+ }
73
+ const next = decideNextAction(db, config);
74
+ lines.push(``, `## Next`);
75
+ lines.push(`- ${next.kind}`);
76
+ if (next.kind === "await_stage_completion") {
77
+ lines.push(`- Awaiting output for stage \`${next.stage_key}\`. When you have it, call **astro_stage_complete** with the stage output text.`);
78
+ }
79
+ else if (next.kind === "delegate_stage") {
80
+ lines.push(`- Need to run stage \`${next.stage_key}\`. Use **astro_workflow_proceed** (or delegate to the matching stage agent).`);
81
+ }
82
+ else if (next.kind === "complete_run") {
83
+ lines.push(`- All stages done. Run can be completed via **astro_workflow_proceed**.`);
84
+ }
85
+ else if (next.kind === "failed") {
86
+ lines.push(`- Run failed at \`${next.stage_key}\`: ${next.error_text}`);
87
+ }
88
+ if (include_board) {
89
+ const counts = db
90
+ .prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state")
91
+ .all();
92
+ lines.push(``, `## Story board`);
93
+ for (const row of counts)
94
+ lines.push(`- ${row.state}: ${row.c}`);
95
+ }
96
+ if (include_recent_events) {
97
+ const evs = db
98
+ .prepare("SELECT created_at, type, run_id, stage_key FROM events ORDER BY created_at DESC LIMIT 10")
99
+ .all();
100
+ lines.push(``, `## Recent events`);
101
+ for (const e of evs)
102
+ lines.push(`- ${e.created_at} — ${e.type}${e.run_id ? ` (run=${e.run_id})` : ""}${e.stage_key ? ` stage=${e.stage_key}` : ""}`);
103
+ }
104
+ return lines.join("\n");
105
+ },
106
+ });
107
+ }
@@ -0,0 +1,23 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+ export declare function createAstroStoryQueueTool(opts: {
5
+ ctx: any;
6
+ config: AstrocodeConfig;
7
+ db: SqliteDb;
8
+ }): ToolDefinition;
9
+ export declare function createAstroStoryApproveTool(opts: {
10
+ ctx: any;
11
+ config: AstrocodeConfig;
12
+ db: SqliteDb;
13
+ }): ToolDefinition;
14
+ export declare function createAstroStoryBoardTool(opts: {
15
+ ctx: any;
16
+ config: AstrocodeConfig;
17
+ db: SqliteDb;
18
+ }): ToolDefinition;
19
+ export declare function createAstroStorySetStateTool(opts: {
20
+ ctx: any;
21
+ config: AstrocodeConfig;
22
+ db: SqliteDb;
23
+ }): ToolDefinition;
@@ -0,0 +1,85 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { withTx } from "../state/db";
3
+ import { nowISO } from "../shared/time";
4
+ import { insertStory } from "../workflow/story-helpers";
5
+ export function createAstroStoryQueueTool(opts) {
6
+ const { db } = opts;
7
+ return tool({
8
+ description: "Create a queued story (ticket) in Astrocode. Returns story_key.",
9
+ args: {
10
+ title: tool.schema.string().min(1),
11
+ body_md: tool.schema.string().default(""),
12
+ epic_key: tool.schema.string().optional(),
13
+ priority: tool.schema.number().int().default(0),
14
+ },
15
+ execute: async ({ title, body_md, epic_key, priority }) => {
16
+ const now = nowISO();
17
+ const story_key = withTx(db, () => {
18
+ return insertStory(db, { title, body_md, epic_key: epic_key ?? null, priority: priority ?? 0, state: 'queued' });
19
+ });
20
+ return `✅ Queued story ${story_key}: ${title}`;
21
+ },
22
+ });
23
+ }
24
+ export function createAstroStoryApproveTool(opts) {
25
+ const { db } = opts;
26
+ return tool({
27
+ description: "Approve a story so it becomes eligible to run.",
28
+ args: {
29
+ story_key: tool.schema.string().min(1),
30
+ },
31
+ execute: async ({ story_key }) => {
32
+ const now = nowISO();
33
+ const row = db.prepare("SELECT story_key, state, title FROM stories WHERE story_key=?").get(story_key);
34
+ if (!row)
35
+ throw new Error(`Story not found: ${story_key}`);
36
+ if (row.state === "approved")
37
+ return `ℹ️ Story ${story_key} already approved.`;
38
+ db.prepare("UPDATE stories SET state='approved', approved_at=?, updated_at=? WHERE story_key=?").run(now, now, story_key);
39
+ return `✅ Approved story ${story_key}: ${row.title}`;
40
+ },
41
+ });
42
+ }
43
+ export function createAstroStoryBoardTool(opts) {
44
+ const { db } = opts;
45
+ return tool({
46
+ description: "Show stories grouped by state (a compact board).",
47
+ args: {
48
+ limit_per_state: tool.schema.number().int().positive().default(20),
49
+ },
50
+ execute: async ({ limit_per_state }) => {
51
+ const states = ["queued", "approved", "in_progress", "blocked", "done", "archived"];
52
+ const lines = [];
53
+ lines.push("# Story board");
54
+ for (const st of states) {
55
+ const rows = db
56
+ .prepare("SELECT story_key, title, priority, created_at FROM stories WHERE state=? ORDER BY priority DESC, created_at ASC LIMIT ?")
57
+ .all(st, limit_per_state);
58
+ lines.push("", `## ${st} (${rows.length})`);
59
+ for (const r of rows)
60
+ lines.push(`- \`${r.story_key}\` (p=${r.priority}) — ${r.title}`);
61
+ }
62
+ return lines.join("\n").trim();
63
+ },
64
+ });
65
+ }
66
+ export function createAstroStorySetStateTool(opts) {
67
+ const { db } = opts;
68
+ return tool({
69
+ description: "Admin: set story state manually (queued|approved|in_progress|done|blocked|archived). Use carefully.",
70
+ args: {
71
+ story_key: tool.schema.string().min(1),
72
+ state: tool.schema.enum(["queued", "approved", "in_progress", "done", "blocked", "archived"]),
73
+ note: tool.schema.string().default(""),
74
+ },
75
+ execute: async ({ story_key, state, note }) => {
76
+ const now = nowISO();
77
+ const row = db.prepare("SELECT story_key, title, state FROM stories WHERE story_key=?").get(story_key);
78
+ if (!row)
79
+ throw new Error(`Story not found: ${story_key}`);
80
+ db.prepare("UPDATE stories SET state=?, updated_at=? WHERE story_key=?").run(state, now, story_key);
81
+ db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, 'story.state_set', ?, ?)").run(`evt_${Date.now()}_${Math.random().toString(16).slice(2)}`, JSON.stringify({ story_key, from: row.state, to: state, note }), now);
82
+ return `✅ Story ${story_key} state: ${row.state} → ${state}${note ? ` (${note})` : ""}`;
83
+ },
84
+ });
85
+ }
@@ -0,0 +1,8 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+ export declare function createAstroWorkflowProceedTool(opts: {
5
+ ctx: any;
6
+ config: AstrocodeConfig;
7
+ db: SqliteDb;
8
+ }): ToolDefinition;
@@ -0,0 +1,197 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { withTx } from "../state/db";
3
+ import { buildContextSnapshot } from "../workflow/context";
4
+ import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun } from "../workflow/state-machine";
5
+ import { buildStageDirective, directiveHash } from "../workflow/directives";
6
+ import { injectChatPrompt } from "../ui/inject";
7
+ import { nowISO } from "../shared/time";
8
+ import { newEventId } from "../state/ids";
9
+ import { createToastManager } from "../ui/toasts";
10
+ function stageGoal(stage, cfg) {
11
+ switch (stage) {
12
+ case "frame":
13
+ return "Define scope, constraints, and an unambiguous Definition of Done.";
14
+ case "plan":
15
+ return `Produce a bounded task plan (<= ${cfg.workflow.plan_max_tasks} tasks) tied to files and tests.`;
16
+ case "spec":
17
+ return "Produce minimal spec/contract: interfaces, invariants, acceptance checks.";
18
+ case "implement":
19
+ return "Implement the spec with minimal changes, referencing diffs and evidence as artifacts.";
20
+ case "review":
21
+ return "Review implementation for correctness, risks, and alignment with spec.";
22
+ case "verify":
23
+ return "Run verification commands and produce evidence artifacts.";
24
+ case "close":
25
+ return "Summarize outcome and confirm acceptance criteria, leaving clear breadcrumbs.";
26
+ }
27
+ }
28
+ function stageConstraints(stage, cfg) {
29
+ const common = [
30
+ "Do not narrate prompts.",
31
+ "Keep baton markdown short and structured.",
32
+ "If blocked: ask exactly ONE question and stop.",
33
+ ];
34
+ if (stage === "plan") {
35
+ common.push(`Hard limit: <= ${cfg.workflow.plan_max_tasks} tasks and <= ${cfg.workflow.plan_max_lines} lines of plan output.`);
36
+ }
37
+ if (stage === "verify" && cfg.workflow.evidence_required.verify) {
38
+ common.push("Evidence required: ASTRO JSON must include evidence[] paths.");
39
+ }
40
+ return common;
41
+ }
42
+ function agentNameForStage(stage, cfg) {
43
+ return cfg.agents.stage_agent_names[stage];
44
+ }
45
+ function buildDelegationPrompt(opts) {
46
+ const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
47
+ const stageUpper = stage_key.toUpperCase();
48
+ return [
49
+ `[SYSTEM DIRECTIVE: ASTROCODE — DELEGATE_STAGE_${stageUpper}]`,
50
+ ``,
51
+ `Do this now, in order:`,
52
+ `1) Call the **task** tool to delegate to subagent \`${stage_agent_name}\`.`,
53
+ ` - Pass this exact prompt text to the subagent (copy/paste):`,
54
+ ``,
55
+ stageDirective,
56
+ ``,
57
+ `2) When the subagent returns, immediately call **astro_stage_complete** with:`,
58
+ ` - run_id = "${run_id}"`,
59
+ ` - stage_key = "${stage_key}"`,
60
+ ` - output_text = (the FULL subagent response text)`,
61
+ ``,
62
+ `3) Then call **astro_workflow_proceed** again (mode=step).`,
63
+ ``,
64
+ `Important: do NOT do any stage work yourself in orchestrator mode.`,
65
+ ].join("\n").trim();
66
+ }
67
+ export function createAstroWorkflowProceedTool(opts) {
68
+ const { ctx, config, db } = opts;
69
+ const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
70
+ return tool({
71
+ description: "Deterministic harness: advances the DB-driven pipeline by one step (or loops bounded). Stops when LLM work is required (delegation/await).",
72
+ args: {
73
+ mode: tool.schema.enum(["step", "loop"]).default(config.workflow.default_mode),
74
+ max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
75
+ },
76
+ execute: async ({ mode, max_steps }) => {
77
+ const sessionId = ctx.sessionID;
78
+ const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
79
+ const actions = [];
80
+ const startedAt = nowISO();
81
+ for (let i = 0; i < steps; i++) {
82
+ const next = decideNextAction(db, config);
83
+ if (next.kind === "idle") {
84
+ actions.push("idle: no approved stories");
85
+ break;
86
+ }
87
+ if (next.kind === "start_run") {
88
+ const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
89
+ actions.push(`started run ${run_id} for story ${next.story_key}`);
90
+ if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
91
+ await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
92
+ }
93
+ if (mode === "step")
94
+ break;
95
+ continue;
96
+ }
97
+ if (next.kind === "complete_run") {
98
+ withTx(db, () => completeRun(db, next.run_id));
99
+ actions.push(`completed run ${next.run_id}`);
100
+ if (config.ui.toasts.enabled && config.ui.toasts.show_run_completed) {
101
+ await toasts.show({ title: "Astrocode", message: `Run completed (${next.run_id})`, variant: "success" });
102
+ }
103
+ if (mode === "step")
104
+ break;
105
+ continue;
106
+ }
107
+ if (next.kind === "delegate_stage") {
108
+ const active = getActiveRun(db);
109
+ if (!active)
110
+ throw new Error("Invariant: delegate_stage but no active run.");
111
+ const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
112
+ const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
113
+ // Mark stage started + set subagent_type to the stage agent.
114
+ const agentName = agentNameForStage(next.stage_key, config);
115
+ withTx(db, () => {
116
+ startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
117
+ });
118
+ if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
119
+ await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
120
+ }
121
+ const context = buildContextSnapshot({ db, config, run_id: active.run_id, next_action: `delegate stage ${next.stage_key}` });
122
+ const stageDirective = buildStageDirective({
123
+ config,
124
+ stage_key: next.stage_key,
125
+ run_id: active.run_id,
126
+ story_key: run.story_key,
127
+ story_title: story?.title ?? "(missing)",
128
+ stage_agent_name: agentName,
129
+ stage_goal: stageGoal(next.stage_key, config),
130
+ stage_constraints: stageConstraints(next.stage_key, config),
131
+ context_snapshot_md: context,
132
+ }).body;
133
+ const delegatePrompt = buildDelegationPrompt({
134
+ stageDirective,
135
+ run_id: active.run_id,
136
+ stage_key: next.stage_key,
137
+ stage_agent_name: agentName,
138
+ });
139
+ // Record in continuations as a stage directive (dedupe by hash)
140
+ const h = directiveHash(delegatePrompt);
141
+ const now = nowISO();
142
+ if (sessionId) {
143
+ 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);
144
+ }
145
+ // Visible injection so user can see state
146
+ if (sessionId) {
147
+ await injectChatPrompt({ ctx, sessionId, text: delegatePrompt });
148
+ }
149
+ actions.push(`delegated stage ${next.stage_key} via ${agentName}`);
150
+ // Stop here; subagent needs to run.
151
+ break;
152
+ }
153
+ if (next.kind === "await_stage_completion") {
154
+ actions.push(`await stage completion: ${next.stage_key}`);
155
+ // Optionally nudge with a short directive
156
+ if (sessionId) {
157
+ const context = buildContextSnapshot({ db, config, run_id: next.run_id, next_action: `complete stage ${next.stage_key}` });
158
+ const prompt = [
159
+ `[SYSTEM DIRECTIVE: ASTROCODE — AWAIT_STAGE_OUTPUT]`,
160
+ ``,
161
+ `Run ${next.run_id} is waiting for stage ${next.stage_key} output.`,
162
+ `If you have the subagent output, call astro_stage_complete with output_text=the FULL output.`,
163
+ ``,
164
+ `Context snapshot:`,
165
+ context,
166
+ ].join("\n").trim();
167
+ const h = directiveHash(prompt);
168
+ const now = nowISO();
169
+ 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);
170
+ await injectChatPrompt({ ctx, sessionId, text: prompt });
171
+ }
172
+ break;
173
+ }
174
+ if (next.kind === "failed") {
175
+ actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
176
+ break;
177
+ }
178
+ // safety
179
+ actions.push(`unhandled next action: ${next.kind}`);
180
+ break;
181
+ }
182
+ // Housekeeping event
183
+ db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, 'workflow.proceed', ?, ?)").run(newEventId(), JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
184
+ const active = getActiveRun(db);
185
+ const lines = [];
186
+ lines.push(`# astro_workflow_proceed`);
187
+ lines.push(`- mode: ${mode}`);
188
+ lines.push(`- steps requested: ${max_steps} (cap=${steps})`);
189
+ if (active)
190
+ lines.push(`- active run: \`${active.run_id}\` (stage=${active.current_stage_key ?? "?"})`);
191
+ lines.push(``, `## Actions`);
192
+ for (const a of actions)
193
+ lines.push(`- ${a}`);
194
+ return lines.join("\n").trim();
195
+ },
196
+ });
197
+ }
@@ -0,0 +1,5 @@
1
+ export declare function injectChatPrompt(opts: {
2
+ ctx: any;
3
+ sessionId: string;
4
+ text: string;
5
+ }): Promise<void>;
@@ -0,0 +1,9 @@
1
+ export async function injectChatPrompt(opts) {
2
+ const { ctx, sessionId, text } = opts;
3
+ await ctx.client.session.prompt({
4
+ path: { id: sessionId },
5
+ body: {
6
+ parts: [{ type: "text", text }],
7
+ },
8
+ });
9
+ }
@@ -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,66 @@
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
+ files: z.ZodDefault<z.ZodArray<z.ZodObject<{
36
+ path: z.ZodString;
37
+ kind: z.ZodDefault<z.ZodString>;
38
+ notes: z.ZodOptional<z.ZodString>;
39
+ }, z.core.$strip>>>;
40
+ evidence: z.ZodDefault<z.ZodArray<z.ZodObject<{
41
+ path: z.ZodString;
42
+ kind: z.ZodDefault<z.ZodString>;
43
+ notes: z.ZodOptional<z.ZodString>;
44
+ }, z.core.$strip>>>;
45
+ new_stories: z.ZodDefault<z.ZodArray<z.ZodObject<{
46
+ title: z.ZodString;
47
+ body_md: z.ZodOptional<z.ZodString>;
48
+ priority: z.ZodOptional<z.ZodNumber>;
49
+ }, z.core.$strip>>>;
50
+ questions: z.ZodDefault<z.ZodArray<z.ZodString>>;
51
+ metrics: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
52
+ }, z.core.$strip>;
53
+ export type AstroJson = z.infer<typeof AstroJsonSchema>;
54
+ export type ParsedStageOutput = {
55
+ baton_md: string;
56
+ astro_json: AstroJson | null;
57
+ astro_json_raw: string | null;
58
+ error: string | null;
59
+ };
60
+ export declare function parseStageOutputText(text: string): ParsedStageOutput;
61
+ export declare function buildBatonSummary(opts: {
62
+ config: AstrocodeConfig;
63
+ stage_key: string;
64
+ astro_json: AstroJson | null;
65
+ baton_md: string;
66
+ }): string;