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.
- package/dist/index.js +6 -0
- package/dist/shared/metrics.d.ts +66 -0
- package/dist/shared/metrics.js +112 -0
- package/dist/src/agents/commands.d.ts +9 -0
- package/dist/src/agents/commands.js +121 -0
- package/dist/src/agents/prompts.d.ts +3 -0
- package/dist/src/agents/prompts.js +232 -0
- package/dist/src/agents/registry.d.ts +6 -0
- package/dist/src/agents/registry.js +242 -0
- package/dist/src/agents/types.d.ts +14 -0
- package/dist/src/agents/types.js +8 -0
- package/dist/src/config/config-handler.d.ts +4 -0
- package/dist/src/config/config-handler.js +46 -0
- package/dist/src/config/defaults.d.ts +3 -0
- package/dist/src/config/defaults.js +3 -0
- package/dist/src/config/loader.d.ts +11 -0
- package/dist/src/config/loader.js +82 -0
- package/dist/src/config/schema.d.ts +194 -0
- package/dist/src/config/schema.js +223 -0
- package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
- package/dist/src/hooks/continuation-enforcer.js +190 -0
- package/dist/src/hooks/inject-provider.d.ts +22 -0
- package/dist/src/hooks/inject-provider.js +120 -0
- package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
- package/dist/src/hooks/tool-output-truncator.js +57 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +308 -0
- package/dist/src/shared/deep-merge.d.ts +8 -0
- package/dist/src/shared/deep-merge.js +25 -0
- package/dist/src/shared/hash.d.ts +1 -0
- package/dist/src/shared/hash.js +4 -0
- package/dist/src/shared/log.d.ts +7 -0
- package/dist/src/shared/log.js +24 -0
- package/dist/src/shared/metrics.d.ts +66 -0
- package/dist/src/shared/metrics.js +112 -0
- package/dist/src/shared/model-tuning.d.ts +9 -0
- package/dist/src/shared/model-tuning.js +28 -0
- package/dist/src/shared/paths.d.ts +19 -0
- package/dist/src/shared/paths.js +64 -0
- package/dist/src/shared/text.d.ts +4 -0
- package/dist/src/shared/text.js +19 -0
- package/dist/src/shared/time.d.ts +1 -0
- package/dist/src/shared/time.js +3 -0
- package/dist/src/state/adapters/index.d.ts +41 -0
- package/dist/src/state/adapters/index.js +115 -0
- package/dist/src/state/db.d.ts +16 -0
- package/dist/src/state/db.js +225 -0
- package/dist/src/state/ids.d.ts +8 -0
- package/dist/src/state/ids.js +25 -0
- package/dist/src/state/repo-lock.d.ts +3 -0
- package/dist/src/state/repo-lock.js +29 -0
- package/dist/src/state/schema.d.ts +2 -0
- package/dist/src/state/schema.js +251 -0
- package/dist/src/state/types.d.ts +71 -0
- package/dist/src/state/types.js +1 -0
- package/dist/src/tools/artifacts.d.ts +18 -0
- package/dist/src/tools/artifacts.js +71 -0
- package/dist/src/tools/health.d.ts +8 -0
- package/dist/src/tools/health.js +119 -0
- package/dist/src/tools/index.d.ts +20 -0
- package/dist/src/tools/index.js +94 -0
- package/dist/src/tools/init.d.ts +17 -0
- package/dist/src/tools/init.js +96 -0
- package/dist/src/tools/injects.d.ts +53 -0
- package/dist/src/tools/injects.js +325 -0
- package/dist/src/tools/metrics.d.ts +7 -0
- package/dist/src/tools/metrics.js +61 -0
- package/dist/src/tools/repair.d.ts +8 -0
- package/dist/src/tools/repair.js +25 -0
- package/dist/src/tools/reset.d.ts +8 -0
- package/dist/src/tools/reset.js +92 -0
- package/dist/src/tools/run.d.ts +13 -0
- package/dist/src/tools/run.js +54 -0
- package/dist/src/tools/spec.d.ts +12 -0
- package/dist/src/tools/spec.js +44 -0
- package/dist/src/tools/stage.d.ts +23 -0
- package/dist/src/tools/stage.js +371 -0
- package/dist/src/tools/status.d.ts +8 -0
- package/dist/src/tools/status.js +125 -0
- package/dist/src/tools/story.d.ts +23 -0
- package/dist/src/tools/story.js +85 -0
- package/dist/src/tools/workflow.d.ts +13 -0
- package/dist/src/tools/workflow.js +355 -0
- package/dist/src/ui/inject.d.ts +12 -0
- package/dist/src/ui/inject.js +107 -0
- package/dist/src/ui/toasts.d.ts +13 -0
- package/dist/src/ui/toasts.js +39 -0
- package/dist/src/workflow/artifacts.d.ts +24 -0
- package/dist/src/workflow/artifacts.js +45 -0
- package/dist/src/workflow/baton.d.ts +72 -0
- package/dist/src/workflow/baton.js +166 -0
- package/dist/src/workflow/context.d.ts +20 -0
- package/dist/src/workflow/context.js +113 -0
- package/dist/src/workflow/directives.d.ts +39 -0
- package/dist/src/workflow/directives.js +137 -0
- package/dist/src/workflow/repair.d.ts +8 -0
- package/dist/src/workflow/repair.js +99 -0
- package/dist/src/workflow/state-machine.d.ts +86 -0
- package/dist/src/workflow/state-machine.js +216 -0
- package/dist/src/workflow/story-helpers.d.ts +9 -0
- package/dist/src/workflow/story-helpers.js +13 -0
- package/dist/state/db.d.ts +1 -0
- package/dist/state/db.js +9 -0
- package/dist/state/repo-lock.d.ts +3 -0
- package/dist/state/repo-lock.js +29 -0
- package/dist/test/integration/db-transactions.test.d.ts +1 -0
- package/dist/test/integration/db-transactions.test.js +126 -0
- package/dist/test/integration/injection-metrics.test.d.ts +1 -0
- package/dist/test/integration/injection-metrics.test.js +129 -0
- package/dist/tools/health.d.ts +8 -0
- package/dist/tools/health.js +119 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/metrics.d.ts +7 -0
- package/dist/tools/metrics.js +61 -0
- package/dist/tools/reset.d.ts +8 -0
- package/dist/tools/reset.js +92 -0
- package/dist/tools/workflow.js +210 -215
- package/dist/ui/inject.d.ts +6 -0
- package/dist/ui/inject.js +86 -67
- package/dist/workflow/state-machine.d.ts +32 -32
- package/dist/workflow/state-machine.js +85 -170
- package/package.json +6 -3
- package/src/index.ts +8 -0
- package/src/shared/metrics.ts +148 -0
- package/src/state/db.ts +10 -1
- package/src/state/repo-lock.ts +158 -0
- package/src/tools/health.ts +128 -0
- package/src/tools/index.ts +12 -3
- package/src/tools/init.ts +26 -14
- package/src/tools/metrics.ts +71 -0
- package/src/tools/repair.ts +21 -8
- package/src/tools/reset.ts +100 -0
- package/src/tools/stage.ts +12 -0
- package/src/tools/status.ts +17 -3
- package/src/tools/story.ts +41 -15
- package/src/tools/workflow.ts +123 -121
- package/src/ui/inject.ts +113 -79
- package/src/workflow/state-machine.ts +123 -227
- package/src/tools/workflow.ts.backup +0 -681
package/src/tools/repair.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import type { AstrocodeConfig } from "../config/schema";
|
|
3
4
|
import type { SqliteDb } from "../state/db";
|
|
4
5
|
import { withTx } from "../state/db";
|
|
5
6
|
import { repairState, formatRepairReport } from "../workflow/repair";
|
|
6
7
|
import { putArtifact } from "../workflow/artifacts";
|
|
7
8
|
import { nowISO } from "../shared/time";
|
|
9
|
+
import { withRepoLock } from "../state/repo-lock";
|
|
8
10
|
|
|
9
11
|
export function createAstroRepairTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
|
|
10
12
|
const { ctx, config, db } = opts;
|
|
@@ -16,16 +18,27 @@ export function createAstroRepairTool(opts: { ctx: any; config: AstrocodeConfig;
|
|
|
16
18
|
},
|
|
17
19
|
execute: async ({ write_report_artifact }) => {
|
|
18
20
|
const repoRoot = ctx.directory as string;
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
22
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
return withRepoLock({
|
|
25
|
+
lockPath,
|
|
26
|
+
repoRoot,
|
|
27
|
+
sessionId,
|
|
28
|
+
owner: "astro_repair",
|
|
29
|
+
fn: async () => {
|
|
30
|
+
const report = withTx(db, () => repairState(db, config));
|
|
31
|
+
const md = formatRepairReport(report);
|
|
27
32
|
|
|
28
|
-
|
|
33
|
+
if (write_report_artifact) {
|
|
34
|
+
const rel = `.astro/repair/repair_${nowISO().replace(/[:.]/g, "-")}.md`;
|
|
35
|
+
const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content: md, meta: { kind: "repair" } });
|
|
36
|
+
return md + `\n\nReport saved: ${rel} (artifact=${a.artifact_id})`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return md;
|
|
40
|
+
},
|
|
41
|
+
});
|
|
29
42
|
},
|
|
30
43
|
});
|
|
31
44
|
}
|
|
@@ -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
|
+
}
|
package/src/tools/stage.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { getAstroPaths, ensureAstroDirs, toPosix } from "../shared/paths";
|
|
|
13
13
|
import { failRun, getActiveRun, getStageRuns, startStage, completeRun } from "../workflow/state-machine";
|
|
14
14
|
import { newEventId, newId } from "../state/ids";
|
|
15
15
|
import { insertStory } from "../workflow/story-helpers";
|
|
16
|
+
import { withRepoLock } from "../state/repo-lock";
|
|
16
17
|
|
|
17
18
|
function nextStageKey(pipeline: StageKey[], current: StageKey): StageKey | null {
|
|
18
19
|
const i = pipeline.indexOf(current);
|
|
@@ -128,6 +129,15 @@ export function createAstroStageCompleteTool(opts: { ctx: any; config: Astrocode
|
|
|
128
129
|
},
|
|
129
130
|
execute: async ({ run_id, stage_key, output_text, allow_new_stories, relation_reason }) => {
|
|
130
131
|
const repoRoot = ctx.directory as string;
|
|
132
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
133
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
134
|
+
|
|
135
|
+
return withRepoLock({
|
|
136
|
+
lockPath,
|
|
137
|
+
repoRoot,
|
|
138
|
+
sessionId,
|
|
139
|
+
owner: "astro_stage_complete",
|
|
140
|
+
fn: async () => {
|
|
131
141
|
const paths = getAstroPaths(repoRoot, config.db.path);
|
|
132
142
|
ensureAstroDirs(paths);
|
|
133
143
|
|
|
@@ -391,6 +401,8 @@ Ensure JSON has required fields (stage_key, status) and valid syntax.`;
|
|
|
391
401
|
lines.push(context);
|
|
392
402
|
|
|
393
403
|
return lines.join("\n").trim();
|
|
404
|
+
},
|
|
405
|
+
});
|
|
394
406
|
},
|
|
395
407
|
});
|
|
396
408
|
}
|
package/src/tools/status.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import type { AstrocodeConfig } from "../config/schema";
|
|
3
4
|
import type { SqliteDb } from "../state/db";
|
|
4
5
|
import { decideNextAction, getActiveRun, getStageRuns, getStory } from "../workflow/state-machine";
|
|
6
|
+
import { withRepoLock } from "../state/repo-lock";
|
|
5
7
|
|
|
6
8
|
function statusIcon(status: string): string {
|
|
7
9
|
switch (status) {
|
|
@@ -36,7 +38,7 @@ function stageIcon(status: string): string {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export function createAstroStatusTool(opts: { ctx: any; config: AstrocodeConfig; db?: SqliteDb | null }): ToolDefinition {
|
|
39
|
-
const { config, db } = opts;
|
|
41
|
+
const { ctx, config, db } = opts;
|
|
40
42
|
|
|
41
43
|
return tool({
|
|
42
44
|
description: "Show a compact Astrocode status dashboard: active run/stage, pipeline, story board counts, and next action.",
|
|
@@ -55,7 +57,17 @@ export function createAstroStatusTool(opts: { ctx: any; config: AstrocodeConfig;
|
|
|
55
57
|
].join("\n");
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
const repoRoot = ctx.directory as string;
|
|
61
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
62
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
63
|
+
|
|
64
|
+
return withRepoLock({
|
|
65
|
+
lockPath,
|
|
66
|
+
repoRoot,
|
|
67
|
+
sessionId,
|
|
68
|
+
owner: "astro_status",
|
|
69
|
+
fn: async () => {
|
|
70
|
+
try {
|
|
59
71
|
const active = getActiveRun(db);
|
|
60
72
|
|
|
61
73
|
const lines: string[] = [];
|
|
@@ -129,7 +141,9 @@ export function createAstroStatusTool(opts: { ctx: any; config: AstrocodeConfig;
|
|
|
129
141
|
`⛔ Database error.`,
|
|
130
142
|
`Error: ${msg}`,
|
|
131
143
|
].join("\n");
|
|
132
|
-
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
});
|
|
133
147
|
},
|
|
134
148
|
});
|
|
135
149
|
}
|
package/src/tools/story.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
import type { AstrocodeConfig } from "../config/schema";
|
|
3
4
|
import type { SqliteDb } from "../state/db";
|
|
4
5
|
import { withTx } from "../state/db";
|
|
@@ -6,10 +7,11 @@ import { nowISO } from "../shared/time";
|
|
|
6
7
|
import type { StoryState } from "../state/types";
|
|
7
8
|
|
|
8
9
|
import { insertStory } from "../workflow/story-helpers";
|
|
10
|
+
import { withRepoLock } from "../state/repo-lock";
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
export function createAstroStoryQueueTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
|
|
12
|
-
const { db } = opts;
|
|
14
|
+
const { ctx, db } = opts;
|
|
13
15
|
|
|
14
16
|
return tool({
|
|
15
17
|
description: "Create a queued story (ticket) in Astrocode. Returns story_key.",
|
|
@@ -20,18 +22,30 @@ export function createAstroStoryQueueTool(opts: { ctx: any; config: AstrocodeCon
|
|
|
20
22
|
priority: tool.schema.number().int().default(0),
|
|
21
23
|
},
|
|
22
24
|
execute: async ({ title, body_md, epic_key, priority }) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const repoRoot = ctx.directory as string;
|
|
26
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
27
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
28
|
+
|
|
29
|
+
return withRepoLock({
|
|
30
|
+
lockPath,
|
|
31
|
+
repoRoot,
|
|
32
|
+
sessionId,
|
|
33
|
+
owner: "astro_story_queue",
|
|
34
|
+
fn: async () => {
|
|
35
|
+
const story_key = withTx(db, () => {
|
|
36
|
+
const key = insertStory(db, { title, body_md, epic_key: epic_key ?? null, priority: priority ?? 0, state: 'queued' });
|
|
37
|
+
return key;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return `✅ Queued story ${story_key}: ${title}`;
|
|
41
|
+
},
|
|
26
42
|
});
|
|
27
|
-
|
|
28
|
-
return `✅ Queued story ${story_key}: ${title}`;
|
|
29
43
|
},
|
|
30
44
|
});
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
export function createAstroStoryApproveTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
|
|
34
|
-
const { db } = opts;
|
|
48
|
+
const { ctx, db } = opts;
|
|
35
49
|
|
|
36
50
|
return tool({
|
|
37
51
|
description: "Approve a story so it becomes eligible to run.",
|
|
@@ -39,14 +53,26 @@ export function createAstroStoryApproveTool(opts: { ctx: any; config: AstrocodeC
|
|
|
39
53
|
story_key: tool.schema.string().min(1),
|
|
40
54
|
},
|
|
41
55
|
execute: async ({ story_key }) => {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
const repoRoot = ctx.directory as string;
|
|
57
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
58
|
+
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
59
|
+
|
|
60
|
+
return withRepoLock({
|
|
61
|
+
lockPath,
|
|
62
|
+
repoRoot,
|
|
63
|
+
sessionId,
|
|
64
|
+
owner: "astro_story_approve",
|
|
65
|
+
fn: async () => {
|
|
66
|
+
const now = nowISO();
|
|
67
|
+
const row = db.prepare("SELECT story_key, state, title FROM stories WHERE story_key=?").get(story_key) as any;
|
|
68
|
+
if (!row) throw new Error(`Story not found: ${story_key}`);
|
|
69
|
+
|
|
70
|
+
if (row.state === "approved") return `ℹ️ Story ${story_key} already approved.`;
|
|
71
|
+
|
|
72
|
+
db.prepare("UPDATE stories SET state='approved', approved_at=?, updated_at=? WHERE story_key=?").run(now, now, story_key);
|
|
73
|
+
return `✅ Approved story ${story_key}: ${row.title}`;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
50
76
|
},
|
|
51
77
|
});
|
|
52
78
|
}
|
package/src/tools/workflow.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
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";
|
|
6
|
+
import { withTx } from "../state/db";
|
|
5
7
|
import type { StageKey } from "../state/types";
|
|
8
|
+
import type { UiEmitEvent } from "../workflow/state-machine";
|
|
6
9
|
import { buildContextSnapshot } from "../workflow/context";
|
|
7
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
decideNextAction,
|
|
12
|
+
createRunForStory,
|
|
13
|
+
startStage,
|
|
14
|
+
completeRun,
|
|
15
|
+
failRun,
|
|
16
|
+
getActiveRun,
|
|
17
|
+
EVENT_TYPES,
|
|
18
|
+
} from "../workflow/state-machine";
|
|
8
19
|
import { buildStageDirective, directiveHash } from "../workflow/directives";
|
|
9
20
|
import { injectChatPrompt } from "../ui/inject";
|
|
10
21
|
import { nowISO } from "../shared/time";
|
|
11
22
|
import { newEventId } from "../state/ids";
|
|
23
|
+
import { withRepoLock } from "../state/repo-lock";
|
|
12
24
|
import { debug } from "../shared/log";
|
|
13
25
|
import { createToastManager } from "../ui/toasts";
|
|
14
26
|
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
27
|
+
import { acquireRepoLock } from "../state/repo-lock";
|
|
15
28
|
|
|
16
29
|
// Agent name mapping for case-sensitive resolution
|
|
17
30
|
export const STAGE_TO_AGENT_MAP: Record<string, string> = {
|
|
@@ -118,10 +131,50 @@ function buildDelegationPrompt(opts: {
|
|
|
118
131
|
].join("\n").trim();
|
|
119
132
|
|
|
120
133
|
debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
|
|
121
|
-
|
|
122
134
|
return prompt;
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
function buildUiMessage(e: UiEmitEvent): { title: string; message: string; variant: "info" | "success" | "error"; chatText: string } {
|
|
138
|
+
switch (e.kind) {
|
|
139
|
+
case "stage_started": {
|
|
140
|
+
const agent = e.agent_name ? ` (${e.agent_name})` : "";
|
|
141
|
+
const title = "Astrocode";
|
|
142
|
+
const message = `Stage started: ${e.stage_key}${agent}`;
|
|
143
|
+
const chatText = [
|
|
144
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
|
|
145
|
+
``,
|
|
146
|
+
`Run: ${e.run_id}`,
|
|
147
|
+
`Stage: ${e.stage_key}${agent}`,
|
|
148
|
+
].join("\n");
|
|
149
|
+
return { title, message, variant: "info", chatText };
|
|
150
|
+
}
|
|
151
|
+
case "run_completed": {
|
|
152
|
+
const title = "Astrocode";
|
|
153
|
+
const message = `Run completed: ${e.run_id}`;
|
|
154
|
+
const chatText = [
|
|
155
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
|
|
156
|
+
``,
|
|
157
|
+
`Run: ${e.run_id}`,
|
|
158
|
+
`Story: ${e.story_key}`,
|
|
159
|
+
].join("\n");
|
|
160
|
+
return { title, message, variant: "success", chatText };
|
|
161
|
+
}
|
|
162
|
+
case "run_failed": {
|
|
163
|
+
const title = "Astrocode";
|
|
164
|
+
const message = `Run failed: ${e.run_id} (${e.stage_key})`;
|
|
165
|
+
const chatText = [
|
|
166
|
+
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
|
|
167
|
+
``,
|
|
168
|
+
`Run: ${e.run_id}`,
|
|
169
|
+
`Story: ${e.story_key}`,
|
|
170
|
+
`Stage: ${e.stage_key}`,
|
|
171
|
+
`Error: ${e.error_text}`,
|
|
172
|
+
].join("\n");
|
|
173
|
+
return { title, message, variant: "error", chatText };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
125
178
|
export function createAstroWorkflowProceedTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb; agents?: Record<string, AgentConfig> }): ToolDefinition {
|
|
126
179
|
const { ctx, config, db, agents } = opts;
|
|
127
180
|
const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
|
|
@@ -134,13 +187,26 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
134
187
|
max_steps: tool.schema.number().int().positive().default(config.workflow.default_max_steps),
|
|
135
188
|
},
|
|
136
189
|
execute: async ({ mode, max_steps }) => {
|
|
190
|
+
const repoRoot = (ctx as any).directory as string;
|
|
191
|
+
const lockPath = path.join(repoRoot, ".astro", "astro.lock");
|
|
137
192
|
const sessionId = (ctx as any).sessionID as string | undefined;
|
|
138
|
-
|
|
193
|
+
|
|
194
|
+
return withRepoLock({
|
|
195
|
+
lockPath,
|
|
196
|
+
repoRoot,
|
|
197
|
+
sessionId,
|
|
198
|
+
owner: "astro_workflow_proceed",
|
|
199
|
+
fn: async () => {
|
|
200
|
+
const steps = Math.min(max_steps, config.workflow.loop_max_steps_hard_cap);
|
|
139
201
|
|
|
140
202
|
const actions: string[] = [];
|
|
141
203
|
const warnings: string[] = [];
|
|
142
204
|
const startedAt = nowISO();
|
|
143
205
|
|
|
206
|
+
// Collect UI events emitted inside state-machine functions, then flush AFTER tx.
|
|
207
|
+
const uiEvents: UiEmitEvent[] = [];
|
|
208
|
+
const emit = (e: UiEmitEvent) => uiEvents.push(e);
|
|
209
|
+
|
|
144
210
|
for (let i = 0; i < steps; i++) {
|
|
145
211
|
const next = decideNextAction(db, config);
|
|
146
212
|
|
|
@@ -150,60 +216,26 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
150
216
|
}
|
|
151
217
|
|
|
152
218
|
if (next.kind === "start_run") {
|
|
153
|
-
//
|
|
154
|
-
const { run_id } = createRunForStory(db, config, next.story_key);
|
|
219
|
+
// SINGLE tx boundary: caller owns tx, state-machine is pure.
|
|
220
|
+
const { run_id } = withTx(db, () => createRunForStory(db, config, next.story_key));
|
|
155
221
|
actions.push(`started run ${run_id} for story ${next.story_key}`);
|
|
156
222
|
|
|
157
|
-
if (config.ui.toasts.enabled && config.ui.toasts.show_run_started) {
|
|
158
|
-
await toasts.show({ title: "Astrocode", message: `Run started (${run_id})`, variant: "success" });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (sessionId) {
|
|
162
|
-
await injectChatPrompt({
|
|
163
|
-
ctx,
|
|
164
|
-
sessionId,
|
|
165
|
-
agent: "Astro",
|
|
166
|
-
text: [
|
|
167
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_STARTED]`,
|
|
168
|
-
``,
|
|
169
|
-
`Run started: \`${run_id}\``,
|
|
170
|
-
`Story: \`${next.story_key}\``,
|
|
171
|
-
``,
|
|
172
|
-
`Next: call **astro_workflow_proceed** again to delegate the first stage.`,
|
|
173
|
-
].join("\n"),
|
|
174
|
-
});
|
|
175
|
-
actions.push(`injected run started message for ${run_id}`);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
223
|
if (mode === "step") break;
|
|
179
224
|
continue;
|
|
180
225
|
}
|
|
181
226
|
|
|
182
227
|
if (next.kind === "complete_run") {
|
|
183
|
-
|
|
184
|
-
completeRun(db, next.run_id);
|
|
228
|
+
withTx(db, () => completeRun(db, next.run_id, emit));
|
|
185
229
|
actions.push(`completed run ${next.run_id}`);
|
|
186
230
|
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
|
|
231
|
+
if (mode === "step") break;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
190
234
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
sessionId,
|
|
196
|
-
agent: "Astro",
|
|
197
|
-
text: [
|
|
198
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_COMPLETED]`,
|
|
199
|
-
``,
|
|
200
|
-
`Run \`${next.run_id}\` completed.`,
|
|
201
|
-
``,
|
|
202
|
-
`Next: call **astro_workflow_proceed** (mode=step) to start the next approved story (if any).`,
|
|
203
|
-
].join("\n"),
|
|
204
|
-
});
|
|
205
|
-
actions.push(`injected run completed message for ${next.run_id}`);
|
|
206
|
-
}
|
|
235
|
+
if (next.kind === "failed") {
|
|
236
|
+
// Ensure DB state reflects failure in one tx; emit UI event.
|
|
237
|
+
withTx(db, () => failRun(db, next.run_id, next.stage_key, next.error_text, emit));
|
|
238
|
+
actions.push(`failed: ${next.stage_key} — ${next.error_text}`);
|
|
207
239
|
|
|
208
240
|
if (mode === "step") break;
|
|
209
241
|
continue;
|
|
@@ -241,30 +273,9 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
241
273
|
}
|
|
242
274
|
|
|
243
275
|
// NOTE: startStage owns its own tx (state-machine.ts).
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
|
|
249
|
-
await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// ✅ explicit injection on startStage (requested)
|
|
253
|
-
if (sessionId) {
|
|
254
|
-
await injectChatPrompt({
|
|
255
|
-
ctx,
|
|
256
|
-
sessionId,
|
|
257
|
-
agent: "Astro",
|
|
258
|
-
text: [
|
|
259
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — STAGE_STARTED]`,
|
|
260
|
-
``,
|
|
261
|
-
`Run: \`${active.run_id}\``,
|
|
262
|
-
`Stage: \`${next.stage_key}\``,
|
|
263
|
-
`Delegated to: \`${agentName}\``,
|
|
264
|
-
].join("\n"),
|
|
265
|
-
});
|
|
266
|
-
actions.push(`injected stage started message for ${next.stage_key}`);
|
|
267
|
-
}
|
|
276
|
+
withTx(db, () => {
|
|
277
|
+
startStage(db, active.run_id, next.stage_key, { subagent_type: agentName }, emit);
|
|
278
|
+
});
|
|
268
279
|
|
|
269
280
|
const context = buildContextSnapshot({
|
|
270
281
|
db,
|
|
@@ -292,20 +303,17 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
292
303
|
stage_agent_name: agentName,
|
|
293
304
|
});
|
|
294
305
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
} catch (e) {
|
|
305
|
-
warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
|
|
306
|
+
// Record continuation (best-effort; no tx wrapper needed but safe either way)
|
|
307
|
+
const h = directiveHash(delegatePrompt);
|
|
308
|
+
const now = nowISO();
|
|
309
|
+
if (sessionId) {
|
|
310
|
+
// This assumes continuations table exists in vNext schema.
|
|
311
|
+
db.prepare(
|
|
312
|
+
"INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'stage', ?, ?)"
|
|
313
|
+
).run(sessionId, active.run_id, h, `delegate ${next.stage_key}`, now);
|
|
306
314
|
}
|
|
307
315
|
|
|
308
|
-
// Visible injection so user can see state
|
|
316
|
+
// Visible injection so user can see state (awaited)
|
|
309
317
|
if (sessionId) {
|
|
310
318
|
await injectChatPrompt({ ctx, sessionId, text: delegatePrompt, agent: "Astro" });
|
|
311
319
|
|
|
@@ -317,7 +325,7 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
317
325
|
`When \`${agentName}\` completes, call:`,
|
|
318
326
|
`astro_stage_complete(run_id="${active.run_id}", stage_key="${next.stage_key}", output_text="[paste subagent output here]")`,
|
|
319
327
|
``,
|
|
320
|
-
`
|
|
328
|
+
`This advances the workflow.`,
|
|
321
329
|
].join("\n");
|
|
322
330
|
|
|
323
331
|
await injectChatPrompt({ ctx, sessionId, text: continueMessage, agent: "Astro" });
|
|
@@ -350,15 +358,11 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
350
358
|
context,
|
|
351
359
|
].join("\n").trim();
|
|
352
360
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
).run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
|
|
359
|
-
} catch (e) {
|
|
360
|
-
warnings.push(`continuations insert failed (non-fatal): ${String(e)}`);
|
|
361
|
-
}
|
|
361
|
+
const h = directiveHash(prompt);
|
|
362
|
+
const now = nowISO();
|
|
363
|
+
db.prepare(
|
|
364
|
+
"INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, 'continue', ?, ?)"
|
|
365
|
+
).run(sessionId, next.run_id, h, `await ${next.stage_key}`, now);
|
|
362
366
|
|
|
363
367
|
await injectChatPrompt({ ctx, sessionId, text: prompt, agent: "Astro" });
|
|
364
368
|
}
|
|
@@ -366,44 +370,40 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
366
370
|
break;
|
|
367
371
|
}
|
|
368
372
|
|
|
369
|
-
|
|
370
|
-
|
|
373
|
+
actions.push(`unhandled next action: ${(next as any).kind}`);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
371
376
|
|
|
372
|
-
|
|
377
|
+
// Flush UI events (toast + prompt) AFTER state transitions
|
|
378
|
+
if (uiEvents.length > 0) {
|
|
379
|
+
for (const e of uiEvents) {
|
|
380
|
+
const msg = buildUiMessage(e);
|
|
381
|
+
|
|
382
|
+
if (config.ui.toasts.enabled) {
|
|
383
|
+
await toasts.show({
|
|
384
|
+
title: msg.title,
|
|
385
|
+
message: msg.message,
|
|
386
|
+
variant: msg.variant,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if ((ctx as any)?.sessionID) {
|
|
373
391
|
await injectChatPrompt({
|
|
374
392
|
ctx,
|
|
375
|
-
sessionId,
|
|
393
|
+
sessionId: (ctx as any).sessionID,
|
|
394
|
+
text: msg.chatText,
|
|
376
395
|
agent: "Astro",
|
|
377
|
-
text: [
|
|
378
|
-
`[SYSTEM DIRECTIVE: ASTROCODE — RUN_FAILED]`,
|
|
379
|
-
``,
|
|
380
|
-
`Run \`${next.run_id}\` failed at stage \`${next.stage_key}\`.`,
|
|
381
|
-
`Error: ${next.error_text}`,
|
|
382
|
-
].join("\n"),
|
|
383
396
|
});
|
|
384
|
-
actions.push(`injected run failed message for ${next.run_id}`);
|
|
385
397
|
}
|
|
386
|
-
|
|
387
|
-
break;
|
|
388
398
|
}
|
|
389
399
|
|
|
390
|
-
actions.push(`
|
|
391
|
-
break;
|
|
400
|
+
actions.push(`ui: flushed ${uiEvents.length} event(s)`);
|
|
392
401
|
}
|
|
393
402
|
|
|
394
|
-
// Housekeeping event
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
).run(
|
|
399
|
-
newEventId(),
|
|
400
|
-
EVENT_TYPES.WORKFLOW_PROCEED,
|
|
401
|
-
JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }),
|
|
402
|
-
nowISO()
|
|
403
|
-
);
|
|
404
|
-
} catch (e) {
|
|
405
|
-
warnings.push(`workflow.proceed event insert failed (non-fatal): ${String(e)}`);
|
|
406
|
-
}
|
|
403
|
+
// Housekeeping event
|
|
404
|
+
db.prepare(
|
|
405
|
+
"INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)"
|
|
406
|
+
).run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
|
|
407
407
|
|
|
408
408
|
const active = getActiveRun(db);
|
|
409
409
|
const lines: string[] = [];
|
|
@@ -420,6 +420,8 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
|
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
return lines.join("\n").trim();
|
|
423
|
+
},
|
|
424
|
+
});
|
|
423
425
|
},
|
|
424
426
|
});
|
|
425
427
|
}
|