astrocode-workflow 0.3.1 → 0.3.3

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 (136) 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 +178 -168
  118. package/dist/ui/inject.js +21 -9
  119. package/package.json +6 -3
  120. package/src/astro/workflow-runner.ts +36 -0
  121. package/src/index.ts +8 -0
  122. package/src/shared/metrics.ts +148 -0
  123. package/src/state/db.ts +10 -1
  124. package/src/state/repo-lock.ts +574 -0
  125. package/src/state/workflow-repo-lock.ts +74 -0
  126. package/src/tools/health.ts +128 -0
  127. package/src/tools/index.ts +12 -3
  128. package/src/tools/init.ts +7 -6
  129. package/src/tools/metrics.ts +71 -0
  130. package/src/tools/repair.ts +1 -0
  131. package/src/tools/reset.ts +100 -0
  132. package/src/tools/stage.ts +1 -0
  133. package/src/tools/status.ts +2 -1
  134. package/src/tools/story.ts +1 -0
  135. package/src/tools/workflow.ts +18 -1
  136. package/src/ui/inject.ts +21 -9
@@ -0,0 +1,128 @@
1
+ // src/tools/health.ts
2
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ import type { AstrocodeConfig } from "../config/schema";
4
+ import type { SqliteDb } from "../state/db";
5
+ import { getSchemaVersion } from "../state/db";
6
+ import { getActiveRun } from "../workflow/state-machine";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+
10
+ export function createAstroHealthTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
11
+ const { ctx, config, db } = opts;
12
+
13
+ return tool({
14
+ description: "Check Astrocode health: DB status, locks, schema, active runs, recent events.",
15
+ args: {},
16
+ execute: async () => {
17
+ const lines: string[] = [];
18
+ const repoRoot = (ctx as any).directory || process.cwd();
19
+ const dbPath = config.db?.path || ".astro/astro.db";
20
+ const fullDbPath = path.resolve(repoRoot, dbPath);
21
+
22
+ // System info
23
+ lines.push("# Astrocode Health Check");
24
+ lines.push(`- PID: ${(process as any).pid || "unknown"}`);
25
+ lines.push(`- Repo: ${repoRoot}`);
26
+ lines.push(`- DB Path: ${fullDbPath}`);
27
+
28
+ // Lock status
29
+ const lockPath = `${repoRoot}/.astro/astro.lock`;
30
+ try {
31
+ if (fs.existsSync(lockPath)) {
32
+ const lockContent = fs.readFileSync(lockPath, "utf8").trim();
33
+ const parts = lockContent.split(" ");
34
+ if (parts.length >= 2) {
35
+ const pid = parseInt(parts[0]);
36
+ const startedAt = parts[1];
37
+
38
+ // Check if PID is still running
39
+ try {
40
+ (process as any).kill(pid, 0); // Signal 0 just checks if process exists
41
+ lines.push(`- Lock: HELD by PID ${pid} (started ${startedAt})`);
42
+ } catch {
43
+ lines.push(`- Lock: STALE (PID ${pid} not running, started ${startedAt})`);
44
+ lines.push(` → Run: rm "${lockPath}"`);
45
+ }
46
+ } else {
47
+ lines.push(`- Lock: MALFORMED (${lockContent})`);
48
+ }
49
+ } else {
50
+ lines.push(`- Lock: NONE (no lock file)`);
51
+ }
52
+ } catch (e) {
53
+ lines.push(`- Lock: ERROR (${String(e)})`);
54
+ }
55
+
56
+ // DB file status
57
+ const dbExists = fs.existsSync(fullDbPath);
58
+ const walExists = fs.existsSync(`${fullDbPath}-wal`);
59
+ const shmExists = fs.existsSync(`${fullDbPath}-shm`);
60
+
61
+ lines.push(`- DB Files:`);
62
+ lines.push(` - Main: ${dbExists ? "EXISTS" : "MISSING"}`);
63
+ lines.push(` - WAL: ${walExists ? "EXISTS" : "MISSING"}`);
64
+ lines.push(` - SHM: ${shmExists ? "EXISTS" : "MISSING"}`);
65
+
66
+ if (!dbExists) {
67
+ lines.push(`- STATUS: DB MISSING - run astro_init first`);
68
+ return lines.join("\n");
69
+ }
70
+
71
+ // Schema version
72
+ try {
73
+ const schemaVersion = getSchemaVersion(db);
74
+ lines.push(`- Schema Version: ${schemaVersion}`);
75
+ } catch (e) {
76
+ lines.push(`- Schema Version: ERROR (${String(e)})`);
77
+ lines.push(`- STATUS: DB CORRUPTED`);
78
+ return lines.join("\n");
79
+ }
80
+
81
+ // Active run
82
+ try {
83
+ const activeRun = getActiveRun(db);
84
+ if (activeRun) {
85
+ lines.push(`- Active Run: ${activeRun.run_id} (${activeRun.status})`);
86
+ lines.push(` - Story: ${activeRun.story_key}`);
87
+ lines.push(` - Stage: ${activeRun.current_stage_key || "none"}`);
88
+ lines.push(` - Started: ${activeRun.started_at}`);
89
+ } else {
90
+ lines.push(`- Active Run: NONE`);
91
+ }
92
+ } catch (e) {
93
+ lines.push(`- Active Run: ERROR (${String(e)})`);
94
+ }
95
+
96
+ // Recent events
97
+ try {
98
+ const events = db.prepare(`
99
+ SELECT event_id, run_id, stage_key, type, created_at
100
+ FROM events
101
+ ORDER BY created_at DESC
102
+ LIMIT 10
103
+ `).all() as any[];
104
+
105
+ lines.push(`- Recent Events (${events.length}):`);
106
+ for (const event of events) {
107
+ const stage = event.stage_key ? `/${event.stage_key}` : "";
108
+ lines.push(` - ${event.created_at}: ${event.type} (${event.run_id || "global"}${stage})`);
109
+ }
110
+ } catch (e) {
111
+ lines.push(`- Recent Events: ERROR (${String(e)})`);
112
+ }
113
+
114
+ // Status summary
115
+ lines.push(``);
116
+ lines.push(`## Status`);
117
+ lines.push(`✅ DB accessible`);
118
+ lines.push(`✅ Schema valid`);
119
+ lines.push(`✅ Lock file checked`);
120
+
121
+ if (walExists || shmExists) {
122
+ lines.push(`⚠️ WAL/SHM files present - indicates unclean shutdown or active transaction`);
123
+ }
124
+
125
+ return lines.join("\n");
126
+ },
127
+ });
128
+ }
@@ -12,6 +12,9 @@ import { createAstroStageStartTool, createAstroStageCompleteTool, createAstroSta
12
12
  import { createAstroArtifactPutTool, createAstroArtifactListTool, createAstroArtifactGetTool } from "./artifacts";
13
13
  import { createAstroInjectPutTool, createAstroInjectListTool, createAstroInjectSearchTool, createAstroInjectGetTool, createAstroInjectEligibleTool, createAstroInjectDebugDueTool } from "./injects";
14
14
  import { createAstroRepairTool } from "./repair";
15
+ import { createAstroHealthTool } from "./health";
16
+ import { createAstroResetTool } from "./reset";
17
+ import { createAstroMetricsTool } from "./metrics";
15
18
 
16
19
  import { AgentConfig } from "@opencode-ai/sdk";
17
20
 
@@ -35,9 +38,12 @@ export function createAstroTools(opts: CreateAstroToolsOptions): Record<string,
35
38
 
36
39
  const tools: Record<string, ToolDefinition> = {};
37
40
 
38
- // Always available tools (work without database - guaranteed DB-independent)
39
- tools.astro_status = createAstroStatusTool({ ctx, config });
40
- tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
41
+ // Always available tools (work without database - guaranteed DB-independent)
42
+ tools.astro_status = createAstroStatusTool({ ctx, config });
43
+ tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
44
+ tools.astro_health = createAstroHealthTool({ ctx, config, db });
45
+ tools.astro_reset = createAstroResetTool({ ctx, config, db });
46
+ tools.astro_metrics = createAstroMetricsTool({ ctx, config });
41
47
 
42
48
  // Recovery tool - available even in limited mode to allow DB initialization
43
49
  tools.astro_init = createAstroInitTool({ ctx, config, runtime });
@@ -100,6 +106,9 @@ export function createAstroTools(opts: CreateAstroToolsOptions): Record<string,
100
106
  ["_astro_inject_eligible", "astro_inject_eligible"],
101
107
  ["_astro_inject_debug_due", "astro_inject_debug_due"],
102
108
  ["_astro_repair", "astro_repair"],
109
+ ["_astro_health", "astro_health"],
110
+ ["_astro_reset", "astro_reset"],
111
+ ["_astro_metrics", "astro_metrics"],
103
112
  ];
