astrocode-workflow 0.1.19 → 0.1.23

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.
@@ -1,2 +1,2 @@
1
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
- 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 in this order:\n1) Baton markdown (short, structured narrative summary)\n2) Valid ASTRO JSON between markers with ALL required fields (do not omit any):\n\n<!-- ASTRO_JSON_BEGIN -->\n{\n \"schema_version\": 1,\n \"stage_key\": \"CURRENT_STAGE\",\n \"status\": \"ok\",\n \"summary\": \"Brief summary of work done\",\n \"decisions\": [],\n \"next_actions\": [],\n \"tasks\": [],\n \"files\": [],\n \"evidence\": [],\n \"new_stories\": [],\n \"questions\": [],\n \"metrics\": {}\n}\n<!-- ASTRO_JSON_END -->\n\nCRITICAL REQUIREMENTS:\n- Include ALL fields shown above (even if empty arrays/objects)\n- stage_key must match the current stage exactly\n- status must be \"ok\", \"blocked\", or \"failed\"\n- summary must be a non-empty string describing the work\n- JSON must be valid syntax and parseable\n- Markers must be exact: <!-- ASTRO_JSON_BEGIN --> and <!-- ASTRO_JSON_END -->\n\nDo not narrate extra text. If blocked, set status=\"blocked\", ask exactly ONE question in questions array, and stop.\n";
2
+ export declare const BASE_STAGE_PROMPT = "You are an Astro stage subagent.\n\n***CRITICAL: Follow the latest [SYSTEM DIRECTIVE: ASTROCODE \u2014 STAGE_*] you receive EXACTLY.***\n\nYour output MUST be in this EXACT format (no deviations):\n\n1) First, a short baton markdown summary (1-3 sentences).\n\n2) Then, immediately after, the ASTRO JSON markers with valid JSON:\n\n<!-- ASTRO_JSON_BEGIN -->\n{\n \"schema_version\": 1,\n \"stage_key\": \"CURRENT_STAGE\",\n \"status\": \"ok\",\n \"summary\": \"Brief summary\",\n \"decisions\": [],\n \"next_actions\": [],\n \"tasks\": [],\n \"files\": [],\n \"evidence\": [],\n \"new_stories\": [],\n \"questions\": [],\n \"metrics\": {}\n}\n<!-- ASTRO_JSON_END -->\n\nMANDATORY RULES:\n- ALL fields above MUST be present (use empty arrays/objects if no data)\n- stage_key MUST be \"CURRENT_STAGE\" (will be replaced)\n- status MUST be \"ok\" unless blocked/failed\n- summary MUST be a string describing the work\n- JSON MUST be valid and parseable\n- Markers MUST be exact as shown\n\nIf blocked, set status=\"blocked\" and put ONE question in questions array. Do not output anything else.\n";
@@ -17,18 +17,20 @@ Operating rules:
17
17
  `;
18
18
  export const BASE_STAGE_PROMPT = `You are an Astro stage subagent.
19
19
 
20
- Follow the latest [SYSTEM DIRECTIVE: ASTROCODE — STAGE_*] you receive.
20
+ ***CRITICAL: Follow the latest [SYSTEM DIRECTIVE: ASTROCODE — STAGE_*] you receive EXACTLY.***
21
21
 
22
- Output exactly in this order:
23
- 1) Baton markdown (short, structured narrative summary)
24
- 2) Valid ASTRO JSON between markers with ALL required fields (do not omit any):
22
+ Your output MUST be in this EXACT format (no deviations):
23
+
24
+ 1) First, a short baton markdown summary (1-3 sentences).
25
+
26
+ 2) Then, immediately after, the ASTRO JSON markers with valid JSON:
25
27
 
26
28
  <!-- ASTRO_JSON_BEGIN -->
27
29
  {
28
30
  "schema_version": 1,
29
31
  "stage_key": "CURRENT_STAGE",
30
32
  "status": "ok",
31
- "summary": "Brief summary of work done",
33
+ "summary": "Brief summary",
32
34
  "decisions": [],
33
35
  "next_actions": [],
34
36
  "tasks": [],
@@ -40,13 +42,13 @@ Output exactly in this order:
40
42
  }
