astrocode-workflow 0.1.56 → 0.1.57

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 SCHEMA_VERSION = 2;
2
- export declare const SCHEMA_SQL = "\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS repo_state (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n schema_version INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n spec_hash_before TEXT,\n spec_hash_after TEXT,\n last_run_id TEXT,\n last_story_key TEXT,\n last_event_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS epics (\n epic_key TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n state TEXT NOT NULL DEFAULT 'active',\n priority INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS story_drafts (\n draft_id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n meta_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS story_keyseq (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n next_story_num INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS stories (\n story_key TEXT PRIMARY KEY,\n epic_key TEXT,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n state TEXT NOT NULL DEFAULT 'queued', -- queued|approved|in_progress|done|blocked|archived\n priority INTEGER NOT NULL DEFAULT 0,\n approved_at TEXT,\n locked_by_run_id TEXT,\n locked_at TEXT,\n in_progress INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n FOREIGN KEY (epic_key) REFERENCES epics(epic_key)\n);\n\nCREATE TABLE IF NOT EXISTS runs (\n run_id TEXT PRIMARY KEY,\n story_key TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'created', -- created|running|completed|failed|aborted\n pipeline_stages_json TEXT NOT NULL DEFAULT '[]',\n current_stage_key TEXT,\n created_at TEXT NOT NULL,\n started_at TEXT,\n completed_at TEXT,\n updated_at TEXT NOT NULL,\n error_text TEXT,\n FOREIGN KEY (story_key) REFERENCES stories(story_key)\n);\n\nCREATE TABLE IF NOT EXISTS stage_runs (\n stage_run_id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL,\n stage_key TEXT NOT NULL,\n stage_index INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending', -- pending|running|completed|failed|skipped\n created_at TEXT NOT NULL,\n subagent_type TEXT,\n subagent_session_id TEXT,\n started_at TEXT,\n completed_at TEXT,\n updated_at TEXT NOT NULL,\n baton_path TEXT,\n summary_md TEXT,\n output_json TEXT,\n error_text TEXT,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS artifacts (\n artifact_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n type TEXT NOT NULL, -- plan|baton|evidence|diff|log|summary|commit|tool_output|snapshot\n path TEXT NOT NULL,\n sha256 TEXT,\n meta_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS tool_runs (\n tool_run_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n tool_name TEXT NOT NULL,\n args_json TEXT NOT NULL DEFAULT '{}',\n output_summary TEXT NOT NULL DEFAULT '',\n output_artifact_id TEXT,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS events (\n event_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n type TEXT NOT NULL,\n body_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS injects (\n inject_id TEXT PRIMARY KEY,\n type TEXT NOT NULL DEFAULT 'note',\n title TEXT NOT NULL,\n body_md TEXT NOT NULL,\n tags_json TEXT NOT NULL DEFAULT '[]',\n scope TEXT NOT NULL DEFAULT 'repo', -- repo|run:<id>|story:<key>|global\n source TEXT NOT NULL DEFAULT 'user', -- user|tool|agent|import\n priority INTEGER NOT NULL DEFAULT 50,\n expires_at TEXT,\n sha256 TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS running_batches (\n batch_id TEXT PRIMARY KEY,\n run_id TEXT,\n session_id TEXT,\n status TEXT NOT NULL DEFAULT 'running', -- running|completed|failed|aborted\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS workflow_metrics (\n metric_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n name TEXT NOT NULL,\n value_num REAL,\n value_text TEXT,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS template_intents (\n intent_key TEXT PRIMARY KEY,\n body_md TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\n-- vNext tables\n\nCREATE TABLE IF NOT EXISTS story_relations (\n parent_story_key TEXT NOT NULL,\n child_story_key TEXT NOT NULL,\n relation_type TEXT NOT NULL DEFAULT 'split',\n reason TEXT NOT NULL DEFAULT '',\n created_at TEXT NOT NULL,\n PRIMARY KEY (parent_story_key, child_story_key),\n FOREIGN KEY (parent_story_key) REFERENCES stories(story_key),\n FOREIGN KEY (child_story_key) REFERENCES stories(story_key)\n);\n\nCREATE TABLE IF NOT EXISTS continuations (\n continuation_id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n run_id TEXT,\n directive_hash TEXT NOT NULL,\n kind TEXT NOT NULL, -- continue|stage|blocked|repair\n reason TEXT NOT NULL DEFAULT '',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_continuations_session_created ON continuations(session_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_continuations_run_created ON continuations(run_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS context_snapshots (\n snapshot_id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL,\n stage_key TEXT NOT NULL,\n summary_md TEXT NOT NULL,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_context_snapshots_run_created ON context_snapshots(run_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS agent_sessions (\n session_id TEXT PRIMARY KEY,\n parent_session_id TEXT,\n agent_name TEXT NOT NULL,\n run_id TEXT,\n stage_key TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\n-- Indexes\n\nCREATE INDEX IF NOT EXISTS idx_stories_state ON stories(state);\nCREATE INDEX IF NOT EXISTS idx_runs_story ON runs(story_key);\nCREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);\nCREATE INDEX IF NOT EXISTS idx_stage_runs_run ON stage_runs(run_id, stage_index);\nCREATE INDEX IF NOT EXISTS idx_artifacts_run_stage ON artifacts(run_id, stage_key, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_tool_runs_run ON tool_runs(run_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_injects_scope_priority ON injects(scope, priority DESC, created_at DESC);\n\n-- Stronger invariants (SQLite partial indexes)\n-- Only one run may be 'running' at a time (single-repo harness by default).\nCREATE UNIQUE INDEX IF NOT EXISTS uniq_single_running_run\n ON runs(status)\n WHERE status = 'running';\n\n-- Only one story may be in_progress=1 at a time (pairs with single running run).\nCREATE UNIQUE INDEX IF NOT EXISTS uniq_single_in_progress_story\n ON stories(in_progress)\n WHERE in_progress = 1;\n\n";
2
+ export declare const SCHEMA_SQL = "\nPRAGMA foreign_keys = ON;\n\nCREATE TABLE IF NOT EXISTS repo_state (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n schema_version INTEGER NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n spec_hash_before TEXT,\n spec_hash_after TEXT,\n last_run_id TEXT,\n last_story_key TEXT,\n last_event_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS settings (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS epics (\n epic_key TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n state TEXT NOT NULL DEFAULT 'active',\n priority INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS story_drafts (\n draft_id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n meta_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS story_keyseq (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n next_story_num INTEGER NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS stories (\n story_key TEXT PRIMARY KEY,\n epic_key TEXT,\n title TEXT NOT NULL,\n body_md TEXT NOT NULL DEFAULT '',\n state TEXT NOT NULL DEFAULT 'queued', -- queued|approved|in_progress|done|blocked|archived\n priority INTEGER NOT NULL DEFAULT 0,\n approved_at TEXT,\n locked_by_run_id TEXT,\n locked_at TEXT,\n in_progress INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n FOREIGN KEY (epic_key) REFERENCES epics(epic_key)\n);\n\nCREATE TABLE IF NOT EXISTS runs (\n run_id TEXT PRIMARY KEY,\n story_key TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'created', -- created|running|completed|failed|aborted\n pipeline_stages_json TEXT NOT NULL DEFAULT '[]',\n current_stage_key TEXT,\n created_at TEXT NOT NULL,\n started_at TEXT,\n completed_at TEXT,\n updated_at TEXT NOT NULL,\n error_text TEXT,\n FOREIGN KEY (story_key) REFERENCES stories(story_key)\n);\n\nCREATE TABLE IF NOT EXISTS stage_runs (\n stage_run_id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL,\n stage_key TEXT NOT NULL,\n stage_index INTEGER NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending', -- pending|running|completed|failed|skipped\n created_at TEXT NOT NULL,\n subagent_type TEXT,\n subagent_session_id TEXT,\n started_at TEXT,\n completed_at TEXT,\n updated_at TEXT NOT NULL,\n baton_path TEXT,\n summary_md TEXT,\n output_json TEXT,\n error_text TEXT,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS artifacts (\n artifact_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n type TEXT NOT NULL, -- plan|baton|evidence|diff|log|summary|commit|tool_output|snapshot\n path TEXT NOT NULL,\n sha256 TEXT,\n meta_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS tool_runs (\n tool_run_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n tool_name TEXT NOT NULL,\n args_json TEXT NOT NULL DEFAULT '{}',\n output_summary TEXT NOT NULL DEFAULT '',\n output_artifact_id TEXT,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS events (\n event_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n type TEXT NOT NULL,\n body_json TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS injects (\n inject_id TEXT PRIMARY KEY,\n type TEXT NOT NULL DEFAULT 'note',\n title TEXT NOT NULL,\n body_md TEXT NOT NULL,\n tags_json TEXT NOT NULL DEFAULT '[]',\n scope TEXT NOT NULL DEFAULT 'repo', -- repo|run:<id>|story:<key>|global\n source TEXT NOT NULL DEFAULT 'user', -- user|tool|agent|import\n priority INTEGER NOT NULL DEFAULT 50,\n expires_at TEXT,\n sha256 TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS running_batches (\n batch_id TEXT PRIMARY KEY,\n run_id TEXT,\n session_id TEXT,\n status TEXT NOT NULL DEFAULT 'running', -- running|completed|failed|aborted\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS workflow_metrics (\n metric_id TEXT PRIMARY KEY,\n run_id TEXT,\n stage_key TEXT,\n name TEXT NOT NULL,\n value_num REAL,\n value_text TEXT,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS template_intents (\n intent_key TEXT PRIMARY KEY,\n body_md TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\n-- vNext tables\n\nCREATE TABLE IF NOT EXISTS story_relations (\n parent_story_key TEXT NOT NULL,\n child_story_key TEXT NOT NULL,\n relation_type TEXT NOT NULL DEFAULT 'split',\n reason TEXT NOT NULL DEFAULT '',\n created_at TEXT NOT NULL,\n PRIMARY KEY (parent_story_key, child_story_key),\n FOREIGN KEY (parent_story_key) REFERENCES stories(story_key),\n FOREIGN KEY (child_story_key) REFERENCES stories(story_key)\n);\n\nCREATE TABLE IF NOT EXISTS continuations (\n continuation_id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n run_id TEXT,\n directive_hash TEXT NOT NULL,\n kind TEXT NOT NULL, -- continue|stage|blocked|repair\n reason TEXT NOT NULL DEFAULT '',\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_continuations_session_created ON continuations(session_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_continuations_run_created ON continuations(run_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS context_snapshots (\n snapshot_id TEXT PRIMARY KEY,\n run_id TEXT NOT NULL,\n stage_key TEXT NOT NULL,\n summary_md TEXT NOT NULL,\n created_at TEXT NOT NULL,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_context_snapshots_run_created ON context_snapshots(run_id, created_at DESC);\n\nCREATE TABLE IF NOT EXISTS agent_sessions (\n session_id TEXT PRIMARY KEY,\n parent_session_id TEXT,\n agent_name TEXT NOT NULL,\n run_id TEXT,\n stage_key TEXT,\n status TEXT NOT NULL DEFAULT 'active',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\n-- Indexes\n\nCREATE INDEX IF NOT EXISTS idx_stories_state ON stories(state);\nCREATE INDEX IF NOT EXISTS idx_runs_story ON runs(story_key);\nCREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);\nCREATE INDEX IF NOT EXISTS idx_stage_runs_run ON stage_runs(run_id, stage_index);\nCREATE INDEX IF NOT EXISTS idx_artifacts_run_stage ON artifacts(run_id, stage_key, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_tool_runs_run ON tool_runs(run_id, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_injects_scope_priority ON injects(scope, priority DESC, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_injects_scope_type_priority_updated ON injects(scope, type, priority DESC, updated_at DESC);\nCREATE INDEX IF NOT EXISTS idx_injects_expires ON injects(expires_at) WHERE expires_at IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_injects_sha256 ON injects(sha256) WHERE sha256 IS NOT NULL;\n\n-- Stronger invariants (SQLite partial indexes)\n-- Only one run may be 'running' at a time (single-repo harness by default).\nCREATE UNIQUE INDEX IF NOT EXISTS uniq_single_running_run\n ON runs(status)\n WHERE status = 'running';\n\n-- Only one story may be in_progress=1 at a time (pairs with single running run).\nCREATE UNIQUE INDEX IF NOT EXISTS uniq_single_in_progress_story\n ON stories(in_progress)\n WHERE in_progress = 1;\n\n";
@@ -233,6 +233,9 @@ CREATE INDEX IF NOT EXISTS idx_artifacts_run_stage ON artifacts(run_id, stage_ke
233
233
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id, created_at DESC);
234
234
  CREATE INDEX IF NOT EXISTS idx_tool_runs_run ON tool_runs(run_id, created_at DESC);
