astrocode-workflow 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/README.md +85 -0
- package/dist/agents/commands.d.ts +9 -0
- package/dist/agents/commands.js +121 -0
- package/dist/agents/prompts.d.ts +2 -0
- package/dist/agents/prompts.js +27 -0
- package/dist/agents/registry.d.ts +6 -0
- package/dist/agents/registry.js +223 -0
- package/dist/agents/types.d.ts +14 -0
- package/dist/agents/types.js +8 -0
- package/dist/config/config-handler.d.ts +4 -0
- package/dist/config/config-handler.js +46 -0
- package/dist/config/defaults.d.ts +3 -0
- package/dist/config/defaults.js +3 -0
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.js +48 -0
- package/dist/config/schema.d.ts +176 -0
- package/dist/config/schema.js +198 -0
- package/dist/hooks/continuation-enforcer.d.ts +26 -0
- package/dist/hooks/continuation-enforcer.js +166 -0
- package/dist/hooks/tool-output-truncator.d.ts +17 -0
- package/dist/hooks/tool-output-truncator.js +56 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +108 -0
- package/dist/shared/deep-merge.d.ts +8 -0
- package/dist/shared/deep-merge.js +25 -0
- package/dist/shared/hash.d.ts +1 -0
- package/dist/shared/hash.js +4 -0
- package/dist/shared/log.d.ts +7 -0
- package/dist/shared/log.js +24 -0
- package/dist/shared/model-tuning.d.ts +9 -0
- package/dist/shared/model-tuning.js +28 -0
- package/dist/shared/paths.d.ts +19 -0
- package/dist/shared/paths.js +51 -0
- package/dist/shared/text.d.ts +4 -0
- package/dist/shared/text.js +19 -0
- package/dist/shared/time.d.ts +1 -0
- package/dist/shared/time.js +3 -0
- package/dist/state/adapters/index.d.ts +39 -0
- package/dist/state/adapters/index.js +119 -0
- package/dist/state/db.d.ts +17 -0
- package/dist/state/db.js +83 -0
- package/dist/state/ids.d.ts +8 -0
- package/dist/state/ids.js +25 -0
- package/dist/state/schema.d.ts +2 -0
- package/dist/state/schema.js +247 -0
- package/dist/state/types.d.ts +70 -0
- package/dist/state/types.js +1 -0
- package/dist/tools/artifacts.d.ts +18 -0
- package/dist/tools/artifacts.js +71 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +100 -0
- package/dist/tools/init.d.ts +8 -0
- package/dist/tools/init.js +41 -0
- package/dist/tools/injects.d.ts +23 -0
- package/dist/tools/injects.js +99 -0
- package/dist/tools/repair.d.ts +8 -0
- package/dist/tools/repair.js +25 -0
- package/dist/tools/run.d.ts +13 -0
- package/dist/tools/run.js +54 -0
- package/dist/tools/spec.d.ts +13 -0
- package/dist/tools/spec.js +41 -0
- package/dist/tools/stage.d.ts +23 -0
- package/dist/tools/stage.js +284 -0
- package/dist/tools/status.d.ts +8 -0
- package/dist/tools/status.js +107 -0
- package/dist/tools/story.d.ts +23 -0
- package/dist/tools/story.js +85 -0
- package/dist/tools/workflow.d.ts +8 -0
- package/dist/tools/workflow.js +197 -0
- package/dist/ui/inject.d.ts +5 -0
- package/dist/ui/inject.js +9 -0
- package/dist/ui/toasts.d.ts +13 -0
- package/dist/ui/toasts.js +39 -0
- package/dist/workflow/artifacts.d.ts +24 -0
- package/dist/workflow/artifacts.js +45 -0
- package/dist/workflow/baton.d.ts +66 -0
- package/dist/workflow/baton.js +101 -0
- package/dist/workflow/context.d.ts +12 -0
- package/dist/workflow/context.js +67 -0
- package/dist/workflow/directives.d.ts +37 -0
- package/dist/workflow/directives.js +111 -0
- package/dist/workflow/repair.d.ts +8 -0
- package/dist/workflow/repair.js +99 -0
- package/dist/workflow/state-machine.d.ts +43 -0
- package/dist/workflow/state-machine.js +127 -0
- package/dist/workflow/story-helpers.d.ts +9 -0
- package/dist/workflow/story-helpers.js +13 -0
- package/package.json +32 -0
- package/src/agents/commands.ts +137 -0
- package/src/agents/prompts.ts +28 -0
- package/src/agents/registry.ts +310 -0
- package/src/agents/types.ts +31 -0
- package/src/config/config-handler.ts +48 -0
- package/src/config/defaults.ts +4 -0
- package/src/config/loader.ts +55 -0
- package/src/config/schema.ts +236 -0
- package/src/hooks/continuation-enforcer.ts +217 -0
- package/src/hooks/tool-output-truncator.ts +82 -0
- package/src/index.ts +131 -0
- package/src/shared/deep-merge.ts +28 -0
- package/src/shared/hash.ts +5 -0
- package/src/shared/log.ts +30 -0
- package/src/shared/model-tuning.ts +48 -0
- package/src/shared/paths.ts +70 -0
- package/src/shared/text.ts +20 -0
- package/src/shared/time.ts +3 -0
- package/src/shims.node.d.ts +20 -0
- package/src/state/adapters/index.ts +155 -0
- package/src/state/db.ts +105 -0
- package/src/state/ids.ts +33 -0
- package/src/state/schema.ts +249 -0
- package/src/state/types.ts +76 -0
- package/src/tools/artifacts.ts +83 -0
- package/src/tools/index.ts +111 -0
- package/src/tools/init.ts +50 -0
- package/src/tools/injects.ts +108 -0
- package/src/tools/repair.ts +31 -0
- package/src/tools/run.ts +62 -0
- package/src/tools/spec.ts +50 -0
- package/src/tools/stage.ts +361 -0
- package/src/tools/status.ts +119 -0
- package/src/tools/story.ts +106 -0
- package/src/tools/workflow.ts +241 -0
- package/src/ui/inject.ts +13 -0
- package/src/ui/toasts.ts +48 -0
- package/src/workflow/artifacts.ts +69 -0
- package/src/workflow/baton.ts +141 -0
- package/src/workflow/context.ts +86 -0
- package/src/workflow/directives.ts +170 -0
- package/src/workflow/repair.ts +138 -0
- package/src/workflow/state-machine.ts +194 -0
- package/src/workflow/story-helpers.ts +18 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { nowISO } from "../shared/time";
|
|
3
|
+
import { sha256Hex } from "../shared/hash";
|
|
4
|
+
function newInjectId() {
|
|
5
|
+
return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
6
|
+
}
|
|
7
|
+
export function createAstroInjectPutTool(opts) {
|
|
8
|
+
const { db } = opts;
|
|
9
|
+
return tool({
|
|
10
|
+
description: "Create/update an inject (note/policy) stored in the DB. Useful for persistent rules.",
|
|
11
|
+
args: {
|
|
12
|
+
inject_id: tool.schema.string().optional(),
|
|
13
|
+
type: tool.schema.string().default("note"),
|
|
14
|
+
title: tool.schema.string().min(1),
|
|
15
|
+
body_md: tool.schema.string().min(1),
|
|
16
|
+
tags_json: tool.schema.string().default("[]"),
|
|
17
|
+
scope: tool.schema.string().default("repo"),
|
|
18
|
+
source: tool.schema.string().default("user"),
|
|
19
|
+
priority: tool.schema.number().int().default(50),
|
|
20
|
+
expires_at: tool.schema.string().nullable().optional(),
|
|
21
|
+
},
|
|
22
|
+
execute: async ({ inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at }) => {
|
|
23
|
+
const id = inject_id ?? newInjectId();
|
|
24
|
+
const now = nowISO();
|
|
25
|
+
const sha = sha256Hex(body_md);
|
|
26
|
+
const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id);
|
|
27
|
+
if (existing) {
|
|
28
|
+
db.prepare("UPDATE injects SET type=?, title=?, body_md=?, tags_json=?, scope=?, source=?, priority=?, expires_at=?, sha256=?, updated_at=? WHERE inject_id=?").run(type, title, body_md, tags_json, scope, source, priority, expires_at ?? null, sha, now, id);
|
|
29
|
+
return `✅ Updated inject ${id}: ${title}`;
|
|
30
|
+
}
|
|
31
|
+
db.prepare("INSERT INTO injects (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(id, type, title, body_md, tags_json, scope, source, priority, expires_at ?? null, sha, now, now);
|
|
32
|
+
return `✅ Created inject ${id}: ${title}`;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function createAstroInjectListTool(opts) {
|
|
37
|
+
const { db } = opts;
|
|
38
|
+
return tool({
|
|
39
|
+
description: "List injects (optionally filtered by scope/type).",
|
|
40
|
+
args: {
|
|
41
|
+
scope: tool.schema.string().optional(),
|
|
42
|
+
type: tool.schema.string().optional(),
|
|
43
|
+
limit: tool.schema.number().int().positive().default(50),
|
|
44
|
+
},
|
|
45
|
+
execute: async ({ scope, type, limit }) => {
|
|
46
|
+
const where = [];
|
|
47
|
+
const params = [];
|
|
48
|
+
if (scope) {
|
|
49
|
+
where.push("scope = ?");
|
|
50
|
+
params.push(scope);
|
|
51
|
+
}
|
|
52
|
+
if (type) {
|
|
53
|
+
where.push("type = ?");
|
|
54
|
+
params.push(type);
|
|
55
|
+
}
|
|
56
|
+
const sql = `SELECT inject_id, type, title, scope, priority, created_at, updated_at FROM injects ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY priority DESC, updated_at DESC LIMIT ?`;
|
|
57
|
+
const rows = db.prepare(sql).all(...params, limit);
|
|
58
|
+
return JSON.stringify(rows, null, 2);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function createAstroInjectGetTool(opts) {
|
|
63
|
+
const { db } = opts;
|
|
64
|
+
return tool({
|
|
65
|
+
description: "Get an inject by id (full body).",
|
|
66
|
+
args: {
|
|
67
|
+
inject_id: tool.schema.string().min(1),
|
|
68
|
+
},
|
|
69
|
+
execute: async ({ inject_id }) => {
|
|
70
|
+
const row = db.prepare("SELECT * FROM injects WHERE inject_id=?").get(inject_id);
|
|
71
|
+
if (!row)
|
|
72
|
+
throw new Error(`Inject not found: ${inject_id}`);
|
|
73
|
+
return JSON.stringify(row, null, 2);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export function createAstroInjectSearchTool(opts) {
|
|
78
|
+
const { db } = opts;
|
|
79
|
+
return tool({
|
|
80
|
+
description: "Search injects by query substring over title/body/tags. Returns matches ordered by priority/recency.",
|
|
81
|
+
args: {
|
|
82
|
+
q: tool.schema.string().min(1),
|
|
83
|
+
scope: tool.schema.string().optional(),
|
|
84
|
+
limit: tool.schema.number().int().positive().default(20),
|
|
85
|
+
},
|
|
86
|
+
execute: async ({ q, scope, limit }) => {
|
|
87
|
+
const like = `%${q}%`;
|
|
88
|
+
const where = ["(title LIKE ? OR body_md LIKE ? OR tags_json LIKE ?)"];
|
|
89
|
+
const params = [like, like, like];
|
|
90
|
+
if (scope) {
|
|
91
|
+
where.push("scope = ?");
|
|
92
|
+
params.push(scope);
|
|
93
|
+
}
|
|
94
|
+
const sql = `SELECT inject_id, type, title, scope, priority, updated_at FROM injects WHERE ${where.join(" AND ")} ORDER BY priority DESC, updated_at DESC LIMIT ?`;
|
|
95
|
+
const rows = db.prepare(sql).all(...params, limit);
|
|
96
|
+
return JSON.stringify(rows, null, 2);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import type { AstrocodeConfig } from "../config/schema";
|
|
3
|
+
import type { SqliteDb } from "../state/db";
|
|
4
|
+
export declare function createAstroRepairTool(opts: {
|
|
5
|
+
ctx: any;
|
|
6
|
+
config: AstrocodeConfig;
|
|
7
|
+
db: SqliteDb;
|
|
8
|
+
}): ToolDefinition;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { withTx } from "../state/db";
|
|
3
|
+
import { repairState, formatRepairReport } from "../workflow/repair";
|
|
4
|
+
import { putArtifact } from "../workflow/artifacts";
|
|
5
|
+
import { nowISO } from "../shared/time";
|
|
6
|
+
export function createAstroRepairTool(opts) {
|
|
7
|
+
const { ctx, config, db } = opts;
|
|
8
|
+
return tool({
|
|
9
|
+
description: "Repair Astrocode invariants and recover from inconsistent DB state. Writes a repair report artifact.",
|
|
10
|
+
args: {
|
|
11
|
+
write_report_artifact: tool.schema.boolean().default(true),
|
|
12
|
+
},
|
|
13
|
+
execute: async ({ write_report_artifact }) => {
|
|
14
|
+
const repoRoot = ctx.directory;
|
|
15
|
+
const report = withTx(db, () => repairState(db, config));
|
|
16
|
+
const md = formatRepairReport(report);
|
|
17
|
+
if (write_report_artifact) {
|
|
18
|
+
const rel = `.astro/repair/repair_${nowISO().replace(/[:.]/g, "-")}.md`;
|
|
19
|
+
const a = putArtifact({ repoRoot, db, run_id: null, stage_key: null, type: "log", rel_path: rel, content: md, meta: { kind: "repair" } });
|
|
20
|
+
return md + `\n\nReport saved: ${rel} (artifact=${a.artifact_id})`;
|
|
21
|
+
}
|
|
22
|
+
return md;
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import type { AstrocodeConfig } from "../config/schema";
|
|
3
|
+
import type { SqliteDb } from "../state/db";
|
|
4
|
+
export declare function createAstroRunGetTool(opts: {
|
|
5
|
+
ctx: any;
|
|
6
|
+
config: AstrocodeConfig;
|
|
7
|
+
db: SqliteDb;
|
|
8
|
+
}): ToolDefinition;
|
|
9
|
+
export declare function createAstroRunAbortTool(opts: {
|
|
10
|
+
ctx: any;
|
|
11
|
+
config: AstrocodeConfig;
|
|
12
|
+
db: SqliteDb;
|
|
13
|
+
}): ToolDefinition;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { abortRun, getActiveRun, getStageRuns, getStory } from "../workflow/state-machine";
|
|
3
|
+
export function createAstroRunGetTool(opts) {
|
|
4
|
+
const { db } = opts;
|
|
5
|
+
return tool({
|
|
6
|
+
description: "Get run details (and stage run statuses). Defaults to active run if run_id omitted.",
|
|
7
|
+
args: {
|
|
8
|
+
run_id: tool.schema.string().optional(),
|
|
9
|
+
include_stage_summaries: tool.schema.boolean().default(false),
|
|
10
|
+
},
|
|
11
|
+
execute: async ({ run_id, include_stage_summaries }) => {
|
|
12
|
+
const active = getActiveRun(db);
|
|
13
|
+
const rid = run_id ?? active?.run_id;
|
|
14
|
+
if (!rid)
|
|
15
|
+
return "No active run.";
|
|
16
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(rid);
|
|
17
|
+
if (!run)
|
|
18
|
+
throw new Error(`Run not found: ${rid}`);
|
|
19
|
+
const story = getStory(db, run.story_key);
|
|
20
|
+
const stages = getStageRuns(db, rid);
|
|
21
|
+
const lines = [];
|
|
22
|
+
lines.push(`# Run ${rid}`);
|
|
23
|
+
lines.push(`- Status: **${run.status}**`);
|
|
24
|
+
lines.push(`- Story: \`${run.story_key}\` — ${story?.title ?? "(missing)"}`);
|
|
25
|
+
lines.push(`- Current stage: \`${run.current_stage_key ?? "?"}\``);
|
|
26
|
+
lines.push("", "## Stages");
|
|
27
|
+
for (const s of stages) {
|
|
28
|
+
lines.push(`- \`${s.stage_key}\` (${s.status})`);
|
|
29
|
+
if (include_stage_summaries && s.summary_md) {
|
|
30
|
+
lines.push(` - summary: ${s.summary_md.split("\n")[0].slice(0, 120)}${s.summary_md.length > 120 ? "…" : ""}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return lines.join("\n").trim();
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export function createAstroRunAbortTool(opts) {
|
|
38
|
+
const { db } = opts;
|
|
39
|
+
return tool({
|
|
40
|
+
description: "Abort a run and unlock its story (returns story to approved). Defaults to active run if run_id omitted.",
|
|
41
|
+
args: {
|
|
42
|
+
run_id: tool.schema.string().optional(),
|
|
43
|
+
reason: tool.schema.string().default("aborted by user"),
|
|
44
|
+
},
|
|
45
|
+
execute: async ({ run_id, reason }) => {
|
|
46
|
+
const active = getActiveRun(db);
|
|
47
|
+
const rid = run_id ?? active?.run_id;
|
|
48
|
+
if (!rid)
|
|
49
|
+
return "No active run to abort.";
|
|
50
|
+
abortRun(db, rid, reason);
|
|
51
|
+
return `🛑 Aborted run ${rid}. Reason: ${reason}`;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import type { AstrocodeConfig } from "../config/schema";
|
|
3
|
+
import type { SqliteDb } from "../state/db";
|
|
4
|
+
export declare function createAstroSpecGetTool(opts: {
|
|
5
|
+
ctx: any;
|
|
6
|
+
config: AstrocodeConfig;
|
|
7
|
+
db: SqliteDb;
|
|
8
|
+
}): ToolDefinition;
|
|
9
|
+
export declare function createAstroSpecSetTool(opts: {
|
|
10
|
+
ctx: any;
|
|
11
|
+
config: AstrocodeConfig;
|
|
12
|
+
db: SqliteDb;
|
|
13
|
+
}): ToolDefinition;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
4
|
+
import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
|
|
5
|
+
import { nowISO } from "../shared/time";
|
|
6
|
+
import { sha256Hex } from "../shared/hash";
|
|
7
|
+
export function createAstroSpecGetTool(opts) {
|
|
8
|
+
const { ctx, config, db } = opts;
|
|
9
|
+
return tool({
|
|
10
|
+
description: "Get current project spec stored at .astro/spec.md",
|
|
11
|
+
args: {},
|
|
12
|
+
execute: async () => {
|
|
13
|
+
const repoRoot = ctx.directory;
|
|
14
|
+
const paths = getAstroPaths(repoRoot, config.db.path);
|
|
15
|
+
ensureAstroDirs(paths);
|
|
16
|
+
if (!fs.existsSync(paths.specPath))
|
|
17
|
+
return "No spec found at .astro/spec.md (run astro_init or astro_spec_set).";
|
|
18
|
+
const md = fs.readFileSync(paths.specPath, "utf-8");
|
|
19
|
+
return md;
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function createAstroSpecSetTool(opts) {
|
|
24
|
+
const { ctx, config, db } = opts;
|
|
25
|
+
return tool({
|
|
26
|
+
description: "Set/replace the project spec at .astro/spec.md and record its hash in the DB.",
|
|
27
|
+
args: {
|
|
28
|
+
spec_md: tool.schema.string().min(1),
|
|
29
|
+
},
|
|
30
|
+
execute: async ({ spec_md }) => {
|
|
31
|
+
const repoRoot = ctx.directory;
|
|
32
|
+
const paths = getAstroPaths(repoRoot, config.db.path);
|
|
33
|
+
ensureAstroDirs(paths);
|
|
34
|
+
fs.writeFileSync(paths.specPath, spec_md);
|
|
35
|
+
const h = sha256Hex(spec_md);
|
|
36
|
+
const now = nowISO();
|
|
37
|
+
db.prepare("UPDATE repo_state SET spec_hash_after=?, updated_at=? WHERE id=1").run(h, now);
|
|
38
|
+
return `✅ Spec updated (${path.relative(repoRoot, paths.specPath)}). sha256=${h}`;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import type { AstrocodeConfig } from "../config/schema";
|
|
3
|
+
import type { SqliteDb } from "../state/db";
|
|
4
|
+
export declare function createAstroStageStartTool(opts: {
|
|
5
|
+
ctx: any;
|
|
6
|
+
config: AstrocodeConfig;
|
|
7
|
+
db: SqliteDb;
|
|
8
|
+
}): ToolDefinition;
|
|
9
|
+
export declare function createAstroStageCompleteTool(opts: {
|
|
10
|
+
ctx: any;
|
|
11
|
+
config: AstrocodeConfig;
|
|
12
|
+
db: SqliteDb;
|
|
13
|
+
}): ToolDefinition;
|
|
14
|
+
export declare function createAstroStageFailTool(opts: {
|
|
15
|
+
ctx: any;
|
|
16
|
+
config: AstrocodeConfig;
|
|
17
|
+
db: SqliteDb;
|
|
18
|
+
}): ToolDefinition;
|
|
19
|
+
export declare function createAstroStageResetTool(opts: {
|
|
20
|
+
ctx: any;
|
|
21
|
+
config: AstrocodeConfig;
|
|
22
|
+
db: SqliteDb;
|
|
23
|
+
}): ToolDefinition;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin/tool";
|
|
3
|
+
import { withTx } from "../state/db";
|
|
4
|
+
import { buildBatonSummary, parseStageOutputText } from "../workflow/baton";
|
|
5
|
+
import { buildContextSnapshot } from "../workflow/context";
|
|
6
|
+
import { putArtifact } from "../workflow/artifacts";
|
|
7
|
+
import { nowISO } from "../shared/time";
|
|
8
|
+
import { getAstroPaths, ensureAstroDirs, toPosix } from "../shared/paths";
|
|
9
|
+
import { failRun, getActiveRun, getStageRuns, startStage, completeRun } from "../workflow/state-machine";
|
|
10
|
+
import { newEventId } from "../state/ids";
|
|
11
|
+
import { insertStory } from "../workflow/story-helpers";
|
|
12
|
+
function nextStageKey(pipeline, current) {
|
|
13
|
+
const i = pipeline.indexOf(current);
|
|
14
|
+
if (i === -1)
|
|
15
|
+
return null;
|
|
16
|
+
return pipeline[i + 1] ?? null;
|
|
17
|
+
}
|
|
18
|
+
function ensureStageMatches(run, stage_key) {
|
|
19
|
+
if (run.current_stage_key && run.current_stage_key !== stage_key) {
|
|
20
|
+
throw new Error(`Stage mismatch: run.current_stage_key=${run.current_stage_key} but got stage_key=${stage_key}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function createAstroStageStartTool(opts) {
|
|
24
|
+
const { config, db } = opts;
|
|
25
|
+
return tool({
|
|
26
|
+
description: "Start a stage for a run (sets stage_run.status=running). Usually called by astro_workflow_proceed.",
|
|
27
|
+
args: {
|
|
28
|
+
run_id: tool.schema.string().optional(),
|
|
29
|
+
stage_key: tool.schema.enum(["frame", "plan", "spec", "implement", "review", "verify", "close"]).optional(),
|
|
30
|
+
subagent_type: tool.schema.string().optional(),
|
|
31
|
+
subagent_session_id: tool.schema.string().optional(),
|
|
32
|
+
},
|
|
33
|
+
execute: async ({ run_id, stage_key, subagent_type, subagent_session_id }) => {
|
|
34
|
+
const active = getActiveRun(db);
|
|
35
|
+
const rid = run_id ?? active?.run_id;
|
|
36
|
+
if (!rid)
|
|
37
|
+
throw new Error("No active run and no run_id provided.");
|
|
38
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(rid);
|
|
39
|
+
if (!run)
|
|
40
|
+
throw new Error(`Run not found: ${rid}`);
|
|
41
|
+
const sk = (stage_key ?? run.current_stage_key);
|
|
42
|
+
if (!sk)
|
|
43
|
+
throw new Error("No stage_key provided and run.current_stage_key is null.");
|
|
44
|
+
startStage(db, rid, sk, { subagent_type, subagent_session_id });
|
|
45
|
+
return `🟦 Started stage ${sk} for run ${rid}`;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function createAstroStageCompleteTool(opts) {
|
|
50
|
+
const { ctx, config, db } = opts;
|
|
51
|
+
return tool({
|
|
52
|
+
description: "Complete a stage from stage-agent output text. Writes baton artifacts, updates stage_runs, advances pipeline, and can auto-queue split stories.",
|
|
53
|
+
args: {
|
|
54
|
+
run_id: tool.schema.string().optional(),
|
|
55
|
+
stage_key: tool.schema.enum(["frame", "plan", "spec", "implement", "review", "verify", "close"]).optional(),
|
|
56
|
+
// Pass FULL stage-agent message text. This tool parses baton + ASTRO JSON markers.
|
|
57
|
+
output_text: tool.schema.string().min(1),
|
|
58
|
+
allow_new_stories: tool.schema.boolean().default(true),
|
|
59
|
+
relation_reason: tool.schema.string().default("split from stage output"),
|
|
60
|
+
},
|
|
61
|
+
execute: async ({ run_id, stage_key, output_text, allow_new_stories, relation_reason }) => {
|
|
62
|
+
const repoRoot = ctx.directory;
|
|
63
|
+
const paths = getAstroPaths(repoRoot, config.db.path);
|
|
64
|
+
ensureAstroDirs(paths);
|
|
65
|
+
const active = getActiveRun(db);
|
|
66
|
+
const rid = run_id ?? active?.run_id;
|
|
67
|
+
if (!rid)
|
|
68
|
+
throw new Error("No active run and no run_id provided.");
|
|
69
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(rid);
|
|
70
|
+
if (!run)
|
|
71
|
+
throw new Error(`Run not found: ${rid}`);
|
|
72
|
+
const sk = (stage_key ?? run.current_stage_key);
|
|
73
|
+
if (!sk)
|
|
74
|
+
throw new Error("No stage_key provided and run.current_stage_key is null.");
|
|
75
|
+
const stageRow = db.prepare("SELECT * FROM stage_runs WHERE run_id=? AND stage_key=?").get(rid, sk);
|
|
76
|
+
if (!stageRow)
|
|
77
|
+
throw new Error(`Stage run not found: ${rid}/${sk}`);
|
|
78
|
+
if (stageRow.status !== "running") {
|
|
79
|
+
throw new Error(`Stage ${sk} is not running (status=${stageRow.status}). Start it first.`);
|
|
80
|
+
}
|
|
81
|
+
const parsed = parseStageOutputText(output_text);
|
|
82
|
+
if (parsed.error || !parsed.astro_json)
|
|
83
|
+
throw new Error(parsed.error ?? "ASTRO JSON missing");
|
|
84
|
+
if (parsed.astro_json.stage_key !== sk) {
|
|
85
|
+
throw new Error(`ASTRO JSON stage_key mismatch: expected ${sk}, got ${parsed.astro_json.stage_key}`);
|
|
86
|
+
}
|
|
87
|
+
// Evidence requirement
|
|
88
|
+
const evidenceRequired = (sk === "verify" && config.workflow.evidence_required.verify) ||
|
|
89
|
+
(sk === "implement" && config.workflow.evidence_required.implement);
|
|
90
|
+
if (evidenceRequired && (parsed.astro_json.evidence ?? []).length === 0) {
|
|
91
|
+
throw new Error(`Evidence is required for stage ${sk} but ASTRO JSON evidence[] is empty.`);
|
|
92
|
+
}
|
|
93
|
+
const batonSummary = buildBatonSummary({ config, stage_key: sk, astro_json: parsed.astro_json, baton_md: parsed.baton_md });
|
|
94
|
+
const now = nowISO();
|
|
95
|
+
const pipeline = JSON.parse(run.pipeline_stages_json ?? "[]");
|
|
96
|
+
const next = nextStageKey(pipeline, sk);
|
|
97
|
+
const stageDirRel = toPosix(path.join(".astro", "runs", rid, sk));
|
|
98
|
+
const batonRel = toPosix(path.join(stageDirRel, config.artifacts.baton_filename));
|
|
99
|
+
const summaryRel = toPosix(path.join(stageDirRel, config.artifacts.baton_summary_filename));
|
|
100
|
+
const jsonRel = toPosix(path.join(stageDirRel, config.artifacts.baton_json_filename));
|
|
101
|
+
const created = [];
|
|
102
|
+
const result = withTx(db, () => {
|
|
103
|
+
// Persist artifacts
|
|
104
|
+
if (config.artifacts.write_full_baton_md) {
|
|
105
|
+
const a = putArtifact({
|
|
106
|
+
repoRoot,
|
|
107
|
+
db,
|
|
108
|
+
run_id: rid,
|
|
109
|
+
stage_key: sk,
|
|
110
|
+
type: "baton",
|
|
111
|
+
rel_path: batonRel,
|
|
112
|
+
content: parsed.baton_md,
|
|
113
|
+
meta: { stage_key: sk },
|
|
114
|
+
});
|
|
115
|
+
created.push({ artifact_id: a.artifact_id, path: batonRel });
|
|
116
|
+
}
|
|
117
|
+
if (config.artifacts.write_baton_summary_md) {
|
|
118
|
+
const a = putArtifact({
|
|
119
|
+
repoRoot,
|
|
120
|
+
db,
|
|
121
|
+
run_id: rid,
|
|
122
|
+
stage_key: sk,
|
|
123
|
+
type: "summary",
|
|
124
|
+
rel_path: summaryRel,
|
|
125
|
+
content: batonSummary,
|
|
126
|
+
meta: { stage_key: sk },
|
|
127
|
+
});
|
|
128
|
+
created.push({ artifact_id: a.artifact_id, path: summaryRel });
|
|
129
|
+
}
|
|
130
|
+
if (config.artifacts.write_baton_output_json) {
|
|
131
|
+
const a = putArtifact({
|
|
132
|
+
repoRoot,
|
|
133
|
+
db,
|
|
134
|
+
run_id: rid,
|
|
135
|
+
stage_key: sk,
|
|
136
|
+
type: "baton",
|
|
137
|
+
rel_path: jsonRel,
|
|
138
|
+
content: JSON.stringify(parsed.astro_json, null, 2),
|
|
139
|
+
meta: { stage_key: sk, schema_version: parsed.astro_json.schema_version },
|
|
140
|
+
});
|
|
141
|
+
created.push({ artifact_id: a.artifact_id, path: jsonRel });
|
|
142
|
+
}
|
|
143
|
+
// Update stage_runs row
|
|
144
|
+
db.prepare("UPDATE stage_runs SET status=?, completed_at=?, updated_at=?, baton_path=?, summary_md=?, output_json=?, error_text=NULL WHERE stage_run_id=?").run(parsed.astro_json.status === "ok" ? "completed" : "failed", now, now, batonRel, batonSummary, parsed.astro_json_raw ?? JSON.stringify(parsed.astro_json), stageRow.stage_run_id);
|
|
145
|
+
// stage event
|
|
146
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(newEventId(), rid, sk, parsed.astro_json.status === "ok" ? "stage.completed" : parsed.astro_json.status === "blocked" ? "stage.blocked" : "stage.failed", JSON.stringify({ artifacts: created, metrics: parsed.astro_json.metrics ?? {} }), now);
|
|
147
|
+
// Metrics
|
|
148
|
+
if (parsed.astro_json.metrics && typeof parsed.astro_json.metrics === "object") {
|
|
149
|
+
const ins = db.prepare("INSERT INTO workflow_metrics (metric_id, run_id, stage_key, name, value_num, value_text, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
150
|
+
for (const [k, v] of Object.entries(parsed.astro_json.metrics)) {
|
|
151
|
+
const metricId = `metric_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
152
|
+
if (typeof v === "number")
|
|
153
|
+
ins.run(metricId, rid, sk, k, v, null, now);
|
|
154
|
+
else
|
|
155
|
+
ins.run(metricId, rid, sk, k, null, String(v), now);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// New stories (split)
|
|
159
|
+
const newStoryKeys = [];
|
|
160
|
+
if (allow_new_stories && parsed.astro_json.new_stories?.length) {
|
|
161
|
+
for (const ns of parsed.astro_json.new_stories) {
|
|
162
|
+
const key = insertStory(db, { title: ns.title, body_md: ns.body_md ?? "", priority: ns.priority ?? 0, state: "queued" });
|
|
163
|
+
newStoryKeys.push(key);
|
|
164
|
+
db.prepare("INSERT OR IGNORE INTO story_relations (parent_story_key, child_story_key, relation_type, reason, created_at) VALUES (?, ?, 'split', ?, ?)").run(run.story_key, key, relation_reason, now);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (parsed.astro_json.status !== "ok") {
|
|
168
|
+
const err = parsed.astro_json.status === "blocked"
|
|
169
|
+
? `blocked: ${(parsed.astro_json.questions?.[0] ?? "needs input")}`
|
|
170
|
+
: `failed: ${(parsed.astro_json.summary ?? "stage failed")}`;
|
|
171
|
+
// Mark run failed (also unlocks story)
|
|
172
|
+
failRun(db, rid, sk, err);
|
|
173
|
+
return { ok: false, next_stage: null, new_stories: newStoryKeys, error: err };
|
|
174
|
+
}
|
|
175
|
+
// Advance run.current_stage_key
|
|
176
|
+
if (next) {
|
|
177
|
+
db.prepare("UPDATE runs SET current_stage_key=?, updated_at=? WHERE run_id=?").run(next, now, rid);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
db.prepare("UPDATE runs SET current_stage_key=NULL, updated_at=? WHERE run_id=?").run(now, rid);
|
|
181
|
+
}
|
|
182
|
+
// If last stage, complete run
|
|
183
|
+
const stageRuns = getStageRuns(db, rid);
|
|
184
|
+
const incomplete = stageRuns.find((s) => s.status !== "completed" && s.status !== "skipped");
|
|
185
|
+
if (!incomplete) {
|
|
186
|
+
completeRun(db, rid);
|
|
187
|
+
return { ok: true, next_stage: null, new_stories: newStoryKeys, completed_run: true };
|
|
188
|
+
}
|
|
189
|
+
return { ok: true, next_stage: next, new_stories: newStoryKeys, completed_run: false };
|
|
190
|
+
});
|
|
191
|
+
const context = buildContextSnapshot({ db, config, run_id: rid });
|
|
192
|
+
const lines = [];
|
|
193
|
+
if (result.ok) {
|
|
194
|
+
lines.push(`✅ Stage ${sk} completed for run ${rid}.`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
lines.push(`⛔ Stage ${sk} ended with status ${parsed.astro_json.status} for run ${rid}.`);
|
|
198
|
+
lines.push(`Reason: ${result.error}`);
|
|
199
|
+
}
|
|
200
|
+
lines.push(``, `Artifacts:`);
|
|
201
|
+
for (const a of created)
|
|
202
|
+
lines.push(`- ${a.path} (id=${a.artifact_id})`);
|
|
203
|
+
if (result.new_stories?.length) {
|
|
204
|
+
lines.push(``, `New stories queued: ${result.new_stories.map((k) => `\`${k}\``).join(", ")}`);
|
|
205
|
+
}
|
|
206
|
+
if (result.ok) {
|
|
207
|
+
if (result.completed_run) {
|
|
208
|
+
lines.push(``, `🎉 Run completed.`);
|
|
209
|
+
}
|
|
210
|
+
else if (result.next_stage) {
|
|
211
|
+
lines.push(``, `Next stage: \`${result.next_stage}\``);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
lines.push(``, `Context snapshot (post-update):`);
|
|
215
|
+
lines.push(context);
|
|
216
|
+
return lines.join("\n").trim();
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
export function createAstroStageFailTool(opts) {
|
|
221
|
+
const { db } = opts;
|
|
222
|
+
return tool({
|
|
223
|
+
description: "Manually fail a stage and mark run failed.",
|
|
224
|
+
args: {
|
|
225
|
+
run_id: tool.schema.string().optional(),
|
|
226
|
+
stage_key: tool.schema.enum(["frame", "plan", "spec", "implement", "review", "verify", "close"]).optional(),
|
|
227
|
+
error_text: tool.schema.string().min(1),
|
|
228
|
+
},
|
|
229
|
+
execute: async ({ run_id, stage_key, error_text }) => {
|
|
230
|
+
const active = getActiveRun(db);
|
|
231
|
+
const rid = run_id ?? active?.run_id;
|
|
232
|
+
if (!rid)
|
|
233
|
+
throw new Error("No active run and no run_id provided.");
|
|
234
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(rid);
|
|
235
|
+
if (!run)
|
|
236
|
+
throw new Error(`Run not found: ${rid}`);
|
|
237
|
+
const sk = (stage_key ?? run.current_stage_key);
|
|
238
|
+
if (!sk)
|
|
239
|
+
throw new Error("No stage_key provided and run.current_stage_key is null.");
|
|
240
|
+
// Update stage_run row too
|
|
241
|
+
const now = nowISO();
|
|
242
|
+
const stageRow = db.prepare("SELECT * FROM stage_runs WHERE run_id=? AND stage_key=?").get(rid, sk);
|
|
243
|
+
if (!stageRow)
|
|
244
|
+
throw new Error(`Stage run not found: ${rid}/${sk}`);
|
|
245
|
+
withTx(db, () => {
|
|
246
|
+
db.prepare("UPDATE stage_runs SET status='failed', completed_at=?, updated_at=?, error_text=? WHERE stage_run_id=?").run(now, now, error_text, stageRow.stage_run_id);
|
|
247
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, ?, 'stage.failed', ?, ?)").run(newEventId(), rid, sk, JSON.stringify({ error_text }), now);
|
|
248
|
+
failRun(db, rid, sk, error_text);
|
|
249
|
+
});
|
|
250
|
+
return `⛔ Failed stage ${sk} and marked run ${rid} failed: ${error_text}`;
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
export function createAstroStageResetTool(opts) {
|
|
255
|
+
const { db } = opts;
|
|
256
|
+
return tool({
|
|
257
|
+
description: "Admin: reset a stage (and later stages) back to pending for a run. Re-opens run as running. Use carefully.",
|
|
258
|
+
args: {
|
|
259
|
+
run_id: tool.schema.string(),
|
|
260
|
+
stage_key: tool.schema.enum(["frame", "plan", "spec", "implement", "review", "verify", "close"]),
|
|
261
|
+
note: tool.schema.string().default("reset by user"),
|
|
262
|
+
},
|
|
263
|
+
execute: async ({ run_id, stage_key, note }) => {
|
|
264
|
+
const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(run_id);
|
|
265
|
+
if (!run)
|
|
266
|
+
throw new Error(`Run not found: ${run_id}`);
|
|
267
|
+
const now = nowISO();
|
|
268
|
+
const pipeline = JSON.parse(run.pipeline_stages_json ?? "[]");
|
|
269
|
+
const idx = pipeline.indexOf(stage_key);
|
|
270
|
+
if (idx === -1)
|
|
271
|
+
throw new Error(`Stage ${stage_key} not in pipeline for run ${run_id}`);
|
|
272
|
+
withTx(db, () => {
|
|
273
|
+
// Reset selected stage + later stages
|
|
274
|
+
db.prepare("UPDATE stage_runs SET status='pending', started_at=NULL, completed_at=NULL, baton_path=NULL, summary_md=NULL, output_json=NULL, error_text=NULL, subagent_session_id=NULL, updated_at=? WHERE run_id=? AND stage_index>=?").run(now, run_id, idx);
|
|
275
|
+
// Re-open run
|
|
276
|
+
db.prepare("UPDATE runs SET status='running', error_text=NULL, completed_at=NULL, current_stage_key=?, updated_at=? WHERE run_id=?").run(stage_key, now, run_id);
|
|
277
|
+
// Re-lock story
|
|
278
|
+
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(run_id, now, now, run.story_key);
|
|
279
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, ?, ?, 'stage.reset', ?, ?)").run(newEventId(), run_id, stage_key, JSON.stringify({ note }), now);
|
|
280
|
+
});
|
|
281
|
+
return `🔄 Reset stage ${stage_key} (and later) for run ${run_id}.`;
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import type { AstrocodeConfig } from "../config/schema";
|
|
3
|
+
import type { SqliteDb } from "../state/db";
|
|
4
|
+
export declare function createAstroStatusTool(opts: {
|
|
5
|
+
ctx: any;
|
|
6
|
+
config: AstrocodeConfig;
|
|
7
|
+
db: SqliteDb;
|
|
8
|
+
}): ToolDefinition;
|