astrocode-workflow 0.1.58 → 0.2.0
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/README.md +243 -11
- package/dist/agents/prompts.d.ts +1 -0
- package/dist/agents/prompts.js +159 -0
- package/dist/agents/registry.js +11 -1
- package/dist/config/loader.js +34 -0
- package/dist/config/schema.d.ts +7 -1
- package/dist/config/schema.js +2 -0
- package/dist/hooks/continuation-enforcer.d.ts +9 -1
- package/dist/hooks/continuation-enforcer.js +2 -1
- package/dist/hooks/inject-provider.d.ts +9 -1
- package/dist/hooks/inject-provider.js +2 -1
- package/dist/hooks/tool-output-truncator.d.ts +9 -1
- package/dist/hooks/tool-output-truncator.js +2 -1
- package/dist/index.js +228 -45
- package/dist/state/adapters/index.d.ts +4 -2
- package/dist/state/adapters/index.js +23 -27
- package/dist/state/db.d.ts +6 -8
- package/dist/state/db.js +106 -45
- package/dist/tools/index.d.ts +13 -3
- package/dist/tools/index.js +14 -31
- package/dist/tools/init.d.ts +10 -1
- package/dist/tools/init.js +73 -18
- package/dist/tools/injects.js +90 -26
- package/dist/tools/spec.d.ts +0 -1
- package/dist/tools/spec.js +4 -1
- package/dist/tools/status.d.ts +1 -1
- package/dist/tools/status.js +70 -52
- package/dist/tools/workflow.js +2 -2
- package/dist/ui/inject.d.ts +16 -2
- package/dist/ui/inject.js +104 -33
- package/dist/workflow/directives.d.ts +2 -0
- package/dist/workflow/directives.js +34 -19
- package/dist/workflow/state-machine.d.ts +46 -3
- package/dist/workflow/state-machine.js +249 -92
- package/package.json +1 -1
- package/src/agents/prompts.ts +160 -0
- package/src/agents/registry.ts +16 -1
- package/src/config/loader.ts +39 -4
- package/src/config/schema.ts +3 -0
- package/src/hooks/continuation-enforcer.ts +9 -2
- package/src/hooks/inject-provider.ts +9 -2
- package/src/hooks/tool-output-truncator.ts +9 -2
- package/src/index.ts +260 -56
- package/src/state/adapters/index.ts +21 -26
- package/src/state/db.ts +114 -58
- package/src/tools/index.ts +29 -31
- package/src/tools/init.ts +91 -22
- package/src/tools/injects.ts +147 -53
- package/src/tools/spec.ts +6 -2
- package/src/tools/status.ts +71 -55
- package/src/tools/workflow.ts +3 -3
- package/src/ui/inject.ts +115 -41
- package/src/workflow/directives.ts +103 -75
- package/src/workflow/state-machine.ts +327 -109
package/src/config/schema.ts
CHANGED
|
@@ -108,6 +108,8 @@ const WorkflowSchema = z.object({
|
|
|
108
108
|
"close",
|
|
109
109
|
]),
|
|
110
110
|
|
|
111
|
+
genesis_planning: z.enum(["off", "first_story_only", "always"]).default("first_story_only"),
|
|
112
|
+
|
|
111
113
|
default_mode: z.enum(["step", "loop"]).default("step"),
|
|
112
114
|
default_max_steps: z.number().int().positive().default(1),
|
|
113
115
|
loop_max_steps_hard_cap: z.number().int().positive().default(200),
|
|
@@ -167,6 +169,7 @@ const AgentsSchema = z.object({
|
|
|
167
169
|
|
|
168
170
|
librarian_name: z.string().default("Librarian"),
|
|
169
171
|
explore_name: z.string().default("Explore"),
|
|
172
|
+
qa_name: z.string().default("QA"),
|
|
170
173
|
|
|
171
174
|
agent_variant_overrides: z
|
|
172
175
|
.record(
|
|
@@ -35,12 +35,19 @@ function msFromIso(iso: string): number {
|
|
|
35
35
|
return Number.isFinite(t) ? t : 0;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
type RuntimeState = {
|
|
39
|
+
db: SqliteDb | null;
|
|
40
|
+
limitedMode: boolean;
|
|
41
|
+
limitedModeReason: null | { code: "db_init_failed"|"schema_too_old"|"schema_downgrade"|"schema_migration_failed"; details: any };
|
|
42
|
+
};
|
|
43
|
+
|
|
38
44
|
export function createContinuationEnforcer(opts: {
|
|
39
45
|
ctx: any;
|
|
40
46
|
config: AstrocodeConfig;
|
|
41
|
-
|
|
47
|
+
runtime: RuntimeState;
|
|
42
48
|
}) {
|
|
43
|
-
const { ctx, config,
|
|
49
|
+
const { ctx, config, runtime } = opts;
|
|
50
|
+
const { db } = runtime;
|
|
44
51
|
|
|
45
52
|
const toasts = createToastManager({ ctx, throttleMs: config.ui.toasts.throttle_ms });
|
|
46
53
|
|
|
@@ -9,12 +9,19 @@ type ChatMessageInput = {
|
|
|
9
9
|
agent: string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
type RuntimeState = {
|
|
13
|
+
db: SqliteDb | null;
|
|
14
|
+
limitedMode: boolean;
|
|
15
|
+
limitedModeReason: null | { code: "db_init_failed"|"schema_too_old"|"schema_downgrade"|"schema_migration_failed"; details: any };
|
|
16
|
+
};
|
|
17
|
+
|
|
12
18
|
export function createInjectProvider(opts: {
|
|
13
19
|
ctx: any;
|
|
14
20
|
config: AstrocodeConfig;
|
|
15
|
-
|
|
21
|
+
runtime: RuntimeState;
|
|
16
22
|
}) {
|
|
17
|
-
const { ctx, config,
|
|
23
|
+
const { ctx, config, runtime } = opts;
|
|
24
|
+
const { db } = runtime;
|
|
18
25
|
|
|
19
26
|
// Cache to avoid re-injecting the same injects repeatedly
|
|
20
27
|
const injectedCache = new Map<string, number>();
|
|
@@ -25,12 +25,19 @@ function pickMaxChars(toolName: string, cfg: AstrocodeConfig): number {
|
|
|
25
25
|
return cfg.truncation.max_chars_default;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
type RuntimeState = {
|
|
29
|
+
db: SqliteDb | null;
|
|
30
|
+
limitedMode: boolean;
|
|
31
|
+
limitedModeReason: null | { code: "db_init_failed"|"schema_too_old"|"schema_downgrade"|"schema_migration_failed"; details: any };
|
|
32
|
+
};
|
|
33
|
+
|
|
28
34
|
export function createToolOutputTruncatorHook(opts: {
|
|
29
35
|
ctx: any;
|
|
30
36
|
config: AstrocodeConfig;
|
|
31
|
-
|
|
37
|
+
runtime: RuntimeState;
|
|
32
38
|
}) {
|
|
33
|
-
const { ctx, config,
|
|
39
|
+
const { ctx, config, runtime } = opts;
|
|
40
|
+
const { db } = runtime;
|
|
34
41
|
|
|
35
42
|
return async function toolExecuteAfter(input: ToolExecuteAfterInput, output: ToolExecuteAfterOutput) {
|
|
36
43
|
if (!config.truncation.enabled) return;
|
package/src/index.ts
CHANGED
|
@@ -1,72 +1,257 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
import { loadAstrocodeConfig } from "./config/loader";
|
|
3
|
+
import type { AstrocodeConfig } from "./config/schema";
|
|
3
4
|
import { createConfigHandler } from "./config/config-handler";
|
|
4
|
-
import { openSqlite, configurePragmas, ensureSchema } from "./state/db";
|
|
5
|
+
import { openSqlite, configurePragmas, ensureSchema, getSchemaVersion } from "./state/db";
|
|
6
|
+
import type { SqliteDb } from "./state/db";
|
|
5
7
|
import { getAstroPaths, ensureAstroDirs } from "./shared/paths";
|
|
6
8
|
import { createAstroTools } from "./tools";
|
|
7
9
|
import { createContinuationEnforcer } from "./hooks/continuation-enforcer";
|
|
8
10
|
import { createToolOutputTruncatorHook } from "./hooks/tool-output-truncator";
|
|
9
11
|
import { createInjectProvider } from "./hooks/inject-provider";
|
|
10
|
-
import { createToastManager } from "./ui/toasts";
|
|
12
|
+
import { createToastManager, type ToastOptions } from "./ui/toasts";
|
|
11
13
|
import { createAstroAgents } from "./agents/registry";
|
|
14
|
+
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
12
15
|
import { info, warn } from "./shared/log";
|
|
13
16
|
|
|
17
|
+
// Type definitions for plugin components
|
|
18
|
+
type ConfigHandler = (config: Record<string, any>) => Promise<void>;
|
|
19
|
+
type ContinuationEnforcer = {
|
|
20
|
+
onToolAfter: (input: any) => Promise<void>;
|
|
21
|
+
onChatMessage: (input: any) => Promise<void>;
|
|
22
|
+
onEvent: (input: any) => Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
type ToolOutputTruncator = (input: any, output: any | null) => Promise<void>;
|
|
25
|
+
type InjectProvider = {
|
|
26
|
+
onChatMessage: (input: any) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
type ToastManager = {
|
|
29
|
+
show: (toast: ToastOptions) => Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Hook names for type safety
|
|
33
|
+
type HookKey = "continuation-enforcer" | "tool-output-truncator" | "inject-provider";
|
|
34
|
+
|
|
35
|
+
// Safe config cloning with structuredClone preference (fallback for older Node versions)
|
|
36
|
+
// CONTRACT: Config is guaranteed JSON-serializable (enforced by loadAstrocodeConfig validation)
|
|
37
|
+
const cloneConfig = <T>(v: T): T => {
|
|
38
|
+
// Prefer structuredClone when available (keeps more types)
|
|
39
|
+
if (typeof (globalThis as any).structuredClone === "function") {
|
|
40
|
+
return (globalThis as any).structuredClone(v);
|
|
41
|
+
}
|
|
42
|
+
// Fallback: JSON clone (safe since config is validated as JSON-serializable)
|
|
43
|
+
return JSON.parse(JSON.stringify(v));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Minimal config defaults for limited-mode fallback
|
|
47
|
+
const applyMinimalConfigDefaults = (cfg: Record<string, any>) => {
|
|
48
|
+
if (!cfg || typeof cfg !== "object") return;
|
|
49
|
+
if (!Array.isArray((cfg as any).disabled_hooks)) (cfg as any).disabled_hooks = [];
|
|
50
|
+
if (!(cfg as any).ui || typeof (cfg as any).ui !== "object") (cfg as any).ui = {};
|
|
51
|
+
if (!(cfg as any).ui.toasts || typeof (cfg as any).ui.toasts !== "object") (cfg as any).ui.toasts = { enabled: false };
|
|
52
|
+
if (typeof (cfg as any).ui.toasts.enabled !== "boolean") (cfg as any).ui.toasts.enabled = false;
|
|
53
|
+
};
|
|
54
|
+
|
|
14
55
|
const Astrocode: Plugin = async (ctx) => {
|
|
15
|
-
|
|
56
|
+
if (!ctx.directory || typeof ctx.directory !== "string") {
|
|
57
|
+
throw new Error("Astrocode requires ctx.directory to be a string repo root.");
|
|
58
|
+
}
|
|
59
|
+
const repoRoot = ctx.directory;
|
|
16
60
|
|
|
17
61
|
// Always load config first - this provides defaults even in limited mode
|
|
18
|
-
let pluginConfig
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
62
|
+
let pluginConfig: AstrocodeConfig;
|
|
63
|
+
try {
|
|
64
|
+
pluginConfig = loadAstrocodeConfig(repoRoot);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
warn(`Config loading failed, using minimal defaults.`, { err: e });
|
|
67
|
+
// Fallback to minimal safe defaults when config loading fails
|
|
68
|
+
pluginConfig = {
|
|
69
|
+
disabled_hooks: [],
|
|
70
|
+
disabled_agents: [],
|
|
71
|
+
determinism: { mode: "on" as const, strict_stage_order: true },
|
|
72
|
+
db: {
|
|
73
|
+
path: ".astro/astro.db",
|
|
74
|
+
busy_timeout_ms: 5000,
|
|
75
|
+
pragmas: {},
|
|
76
|
+
schema_version_required: 2,
|
|
77
|
+
allow_auto_migrate: true,
|
|
78
|
+
fail_on_downgrade: true,
|
|
79
|
+
},
|
|
80
|
+
ui: {
|
|
81
|
+
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 },
|
|
82
|
+
},
|
|
83
|
+
agents: {
|
|
84
|
+
orchestrator_name: "Astro",
|
|
85
|
+
stage_agent_names: { frame: "Frame", plan: "Plan", spec: "Spec", implement: "Implement", review: "Review", verify: "Verify", close: "Close" },
|
|
86
|
+
librarian_name: "Librarian",
|
|
87
|
+
explore_name: "Explore",
|
|
88
|
+
qa_name: "QA",
|
|
89
|
+
agent_variant_overrides: {},
|
|
90
|
+
},
|
|
91
|
+
workflow: {
|
|
92
|
+
default_mode: "step" as const,
|
|
93
|
+
default_max_steps: 10,
|
|
94
|
+
loop_max_steps_hard_cap: 100,
|
|
95
|
+
evidence_required: { verify: false },
|
|
96
|
+
},
|
|
97
|
+
permissions: { enforce_task_tool_restrictions: false, deny_delegate_task_in_subagents: false },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
22
100
|
|
|
23
101
|
// Always ensure .astro directories exist, even in limited mode
|
|
24
102
|
const paths = getAstroPaths(repoRoot, pluginConfig.db.path);
|
|
25
103
|
ensureAstroDirs(paths);
|
|
26
104
|
|
|
27
|
-
let db:
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
105
|
+
let db: SqliteDb | null = null;
|
|
106
|
+
let agents: Record<string, AgentConfig> | undefined = undefined;
|
|
107
|
+
let configHandler: ConfigHandler = async () => {
|
|
108
|
+
throw new Error("ConfigHandler called before initialization");
|
|
109
|
+
};
|
|
110
|
+
let continuation: ContinuationEnforcer | null = null;
|
|
111
|
+
let truncatorHook: ToolOutputTruncator | null = null;
|
|
112
|
+
let injectProvider: InjectProvider | null = null;
|
|
113
|
+
let toastManager: ToastManager | null = null;
|
|
114
|
+
let limitedModeReason: null | { code: "db_init_failed"|"schema_too_old"|"schema_downgrade"|"schema_migration_failed"; details: any } = null;
|
|
115
|
+
let limitedMode = false;
|
|
34
116
|
|
|
117
|
+
// Phase 1: Database initialization (only DB-related operations)
|
|
35
118
|
try {
|
|
36
|
-
|
|
37
119
|
db = openSqlite(paths.dbPath, { busyTimeoutMs: pluginConfig.db.busy_timeout_ms });
|
|
38
120
|
configurePragmas(db, pluginConfig.db.pragmas);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
121
|
+
// Policy enforced in harness; db layer is mechanics only (migration + structural schema creation)
|
|
122
|
+
ensureSchema(db, { allowAutoMigrate: pluginConfig.db.allow_auto_migrate });
|
|
123
|
+
|
|
124
|
+
// Harness-exclusive: enforce schema version requirement
|
|
125
|
+
|
|
126
|
+
// Explicitly enforce schema version requirement (harness-level policy)
|
|
127
|
+
const required = pluginConfig.db.schema_version_required;
|
|
128
|
+
let current = getSchemaVersion(db);
|
|
129
|
+
|
|
130
|
+
if (current < required) {
|
|
131
|
+
if (!pluginConfig.db.allow_auto_migrate) {
|
|
132
|
+
// Migration disabled - enter limited mode
|
|
133
|
+
limitedModeReason = { code: "schema_too_old", details: { current, required } };
|
|
134
|
+
warn("Entering limited mode (schema too old, migration disabled).", {
|
|
135
|
+
dbPath: paths.dbPath,
|
|
136
|
+
schemaCurrent: current,
|
|
137
|
+
schemaRequired: required,
|
|
138
|
+
failOnDowngrade: pluginConfig.db.fail_on_downgrade,
|
|
139
|
+
allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
|
|
140
|
+
});
|
|
141
|
+
db.close?.();
|
|
142
|
+
db = null;
|
|
143
|
+
} else {
|
|
144
|
+
// Migration allowed - re-run ensureSchema (idempotent) in case first call failed
|
|
145
|
+
ensureSchema(db, { allowAutoMigrate: true });
|
|
146
|
+
current = getSchemaVersion(db);
|
|
53
147
|
|
|
54
|
-
|
|
55
|
-
|
|
148
|
+
if (current < required) {
|
|
149
|
+
// Migration failed - enter limited mode
|
|
150
|
+
limitedModeReason = { code: "schema_migration_failed", details: { current, required } };
|
|
151
|
+
warn("Entering limited mode (schema migration failed).", {
|
|
152
|
+
dbPath: paths.dbPath,
|
|
153
|
+
schemaCurrent: current,
|
|
154
|
+
schemaRequired: required,
|
|
155
|
+
failOnDowngrade: pluginConfig.db.fail_on_downgrade,
|
|
156
|
+
allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
|
|
157
|
+
});
|
|
158
|
+
db.close?.();
|
|
159
|
+
db = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} else if (current > required && pluginConfig.db.fail_on_downgrade) {
|
|
163
|
+
limitedModeReason = { code: "schema_downgrade", details: { current, required } };
|
|
164
|
+
warn("Entering limited mode (schema downgrade).", {
|
|
165
|
+
dbPath: paths.dbPath,
|
|
166
|
+
schemaCurrent: current,
|
|
167
|
+
schemaRequired: required,
|
|
168
|
+
failOnDowngrade: pluginConfig.db.fail_on_downgrade,
|
|
169
|
+
allowAutoMigrate: pluginConfig.db.allow_auto_migrate,
|
|
170
|
+
});
|
|
171
|
+
db.close?.();
|
|
172
|
+
db = null;
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
// True database initialization failure - enter limited mode
|
|
176
|
+
warn(`Database initialization failed. Entering limited mode with reduced functionality.`, { err: e });
|
|
177
|
+
// Clean up partially-opened database connection
|
|
178
|
+
if (db && typeof db.close === "function") {
|
|
179
|
+
try {
|
|
180
|
+
db.close();
|
|
181
|
+
} catch (closeErr) {
|
|
182
|
+
// Ignore close errors during cleanup
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
db = null;
|
|
186
|
+
limitedModeReason = { code: "db_init_failed", details: e };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Derive limited mode flag once for single source of truth
|
|
190
|
+
limitedMode = db === null;
|
|
191
|
+
|
|
192
|
+
// Single runtime state object for downstream components
|
|
193
|
+
const runtime = { db, limitedMode, limitedModeReason };
|
|
194
|
+
|
|
195
|
+
// Phase 2: Component initialization (based on DB availability)
|
|
196
|
+
if (!limitedMode) {
|
|
197
|
+
// Normal mode: DB available, create all components
|
|
198
|
+
try {
|
|
199
|
+
agents = createAstroAgents({ pluginConfig });
|
|
200
|
+
|
|
201
|
+
// Database initialized successfully
|
|
202
|
+
info("Database initialized successfully. Plugin ready.");
|
|
203
|
+
configHandler = createConfigHandler({ pluginConfig });
|
|
204
|
+
continuation = createContinuationEnforcer({ ctx, config: pluginConfig, runtime });
|
|
205
|
+
truncatorHook = createToolOutputTruncatorHook({ ctx, config: pluginConfig, runtime });
|
|
206
|
+
injectProvider = createInjectProvider({ ctx, config: pluginConfig, runtime });
|
|
207
|
+
toastManager = createToastManager({ ctx, throttleMs: pluginConfig.ui.toasts.throttle_ms });
|
|
208
|
+
} catch (e) {
|
|
209
|
+
// Component failure with working DB → fail plugin boot
|
|
210
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
211
|
+
throw new Error("Plugin initialization failed after successful database setup", { cause: err });
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// Limited mode: DB unavailable, create minimal components
|
|
215
|
+
// Clone the already-loaded config to avoid mutating shared state, then apply limited mode overrides
|
|
216
|
+
pluginConfig = cloneConfig(pluginConfig);
|
|
217
|
+
|
|
218
|
+
// Apply limited-mode config overrides BEFORE creating any components
|
|
219
|
+
pluginConfig.disabled_hooks = Array.from(new Set([...(pluginConfig.disabled_hooks || []), "continuation-enforcer", "tool-output-truncator", "inject-provider"]));
|
|
56
220
|
pluginConfig.ui.toasts.enabled = false;
|
|
57
221
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
222
|
+
// Show one-shot limited mode notification via logging (keeps toast policy consistent)
|
|
223
|
+
const reasonMessage = limitedModeReason ?
|
|
224
|
+
(limitedModeReason.code === "db_init_failed" ? "Database initialization failed. Check native deps or run astro_init." :
|
|
225
|
+
limitedModeReason.code === "schema_too_old" ? "Database schema too old and migration disabled." :
|
|
226
|
+
limitedModeReason.code === "schema_downgrade" ? "Database schema newer than plugin (downgrade detected)." :
|
|
227
|
+
limitedModeReason.code === "schema_migration_failed" ? "Database schema migration failed." :
|
|
228
|
+
"Database unavailable.") :
|
|
229
|
+
"Database unavailable.";
|
|
230
|
+
warn(`Astrocode: Limited Mode - ${reasonMessage}`, { dbPath: paths.dbPath });
|
|
231
|
+
|
|
232
|
+
// Create limited functionality (no global toast manager since toasts are disabled)
|
|
233
|
+
try {
|
|
234
|
+
configHandler = createConfigHandler({ pluginConfig });
|
|
235
|
+
} catch (e) {
|
|
236
|
+
warn("Limited Mode: config handler unavailable", { err: e });
|
|
237
|
+
configHandler = async (config: Record<string, any>) => {
|
|
238
|
+
applyMinimalConfigDefaults(config);
|
|
239
|
+
};
|
|
240
|
+
}
|
|
62
241
|
continuation = null;
|
|
63
242
|
truncatorHook = null;
|
|
64
243
|
injectProvider = null;
|
|
65
|
-
|
|
244
|
+
toastManager = null; // No global toast manager in limited mode
|
|
66
245
|
}
|
|
67
246
|
|
|
247
|
+
// Helper for hook enablement checks (config is now finalized)
|
|
248
|
+
const hookEnabled = (key: HookKey) => !pluginConfig.disabled_hooks?.includes(key);
|
|
249
|
+
|
|
250
|
+
// Create tools once with final state (limited mode gets DB-safe tool implementations)
|
|
251
|
+
const tools = createAstroTools({ ctx, config: pluginConfig, agents, runtime });
|
|
252
|
+
|
|
68
253
|
return {
|
|
69
|
-
name: "
|
|
254
|
+
name: "astrocode-harness",
|
|
70
255
|
|
|
71
256
|
// Register agents
|
|
72
257
|
agents,
|
|
@@ -75,63 +260,82 @@ const Astrocode: Plugin = async (ctx) => {
|
|
|
75
260
|
config: configHandler,
|
|
76
261
|
|
|
77
262
|
// Register tools
|
|
78
|
-
tool:
|
|
263
|
+
tool: tools,
|
|
79
264
|
|
|
80
265
|
// Limit created subagents from spawning more subagents (OMO-style).
|
|
81
266
|
"tool.execute.before": async (input: any, output: any) => {
|
|
82
267
|
if (!pluginConfig.permissions.enforce_task_tool_restrictions) return;
|
|
83
|
-
if (input
|
|
84
|
-
|
|
85
|
-
output.args = output.args ?? {};
|
|
268
|
+
if (input?.tool !== "task") return;
|
|
86
269
|
|
|
87
|
-
const
|
|
270
|
+
const baseArgs =
|
|
271
|
+
(output && typeof output === "object" ? (output as any).args : undefined) ?? {};
|
|
272
|
+
const baseTools =
|
|
273
|
+
(baseArgs && typeof baseArgs === "object" && (baseArgs as any).tools && typeof (baseArgs as any).tools === "object")
|
|
274
|
+
? (baseArgs as any).tools
|
|
275
|
+
: {};
|
|
88
276
|
|
|
277
|
+
const nextTools = { ...baseTools };
|
|
89
278
|
if (pluginConfig.permissions.deny_delegate_task_in_subagents) {
|
|
90
|
-
|
|
279
|
+
nextTools.delegate_task = false;
|
|
91
280
|
}
|
|
92
281
|
|
|
93
|
-
|
|
282
|
+
const nextArgs = { ...baseArgs, tools: nextTools };
|
|
283
|
+
|
|
284
|
+
// Back-compat: mutate if possible
|
|
285
|
+
if (output && typeof output === "object" && !Object.isFrozen(output)) {
|
|
286
|
+
(output as any).args = nextArgs;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Forward-compat: return conservative merge
|
|
290
|
+
if (output && typeof output === "object") {
|
|
291
|
+
return { ...(output as any), args: nextArgs };
|
|
292
|
+
}
|
|
293
|
+
return { args: nextArgs };
|
|
94
294
|
},
|
|
95
295
|
|
|
96
296
|
"tool.execute.after": async (input: any, output: any) => {
|
|
97
297
|
// Truncate huge tool outputs to artifacts
|
|
98
|
-
if (truncatorHook &&
|
|
99
|
-
await truncatorHook(input, output);
|
|
298
|
+
if (truncatorHook && hookEnabled("tool-output-truncator")) {
|
|
299
|
+
await truncatorHook(input, output ?? null);
|
|
100
300
|
}
|
|
101
301
|
|
|
102
302
|
// Schedule continuation (do not immediately spam)
|
|
103
|
-
if (continuation &&
|
|
303
|
+
if (continuation && hookEnabled("continuation-enforcer")) {
|
|
104
304
|
await continuation.onToolAfter(input);
|
|
105
305
|
}
|
|
306
|
+
|
|
307
|
+
return output;
|
|
106
308
|
},
|
|
107
309
|
|
|
108
310
|
"chat.message": async (input: any, output: any) => {
|
|
109
|
-
if (injectProvider &&
|
|
311
|
+
if (injectProvider && hookEnabled("inject-provider")) {
|
|
110
312
|
await injectProvider.onChatMessage(input);
|
|
111
313
|
}
|
|
112
|
-
if (continuation &&
|
|
314
|
+
if (continuation && hookEnabled("continuation-enforcer")) {
|
|
113
315
|
await continuation.onChatMessage(input);
|
|
114
316
|
}
|
|
115
317
|
return output;
|
|
116
318
|
},
|
|
117
319
|
|
|
118
320
|
event: async (input: any) => {
|
|
119
|
-
if (continuation &&
|
|
321
|
+
if (continuation && hookEnabled("continuation-enforcer")) {
|
|
120
322
|
await continuation.onEvent(input);
|
|
121
323
|
}
|
|
122
324
|
},
|
|
123
325
|
|
|
124
326
|
// Best-effort cleanup
|
|
125
327
|
close: async () => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
328
|
+
if (db && typeof db.close === "function") {
|
|
329
|
+
try {
|
|
330
|
+
db.close();
|
|
331
|
+
} catch {
|
|
332
|
+
// ignore
|
|
333
|
+
}
|
|
130
334
|
}
|
|
131
335
|
|
|
132
|
-
if (
|
|
336
|
+
if (toastManager && pluginConfig.ui.toasts.enabled) {
|
|
133
337
|
try {
|
|
134
|
-
await
|
|
338
|
+
await toastManager.show({ title: "Astrocode", message: "Plugin closed", variant: "info" });
|
|
135
339
|
} catch {
|
|
136
340
|
// ignore
|
|
137
341
|
}
|
|
@@ -18,27 +18,18 @@ export interface DatabaseAdapter {
|
|
|
18
18
|
open(path: string, opts?: { busyTimeoutMs?: number }): DatabaseConnection;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
export class
|
|
21
|
+
// Null adapter for when no database driver is available
|
|
22
|
+
export class NullAdapter implements DatabaseAdapter {
|
|
23
23
|
isAvailable(): boolean {
|
|
24
|
-
return
|
|
24
|
+
return false;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
open(): DatabaseConnection {
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
pragma: () => {},
|
|
31
|
-
exec: () => {},
|
|
32
|
-
prepare: () => ({
|
|
33
|
-
run: () => ({ changes: 0, lastInsertRowid: null }),
|
|
34
|
-
get: () => null,
|
|
35
|
-
all: () => []
|
|
36
|
-
}),
|
|
37
|
-
close: () => {}
|
|
38
|
-
};
|
|
27
|
+
open(path: string, opts?: { busyTimeoutMs?: number }): DatabaseConnection {
|
|
28
|
+
throw new Error("No SQLite driver available");
|
|
39
29
|
}
|
|
40
30
|
}
|
|
41
31
|
|
|
32
|
+
|
|
42
33
|
// Bun SQLite adapter - singleton pattern to avoid multiple initializations
|
|
43
34
|
let bunDatabase: any = null;
|
|
44
35
|
let bunDatabaseInitialized = false;
|
|
@@ -127,7 +118,7 @@ export class BetterSqliteAdapter implements DatabaseAdapter {
|
|
|
127
118
|
}
|
|
128
119
|
|
|
129
120
|
return {
|
|
130
|
-
pragma: (sql: string) => db.pragma(sql
|
|
121
|
+
pragma: (sql: string) => db.pragma(sql),
|
|
131
122
|
exec: (sql: string) => db.exec(sql),
|
|
132
123
|
prepare: (sql: string) => db.prepare(sql),
|
|
133
124
|
close: () => db.close()
|
|
@@ -140,16 +131,20 @@ export function createDatabaseAdapter(): DatabaseAdapter {
|
|
|
140
131
|
const isBun = typeof (globalThis as any).Bun !== 'undefined';
|
|
141
132
|
|
|
142
133
|
if (isBun) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
134
|
+
// Prefer bun:sqlite in Bun runtime
|
|
135
|
+
const bunAdapter = new BunSqliteAdapter();
|
|
136
|
+
if (bunAdapter.isAvailable()) return bunAdapter;
|
|
137
|
+
// Fallback to better-sqlite3
|
|
138
|
+
const betterAdapter = new BetterSqliteAdapter();
|
|
139
|
+
if (betterAdapter.isAvailable()) return betterAdapter;
|
|
148
140
|
} else {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
141
|
+
// Prefer better-sqlite3 in Node.js
|
|
142
|
+
const betterAdapter = new BetterSqliteAdapter();
|
|
143
|
+
if (betterAdapter.isAvailable()) return betterAdapter;
|
|
144
|
+
// Fallback to bun:sqlite (rare but possible)
|
|
145
|
+
const bunAdapter = new BunSqliteAdapter();
|
|
146
|
+
if (bunAdapter.isAvailable()) return bunAdapter;
|
|
154
147
|
}
|
|
148
|
+
|
|
149
|
+
return new NullAdapter();
|
|
155
150
|
}
|