235
235
  CREATE INDEX IF NOT EXISTS idx_injects_scope_priority ON injects(scope, priority DESC, created_at DESC);
236
+ CREATE INDEX IF NOT EXISTS idx_injects_scope_type_priority_updated ON injects(scope, type, priority DESC, updated_at DESC);
237
+ CREATE INDEX IF NOT EXISTS idx_injects_expires ON injects(expires_at) WHERE expires_at IS NOT NULL;
238
+ CREATE INDEX IF NOT EXISTS idx_injects_sha256 ON injects(sha256) WHERE sha256 IS NOT NULL;
236
239
 
237
240
  -- Stronger invariants (SQLite partial indexes)
238
241
  -- Only one run may be 'running' at a time (single-repo harness by default).
@@ -1,6 +1,29 @@
1
1
  import { tool } from "@opencode-ai/plugin/tool";
2
+ import { withTx } from "../state/db";
2
3
  import { nowISO } from "../shared/time";
3
4
  import { sha256Hex } from "../shared/hash";
5
+ const VALID_INJECT_TYPES = ['note', 'policy', 'reminder', 'debug'];
6
+ function validateInjectType(type) {
7
+ if (!VALID_INJECT_TYPES.includes(type)) {
8
+ throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(', ')}`);
9
+ }
10
+ return type;
11
+ }
12
+ function validateTimestamp(timestamp) {
13
+ if (!timestamp)
14
+ return null;
15
+ // Check if it's a valid ISO 8601 timestamp with Z suffix
16
+ const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
17
+ if (!isoRegex.test(timestamp)) {
18
+ throw new Error(`Invalid timestamp format. Expected ISO 8601 UTC with Z suffix (e.g., "2026-01-23T13:05:19.000Z"), got: "${timestamp}"`);
19
+ }
20
+ // Additional validation: ensure it's parseable and represents a valid date
21
+ const parsed = new Date(timestamp);
22
+ if (isNaN(parsed.getTime())) {
23
+ throw new Error(`Invalid timestamp: "${timestamp}" does not represent a valid date`);
24
+ }
25
+ return timestamp;
26
+ }
4
27
  function newInjectId() {
5
28
  return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
6
29
  }
@@ -23,13 +46,33 @@ export function createAstroInjectPutTool(opts) {
23
46
  const id = inject_id ?? newInjectId();
24
47
  const now = nowISO();
25
48
  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}`;
49
+ // Validate inputs
50
+ const validatedType = validateInjectType(type);
51
+ const validatedExpiresAt = validateTimestamp(expires_at);
52
+ return withTx(db, () => {
53
+ const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id);
54
+ if (existing) {
55
+ // Use INSERT ... ON CONFLICT for atomic updates
56
+ db.prepare(`
57
+ INSERT INTO injects (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at)
58
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
59
+ ON CONFLICT(inject_id) DO UPDATE SET
60
+ type=excluded.type,
61
+ title=excluded.title,
62
+ body_md=excluded.body_md,
63
+ tags_json=excluded.tags_json,
64
+ scope=excluded.scope,
65
+ source=excluded.source,
66
+ priority=excluded.priority,
67
+ expires_at=excluded.expires_at,
68
+ sha256=excluded.sha256,
69
+ updated_at=excluded.updated_at
70
+ `).run(id, type, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
71
+ return `✅ Updated inject ${id}: ${title}`;
72
+ }
73
+ 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, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
74
+ return `✅ Created inject ${id}: ${title}`;
75
+ });
33
76
  },
