astrocode-workflow 0.3.0 → 0.3.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 (139) 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/config/config-handler.d.ts +4 -0
  13. package/dist/src/config/config-handler.js +46 -0
  14. package/dist/src/config/defaults.d.ts +3 -0
  15. package/dist/src/config/defaults.js +3 -0
  16. package/dist/src/config/loader.d.ts +11 -0
  17. package/dist/src/config/loader.js +82 -0
  18. package/dist/src/config/schema.d.ts +194 -0
  19. package/dist/src/config/schema.js +223 -0
  20. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  21. package/dist/src/hooks/continuation-enforcer.js +190 -0
  22. package/dist/src/hooks/inject-provider.d.ts +22 -0
  23. package/dist/src/hooks/inject-provider.js +120 -0
  24. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  25. package/dist/src/hooks/tool-output-truncator.js +57 -0
  26. package/dist/src/index.d.ts +3 -0
  27. package/dist/src/index.js +308 -0
  28. package/dist/src/shared/deep-merge.d.ts +8 -0
  29. package/dist/src/shared/deep-merge.js +25 -0
  30. package/dist/src/shared/hash.d.ts +1 -0
  31. package/dist/src/shared/hash.js +4 -0
  32. package/dist/src/shared/log.d.ts +7 -0
  33. package/dist/src/shared/log.js +24 -0
  34. package/dist/src/shared/metrics.d.ts +66 -0
  35. package/dist/src/shared/metrics.js +112 -0
  36. package/dist/src/shared/model-tuning.d.ts +9 -0
  37. package/dist/src/shared/model-tuning.js +28 -0
  38. package/dist/src/shared/paths.d.ts +19 -0
  39. package/dist/src/shared/paths.js +64 -0
  40. package/dist/src/shared/text.d.ts +4 -0
  41. package/dist/src/shared/text.js +19 -0
  42. package/dist/src/shared/time.d.ts +1 -0
  43. package/dist/src/shared/time.js +3 -0
  44. package/dist/src/state/adapters/index.d.ts +41 -0
  45. package/dist/src/state/adapters/index.js +115 -0
  46. package/dist/src/state/db.d.ts +16 -0
  47. package/dist/src/state/db.js +225 -0
  48. package/dist/src/state/ids.d.ts +8 -0
  49. package/dist/src/state/ids.js +25 -0
  50. package/dist/src/state/repo-lock.d.ts +3 -0
  51. package/dist/src/state/repo-lock.js +29 -0
  52. package/dist/src/state/schema.d.ts +2 -0
  53. package/dist/src/state/schema.js +251 -0
  54. package/dist/src/state/types.d.ts +71 -0
  55. package/dist/src/state/types.js +1 -0
  56. package/dist/src/tools/artifacts.d.ts +18 -0
  57. package/dist/src/tools/artifacts.js +71 -0
  58. package/dist/src/tools/health.d.ts +8 -0
  59. package/dist/src/tools/health.js +119 -0
  60. package/dist/src/tools/index.d.ts +20 -0
  61. package/dist/src/tools/index.js +94 -0
  62. package/dist/src/tools/init.d.ts +17 -0
  63. package/dist/src/tools/init.js +96 -0
  64. package/dist/src/tools/injects.d.ts +53 -0
  65. package/dist/src/tools/injects.js +325 -0
  66. package/dist/src/tools/metrics.d.ts +7 -0
  67. package/dist/src/tools/metrics.js +61 -0
  68. package/dist/src/tools/repair.d.ts +8 -0
  69. package/dist/src/tools/repair.js +25 -0
  70. package/dist/src/tools/reset.d.ts +8 -0
  71. package/dist/src/tools/reset.js +92 -0
  72. package/dist/src/tools/run.d.ts +13 -0
  73. package/dist/src/tools/run.js +54 -0
  74. package/dist/src/tools/spec.d.ts +12 -0
  75. package/dist/src/tools/spec.js +44 -0
  76. package/dist/src/tools/stage.d.ts +23 -0
  77. package/dist/src/tools/stage.js +371 -0
  78. package/dist/src/tools/status.d.ts +8 -0
  79. package/dist/src/tools/status.js +125 -0
  80. package/dist/src/tools/story.d.ts +23 -0
  81. package/dist/src/tools/story.js +85 -0
  82. package/dist/src/tools/workflow.d.ts +13 -0
  83. package/dist/src/tools/workflow.js +355 -0
  84. package/dist/src/ui/inject.d.ts +12 -0
  85. package/dist/src/ui/inject.js +107 -0
  86. package/dist/src/ui/toasts.d.ts +13 -0
  87. package/dist/src/ui/toasts.js +39 -0
  88. package/dist/src/workflow/artifacts.d.ts +24 -0
  89. package/dist/src/workflow/artifacts.js +45 -0
  90. package/dist/src/workflow/baton.d.ts +72 -0
  91. package/dist/src/workflow/baton.js +166 -0
  92. package/dist/src/workflow/context.d.ts +20 -0
  93. package/dist/src/workflow/context.js +113 -0
  94. package/dist/src/workflow/directives.d.ts +39 -0
  95. package/dist/src/workflow/directives.js +137 -0
  96. package/dist/src/workflow/repair.d.ts +8 -0
  97. package/dist/src/workflow/repair.js +99 -0
  98. package/dist/src/workflow/state-machine.d.ts +86 -0
  99. package/dist/src/workflow/state-machine.js +216 -0
  100. package/dist/src/workflow/story-helpers.d.ts +9 -0
  101. package/dist/src/workflow/story-helpers.js +13 -0
  102. package/dist/state/db.d.ts +1 -0
  103. package/dist/state/db.js +9 -0
  104. package/dist/state/repo-lock.d.ts +3 -0
  105. package/dist/state/repo-lock.js +29 -0
  106. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  107. package/dist/test/integration/db-transactions.test.js +126 -0
  108. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  109. package/dist/test/integration/injection-metrics.test.js +129 -0
  110. package/dist/tools/health.d.ts +8 -0
  111. package/dist/tools/health.js +119 -0
  112. package/dist/tools/index.js +9 -0
  113. package/dist/tools/metrics.d.ts +7 -0
  114. package/dist/tools/metrics.js +61 -0
  115. package/dist/tools/reset.d.ts +8 -0
  116. package/dist/tools/reset.js +92 -0
  117. package/dist/tools/workflow.js +210 -215
  118. package/dist/ui/inject.d.ts +6 -0
  119. package/dist/ui/inject.js +86 -67
  120. package/dist/workflow/state-machine.d.ts +32 -32
  121. package/dist/workflow/state-machine.js +85 -170
  122. package/package.json +6 -3
  123. package/src/index.ts +8 -0
  124. package/src/shared/metrics.ts +148 -0
  125. package/src/state/db.ts +10 -1
  126. package/src/state/repo-lock.ts +158 -0
  127. package/src/tools/health.ts +128 -0
  128. package/src/tools/index.ts +12 -3
  129. package/src/tools/init.ts +26 -14
  130. package/src/tools/metrics.ts +71 -0
  131. package/src/tools/repair.ts +21 -8
  132. package/src/tools/reset.ts +100 -0
  133. package/src/tools/stage.ts +12 -0
  134. package/src/tools/status.ts +17 -3
  135. package/src/tools/story.ts +41 -15
  136. package/src/tools/workflow.ts +123 -121
  137. package/src/ui/inject.ts +113 -79
  138. package/src/workflow/state-machine.ts +123 -227
  139. package/src/tools/workflow.ts.backup +0 -681
@@ -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;
@@ -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;