astrocode-workflow 0.4.0 → 0.4.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.
Files changed (144) hide show
  1. package/dist/index.js +6 -0
  2. package/dist/shared/metrics.d.ts +66 -0
  3. package/dist/shared/metrics.js +112 -0
  4. package/dist/src/agents/commands.d.ts +9 -0
  5. package/dist/src/agents/commands.js +121 -0
  6. package/dist/src/agents/prompts.d.ts +3 -0
  7. package/dist/src/agents/prompts.js +232 -0
  8. package/dist/src/agents/registry.d.ts +6 -0
  9. package/dist/src/agents/registry.js +242 -0
  10. package/dist/src/agents/types.d.ts +14 -0
  11. package/dist/src/agents/types.js +8 -0
  12. package/dist/src/astro/workflow-runner.d.ts +11 -0
  13. package/dist/src/astro/workflow-runner.js +14 -0
  14. package/dist/src/config/config-handler.d.ts +4 -0
  15. package/dist/src/config/config-handler.js +46 -0
  16. package/dist/src/config/defaults.d.ts +3 -0
  17. package/dist/src/config/defaults.js +3 -0
  18. package/dist/src/config/loader.d.ts +11 -0
  19. package/dist/src/config/loader.js +82 -0
  20. package/dist/src/config/schema.d.ts +195 -0
  21. package/dist/src/config/schema.js +224 -0
  22. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  23. package/dist/src/hooks/continuation-enforcer.js +190 -0
  24. package/dist/src/hooks/inject-provider.d.ts +27 -0
  25. package/dist/src/hooks/inject-provider.js +189 -0
  26. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  27. package/dist/src/hooks/tool-output-truncator.js +57 -0
  28. package/dist/src/index.d.ts +3 -0
  29. package/dist/src/index.js +307 -0
  30. package/dist/src/shared/deep-merge.d.ts +8 -0
  31. package/dist/src/shared/deep-merge.js +25 -0
  32. package/dist/src/shared/hash.d.ts +1 -0
  33. package/dist/src/shared/hash.js +4 -0
  34. package/dist/src/shared/log.d.ts +7 -0
  35. package/dist/src/shared/log.js +24 -0
  36. package/dist/src/shared/metrics.d.ts +66 -0
  37. package/dist/src/shared/metrics.js +112 -0
  38. package/dist/src/shared/model-tuning.d.ts +9 -0
  39. package/dist/src/shared/model-tuning.js +28 -0
  40. package/dist/src/shared/paths.d.ts +19 -0
  41. package/dist/src/shared/paths.js +64 -0
  42. package/dist/src/shared/text.d.ts +4 -0
  43. package/dist/src/shared/text.js +19 -0
  44. package/dist/src/shared/time.d.ts +1 -0
  45. package/dist/src/shared/time.js +3 -0
  46. package/dist/src/state/adapters/index.d.ts +41 -0
  47. package/dist/src/state/adapters/index.js +115 -0
  48. package/dist/src/state/db.d.ts +16 -0
  49. package/dist/src/state/db.js +225 -0
  50. package/dist/src/state/ids.d.ts +8 -0
  51. package/dist/src/state/ids.js +25 -0
  52. package/dist/src/state/repo-lock.d.ts +67 -0
  53. package/dist/src/state/repo-lock.js +580 -0
  54. package/dist/src/state/schema.d.ts +2 -0
  55. package/dist/src/state/schema.js +258 -0
  56. package/dist/src/state/types.d.ts +71 -0
  57. package/dist/src/state/types.js +1 -0
  58. package/dist/src/state/workflow-repo-lock.d.ts +23 -0
  59. package/dist/src/state/workflow-repo-lock.js +83 -0
  60. package/dist/src/tools/artifacts.d.ts +18 -0
  61. package/dist/src/tools/artifacts.js +71 -0
  62. package/dist/src/tools/health.d.ts +8 -0
  63. package/dist/src/tools/health.js +88 -0
  64. package/dist/src/tools/index.d.ts +20 -0
  65. package/dist/src/tools/index.js +94 -0
  66. package/dist/src/tools/init.d.ts +17 -0
  67. package/dist/src/tools/init.js +96 -0
  68. package/dist/src/tools/injects.d.ts +53 -0
  69. package/dist/src/tools/injects.js +325 -0
  70. package/dist/src/tools/lock.d.ts +4 -0
  71. package/dist/src/tools/lock.js +78 -0
  72. package/dist/src/tools/metrics.d.ts +7 -0
  73. package/dist/src/tools/metrics.js +61 -0
  74. package/dist/src/tools/repair.d.ts +8 -0
  75. package/dist/src/tools/repair.js +26 -0
  76. package/dist/src/tools/reset.d.ts +8 -0
  77. package/dist/src/tools/reset.js +92 -0
  78. package/dist/src/tools/run.d.ts +13 -0
  79. package/dist/src/tools/run.js +54 -0
  80. package/dist/src/tools/spec.d.ts +12 -0
  81. package/dist/src/tools/spec.js +44 -0
  82. package/dist/src/tools/stage.d.ts +23 -0
  83. package/dist/src/tools/stage.js +371 -0
  84. package/dist/src/tools/status.d.ts +8 -0
  85. package/dist/src/tools/status.js +125 -0
  86. package/dist/src/tools/story.d.ts +23 -0
  87. package/dist/src/tools/story.js +85 -0
  88. package/dist/src/tools/workflow.d.ts +13 -0
  89. package/dist/src/tools/workflow.js +345 -0
  90. package/dist/src/ui/inject.d.ts +12 -0
  91. package/dist/src/ui/inject.js +107 -0
  92. package/dist/src/ui/toasts.d.ts +13 -0
  93. package/dist/src/ui/toasts.js +39 -0
  94. package/dist/src/workflow/artifacts.d.ts +24 -0
  95. package/dist/src/workflow/artifacts.js +45 -0
  96. package/dist/src/workflow/baton.d.ts +72 -0
  97. package/dist/src/workflow/baton.js +166 -0
  98. package/dist/src/workflow/context.d.ts +20 -0
  99. package/dist/src/workflow/context.js +113 -0
  100. package/dist/src/workflow/directives.d.ts +39 -0
  101. package/dist/src/workflow/directives.js +137 -0
  102. package/dist/src/workflow/repair.d.ts +8 -0
  103. package/dist/src/workflow/repair.js +99 -0
  104. package/dist/src/workflow/state-machine.d.ts +86 -0
  105. package/dist/src/workflow/state-machine.js +216 -0
  106. package/dist/src/workflow/story-helpers.d.ts +9 -0
  107. package/dist/src/workflow/story-helpers.js +13 -0
  108. package/dist/state/db.d.ts +1 -0
  109. package/dist/state/db.js +9 -0
  110. package/dist/state/repo-lock.d.ts +3 -0
  111. package/dist/state/repo-lock.js +29 -0
  112. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  113. package/dist/test/integration/db-transactions.test.js +126 -0
  114. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  115. package/dist/test/integration/injection-metrics.test.js +129 -0
  116. package/dist/tools/health.d.ts +8 -0
  117. package/dist/tools/health.js +119 -0
  118. package/dist/tools/index.js +9 -0
  119. package/dist/tools/metrics.d.ts +7 -0
  120. package/dist/tools/metrics.js +61 -0
  121. package/dist/tools/reset.d.ts +8 -0
  122. package/dist/tools/reset.js +92 -0
  123. package/dist/tools/workflow.js +178 -168
  124. package/dist/ui/inject.js +21 -9
  125. package/package.json +6 -4
  126. package/src/astro/workflow-runner.ts +16 -0
  127. package/src/config/schema.ts +1 -0
  128. package/src/hooks/inject-provider.ts +94 -14
  129. package/src/index.ts +7 -0
  130. package/src/shared/metrics.ts +148 -0
  131. package/src/state/db.ts +10 -1
  132. package/src/state/schema.ts +8 -1
  133. package/src/tools/health.ts +99 -0
  134. package/src/tools/index.ts +12 -3
  135. package/src/tools/init.ts +7 -6
  136. package/src/tools/metrics.ts +71 -0
  137. package/src/tools/repair.ts +8 -4
  138. package/src/tools/reset.ts +100 -0
  139. package/src/tools/stage.ts +1 -0
  140. package/src/tools/status.ts +2 -1
  141. package/src/tools/story.ts +1 -0
  142. package/src/tools/workflow.ts +2 -0
  143. package/src/ui/inject.ts +21 -9
  144. package/src/workflow/repair.ts +2 -2