34
77
  });
35
78
  }
@@ -220,15 +220,23 @@ export function createAstroWorkflowProceedTool(opts) {
220
220
  return false;
221
221
  };
222
222
  if (!agentExists(agentName)) {
223
+ const originalAgent = agentName;
223
224
  console.warn(`[Astrocode] Agent ${agentName} not found in config. Falling back to orchestrator.`);
224
- // Skip General fallback for stage agents to avoid malformed output
225
+ // First fallback: orchestrator
225
226
  agentName = config.agents?.orchestrator_name || "Astro";
226
227
  if (!agentExists(agentName)) {
227
- throw new Error(`Critical: No agents available for delegation. Primary: ${resolveAgentName(next.stage_key, config, agents, warnings)}, Orchestrator: ${agentName}`);
228
+ console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
229
+ // Final fallback: General (guaranteed to exist)
230
+ agentName = "General";
231
+ if (!agentExists(agentName)) {
232
+ throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
233
+ }
228
234
  }
229
235
  }
230
236
  withTx(db, () => {
231
237
  startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
238
+ // Log delegation observability
239
+ console.log(`[Astrocode:delegate] run_id=${active.run_id} stage=${next.stage_key} agent=${agentName} fallback=${agentName !== resolveAgentName(next.stage_key, config, agents) ? 'yes' : 'no'}`);
232
240
  });
