astrocode-workflow 0.1.59 → 0.3.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/dist/state/db.d.ts +1 -1
- package/dist/state/db.js +62 -4
- package/dist/tools/injects.js +90 -26
- package/dist/tools/workflow.d.ts +1 -1
- package/dist/tools/workflow.js +114 -94
- package/dist/ui/inject.d.ts +1 -1
- package/dist/ui/inject.js +74 -33
- package/dist/workflow/state-machine.d.ts +33 -17
- package/dist/workflow/state-machine.js +116 -40
- package/package.json +1 -1
- package/src/state/db.ts +63 -4
- package/src/tools/injects.ts +147 -53
- package/src/tools/workflow.ts +155 -136
- package/src/tools/workflow.ts.backup +681 -0
- package/src/ui/inject.ts +86 -41
- package/src/workflow/state-machine.ts +161 -67
package/dist/state/db.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export declare function openSqlite(dbPath: string, opts?: {
|
|
|
4
4
|
busyTimeoutMs?: number;
|
|
5
5
|
}): SqliteDb;
|
|
6
6
|
export declare function configurePragmas(db: SqliteDb, pragmas: Record<string, any>): void;
|
|
7
|
-
/** BEGIN IMMEDIATE transaction helper. */
|
|
7
|
+
/** BEGIN IMMEDIATE transaction helper (re-entrant). */
|
|
8
8
|
export declare function withTx<T>(db: SqliteDb, fn: () => T, opts?: {
|
|
9
9
|
require?: boolean;
|
|
10
10
|
}): T;
|
package/dist/state/db.js
CHANGED
|
@@ -46,7 +46,30 @@ export function configurePragmas(db, pragmas) {
|
|
|
46
46
|
if (pragmas.temp_store)
|
|
47
47
|
db.pragma(`temp_store = ${pragmas.temp_store}`);
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Re-entrant transaction helper.
|
|
51
|
+
*
|
|
52
|
+
* SQLite rejects BEGIN inside BEGIN. We use:
|
|
53
|
+
* - depth=0: BEGIN IMMEDIATE ... COMMIT/ROLLBACK
|
|
54
|
+
* - depth>0: SAVEPOINT sp_n ... RELEASE / ROLLBACK TO + RELEASE
|
|
55
|
+
*
|
|
56
|
+
* This allows callers to safely nest withTx across layers (tools -> workflow -> state machine)
|
|
57
|
+
* without "cannot start a transaction within a transaction".
|
|
58
|
+
*/
|
|
59
|
+
const TX_DEPTH = new WeakMap();
|
|
60
|
+
function getDepth(db) {
|
|
61
|
+
return TX_DEPTH.get(db) ?? 0;
|
|
62
|
+
}
|
|
63
|
+
function setDepth(db, depth) {
|
|
64
|
+
if (depth <= 0)
|
|
65
|
+
TX_DEPTH.delete(db);
|
|
66
|
+
else
|
|
67
|
+
TX_DEPTH.set(db, depth);
|
|
68
|
+
}
|
|
69
|
+
function savepointName(depth) {
|
|
70
|
+
return `sp_${depth}`;
|
|
71
|
+
}
|
|
72
|
+
/** BEGIN IMMEDIATE transaction helper (re-entrant). */
|
|
50
73
|
export function withTx(db, fn, opts) {
|
|
51
74
|
const adapter = createDatabaseAdapter();
|
|
52
75
|
const available = adapter.isAvailable();
|
|
@@ -55,21 +78,56 @@ export function withTx(db, fn, opts) {
|
|
|
55
78
|
throw new Error("Database adapter unavailable; transaction required.");
|
|
56
79
|
return fn();
|
|
57
80
|
}
|
|
58
|
-
db
|
|
81
|
+
const depth = getDepth(db);
|
|
82
|
+
if (depth === 0) {
|
|
83
|
+
db.exec("BEGIN IMMEDIATE");
|
|
84
|
+
setDepth(db, 1);
|
|
85
|
+
try {
|
|
86
|
+
const out = fn();
|
|
87
|
+
db.exec("COMMIT");
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
try {
|
|
92
|
+
db.exec("ROLLBACK");
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// ignore
|
|
96
|
+
}
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
setDepth(db, 0);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Nested: use SAVEPOINT
|
|
104
|
+
const nextDepth = depth + 1;
|
|
105
|
+
const sp = savepointName(nextDepth);
|
|
106
|
+
db.exec(`SAVEPOINT ${sp}`);
|
|
107
|
+
setDepth(db, nextDepth);
|
|
59
108
|
try {
|
|
60
109
|
const out = fn();
|
|
61
|
-
db.exec(
|
|
110
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
62
111
|
return out;
|
|
63
112
|
}
|
|
64
113
|
catch (e) {
|
|
65
114
|
try {
|
|
66
|
-
db.exec(
|
|
115
|
+
db.exec(`ROLLBACK TO SAVEPOINT ${sp}`);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// ignore
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
db.exec(`RELEASE SAVEPOINT ${sp}`);
|
|
67
122
|
}
|
|
68
123
|
catch {
|
|
69
124
|
// ignore
|
|
70
125
|
}
|
|
71
126
|
throw e;
|
|
72
127
|
}
|
|
128
|
+
finally {
|
|
129
|
+
setDepth(db, depth);
|
|
130
|
+
}
|
|
73
131
|
}
|
|
74
132
|
export function getSchemaVersion(db) {
|
|
75
133
|
try {
|
package/dist/tools/injects.js
CHANGED
|
@@ -2,28 +2,41 @@ import { tool } from "@opencode-ai/plugin/tool";
|
|
|
2
2
|
import { withTx } from "../state/db";
|
|
3
3
|
import { nowISO } from "../shared/time";
|
|
4
4
|
import { sha256Hex } from "../shared/hash";
|
|
5
|
-
const VALID_INJECT_TYPES = [
|
|
5
|
+
const VALID_INJECT_TYPES = ["note", "policy", "reminder", "debug"];
|
|
6
6
|
function validateInjectType(type) {
|
|
7
7
|
if (!VALID_INJECT_TYPES.includes(type)) {
|
|
8
|
-
throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(
|
|
8
|
+
throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(", ")}`);
|
|
9
9
|
}
|
|
10
10
|
return type;
|
|
11
11
|
}
|
|
12
12
|
function validateTimestamp(timestamp) {
|
|
13
13
|
if (!timestamp)
|
|
14
14
|
return null;
|
|
15
|
-
//
|
|
15
|
+
// Strict ISO 8601 UTC with Z suffix, sortable as string.
|
|
16
16
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
|
|
17
17
|
if (!isoRegex.test(timestamp)) {
|
|
18
18
|
throw new Error(`Invalid timestamp format. Expected ISO 8601 UTC with Z suffix (e.g., "2026-01-23T13:05:19.000Z"), got: "${timestamp}"`);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (isNaN(parsed.getTime())) {
|
|
20
|
+
const parsed = Date.parse(timestamp);
|
|
21
|
+
if (!Number.isFinite(parsed)) {
|
|
23
22
|
throw new Error(`Invalid timestamp: "${timestamp}" does not represent a valid date`);
|
|
24
23
|
}
|
|
25
24
|
return timestamp;
|
|
26
25
|
}
|
|
26
|
+
function parseJsonStringArray(name, raw) {
|
|
27
|
+
let v;
|
|
28
|
+
try {
|
|
29
|
+
v = JSON.parse(raw);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
33
|
+
throw new Error(`${name} must be valid JSON. Parse error: ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) {
|
|
36
|
+
throw new Error(`${name} must be a JSON array of strings`);
|
|
37
|
+
}
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
27
40
|
function newInjectId() {
|
|
28
41
|
return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
29
42
|
}
|
|
@@ -46,15 +59,34 @@ export function createAstroInjectPutTool(opts) {
|
|
|
46
59
|
const id = inject_id ?? newInjectId();
|
|
47
60
|
const now = nowISO();
|
|
48
61
|
const sha = sha256Hex(body_md);
|
|
49
|
-
// Validate inputs
|
|
50
62
|
const validatedType = validateInjectType(type);
|
|
51
63
|
const validatedExpiresAt = validateTimestamp(expires_at);
|
|
64
|
+
// Ensure tags_json is at least valid JSON (we do not enforce schema here beyond validity).
|
|
65
|
+
try {
|
|
66
|
+
JSON.parse(tags_json);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
70
|
+
throw new Error(`tags_json must be valid JSON. Parse error: ${msg}`);
|
|
71
|
+
}
|
|
52
72
|
return withTx(db, () => {
|
|
53
73
|
const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id);
|
|
54
74
|
if (existing) {
|
|
55
|
-
// Use INSERT ... ON CONFLICT for atomic updates
|
|
56
75
|
db.prepare(`
|
|
57
|
-
INSERT INTO injects (
|
|
76
|
+
INSERT INTO injects (
|
|
77
|
+
inject_id,
|
|
78
|
+
type,
|
|
79
|
+
title,
|
|
80
|
+
body_md,
|
|
81
|
+
tags_json,
|
|
82
|
+
scope,
|
|
83
|
+
source,
|
|
84
|
+
priority,
|
|
85
|
+
expires_at,
|
|
86
|
+
sha256,
|
|
87
|
+
created_at,
|
|
88
|
+
updated_at
|
|
89
|
+
)
|
|
58
90
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
59
91
|
ON CONFLICT(inject_id) DO UPDATE SET
|
|
60
92
|
type=excluded.type,
|
|
@@ -67,7 +99,7 @@ export function createAstroInjectPutTool(opts) {
|
|
|
67
99
|
expires_at=excluded.expires_at,
|
|
68
100
|
sha256=excluded.sha256,
|
|
69
101
|
updated_at=excluded.updated_at
|
|
70
|
-
`).run(id,
|
|
102
|
+
`).run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
|
|
71
103
|
return `✅ Updated inject ${id}: ${title}`;
|
|
72
104
|
}
|
|
73
105
|
db.prepare("INSERT INTO injects (inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at, sha256, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)").run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
|
|
@@ -93,10 +125,18 @@ export function createAstroInjectListTool(opts) {
|
|
|
93
125
|
params.push(scope);
|
|
94
126
|
}
|
|
95
127
|
if (type) {
|
|
128
|
+
// Keep list tool permissive (debugging), but still prevents obvious garbage if used.
|
|
129
|
+
validateInjectType(type);
|
|
96
130
|
where.push("type = ?");
|
|
97
131
|
params.push(type);
|
|
98
132
|
}
|
|
99
|
-
const sql = `
|
|
133
|
+
const sql = `
|
|
134
|
+
SELECT inject_id, type, title, scope, priority, created_at, updated_at
|
|
135
|
+
FROM injects
|
|
136
|
+
${where.length ? "WHERE " + where.join(" AND ") : ""}
|
|
137
|
+
ORDER BY priority DESC, updated_at DESC
|
|
138
|
+
LIMIT ?
|
|
139
|
+
`;
|
|
100
140
|
const rows = db.prepare(sql).all(...params, limit);
|
|
101
141
|
return JSON.stringify(rows, null, 2);
|
|
102
142
|
},
|
|
@@ -134,7 +174,13 @@ export function createAstroInjectSearchTool(opts) {
|
|
|
134
174
|
where.push("scope = ?");
|
|
135
175
|
params.push(scope);
|
|
136
176
|
}
|
|
137
|
-
const sql = `
|
|
177
|
+
const sql = `
|
|
178
|
+
SELECT inject_id, type, title, scope, priority, updated_at
|
|
179
|
+
FROM injects
|
|
180
|
+
WHERE ${where.join(" AND ")}
|
|
181
|
+
ORDER BY priority DESC, updated_at DESC
|
|
182
|
+
LIMIT ?
|
|
183
|
+
`;
|
|
138
184
|
const rows = db.prepare(sql).all(...params, limit);
|
|
139
185
|
return JSON.stringify(rows, null, 2);
|
|
140
186
|
},
|
|
@@ -142,7 +188,13 @@ export function createAstroInjectSearchTool(opts) {
|
|
|
142
188
|
}
|
|
143
189
|
export function selectEligibleInjects(db, opts) {
|
|
144
190
|
const { nowIso, scopeAllowlist, typeAllowlist, limit = 50 } = opts;
|
|
145
|
-
|
|
191
|
+
if (!Array.isArray(scopeAllowlist) || scopeAllowlist.length === 0) {
|
|
192
|
+
throw new Error("selectEligibleInjects: scopeAllowlist must be a non-empty array");
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(typeAllowlist) || typeAllowlist.length === 0) {
|
|
195
|
+
throw new Error("selectEligibleInjects: typeAllowlist must be a non-empty array");
|
|
196
|
+
}
|
|
197
|
+
// Build placeholders safely (guaranteed non-empty).
|
|
146
198
|
const scopeQs = scopeAllowlist.map(() => "?").join(", ");
|
|
147
199
|
const typeQs = typeAllowlist.map(() => "?").join(", ");
|
|
148
200
|
const sql = `
|
|
@@ -168,8 +220,11 @@ export function createAstroInjectEligibleTool(opts) {
|
|
|
168
220
|
},
|
|
169
221
|
execute: async ({ scopes_json, types_json, limit }) => {
|
|
170
222
|
const now = nowISO();
|
|
171
|
-
const scopes =
|
|
172
|
-
const types =
|
|
223
|
+
const scopes = parseJsonStringArray("scopes_json", scopes_json);
|
|
224
|
+
const types = parseJsonStringArray("types_json", types_json);
|
|
225
|
+
// Validate types against the known set to keep selection sane.
|
|
226
|
+
for (const t of types)
|
|
227
|
+
validateInjectType(t);
|
|
173
228
|
const rows = selectEligibleInjects(db, {
|
|
174
229
|
nowIso: now,
|
|
175
230
|
scopeAllowlist: scopes,
|
|
@@ -190,30 +245,38 @@ export function createAstroInjectDebugDueTool(opts) {
|
|
|
190
245
|
},
|
|
191
246
|
execute: async ({ scopes_json, types_json }) => {
|
|
192
247
|
const now = nowISO();
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
|
|
248
|
+
const nowMs = Date.parse(now);
|
|
249
|
+
const scopes = parseJsonStringArray("scopes_json", scopes_json);
|
|
250
|
+
const types = parseJsonStringArray("types_json", types_json);
|
|
251
|
+
for (const t of types)
|
|
252
|
+
validateInjectType(t);
|
|
196
253
|
const allInjects = db.prepare("SELECT * FROM injects").all();
|
|
197
254
|
let total = allInjects.length;
|
|
198
255
|
let selected = 0;
|
|
199
256
|
let skippedExpired = 0;
|
|
200
257
|
let skippedScope = 0;
|
|
201
258
|
let skippedType = 0;
|
|
259
|
+
let skippedUnparseableExpiresAt = 0;
|
|
202
260
|
const excludedReasons = [];
|
|
203
261
|
const selectedInjects = [];
|
|
204
262
|
for (const inject of allInjects) {
|
|
205
263
|
const reasons = [];
|
|
206
|
-
//
|
|
207
|
-
if (inject.expires_at
|
|
208
|
-
|
|
209
|
-
|
|
264
|
+
// Expiration: parse to ms for correctness across legacy rows.
|
|
265
|
+
if (inject.expires_at) {
|
|
266
|
+
const expMs = Date.parse(String(inject.expires_at));
|
|
267
|
+
if (!Number.isFinite(expMs)) {
|
|
268
|
+
reasons.push("expires_at_unparseable");
|
|
269
|
+
skippedUnparseableExpiresAt++;
|
|
270
|
+
}
|
|
271
|
+
else if (expMs <= nowMs) {
|
|
272
|
+
reasons.push("expired");
|
|
273
|
+
skippedExpired++;
|
|
274
|
+
}
|
|
210
275
|
}
|
|
211
|
-
// Check scope
|
|
212
276
|
if (!scopes.includes(inject.scope)) {
|
|
213
277
|
reasons.push("scope");
|
|
214
278
|
skippedScope++;
|
|
215
279
|
}
|
|
216
|
-
// Check type
|
|
217
280
|
if (!types.includes(inject.type)) {
|
|
218
281
|
reasons.push("type");
|
|
219
282
|
skippedType++;
|
|
@@ -222,7 +285,7 @@ export function createAstroInjectDebugDueTool(opts) {
|
|
|
222
285
|
excludedReasons.push({
|
|
223
286
|
inject_id: inject.inject_id,
|
|
224
287
|
title: inject.title,
|
|
225
|
-
reasons
|
|
288
|
+
reasons,
|
|
226
289
|
scope: inject.scope,
|
|
227
290
|
type: inject.type,
|
|
228
291
|
expires_at: inject.expires_at,
|
|
@@ -249,9 +312,10 @@ export function createAstroInjectDebugDueTool(opts) {
|
|
|
249
312
|
excluded_total: total - selected,
|
|
250
313
|
skipped_breakdown: {
|
|
251
314
|
expired: skippedExpired,
|
|
315
|
+
expires_at_unparseable: skippedUnparseableExpiresAt,
|
|
252
316
|
scope: skippedScope,
|
|
253
317
|
type: skippedType,
|
|
254
|
-
}
|
|
318
|
+
},
|
|
255
319
|
},
|
|
256
320
|
selected_injects: selectedInjects,
|
|
257
321
|
excluded_injects: excludedReasons,
|
package/dist/tools/workflow.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
|
|
|
2
2
|
import type { AstrocodeConfig } from "../config/schema";
|
|
3
3
|
import type { SqliteDb } from "../state/db";
|
|
4
4
|
import type { StageKey } from "../state/types";
|
|
5
|
+
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
5
6
|
export declare const STAGE_TO_AGENT_MAP: Record<string, string>;
|
|
6
7
|
export declare function resolveAgentName(stageKey: StageKey, config: AstrocodeConfig, agents?: any, warnings?: string[]): string;
|
|
7
|
-
import { AgentConfig } from "@opencode-ai/sdk";
|
|
8
8
|
export declare function createAstroWorkflowProceedTool(opts: {
|
|
9
9
|
ctx: any;
|
|
10
10
|
config: AstrocodeConfig;
|