astrocode-workflow 0.1.53 → 0.1.55

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.
@@ -172,6 +172,12 @@ export declare const AstrocodeConfigSchema: z.ZodDefault<z.ZodObject<{
172
172
  idle_prompt_ms: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
173
173
  }, z.core.$strip>>>;
174
174
  }, z.core.$strip>>>;
175
+ inject: z.ZodOptional<z.ZodDefault<z.ZodObject<{
176
+ enabled: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
177
+ scope_allowlist: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
178
+ type_allowlist: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
179
+ max_per_turn: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
180
+ }, z.core.$strip>>>;
175
181
  }, z.core.$strip>>;
176
182
  export type AstrocodeConfig = z.infer<typeof AstrocodeConfigSchema>;
177
183
  export {};
@@ -161,6 +161,15 @@ const GitSchema = z.object({
161
161
  commit_message_template: z.string().default("astro: {{story_key}} {{title}}"),
162
162
  persist_diff_artifacts: z.boolean().default(true),
163
163
  }).partial().default({});
164
+ const InjectSchema = z
165
+ .object({
166
+ enabled: z.boolean().default(true),
167
+ scope_allowlist: z.array(z.string()).default(["repo", "global"]),
168
+ type_allowlist: z.array(z.string()).default(["note", "policy"]),
169
+ max_per_turn: z.number().int().positive().default(5),
170
+ })
171
+ .partial()
172
+ .default({});
164
173
  const UiSchema = z
