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.
- package/dist/index.js +6 -0
- package/dist/shared/metrics.d.ts +66 -0
- package/dist/shared/metrics.js +112 -0
- package/dist/src/agents/commands.d.ts +9 -0
- package/dist/src/agents/commands.js +121 -0
- package/dist/src/agents/prompts.d.ts +3 -0
- package/dist/src/agents/prompts.js +232 -0
- package/dist/src/agents/registry.d.ts +6 -0
- package/dist/src/agents/registry.js +242 -0
- package/dist/src/agents/types.d.ts +14 -0
- package/dist/src/agents/types.js +8 -0
- package/dist/src/config/config-handler.d.ts +4 -0
- package/dist/src/config/config-handler.js +46 -0
- package/dist/src/config/defaults.d.ts +3 -0
- package/dist/src/config/defaults.js +3 -0
- package/dist/src/config/loader.d.ts +11 -0
- package/dist/src/config/loader.js +82 -0
- package/dist/src/config/schema.d.ts +194 -0
- package/dist/src/config/schema.js +223 -0
- package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
- package/dist/src/hooks/continuation-enforcer.js +190 -0
- package/dist/src/hooks/inject-provider.d.ts +22 -0
- package/dist/src/hooks/inject-provider.js +120 -0
- package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
- package/dist/src/hooks/tool-output-truncator.js +57 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +308 -0
- package/dist/src/shared/deep-merge.d.ts +8 -0
- package/dist/src/shared/deep-merge.js +25 -0
- package/dist/src/shared/hash.d.ts +1 -0
- package/dist/src/shared/hash.js +4 -0
- package/dist/src/shared/log.d.ts +7 -0
- package/dist/src/shared/log.js +24 -0
- package/dist/src/shared/metrics.d.ts +66 -0
- package/dist/src/shared/metrics.js +112 -0
- package/dist/src/shared/model-tuning.d.ts +9 -0
- package/dist/src/shared/model-tuning.js +28 -0
- package/dist/src/shared/paths.d.ts +19 -0
- package/dist/src/shared/paths.js +64 -0
- package/dist/src/shared/text.d.ts +4 -0
- package/dist/src/shared/text.js +19 -0
- package/dist/src/shared/time.d.ts +1 -0
- package/dist/src/shared/time.js +3 -0
- package/dist/src/state/adapters/index.d.ts +41 -0
- package/dist/src/state/adapters/index.js +115 -0
- package/dist/src/state/db.d.ts +16 -0
- package/dist/src/state/db.js +225 -0
- package/dist/src/state/ids.d.ts +8 -0
- package/dist/src/state/ids.js +25 -0
- package/dist/src/state/repo-lock.d.ts +3 -0
- package/dist/src/state/repo-lock.js +29 -0
- package/dist/src/state/schema.d.ts +2 -0
- package/dist/src/state/schema.js +251 -0
- package/dist/src/state/types.d.ts +71 -0
- package/dist/src/state/types.js +1 -0
- package/dist/src/tools/artifacts.d.ts +18 -0
- package/dist/src/tools/artifacts.js +71 -0
- package/dist/src/tools/health.d.ts +8 -0
- package/dist/src/tools/health.js +119 -0
- package/dist/src/tools/index.d.ts +20 -0
- package/dist/src/tools/index.js +94 -0
- package/dist/src/tools/init.d.ts +17 -0
- package/dist/src/tools/init.js +96 -0
- package/dist/src/tools/injects.d.ts +53 -0
- package/dist/src/tools/injects.js +325 -0
- package/dist/src/tools/metrics.d.ts +7 -0
- package/dist/src/tools/metrics.js +61 -0
- package/dist/src/tools/repair.d.ts +8 -0
- package/dist/src/tools/repair.js +25 -0
- package/dist/src/tools/reset.d.ts +8 -0
- package/dist/src/tools/reset.js +92 -0
- package/dist/src/tools/run.d.ts +13 -0
- package/dist/src/tools/run.js +54 -0
- package/dist/src/tools/spec.d.ts +12 -0
- package/dist/src/tools/spec.js +44 -0
- package/dist/src/tools/stage.d.ts +23 -0
- package/dist/src/tools/stage.js +371 -0
- package/dist/src/tools/status.d.ts +8 -0
- package/dist/src/tools/status.js +125 -0
- package/dist/src/tools/story.d.ts +23 -0
- package/dist/src/tools/story.js +85 -0
- package/dist/src/tools/workflow.d.ts +13 -0
- package/dist/src/tools/workflow.js +355 -0
- package/dist/src/ui/inject.d.ts +12 -0
- package/dist/src/ui/inject.js +107 -0
- package/dist/src/ui/toasts.d.ts +13 -0
- package/dist/src/ui/toasts.js +39 -0
- package/dist/src/workflow/artifacts.d.ts +24 -0
- package/dist/src/workflow/artifacts.js +45 -0
- package/dist/src/workflow/baton.d.ts +72 -0
- package/dist/src/workflow/baton.js +166 -0
- package/dist/src/workflow/context.d.ts +20 -0
- package/dist/src/workflow/context.js +113 -0
- package/dist/src/workflow/directives.d.ts +39 -0
- package/dist/src/workflow/directives.js +137 -0
- package/dist/src/workflow/repair.d.ts +8 -0
- package/dist/src/workflow/repair.js +99 -0
- package/dist/src/workflow/state-machine.d.ts +86 -0
- package/dist/src/workflow/state-machine.js +216 -0
- package/dist/src/workflow/story-helpers.d.ts +9 -0
- package/dist/src/workflow/story-helpers.js +13 -0
- package/dist/state/db.d.ts +1 -0
- package/dist/state/db.js +9 -0
- package/dist/state/repo-lock.d.ts +3 -0
- package/dist/state/repo-lock.js +29 -0
- package/dist/test/integration/db-transactions.test.d.ts +1 -0
- package/dist/test/integration/db-transactions.test.js +126 -0
- package/dist/test/integration/injection-metrics.test.d.ts +1 -0
- package/dist/test/integration/injection-metrics.test.js +129 -0
- package/dist/tools/health.d.ts +8 -0
- package/dist/tools/health.js +119 -0
- package/dist/tools/index.js +9 -0
- package/dist/tools/metrics.d.ts +7 -0
- package/dist/tools/metrics.js +61 -0
- package/dist/tools/reset.d.ts +8 -0
- package/dist/tools/reset.js +92 -0
- package/dist/tools/workflow.js +210 -215
- package/dist/ui/inject.d.ts +6 -0
- package/dist/ui/inject.js +86 -67
- package/dist/workflow/state-machine.d.ts +32 -32
- package/dist/workflow/state-machine.js +85 -170
- package/package.json +6 -3
- package/src/index.ts +8 -0
- package/src/shared/metrics.ts +148 -0
- package/src/state/db.ts +10 -1
- package/src/state/repo-lock.ts +158 -0
- package/src/tools/health.ts +128 -0
- package/src/tools/index.ts +12 -3
- package/src/tools/init.ts +26 -14
- package/src/tools/metrics.ts +71 -0
- package/src/tools/repair.ts +21 -8
- package/src/tools/reset.ts +100 -0
- package/src/tools/stage.ts +12 -0
- package/src/tools/status.ts +17 -3
- package/src/tools/story.ts +41 -15
- package/src/tools/workflow.ts +123 -121
- package/src/ui/inject.ts +113 -79
- package/src/workflow/state-machine.ts +123 -227
- 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,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,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;
|