astrocode-workflow 0.3.0 → 0.3.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 (139) 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/config/config-handler.d.ts +4 -0
  13. package/dist/src/config/config-handler.js +46 -0
  14. package/dist/src/config/defaults.d.ts +3 -0
  15. package/dist/src/config/defaults.js +3 -0
  16. package/dist/src/config/loader.d.ts +11 -0
  17. package/dist/src/config/loader.js +82 -0
  18. package/dist/src/config/schema.d.ts +194 -0
  19. package/dist/src/config/schema.js +223 -0
  20. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  21. package/dist/src/hooks/continuation-enforcer.js +190 -0
  22. package/dist/src/hooks/inject-provider.d.ts +22 -0
  23. package/dist/src/hooks/inject-provider.js +120 -0
  24. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  25. package/dist/src/hooks/tool-output-truncator.js +57 -0
  26. package/dist/src/index.d.ts +3 -0
  27. package/dist/src/index.js +308 -0
  28. package/dist/src/shared/deep-merge.d.ts +8 -0
  29. package/dist/src/shared/deep-merge.js +25 -0
  30. package/dist/src/shared/hash.d.ts +1 -0
  31. package/dist/src/shared/hash.js +4 -0
  32. package/dist/src/shared/log.d.ts +7 -0
  33. package/dist/src/shared/log.js +24 -0
  34. package/dist/src/shared/metrics.d.ts +66 -0
  35. package/dist/src/shared/metrics.js +112 -0
  36. package/dist/src/shared/model-tuning.d.ts +9 -0
  37. package/dist/src/shared/model-tuning.js +28 -0
  38. package/dist/src/shared/paths.d.ts +19 -0
  39. package/dist/src/shared/paths.js +64 -0
  40. package/dist/src/shared/text.d.ts +4 -0
  41. package/dist/src/shared/text.js +19 -0
  42. package/dist/src/shared/time.d.ts +1 -0
  43. package/dist/src/shared/time.js +3 -0
  44. package/dist/src/state/adapters/index.d.ts +41 -0
  45. package/dist/src/state/adapters/index.js +115 -0
  46. package/dist/src/state/db.d.ts +16 -0
  47. package/dist/src/state/db.js +225 -0
  48. package/dist/src/state/ids.d.ts +8 -0
  49. package/dist/src/state/ids.js +25 -0
  50. package/dist/src/state/repo-lock.d.ts +3 -0
  51. package/dist/src/state/repo-lock.js +29 -0
  52. package/dist/src/state/schema.d.ts +2 -0
  53. package/dist/src/state/schema.js +251 -0
  54. package/dist/src/state/types.d.ts +71 -0
  55. package/dist/src/state/types.js +1 -0
  56. package/dist/src/tools/artifacts.d.ts +18 -0
  57. package/dist/src/tools/artifacts.js +71 -0
  58. package/dist/src/tools/health.d.ts +8 -0
  59. package/dist/src/tools/health.js +119 -0
  60. package/dist/src/tools/index.d.ts +20 -0
  61. package/dist/src/tools/index.js +94 -0
  62. package/dist/src/tools/init.d.ts +17 -0
  63. package/dist/src/tools/init.js +96 -0
  64. package/dist/src/tools/injects.d.ts +53 -0
  65. package/dist/src/tools/injects.js +325 -0
  66. package/dist/src/tools/metrics.d.ts +7 -0
  67. package/dist/src/tools/metrics.js +61 -0
  68. package/dist/src/tools/repair.d.ts +8 -0
  69. package/dist/src/tools/repair.js +25 -0
  70. package/dist/src/tools/reset.d.ts +8 -0
  71. package/dist/src/tools/reset.js +92 -0
  72. package/dist/src/tools/run.d.ts +13 -0
  73. package/dist/src/tools/run.js +54 -0
  74. package/dist/src/tools/spec.d.ts +12 -0
  75. package/dist/src/tools/spec.js +44 -0
  76. package/dist/src/tools/stage.d.ts +23 -0
  77. package/dist/src/tools/stage.js +371 -0
  78. package/dist/src/tools/status.d.ts +8 -0
  79. package/dist/src/tools/status.js +125 -0
  80. package/dist/src/tools/story.d.ts +23 -0
  81. package/dist/src/tools/story.js +85 -0
  82. package/dist/src/tools/workflow.d.ts +13 -0
  83. package/dist/src/tools/workflow.js +355 -0
  84. package/dist/src/ui/inject.d.ts +12 -0
  85. package/dist/src/ui/inject.js +107 -0
  86. package/dist/src/ui/toasts.d.ts +13 -0
  87. package/dist/src/ui/toasts.js +39 -0
  88. package/dist/src/workflow/artifacts.d.ts +24 -0
  89. package/dist/src/workflow/artifacts.js +45 -0
  90. package/dist/src/workflow/baton.d.ts +72 -0
  91. package/dist/src/workflow/baton.js +166 -0
  92. package/dist/src/workflow/context.d.ts +20 -0
  93. package/dist/src/workflow/context.js +113 -0
  94. package/dist/src/workflow/directives.d.ts +39 -0
  95. package/dist/src/workflow/directives.js +137 -0
  96. package/dist/src/workflow/repair.d.ts +8 -0
  97. package/dist/src/workflow/repair.js +99 -0
  98. package/dist/src/workflow/state-machine.d.ts +86 -0
  99. package/dist/src/workflow/state-machine.js +216 -0
  100. package/dist/src/workflow/story-helpers.d.ts +9 -0
  101. package/dist/src/workflow/story-helpers.js +13 -0
  102. package/dist/state/db.d.ts +1 -0
  103. package/dist/state/db.js +9 -0
  104. package/dist/state/repo-lock.d.ts +3 -0
  105. package/dist/state/repo-lock.js +29 -0
  106. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  107. package/dist/test/integration/db-transactions.test.js +126 -0
  108. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  109. package/dist/test/integration/injection-metrics.test.js +129 -0
  110. package/dist/tools/health.d.ts +8 -0
  111. package/dist/tools/health.js +119 -0
  112. package/dist/tools/index.js +9 -0
  113. package/dist/tools/metrics.d.ts +7 -0
  114. package/dist/tools/metrics.js +61 -0
  115. package/dist/tools/reset.d.ts +8 -0
  116. package/dist/tools/reset.js +92 -0
  117. package/dist/tools/workflow.js +210 -215
  118. package/dist/ui/inject.d.ts +6 -0
  119. package/dist/ui/inject.js +86 -67
  120. package/dist/workflow/state-machine.d.ts +32 -32
  121. package/dist/workflow/state-machine.js +85 -170
  122. package/package.json +6 -3
  123. package/src/index.ts +8 -0
  124. package/src/shared/metrics.ts +148 -0
  125. package/src/state/db.ts +10 -1
  126. package/src/state/repo-lock.ts +158 -0
  127. package/src/tools/health.ts +128 -0
  128. package/src/tools/index.ts +12 -3
  129. package/src/tools/init.ts +26 -14
  130. package/src/tools/metrics.ts +71 -0
  131. package/src/tools/repair.ts +21 -8
  132. package/src/tools/reset.ts +100 -0
  133. package/src/tools/stage.ts +12 -0
  134. package/src/tools/status.ts +17 -3
  135. package/src/tools/story.ts +41 -15
  136. package/src/tools/workflow.ts +123 -121
  137. package/src/ui/inject.ts +113 -79
  138. package/src/workflow/state-machine.ts +123 -227
  139. package/src/tools/workflow.ts.backup +0 -681