165
174
  .object({
166
175
  toasts: ToastsSchema,
@@ -196,4 +205,5 @@ export const AstrocodeConfigSchema = z.object({
196
205
  permissions: PermissionsSchema,
197
206
  git: GitSchema,
198
207
  ui: UiSchema,
208
+ inject: InjectSchema,
199
209
  }).partial().default({});
@@ -0,0 +1,14 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import type { SqliteDb } from "../state/db";
3
+ type ChatMessageInput = {
4
+ sessionID: string;
5
+ agent: string;
6
+ };
7
+ export declare function createInjectProvider(opts: {
8
+ ctx: any;
9
+ config: AstrocodeConfig;
10
+ db: SqliteDb;
11
+ }): {
12
+ onChatMessage(input: ChatMessageInput): Promise<void>;
13
+ };
14
+ export {};
@@ -0,0 +1,57 @@
1
+ import { selectEligibleInjects } from "../tools/injects";
2
+ import { injectChatPrompt } from "../ui/inject";
3
+ import { nowISO } from "../shared/time";
4
+ export function createInjectProvider(opts) {
5
+ const { ctx, config, db } = opts;
6
+ // Cache to avoid re-injecting the same injects repeatedly
7
+ const injectedCache = new Map();
8
+ function shouldSkipInject(injectId, nowMs) {
9
+ const lastInjected = injectedCache.get(injectId);
10
+ if (!lastInjected)
11
+ return false;
12
+ // Skip if injected within the last 5 minutes (configurable?)
13
+ const cooldownMs = 5 * 60 * 1000;
14
+ return nowMs - lastInjected < cooldownMs;
15
+ }
16
+ function markInjected(injectId, nowMs) {
17
+ injectedCache.set(injectId, nowMs);
18
+ }
19
+ async function injectEligibleInjects(sessionId) {
20
+ const now = nowISO();
21
+ const nowMs = Date.now();
22
+ // Get eligible injects - use allowlists from config or defaults
23
+ const scopeAllowlist = config.inject?.scope_allowlist ?? ["repo", "global"];
24
+ const typeAllowlist = config.inject?.type_allowlist ?? ["note", "policy"];
25
+ const eligibleInjects = selectEligibleInjects(db, {
26
+ nowIso: now,
27
+ scopeAllowlist,
28
+ typeAllowlist,
29
+ limit: config.inject?.max_per_turn ?? 5,
30
+ });
31
+ if (eligibleInjects.length === 0)
32
+ return;
33
+ // Inject each eligible inject, skipping recently injected ones
34
+ for (const inject of eligibleInjects) {
35
+ if (shouldSkipInject(inject.inject_id, nowMs))
36
+ continue;
37
+ // Format as injection message
38
+ const formattedText = `[Inject: ${inject.title}]\n\n${inject.body_md}`;
39
+ await injectChatPrompt({
40
+ ctx,
41
+ sessionId,
42
+ text: formattedText,
43
+ agent: "Astrocode"
44
+ });
45
+ markInjected(inject.inject_id, nowMs);
46
+ }
47
+ }
48
+ // Public hook handlers
49
+ return {
50
+ async onChatMessage(input) {
51
+ if (!config.inject?.enabled)
52
+ return;
53
+ // Inject eligible injects before processing the user's message
54
+ await injectEligibleInjects(input.sessionID);
55
+ },
56
+ };
57
+ }
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { getAstroPaths, ensureAstroDirs } from "./shared/paths";
5
5
  import { createAstroTools } from "./tools";
6
6
  import { createContinuationEnforcer } from "./hooks/continuation-enforcer";
7
7
  import { createToolOutputTruncatorHook } from "./hooks/tool-output-truncator";
8
+ import { createInjectProvider } from "./hooks/inject-provider";
8
9
  import { createToastManager } from "./ui/toasts";
9
10
  import { createAstroAgents } from "./agents/registry";
10
11
  const Astrocode = async (ctx) => {
@@ -21,6 +22,7 @@ const Astrocode = async (ctx) => {
21
22
  let configHandler = null;
22
23
  let continuation = null;
23
24
  let truncatorHook = null;
25
+ let injectProvider = null;
24
26
  let toasts = null;
25
27
  try {
26
28
  db = openSqlite(paths.dbPath, { busyTimeoutMs: pluginConfig.db.busy_timeout_ms });
@@ -31,6 +33,7 @@ const Astrocode = async (ctx) => {
31
33
  tools = createAstroTools({ ctx, config: pluginConfig, db, agents });
32
34
  continuation = createContinuationEnforcer({ ctx, config: pluginConfig, db });
33
35
  truncatorHook = createToolOutputTruncatorHook({ ctx, config: pluginConfig, db });
36
+ injectProvider = createInjectProvider({ ctx, config: pluginConfig, db });
34
37
  toasts = createToastManager({ ctx, throttleMs: pluginConfig.ui.toasts.throttle_ms });
35
38
  }
36
39
  catch (e) {
@@ -46,6 +49,7 @@ const Astrocode = async (ctx) => {
46
49
  tools = createAstroTools({ ctx, config: pluginConfig, db, agents });
47
50
  continuation = null;
48
51
  truncatorHook = null;
52
+ injectProvider = null;
49
53
  toasts = null;
50
54
  }
51
55
  return {
@@ -80,6 +84,9 @@ const Astrocode = async (ctx) => {
80
84
  }
81
85
  },
82
86
  "chat.message": async (input, output) => {
87
+ if (injectProvider && !pluginConfig.disabled_hooks.includes("inject-provider")) {
88
+ await injectProvider.onChatMessage(input);
89
+ }
83
90
  if (continuation && !pluginConfig.disabled_hooks.includes("continuation-enforcer")) {
84
91
  await continuation.onChatMessage(input);
85
92
  }
@@ -21,3 +21,28 @@ export declare function createAstroInjectSearchTool(opts: {
21
21
  config: AstrocodeConfig;
22
22
  db: SqliteDb;
23
23
  }): ToolDefinition;
24
+ export type InjectRow = {
25
+ inject_id: string;
26
+ type: string;
27
+ title: string;
28
+ body_md: string;
29
+ tags_json: string;
30
+ scope: string;
31
+ source: string;
32
+ priority: number;
33
+ expires_at: string | null;
34
+ sha256: string;
35
+ created_at: string;
36
+ updated_at: string;
37
+ };
38
+ export declare function selectEligibleInjects(db: SqliteDb, opts: {
39
+ nowIso: string;
40
+ scopeAllowlist: string[];
41
+ typeAllowlist: string[];
42
+ limit?: number;
43
+ }): InjectRow[];
44
+ export declare function createAstroInjectEligibleTool(opts: {
45
+ ctx: any;
46
+ config: AstrocodeConfig;
47
+ db: SqliteDb;
48
+ }): ToolDefinition;
@@ -97,3 +97,43 @@ export function createAstroInjectSearchTool(opts) {
97
97
  },
98
98
  });
99
99
  }
100
+ export function selectEligibleInjects(db, opts) {
101
+ const { nowIso, scopeAllowlist, typeAllowlist, limit = 50 } = opts;
102
+ // Build placeholders safely
103
+ const scopeQs = scopeAllowlist.map(() => "?").join(", ");
104
+ const typeQs = typeAllowlist.map(() => "?").join(", ");
105
+ const sql = `
106
+ SELECT *
107
+ FROM injects
108
+ WHERE (expires_at IS NULL OR expires_at > ?)
109
+ AND scope IN (${scopeQs})
110
+ AND type IN (${typeQs})
111
+ ORDER BY priority DESC, updated_at DESC
112
+ LIMIT ?
113
+ `;
114
+ const params = [nowIso, ...scopeAllowlist, ...typeAllowlist, limit];
115
+ return db.prepare(sql).all(...params);
116
+ }
117
+ export function createAstroInjectEligibleTool(opts) {
118
+ const { db } = opts;
119
+ return tool({
120
+ description: "Debug: show which injects are eligible right now for injection.",
121
+ args: {
122
+ scopes_json: tool.schema.string().default('["repo","global"]'),
123
+ types_json: tool.schema.string().default('["note","policy"]'),
124
+ limit: tool.schema.number().int().positive().default(50),
125
+ },
126
+ execute: async ({ scopes_json, types_json, limit }) => {
127
+ const now = nowISO();
128
+ const scopes = JSON.parse(scopes_json);
129
+ const types = JSON.parse(types_json);
130
+ const rows = selectEligibleInjects(db, {
131
+ nowIso: now,
132
+ scopeAllowlist: scopes,
133
+ typeAllowlist: types,
134
+ limit,
135
+ });
136
+ return JSON.stringify({ now, count: rows.length, rows }, null, 2);
137
+ },
138
+ });
139
+ }
@@ -10,13 +10,13 @@ import { debug } from "../shared/log";
10
10
  import { createToastManager } from "../ui/toasts";
11
11
  // Agent name mapping for case-sensitive resolution
12
12
  const STAGE_TO_AGENT_MAP = {
13
- frame: "frame",
14
- plan: "plan",
15
- spec: "spec",
16
- implement: "implement",
17
- review: "review",
18
- verify: "verify",
19
- close: "close"
13
+ frame: "Frame",
14
+ plan: "Plan",
15
+ spec: "Spec",
16
+ implement: "Implement",
17
+ review: "Review",
18
+ verify: "Verify",
19
+ close: "Close"
20
20
  };
21
21
  function resolveAgentName(stageKey, config) {
22
22
  // Use configurable agent names from config, fallback to hardcoded map, then General
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astrocode-workflow",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -194,6 +194,16 @@ const GitSchema = z.object({
194
194
  persist_diff_artifacts: z.boolean().default(true),
195
195
  }).partial().default({});
196
196
 
197
+ const InjectSchema = z
198
+ .object({
199
+ enabled: z.boolean().default(true),
200
+ scope_allowlist: z.array(z.string()).default(["repo", "global"]),
201
+ type_allowlist: z.array(z.string()).default(["note", "policy"]),
202
+ max_per_turn: z.number().int().positive().default(5),
203
+ })
204
+ .partial()
205
+ .default({});
206
+
197
207
  const UiSchema = z
198
208
  .object({
199
209
  toasts: ToastsSchema,
@@ -232,6 +242,7 @@ export const AstrocodeConfigSchema = z.object({
232
242
  permissions: PermissionsSchema,
233
243
  git: GitSchema,
234
244
  ui: UiSchema,
245
+ inject: InjectSchema,
235
246
  }).partial().default({});
236
247
 
237
248
  export type AstrocodeConfig = z.infer<typeof AstrocodeConfigSchema>;
@@ -0,0 +1,79 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import type { SqliteDb } from "../state/db";
3
+ import { selectEligibleInjects } from "../tools/injects";
4
+ import { injectChatPrompt } from "../ui/inject";
5
+ import { nowISO } from "../shared/time";
6
+
7
+ type ChatMessageInput = {
8
+ sessionID: string;
9
+ agent: string;
10
+ };
11
+
12
+ export function createInjectProvider(opts: {
13
+ ctx: any;
14
+ config: AstrocodeConfig;
15
+ db: SqliteDb;
16
+ }) {
17
+ const { ctx, config, db } = opts;
18
+
19
+ // Cache to avoid re-injecting the same injects repeatedly
20
+ const injectedCache = new Map<string, number>();
21
+
22
+ function shouldSkipInject(injectId: string, nowMs: number): boolean {
23
+ const lastInjected = injectedCache.get(injectId);
24
+ if (!lastInjected) return false;
25
+
26
+ // Skip if injected within the last 5 minutes (configurable?)
27
+ const cooldownMs = 5 * 60 * 1000;
28
+ return nowMs - lastInjected < cooldownMs;
29
+ }
30
+
31
+ function markInjected(injectId: string, nowMs: number) {
32
+ injectedCache.set(injectId, nowMs);
33
+ }
34
+
35
+ async function injectEligibleInjects(sessionId: string) {
36
+ const now = nowISO();
37
+ const nowMs = Date.now();
38
+
39
+ // Get eligible injects - use allowlists from config or defaults
40
+ const scopeAllowlist = config.inject?.scope_allowlist ?? ["repo", "global"];
41
+ const typeAllowlist = config.inject?.type_allowlist ?? ["note", "policy"];
42
+
43
+ const eligibleInjects = selectEligibleInjects(db, {
44
+ nowIso: now,
45
+ scopeAllowlist,
46
+ typeAllowlist,
47
+ limit: config.inject?.max_per_turn ?? 5,
48
+ });
49
+
50
+ if (eligibleInjects.length === 0) return;
51
+
52
+ // Inject each eligible inject, skipping recently injected ones
53
+ for (const inject of eligibleInjects) {
54
+ if (shouldSkipInject(inject.inject_id, nowMs)) continue;
55
+
56
+ // Format as injection message
57
+ const formattedText = `[Inject: ${inject.title}]\n\n${inject.body_md}`;
58
+
59
+ await injectChatPrompt({
60
+ ctx,
61
+ sessionId,
62
+ text: formattedText,
63
+ agent: "Astrocode"
64
+ });
65
+
66
+ markInjected(inject.inject_id, nowMs);
67
+ }
68
+ }
69
+
70
+ // Public hook handlers
71
+ return {
72
+ async onChatMessage(input: ChatMessageInput) {
73
+ if (!config.inject?.enabled) return;
74
+
75
+ // Inject eligible injects before processing the user's message
76
+ await injectEligibleInjects(input.sessionID);
77
+ },
78
+ };
79
+ }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import { getAstroPaths, ensureAstroDirs } from "./shared/paths";
6
6
  import { createAstroTools } from "./tools";
7
7
  import { createContinuationEnforcer } from "./hooks/continuation-enforcer";
8
8
  import { createToolOutputTruncatorHook } from "./hooks/tool-output-truncator";
9
+ import { createInjectProvider } from "./hooks/inject-provider";
9
10
  import { createToastManager } from "./ui/toasts";
10
11
  import { createAstroAgents } from "./agents/registry";
11
12
  import { info, warn } from "./shared/log";
@@ -25,10 +26,11 @@ const Astrocode: Plugin = async (ctx) => {
25
26
 
26
27
  let db: any = null;
27
28
  let tools: any = null;
28
- let configHandler: any = null;
29
- let continuation: any = null;
30
- let truncatorHook: any = null;
31
- let toasts: any = null;
29
+ let configHandler: any = null;
30
+ let continuation: any = null;
31
+ let truncatorHook: any = null;
32
+ let injectProvider: any = null;
33
+ let toasts: any = null;
32
34
 
33
35
  try {
34
36
 
@@ -41,6 +43,7 @@ const Astrocode: Plugin = async (ctx) => {
41
43
  tools = createAstroTools({ ctx, config: pluginConfig, db, agents });
42
44
  continuation = createContinuationEnforcer({ ctx, config: pluginConfig, db });
43
45
  truncatorHook = createToolOutputTruncatorHook({ ctx, config: pluginConfig, db });
46
+ injectProvider = createInjectProvider({ ctx, config: pluginConfig, db });
44
47
  toasts = createToastManager({ ctx, throttleMs: pluginConfig.ui.toasts.throttle_ms });
45
48
  } catch (e) {
46
49
  // Database initialization failed - setup limited mode
@@ -58,6 +61,7 @@ const Astrocode: Plugin = async (ctx) => {
58
61
  tools = createAstroTools({ ctx, config: pluginConfig, db, agents });
59
62
  continuation = null;
60
63
  truncatorHook = null;
64
+ injectProvider = null;
61
65
  toasts = null;
62
66
  }
63
67
 
@@ -102,6 +106,9 @@ const Astrocode: Plugin = async (ctx) => {
102
106
  },
103
107
 
104
108
  "chat.message": async (input: any, output: any) => {
109
+ if (injectProvider && !pluginConfig.disabled_hooks.includes("inject-provider")) {
110
+ await injectProvider.onChatMessage(input);
111
+ }
105
112
  if (continuation && !pluginConfig.disabled_hooks.includes("continuation-enforcer")) {
106
113
  await continuation.onChatMessage(input);
107
114
  }
@@ -106,3 +106,71 @@ export function createAstroInjectSearchTool(opts: { ctx: any; config: AstrocodeC
106
106
  },
107
107
  });
108
108
  }
109
+
110
+ export type InjectRow = {
111
+ inject_id: string;
112
+ type: string;
113
+ title: string;
114
+ body_md: string;
115
+ tags_json: string;
116
+ scope: string;
117
+ source: string;
118
+ priority: number;
119
+ expires_at: string | null;
120
+ sha256: string;
121
+ created_at: string;
122
+ updated_at: string;
123
+ };
124
+
125
+ export function selectEligibleInjects(db: SqliteDb, opts: {
126
+ nowIso: string;
127
+ scopeAllowlist: string[];
128
+ typeAllowlist: string[];
129
+ limit?: number;
130
+ }): InjectRow[] {
131
+ const { nowIso, scopeAllowlist, typeAllowlist, limit = 50 } = opts;
132
+
133
+ // Build placeholders safely
134
+ const scopeQs = scopeAllowlist.map(() => "?").join(", ");
135
+ const typeQs = typeAllowlist.map(() => "?").join(", ");
136
+
137
+ const sql = `
138
+ SELECT *
139
+ FROM injects
140
+ WHERE (expires_at IS NULL OR expires_at > ?)
141
+ AND scope IN (${scopeQs})
142
+ AND type IN (${typeQs})
143
+ ORDER BY priority DESC, updated_at DESC
144
+ LIMIT ?
145
+ `;
146
+
147
+ const params = [nowIso, ...scopeAllowlist, ...typeAllowlist, limit];
148
+ return db.prepare(sql).all(...params) as InjectRow[];
149
+ }
150
+
151
+ export function createAstroInjectEligibleTool(opts: { ctx: any; config: AstrocodeConfig; db: SqliteDb }): ToolDefinition {
152
+ const { db } = opts;
153
+
154
+ return tool({
155
+ description: "Debug: show which injects are eligible right now for injection.",
156
+ args: {
157
+ scopes_json: tool.schema.string().default('["repo","global"]'),
158
+ types_json: tool.schema.string().default('["note","policy"]'),
159
+ limit: tool.schema.number().int().positive().default(50),
160
+ },
161
+ execute: async ({ scopes_json, types_json, limit }) => {
162
+ const now = nowISO();
163
+ const scopes = JSON.parse(scopes_json) as string[];
164
+ const types = JSON.parse(types_json) as string[];
165
+
166
+ const rows = selectEligibleInjects(db, {
167
+ nowIso: now,
168
+ scopeAllowlist: scopes,
169
+ typeAllowlist: types,
170
+ limit,
171
+ });
172
+
173
+ return JSON.stringify({ now, count: rows.length, rows }, null, 2);
174
+ },
175
+ });
176
+ }
@@ -14,13 +14,13 @@ import { createToastManager } from "../ui/toasts";
14
14
 
15
15
  // Agent name mapping for case-sensitive resolution
16
16
  const STAGE_TO_AGENT_MAP: Record<string, string> = {
17
- frame: "frame",
18
- plan: "plan",
19
- spec: "spec",
20
- implement: "implement",
21
- review: "review",
22
- verify: "verify",
23
- close: "close"
17
+ frame: "Frame",
18
+ plan: "Plan",
19
+ spec: "Spec",
20
+ implement: "Implement",
21
+ review: "Review",
22
+ verify: "Verify",
23
+ close: "Close"
24
24
  };
25
25
 
26
26
  function resolveAgentName(stageKey: StageKey, config: AstrocodeConfig): string {