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,166 @@
1
+ import { clampLines, normalizeNewlines, stripCodeFences } from "../shared/text";
2
+ import { z, ZodError } from "zod";
3
+ export const ASTRO_JSON_BEGIN = "<!-- ASTRO_JSON_BEGIN -->";
4
+ export const ASTRO_JSON_END = "<!-- ASTRO_JSON_END -->";
5
+ export const StageKeySchema = z.enum(["frame", "plan", "spec", "implement", "review", "verify", "close"]);
6
+ export const AstroJsonSchema = z.object({
7
+ schema_version: z.number().int().default(1),
8
+ run_id: z.string().optional(),
9
+ story_key: z.string().optional(),
10
+ stage_key: StageKeySchema,
11
+ status: z.enum(["ok", "blocked", "failed"]).default("ok"),
12
+ summary: z.string().default(""),
13
+ decisions: z.array(z.string()).default([]),
14
+ next_actions: z.array(z.string()).default([]),
15
+ tasks: z.array(z.object({
16
+ title: z.string(),
17
+ description: z.string().optional(),
18
+ complexity: z.number().int().min(1).max(10).optional(),
19
+ subtasks: z.array(z.string()).optional(),
20
+ })).default([]),
21
+ files: z.array(z.object({
22
+ path: z.string(),
23
+ kind: z.string().default("file"),
24
+ notes: z.string().optional(),
25
+ })).default([]),
26
+ evidence: z.array(z.object({
27
+ path: z.string(),
28
+ kind: z.string().default("evidence"),
29
+ notes: z.string().optional(),
30
+ })).default([]),
31
+ new_stories: z.array(z.object({
32
+ title: z.string(),
33
+ body_md: z.string().optional(),
34
+ priority: z.number().int().optional(),
35
+ })).default([]),
36
+ questions: z.array(z.string()).default([]),
37
+ metrics: z.record(z.string(), z.union([z.number(), z.string()])).default({}),
38
+ });
39
+ export function parseStageOutputText(text) {
40
+ const norm = normalizeNewlines(text ?? "").trim();
41
+ const beginIdx = norm.indexOf(ASTRO_JSON_BEGIN);
42
+ const endIdx = norm.indexOf(ASTRO_JSON_END);
43
+ let baton_md = "";
44
+ let jsonRaw = "";
45
+ let fallbackUsed = false;
46
+ if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) {
47
+ // Enhanced fallback: scan for any JSON object in the text
48
+ const jsonMatch = norm.match(/\{[\s\S]*\}/);
49
+ if (jsonMatch) {
50
+ try {
51
+ const parsed = JSON.parse(jsonMatch[0]);
52
+ const astroJson = AstroJsonSchema.parse(parsed);
53
+ return {
54
+ baton_md: norm.replace(jsonMatch[0], "").trim(),
55
+ astro_json: astroJson,
56
+ astro_json_raw: jsonMatch[0],
57
+ error: null
58
+ };
59
+ }
60
+ catch (e) {
61
+ // JSON found but invalid, continue to marker error
62
+ }
63
+ }
64
+ // Fallback: if no markers, check if the entire text is JSON
65
+ try {
66
+ const parsed = JSON.parse(norm);
67
+ const astroJson = AstroJsonSchema.parse(parsed);
68
+ return {
69
+ baton_md: "",
70
+ astro_json: astroJson,
71
+ astro_json_raw: norm,
72
+ error: null
73
+ };
74
+ }
75
+ catch (e) {
76
+ // Not JSON, proceed with marker error
77
+ }
78
+ // If no JSON found, create a default structure from the text
79
+ const defaultJson = {
80
+ schema_version: 1,
81
+ stage_key: "frame", // Default, will be overridden by actual stage_key
82
+ status: "ok",
83
+ summary: norm.trim() || "Stage completed without structured output",
84
+ decisions: [],
85
+ next_actions: [],
86
+ tasks: [],
87
+ files: [],
88
+ evidence: [],
89
+ new_stories: [],
90
+ questions: [],
91
+ metrics: {}
92
+ };
93
+ return {
94
+ baton_md: "",
95
+ astro_json: defaultJson,
96
+ astro_json_raw: JSON.stringify(defaultJson, null, 2),
97
+ error: null
98
+ };
99
+ }
100
+ const before = norm.slice(0, beginIdx).trim();
101
+ jsonRaw = norm.slice(beginIdx + ASTRO_JSON_BEGIN.length, endIdx).trim();
102
+ const after = norm.slice(endIdx + ASTRO_JSON_END.length).trim();
103
+ baton_md = [before, after].filter(Boolean).join("\n\n").trim();
104
+ try {
105
+ const cleaned = stripCodeFences(jsonRaw).trim();
106
+ const parsed = JSON.parse(cleaned);
107
+ const astroJson = AstroJsonSchema.parse(parsed);
108
+ return { baton_md, astro_json: astroJson, astro_json_raw: cleaned, error: null };
109
+ }
110
+ catch (e) {
111
+ if (e instanceof ZodError) {
112
+ return {
113
+ baton_md,
114
+ astro_json: null,
115
+ astro_json_raw: jsonRaw,
116
+ error: `Schema validation failed: ${e.message}. Ensure JSON conforms to ASTRO schema with required fields like stage_key, status, etc.`,
117
+ };
118
+ }
119
+ else {
120
+ return {
121
+ baton_md: norm,
122
+ astro_json: null,
123
+ astro_json_raw: null,
124
+ error: `JSON parsing failed: ${String(e)}. Ensure valid JSON syntax between ${ASTRO_JSON_BEGIN} and ${ASTRO_JSON_END} markers.`,
125
+ };
126
+ }
127
+ }
128
+ }
129
+ export function buildBatonSummary(opts) {
130
+ const { config, stage_key, astro_json, baton_md } = opts;
131
+ const maxLines = config.context_compaction.baton_summary_max_lines;
132
+ const lines = [];
133
+ lines.push(`# ${stage_key} — Summary`);
134
+ if (astro_json?.summary) {
135
+ lines.push("", astro_json.summary.trim());
136
+ }
137
+ else {
138
+ // Fallback: first non-empty paragraph
139
+ const paras = baton_md.split(/\n\n+/).map((p) => p.trim()).filter(Boolean);
140
+ if (paras[0])
141
+ lines.push("", paras[0]);
142
+ }
143
+ const addList = (title, items) => {
144
+ if (!items.length)
145
+ return;
146
+ lines.push("", `## ${title}`);
147
+ for (const it of items.slice(0, 12))
148
+ lines.push(`- ${it}`);
149
+ };
150
+ addList("Decisions", astro_json?.decisions ?? []);
151
+ addList("Next actions", astro_json?.next_actions ?? []);
152
+ const files = astro_json?.files ?? [];
153
+ if (files.length) {
154
+ lines.push("", "## Files");
155
+ for (const f of files.slice(0, 15))
156
+ lines.push(`- \`${f.path}\` (${f.kind})${f.notes ? ` — ${f.notes}` : ""}`);
157
+ }
158
+ const evidence = astro_json?.evidence ?? [];
159
+ if (evidence.length) {
160
+ lines.push("", "## Evidence");
161
+ for (const ev of evidence.slice(0, 15))
162
+ lines.push(`- \`${ev.path}\` (${ev.kind})${ev.notes ? ` — ${ev.notes}` : ""}`);
163
+ }
164
+ const out = lines.join("\n").trim();
165
+ return clampLines(out, maxLines);
166
+ }
@@ -0,0 +1,20 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import type { SqliteDb } from "../state/db";
3
+ import type { RunRow, StageRunRow, StoryRow } from "../state/types";
4
+ export declare function getRun(db: SqliteDb, runId: string): RunRow | null;
5
+ export declare function getStory(db: SqliteDb, storyKey: string): StoryRow | null;
6
+ export declare function listStageRuns(db: SqliteDb, runId: string): StageRunRow[];
7
+ /**
8
+ * Check if a context snapshot is stale by comparing DB timestamps
9
+ */
10
+ export declare function isContextSnapshotStale(snapshotText: string, db: SqliteDb, maxAgeSeconds?: number): boolean;
11
+ /**
12
+ * Add staleness indicator to context snapshot if needed
13
+ */
14
+ export declare function addStalenessIndicator(snapshotText: string, db: SqliteDb, maxAgeSeconds?: number): string;
15
+ export declare function buildContextSnapshot(opts: {
16
+ db: SqliteDb;
17
+ config: AstrocodeConfig;
18
+ run_id: string;
19
+ next_action?: string;
20
+ }): string;
@@ -0,0 +1,113 @@
1
+ import { clampLines } from "../shared/text";
2
+ export function getRun(db, runId) {
3
+ const row = db.prepare("SELECT * FROM runs WHERE run_id = ?").get(runId);
4
+ return row ?? null;
5
+ }
6
+ export function getStory(db, storyKey) {
7
+ const row = db.prepare("SELECT * FROM stories WHERE story_key = ?").get(storyKey);
8
+ return row ?? null;
9
+ }
10
+ export function listStageRuns(db, runId) {
11
+ return db
12
+ .prepare("SELECT * FROM stage_runs WHERE run_id = ? ORDER BY stage_index ASC")
13
+ .all(runId);
14
+ }
15
+ function statusIcon(status) {
16
+ switch (status) {
17
+ case "completed":
18
+ return "✅";
19
+ case "running":
20
+ return "🟦";
21
+ case "failed":
22
+ return "⛔";
23
+ case "skipped":
24
+ return "⏭️";
25
+ case "pending":
26
+ default:
27
+ return "⬜";
28
+ }
29
+ }
30
+ /**
31
+ * Check if a context snapshot is stale by comparing DB timestamps
32
+ */
33
+ export function isContextSnapshotStale(snapshotText, db, maxAgeSeconds = 300) {
34
+ // Extract run_id from snapshot
35
+ const runIdMatch = snapshotText.match(/Run: `([^`]+)`/);
36
+ if (!runIdMatch)
37
+ return true; // Can't validate without run_id
38
+ const runId = runIdMatch[1];
39
+ // Extract snapshot's claimed updated_at
40
+ const snapshotUpdatedMatch = snapshotText.match(/updated: ([^\)]+)\)/);
41
+ if (!snapshotUpdatedMatch)
42
+ return true; // Fallback to age-based check
43
+ try {
44
+ const snapshotUpdatedAt = snapshotUpdatedMatch[1];
45
+ const currentRun = db.prepare("SELECT updated_at FROM runs WHERE run_id = ?").get(runId);
46
+ if (!currentRun?.updated_at)
47
+ return true; // Run doesn't exist
48
+ // Compare timestamps - if DB is newer than snapshot claims, snapshot is stale
49
+ const snapshotTime = new Date(snapshotUpdatedAt).getTime();
50
+ const currentTime = new Date(currentRun.updated_at).getTime();
51
+ return currentTime > snapshotTime;
52
+ }
53
+ catch (error) {
54
+ // Fallback to age-based staleness if parsing fails
55
+ const timestampMatch = snapshotText.match(/generated: (\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/);
56
+ if (!timestampMatch)
57
+ return false;
58
+ const generatedAt = new Date(timestampMatch[1]);
59
+ const now = new Date();
60
+ const ageSeconds = (now.getTime() - generatedAt.getTime()) / 1000;
61
+ return ageSeconds > maxAgeSeconds;
62
+ }
63
+ }
64
+ /**
65
+ * Add staleness indicator to context snapshot if needed
66
+ */
67
+ export function addStalenessIndicator(snapshotText, db, maxAgeSeconds = 300) {
68
+ if (isContextSnapshotStale(snapshotText, db, maxAgeSeconds)) {
69
+ return snapshotText.replace(/# Astrocode Context \(generated: ([^\)]+)\)/, "# Astrocode Context (generated: $1) ⚠️ STALE - DB state has changed");
70
+ }
71
+ return snapshotText;
72
+ }
73
+ export function buildContextSnapshot(opts) {
74
+ const { db, config, run_id, next_action } = opts;
75
+ const run = getRun(db, run_id);
76
+ if (!run)
77
+ return `Run not found: ${run_id}`;
78
+ const story = getStory(db, run.story_key);
79
+ const stageRuns = listStageRuns(db, run_id);
80
+ const lines = [];
81
+ // Add timestamps for staleness checking
82
+ const now = new Date();
83
+ const timestamp = now.toISOString();
84
+ lines.push(`# Astrocode Context (generated: ${timestamp})`);
85
+ lines.push(`- Run: \`${run.run_id}\` Status: **${run.status}** (updated: ${run.updated_at})`);
86
+ if (run.current_stage_key)
87
+ lines.push(`- Current stage: \`${run.current_stage_key}\``);
88
+ if (next_action)
89
+ lines.push(`- Next action: ${next_action}`);
90
+ if (story) {
91
+ lines.push(`- Story: \`${story.story_key}\` — ${story.title}`);
92
+ if (story.body_md?.trim()) {
93
+ const first = story.body_md.trim().split(/\n\n+/)[0]?.trim();
94
+ if (first)
95
+ lines.push(`- Story summary: ${first}`);
96
+ }
97
+ }
98
+ lines.push(``, `## Pipeline`);
99
+ for (const s of stageRuns) {
100
+ lines.push(`- ${statusIcon(s.status)} \`${s.stage_key}\` (${s.status})`);
101
+ }
102
+ const completed = stageRuns.filter((s) => s.status === "completed" && (s.summary_md ?? "").trim());
103
+ const lastSummaries = completed.slice(-config.context_compaction.snapshot_after_stage_count);
104
+ if (lastSummaries.length) {
105
+ lines.push(``, `## Recent stage summaries`);
106
+ for (const s of lastSummaries) {
107
+ lines.push(``, `### ${s.stage_key}`);
108
+ lines.push((s.summary_md ?? "").trim());
109
+ }
110
+ }
111
+ const out = lines.join("\n").trim();
112
+ return clampLines(out, config.context_compaction.snapshot_max_lines);
113
+ }
@@ -0,0 +1,39 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import type { StageKey } from "../state/types";
3
+ export type DirectiveKind = "continue" | "stage" | "blocked" | "repair";
4
+ export type BuiltDirective = {
5
+ kind: DirectiveKind;
6
+ title: string;
7
+ body: string;
8
+ hash: string;
9
+ };
10
+ export declare function directiveHash(body: string): string;
11
+ export declare function buildContinueDirective(opts: {
12
+ config: AstrocodeConfig;
13
+ run_id: string;
14
+ stage_key: string | null;
15
+ next_action: string;
16
+ context_snapshot_md: string;
17
+ }): BuiltDirective;
18
+ export declare function buildBlockedDirective(opts: {
19
+ config?: AstrocodeConfig;
20
+ run_id: string;
21
+ stage_key: string;
22
+ question: string;
23
+ context_snapshot_md: string;
24
+ }): BuiltDirective;
25
+ export declare function buildRepairDirective(opts: {
26
+ config?: AstrocodeConfig;
27
+ report_md: string;
28
+ }): BuiltDirective;
29
+ export declare function buildStageDirective(opts: {
30
+ config: AstrocodeConfig;
31
+ stage_key: StageKey;
32
+ run_id: string;
33
+ story_key: string;
34
+ story_title: string;
35
+ stage_agent_name: string;
36
+ stage_goal: string;
37
+ stage_constraints: string[];
38
+ context_snapshot_md: string;
39
+ }): BuiltDirective;
@@ -0,0 +1,137 @@
1
+ import { sha256Hex } from "../shared/hash";
2
+ import { clampChars, normalizeNewlines } from "../shared/text";
3
+ function getInjectMaxChars(config) {
4
+ // Deterministic fallback for older configs.
5
+ const v = config?.context_compaction?.inject_max_chars;
6
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 12000;
7
+ }
8
+ export function directiveHash(body) {
9
+ // Stable hash to dedupe: normalize newlines + trim
10
+ const norm = normalizeNewlines(body).trim();
11
+ return sha256Hex(norm);
12
+ }
13
+ function finalizeBody(body, maxChars) {
14
+ // Normalize first, clamp second, trim last => hash/body match exactly.
15
+ const norm = normalizeNewlines(body);
16
+ const clamped = clampChars(norm, maxChars);
17
+ return clamped.trim();
18
+ }
19
+ export function buildContinueDirective(opts) {
20
+ const { config, run_id, stage_key, next_action, context_snapshot_md } = opts;
21
+ const maxChars = getInjectMaxChars(config);
22
+ const body = finalizeBody([
23
+ `[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
24
+ ``,
25
+ `This directive is injected by the Astro agent to continue the workflow.`,
26
+ ``,
27
+ `Run: \`${run_id}\`${stage_key ? ` Stage: \`${stage_key}\`` : ""}`,
28
+ ``,
29
+ `Next action: ${next_action}`,
30
+ ``,
31
+ `Rules:`,
32
+ `- Do not stop early. Keep going until the run is completed, failed, or blocked.`,
33
+ `- Prefer tools over prose.`,
34
+ `- If blocked, ask exactly ONE question and stop.`,
35
+ ``,
36
+ `Context snapshot:`,
37
+ (context_snapshot_md ?? "").trim(),
38
+ ].join("\n"), maxChars);
39
+ return {
40
+ kind: "continue",
41
+ title: "ASTROCODE — CONTINUE",
42
+ body,
43
+ hash: directiveHash(body),
44
+ };
45
+ }
46
+ export function buildBlockedDirective(opts) {
47
+ const { config, run_id, stage_key, question, context_snapshot_md } = opts;
48
+ const maxChars = getInjectMaxChars(config);
49
+ const body = finalizeBody([
50
+ `[SYSTEM DIRECTIVE: ASTROCODE — BLOCKED]`,
51
+ ``,
52
+ `This directive is injected by the Astro agent indicating the workflow is blocked.`,
53
+ ``,
54
+ `Run: \`${run_id}\` Stage: \`${stage_key}\``,
55
+ ``,
56
+ `You are blocked. Ask the user exactly ONE question (below), then stop.`,
57
+ ``,
58
+ `Question: ${question}`,
59
+ ``,
60
+ `Context snapshot:`,
61
+ (context_snapshot_md ?? "").trim(),
62
+ ].join("\n"), maxChars);
63
+ return {
64
+ kind: "blocked",
65
+ title: "ASTROCODE — BLOCKED",
66
+ body,
67
+ hash: directiveHash(body),
68
+ };
69
+ }
70
+ export function buildRepairDirective(opts) {
71
+ const maxChars = getInjectMaxChars(opts.config);
72
+ const body = finalizeBody([
73
+ `[SYSTEM DIRECTIVE: ASTROCODE — REPAIR]`,
74
+ ``,
75
+ `This directive is injected by the Astro agent after performing a repair pass.`,
76
+ ``,
77
+ `Astrocode performed a repair pass. Summarize in <= 10 lines and continue.`,
78
+ ``,
79
+ `Repair report:`,
80
+ (opts.report_md ?? "").trim(),
81
+ ].join("\n"), maxChars);
82
+ return {
83
+ kind: "repair",
84
+ title: "ASTROCODE — REPAIR",
85
+ body,
86
+ hash: directiveHash(body),
87
+ };
88
+ }
89
+ export function buildStageDirective(opts) {
90
+ const { config, stage_key, run_id, story_key, story_title, stage_agent_name, stage_goal, stage_constraints, context_snapshot_md, } = opts;
91
+ const maxChars = getInjectMaxChars(config);
92
+ const stageKeyUpper = String(stage_key).toUpperCase();
93
+ const constraintsBlock = Array.isArray(stage_constraints) && stage_constraints.length
94
+ ? ["", "Constraints:", ...stage_constraints.map((c) => `- ${c}`)].join("\n")
95
+ : "";
96
+ const body = finalizeBody([
97
+ `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_${stageKeyUpper}]`,
98
+ ``,
99
+ `This directive is injected by the Astro agent to delegate the stage task.`,
100
+ ``,
101
+ `You are: \`${stage_agent_name}\``,
102
+ `Run: \`${run_id}\``,
103
+ `Story: \`${story_key}\` — ${story_title}`,
104
+ ``,
105
+ `Stage goal: ${stage_goal}`,
106
+ constraintsBlock,
107
+ ``,
108
+ `Output contract (strict):`,
109
+ `1) Baton markdown (short, structured)`,
110
+ `2) ASTRO JSON between markers:`,
111
+ ` <!-- ASTRO_JSON_BEGIN -->`,
112
+ ` {`,
113
+ ` "schema_version": 1,`,
114
+ ` "stage_key": "${stage_key}",`,
115
+ ` "status": "ok",`,
116
+ ` "...": "..."`,
117
+ ` }`,
118
+ ` <!-- ASTRO_JSON_END -->`,
119
+ ``,
120
+ `ASTRO JSON requirements:`,
121
+ `- stage_key must be "${stage_key}"`,
122
+ `- status must be "ok" | "blocked" | "failed"`,
123
+ `- include summary + next_actions`,
124
+ `- include files/evidence paths when relevant`,
125
+ ``,
126
+ `If blocked: ask exactly ONE question and stop.`,
127
+ ``,
128
+ `Context snapshot:`,
129
+ (context_snapshot_md ?? "").trim(),
130
+ ].join("\n"), maxChars);
131
+ return {
132
+ kind: "stage",
133
+ title: `ASTROCODE — STAGE_${stageKeyUpper}`,
134
+ body,
135
+ hash: directiveHash(body),
136
+ };
137
+ }
@@ -0,0 +1,8 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import type { SqliteDb } from "../state/db";
3
+ export type RepairReport = {
4
+ actions: string[];
5
+ warnings: string[];
6
+ };
7
+ export declare function repairState(db: SqliteDb, config: AstrocodeConfig): RepairReport;
8
+ export declare function formatRepairReport(report: RepairReport): string;
@@ -0,0 +1,99 @@
1
+ import { nowISO } from "../shared/time";
2
+ import { newEventId, newStageRunId } from "../state/ids";
3
+ function push(report, line) {
4
+ report.actions.push(line);
5
+ }
6
+ function warn(report, line) {
7
+ report.warnings.push(line);
8
+ }
9
+ export function repairState(db, config) {
10
+ const report = { actions: [], warnings: [] };
11
+ const now = nowISO();
12
+ // 1) Multiple running runs -> abort extras (keep most recent started_at)
13
+ const running = db
14
+ .prepare("SELECT * FROM runs WHERE status='running' ORDER BY started_at DESC, created_at DESC")
15
+ .all();
16
+ if (running.length > 1) {
17
+ const keep = running[0];
18
+ for (const r of running.slice(1)) {
19
+ db.prepare("UPDATE runs SET status='aborted', error_text=?, completed_at=?, updated_at=? WHERE run_id=?").run("repair: multiple running runs", now, now, r.run_id);
20
+ // unlock story
21
+ db.prepare("UPDATE stories SET state='approved', in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?").run(now, r.story_key);
22
+ db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, NULL, 'run.aborted', ?, ?)").run(newEventId(), r.run_id, JSON.stringify({ reason: "repair: multiple running runs" }), now);
23
+ push(report, `Aborted extra running run ${r.run_id} (kept ${keep.run_id})`);
24
+ }
25
+ }
26
+ // 2) Fix story locks for the active run
27
+ const active = db.prepare("SELECT * FROM runs WHERE status='running' ORDER BY started_at DESC LIMIT 1").get();
28
+ if (active) {
29
+ const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(active.story_key);
30
+ if (!story) {
31
+ warn(report, `Active run ${active.run_id} references missing story ${active.story_key}`);
32
+ }
33
+ else {
34
+ if (story.locked_by_run_id !== active.run_id || story.in_progress !== 1 || story.state !== "in_progress") {
35
+ db.prepare("UPDATE stories SET state='in_progress', in_progress=1, locked_by_run_id=?, locked_at=COALESCE(locked_at, ?), updated_at=? WHERE story_key=?").run(active.run_id, now, now, story.story_key);
36
+ push(report, `Repaired story lock for ${story.story_key} to run ${active.run_id}`);
37
+ }
38
+ }
39
+ // 3) Ensure stage_runs exist for pipeline
40
+ const pipeline = JSON.parse(active.pipeline_stages_json ?? "[]");
41
+ const stageRuns = db
42
+ .prepare("SELECT * FROM stage_runs WHERE run_id=? ORDER BY stage_index ASC")
43
+ .all(active.run_id);
44
+ if (stageRuns.length < pipeline.length) {
45
+ const existingKeys = new Set(stageRuns.map((s) => s.stage_key));
46
+ const insert = db.prepare("INSERT INTO stage_runs (stage_run_id, run_id, stage_key, stage_index, status, created_at, updated_at) VALUES (?, ?, ?, ?, 'pending', ?, ?)");
47
+ pipeline.forEach((key, idx) => {
48
+ if (!existingKeys.has(key)) {
49
+ insert.run(newStageRunId(), active.run_id, key, idx, now, now);
50
+ push(report, `Inserted missing stage_run ${key} for run ${active.run_id}`);
51
+ }
52
+ });
53
+ }
54
+ // 4) If current_stage_key missing, set to first incomplete
55
+ const refreshed = db
56
+ .prepare("SELECT * FROM stage_runs WHERE run_id=? ORDER BY stage_index ASC")
57
+ .all(active.run_id);
58
+ const cur = refreshed.find((s) => s.status !== "completed" && s.status !== "skipped");
59
+ if (cur && active.current_stage_key !== cur.stage_key) {
60
+ db.prepare("UPDATE runs SET current_stage_key=?, updated_at=? WHERE run_id=?").run(cur.stage_key, now, active.run_id);
61
+ push(report, `Set run.current_stage_key to ${cur.stage_key} for ${active.run_id}`);
62
+ }
63
+ }
64
+ // 5) Orphaned story locks (in_progress=1 but no running run)
65
+ const lockedStories = db
66
+ .prepare("SELECT * FROM stories WHERE in_progress=1")
67
+ .all();
68
+ for (const s of lockedStories) {
69
+ const run = s.locked_by_run_id
70
+ ? db.prepare("SELECT * FROM runs WHERE run_id=?").get(s.locked_by_run_id)
71
+ : undefined;
72
+ if (!run || run.status !== "running") {
73
+ db.prepare("UPDATE stories SET in_progress=0, locked_by_run_id=NULL, locked_at=NULL, updated_at=? WHERE story_key=?").run(now, s.story_key);
74
+ // Keep state conservative: if it was in_progress but no run, revert to approved.
75
+ db.prepare("UPDATE stories SET state='approved', updated_at=? WHERE story_key=? AND state='in_progress'").run(now, s.story_key);
76
+ push(report, `Cleared orphaned lock on story ${s.story_key}`);
77
+ }
78
+ }
79
+ if (!report.actions.length && !report.warnings.length) {
80
+ report.actions.push("No repairs needed.");
81
+ }
82
+ // Event marker
83
+ db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, 'repair.completed', ?, ?)").run(newEventId(), JSON.stringify({ actions: report.actions, warnings: report.warnings, schema: config.db.schema_version_required }), now);
84
+ return report;
85
+ }
86
+ export function formatRepairReport(report) {
87
+ const lines = [];
88
+ lines.push("# Astrocode repair report");
89
+ lines.push("");
90
+ lines.push("## Actions");
91
+ for (const a of report.actions)
92
+ lines.push(`- ${a}`);
93
+ if (report.warnings.length) {
94
+ lines.push("", "## Warnings");
95
+ for (const w of report.warnings)
96
+ lines.push(`- ${w}`);
97
+ }
98
+ return lines.join("\n").trim();
99
+ }
@@ -0,0 +1,86 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import type { SqliteDb } from "../state/db";
3
+ import type { RunRow, StageKey, StageRunRow, StoryRow } from "../state/types";
4
+ export declare const EVENT_TYPES: {
5
+ readonly RUN_STARTED: "run.started";
6
+ readonly RUN_COMPLETED: "run.completed";
7
+ readonly RUN_FAILED: "run.failed";
8
+ readonly RUN_ABORTED: "run.aborted";
9
+ readonly RUN_GENESIS_PLANNING_ATTACHED: "run.genesis_planning_attached";
10
+ readonly STAGE_STARTED: "stage.started";
11
+ readonly WORKFLOW_PROCEED: "workflow.proceed";
12
+ };
13
+ export type UiEmitEvent = {
14
+ kind: "stage_started";
15
+ run_id: string;
16
+ stage_key: StageKey;
17
+ agent_name?: string;
18
+ } | {
19
+ kind: "run_completed";
20
+ run_id: string;
21
+ story_key: string;
22
+ } | {
23
+ kind: "run_failed";
24
+ run_id: string;
25
+ story_key: string;
26
+ stage_key: StageKey;
27
+ error_text: string;
28
+ };
29
+ export type UiEmit = (e: UiEmitEvent) => void;
30
+ /**
31
+ * PLANNING-FIRST REDESIGN
32
+ * ----------------------
33
+ * Never mutate story title/body.
34
+ *
35
+ * Deterministic trigger:
36
+ * - config.workflow.genesis_planning:
37
+ * - "off" => never attach directive
38
+ * - "first_story_only"=> only when story_key === "S-0001"
39
+ * - "always" => attach for every run
40
+ *
41
+ * Contract: DB is already initialized before workflow is used:
42
+ * - schema tables exist
43
+ * - repo_state singleton row (id=1) exists
44
+ *
45
+ * IMPORTANT: Do NOT call withTx() in here. The caller owns transaction boundaries.
46
+ */
47
+ export type NextAction = {
48
+ kind: "idle";
49
+ reason: "no_approved_stories";
50
+ } | {
51
+ kind: "start_run";
52
+ story_key: string;
53
+ } | {
54
+ kind: "delegate_stage";
55
+ run_id: string;
56
+ stage_key: StageKey;
57
+ stage_run_id: string;
58
+ } | {
59
+ kind: "await_stage_completion";
60
+ run_id: string;
61
+ stage_key: StageKey;
62
+ stage_run_id: string;
63
+ } | {
64
+ kind: "complete_run";
65
+ run_id: string;
66
+ } | {
67
+ kind: "failed";
68
+ run_id: string;
69
+ stage_key: StageKey;
70
+ error_text: string;
71
+ };
72
+ export declare function getActiveRun(db: SqliteDb): RunRow | null;
73
+ export declare function getStory(db: SqliteDb, storyKey: string): StoryRow | null;
74
+ export declare function getStageRuns(db: SqliteDb, runId: string): StageRunRow[];
75
+ export declare function getCurrentStageRun(stageRuns: StageRunRow[]): StageRunRow | null;
76
+ export declare function decideNextAction(db: SqliteDb, config: AstrocodeConfig): NextAction;
77
+ export declare function createRunForStory(db: SqliteDb, config: AstrocodeConfig, storyKey: string): {
78
+ run_id: string;
79
+ };
80
+ export declare function startStage(db: SqliteDb, runId: string, stageKey: StageKey, meta?: {
81
+ subagent_type?: string;
82
+ subagent_session_id?: string;
83
+ }, emit?: UiEmit): void;
84
+ export declare function completeRun(db: SqliteDb, runId: string, emit?: UiEmit): void;
85
+ export declare function failRun(db: SqliteDb, runId: string, stageKey: StageKey, errorText: string, emit?: UiEmit): void;
86
+ export declare function abortRun(db: SqliteDb, runId: string, reason: string): void;