@@ -0,0 +1,308 @@
1
+ import { loadAstrocodeConfig } from "./config/loader";
2
+ import { createConfigHandler } from "./config/config-handler";
3
+ import { openSqlite, configurePragmas, ensureSchema, getSchemaVersion } from "./state/db";
4
+ import { getAstroPaths, ensureAstroDirs } from "./shared/paths";
5
+ import { createAstroTools } from "./tools";
6
+ import { createContinuationEnforcer } from "./hooks/continuation-enforcer";
7
+ import { createToolOutputTruncatorHook } from "./hooks/tool-output-truncator";
8
+ import { createInjectProvider } from "./hooks/inject-provider";
9
+ import { createToastManager } from "./ui/toasts";
10
+ import { createAstroAgents } from "./agents/registry";
11
+ import { info, warn } from "./shared/log";
12
+ import { acquireRepoLock } from "./state/repo-lock";
13
+ // Safe config cloning with structuredClone preference (fallback for older Node versions)
14
+ // CONTRACT: Config is guaranteed JSON-serializable (enforced by loadAstrocodeConfig validation)
15
+ const cloneConfig = (v) => {
16
+ // Prefer structuredClone when available (keeps more types)
17
+ if (typeof globalThis.structuredClone === "function") {
18
+ return globalThis.structuredClone(v);
19
+ }
20
+ // Fallback: JSON clone (safe since config is validated as JSON-serializable)
21
+ return JSON.parse(JSON.stringify(v));
22
+ };
23
+ // Minimal config defaults for limited-mode fallback
24
+ const applyMinimalConfigDefaults = (cfg) => {
25
+ if (!cfg || typeof cfg !== "object")
26
+ return;
27
+ if (!Array.isArray(cfg.disabled_hooks))
28
+ cfg.disabled_hooks = [];
29
+ if (!cfg.ui || typeof cfg.ui !== "object")
30
+ cfg.ui = {};
31
+ if (!cfg.ui.toasts || typeof cfg.ui.toasts !== "object")
32
+ cfg.ui.toasts = { enabled: false };
33
+ if (typeof cfg.ui.toasts.enabled !== "boolean")
34
+ cfg.ui.toasts.enabled = false;
35
+ };
36
+ const Astrocode = async (ctx) => {
37
+ if (!ctx.directory || typeof ctx.directory !== "string") {
38
+ throw new Error("Astrocode requires ctx.directory to be a string repo root.");
39
+ }
40
+ const repoRoot = ctx.directory;
41
+ // Acquire exclusive repo lock to prevent multiple processes from corrupting the database
42
+ const lockPath = `${repoRoot}/.astro/astro.lock`;
43
+ const repoLock = acquireRepoLock(lockPath);
44
+ // Always load config first - this provides defaults even in limited mode
45
+ let pluginConfig;
46
+ try {
47
+ pluginConfig = loadAstrocodeConfig(repoRoot);
48
+ }
49
+ catch (e) {
50
+ warn(`Config loading failed, using minimal defaults.`, { err: e });
51
+ // Fallback to minimal safe defaults when config loading fails
52
+ pluginConfig = {
53
+ disabled_hooks: [],
54
+ disabled_agents: [],
55
+ determinism: { mode: "on", strict_stage_order: true },
56
+ db: {
57
+ path: ".astro/astro.db",
58
+ busy_timeout_ms: 5000,
59
+ pragmas: {},
60
+ schema_version_required: 2,
61
+ allow_auto_migrate: true,
62
+ fail_on_downgrade: true,
63
+ },
64
+ ui: {
65
+ toasts: { enabled: true, throttle_ms: 1500, show_run_started: true, show_stage_started: true, show_stage_completed: true, show_stage_failed: true, show_run_completed: true, show_auto_continue: true },
66
+ },
67
+ agents: {
68
+ orchestrator_name: "Astro",
69
+ stage_agent_names: { frame: "Frame", plan: "Plan", spec: "Spec", implement: "Implement", review: "Review", verify: "Verify", close: "Close" },
70
+ librarian_name: "Librarian",
71
+ explore_name: "Explore",
72
+ qa_name: "QA",
73
+ agent_variant_overrides: {},
74
+ },
75
+ workflow: {
76
+ default_mode: "step",
77
+ default_max_steps: 10,
78
+ loop_max_steps_hard_cap: 100,
79
+ evidence_required: { verify: false },
80
+ },
81
+ permissions: { enforce_task_tool_restrictions: false, deny_delegate_task_in_subagents: false },
82
+ };
83
+ }
84
+ // Always ensure .astro directories exist, even in limited mode
85
+ const paths = getAstroPaths(repoRoot, pluginConfig.db.path);
86
+ ensureAstroDirs(paths);
87
+ let db = null;
88
+ let agents = undefined;
89
+ let configHandler = async () => {
90
+ throw new Error("ConfigHandler called before initialization");
91
+ };
92
+ let continuation = null;
93
+ let truncatorHook = null;
94
+ let injectProvider = null;
95
+ let toastManager = null;
96
+ let limitedModeReason = null;
97
+ let limitedMode = false;
98
+ // Phase 1: Database initialization (only DB-related operations)
99
+ try {
100
+ db = openSqlite(paths.dbPath, { busyTimeoutMs: pluginConfig.db.busy_timeout_ms });
101
+ configurePragmas(db, pluginConfig.db.pragmas);
102
+ // Policy enforced in harness; db layer is mechanics only (migration + structural schema creation)
103
+ ensureSchema(db, { allowAutoMigrate: pluginConfig.db.allow_auto_migrate });
104
+ // Harness-exclusive: enforce schema version requirement
105
+ // Explicitly enforce schema version requirement (harness-level policy)
106
+ const required = pluginConfig.db.schema_version_required;
107
+ let current = getSchemaVersion(db);
108
+ if (current < required) {
109
+ if (!pluginConfig.db.allow_auto_migrate) {
110
+ // Migration disabled - enter limited mode
111
+ limitedModeReason = { code: "schema_too_old", details: { current, required } };
112
+ warn("Entering limited mode (schema too old, migration disabled).", {
113
+ dbPath: paths.dbPath,
114
+ schemaCurrent: current,
115
+ schemaRequired: required,
116
+ failOnDowngrade: pluginConfig.db.fail_on_downgrade,
117
+ allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
118
+ });
119
+ db.close?.();
120
+ db = null;
121
+ }
122
+ else {
123
+ // Migration allowed - re-run ensureSchema (idempotent) in case first call failed
124
+ ensureSchema(db, { allowAutoMigrate: true });
125
+ current = getSchemaVersion(db);
126
+ if (current < required) {
127
+ // Migration failed - enter limited mode
128
+ limitedModeReason = { code: "schema_migration_failed", details: { current, required } };
129
+ warn("Entering limited mode (schema migration failed).", {
130
+ dbPath: paths.dbPath,
131
+ schemaCurrent: current,
132
+ schemaRequired: required,
133
+ failOnDowngrade: pluginConfig.db.fail_on_downgrade,
134
+ allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
135
+ });
136
+ db.close?.();
137
+ db = null;
138
+ }
139
+ }
140
+ }
141
+ else if (current > required && pluginConfig.db.fail_on_downgrade) {
142
+ limitedModeReason = { code: "schema_downgrade", details: { current, required } };
143
+ warn("Entering limited mode (schema downgrade).", {
144
+ dbPath: paths.dbPath,
145
+ schemaCurrent: current,
146
+ schemaRequired: required,
147
+ failOnDowngrade: pluginConfig.db.fail_on_downgrade,
148
+ allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
149
+ });
150
+ db.close?.();
151
+ db = null;
152
+ }
153
+ }
154
+ catch (e) {
155
+ // True database initialization failure - enter limited mode
156
+ warn(`Database initialization failed. Entering limited mode with reduced functionality.`, { err: e });
157
+ // Clean up partially-opened database connection
158
+ if (db && typeof db.close === "function") {
159
+ try {
160
+ db.close();
161
+ }
162
+ catch (closeErr) {
163
+ // Ignore close errors during cleanup
164
+ }
165
+ }
166
+ db = null;
167
+ limitedModeReason = { code: "db_init_failed", details: e };
168
+ }
169
+ // Derive limited mode flag once for single source of truth
170
+ limitedMode = db === null;
171
+ // Single runtime state object for downstream components
172
+ const runtime = { db, limitedMode, limitedModeReason };
173
+ // Phase 2: Component initialization (based on DB availability)
174
+ if (!limitedMode) {
175
+ // Normal mode: DB available, create all components
176
+ try {
177
+ agents = createAstroAgents({ pluginConfig });
178
+ // Database initialized successfully
179
+ info("Database initialized successfully. Plugin ready.");
180
+ configHandler = createConfigHandler({ pluginConfig });
181
+ continuation = createContinuationEnforcer({ ctx, config: pluginConfig, runtime });
182
+ truncatorHook = createToolOutputTruncatorHook({ ctx, config: pluginConfig, runtime });
183
+ injectProvider = createInjectProvider({ ctx, config: pluginConfig, runtime });
184
+ toastManager = createToastManager({ ctx, throttleMs: pluginConfig.ui.toasts.throttle_ms });
185
+ }
186
+ catch (e) {
187
+ // Component failure with working DB → fail plugin boot
188
+ const err = e instanceof Error ? e : new Error(String(e));
189
+ throw new Error("Plugin initialization failed after successful database setup", { cause: err });
190
+ }
191
+ }
192
+ else {
193
+ // Limited mode: DB unavailable, create minimal components
194
+ // Clone the already-loaded config to avoid mutating shared state, then apply limited mode overrides
195
+ pluginConfig = cloneConfig(pluginConfig);
196
+ // Apply limited-mode config overrides BEFORE creating any components
197
+ pluginConfig.disabled_hooks = Array.from(new Set([...(pluginConfig.disabled_hooks || []), "continuation-enforcer", "tool-output-truncator", "inject-provider"]));
198
+ pluginConfig.ui.toasts.enabled = false;
199
+ // Show one-shot limited mode notification via logging (keeps toast policy consistent)
200
+ const reasonMessage = limitedModeReason ?
201
+ (limitedModeReason.code === "db_init_failed" ? "Database initialization failed. Check native deps or run astro_init." :
202
+ limitedModeReason.code === "schema_too_old" ? "Database schema too old and migration disabled." :
203
+ limitedModeReason.code === "schema_downgrade" ? "Database schema newer than plugin (downgrade detected)." :
204
+ limitedModeReason.code === "schema_migration_failed" ? "Database schema migration failed." :
205
+ "Database unavailable.") :
206
+ "Database unavailable.";
207
+ warn(`Astrocode: Limited Mode - ${reasonMessage}`, { dbPath: paths.dbPath });
208
+ // Create limited functionality (no global toast manager since toasts are disabled)
209
+ try {
210
+ configHandler = createConfigHandler({ pluginConfig });
211
+ }
212
+ catch (e) {
213
+ warn("Limited Mode: config handler unavailable", { err: e });
214
+ configHandler = async (config) => {
215
+ applyMinimalConfigDefaults(config);
216
+ };
217
+ }
218
+ continuation = null;
219
+ truncatorHook = null;
220
+ injectProvider = null;
221
+ toastManager = null; // No global toast manager in limited mode
222
+ }
223
+ // Helper for hook enablement checks (config is now finalized)
224
+ const hookEnabled = (key) => !pluginConfig.disabled_hooks?.includes(key);
225
+ // Create tools once with final state (limited mode gets DB-safe tool implementations)
226
+ const tools = createAstroTools({ ctx, config: pluginConfig, agents, runtime });
227
+ return {
228
+ name: "astrocode-harness",
229
+ // Register agents
230
+ agents,
231
+ // Merge agents + slash commands into system config
232
+ config: configHandler,
233
+ // Register tools
234
+ tool: tools,
235
+ // Limit created subagents from spawning more subagents (OMO-style).
236
+ "tool.execute.before": async (input, output) => {
237
+ if (!pluginConfig.permissions.enforce_task_tool_restrictions)
238
+ return;
239
+ if (input?.tool !== "task")
240
+ return;
241
+ const baseArgs = (output && typeof output === "object" ? output.args : undefined) ?? {};
242
+ const baseTools = (baseArgs && typeof baseArgs === "object" && baseArgs.tools && typeof baseArgs.tools === "object")
243
+ ? baseArgs.tools
244
+ : {};
245
+ const nextTools = { ...baseTools };
246
+ if (pluginConfig.permissions.deny_delegate_task_in_subagents) {
247
+ nextTools.delegate_task = false;
248
+ }
249
+ const nextArgs = { ...baseArgs, tools: nextTools };
250
+ // Back-compat: mutate if possible
251
+ if (output && typeof output === "object" && !Object.isFrozen(output)) {
252
+ output.args = nextArgs;
253
+ }
254
+ // Forward-compat: return conservative merge
255
+ if (output && typeof output === "object") {
256
+ return { ...output, args: nextArgs };
257
+ }
258
+ return { args: nextArgs };
259
+ },
260
+ "tool.execute.after": async (input, output) => {
261
+ // Truncate huge tool outputs to artifacts
262
+ if (truncatorHook && hookEnabled("tool-output-truncator")) {
263
+ await truncatorHook(input, output ?? null);
264
+ }
265
+ // Schedule continuation (do not immediately spam)
266
+ if (continuation && hookEnabled("continuation-enforcer")) {
267
+ await continuation.onToolAfter(input);
268
+ }
269
+ return output;
270
+ },
271
+ "chat.message": async (input, output) => {
272
+ if (injectProvider && hookEnabled("inject-provider")) {
273
+ await injectProvider.onChatMessage(input);
274
+ }
275
+ if (continuation && hookEnabled("continuation-enforcer")) {
276
+ await continuation.onChatMessage(input);
277
+ }
278
+ return output;
279
+ },
280
+ event: async (input) => {
281
+ if (continuation && hookEnabled("continuation-enforcer")) {
282
+ await continuation.onEvent(input);
283
+ }
284
+ },
285
+ // Best-effort cleanup
286
+ close: async () => {
287
+ // Release repo lock first (important for process termination)
288
+ repoLock.release();
289
+ if (db && typeof db.close === "function") {
290
+ try {
291
+ db.close();
292
+ }
293
+ catch {
294
+ // ignore
295
+ }
296
+ }
297
+ if (toastManager && pluginConfig.ui.toasts.enabled) {
298
+ try {
299
+ await toastManager.show({ title: "Astrocode", message: "Plugin closed", variant: "info" });
300
+ }
301
+ catch {
302
+ // ignore
303
+ }
304
+ }
305
+ },
306
+ };
307
+ };
308
+ export default Astrocode;
@@ -0,0 +1,8 @@
1
+ export declare function isPlainObject(v: unknown): v is Record<string, unknown>;
2
+ /**
3
+ * Deep merge:
4
+ * - objects merge recursively
5
+ * - arrays replace
6
+ * - primitives overwrite
7
+ */
8
+ export declare function deepMerge<T>(base: T, patch: Partial<T>): T;
@@ -0,0 +1,25 @@
1
+ export function isPlainObject(v) {
2
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3
+ }
4
+ /**
5
+ * Deep merge:
6
+ * - objects merge recursively
7
+ * - arrays replace
8
+ * - primitives overwrite
9
+ */
10
+ export function deepMerge(base, patch) {
11
+ if (!isPlainObject(base) || !isPlainObject(patch)) {
12
+ return patch ?? base;
13
+ }
14
+ const out = { ...base };
15
+ for (const [k, v] of Object.entries(patch)) {
16
+ const cur = out[k];
17
+ if (isPlainObject(cur) && isPlainObject(v)) {
18
+ out[k] = deepMerge(cur, v);
19
+ }
20
+ else {
21
+ out[k] = v;
22
+ }
23
+ }
24
+ return out;
25
+ }
@@ -0,0 +1 @@
1
+ export declare function sha256Hex(input: string | Buffer): string;
@@ -0,0 +1,4 @@
1
+ import { createHash } from "node:crypto";
2
+ export function sha256Hex(input) {
3
+ return createHash("sha256").update(input).digest("hex");
4
+ }
@@ -0,0 +1,7 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export declare function setLogLevel(level: LogLevel): void;
3
+ export declare function log(level: LogLevel, message: string, meta?: unknown): void;
4
+ export declare const debug: (msg: string, meta?: unknown) => void;
5
+ export declare const info: (msg: string, meta?: unknown) => void;
6
+ export declare const warn: (msg: string, meta?: unknown) => void;
7
+ export declare const error: (msg: string, meta?: unknown) => void;
@@ -0,0 +1,24 @@
1
+ let CURRENT_LEVEL = process.env.ASTRO_LOG_LEVEL ?? "info";
2
+ const ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
3
+ export function setLogLevel(level) {
4
+ CURRENT_LEVEL = level;
5
+ }
6
+ function shouldLog(level) {
7
+ return ORDER[level] >= ORDER[CURRENT_LEVEL];
8
+ }
9
+ export function log(level, message, meta) {
10
+ if (!shouldLog(level))
11
+ return;
12
+ const prefix = `[astrocode:${level}]`;
13
+ if (meta === undefined) {
14
+ // eslint-disable-next-line no-console
15
+ console.log(prefix, message);
16
+ return;
17
+ }
18
+ // eslint-disable-next-line no-console
19
+ console.log(prefix, message, meta);
20
+ }
21
+ export const debug = (msg, meta) => log("debug", msg, meta);
22
+ export const info = (msg, meta) => log("info", msg, meta);
23
+ export const warn = (msg, meta) => log("warn", msg, meta);
24
+ export const error = (msg, meta) => log("error", msg, meta);
@@ -0,0 +1,66 @@
1
+ export interface TransactionMetrics {
2
+ startTime: number;
3
+ duration: number;
4
+ success: boolean;
5
+ nestedDepth: number;
6
+ operation?: string;
7
+ }
8
+ export interface InjectionMetrics {
9
+ sessionId: string;
10
+ attempts: number;
11
+ duration: number;
12
+ success: boolean;
13
+ agent?: string;
14
+ }
15
+ export interface SystemMetrics {
16
+ transactions: TransactionMetrics[];
17
+ injections: InjectionMetrics[];
18
+ errors: Array<{
19
+ type: string;
20
+ message: string;
21
+ timestamp: number;
22
+ }>;
23
+ }
24
+ declare class MetricsCollector {
25
+ private transactions;
26
+ private injections;
27
+ private errors;
28
+ private maxEntries;
29
+ recordTransaction(metrics: TransactionMetrics): void;
30
+ recordInjection(metrics: InjectionMetrics): void;
31
+ recordError(type: string, message: string): void;
32
+ getMetrics(): SystemMetrics;
33
+ getTransactionStats(): {
34
+ total: number;
35
+ successful: number;
36
+ failed: number;
37
+ successRate: number;
38
+ avgDuration: number;
39
+ avgNestedDepth: number;
40
+ minDuration: number;
41
+ maxDuration: number;
42
+ };
43
+ getInjectionStats(): {
44
+ total: number;
45
+ successful: number;
46
+ failed: number;
47
+ successRate: number;
48
+ avgAttempts: number;
49
+ avgDuration: number;
50
+ totalRetries: number;
51
+ };
52
+ clear(): void;
53
+ }
54
+ export declare const metrics: MetricsCollector;
55
+ export declare function recordTransaction(metricsData: Omit<TransactionMetrics, 'startTime' | 'duration' | 'success'>): any;
56
+ export declare function recordInjection(metricsData: Omit<InjectionMetrics, 'duration' | 'success'>): {
57
+ start(): {
58
+ startTime: number;
59
+ agent?: string;
60
+ sessionId: string;
61
+ attempts: number;
62
+ };
63
+ end(startData: any, success: boolean): void;
64
+ };
65
+ export declare function recordError(type: string, message: string): void;
66
+ export {};
@@ -0,0 +1,112 @@
1
+ // src/shared/metrics.ts
2
+ class MetricsCollector {
3
+ transactions = [];
4
+ injections = [];
5
+ errors = [];
6
+ maxEntries = 1000; // Keep last 1000 entries per type
7
+ recordTransaction(metrics) {
8
+ this.transactions.push(metrics);
9
+ if (this.transactions.length > this.maxEntries) {
10
+ this.transactions.shift();
11
+ }
12
+ }
13
+ recordInjection(metrics) {
14
+ this.injections.push(metrics);
15
+ if (this.injections.length > this.maxEntries) {
16
+ this.injections.shift();
17
+ }
18
+ }
19
+ recordError(type, message) {
20
+ this.errors.push({ type, message, timestamp: Date.now() });
21
+ if (this.errors.length > this.maxEntries) {
22
+ this.errors.shift();
23
+ }
24
+ }
25
+ getMetrics() {
26
+ return {
27
+ transactions: [...this.transactions],
28
+ injections: [...this.injections],
29
+ errors: [...this.errors],
30
+ };
31
+ }
32
+ getTransactionStats() {
33
+ const txs = this.transactions;
34
+ if (txs.length === 0)
35
+ return null;
36
+ const successful = txs.filter(t => t.success);
37
+ const failed = txs.filter(t => !t.success);
38
+ return {
39
+ total: txs.length,
40
+ successful: successful.length,
41
+ failed: failed.length,
42
+ successRate: successful.length / txs.length,
43
+ avgDuration: txs.reduce((sum, t) => sum + t.duration, 0) / txs.length,
44
+ avgNestedDepth: txs.reduce((sum, t) => sum + t.nestedDepth, 0) / txs.length,
45
+ minDuration: Math.min(...txs.map(t => t.duration)),
46
+ maxDuration: Math.max(...txs.map(t => t.duration)),
47
+ };
48
+ }
49
+ getInjectionStats() {
50
+ const injections = this.injections;
51
+ if (injections.length === 0)
52
+ return null;
53
+ const successful = injections.filter(i => i.success);
54
+ const failed = injections.filter(i => !i.success);
55
+ return {
56
+ total: injections.length,
57
+ successful: successful.length,
58
+ failed: failed.length,
59
+ successRate: successful.length / injections.length,
60
+ avgAttempts: injections.reduce((sum, i) => sum + i.attempts, 0) / injections.length,
61
+ avgDuration: injections.reduce((sum, i) => sum + i.duration, 0) / injections.length,
62
+ totalRetries: injections.reduce((sum, i) => sum + Math.max(0, i.attempts - 1), 0),
63
+ };
64
+ }
65
+ clear() {
66
+ this.transactions = [];
67
+ this.injections = [];
68
+ this.errors = [];
69
+ }
70
+ }
71
+ // Global singleton
72
+ export const metrics = new MetricsCollector();
73
+ // Convenience functions
74
+ export function recordTransaction(metricsData) {
75
+ return {
76
+ start() {
77
+ return {
78
+ ...metricsData,
79
+ startTime: Date.now(),
80
+ };
81
+ },
82
+ end(startData, success) {
83
+ const duration = Date.now() - startData.startTime;
84
+ metrics.recordTransaction({
85
+ ...startData,
86
+ duration,
87
+ success,
88
+ });
89
+ },
90
+ };
91
+ }
92
+ export function recordInjection(metricsData) {
93
+ return {
94
+ start() {
95
+ return {
96
+ ...metricsData,
97
+ startTime: Date.now(),
98
+ };
99
+ },
100
+ end(startData, success) {
101
+ const duration = Date.now() - startData.startTime;
102
+ metrics.recordInjection({
103
+ ...startData,
104
+ duration,
105
+ success,
106
+ });
107
+ },
108
+ };
109
+ }
110
+ export function recordError(type, message) {
111
+ metrics.recordError(type, message);
112
+ }
@@ -0,0 +1,9 @@
1
+ import type { AgentConfig } from "@opencode-ai/sdk";
2
+ export type AstroCognitionPreset = "orchestrator" | "frame" | "plan" | "spec" | "implement" | "review" | "verify" | "close" | "utility";
3
+ /**
4
+ * OMO-style: if GPT model => reasoningEffort/textVerbosity.
5
+ * Otherwise => Anthropic-style thinking budget.
6
+ *
7
+ * Not all providers honor these fields; safe to include.
8
+ */
9
+ export declare function applyModelTuning(base: AgentConfig, preset: AstroCognitionPreset): AgentConfig;
@@ -0,0 +1,28 @@
1
+ import { isGptModel } from "../agents/types";
2
+ /**
3
+ * OMO-style: if GPT model => reasoningEffort/textVerbosity.
4
+ * Otherwise => Anthropic-style thinking budget.
5
+ *
6
+ * Not all providers honor these fields; safe to include.
7
+ */
8
+ export function applyModelTuning(base, preset) {
9
+ const model = base.model ?? "";
10
+ if (!model)
11
+ return base;
12
+ if (isGptModel(model)) {
13
+ const reasoningEffort = preset === "orchestrator" ? "medium"
14
+ : preset === "implement" ? "high"
15
+ : "medium";
16
+ const textVerbosity = preset === "orchestrator" ? "low"
17
+ : preset === "review" ? "high"
18
+ : "medium";
19
+ const { thinking, ...rest } = base;
20
+ return { ...rest, reasoningEffort, textVerbosity };
21
+ }
22
+ const budgetTokens = preset === "implement" ? 32_000
23
+ : preset === "review" ? 24_000
24
+ : preset === "orchestrator" ? 16_000
25
+ : 12_000;
26
+ const { reasoningEffort, textVerbosity, ...rest } = base;
27
+ return { ...rest, thinking: { type: "enabled", budgetTokens } };
28
+ }
@@ -0,0 +1,19 @@
1
+ /** Normalize to posix-like separators for DB paths. */
2
+ export declare function toPosix(p: string): string;
3
+ export declare function ensureDir(p: string): void;
4
+ export declare function joinRepo(root: string, ...parts: string[]): string;
5
+ export type AstroPaths = {
6
+ repoRoot: string;
7
+ astroRoot: string;
8
+ dbPath: string;
9
+ runsDir: string;
10
+ specPath: string;
11
+ toolOutputDir: string;
12
+ configPathPreferred: string;
13
+ configPathFallback: string;
14
+ };
15
+ export declare function getAstroPaths(repoRoot: string, dbPathOverride?: string): AstroPaths;
16
+ export declare function ensureAstroDirs(paths: AstroPaths): void;
17
+ export declare function runDir(paths: AstroPaths, runId: string): string;
18
+ export declare function stageDir(paths: AstroPaths, runId: string, stageKey: string): string;
19
+ export declare function assertInsideAstro(repoRoot: string, filePath: string): void;