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/dist/tools/index.d.ts
CHANGED
|
@@ -2,9 +2,19 @@ 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 { AgentConfig } from "@opencode-ai/sdk";
|
|
5
|
-
|
|
5
|
+
type RuntimeState = {
|
|
6
|
+
db: SqliteDb | null;
|
|
7
|
+
limitedMode: boolean;
|
|
8
|
+
limitedModeReason: null | {
|
|
9
|
+
code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
|
|
10
|
+
details: any;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
type CreateAstroToolsOptions = {
|
|
6
14
|
ctx: any;
|
|
7
15
|
config: AstrocodeConfig;
|
|
8
|
-
db: SqliteDb;
|
|
9
16
|
agents?: Record<string, AgentConfig>;
|
|
10
|
-
|
|
17
|
+
runtime: RuntimeState;
|
|
18
|
+
};
|
|
19
|
+
export declare function createAstroTools(opts: CreateAstroToolsOptions): Record<string, ToolDefinition>;
|
|
20
|
+
export {};
|
package/dist/tools/index.js
CHANGED
|
@@ -9,17 +9,21 @@ import { createAstroArtifactPutTool, createAstroArtifactListTool, createAstroArt
|
|
|
9
9
|
import { createAstroInjectPutTool, createAstroInjectListTool, createAstroInjectSearchTool, createAstroInjectGetTool, createAstroInjectEligibleTool, createAstroInjectDebugDueTool } from "./injects";
|
|
10
10
|
import { createAstroRepairTool } from "./repair";
|
|
11
11
|
export function createAstroTools(opts) {
|
|
12
|
-
const { ctx, config,
|
|
13
|
-
const
|
|
12
|
+
const { ctx, config, agents, runtime } = opts;
|
|
13
|
+
const { db } = runtime;
|
|
14
|
+
const hasDatabase = db !== null; // Source of truth: DB availability
|
|
14
15
|
const tools = {};
|
|
15
|
-
// Always available tools
|
|
16
|
-
tools.astro_status = createAstroStatusTool({ ctx, config
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
tools.
|
|
16
|
+
// Always available tools (work without database - guaranteed DB-independent)
|
|
17
|
+
tools.astro_status = createAstroStatusTool({ ctx, config });
|
|
18
|
+
tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
|
|
19
|
+
// Recovery tool - available even in limited mode to allow DB initialization
|
|
20
|
+
tools.astro_init = createAstroInitTool({ ctx, config, runtime });
|
|
20
21
|
// Database-dependent tools
|
|
21
22
|
if (hasDatabase) {
|
|
22
|
-
|
|
23
|
+
// Ensure agents are available for workflow tools that require them
|
|
24
|
+
if (!agents) {
|
|
25
|
+
throw new Error("astro_workflow_proceed requires agents to be provided in normal mode.");
|
|
26
|
+
}
|
|
23
27
|
tools.astro_story_queue = createAstroStoryQueueTool({ ctx, config, db });
|
|
24
28
|
tools.astro_story_approve = createAstroStoryApproveTool({ ctx, config, db });
|
|
25
29
|
tools.astro_story_board = createAstroStoryBoardTool({ ctx, config, db });
|
|
@@ -43,29 +47,6 @@ export function createAstroTools(opts) {
|
|
|
43
47
|
tools.astro_inject_debug_due = createAstroInjectDebugDueTool({ ctx, config, db });
|
|
44
48
|
tools.astro_repair = createAstroRepairTool({ ctx, config, db });
|
|
45
49
|
}
|
|
46
|
-
else {
|
|
47
|
-
// Limited mode tools - provide helpful messages instead of failing
|
|
48
|
-
tools.astro_init = {
|
|
49
|
-
description: "Initialize Astrocode (requires database - currently unavailable)",
|
|
50
|
-
args: {},
|
|
51
|
-
execute: async () => "❌ Database not available. Astrocode is running in limited mode."
|
|
52
|
-
};
|
|
53
|
-
tools.astro_story_queue = {
|
|
54
|
-
description: "Queue a story (requires database - currently unavailable)",
|
|
55
|
-
args: {},
|
|
56
|
-
execute: async () => "❌ Database not available. Astrocode is running in limited mode."
|
|
57
|
-
};
|
|
58
|
-
tools.astro_spec_set = {
|
|
59
|
-
description: "Set project spec (requires database for hash tracking - currently unavailable)",
|
|
60
|
-
args: {},
|
|
61
|
-
execute: async () => "❌ Database not available. Astrocode is running in limited mode."
|
|
62
|
-
};
|
|
63
|
-
tools.astro_workflow_proceed = {
|
|
64
|
-
description: "Advance workflow (requires database - currently unavailable)",
|
|
65
|
-
args: {},
|
|
66
|
-
execute: async () => "❌ Database not available. Astrocode is running in limited mode."
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
50
|
// Create aliases for backward compatibility
|
|
70
51
|
const aliases = [
|
|
71
52
|
["_astro_init", "astro_init"],
|
|
@@ -90,6 +71,8 @@ export function createAstroTools(opts) {
|
|
|
90
71
|
["_astro_inject_list", "astro_inject_list"],
|
|
91
72
|
["_astro_inject_search", "astro_inject_search"],
|
|
92
73
|
["_astro_inject_get", "astro_inject_get"],
|
|
74
|
+
["_astro_inject_eligible", "astro_inject_eligible"],
|
|
75
|
+
["_astro_inject_debug_due", "astro_inject_debug_due"],
|
|
93
76
|
["_astro_repair", "astro_repair"],
|
|
94
77
|
];
|
|
95
78
|
// Only add aliases for tools that exist
|
package/dist/tools/init.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
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
|
+
type RuntimeState = {
|
|
5
|
+
db: SqliteDb | null;
|
|
6
|
+
limitedMode: boolean;
|
|
7
|
+
limitedModeReason: null | {
|
|
8
|
+
code: "db_init_failed" | "schema_too_old" | "schema_downgrade" | "schema_migration_failed";
|
|
9
|
+
details: any;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
4
12
|
export declare function createAstroInitTool(opts: {
|
|
5
13
|
ctx: any;
|
|
6
14
|
config: AstrocodeConfig;
|
|
7
|
-
|
|
15
|
+
runtime: RuntimeState;
|
|
8
16
|
}): ToolDefinition;
|
|
17
|
+
export {};
|
package/dist/tools/init.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
4
|
-
import { ensureSchema } from "../state/db";
|
|
4
|
+
import { ensureSchema, openSqlite, configurePragmas } from "../state/db";
|
|
5
5
|
import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
|
|
6
6
|
import { nowISO } from "../shared/time";
|
|
7
7
|
import { sha256Hex } from "../shared/hash";
|
|
8
8
|
export function createAstroInitTool(opts) {
|
|
9
|
-
const { ctx, config,
|
|
9
|
+
const { ctx, config, runtime } = opts;
|
|
10
10
|
return tool({
|
|
11
11
|
description: "Initialize Astrocode vNext in this repo: create .astro directories, ensure SQLite schema, and create a placeholder spec if missing. Idempotent.",
|
|
12
12
|
args: {
|
|
@@ -17,25 +17,80 @@ export function createAstroInitTool(opts) {
|
|
|
17
17
|
const repoRoot = ctx.directory;
|
|
18
18
|
const paths = getAstroPaths(repoRoot, config.db.path);
|
|
19
19
|
ensureAstroDirs(paths);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
const hadDbAlready = !!runtime.db;
|
|
21
|
+
let db = runtime.db;
|
|
22
|
+
let publishedToRuntime = false;
|
|
23
|
+
try {
|
|
24
|
+
if (!db) {
|
|
25
|
+
try {
|
|
26
|
+
db = openSqlite(paths.dbPath, { busyTimeoutMs: config.db.busy_timeout_ms });
|
|
27
|
+
configurePragmas(db, config.db.pragmas);
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
31
|
+
throw new Error(`❌ Failed to open database at ${paths.dbPath}: ${msg}. Install a SQLite driver (better-sqlite3/bun:sqlite) and check file permissions.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Source-of-truth for schema + repo_state invariants.
|
|
35
|
+
ensureSchema(db, { allowAutoMigrate: config.db.allow_auto_migrate });
|
|
36
|
+
// Postcondition: repo_state must exist after ensureSchema.
|
|
37
|
+
try {
|
|
38
|
+
db.prepare("SELECT schema_version FROM repo_state WHERE id = 1").get();
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
42
|
+
throw new Error(`❌ Schema initialization incomplete: repo_state missing/unreadable after ensureSchema (${msg})`);
|
|
43
|
+
}
|
|
44
|
+
if (ensure_spec) {
|
|
45
|
+
if (!fs.existsSync(paths.specPath)) {
|
|
46
|
+
fs.writeFileSync(paths.specPath, spec_placeholder);
|
|
47
|
+
}
|
|
48
|
+
const content = fs.readFileSync(paths.specPath, "utf8");
|
|
49
|
+
const specHash = sha256Hex(content);
|
|
25
50
|
const now = nowISO();
|
|
26
|
-
|
|
51
|
+
// Update hash; fail with a clear error if schema is mismatched.
|
|
52
|
+
try {
|
|
53
|
+
db.prepare(`
|
|
54
|
+
UPDATE repo_state
|
|
55
|
+
SET spec_hash_after = ?, updated_at = ?
|
|
56
|
+
WHERE id = 1
|
|
57
|
+
`).run(specHash, now);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
61
|
+
throw new Error(`❌ Failed to update repo_state spec hash (schema mismatch?): ${msg}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Best-effort: if we recovered DB in-process, publish it so the harness can use it immediately.
|
|
65
|
+
// (If your harness reads runtime at creation-time only, this still helps future tool calls.)
|
|
66
|
+
if (!hadDbAlready && db) {
|
|
67
|
+
runtime.db = db;
|
|
68
|
+
runtime.limitedMode = false;
|
|
69
|
+
runtime.limitedModeReason = null;
|
|
70
|
+
publishedToRuntime = true;
|
|
71
|
+
}
|
|
72
|
+
const stat = db.prepare("SELECT schema_version, created_at, updated_at FROM repo_state WHERE id=1").get();
|
|
73
|
+
return [
|
|
74
|
+
`✅ Astrocode initialized.`,
|
|
75
|
+
``,
|
|
76
|
+
`- Repo: ${repoRoot}`,
|
|
77
|
+
`- DB: ${path.relative(repoRoot, paths.dbPath)} (schema_version=${stat?.schema_version ?? "?"})`,
|
|
78
|
+
`- Spec: ${path.relative(repoRoot, paths.specPath)}`,
|
|
79
|
+
``,
|
|
80
|
+
publishedToRuntime
|
|
81
|
+
? `Next: run /astro-status. (DB recovered in-process.)`
|
|
82
|
+
: `Next: restart the agent/runtime if Astrocode is still in Limited Mode, then run /astro-status.`,
|
|
83
|
+
].join("\n");
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
// Only close if this tool opened it AND we did not publish it for ongoing use.
|
|
87
|
+
if (!hadDbAlready && !publishedToRuntime && db && typeof db.close === "function") {
|
|
88
|
+
try {
|
|
89
|
+
db.close();
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
27
92
|
}
|
|
28
93
|
}
|
|
29
|
-
const stat = db.prepare("SELECT schema_version, created_at, updated_at FROM repo_state WHERE id=1").get();
|
|
30
|
-
return [
|
|
31
|
-
`✅ Astrocode initialized.`,
|
|
32
|
-
``,
|
|
33
|
-
`- Repo: ${repoRoot}`,
|
|
34
|
-
`- DB: ${path.relative(repoRoot, paths.dbPath)} (schema_version=${stat?.schema_version ?? "?"})`,
|
|
35
|
-
`- Spec: ${path.relative(repoRoot, paths.specPath)}`,
|
|
36
|
-
``,
|
|
37
|
-
`Next: queue a story (astro_story_queue) and approve it (astro_story_approve), or run /astro-status.`,
|
|
38
|
-
].join("\n");
|
|
39
94
|
},
|
|
40
95
|
});
|
|
41
96
|
}
|
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/spec.d.ts
CHANGED
package/dist/tools/spec.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
|
|
|
5
5
|
import { nowISO } from "../shared/time";
|
|
6
6
|
import { sha256Hex } from "../shared/hash";
|
|
7
7
|
export function createAstroSpecGetTool(opts) {
|
|
8
|
-
const { ctx, config
|
|
8
|
+
const { ctx, config } = opts;
|
|
9
9
|
return tool({
|
|
10
10
|
description: "Get current project spec stored at .astro/spec.md",
|
|
11
11
|
args: {},
|
|
@@ -28,6 +28,9 @@ export function createAstroSpecSetTool(opts) {
|
|
|
28
28
|
spec_md: tool.schema.string().min(1),
|
|
29
29
|
},
|
|
30
30
|
execute: async ({ spec_md }) => {
|
|
31
|
+
if (!db) {
|
|
32
|
+
return "❌ Database not available. Cannot track spec hash. Astrocode is running in limited mode.";
|
|
33
|
+
}
|
|
31
34
|
const repoRoot = ctx.directory;
|
|
32
35
|
const paths = getAstroPaths(repoRoot, config.db.path);
|
|
33
36
|
ensureAstroDirs(paths);
|
package/dist/tools/status.d.ts
CHANGED
package/dist/tools/status.js
CHANGED
|
@@ -39,69 +39,87 @@ export function createAstroStatusTool(opts) {
|
|
|
39
39
|
include_recent_events: tool.schema.boolean().default(false),
|
|
40
40
|
},
|
|
41
41
|
execute: async ({ include_board, include_recent_events }) => {
|
|
42
|
-
// Check if database is available
|
|
43
42
|
if (!db) {
|
|
44
|
-
return
|
|
43
|
+
return [
|
|
44
|
+
`⚠️ Astrocode not initialized.`,
|
|
45
|
+
``,
|
|
46
|
+
`- Reason: Database not available`,
|
|
47
|
+
``,
|
|
48
|
+
`Next: run **astro_init**, then restart the agent/runtime, then run /astro-status.`,
|
|
49
|
+
].join("\n");
|
|
45
50
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
try {
|
|
52
|
+
const active = getActiveRun(db);
|
|
53
|
+
const lines = [];
|
|
54
|
+
lines.push(`# Astrocode Status`);
|
|
55
|
+
if (!active) {
|
|
56
|
+
lines.push(`- Active run: *(none)*`);
|
|
57
|
+
const next = decideNextAction(db, config);
|
|
58
|
+
if (next.kind === "idle")
|
|
59
|
+
lines.push(`- Next: ${next.reason === "no_approved_stories" ? "No approved stories." : "Idle."}`);
|
|
60
|
+
if (include_board) {
|
|
61
|
+
const counts = db.prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state").all();
|
|
62
|
+
lines.push(``, `## Story board`);
|
|
63
|
+
for (const row of counts)
|
|
64
|
+
lines.push(`- ${row.state}: ${row.c}`);
|
|
65
|
+
}
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
const story = getStory(db, active.story_key);
|
|
69
|
+
const stageRuns = getStageRuns(db, active.run_id);
|
|
70
|
+
lines.push(`- Active run: \`${active.run_id}\` ${statusIcon(active.status)} **${active.status}**`);
|
|
71
|
+
lines.push(`- Story: \`${active.story_key}\` — ${story?.title ?? "(missing)"} (${story?.state ?? "?"})`);
|
|
72
|
+
lines.push(`- Current stage: \`${active.current_stage_key ?? "?"}\``);
|
|
73
|
+
lines.push(``, `## Pipeline`);
|
|
74
|
+
for (const s of stageRuns)
|
|
75
|
+
lines.push(`- ${stageIcon(s.status)} \`${s.stage_key}\` (${s.status})`);
|
|
51
76
|
const next = decideNextAction(db, config);
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
lines.push(``, `## Next`, `- ${next.kind}`);
|
|
78
|
+
if (next.kind === "await_stage_completion") {
|
|
79
|
+
lines.push(`- Awaiting output for stage \`${next.stage_key}\`. When you have it, call **astro_stage_complete** with the stage output text.`);
|
|
80
|
+
}
|
|
81
|
+
else if (next.kind === "delegate_stage") {
|
|
82
|
+
lines.push(`- Need to run stage \`${next.stage_key}\`. Use **astro_workflow_proceed** (or delegate to the matching stage agent).`);
|
|
83
|
+
}
|
|
84
|
+
else if (next.kind === "complete_run") {
|
|
85
|
+
lines.push(`- All stages done. Run can be completed via **astro_workflow_proceed**.`);
|
|
86
|
+
}
|
|
87
|
+
else if (next.kind === "failed") {
|
|
88
|
+
lines.push(`- Run failed at \`${next.stage_key}\`: ${next.error_text}`);
|
|
89
|
+
}
|
|
54
90
|
if (include_board) {
|
|
55
|
-
const counts = db
|
|
56
|
-
.prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state")
|
|
57
|
-
.all();
|
|
91
|
+
const counts = db.prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state").all();
|
|
58
92
|
lines.push(``, `## Story board`);
|
|
59
93
|
for (const row of counts)
|
|
60
94
|
lines.push(`- ${row.state}: ${row.c}`);
|
|
61
95
|
}
|
|
96
|
+
if (include_recent_events) {
|
|
97
|
+
const evs = db.prepare("SELECT created_at, type, run_id, stage_key FROM events ORDER BY created_at DESC LIMIT 10").all();
|
|
98
|
+
lines.push(``, `## Recent events`);
|
|
99
|
+
for (const e of evs)
|
|
100
|
+
lines.push(`- ${e.created_at} — ${e.type}${e.run_id ? ` (run=${e.run_id})` : ""}${e.stage_key ? ` stage=${e.stage_key}` : ""}`);
|
|
101
|
+
}
|
|
62
102
|
return lines.join("\n");
|
|
63
103
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
else if (next.kind === "complete_run") {
|
|
83
|
-
lines.push(`- All stages done. Run can be completed via **astro_workflow_proceed**.`);
|
|
84
|
-
}
|
|
85
|
-
else if (next.kind === "failed") {
|
|
86
|
-
lines.push(`- Run failed at \`${next.stage_key}\`: ${next.error_text}`);
|
|
87
|
-
}
|
|
88
|
-
if (include_board) {
|
|
89
|
-
const counts = db
|
|
90
|
-
.prepare("SELECT state, COUNT(*) as c FROM stories GROUP BY state ORDER BY state")
|
|
91
|
-
.all();
|
|
92
|
-
lines.push(``, `## Story board`);
|
|
93
|
-
for (const row of counts)
|
|
94
|
-
lines.push(`- ${row.state}: ${row.c}`);
|
|
95
|
-
}
|
|
96
|
-
if (include_recent_events) {
|
|
97
|
-
const evs = db
|
|
98
|
-
.prepare("SELECT created_at, type, run_id, stage_key FROM events ORDER BY created_at DESC LIMIT 10")
|
|
99
|
-
.all();
|
|
100
|
-
lines.push(``, `## Recent events`);
|
|
101
|
-
for (const e of evs)
|
|
102
|
-
lines.push(`- ${e.created_at} — ${e.type}${e.run_id ? ` (run=${e.run_id})` : ""}${e.stage_key ? ` stage=${e.stage_key}` : ""}`);
|
|
104
|
+
catch (e) {
|
|
105
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
106
|
+
if (msg.includes("no such table") || msg.includes("no such column")) {
|
|
107
|
+
return [
|
|
108
|
+
`⚠️ Astrocode not initialized.`,
|
|
109
|
+
``,
|
|
110
|
+
`- Reason: Database present but schema is not initialized or is incompatible`,
|
|
111
|
+
`- Error: ${msg}`,
|
|
112
|
+
``,
|
|
113
|
+
`Next: run **astro_init**, then restart the agent/runtime, then run /astro-status.`,
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
return [
|
|
117
|
+
`# Astrocode Status`,
|
|
118
|
+
``,
|
|
119
|
+
`⛔ Database error.`,
|
|
120
|
+
`Error: ${msg}`,
|
|
121
|
+
].join("\n");
|
|
103
122
|
}
|
|
104
|
-
return lines.join("\n");
|
|
105
123
|
},
|
|
106
124
|
});
|
|
107
125
|
}
|
package/dist/tools/workflow.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
2
|
import { withTx } from "../state/db";
|
|
3
3
|
import { buildContextSnapshot } from "../workflow/context";
|
|
4
|
-
import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun } from "../workflow/state-machine";
|
|
4
|
+
import { decideNextAction, createRunForStory, startStage, completeRun, getActiveRun, EVENT_TYPES } from "../workflow/state-machine";
|
|
5
5
|
import { buildStageDirective, directiveHash } from "../workflow/directives";
|
|
6
6
|
import { injectChatPrompt } from "../ui/inject";
|
|
7
7
|
import { nowISO } from "../shared/time";
|
|
@@ -318,7 +318,7 @@ export function createAstroWorkflowProceedTool(opts) {
|
|
|
318
318
|
break;
|
|
319
319
|
}
|
|
320
320
|
// Housekeeping event
|
|
321
|
-
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL,
|
|
321
|
+
db.prepare("INSERT INTO events (event_id, run_id, stage_key, type, body_json, created_at) VALUES (?, NULL, NULL, ?, ?, ?)").run(newEventId(), EVENT_TYPES.WORKFLOW_PROCEED, JSON.stringify({ started_at: startedAt, mode, max_steps: steps, actions }), nowISO());
|
|
322
322
|
const active = getActiveRun(db);
|
|
323
323
|
const lines = [];
|
|
324
324
|
lines.push(`# astro_workflow_proceed`);
|