233
241
  if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {
234
242
  await toasts.show({ title: "Astrocode", message: `Stage started: ${next.stage_key}`, variant: "info" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrocode-workflow",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -235,6 +235,9 @@ CREATE INDEX IF NOT EXISTS idx_artifacts_run_stage ON artifacts(run_id, stage_ke
235
235
  CREATE INDEX IF NOT EXISTS idx_events_run ON events(run_id, created_at DESC);
236
236
  CREATE INDEX IF NOT EXISTS idx_tool_runs_run ON tool_runs(run_id, created_at DESC);
237
237
  CREATE INDEX IF NOT EXISTS idx_injects_scope_priority ON injects(scope, priority DESC, created_at DESC);
238
+ CREATE INDEX IF NOT EXISTS idx_injects_scope_type_priority_updated ON injects(scope, type, priority DESC, updated_at DESC);
239
+ CREATE INDEX IF NOT EXISTS idx_injects_expires ON injects(expires_at) WHERE expires_at IS NOT NULL;
240
+ CREATE INDEX IF NOT EXISTS idx_injects_sha256 ON injects(sha256) WHERE sha256 IS NOT NULL;
238
241
 
239
242
  -- Stronger invariants (SQLite partial indexes)
240
243
  -- Only one run may be 'running' at a time (single-repo harness by default).
@@ -1,9 +1,38 @@
1
1
  import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool";
2
2
  import type { AstrocodeConfig } from "../config/schema";
3
3
  import type { SqliteDb } from "../state/db";
4
+ import { withTx } from "../state/db";
4
5
  import { nowISO } from "../shared/time";
5
6
  import { sha256Hex } from "../shared/hash";
6
7
 
8
+ const VALID_INJECT_TYPES = ['note', 'policy', 'reminder', 'debug'] as const;
9
+ type InjectType = typeof VALID_INJECT_TYPES[number];
10
+
11
+ function validateInjectType(type: string): InjectType {
12
+ if (!VALID_INJECT_TYPES.includes(type as InjectType)) {
13
+ throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(', ')}`);
14
+ }
15
+ return type as InjectType;
16
+ }
17
+
18
+ function validateTimestamp(timestamp: string | null): string | null {
19
+ if (!timestamp) return null;
20
+
21
+ // Check if it's a valid ISO 8601 timestamp with Z suffix
22
+ const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
23
+ if (!isoRegex.test(timestamp)) {
24
+ throw new Error(`Invalid timestamp format. Expected ISO 8601 UTC with Z suffix (e.g., "2026-01-23T13:05:19.000Z"), got: "${timestamp}"`);
25
+ }
26
+
27
+ // Additional validation: ensure it's parseable and represents a valid date
28
+ const parsed = new Date(timestamp);
29
+ if (isNaN(parsed.getTime())) {
30
+ throw new Error(`Invalid timestamp: "${timestamp}" does not represent a valid date`);
31
+ }
32
+
33
+ return timestamp;
34
+ }
35
+
7
36
  function newInjectId(): string {
8
37
  return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
9
38
  }
@@ -29,20 +58,39 @@ export function createAstroInjectPutTool(opts: { ctx: any; config: AstrocodeConf
29
58
  const now = nowISO();
30
59
  const sha = sha256Hex(body_md);
31
60
 
32
- const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id) as any;
61
+ // Validate inputs
62
+ const validatedType = validateInjectType(type);
63
+ const validatedExpiresAt = validateTimestamp(expires_at);
64
+
65
+ return withTx(db, () => {
66
+ const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id) as any;
67
+
68
+ if (existing) {
69
+ // Use INSERT ... ON CONFLICT for atomic updates
70
+ db.prepare(`
71
+ INSERT INTO injects (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at)
72
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
73
+ ON CONFLICT(inject_id) DO UPDATE SET
74
+ type=excluded.type,
75
+ title=excluded.title,
76
+ body_md=excluded.body_md,
77
+ tags_json=excluded.tags_json,
78
+ scope=excluded.scope,
79
+ source=excluded.source,
80
+ priority=excluded.priority,
81
+ expires_at=excluded.expires_at,
82
+ sha256=excluded.sha256,
83
+ updated_at=excluded.updated_at
84
+ `).run(id, type, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
85
+ return `✅ Updated inject ${id}: ${title}`;
86
+ }
33
87
 
34
- if (existing) {
35
88
  db.prepare(
36
- "UPDATE injects SET type=?, title=?, body_md=?, tags_json=?, scope=?, source=?, priority=?, expires_at=?, sha256=?, updated_at=? WHERE inject_id=?"
37
- ).run(type, title, body_md, tags_json, scope, source, priority, expires_at ?? null, sha, now, id);
38
- return `✅ Updated inject ${id}: ${title}`;
39
- }
40
-
41
- db.prepare(
42
- "INSERT INTO injects (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
43
- ).run(id, type, title, body_md, tags_json, scope, source, priority, expires_at ?? null, sha, now, now);
89
+ "INSERT INTO injects (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
90
+ ).run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
44
91
 
45
- return `✅ Created inject ${id}: ${title}`;
92
+ return `✅ Created inject ${id}: ${title}`;
93
+ });
46
94
  },
47
95
  });
48
96
  }
@@ -262,17 +262,26 @@ export function createAstroWorkflowProceedTool(opts: { ctx: any; config: Astroco
262
262
  return false;
263
263
  };
264
264
 
265
- if (!agentExists(agentName)) {
266
- console.warn(`[Astrocode] Agent ${agentName} not found in config. Falling back to orchestrator.`);
267
- // Skip General fallback for stage agents to avoid malformed output
268
- agentName = config.agents?.orchestrator_name || "Astro";
269
- if (!agentExists(agentName)) {
270
- throw new Error(`Critical: No agents available for delegation. Primary: ${resolveAgentName(next.stage_key, config, agents, warnings)}, Orchestrator: ${agentName}`);
271
- }
272
- }
265
+ if (!agentExists(agentName)) {
266
+ const originalAgent = agentName;
267
+ console.warn(`[Astrocode] Agent ${agentName} not found in config. Falling back to orchestrator.`);
268
+ // First fallback: orchestrator
269
+ agentName = config.agents?.orchestrator_name || "Astro";
270
+ if (!agentExists(agentName)) {
271
+ console.warn(`[Astrocode] Orchestrator ${agentName} not available. Falling back to General.`);
272
+ // Final fallback: General (guaranteed to exist)
273
+ agentName = "General";
274
+ if (!agentExists(agentName)) {
275
+ throw new Error(`Critical: No agents available for delegation. Primary: ${originalAgent}, Orchestrator: ${config.agents?.orchestrator_name || "Astro"}, General: unavailable`);
276
+ }
277
+ }
278
+ }
273
279
 
274
280
  withTx(db, () => {
275
281
  startStage(db, active.run_id, next.stage_key, { subagent_type: agentName });
282
+
283
+ // Log delegation observability
284
+ console.log(`[Astrocode:delegate] run_id=${active.run_id} stage=${next.stage_key} agent=${agentName} fallback=${agentName !== resolveAgentName(next.stage_key, config, agents) ? 'yes' : 'no'}`);
276
285
  });
277
286
 
278
287
  if (config.ui.toasts.enabled && config.ui.toasts.show_stage_started) {