104
113
 
105
114
  // Only add aliases for tools that exist
package/src/tools/init.ts CHANGED
@@ -8,6 +8,7 @@ import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
8
8
  import { nowISO } from "../shared/time";
9
9
  import { sha256Hex } from "../shared/hash";
10
10
 
11
+
11
12
  type RuntimeState = {
12
13
  db: SqliteDb | null;
13
14
  limitedMode: boolean;
@@ -29,14 +30,14 @@ export function createAstroInitTool(opts: { ctx: any; config: AstrocodeConfig; r
29
30
  },
30
31
  execute: async ({ ensure_spec, spec_placeholder }) => {
31
32
  const repoRoot = ctx.directory as string;
32
- const paths = getAstroPaths(repoRoot, config.db.path);
33
- ensureAstroDirs(paths);
33
+ const paths = getAstroPaths(repoRoot, config.db.path);
34
+ ensureAstroDirs(paths);
34
35
 
35
- const hadDbAlready = !!runtime.db;
36
- let db: SqliteDb | null = runtime.db;
37
- let publishedToRuntime = false;
36
+ const hadDbAlready = !!runtime.db;
37
+ let db: SqliteDb | null = runtime.db;
38
+ let publishedToRuntime = false;
38
39
 
39
- try {
40
+ try {
40
41
  if (!db) {
41
42
  try {
42
43
  db = openSqlite(paths.dbPath, { busyTimeoutMs: config.db.busy_timeout_ms });
@@ -0,0 +1,71 @@
1
+ // src/tools/metrics.ts
2
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ import { metrics } from "../shared/metrics";
4
+
5
+ type CreateAstroMetricsToolOptions = {
6
+ ctx: any;
7
+ config: any;
8
+ };
9
+
10
+ export function createAstroMetricsTool(opts: CreateAstroMetricsToolOptions): ToolDefinition {
11
+ return tool({
12
+ description: "Get performance metrics for Astrocode operations including transaction times, injection success rates, and error statistics.",
13
+ args: {},
14
+ execute: async () => {
15
+ return runMetricsTool();
16
+ },
17
+ });
18
+ }
19
+
20
+ function runMetricsTool(): string {
21
+ const stats = metrics.getMetrics();
22
+ const txStats = metrics.getTransactionStats();
23
+ const injectionStats = metrics.getInjectionStats();
24
+
25
+ let output = "# Astrocode Performance Metrics\n\n";
26
+
27
+ // Transaction Stats
28
+ if (txStats) {
29
+ output += "## Database Transactions\n\n";
30
+ output += `**Total:** ${txStats.total}\n`;
31
+ output += `**Success Rate:** ${(txStats.successRate * 100).toFixed(1)}% (${txStats.successful}/${txStats.total})\n`;
32
+ output += `**Average Duration:** ${txStats.avgDuration.toFixed(2)}ms\n`;
33
+ output += `**Duration Range:** ${txStats.minDuration}ms - ${txStats.maxDuration}ms\n`;
34
+ output += `**Average Nesting Depth:** ${txStats.avgNestedDepth.toFixed(1)}\n\n`;
35
+ } else {
36
+ output += "## Database Transactions\n\nNo transaction data available.\n\n";
37
+ }
38
+
39
+ // Injection Stats
40
+ if (injectionStats) {
41
+ output += "## UI Injections\n\n";
42
+ output += `**Total:** ${injectionStats.total}\n`;
43
+ output += `**Success Rate:** ${(injectionStats.successRate * 100).toFixed(1)}% (${injectionStats.successful}/${injectionStats.total})\n`;
44
+ output += `**Average Attempts:** ${injectionStats.avgAttempts.toFixed(1)}\n`;
45
+ output += `**Total Retries:** ${injectionStats.totalRetries}\n`;
46
+ output += `**Average Duration:** ${injectionStats.avgDuration.toFixed(2)}ms\n\n`;
47
+ } else {
48
+ output += "## UI Injections\n\nNo injection data available.\n\n";
49
+ }
50
+
51
+ // Recent Errors
52
+ if (stats.errors.length > 0) {
53
+ output += "## Recent Errors\n\n";
54
+ const recentErrors = stats.errors.slice(-10); // Last 10 errors
55
+ for (const error of recentErrors) {
56
+ const timestamp = new Date(error.timestamp).toISOString();
57
+ output += `- **[${error.type}]** ${timestamp}: ${error.message}\n`;
58
+ }
59
+ output += "\n";
60
+ } else {
61
+ output += "## Recent Errors\n\nNo errors recorded.\n\n";
62
+ }
63
+
64
+ // Raw Data Summary
65
+ output += "## Data Summary\n\n";
66
+ output += `**Transactions Tracked:** ${stats.transactions.length}\n`;
67
+ output += `**Injections Tracked:** ${stats.injections.length}\n`;
68
+ output += `**Errors Recorded:** ${stats.errors.length}\n`;
69
+
70
+ return output;
71
+ }
@@ -6,6 +6,7 @@ import { repairState, formatRepairReport } from "../workflow/repair";
6
6
  import { putArtifact } from "../workflow/artifacts";
7
7
  import { nowISO } from "../shared/time";
8
8
 
9
+
9
10
  export function createAstroRepairTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
10
11
  const { ctx, config, db } = opts;
11
12
 
@@ -0,0 +1,100 @@
1
+ // src/tools/reset.ts
2
+ import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ import type { AstrocodeConfig } from "../config/schema";
4
+ import type { SqliteDb } from "../state/db";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+
8
+ export function createAstroResetTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
9
+ const { ctx, config, db } = opts;
10
+
11
+ return tool({
12
+ description: "Reset Astrocode database: safely delete all DB files and WAL/SHM after killing concurrent processes.",
13
+ args: {
14
+ confirm: tool.schema.string().default("").describe("Type 'RESET' to confirm destructive operation"),
15
+ },
16
+ execute: async ({ confirm }) => {
17
+ if (confirm !== "RESET") {
18
+ return [
19
+ "❌ Reset cancelled - confirmation required",
20
+ "",
21
+ "This operation will:",
22
+ "- Delete .astro/astro.db",
23
+ "- Delete .astro/astro.db-wal (if exists)",
24
+ "- Delete .astro/astro.db-shm (if exists)",
25
+ "- Lose all workflow data, stories, runs, artifacts",
26
+ "",
27
+ "To confirm: astro_reset(confirm=\"RESET\")",
28
+ ].join("\n");
29
+ }
30
+
31
+ const repoRoot = (ctx as any).directory || process.cwd();
32
+ const dbPath = config.db?.path || ".astro/astro.db";
33
+ const fullDbPath = path.resolve(repoRoot, dbPath);
34
+
35
+ const lines: string[] = [];
36
+ lines.push("🗑️ Astrocode Database Reset");
37
+ lines.push(`- Repo: ${repoRoot}`);
38
+ lines.push(`- Target: ${fullDbPath}`);
39
+
40
+ // Check for lock file
41
+ const lockPath = `${repoRoot}/.astro/astro.lock`;
42
+ if (fs.existsSync(lockPath)) {
43
+ try {
44
+ const lockContent = fs.readFileSync(lockPath, "utf8").trim();
45
+ const pid = parseInt(lockContent.split(" ")[0]);
46
+
47
+ lines.push(`- Lock file found for PID ${pid}`);
48
+
49
+ // Try to kill the process
50
+ try {
51
+ (process as any).kill(pid, 'SIGTERM');
52
+ lines.push(`- Sent SIGTERM to PID ${pid}, waiting 2s...`);
53
+ await new Promise(resolve => setTimeout(resolve, 2000));
54
+ } catch (e) {
55
+ lines.push(`- Could not kill PID ${pid}: ${String(e)}`);
56
+ }
57
+ } catch (e) {
58
+ lines.push(`- Error reading lock file: ${String(e)}`);
59
+ }
60
+ }
61
+
62
+ // Delete DB files
63
+ const filesToDelete = [
64
+ fullDbPath,
65
+ `${fullDbPath}-wal`,
66
+ `${fullDbPath}-shm`,
67
+ lockPath,
68
+ ];
69
+
70
+ let deletedCount = 0;
71
+ for (const filePath of filesToDelete) {
72
+ try {
73
+ if (fs.existsSync(filePath)) {
74
+ fs.unlinkSync(filePath);
75
+ lines.push(`- Deleted: ${path.relative(repoRoot, filePath)}`);
76
+ deletedCount++;
77
+ } else {
78
+ lines.push(`- Skipped: ${path.relative(repoRoot, filePath)} (not found)`);
79
+ }
80
+ } catch (e) {
81
+ lines.push(`- Failed to delete ${path.relative(repoRoot, filePath)}: ${String(e)}`);
82
+ }
83
+ }
84
+
85
+ lines.push(``);
86
+ if (deletedCount > 0) {
87
+ lines.push(`✅ Reset complete - ${deletedCount} files deleted`);
88
+ lines.push(``);
89
+ lines.push(`Next steps:`);
90
+ lines.push(`1. Run: astro_init`);
91
+ lines.push(`2. Run: astro_status`);
92
+ lines.push(`3. Import your stories and restart workflow`);
93
+ } else {
94
+ lines.push(`ℹ️ No files found to delete`);
95
+ }
96
+
97
+ return lines.join("\n");
98
+ },
99
+ });
100
+ }
@@ -14,6 +14,7 @@ import { failRun, getActiveRun, getStageRuns, startStage, completeRun } from "..
14
14
  import { newEventId, newId } from "../state/ids";
15
15
  import { insertStory } from "../workflow/story-helpers";
16
16
 
17
+
17
18
  function nextStageKey(pipeline: StageKey[], current: StageKey): StageKey | null {
18
19
  const i = pipeline.indexOf(current);
19
20
  if (i === -1) return null;
@@ -3,6 +3,7 @@ import type { AstrocodeConfig } from "../config/schema";
3
3
  import type { SqliteDb } from "../state/db";
4
4
  import { decideNextAction, getActiveRun, getStageRuns, getStory } from "../workflow/state-machine";
5
5
 
6
+
6
7
  function statusIcon(status: string): string {
7
8
  switch (status) {
8
9
  case "running":
@@ -36,7 +37,7 @@ function stageIcon(status: string): string {
36
37
  }
37
38
 
38
39
  export function createAstroStatusTool(opts: { ctx: any; config: AstrocodeConfig; db?: SqliteDb | null }): ToolDefinition {
39
- const { config, db } = opts;
40
+ const { ctx, config, db } = opts;
40
41
 
41
42
  return tool({
42
43
  description: "Show a compact Astrocode status dashboard: active run/stage, pipeline, story board counts, and next action.",
@@ -8,6 +8,7 @@ import type { StoryState } from "../state/types";
8
8
  import { insertStory } from "../workflow/story-helpers";
9
9
 
10
10
 
11
+
11
12
  export function createAstroStoryQueueTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
12
13
  const { db } = opts;
13
14
 
@@ -1,5 +1,6 @@
1
1
  // src/tools/workflow.ts
2
2
  import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ import path from "node:path";
3
4
  import type { AstrocodeConfig } from "../config/schema";
4
5
  import type { SqliteDb } from "../state/db";
5
6
  import { withTx } from "../state/db";
@@ -19,9 +20,12 @@ import { buildStageDirective, directiveHash } from "../workflow/directives";
19
20
  import { injectChatPrompt } from "../ui/inject";
20
21
  import { nowISO } from "../shared/time";
21
22
  import { newEventId } from "../state/ids";
23
+
22
24
  import { debug } from "../shared/log";
23
25
  import { createToastManager } from "../ui/toasts";
24
26
  import type { AgentConfig } from "@opencode-ai/sdk";
27
+ import { acquireRepoLock } from "../state/repo-lock";
28
+ import { workflowRepoLock } from "../state/workflow-repo-lock";
25
29
 
26
30
  // Agent name mapping for case-sensitive resolution
27
31
  export const STAGE_TO_AGENT_MAP: Record<string, string> = {
@@ -184,8 +188,19 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
184
188
  max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
185
189
  },
186
190
  execute: async ({ mode, max_steps }) => {
191
+ const repoRoot = (ctx as any).directory as string;
192
+ const lockPath = path.join(repoRoot, ".astro", "astro.lock");
187
193
  const sessionId = (ctx as any).sessionID as string | undefined;
188
- const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
194
+
195
+ return workflowRepoLock(
196
+ { acquireRepoLock },
197
+ {
198
+ lockPath,
199
+ repoRoot,
200
+ sessionId,
201
+ owner: "astro_workflow_proceed",
202
+ fn: async () => {
203
+ const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
189
204
 
190
205
  const actions: string[] = [];
191
206
  const warnings: string[] = [];
@@ -408,6 +423,8 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
408
423
  }
409
424
 
410
425
  return lines.join("\n").trim();
426
+ },
427
+ });
411
428
  },
412
429
  });
413
430
  }
package/src/ui/inject.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  // src/ui/inject.ts
2
+ import { recordInjection, recordError } from "../shared/metrics";
2
3
  type InjectionItem = {
3
4
  ctx: any;
4
5
  sessionId: string;
@@ -33,16 +34,26 @@ async function tryInjectOnce(item: InjectionItem): Promise<void> {
33
34
  const { ctx, sessionId, text, agent = "Astro" } = item;
34
35
  const prefixedText = `[${agent}]\n\n${text}`;
35
36
 
36
- const { session, prompt } = getPromptInvoker(ctx);
37
+ const injectionRecorder = recordInjection({ sessionId, attempts: item.attempts + 1, agent });
38
+ const injectionStart = injectionRecorder.start();
37
39
 
38
- // IMPORTANT: force correct `this` binding
39
- await prompt.call(session, {
40
- path: { id: sessionId },
41
- body: {
42
- parts: [{ type: "text", text: prefixedText }],
43
- agent,
44
- },
45
- });
40
+ try {
41
+ const { session, prompt } = getPromptInvoker(ctx);
42
+
43
+ // IMPORTANT: force correct `this` binding
44
+ await prompt.call(session, {
45
+ path: { id: sessionId },
46
+ body: {
47
+ parts: [{ type: "text", text: prefixedText }],
48
+ agent,
49
+ },
50
+ });
51
+
52
+ injectionRecorder.end(injectionStart, true);
53
+ } catch (error) {
54
+ injectionRecorder.end(injectionStart, false);
55
+ throw error;
56
+ }
46
57
  }
47
58
 
48
59
  async function runSessionQueue(sessionId: string) {
@@ -69,6 +80,7 @@ async function runSessionQueue(sessionId: string) {
69
80
  console.warn(
70
81
  `[Astrocode] Injection failed permanently after ${item.attempts} attempts: ${msg}`
71
82
  );
83
+ recordError("injection_failure", `Injection failed after ${item.attempts} attempts: ${msg}`);
72
84
  item.reject(err);
73
85
  continue;
74
86
  }