@@ -0,0 +1,224 @@
1
+ import { z } from "zod";
2
+ export const PermissionValueSchema = z.enum(["ask", "allow", "deny"]);
3
+ const StageKeySchema = z.enum([
4
+ "frame",
5
+ "plan",
6
+ "spec",
7
+ "implement",
8
+ "review",
9
+ "verify",
10
+ "close",
11
+ ]);
12
+ const InjectionModeSchema = z.enum(["visible", "silent"]);
13
+ const TruncationPolicySchema = z
14
+ .object({
15
+ enabled: z.boolean().default(true),
16
+ truncate_all_tool_outputs: z.boolean().default(false),
17
+ max_chars_default: z.number().int().positive().default(200_000),
18
+ max_chars_webfetch: z.number().int().positive().default(40_000),
19
+ max_chars_diff: z.number().int().positive().default(120_000),
20
+ persist_truncated_outputs: z.boolean().default(true),
21
+ })
22
+ .partial()
23
+ .default({});
24
+ const ContextCompactionSchema = z
25
+ .object({
26
+ enabled: z.boolean().default(true),
27
+ snapshot_after_stage_count: z.number().int().positive().default(2),
28
+ snapshot_max_lines: z.number().int().positive().default(60),
29
+ baton_summary_max_lines: z.number().int().positive().default(25),
30
+ inject_max_chars: z.number().int().positive().default(18_000),
31
+ })
32
+ .partial()
33
+ .default({});
34
+ const ContinuationSchema = z
35
+ .object({
36
+ enabled: z.boolean().default(true),
37
+ injection_mode: InjectionModeSchema.default("visible"),
38
+ inject_on_session_idle: z.boolean().default(true),
39
+ session_idle_ms: z.number().int().positive().default(12_000),
40
+ inject_on_tool_done_if_run_active: z.boolean().default(true),
41
+ inject_on_message_done_if_run_active: z.boolean().default(true),
42
+ dedupe_window_ms: z.number().int().positive().default(20_000),
43
+ max_same_directive_repeats: z.number().int().positive().default(1),
44
+ auto_continue: z.boolean().default(false),
45
+ auto_continue_delay_ms: z.number().int().positive().default(1500),
46
+ max_auto_steps_per_session: z.number().int().positive().default(50),
47
+ })
48
+ .partial()
49
+ .default({});
50
+ const ToastsSchema = z
51
+ .object({
52
+ enabled: z.boolean().default(true),
53
+ throttle_ms: z.number().int().positive().default(1500),
54
+ show_run_started: z.boolean().default(true),
55
+ show_stage_started: z.boolean().default(true),
56
+ show_stage_completed: z.boolean().default(true),
57
+ show_stage_failed: z.boolean().default(true),
58
+ show_run_completed: z.boolean().default(true),
59
+ show_auto_continue: z.boolean().default(true),
60
+ })
61
+ .partial()
62
+ .default({});
63
+ const DbSchema = z
64
+ .object({
65
+ path: z.string().default(".astro/astro.db"),
66
+ busy_timeout_ms: z.number().int().positive().default(5000),
67
+ pragmas: z
68
+ .object({
69
+ journal_mode: z.enum(["WAL", "DELETE"]).default("WAL"),
70
+ synchronous: z.enum(["NORMAL", "FULL", "OFF"]).default("NORMAL"),
71
+ foreign_keys: z.boolean().default(true),
72
+ temp_store: z.enum(["DEFAULT", "MEMORY", "FILE"]).default("MEMORY"),
73
+ })
74
+ // Why: Zod's .default({}) requires the *type* to accept an empty object.
75
+ // We still want runtime defaults for each key, so we make keys optional.
76
+ .partial()
77
+ .default({}),
78
+ schema_version_required: z.number().int().positive().default(2),
79
+ allow_auto_migrate: z.boolean().default(true),
80
+ fail_on_downgrade: z.boolean().default(true),
81
+ })
82
+ // Why: allow "db: {}" (or missing db) while still applying defaults.
83
+ .partial()
84
+ .default({});
85
+ const WorkflowSchema = z.object({
86
+ pipeline: z.array(StageKeySchema).default([
87
+ "frame",
88
+ "plan",
89
+ "spec",
90
+ "implement",
91
+ "review",
92
+ "verify",
93
+ "close",
94
+ ]),
95
+ genesis_planning: z.enum(["off", "first_story_only", "always"]).default("first_story_only"),
96
+ default_mode: z.enum(["step", "loop"]).default("step"),
97
+ default_max_steps: z.number().int().positive().default(1),
98
+ loop_max_steps_hard_cap: z.number().int().positive().default(200),
99
+ plan_max_tasks: z.number().int().positive().default(500),
100
+ plan_max_lines: z.number().int().positive().default(2000),
101
+ baton_summary_max_lines: z.number().int().positive().default(20),
102
+ forbid_prompt_narration: z.boolean().default(true),
103
+ single_active_run_per_repo: z.boolean().default(true),
104
+ lock_timeout_ms: z.number().int().positive().default(4000),
105
+ role_first_subagents: z.boolean().default(true),
106
+ evidence_required: z
107
+ .object({
108
+ verify: z.boolean().default(true),
109
+ implement: z.boolean().default(false),
110
+ })
111
+ // NOTE: We want callers to be able to omit the whole object ("{}")
112
+ // while still receiving per-field defaults at parse time.
113
+ .partial()
114
+ .default({}),
115
+ }).partial().default({});
116
+ const ArtifactsSchema = z.object({
117
+ root_dir: z.string().default(".astro"),
118
+ runs_dir: z.string().default(".astro/runs"),
119
+ spec_path: z.string().default(".astro/spec.md"),
120
+ write_full_baton_md: z.boolean().default(true),
121
+ write_baton_summary_md: z.boolean().default(true),
122
+ write_baton_output_json: z.boolean().default(true),
123
+ baton_filename: z.string().default("baton.md"),
124
+ baton_summary_filename: z.string().default("baton.summary.md"),
125
+ baton_json_filename: z.string().default("baton.json"),
126
+ }).partial().default({});
127
+ const AgentsSchema = z.object({
128
+ // Display name for the *primary* agent tab.
129
+ orchestrator_name: z.string().default("Astro"),
130
+ // Display names for the stage sub-agents.
131
+ stage_agent_names: z
132
+ .object({
133
+ frame: z.string().default("Frame"),
134
+ plan: z.string().default("Plan"),
135
+ spec: z.string().default("Spec"),
136
+ implement: z.string().default("Implement"),
137
+ review: z.string().default("Review"),
138
+ verify: z.string().default("Verify"),
139
+ close: z.string().default("Close"),
140
+ })
141
+ .partial()
142
+ .default({}),
143
+ librarian_name: z.string().default("Librarian"),
144
+ explore_name: z.string().default("Explore"),
145
+ qa_name: z.string().default("QA"),
146
+ agent_variant_overrides: z
147
+ .record(z.string(), z.object({
148
+ variant: z.string().optional(),
149
+ model: z.string().optional(),
150
+ }))
151
+ .default({}),
152
+ }).partial().default({});
153
+ const PermissionsSchema = z.object({
154
+ enforce_task_tool_restrictions: z.boolean().default(true),
155
+ deny_delegate_task_in_subagents: z.boolean().default(true),
156
+ }).partial().default({});
157
+ const GitSchema = z.object({
158
+ enabled: z.boolean().default(true),
159
+ allow_dirty_start: z.boolean().default(true),
160
+ auto_branch: z.boolean().default(true),
161
+ branch_prefix: z.string().default("astro/"),
162
+ auto_commit: z.boolean().default(false),
163
+ commit_message_template: z.string().default("astro: {{story_key}} {{title}}"),
164
+ persist_diff_artifacts: z.boolean().default(true),
165
+ }).partial().default({});
166
+ const InjectSchema = z
167
+ .object({
168
+ enabled: z.boolean().default(true),
169
+ scope_allowlist: z.array(z.string()).default(["repo", "global"]),
170
+ type_allowlist: z.array(z.string()).default(["note", "policy"]),
171
+ max_per_turn: z.number().int().positive().default(5),
172
+ auto_approve_queued_stories: z.boolean().default(false).describe("Auto-approve highest priority queued story after workflow tools"),
173
+ })
174
+ .partial()
175
+ .default({});
176
+ const DebugSchema = z
177
+ .object({
178
+ telemetry: z
179
+ .object({
180
+ enabled: z.boolean().default(false),
181
+ })
182
+ .partial()
183
+ .default({}),
184
+ })
185
+ .partial()
186
+ .default({});
187
+ const UiSchema = z
188
+ .object({
189
+ toasts: ToastsSchema,
190
+ continue_prompt: z
191
+ .object({
192
+ enabled: z.boolean().default(true),
193
+ mode: z.enum(["toast_button", "popup", "chat_only"]).default("toast_button"),
194
+ idle_prompt_ms: z.number().int().positive().default(20_000),
195
+ })
196
+ .partial()
197
+ .default({}),
198
+ })
199
+ .partial()
200
+ .default({});
201
+ export const AstrocodeConfigSchema = z.object({
202
+ disabled_hooks: z.array(z.string()).default([]),
203
+ disabled_agents: z.array(z.string()).default([]),
204
+ disabled_commands: z.array(z.string()).default([]),
205
+ determinism: z
206
+ .object({
207
+ mode: z.enum(["on", "off"]).default("on"),
208
+ strict_stage_order: z.boolean().default(true),
209
+ })
210
+ .partial()
211
+ .default({}),
212
+ db: DbSchema,
213
+ workflow: WorkflowSchema,
214
+ continuation: ContinuationSchema,
215
+ truncation: TruncationPolicySchema,
216
+ context_compaction: ContextCompactionSchema,
217
+ artifacts: ArtifactsSchema,
218
+ agents: AgentsSchema,
219
+ permissions: PermissionsSchema,
220
+ git: GitSchema,
221
+ ui: UiSchema,
222
+ inject: InjectSchema,
223
+ debug: DebugSchema,
224
+ }).partial().default({});
@@ -0,0 +1,34 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import type { SqliteDb } from "../state/db";
3
+ type ToolExecuteAfterInput = {
4
+ tool: string;
5
+ sessionID?: string;
6
+ };
7
+ type ChatMessageInput = {
8
+ sessionID: string;
9
+ agent: string;
10
+ };
11
+ type EventInput = {
12
+ event: {
13
+ type: string;
14
+ properties: any;
15
+ };
16
+ };
17
+ type RuntimeState = {
18
+ db: SqliteDb | null;
19
+ limitedMode: boolean;
20
+ limitedModeReason: null | {
21
+ code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
22
+ details: any;
23
+ };
24
+ };
25
+ export declare function createContinuationEnforcer(opts: {
26
+ ctx: any;
27
+ config: AstrocodeConfig;
28
+ runtime: RuntimeState;
29
+ }): {
30
+ onToolAfter(input: ToolExecuteAfterInput): Promise<void>;
31
+ onChatMessage(_input: ChatMessageInput): Promise<void>;
32
+ onEvent(input: EventInput): Promise<void>;
33
+ };
34
+ export {};
@@ -0,0 +1,190 @@
1
+ import { buildContextSnapshot } from "../workflow/context";
2
+ import { decideNextAction, getActiveRun } from "../workflow/state-machine";
3
+ import { buildContinueDirective } from "../workflow/directives";
4
+ import { injectChatPrompt } from "../ui/inject";
5
+ import { nowISO } from "../shared/time";
6
+ import { createToastManager } from "../ui/toasts";
7
+ function msFromIso(iso) {
8
+ const t = Date.parse(iso);
9
+ return Number.isFinite(t) ? t : 0;
10
+ }
11
+ export function createContinuationEnforcer(opts) {
12
+ const { ctx, config, runtime } = opts;
13
+ const { db } = runtime;
14
+ const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
15
+ const sessions = new Map();
16
+ function getState(sessionId) {
17
+ const cur = sessions.get(sessionId);
18
+ if (cur)
19
+ return cur;
20
+ const state = { lastHash: null, lastAtMs: 0, repeats: 0, autoSteps: 0, idleTimer: null, periodicTimer: null };
21
+ sessions.set(sessionId, state);
22
+ return state;
23
+ }
24
+ function clearIdleTimer(sessionId) {
25
+ const s = getState(sessionId);
26
+ if (s.idleTimer) {
27
+ clearTimeout(s.idleTimer);
28
+ s.idleTimer = null;
29
+ }
30
+ }
31
+ function clearPeriodicTimer(sessionId) {
32
+ const s = getState(sessionId);
33
+ if (s.periodicTimer) {
34
+ clearInterval(s.periodicTimer);
35
+ s.periodicTimer = null;
36
+ }
37
+ }
38
+ function scheduleIdleInjection(sessionId) {
39
+ clearIdleTimer(sessionId);
40
+ if (!config.continuation.enabled)
41
+ return;
42
+ if (!config.continuation.inject_on_session_idle)
43
+ return;
44
+ const delay = config.continuation.session_idle_ms;
45
+ const s = getState(sessionId);
46
+ s.idleTimer = setTimeout(() => {
47
+ // Fire and forget
48
+ void maybeInjectContinue(sessionId, "idle_timer");
49
+ }, delay);
50
+ }
51
+ function schedulePeriodicInjection(sessionId) {
52
+ clearPeriodicTimer(sessionId);
53
+ if (!config.continuation.enabled)
54
+ return;
55
+ // Inject every 3 minutes (180,000 ms)
56
+ const interval = 3 * 60 * 1000;
57
+ const s = getState(sessionId);
58
+ s.periodicTimer = setInterval(() => {
59
+ // Fire and forget
60
+ void maybeInjectContinue(sessionId, "periodic_timer");
61
+ }, interval);
62
+ }
63
+ function shouldDedupe(sessionId, directive) {
64
+ const s = getState(sessionId);
65
+ const now = Date.now();
66
+ // Memory window
67
+ if (s.lastHash === directive.hash && now - s.lastAtMs < config.continuation.dedupe_window_ms) {
68
+ if (s.repeats >= config.continuation.max_same_directive_repeats)
69
+ return true;
70
+ }
71
+ // DB window (durable)
72
+ const cutoff = new Date(now - config.continuation.dedupe_window_ms).toISOString();
73
+ const row = db
74
+ .prepare("SELECT COUNT(*) as c FROM continuations WHERE session_id=? AND directive_hash=? AND created_at > ?")
75
+ .get(sessionId, directive.hash, cutoff);
76
+ if ((row?.c ?? 0) >= config.continuation.max_same_directive_repeats)
77
+ return true;
78
+ return false;
79
+ }
80
+ async function recordContinuation(sessionId, runId, directive, reason) {
81
+ db.prepare("INSERT INTO continuations (session_id, run_id, directive_hash, kind, reason, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, runId, directive.hash, directive.kind, reason, nowISO());
82
+ const s = getState(sessionId);
83
+ const now = Date.now();
84
+ if (s.lastHash === directive.hash && now - s.lastAtMs < config.continuation.dedupe_window_ms) {
85
+ s.repeats += 1;
86
+ }
87
+ else {
88
+ s.lastHash = directive.hash;
89
+ s.repeats = 1;
90
+ }
91
+ s.lastAtMs = now;
92
+ }
93
+ function formatNextAction(next) {
94
+ switch (next.kind) {
95
+ case "idle":
96
+ return "No approved stories. Queue/approve a story.";
97
+ case "start_run":
98
+ return `Start run for story ${next.story_key}.`;
99
+ case "delegate_stage":
100
+ return `Delegate stage ${next.stage_key}.`;
101
+ case "await_stage_completion":
102
+ return `Await stage ${next.stage_key} completion. If you have stage output, call astro_stage_complete.`;
103
+ case "complete_run":
104
+ return `Complete run ${next.run_id}.`;
105
+ case "failed":
106
+ return `Run failed at stage ${next.stage_key}: ${next.error_text}`;
107
+ default:
108
+ return "Continue.";
109
+ }
110
+ }
111
+ async function maybeInjectContinue(sessionId, reason) {
112
+ if (!config.continuation.enabled)
113
+ return;
114
+ // Require active run
115
+ const active = getActiveRun(db);
116
+ if (!active)
117
+ return;
118
+ const next = decideNextAction(db, config);
119
+ // If failed, don't auto-inject "continue" — surface via toast and stop.
120
+ if (next.kind === "failed") {
121
+ if (config.ui.toasts.enabled && config.ui.toasts.show_stage_failed) {
122
+ await toasts.show({ title: "Astrocode", message: `Run failed at ${next.stage_key}`, variant: "error" });
123
+ }
124
+ return;
125
+ }
126
+ const nextActionStr = formatNextAction(next);
127
+ const context = buildContextSnapshot({ db, config, run_id: active.run_id, next_action: nextActionStr });
128
+ const directive = buildContinueDirective({
129
+ config,
130
+ run_id: active.run_id,
131
+ stage_key: active.current_stage_key,
132
+ next_action: nextActionStr,
133
+ context_snapshot_md: context,
134
+ });
135
+ if (shouldDedupe(sessionId, directive))
136
+ return;
137
+ await recordContinuation(sessionId, active.run_id, directive, reason);
138
+ // Injection mode
139
+ if (config.continuation.injection_mode === "visible") {
140
+ await injectChatPrompt({ ctx, sessionId, text: directive.body, agent: "Astro" });
141
+ }
142
+ else {
143
+ // Silent mode is TODO: requires experimental.chat.messages.transform.
144
+ // For v2-alpha, we fall back to visible injection but mark it.
145
+ await injectChatPrompt({ ctx, sessionId, text: directive.body + "\n\n(Injected in silent mode fallback)", agent: "Astro" });
146
+ }
147
+ if (config.ui.toasts.enabled && config.ui.toasts.show_auto_continue) {
148
+ await toasts.show({ title: "Astrocode", message: "Continue directive injected", variant: "info" });
149
+ }
150
+ }
151
+ // Public hook handlers
152
+ return {
153
+ async onToolAfter(input) {
154
+ const sessionId = input.sessionID ?? ctx.sessionID;
155
+ if (!sessionId)
156
+ return;
157
+ if (!config.continuation.inject_on_tool_done_if_run_active)
158
+ return;
159
+ // Inject continuation immediately after any tool execution
160
+ void maybeInjectContinue(sessionId, "tool_execution");
161
+ scheduleIdleInjection(sessionId);
162
+ },
163
+ async onChatMessage(_input) {
164
+ if (!config.continuation.inject_on_message_done_if_run_active)
165
+ return;
166
+ scheduleIdleInjection(_input.sessionID);
167
+ },
168
+ async onEvent(input) {
169
+ const type = input.event.type;
170
+ const sessionId = input.event.properties?.sessionID;
171
+ if (!sessionId)
172
+ return;
173
+ if (type === "session.idle") {
174
+ if (!config.continuation.inject_on_session_idle)
175
+ return;
176
+ await maybeInjectContinue(sessionId, "session.idle");
177
+ }
178
+ if (type === "session.created") {
179
+ // When a session is created and there is an active run, nudge.
180
+ scheduleIdleInjection(sessionId);
181
+ schedulePeriodicInjection(sessionId);
182
+ }
183
+ if (type === "session.deleted") {
184
+ clearIdleTimer(sessionId);
185
+ clearPeriodicTimer(sessionId);
186
+ sessions.delete(sessionId);
187
+ }
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,27 @@
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
+ type ToolExecuteAfterInput = {
8
+ tool: string;
9
+ sessionID?: string;
10
+ };
11
+ type RuntimeState = {
12
+ db: SqliteDb | null;
13
+ limitedMode: boolean;
14
+ limitedModeReason: null | {
15
+ code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
16
+ details: any;
17
+ };
18
+ };
19
+ export declare function createInjectProvider(opts: {
20
+ ctx: any;
21
+ config: AstrocodeConfig;
22
+ runtime: RuntimeState;
23
+ }): {
24
+ onChatMessage(input: ChatMessageInput): Promise<void>;
25
+ onToolAfter(input: ToolExecuteAfterInput): Promise<void>;
26
+ };
27
+ export {};
@@ -0,0 +1,189 @@
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, runtime } = opts;
6
+ const { db } = runtime;
7
+ // Cache to avoid re-injecting the same injects repeatedly
8
+ // Map of inject_id -> last injected timestamp
9
+ const injectedCache = new Map();
10
+ function shouldSkipInject(injectId, nowMs) {
11
+ const lastInjected = injectedCache.get(injectId);
12
+ if (!lastInjected)
13
+ return false;
14
+ // REDUCED cooldown from 5 minutes to 1 minute
15
+ // This allows injects to appear more frequently during workflow
16
+ const cooldownMs = 1 * 60 * 1000;
17
+ return nowMs - lastInjected < cooldownMs;
18
+ }
19
+ function markInjected(injectId, nowMs) {
20
+ injectedCache.set(injectId, nowMs);
21
+ // Clean up old entries to prevent memory leak
22
+ // Remove entries older than 10 minutes
23
+ const tenMinutesAgo = nowMs - (10 * 60 * 1000);
24
+ for (const [id, timestamp] of injectedCache.entries()) {
25
+ if (timestamp < tenMinutesAgo) {
26
+ injectedCache.delete(id);
27
+ }
28
+ }
29
+ }
30
+ function getInjectionDiagnostics(nowIso, scopeAllowlist, typeAllowlist) {
31
+ // Get ALL injects to analyze filtering
32
+ const allInjects = db.prepare("SELECT * FROM injects").all();
33
+ let total = allInjects.length;
34
+ let selected = 0;
35
+ let skippedExpired = 0;
36
+ let skippedScope = 0;
37
+ let skippedType = 0;
38
+ let eligibleIds = [];
39
+ for (const inject of allInjects) {
40
+ // Check expiration
41
+ if (inject.expires_at && inject.expires_at <= nowIso) {
42
+ skippedExpired++;
43
+ continue;
44
+ }
45
+ // Check scope
46
+ if (!scopeAllowlist.includes(inject.scope)) {
47
+ skippedScope++;
48
+ continue;
49
+ }
50
+ // Check type
51
+ if (!typeAllowlist.includes(inject.type)) {
52
+ skippedType++;
53
+ continue;
54
+ }
55
+ // This inject is eligible
56
+ selected++;
57
+ eligibleIds.push(inject.inject_id);
58
+ }
59
+ return {
60
+ now: nowIso,
61
+ scopes_considered: scopeAllowlist,
62
+ types_considered: typeAllowlist,
63
+ total_injects: total,
64
+ selected_eligible: selected,
65
+ skipped: {
66
+ expired: skippedExpired,
67
+ scope: skippedScope,
68
+ type: skippedType,
69
+ },
70
+ eligible_ids: eligibleIds,
71
+ };
72
+ }
73
+ async function injectEligibleInjects(sessionId, context) {
74
+ const now = nowISO();
75
+ const nowMs = Date.now();
76
+ // Get allowlists from config or defaults
77
+ const scopeAllowlist = config.inject?.scope_allowlist ?? ["repo", "global"];
78
+ const typeAllowlist = config.inject?.type_allowlist ?? ["note", "policy"];
79
+ const EMIT_TELEMETRY = config.debug?.telemetry?.enabled ?? false;
80
+ // Get diagnostic data
81
+ const diagnostics = getInjectionDiagnostics(now, scopeAllowlist, typeAllowlist);
82
+ const eligibleInjects = selectEligibleInjects(db, {
83
+ nowIso: now,
84
+ scopeAllowlist,
85
+ typeAllowlist,
86
+ limit: config.inject?.max_per_turn ?? 5,
87
+ });
88
+ let injected = 0;
89
+ let skippedDeduped = 0;
90
+ if (eligibleInjects.length === 0) {
91
+ // Log when no injects are eligible
92
+ if (EMIT_TELEMETRY) {
93
+ // eslint-disable-next-line no-console
94
+ console.log(`[Astrocode:inject] ${now} context=${context ?? 'unknown'} selected=0 injected=0 skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:0}`);
95
+ }
96
+ return;
97
+ }
98
+ // Inject each eligible inject, skipping recently injected ones
99
+ for (const inject of eligibleInjects) {
100
+ if (shouldSkipInject(inject.inject_id, nowMs)) {
101
+ skippedDeduped++;
102
+ continue;
103
+ }
104
+ // Format as injection message
105
+ const formattedText = `[Inject: ${inject.title}]\n\n${inject.body_md}`;
106
+ try {
107
+ await injectChatPrompt({
108
+ ctx,
109
+ sessionId,
110
+ text: formattedText,
111
+ agent: "Astrocode"
112
+ });
113
+ injected++;
114
+ markInjected(inject.inject_id, nowMs);
115
+ }
116
+ catch (err) {
117
+ // Log injection failures but don't crash
118
+ // eslint-disable-next-line no-console
119
+ console.error(`[Astrocode:inject] Failed to inject ${inject.inject_id}:`, err);
120
+ }
121
+ }
122
+ // Log diagnostic summary
123
+ if (EMIT_TELEMETRY || injected > 0) {
124
+ // eslint-disable-next-line no-console
125
+ console.log(`[Astrocode:inject] ${now} context=${context ?? 'unknown'} selected=${diagnostics.selected_eligible} injected=${injected} skipped={expired:${diagnostics.skipped.expired} scope:${diagnostics.skipped.scope} type:${diagnostics.skipped.type} deduped:${skippedDeduped}}`);
126
+ }
127
+ }
128
+ // Workflow-related tools that should trigger inject + auto-approval
129
+ const WORKFLOW_TOOLS = new Set([
130
+ 'astro_workflow_proceed',
131
+ 'astro_story_queue',
132
+ 'astro_story_approve',
133
+ 'astro_stage_start',
134
+ 'astro_stage_complete',
135
+ 'astro_stage_fail',
136
+ 'astro_run_abort',
137
+ ]);
138
+ // Auto-approve queued stories if enabled
139
+ async function maybeAutoApprove(sessionId) {
140
+ if (!config.inject?.auto_approve_queued_stories)
141
+ return;
142
+ try {
143
+ // Get all queued stories
144
+ const queued = db.prepare("SELECT story_key, title FROM stories WHERE state='queued' ORDER BY priority DESC, created_at ASC").all();
145
+ if (queued.length === 0)
146
+ return;
147
+ // Auto-approve the highest priority queued story
148
+ const story = queued[0];
149
+ db.prepare("UPDATE stories SET state='approved', updated_at=? WHERE story_key=?").run(nowISO(), story.story_key);
150
+ // eslint-disable-next-line no-console
151
+ console.log(`[Astrocode:inject] Auto-approved story ${story.story_key}: ${story.title}`);
152
+ // Inject a notification about the auto-approval
153
+ await injectChatPrompt({
154
+ ctx,
155
+ sessionId,
156
+ text: `✅ Auto-approved story ${story.story_key}: ${story.title}`,
157
+ agent: "Astrocode"
158
+ });
159
+ }
160
+ catch (err) {
161
+ // eslint-disable-next-line no-console
162
+ console.error(`[Astrocode:inject] Auto-approval failed:`, err);
163
+ }
164
+ }
165
+ // Public hook handlers
166
+ return {
167
+ async onChatMessage(input) {
168
+ if (!config.inject?.enabled)
169
+ return;
170
+ // Inject eligible injects before processing the user's message
171
+ await injectEligibleInjects(input.sessionID, 'chat_message');
172
+ },
173
+ async onToolAfter(input) {
174
+ if (!config.inject?.enabled)
175
+ return;
176
+ // Only inject after workflow-related tools
177
+ if (!WORKFLOW_TOOLS.has(input.tool))
178
+ return;
179
+ // Extract sessionID (same pattern as continuation enforcer)
180
+ const sessionId = input.sessionID ?? ctx.sessionID;
181
+ if (!sessionId)
182
+ return;
183
+ // Auto-approve queued stories if enabled
184
+ await maybeAutoApprove(sessionId);
185
+ // Inject eligible injects after workflow tool execution
186
+ await injectEligibleInjects(sessionId, `tool_after:${input.tool}`);
187
+ },
188
+ };
189
+ }
@@ -0,0 +1,25 @@
1
+ import type { AstrocodeConfig } from "../config/schema";
2
+ import type { SqliteDb } from "../state/db";
3
+ type ToolExecuteAfterInput = {
4
+ tool: string;
5
+ sessionID?: string;
6
+ };
7
+ type ToolExecuteAfterOutput = {
8
+ title?: string;
9
+ output?: string;
10
+ metadata?: Record<string, any>;
11
+ };
12
+ type RuntimeState = {
13
+ db: SqliteDb | null;
14
+ limitedMode: boolean;
15
+ limitedModeReason: null | {
16
+ code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
17
+ details: any;
18
+ };
19
+ };
20
+ export declare function createToolOutputTruncatorHook(opts: {
21
+ ctx: any;
22
+ config: AstrocodeConfig;
23
+ runtime: RuntimeState;
24
+ }): (input: ToolExecuteAfterInput, output: ToolExecuteAfterOutput) => Promise<void>;
25
+ export {};