41
43
  <!-- ASTRO_JSON_END -->
42
44
 
43
- CRITICAL REQUIREMENTS:
44
- - Include ALL fields shown above (even if empty arrays/objects)
45
- - stage_key must match the current stage exactly
46
- - status must be "ok", "blocked", or "failed"
47
- - summary must be a non-empty string describing the work
48
- - JSON must be valid syntax and parseable
49
- - Markers must be exact: <!-- ASTRO_JSON_BEGIN --> and <!-- ASTRO_JSON_END -->
45
+ MANDATORY RULES:
46
+ - ALL fields above MUST be present (use empty arrays/objects if no data)
47
+ - stage_key MUST be "CURRENT_STAGE" (will be replaced)
48
+ - status MUST be "ok" unless blocked/failed
49
+ - summary MUST be a string describing the work
50
+ - JSON MUST be valid and parseable
51
+ - Markers MUST be exact as shown
50
52
 
51
- Do not narrate extra text. If blocked, set status="blocked", ask exactly ONE question in questions array, and stop.
53
+ If blocked, set status="blocked" and put ONE question in questions array. Do not output anything else.
52
54
  `;
@@ -42,9 +42,21 @@ export function assertInsideAstro(repoRoot, filePath) {
42
42
  const absRepo = path.resolve(repoRoot);
43
43
  const abs = path.resolve(filePath);
44
44
  const astroRoot = path.resolve(path.join(repoRoot, ".astro"));
45
+ // Allow writing certain files in the repo root
46
+ const relPath = path.relative(repoRoot, filePath);
47
+ const allowedOutside = [
48
+ "stories.md",
49
+ "README.md",
50
+ "CHANGELOG.md",
51
+ "CONTRIBUTING.md",
52
+ "LICENSE",
53
+ ".gitignore"
54
+ ];
45
55
  if (!abs.startsWith(astroRoot + path.sep) && abs !== astroRoot) {
46
- const relPath = path.relative(repoRoot, filePath);
47
- throw new Error(`Refusing to write outside .astro: ${filePath} (relative: ${relPath}, astroRoot: ${astroRoot})`);
56
+ // Check if it's an allowed file in repo root
57
+ if (!allowedOutside.includes(relPath)) {
58
+ throw new Error(`Refusing to write outside .astro: ${filePath} (relative: ${relPath}, astroRoot: ${astroRoot})`);
59
+ }
48
60
  }
49
61
  if (!abs.startsWith(absRepo + path.sep) && abs !== absRepo) {
50
62
  throw new Error(`Refusing to write outside repo root: ${filePath}`);
@@ -6,6 +6,7 @@ import { buildStageDirective, directiveHash } from "../workflow/directives";
6
6
  import { injectChatPrompt } from "../ui/inject";
7
7
  import { nowISO } from "../shared/time";
8
8
  import { newEventId } from "../state/ids";
9
+ import { debug } from "../shared/log";
9
10
  import { createToastManager } from "../ui/toasts";
10
11
  // Agent name mapping for case-sensitive resolution
11
12
  const STAGE_TO_AGENT_MAP = {
@@ -63,7 +64,7 @@ function agentNameForStage(stage, cfg) {
63
64
  function buildDelegationPrompt(opts) {
64
65
  const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
65
66
  const stageUpper = stage_key.toUpperCase();
66
- return [
67
+ const prompt = [
67
68
  `[SYSTEM DIRECTIVE: ASTROCODE — DELEGATE_STAGE_${stageUpper}]`,
68
69
  ``,
69
70
  `Do this now, in order:`,
@@ -81,6 +82,9 @@ function buildDelegationPrompt(opts) {
81
82
  ``,
82
83
  `Important: do NOT do any stage work yourself in orchestrator mode.`,
83
84
  ].join("\n").trim();
85
+ // Debug log the delegation prompt to troubleshoot agent output issues
86
+ debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
87
+ return prompt;
84
88
  }
85
89
  export function createAstroWorkflowProceedTool(opts) {
86
90
  const { ctx, config, db, agents } = opts;
package/dist/ui/inject.js CHANGED
@@ -2,9 +2,11 @@ let isInjecting = false;
2
2
  let queuedInjection = null;
3
3
  export async function injectChatPrompt(opts) {
4
4
  const { ctx, sessionId, text } = opts;
5
+ // Prefix to clearly indicate source as Astro agent
6
+ const prefixedText = `[Astro Agent]\n\n${text}`;
5
7
  if (isInjecting) {
6
8
  // Replace any existing queued injection (keep only latest)
7
- queuedInjection = opts;
9
+ queuedInjection = { ...opts, text: prefixedText };
8
10
  return;
9
11
  }
10
12
  isInjecting = true;
@@ -12,7 +14,7 @@ export async function injectChatPrompt(opts) {
12
14
  await ctx.client.session.prompt({
13
15
  path: { id: sessionId },
14
16
  body: {
15
- parts: [{ type: "text", text }],
17
+ parts: [{ type: "text", text: prefixedText }],
16
18
  },
17
19
  });
18
20
  }
@@ -23,7 +25,7 @@ export async function injectChatPrompt(opts) {
23
25
  const next = queuedInjection;
24
26
  queuedInjection = null;
25
27
  // Schedule next injection asynchronously to prevent recursion
26
- setImmediate(() => injectChatPrompt(next));
28
+ setImmediate(() => injectChatPrompt({ ...queuedInjection, text: prefixedText }));
27
29
  }
28
30
  }
29
31
  }
@@ -1,5 +1,6 @@
1
1
  import { clampLines, normalizeNewlines, stripCodeFences } from "../shared/text";
2
2
  import { z, ZodError } from "zod";
3
+ import { debug } from "../shared/log";
3
4
  export const ASTRO_JSON_BEGIN = "<!-- ASTRO_JSON_BEGIN -->";
4
5
  export const ASTRO_JSON_END = "<!-- ASTRO_JSON_END -->";
5
6
  export const StageKeySchema = z.enum(["frame", "plan", "spec", "implement", "review", "verify", "close"]);
@@ -40,28 +41,46 @@ export function parseStageOutputText(text) {
40
41
  const norm = normalizeNewlines(text ?? "").trim();
41
42
  const beginIdx = norm.indexOf(ASTRO_JSON_BEGIN);
42
43
  const endIdx = norm.indexOf(ASTRO_JSON_END);
44
+ let baton_md = "";
45
+ let jsonRaw = "";
46
+ let fallbackUsed = false;
43
47
  if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) {
48
+ // Fallback: if no markers, check if the entire text is JSON
49
+ try {
50
+ const parsed = JSON.parse(norm);
51
+ const astroJson = AstroJsonSchema.parse(parsed);
52
+ debug("Used JSON fallback parsing (no markers found)", { stage_key: astroJson.stage_key });
53
+ return {
54
+ baton_md: "",
55
+ astro_json: astroJson,
56
+ astro_json_raw: norm,
57
+ error: null
58
+ };
59
+ }
60
+ catch (e) {
61
+ // Not JSON, proceed with marker error
62
+ }
44
63
  return {
45
64
  baton_md: norm,
46
65
  astro_json: null,
47
66
  astro_json_raw: null,
48
- error: `Missing ASTRO JSON markers. Expected markers ${ASTRO_JSON_BEGIN} ... ${ASTRO_JSON_END}`,
67
+ error: `Missing ASTRO JSON markers. Expected markers ${ASTRO_JSON_BEGIN} ... ${ASTRO_JSON_END}. If output is pure JSON, ensure it's wrapped in markers.`,
49
68
  };
