astrocode-workflow 0.0.5 → 0.1.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/agents/prompts.d.ts +1 -1
- package/dist/agents/prompts.js +4 -1
- package/dist/agents/registry.js +7 -7
- package/dist/config/schema.d.ts +1 -0
- package/dist/config/schema.js +8 -7
- package/dist/tools/stage.js +45 -1
- package/dist/tools/story.js +2 -3
- package/dist/tools/workflow.js +2 -2
- package/dist/ui/inject.js +26 -6
- package/dist/workflow/state-machine.js +14 -0
- package/package.json +2 -1
- package/src/agents/prompts.ts +4 -1
- package/src/agents/registry.ts +7 -7
- package/src/config/schema.ts +8 -7
- package/src/tools/stage.ts +62 -1
- package/src/tools/story.ts +2 -3
- package/src/tools/workflow.ts +2 -2
- package/src/ui/inject.ts +29 -6
- package/src/workflow/state-machine.ts +23 -4
package/dist/agents/prompts.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const BASE_ORCH_PROMPT = "You are Astro (Orchestrator) for Astrocode.\n\nMission:\n- Advance a deterministic pipeline: frame \u2192 plan \u2192 spec \u2192 implement \u2192 review \u2192 verify \u2192 close.\n- The SQLite DB is the source of truth. Prefer tools over prose.\n- Never narrate what prompts you received.\n- Keep outputs short; store large outputs as artifacts and reference paths.\n\nOperating rules:\n- Prefer calling astro_workflow_proceed (step/loop) and astro_status.\n- Delegate stage work only to the stage subagent matching the current stage.\n- If a stage subagent returns status=blocked, inject the BLOCKED directive and stop.\n- Never delegate from subagents (enforced by permissions).\n";
|
|
1
|
+
export declare const BASE_ORCH_PROMPT = "You are Astro (Orchestrator) for Astrocode.\n\nMission:\n- Advance a deterministic pipeline: frame \u2192 plan \u2192 spec \u2192 implement \u2192 review \u2192 verify \u2192 close.\n- The SQLite DB is the source of truth. Prefer tools over prose.\n- Never narrate what prompts you received.\n- Keep outputs short; store large outputs as artifacts and reference paths.\n\nOperating rules:\n- Only start new runs when the user explicitly requests implementation, workflow management, or story processing.\n- Answer questions directly when possible without starting workflows.\n- Prefer calling astro_workflow_proceed (step/loop) and astro_status only when actively managing a workflow.\n- Delegate stage work only to the stage subagent matching the current stage.\n- If a stage subagent returns status=blocked, inject the BLOCKED directive and stop.\n- Never delegate from subagents (enforced by permissions).\n- Be discretionary: assess if the user's request requires workflow initiation or just information.\n";
|
|
2
2
|
export declare const BASE_STAGE_PROMPT = "You are an Astro stage subagent.\n\nFollow the latest [SYSTEM DIRECTIVE: ASTROCODE \u2014 STAGE_*] you receive.\n\nOutput exactly:\n1) Baton markdown (short, structured)\n2) Valid ASTRO JSON between markers:\n<!-- ASTRO_JSON_BEGIN -->\n{...}\n<!-- ASTRO_JSON_END -->\n\nDo not narrate. If blocked, ask exactly ONE question and stop.\n";
|
package/dist/agents/prompts.js
CHANGED
|
@@ -7,10 +7,13 @@ Mission:
|
|
|
7
7
|
- Keep outputs short; store large outputs as artifacts and reference paths.
|
|
8
8
|
|
|
9
9
|
Operating rules:
|
|
10
|
-
-
|
|
10
|
+
- Only start new runs when the user explicitly requests implementation, workflow management, or story processing.
|
|
11
|
+
- Answer questions directly when possible without starting workflows.
|
|
12
|
+
- Prefer calling astro_workflow_proceed (step/loop) and astro_status only when actively managing a workflow.
|
|
11
13
|
- Delegate stage work only to the stage subagent matching the current stage.
|
|
12
14
|
- If a stage subagent returns status=blocked, inject the BLOCKED directive and stop.
|
|
13
15
|
- Never delegate from subagents (enforced by permissions).
|
|
16
|
+
- Be discretionary: assess if the user's request requires workflow initiation or just information.
|
|
14
17
|
`;
|
|
15
18
|
export const BASE_STAGE_PROMPT = `You are an Astro stage subagent.
|
|
16
19
|
|
package/dist/agents/registry.js
CHANGED
|
@@ -98,13 +98,13 @@ export function createAstroAgents(opts) {
|
|
|
98
98
|
pluginConfig.agents = {
|
|
99
99
|
orchestrator_name: "Astro",
|
|
100
100
|
stage_agent_names: {
|
|
101
|
-
frame: "
|
|
102
|
-
plan: "
|
|
103
|
-
spec: "
|
|
104
|
-
implement: "
|
|
105
|
-
review: "
|
|
106
|
-
verify: "
|
|
107
|
-
close: "
|
|
101
|
+
frame: "Frame",
|
|
102
|
+
plan: "Plan",
|
|
103
|
+
spec: "Spec",
|
|
104
|
+
implement: "Implement",
|
|
105
|
+
review: "Review",
|
|
106
|
+
verify: "Verify",
|
|
107
|
+
close: "Close"
|
|
108
108
|
},
|
|
109
109
|
librarian_name: "Librarian",
|
|
110
110
|
explore_name: "Explore",
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export declare const AstrocodeConfigSchema: z.ZodDefault<z.ZodObject<{
|
|
|
68
68
|
loop_max_steps_hard_cap: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
69
69
|
plan_max_tasks: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
70
70
|
plan_max_lines: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
71
|
+
baton_summary_max_lines: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
|
71
72
|
forbid_prompt_narration: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
72
73
|
single_active_run_per_repo: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
73
74
|
lock_timeout_ms: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
|
package/dist/config/schema.js
CHANGED
|
@@ -97,6 +97,7 @@ const WorkflowSchema = z.object({
|
|
|
97
97
|
loop_max_steps_hard_cap: z.number().int().positive().default(200),
|
|
98
98
|
plan_max_tasks: z.number().int().positive().default(500),
|
|
99
99
|
plan_max_lines: z.number().int().positive().default(2000),
|
|
100
|
+
baton_summary_max_lines: z.number().int().positive().default(20),
|
|
100
101
|
forbid_prompt_narration: z.boolean().default(true),
|
|
101
102
|
single_active_run_per_repo: z.boolean().default(true),
|
|
102
103
|
lock_timeout_ms: z.number().int().positive().default(4000),
|
|
@@ -128,13 +129,13 @@ const AgentsSchema = z.object({
|
|
|
128
129
|
// Display names for the stage sub-agents.
|
|
129
130
|
stage_agent_names: z
|
|
130
131
|
.object({
|
|
131
|
-
frame: z.string().default("
|
|
132
|
-
plan: z.string().default("
|
|
133
|
-
spec: z.string().default("
|
|
134
|
-
implement: z.string().default("
|
|
135
|
-
review: z.string().default("
|
|
136
|
-
verify: z.string().default("
|
|
137
|
-
close: z.string().default("
|
|
132
|
+
frame: z.string().default("Frame"),
|
|
133
|
+
plan: z.string().default("Plan"),
|
|
134
|
+
spec: z.string().default("Spec"),
|
|
135
|
+
implement: z.string().default("Implement"),
|
|
136
|
+
review: z.string().default("Review"),
|
|
137
|
+
verify: z.string().default("Verify"),
|
|
138
|
+
close: z.string().default("Close"),
|
|
138
139
|
})
|
|
139
140
|
.partial()
|
|
140
141
|
.default({}),
|
package/dist/tools/stage.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
3
4
|
import { withTx } from "../state/db";
|
|
4
5
|
import { buildBatonSummary, parseStageOutputText } from "../workflow/baton";
|
|
@@ -94,7 +95,7 @@ export function createAstroStageCompleteTool(opts) {
|
|
|
94
95
|
const batonSummary = buildBatonSummary({ config, stage_key: sk, astro_json: parsed.astro_json, baton_md: parsed.baton_md });
|
|
95
96
|
const now = nowISO();
|
|
96
97
|
const pipeline = JSON.parse(run.pipeline_stages_json ?? "[]");
|
|
97
|
-
|
|
98
|
+
let next = nextStageKey(pipeline, sk);
|
|
98
99
|
const stageDirRel = toPosix(path.join(".astro", "runs", rid, sk));
|
|
99
100
|
const batonRel = toPosix(path.join(stageDirRel, config.artifacts.baton_filename));
|
|
100
101
|
const summaryRel = toPosix(path.join(stageDirRel, config.artifacts.baton_summary_filename));
|
|
@@ -198,6 +199,49 @@ export function createAstroStageCompleteTool(opts) {
|
|
|
198
199
|
}
|
|
199
200
|
}
|
|
200
201
|
}
|
|
202
|
+
// Skip spec stage if spec already exists
|
|
203
|
+
if (sk === "plan" && next === "spec") {
|
|
204
|
+
const specPath = path.join(repoRoot, ".astro", "spec.md");
|
|
205
|
+
if (fs.existsSync(specPath) && fs.statSync(specPath).size > 100) {
|
|
206
|
+
// Skip spec
|
|
207
|
+
db.prepare("INSERT INTO stage_runs (stage_run_id, run_id, stage_key, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
208
|
+
.run(newId("stage"), rid, "spec", "skipped", now, now);
|
|
209
|
+
next = "implement";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Split stories during implementation if tasks are identified
|
|
213
|
+
if (sk === "implement" && allow_new_stories && parsed.astro_json.tasks?.length) {
|
|
214
|
+
for (const task of parsed.astro_json.tasks) {
|
|
215
|
+
const complexity = task.complexity ?? 5;
|
|
216
|
+
const subtasks = task.subtasks ?? [];
|
|
217
|
+
if (subtasks.length > 0) {
|
|
218
|
+
// Split into subtasks
|
|
219
|
+
for (const subtask of subtasks) {
|
|
220
|
+
const key = insertStory(db, {
|
|
221
|
+
title: `${task.title}: ${subtask}`,
|
|
222
|
+
body_md: task.description ?? "",
|
|
223
|
+
priority: Math.max(1, 10 - complexity),
|
|
224
|
+
state: "queued",
|
|
225
|
+
epic_key: run.story_key
|
|
226
|
+
});
|
|
227
|
+
newStoryKeys.push(key);
|
|
228
|
+
db.prepare("INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)").run(newId("rel"), run.story_key, key, "split from implement", now);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (complexity > 6) {
|
|
232
|
+
// Split complex tasks
|
|
233
|
+
const key = insertStory(db, {
|
|
234
|
+
title: task.title,
|
|
235
|
+
body_md: task.description ?? "",
|
|
236
|
+
priority: Math.max(1, 10 - complexity),
|
|
237
|
+
state: "queued",
|
|
238
|
+
epic_key: run.story_key
|
|
239
|
+
});
|
|
240
|
+
newStoryKeys.push(key);
|
|
241
|
+
db.prepare("INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)").run(newId("rel"), run.story_key, key, "split from implement", now);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
201
245
|
if (parsed.astro_json.status !== "ok") {
|
|
202
246
|
const err = parsed.astro_json.status === "blocked"
|
|
203
247
|
? `blocked: ${(parsed.astro_json.questions?.[0] ?? "needs input")}`
|
package/dist/tools/story.js
CHANGED
|
@@ -14,9 +14,8 @@ export function createAstroStoryQueueTool(opts) {
|
|
|
14
14
|
},
|
|
15
15
|
execute: async ({ title, body_md, epic_key, priority }) => {
|
|
16
16
|
// If the story seems like a large implementation, convert to planning story
|
|
17
|
-
const isLargeImplementation = title.toLowerCase().includes('implement') &&
|
|
18
|
-
(body_md?.length || 0) >
|
|
19
|
-
(body_md?.toLowerCase().includes('full') || body_md?.toLowerCase().includes('complete'));
|
|
17
|
+
const isLargeImplementation = (title.toLowerCase().includes('implement') || body_md?.toLowerCase().includes('implement')) &&
|
|
18
|
+
(body_md?.length || 0) > 100;
|
|
20
19
|
let finalTitle = title;
|
|
21
20
|
let finalBody = body_md;
|
|
22
21
|
if (isLargeImplementation) {
|
package/dist/tools/workflow.js
CHANGED
|
@@ -12,7 +12,7 @@ function stageGoal(stage, cfg) {
|
|
|
12
12
|
case "frame":
|
|
13
13
|
return "Define scope, constraints, and an unambiguous Definition of Done.";
|
|
14
14
|
case "plan":
|
|
15
|
-
return `
|
|
15
|
+
return `Create 50-200 detailed implementation stories, each focused on a specific, implementable task. Break down every component into separate stories with clear acceptance criteria.`;
|
|
16
16
|
case "spec":
|
|
17
17
|
return "Produce minimal spec/contract: interfaces, invariants, acceptance checks.";
|
|
18
18
|
case "implement":
|
|
@@ -32,7 +32,7 @@ function stageConstraints(stage, cfg) {
|
|
|
32
32
|
"If blocked: ask exactly ONE question and stop.",
|
|
33
33
|
];
|
|
34
34
|
if (stage === "plan") {
|
|
35
|
-
common.push(`
|
|
35
|
+
common.push(`Create 50-200 stories; each story must be implementable in 2-8 hours with clear acceptance criteria.`);
|
|
36
36
|
}
|
|
37
37
|
if (stage === "verify" && cfg.workflow.evidence_required.verify) {
|
|
38
38
|
common.push("Evidence required: ASTRO JSON must include evidence[] paths.");
|
package/dist/ui/inject.js
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
|
+
let isInjecting = false;
|
|
2
|
+
let queuedInjection = null;
|
|
1
3
|
export async function injectChatPrompt(opts) {
|
|
2
4
|
const { ctx, sessionId, text } = opts;
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
if (isInjecting) {
|
|
6
|
+
// Replace any existing queued injection (keep only latest)
|
|
7
|
+
queuedInjection = opts;
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
isInjecting = true;
|
|
11
|
+
try {
|
|
12
|
+
await ctx.client.session.prompt({
|
|
13
|
+
path: { id: sessionId },
|
|
14
|
+
body: {
|
|
15
|
+
parts: [{ type: "text", text }],
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
isInjecting = false;
|
|
21
|
+
// Process queued injection if any
|
|
22
|
+
if (queuedInjection) {
|
|
23
|
+
const next = queuedInjection;
|
|
24
|
+
queuedInjection = null;
|
|
25
|
+
// Schedule next injection asynchronously to prevent recursion
|
|
26
|
+
setImmediate(() => injectChatPrompt(next));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
9
29
|
}
|
|
@@ -46,6 +46,11 @@ export function decideNextAction(db, config) {
|
|
|
46
46
|
warn("Unexpected stage status in decideNextAction", { status: current.status, stage_key: current.stage_key });
|
|
47
47
|
return { kind: "await_stage_completion", run_id: activeRun.run_id, stage_key: current.stage_key, stage_run_id: current.stage_run_id };
|
|
48
48
|
}
|
|
49
|
+
function isInitialStory(db, storyKey) {
|
|
50
|
+
// Check if this story has no parent relations (is top-level)
|
|
51
|
+
const relations = db.prepare("SELECT COUNT(*) as count FROM story_relations WHERE child_key=?").get(storyKey);
|
|
52
|
+
return relations.count === 0;
|
|
53
|
+
}
|
|
49
54
|
export function createRunForStory(db, config, storyKey) {
|
|
50
55
|
const story = getStory(db, storyKey);
|
|
51
56
|
if (!story)
|
|
@@ -55,6 +60,15 @@ export function createRunForStory(db, config, storyKey) {
|
|
|
55
60
|
const run_id = newRunId();
|
|
56
61
|
const now = nowISO();
|
|
57
62
|
const pipeline = config.workflow.pipeline;
|
|
63
|
+
// Convert to genesis planning story if needed
|
|
64
|
+
const isGenesisCandidate = storyKey === 'S-0001' || isInitialStory(db, storyKey) ||
|
|
65
|
+
(story.body_md && story.body_md.length > 100 &&
|
|
66
|
+
(story.title.toLowerCase().includes('implement') || story.body_md.toLowerCase().includes('implement')));
|
|
67
|
+
if (isGenesisCandidate) {
|
|
68
|
+
const planningTitle = `Plan and decompose: ${story.title}`;
|
|
69
|
+
const planningBody = `Analyze the requirements and break down "${story.title}" into 50-200 detailed, granular implementation stories. Each story should be focused on a specific, implementable task with clear acceptance criteria.\n\nOriginal request: ${story.body_md || ''}`;
|
|
70
|
+
db.prepare("UPDATE stories SET title=?, body_md=? WHERE story_key=?").run(planningTitle, planningBody, storyKey);
|
|
71
|
+
}
|
|
58
72
|
// Lock story
|
|
59
73
|
db.prepare("UPDATE stories SET state='in_progress', in_progress=1, locked_by_run_id=?, locked_at=?, updated_at=? WHERE story_key=?").run(run_id, now, now, storyKey);
|
|
60
74
|
db.prepare("INSERT INTO runs (run_id, story_key, status, pipeline_stages_json, current_stage_key, created_at, started_at, updated_at) VALUES (?, ?, 'running', ?, ?, ?, ?, ?)").run(run_id, storyKey, JSON.stringify(pipeline), pipeline[0] ?? null, now, now, now);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astrocode-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@opencode-ai/plugin": "^1.1.19",
|
|
20
20
|
"@opencode-ai/sdk": "^1.1.19",
|
|
21
|
+
"astrocode-workflow": "0.1.1",
|
|
21
22
|
"jsonc-parser": "^3.2.0",
|
|
22
23
|
"zod": "4.1.8"
|
|
23
24
|
},
|
package/src/agents/prompts.ts
CHANGED
|
@@ -7,10 +7,13 @@ Mission:
|
|
|
7
7
|
- Keep outputs short; store large outputs as artifacts and reference paths.
|
|
8
8
|
|
|
9
9
|
Operating rules:
|
|
10
|
-
-
|
|
10
|
+
- Only start new runs when the user explicitly requests implementation, workflow management, or story processing.
|
|
11
|
+
- Answer questions directly when possible without starting workflows.
|
|
12
|
+
- Prefer calling astro_workflow_proceed (step/loop) and astro_status only when actively managing a workflow.
|
|
11
13
|
- Delegate stage work only to the stage subagent matching the current stage.
|
|
12
14
|
- If a stage subagent returns status=blocked, inject the BLOCKED directive and stop.
|
|
13
15
|
- Never delegate from subagents (enforced by permissions).
|
|
16
|
+
- Be discretionary: assess if the user's request requires workflow initiation or just information.
|
|
14
17
|
`;
|
|
15
18
|
|
|
16
19
|
export const BASE_STAGE_PROMPT = `You are an Astro stage subagent.
|
package/src/agents/registry.ts
CHANGED
|
@@ -126,13 +126,13 @@ export function createAstroAgents(opts: {
|
|
|
126
126
|
pluginConfig.agents = {
|
|
127
127
|
orchestrator_name: "Astro",
|
|
128
128
|
stage_agent_names: {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
frame: "Frame",
|
|
130
|
+
plan: "Plan",
|
|
131
|
+
spec: "Spec",
|
|
132
|
+
implement: "Implement",
|
|
133
|
+
review: "Review",
|
|
134
|
+
verify: "Verify",
|
|
135
|
+
close: "Close"
|
|
136
136
|
},
|
|
137
137
|
librarian_name: "Librarian",
|
|
138
138
|
explore_name: "Explore",
|
package/src/config/schema.ts
CHANGED
|
@@ -114,6 +114,7 @@ const WorkflowSchema = z.object({
|
|
|
114
114
|
|
|
115
115
|
plan_max_tasks: z.number().int().positive().default(500),
|
|
116
116
|
plan_max_lines: z.number().int().positive().default(2000),
|
|
117
|
+
baton_summary_max_lines: z.number().int().positive().default(20),
|
|
117
118
|
|
|
118
119
|
forbid_prompt_narration: z.boolean().default(true),
|
|
119
120
|
single_active_run_per_repo: z.boolean().default(true),
|
|
@@ -153,13 +154,13 @@ const AgentsSchema = z.object({
|
|
|
153
154
|
// Display names for the stage sub-agents.
|
|
154
155
|
stage_agent_names: z
|
|
155
156
|
.object({
|
|
156
|
-
frame: z.string().default("
|
|
157
|
-
plan: z.string().default("
|
|
158
|
-
spec: z.string().default("
|
|
159
|
-
implement: z.string().default("
|
|
160
|
-
review: z.string().default("
|
|
161
|
-
verify: z.string().default("
|
|
162
|
-
close: z.string().default("
|
|
157
|
+
frame: z.string().default("Frame"),
|
|
158
|
+
plan: z.string().default("Plan"),
|
|
159
|
+
spec: z.string().default("Spec"),
|
|
160
|
+
implement: z.string().default("Implement"),
|
|
161
|
+
review: z.string().default("Review"),
|
|
162
|
+
verify: z.string().default("Verify"),
|
|
163
|
+
close: z.string().default("Close"),
|
|
163
164
|
})
|
|
164
165
|
.partial()
|
|
165
166
|
.default({}),
|
package/src/tools/stage.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
2
3
|
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
3
4
|
import type { AstrocodeConfig } from "../config/schema";
|
|
4
5
|
import type { SqliteDb } from "../state/db";
|
|
@@ -113,7 +114,7 @@ export function createAstroStageCompleteTool(opts: { ctx: any; config: Astrocode
|
|
|
113
114
|
|
|
114
115
|
const now = nowISO();
|
|
115
116
|
const pipeline = JSON.parse(run.pipeline_stages_json ?? "[]") as StageKey[];
|
|
116
|
-
|
|
117
|
+
let next = nextStageKey(pipeline, sk);
|
|
117
118
|
|
|
118
119
|
const stageDirRel = toPosix(path.join(".astro", "runs", rid, sk));
|
|
119
120
|
const batonRel = toPosix(path.join(stageDirRel, config.artifacts.baton_filename));
|
|
@@ -270,6 +271,66 @@ export function createAstroStageCompleteTool(opts: { ctx: any; config: Astrocode
|
|
|
270
271
|
}
|
|
271
272
|
}
|
|
272
273
|
|
|
274
|
+
// Skip spec stage if spec already exists
|
|
275
|
+
if (sk === "plan" && next === "spec") {
|
|
276
|
+
const specPath = path.join(repoRoot, ".astro", "spec.md");
|
|
277
|
+
if (fs.existsSync(specPath) && fs.statSync(specPath).size > 100) {
|
|
278
|
+
// Skip spec
|
|
279
|
+
db.prepare("INSERT INTO stage_runs (stage_run_id, run_id, stage_key, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)")
|
|
280
|
+
.run(newId("stage"), rid, "spec", "skipped", now, now);
|
|
281
|
+
next = "implement";
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Split stories during implementation if tasks are identified
|
|
286
|
+
if (sk === "implement" && allow_new_stories && parsed.astro_json.tasks?.length) {
|
|
287
|
+
for (const task of parsed.astro_json.tasks) {
|
|
288
|
+
const complexity = task.complexity ?? 5;
|
|
289
|
+
const subtasks = task.subtasks ?? [];
|
|
290
|
+
if (subtasks.length > 0) {
|
|
291
|
+
// Split into subtasks
|
|
292
|
+
for (const subtask of subtasks) {
|
|
293
|
+
const key = insertStory(db, {
|
|
294
|
+
title: `${task.title}: ${subtask}`,
|
|
295
|
+
body_md: task.description ?? "",
|
|
296
|
+
priority: Math.max(1, 10 - complexity),
|
|
297
|
+
state: "queued",
|
|
298
|
+
epic_key: run.story_key
|
|
299
|
+
});
|
|
300
|
+
newStoryKeys.push(key);
|
|
301
|
+
db.prepare(
|
|
302
|
+
"INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
303
|
+
).run(
|
|
304
|
+
newId("rel"),
|
|
305
|
+
run.story_key,
|
|
306
|
+
key,
|
|
307
|
+
"split from implement",
|
|
308
|
+
now
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
} else if (complexity > 6) {
|
|
312
|
+
// Split complex tasks
|
|
313
|
+
const key = insertStory(db, {
|
|
314
|
+
title: task.title,
|
|
315
|
+
body_md: task.description ?? "",
|
|
316
|
+
priority: Math.max(1, 10 - complexity),
|
|
317
|
+
state: "queued",
|
|
318
|
+
epic_key: run.story_key
|
|
319
|
+
});
|
|
320
|
+
newStoryKeys.push(key);
|
|
321
|
+
db.prepare(
|
|
322
|
+
"INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
323
|
+
).run(
|
|
324
|
+
newId("rel"),
|
|
325
|
+
run.story_key,
|
|
326
|
+
key,
|
|
327
|
+
"split from implement",
|
|
328
|
+
now
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
273
334
|
if (parsed.astro_json.status !== "ok") {
|
|
274
335
|
const err = parsed.astro_json.status === "blocked"
|
|
275
336
|
? `blocked: ${(parsed.astro_json.questions?.[0] ?? "needs input")}`
|
package/src/tools/story.ts
CHANGED
|
@@ -21,9 +21,8 @@ export function createAstroStoryQueueTool(opts: { ctx: any; config: AstrocodeCon
|
|
|
21
21
|
},
|
|
22
22
|
execute: async ({ title, body_md, epic_key, priority }) => {
|
|
23
23
|
// If the story seems like a large implementation, convert to planning story
|
|
24
|
-
const isLargeImplementation = title.toLowerCase().includes('implement') &&
|
|
25
|
-
(body_md?.length || 0) >
|
|
26
|
-
(body_md?.toLowerCase().includes('full') || body_md?.toLowerCase().includes('complete'));
|
|
24
|
+
const isLargeImplementation = (title.toLowerCase().includes('implement') || body_md?.toLowerCase().includes('implement')) &&
|
|
25
|
+
(body_md?.length || 0) > 100;
|
|
27
26
|
|
|
28
27
|
let finalTitle = title;
|
|
29
28
|
let finalBody = body_md;
|
package/src/tools/workflow.ts
CHANGED
|
@@ -16,7 +16,7 @@ function stageGoal(stage: StageKey, cfg: AstrocodeConfig): string {
|
|
|
16
16
|
case "frame":
|
|
17
17
|
return "Define scope, constraints, and an unambiguous Definition of Done.";
|
|
18
18
|
case "plan":
|
|
19
|
-
return `
|
|
19
|
+
return `Create 50-200 detailed implementation stories, each focused on a specific, implementable task. Break down every component into separate stories with clear acceptance criteria.`;
|
|
20
20
|
case "spec":
|
|
21
21
|
return "Produce minimal spec/contract: interfaces, invariants, acceptance checks.";
|
|
22
22
|
case "implement":
|
|
@@ -38,7 +38,7 @@ function stageConstraints(stage: StageKey, cfg: AstrocodeConfig): string[] {
|
|
|
38
38
|
];
|
|
39
39
|
|
|
40
40
|
if (stage === "plan") {
|
|
41
|
-
common.push(`
|
|
41
|
+
common.push(`Create 50-200 stories; each story must be implementable in 2-8 hours with clear acceptance criteria.`);
|
|
42
42
|
}
|
|
43
43
|
if (stage === "verify" && cfg.workflow.evidence_required.verify) {
|
|
44
44
|
common.push("Evidence required: ASTRO JSON must include evidence[] paths.");
|
package/src/ui/inject.ts
CHANGED
|
@@ -1,13 +1,36 @@
|
|
|
1
|
+
let isInjecting = false;
|
|
2
|
+
let queuedInjection: { ctx: any; sessionId: string; text: string } | null = null;
|
|
3
|
+
|
|
1
4
|
export async function injectChatPrompt(opts: {
|
|
2
5
|
ctx: any;
|
|
3
6
|
sessionId: string;
|
|
4
7
|
text: string;
|
|
5
8
|
}) {
|
|
6
9
|
const { ctx, sessionId, text } = opts;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
10
|
+
|
|
11
|
+
if (isInjecting) {
|
|
12
|
+
// Replace any existing queued injection (keep only latest)
|
|
13
|
+
queuedInjection = opts;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
isInjecting = true;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await ctx.client.session.prompt({
|
|
21
|
+
path: { id: sessionId },
|
|
22
|
+
body: {
|
|
23
|
+
parts: [{ type: "text", text }],
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
} finally {
|
|
27
|
+
isInjecting = false;
|
|
28
|
+
// Process queued injection if any
|
|
29
|
+
if (queuedInjection) {
|
|
30
|
+
const next = queuedInjection;
|
|
31
|
+
queuedInjection = null;
|
|
32
|
+
// Schedule next injection asynchronously to prevent recursion
|
|
33
|
+
setImmediate(() => injectChatPrompt(next));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
13
36
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { AstrocodeConfig } from "../config/schema";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import type { SqliteDb } from "../state/db";
|
|
3
5
|
import type { RunRow, StageKey, StageRunRow, StoryRow } from "../state/types";
|
|
4
6
|
import { nowISO } from "../shared/time";
|
|
@@ -72,6 +74,12 @@ export function decideNextAction(db: SqliteDb, config: AstrocodeConfig): NextAct
|
|
|
72
74
|
return { kind: "await_stage_completion", run_id: activeRun.run_id, stage_key: current.stage_key, stage_run_id: current.stage_run_id };
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
function isInitialStory(db: SqliteDb, storyKey: string): boolean {
|
|
78
|
+
// Check if this story has no parent relations (is top-level)
|
|
79
|
+
const relations = db.prepare("SELECT COUNT(*) as count FROM story_relations WHERE child_key=?").get(storyKey) as { count: number };
|
|
80
|
+
return relations.count === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
export function createRunForStory(db: SqliteDb, config: AstrocodeConfig, storyKey: string): { run_id: string } {
|
|
76
84
|
const story = getStory(db, storyKey);
|
|
77
85
|
if (!story) throw new Error(`Story not found: ${storyKey}`);
|
|
@@ -81,10 +89,21 @@ export function createRunForStory(db: SqliteDb, config: AstrocodeConfig, storyKe
|
|
|
81
89
|
const now = nowISO();
|
|
82
90
|
const pipeline = config.workflow.pipeline;
|
|
83
91
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
// Convert to genesis planning story if needed
|
|
93
|
+
const isGenesisCandidate = storyKey === 'S-0001' || isInitialStory(db, storyKey) ||
|
|
94
|
+
(story.body_md && story.body_md.length > 100 &&
|
|
95
|
+
(story.title.toLowerCase().includes('implement') || story.body_md.toLowerCase().includes('implement')));
|
|
96
|
+
|
|
97
|
+
if (isGenesisCandidate) {
|
|
98
|
+
const planningTitle = `Plan and decompose: ${story.title}`;
|
|
99
|
+
const planningBody = `Analyze the requirements and break down "${story.title}" into 50-200 detailed, granular implementation stories. Each story should be focused on a specific, implementable task with clear acceptance criteria.\n\nOriginal request: ${story.body_md || ''}`;
|
|
100
|
+
db.prepare("UPDATE stories SET title=?, body_md=? WHERE story_key=?").run(planningTitle, planningBody, storyKey);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Lock story
|
|
104
|
+
db.prepare(
|
|
105
|
+
"UPDATE stories SET state='in_progress', in_progress=1, locked_by_run_id=?, locked_at=?, updated_at=? WHERE story_key=?"
|
|
106
|
+
).run(run_id, now, now, storyKey);
|
|
88
107
|
|
|
89
108
|
db.prepare(
|
|
90
109
|
"INSERT INTO runs (run_id, story_key, status, pipeline_stages_json, current_stage_key, created_at, started_at, updated_at) VALUES (?, ?, 'running', ?, ?, ?, ?, ?)"
|