astrocode-workflow 0.0.1 → 0.0.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.
@@ -95,8 +95,8 @@ const WorkflowSchema = z.object({
95
95
  default_mode: z.enum(["step", "loop"]).default("step"),
96
96
  default_max_steps: z.number().int().positive().default(1),
97
97
  loop_max_steps_hard_cap: z.number().int().positive().default(200),
98
- plan_max_tasks: z.number().int().positive().default(7),
99
- plan_max_lines: z.number().int().positive().default(80),
98
+ plan_max_tasks: z.number().int().positive().default(500),
99
+ plan_max_lines: z.number().int().positive().default(2000),
100
100
  forbid_prompt_narration: z.boolean().default(true),
101
101
  single_active_run_per_repo: z.boolean().default(true),
102
102
  lock_timeout_ms: z.number().int().positive().default(4000),
@@ -7,7 +7,7 @@ import { putArtifact } from "../workflow/artifacts";
7
7
  import { nowISO } from "../shared/time";
8
8
  import { getAstroPaths, ensureAstroDirs, toPosix } from "../shared/paths";
9
9
  import { failRun, getActiveRun, getStageRuns, startStage, completeRun } from "../workflow/state-machine";
10
- import { newEventId } from "../state/ids";
10
+ import { newEventId, newId } from "../state/ids";
11
11
  import { insertStory } from "../workflow/story-helpers";
12
12
  function nextStageKey(pipeline, current) {
13
13
  const i = pipeline.indexOf(current);
@@ -65,30 +65,31 @@ export function createAstroStageCompleteTool(opts) {
65
65
  const active = getActiveRun(db);
66
66
  const rid = run_id ?? active?.run_id;
67
67
  if (!rid)
68
- throw new Error("No active run and no run_id provided.");
68
+ return "❌ No active run found. Start a run first.";
69
69
  const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(rid);
70
70
  if (!run)
71
- throw new Error(`Run not found: ${rid}`);
71
+ return `❌ Run not found: ${rid}`;
72
72
  const sk = (stage_key ?? run.current_stage_key);
73
73
  if (!sk)
74
- throw new Error("No stage_key provided and run.current_stage_key is null.");
74
+ return "❌ No stage_key provided and run has no current stage.";
75
75
  const stageRow = db.prepare("SELECT * FROM stage_runs WHERE run_id=? AND stage_key=?").get(rid, sk);
76
76
  if (!stageRow)
77
- throw new Error(`Stage run not found: ${rid}/${sk}`);
77
+ return `❌ Stage run not found: ${rid}/${sk}`;
78
78
  if (stageRow.status !== "running") {
79
- throw new Error(`Stage ${sk} is not running (status=${stageRow.status}). Start it first.`);
79
+ return `❌ Stage ${sk} is not running (status=${stageRow.status}). Start it first with astro_stage_start.`;
80
80
  }
81
81
  const parsed = parseStageOutputText(output_text);
82
- if (parsed.error || !parsed.astro_json)
83
- throw new Error(parsed.error ?? "ASTRO JSON missing");
82
+ if (parsed.error || !parsed.astro_json) {
83
+ return `❌ Stage completion failed: ${parsed.error ?? "ASTRO JSON missing"}. Please ensure output includes proper JSON markers.`;
84
+ }
84
85
  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
+ return `❌ Stage completion failed: ASTRO JSON stage_key mismatch (expected ${sk}, got ${parsed.astro_json.stage_key}).`;
86
87
  }
87
88
  // Evidence requirement
88
89
  const evidenceRequired = (sk === "verify" && config.workflow.evidence_required.verify) ||
89
90
  (sk === "implement" && config.workflow.evidence_required.implement);
90
91
  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
+ return `❌ Evidence is required for stage ${sk} but ASTRO JSON evidence[] is empty. Please provide evidence files.`;
92
93
  }
93
94
  const batonSummary = buildBatonSummary({ config, stage_key: sk, astro_json: parsed.astro_json, baton_md: parsed.baton_md });
94
95
  const now = nowISO();
@@ -161,7 +162,40 @@ export function createAstroStageCompleteTool(opts) {
161
162
  for (const ns of parsed.astro_json.new_stories) {
162
163
  const key = insertStory(db, { title: ns.title, body_md: ns.body_md ?? "", priority: ns.priority ?? 0, state: "queued" });
163
164
  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
+ db.prepare("INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)").run(newId("rel"), run.story_key, key, relation_reason, now);
166
+ }
167
+ }
168
+ // Automatic story splitting for complex tasks
169
+ if (allow_new_stories && parsed.astro_json.tasks?.length) {
170
+ for (const task of parsed.astro_json.tasks) {
171
+ const complexity = task.complexity ?? 5;
172
+ const subtasks = task.subtasks ?? [];
173
+ if (subtasks.length > 0) {
174
+ // Split into subtasks
175
+ for (const subtask of subtasks) {
176
+ const key = insertStory(db, {
177
+ title: `${task.title}: ${subtask}`,
178
+ body_md: task.description ?? "",
179
+ priority: Math.max(1, 10 - complexity),
180
+ state: "queued",
181
+ epic_key: run.story_key
182
+ });
183
+ newStoryKeys.push(key);
184
+ db.prepare("INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)").run(newId("rel"), run.story_key, key, "auto-split subtask", now);
185
+ }
186
+ }
187
+ else if (complexity > 5) {
188
+ // Split complex tasks without subtasks
189
+ const key = insertStory(db, {
190
+ title: task.title,
191
+ body_md: task.description ?? "",
192
+ priority: Math.max(1, 10 - complexity),
193
+ state: "queued",
194
+ epic_key: run.story_key
195
+ });
196
+ newStoryKeys.push(key);
197
+ db.prepare("INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)").run(newId("rel"), run.story_key, key, "auto-split from plan", now);
198
+ }
165
199
  }
166
200
  }
167
201
  if (parsed.astro_json.status !== "ok") {
@@ -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 `Produce a bounded task plan (<= ${cfg.workflow.plan_max_tasks} tasks) tied to files and tests.`;
15
+ return `Break down the work into 10-500 detailed tasks with subtasks, estimating complexity (1-10) for each. Focus on granular, implementable units.`;
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(`Hard limit: <= ${cfg.workflow.plan_max_tasks} tasks and <= ${cfg.workflow.plan_max_lines} lines of plan output.`);
35
+ common.push(`Aim for 10-500 tasks; no hard upper limit but prioritize granularity.`);
36
36
  }
37
37
  if (stage === "verify" && cfg.workflow.evidence_required.verify) {
38
38
  common.push("Evidence required: ASTRO JSON must include evidence[] paths.");
@@ -111,7 +111,7 @@ export function createAstroWorkflowProceedTool(opts) {
111
111
  const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id);
112
112
  const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key);
113
113
  // Mark stage started + set subagent_type to the stage agent.
114
- const agentName = agentNameForStage(next.stage_key, config);
114
+ const agentName = next.stage_key;
115
115
  withTx(db, () => {
116
116
  startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
117
117
  });
@@ -32,6 +32,12 @@ export declare const AstroJsonSchema: z.ZodObject<{
32
32
  summary: z.ZodDefault<z.ZodString>;
33
33
  decisions: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
34
  next_actions: z.ZodDefault<z.ZodArray<z.ZodString>>;
35
+ tasks: z.ZodDefault<z.ZodArray<z.ZodObject<{
36
+ title: z.ZodString;
37
+ description: z.ZodOptional<z.ZodString>;
38
+ complexity: z.ZodOptional<z.ZodNumber>;
39
+ subtasks: z.ZodOptional<z.ZodArray<z.ZodString>>;
40
+ }, z.core.$strip>>>;
35
41
  files: z.ZodDefault<z.ZodArray<z.ZodObject<{
36
42
  path: z.ZodString;
37
43
  kind: z.ZodDefault<z.ZodString>;
@@ -12,6 +12,12 @@ export const AstroJsonSchema = z.object({
12
12
  summary: z.string().default(""),
13
13
  decisions: z.array(z.string()).default([]),
14
14
  next_actions: z.array(z.string()).default([]),
15
+ tasks: z.array(z.object({
16
+ title: z.string(),
17
+ description: z.string().optional(),
18
+ complexity: z.number().int().min(1).max(10).optional(),
19
+ subtasks: z.array(z.string()).optional(),
20
+ })).default([]),
15
21
  files: z.array(z.object({
16
22
  path: z.string(),
17
23
  kind: z.string().default("file"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrocode-workflow",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -112,8 +112,8 @@ const WorkflowSchema = z.object({
112
112
  default_max_steps: z.number().int().positive().default(1),
113
113
  loop_max_steps_hard_cap: z.number().int().positive().default(200),
114
114
 
115
- plan_max_tasks: z.number().int().positive().default(7),
116
- plan_max_lines: z.number().int().positive().default(80),
115
+ plan_max_tasks: z.number().int().positive().default(500),
116
+ plan_max_lines: z.number().int().positive().default(2000),
117
117
 
118
118
  forbid_prompt_narration: z.boolean().default(true),
119
119
  single_active_run_per_repo: z.boolean().default(true),
@@ -10,7 +10,7 @@ import { putArtifact } from "../workflow/artifacts";
10
10
  import { nowISO } from "../shared/time";
11
11
  import { getAstroPaths, ensureAstroDirs, toPosix } from "../shared/paths";
12
12
  import { failRun, getActiveRun, getStageRuns, startStage, completeRun } from "../workflow/state-machine";
13
- import { newEventId } from "../state/ids";
13
+ import { newEventId, newId } from "../state/ids";
14
14
  import { insertStory } from "../workflow/story-helpers";
15
15
 
16
16
  function nextStageKey(pipeline: StageKey[], current: StageKey): StageKey | null {
@@ -75,37 +75,39 @@ export function createAstroStageCompleteTool(opts: { ctx: any; config: Astrocode
75
75
  const paths = getAstroPaths(repoRoot, config.db.path);
76
76
  ensureAstroDirs(paths);
77
77
 
78
- const active = getActiveRun(db);
79
- const rid = run_id ?? active?.run_id;
80
- if (!rid) throw new Error("No active run and no run_id provided.");
78
+ const active = getActiveRun(db);
79
+ const rid = run_id ?? active?.run_id;
80
+ if (!rid) return "❌ No active run found. Start a run first.";
81
81
 
82
- const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(rid) as any;
83
- if (!run) throw new Error(`Run not found: ${rid}`);
82
+ const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(rid) as any;
83
+ if (!run) return `❌ Run not found: ${rid}`;
84
84
 
85
- const sk = (stage_key ?? run.current_stage_key) as StageKey;
86
- if (!sk) throw new Error("No stage_key provided and run.current_stage_key is null.");
85
+ const sk = (stage_key ?? run.current_stage_key) as StageKey;
86
+ if (!sk) return "❌ No stage_key provided and run has no current stage.";
87
87
 
88
- const stageRow = db.prepare("SELECT * FROM stage_runs WHERE run_id=? AND stage_key=?").get(rid, sk) as StageRunRow | undefined;
89
- if (!stageRow) throw new Error(`Stage run not found: ${rid}/${sk}`);
90
- if (stageRow.status !== "running") {
91
- throw new Error(`Stage ${sk} is not running (status=${stageRow.status}). Start it first.`);
92
- }
88
+ const stageRow = db.prepare("SELECT * FROM stage_runs WHERE run_id=? AND stage_key=?").get(rid, sk) as StageRunRow | undefined;
89
+ if (!stageRow) return `❌ Stage run not found: ${rid}/${sk}`;
90
+ if (stageRow.status !== "running") {
91
+ return `❌ Stage ${sk} is not running (status=${stageRow.status}). Start it first with astro_stage_start.`;
92
+ }
93
93
 
94
- const parsed = parseStageOutputText(output_text);
95
- if (parsed.error || !parsed.astro_json) throw new Error(parsed.error ?? "ASTRO JSON missing");
94
+ const parsed = parseStageOutputText(output_text);
95
+ if (parsed.error || !parsed.astro_json) {
96
+ return `❌ Stage completion failed: ${parsed.error ?? "ASTRO JSON missing"}. Please ensure output includes proper JSON markers.`;
97
+ }
96
98
 
97
- if (parsed.astro_json.stage_key !== sk) {
98
- throw new Error(`ASTRO JSON stage_key mismatch: expected ${sk}, got ${parsed.astro_json.stage_key}`);
99
- }
99
+ if (parsed.astro_json.stage_key !== sk) {
100
+ return `❌ Stage completion failed: ASTRO JSON stage_key mismatch (expected ${sk}, got ${parsed.astro_json.stage_key}).`;
101
+ }
100
102
 
101
103
  // Evidence requirement
102
104
  const evidenceRequired =
103
105
  (sk === "verify" && config.workflow.evidence_required.verify) ||
104
106
  (sk === "implement" && config.workflow.evidence_required.implement);
105
107
 
106
- if (evidenceRequired && (parsed.astro_json.evidence ?? []).length === 0) {
107
- throw new Error(`Evidence is required for stage ${sk} but ASTRO JSON evidence[] is empty.`);
108
- }
108
+ if (evidenceRequired && (parsed.astro_json.evidence ?? []).length === 0) {
109
+ return `❌ Evidence is required for stage ${sk} but ASTRO JSON evidence[] is empty. Please provide evidence files.`;
110
+ }
109
111
 
110
112
  const batonSummary = buildBatonSummary({ config, stage_key: sk, astro_json: parsed.astro_json, baton_md: parsed.baton_md });
111
113
 
@@ -208,8 +210,63 @@ export function createAstroStageCompleteTool(opts: { ctx: any; config: Astrocode
208
210
  const key = insertStory(db, { title: ns.title, body_md: ns.body_md ?? "", priority: ns.priority ?? 0, state: "queued" });
209
211
  newStoryKeys.push(key);
210
212
  db.prepare(
211
- "INSERT OR IGNORE INTO story_relations (parent_story_key, child_story_key, relation_type, reason, created_at) VALUES (?, ?, 'split', ?, ?)"
212
- ).run(run.story_key, key, relation_reason, now);
213
+ "INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)"
214
+ ).run(
215
+ newId("rel"),
216
+ run.story_key,
217
+ key,
218
+ relation_reason,
219
+ now
220
+ );
221
+ }
222
+ }
223
+
224
+ // Automatic story splitting for complex tasks
225
+ if (allow_new_stories && parsed.astro_json.tasks?.length) {
226
+ for (const task of parsed.astro_json.tasks) {
227
+ const complexity = task.complexity ?? 5;
228
+ const subtasks = task.subtasks ?? [];
229
+ if (subtasks.length > 0) {
230
+ // Split into subtasks
231
+ for (const subtask of subtasks) {
232
+ const key = insertStory(db, {
233
+ title: `${task.title}: ${subtask}`,
234
+ body_md: task.description ?? "",
235
+ priority: Math.max(1, 10 - complexity),
236
+ state: "queued",
237
+ epic_key: run.story_key
238
+ });
239
+ newStoryKeys.push(key);
240
+ db.prepare(
241
+ "INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)"
242
+ ).run(
243
+ newId("rel"),
244
+ run.story_key,
245
+ key,
246
+ "auto-split subtask",
247
+ now
248
+ );
249
+ }
250
+ } else if (complexity > 5) {
251
+ // Split complex tasks without subtasks
252
+ const key = insertStory(db, {
253
+ title: task.title,
254
+ body_md: task.description ?? "",
255
+ priority: Math.max(1, 10 - complexity),
256
+ state: "queued",
257
+ epic_key: run.story_key
258
+ });
259
+ newStoryKeys.push(key);
260
+ db.prepare(
261
+ "INSERT INTO story_relations (relation_id, parent_key, child_key, relation_type, created_at) VALUES (?, ?, ?, ?, ?)"
262
+ ).run(
263
+ newId("rel"),
264
+ run.story_key,
265
+ key,
266
+ "auto-split from plan",
267
+ now
268
+ );
269
+ }
213
270
  }
214
271
  }
215
272
 
@@ -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 `Produce a bounded task plan (<= ${cfg.workflow.plan_max_tasks} tasks) tied to files and tests.`;
19
+ return `Break down the work into 10-500 detailed tasks with subtasks, estimating complexity (1-10) for each. Focus on granular, implementable units.`;
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(`Hard limit: <= ${cfg.workflow.plan_max_tasks} tasks and <= ${cfg.workflow.plan_max_lines} lines of plan output.`);
41
+ common.push(`Aim for 10-500 tasks; no hard upper limit but prioritize granularity.`);
42
42
  }
43
43
  if (stage === "verify" && cfg.workflow.evidence_required.verify) {
44
44
  common.push("Evidence required: ASTRO JSON must include evidence[] paths.");
@@ -135,8 +135,8 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
135
135
  const run = db.prepare("SELECT * FROM runs WHERE run_id=?").get(active.run_id) as any;
136
136
  const story = db.prepare("SELECT * FROM stories WHERE story_key=?").get(run.story_key) as any;
137
137
 
138
- // Mark stage started + set subagent_type to the stage agent.
139
- const agentName = agentNameForStage(next.stage_key, config);
138
+ // Mark stage started + set subagent_type to the stage agent.
139
+ const agentName = next.stage_key;
140
140
  withTx(db, () => {
141
141
  startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
142
142
  });
@@ -21,6 +21,15 @@ export const AstroJsonSchema = z.object({
21
21
  decisions: z.array(z.string()).default([]),
22
22
  next_actions: z.array(z.string()).default([]),
23
23
 
24
+ tasks: z.array(
25
+ z.object({
26
+ title: z.string(),
27
+ description: z.string().optional(),
28
+ complexity: z.number().int().min(1).max(10).optional(),
29
+ subtasks: z.array(z.string()).optional(),
30
+ })
31
+ ).default([]),
32
+
24
33
  files: z.array(
25
34
  z.object({
26
35
  path: z.string(),