50
69
  }
51
70
  const before = norm.slice(0, beginIdx).trim();
52
- const jsonRaw = norm.slice(beginIdx + ASTRO_JSON_BEGIN.length, endIdx).trim();
71
+ jsonRaw = norm.slice(beginIdx + ASTRO_JSON_BEGIN.length, endIdx).trim();
53
72
  const after = norm.slice(endIdx + ASTRO_JSON_END.length).trim();
54
- const baton = [before, after].filter(Boolean).join("\n\n").trim();
73
+ baton_md = [before, after].filter(Boolean).join("\n\n").trim();
55
74
  try {
56
75
  const cleaned = stripCodeFences(jsonRaw).trim();
57
76
  const parsed = JSON.parse(cleaned);
58
77
  const astroJson = AstroJsonSchema.parse(parsed);
59
- return { baton_md: baton, astro_json: astroJson, astro_json_raw: cleaned, error: null };
78
+ return { baton_md, astro_json: astroJson, astro_json_raw: cleaned, error: null };
60
79
  }
61
80
  catch (e) {
62
81
  if (e instanceof ZodError) {
63
82
  return {
64
- baton_md: baton,
83
+ baton_md,
65
84
  astro_json: null,
66
85
  astro_json_raw: jsonRaw,
67
86
  error: `Schema validation failed: ${e.message}. Ensure JSON conforms to ASTRO schema with required fields like stage_key, status, etc.`,
@@ -10,6 +10,8 @@ export function buildContinueDirective(opts) {
10
10
  const body = clampChars(normalizeNewlines([
11
11
  `[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
12
12
  ``,
13
+ `This directive is injected by the Astro agent to continue the workflow.`,
14
+ ``,
13
15
  `Run: \`${run_id}\`${stage_key ? ` Stage: \`${stage_key}\`` : ""}`,
14
16
  ``,
15
17
  `Next action: ${next_action}`,
@@ -34,6 +36,8 @@ export function buildBlockedDirective(opts) {
34
36
  const body = normalizeNewlines([
35
37
  `[SYSTEM DIRECTIVE: ASTROCODE — BLOCKED]`,
36
38
  ``,
39
+ `This directive is injected by the Astro agent indicating the workflow is blocked.`,
40
+ ``,
37
41
  `Run: \`${run_id}\` Stage: \`${stage_key}\``,
38
42
  ``,
39
43
  `You are blocked. Ask the user exactly ONE question (below), then stop.`,
@@ -54,6 +58,8 @@ export function buildRepairDirective(opts) {
54
58
  const body = normalizeNewlines([
55
59
  `[SYSTEM DIRECTIVE: ASTROCODE — REPAIR]`,
56
60
  ``,
61
+ `This directive is injected by the Astro agent after performing a repair pass.`,
62
+ ``,
57
63
  `Astrocode performed a repair pass. Summarize in <= 10 lines and continue.`,
58
64
  ``,
59
65
  `Repair report:`,
@@ -75,6 +81,8 @@ export function buildStageDirective(opts) {
75
81
  const body = clampChars(normalizeNewlines([
76
82
  `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_${stageKeyUpper}]`,
77
83
  ``,
84
+ `This directive is injected by the Astro agent to delegate the stage task.`,
85
+ ``,
78
86
  `You are: \`${stage_agent_name}\``,
79
87
  `Run: \`${run_id}\``,
80
88
  `Story: \`${story_key}\` — ${story_title}`,
@@ -90,7 +98,6 @@ export function buildStageDirective(opts) {
90
98
  ` "schema_version": 1,`,
91
99
  ` "stage_key": "${stage_key}",`,
92
100
  ` "status": "ok",`,
93
- ` "summary": "Brief summary of work done",`,
94
101
  ` ...`,
95
102
  ` }`,
96
103
  ` ${"<!-- ASTRO_JSON_END -->"}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrocode-workflow",
3
- "version": "0.1.19",
3
+ "version": "0.1.23",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,18 +18,20 @@ Operating rules:
18
18
 
19
19
  export const BASE_STAGE_PROMPT = `You are an Astro stage subagent.
20
20
 
21
- Follow the latest [SYSTEM DIRECTIVE: ASTROCODE — STAGE_*] you receive.
21
+ ***CRITICAL: Follow the latest [SYSTEM DIRECTIVE: ASTROCODE — STAGE_*] you receive EXACTLY.***
22
22
 
23
- Output exactly in this order:
24
- 1) Baton markdown (short, structured narrative summary)
25
- 2) Valid ASTRO JSON between markers with ALL required fields (do not omit any):
23
+ Your output MUST be in this EXACT format (no deviations):
24
+
25
+ 1) First, a short baton markdown summary (1-3 sentences).
26
+
27
+ 2) Then, immediately after, the ASTRO JSON markers with valid JSON:
26
28
 
27
29
  <!-- ASTRO_JSON_BEGIN -->
28
30
  {
29
31
  "schema_version": 1,
30
32
  "stage_key": "CURRENT_STAGE",
31
33
  "status": "ok",
32
- "summary": "Brief summary of work done",
34
+ "summary": "Brief summary",
33
35
  "decisions": [],
34
36
  "next_actions": [],
35
37
  "tasks": [],
@@ -41,13 +43,13 @@ Output exactly in this order:
41
43
  }
42
44
  <!-- ASTRO_JSON_END -->
43
45
 
44
- CRITICAL REQUIREMENTS:
45
- - Include ALL fields shown above (even if empty arrays/objects)
46
- - stage_key must match the current stage exactly
47
- - status must be "ok", "blocked", or "failed"
48
- - summary must be a non-empty string describing the work
49
- - JSON must be valid syntax and parseable
50
- - Markers must be exact: <!-- ASTRO_JSON_BEGIN --> and <!-- ASTRO_JSON_END -->
46
+ MANDATORY RULES:
47
+ - ALL fields above MUST be present (use empty arrays/objects if no data)
48
+ - stage_key MUST be "CURRENT_STAGE" (will be replaced)
49
+ - status MUST be "ok" unless blocked/failed
50
+ - summary MUST be a string describing the work
51
+ - JSON MUST be valid and parseable
52
+ - Markers MUST be exact as shown
51
53
 
52
- Do not narrate extra text. If blocked, set status="blocked", ask exactly ONE question in questions array, and stop.
54
+ If blocked, set status="blocked" and put ONE question in questions array. Do not output anything else.
53
55
  `;
@@ -61,10 +61,25 @@ export function assertInsideAstro(repoRoot: string, filePath: string) {
61
61
  const absRepo = path.resolve(repoRoot);
62
62
  const abs = path.resolve(filePath);
63
63
  const astroRoot = path.resolve(path.join(repoRoot, ".astro"));
64
+
65
+ // Allow writing certain files in the repo root
66
+ const relPath = path.relative(repoRoot, filePath);
67
+ const allowedOutside = [
68
+ "stories.md",
69
+ "README.md",
70
+ "CHANGELOG.md",
71
+ "CONTRIBUTING.md",
72
+ "LICENSE",
73
+ ".gitignore"
74
+ ];
75
+
64
76
  if (!abs.startsWith(astroRoot + path.sep) && abs !== astroRoot) {
65
- const relPath = path.relative(repoRoot, filePath);
66
- throw new Error(`Refusing to write outside .astro: ${filePath} (relative: ${relPath}, astroRoot: ${astroRoot})`);
77
+ // Check if it's an allowed file in repo root
78
+ if (!allowedOutside.includes(relPath)) {
79
+ throw new Error(`Refusing to write outside .astro: ${filePath} (relative: ${relPath}, astroRoot: ${astroRoot})`);
80
+ }
67
81
  }
82
+
68
83
  if (!abs.startsWith(absRepo + path.sep) && abs !== absRepo) {
69
84
  throw new Error(`Refusing to write outside repo root: ${filePath}`);
70
85
  }
@@ -9,6 +9,7 @@ import { buildStageDirective, directiveHash } from "../workflow/directives";
9
9
  import { injectChatPrompt } from "../ui/inject";
10
10
  import { nowISO } from "../shared/time";
11
11
  import { newEventId } from "../state/ids";
12
+ import { debug } from "../shared/log";
12
13
  import { createToastManager } from "../ui/toasts";
13
14
 
14
15
  // Agent name mapping for case-sensitive resolution
@@ -79,7 +80,7 @@ function buildDelegationPrompt(opts: {
79
80
  const { stageDirective, run_id, stage_key, stage_agent_name } = opts;
80
81
  const stageUpper = stage_key.toUpperCase();
81
82
 
82
- return [
83
+ const prompt = [
83
84
  `[SYSTEM DIRECTIVE: ASTROCODE — DELEGATE_STAGE_${stageUpper}]`,
84
85
  ``,
85
86
  `Do this now, in order:`,
@@ -97,6 +98,11 @@ function buildDelegationPrompt(opts: {
97
98
  ``,
98
99
  `Important: do NOT do any stage work yourself in orchestrator mode.`,
99
100
  ].join("\n").trim();
101
+
102
+ // Debug log the delegation prompt to troubleshoot agent output issues
103
+ debug(`Delegating stage ${stage_key} to agent ${stage_agent_name}`, { prompt_length: prompt.length });
104
+
105
+ return prompt;
100
106
  }
101
107
 
102
108
  import { AgentConfig } from "@opencode-ai/sdk";
package/src/ui/inject.ts CHANGED
@@ -8,9 +8,12 @@ export async function injectChatPrompt(opts: {
8
8
  }) {
9
9
  const { ctx, sessionId, text } = opts;
10
10
 
11
+ // Prefix to clearly indicate source as Astro agent
12
+ const prefixedText = `[Astro Agent]\n\n${text}`;
13
+
11
14
  if (isInjecting) {
12
15
  // Replace any existing queued injection (keep only latest)
13
- queuedInjection = opts;
16
+ queuedInjection = { ...opts, text: prefixedText };
14
17
  return;
15
18
  }
16
19
 
@@ -20,7 +23,7 @@ export async function injectChatPrompt(opts: {
20
23
  await ctx.client.session.prompt({
21
24
  path: { id: sessionId },
22
25
  body: {
23
- parts: [{ type: "text", text }],
26
+ parts: [{ type: "text", text: prefixedText }],
24
27
  },
25
28
  });
26
29
  } finally {
@@ -30,7 +33,7 @@ export async function injectChatPrompt(opts: {
30
33
  const next = queuedInjection;
31
34
  queuedInjection = null;
32
35
  // Schedule next injection asynchronously to prevent recursion
33
- setImmediate(() => injectChatPrompt(next));
36
+ setImmediate(() => injectChatPrompt({ ...queuedInjection, text: prefixedText }));
34
37
  }
35
38
  }
36
39
  }
@@ -2,6 +2,7 @@ import { tool } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
3
  import { clampLines, normalizeNewlines, stripCodeFences } from "../shared/text";
4
4
  import { z, ZodError } from "zod";
5
+ import { debug } from "../shared/log";
5
6
 
6
7
  export const ASTRO_JSON_BEGIN = "<!-- ASTRO_JSON_BEGIN -->";
7
8
  export const ASTRO_JSON_END = "<!-- ASTRO_JSON_END -->";
@@ -74,30 +75,49 @@ export function parseStageOutputText(text: string): ParsedStageOutput {
74
75
  const beginIdx = norm.indexOf(ASTRO_JSON_BEGIN);
75
76
  const endIdx = norm.indexOf(ASTRO_JSON_END);
76
77
 
78
+ let baton_md = "";
79
+ let jsonRaw = "";
80
+ let fallbackUsed = false;
81
+
77
82
  if (beginIdx === -1 || endIdx === -1 || endIdx <= beginIdx) {
83
+ // Fallback: if no markers, check if the entire text is JSON
84
+ try {
85
+ const parsed = JSON.parse(norm) as unknown;
86
+ const astroJson = AstroJsonSchema.parse(parsed);
87
+ debug("Used JSON fallback parsing (no markers found)", { stage_key: astroJson.stage_key });
88
+ return {
89
+ baton_md: "",
90
+ astro_json: astroJson,
91
+ astro_json_raw: norm,
92
+ error: null
93
+ };
94
+ } catch (e) {
95
+ // Not JSON, proceed with marker error
96
+ }
97
+
78
98
  return {
79
99
  baton_md: norm,
80
100
  astro_json: null,
81
101
  astro_json_raw: null,
82
- error: `Missing ASTRO JSON markers. Expected markers ${ASTRO_JSON_BEGIN} ... ${ASTRO_JSON_END}`,
102
+ error: `Missing ASTRO JSON markers. Expected markers ${ASTRO_JSON_BEGIN} ... ${ASTRO_JSON_END}. If output is pure JSON, ensure it's wrapped in markers.`,
83
103
  };
84
104
  }
85
105
 
86
106
  const before = norm.slice(0, beginIdx).trim();
87
- const jsonRaw = norm.slice(beginIdx + ASTRO_JSON_BEGIN.length, endIdx).trim();
107
+ jsonRaw = norm.slice(beginIdx + ASTRO_JSON_BEGIN.length, endIdx).trim();
88
108
  const after = norm.slice(endIdx + ASTRO_JSON_END.length).trim();
89
109
 
90
- const baton = [before, after].filter(Boolean).join("\n\n").trim();
110
+ baton_md = [before, after].filter(Boolean).join("\n\n").trim();
91
111
 
92
112
  try {
93
113
  const cleaned = stripCodeFences(jsonRaw).trim();
94
114
  const parsed = JSON.parse(cleaned) as unknown;
95
115
  const astroJson = AstroJsonSchema.parse(parsed);
96
- return { baton_md: baton, astro_json: astroJson, astro_json_raw: cleaned, error: null };
116
+ return { baton_md, astro_json: astroJson, astro_json_raw: cleaned, error: null };
97
117
  } catch (e) {
98
118
  if (e instanceof ZodError) {
99
119
  return {
100
- baton_md: baton,
120
+ baton_md,
101
121
  astro_json: null,
102
122
  astro_json_raw: jsonRaw,
103
123
  error: `Schema validation failed: ${e.message}. Ensure JSON conforms to ASTRO schema with required fields like stage_key, status, etc.`,
@@ -32,6 +32,8 @@ export function buildContinueDirective(opts: {
32
32
  [
33
33
  `[SYSTEM DIRECTIVE: ASTROCODE — CONTINUE]`,
34
34
  ``,
35
+ `This directive is injected by the Astro agent to continue the workflow.`,
36
+ ``,
35
37
  `Run: \`${run_id}\`${stage_key ? ` Stage: \`${stage_key}\`` : ""}`,
36
38
  ``,
37
39
  `Next action: ${next_action}`,
@@ -67,6 +69,8 @@ export function buildBlockedDirective(opts: {
67
69
  [
68
70
  `[SYSTEM DIRECTIVE: ASTROCODE — BLOCKED]`,
69
71
  ``,
72
+ `This directive is injected by the Astro agent indicating the workflow is blocked.`,
73
+ ``,
70
74
  `Run: \`${run_id}\` Stage: \`${stage_key}\``,
71
75
  ``,
72
76
  `You are blocked. Ask the user exactly ONE question (below), then stop.`,
@@ -91,6 +95,8 @@ export function buildRepairDirective(opts: { report_md: string }): BuiltDirectiv
91
95
  [
92
96
  `[SYSTEM DIRECTIVE: ASTROCODE — REPAIR]`,
93
97
  ``,
98
+ `This directive is injected by the Astro agent after performing a repair pass.`,
99
+ ``,
94
100
  `Astrocode performed a repair pass. Summarize in <= 10 lines and continue.`,
95
101
  ``,
96
102
  `Repair report:`,
@@ -130,6 +136,8 @@ export function buildStageDirective(opts: {
130
136
  [
131
137
  `[SYSTEM DIRECTIVE: ASTROCODE — STAGE_${stageKeyUpper}]`,
132
138
  ``,
139
+ `This directive is injected by the Astro agent to delegate the stage task.`,
140
+ ``,
133
141
  `You are: \`${stage_agent_name}\``,
134
142
  `Run: \`${run_id}\``,
135
143
  `Story: \`${story_key}\` — ${story_title}`,
@@ -145,7 +153,6 @@ export function buildStageDirective(opts: {
145
153
  ` "schema_version": 1,`,
146
154
  ` "stage_key": "${stage_key}",`,
147
155
  ` "status": "ok",`,
148
- ` "summary": "Brief summary of work done",`,
149
156
  ` ...`,
150
157
  ` }`,
151
158
  ` ${"<!-- ASTRO_JSON_END -->"}`,