astrocode-workflow 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/index.js +6 -0
  2. package/dist/shared/metrics.d.ts +66 -0
  3. package/dist/shared/metrics.js +112 -0
  4. package/dist/src/agents/commands.d.ts +9 -0
  5. package/dist/src/agents/commands.js +121 -0
  6. package/dist/src/agents/prompts.d.ts +3 -0
  7. package/dist/src/agents/prompts.js +232 -0
  8. package/dist/src/agents/registry.d.ts +6 -0
  9. package/dist/src/agents/registry.js +242 -0
  10. package/dist/src/agents/types.d.ts +14 -0
  11. package/dist/src/agents/types.js +8 -0
  12. package/dist/src/astro/workflow-runner.d.ts +11 -0
  13. package/dist/src/astro/workflow-runner.js +14 -0
  14. package/dist/src/config/config-handler.d.ts +4 -0
  15. package/dist/src/config/config-handler.js +46 -0
  16. package/dist/src/config/defaults.d.ts +3 -0
  17. package/dist/src/config/defaults.js +3 -0
  18. package/dist/src/config/loader.d.ts +11 -0
  19. package/dist/src/config/loader.js +82 -0
  20. package/dist/src/config/schema.d.ts +195 -0
  21. package/dist/src/config/schema.js +224 -0
  22. package/dist/src/hooks/continuation-enforcer.d.ts +34 -0
  23. package/dist/src/hooks/continuation-enforcer.js +190 -0
  24. package/dist/src/hooks/inject-provider.d.ts +27 -0
  25. package/dist/src/hooks/inject-provider.js +189 -0
  26. package/dist/src/hooks/tool-output-truncator.d.ts +25 -0
  27. package/dist/src/hooks/tool-output-truncator.js +57 -0
  28. package/dist/src/index.d.ts +3 -0
  29. package/dist/src/index.js +307 -0
  30. package/dist/src/shared/deep-merge.d.ts +8 -0
  31. package/dist/src/shared/deep-merge.js +25 -0
  32. package/dist/src/shared/hash.d.ts +1 -0
  33. package/dist/src/shared/hash.js +4 -0
  34. package/dist/src/shared/log.d.ts +7 -0
  35. package/dist/src/shared/log.js +24 -0
  36. package/dist/src/shared/metrics.d.ts +66 -0
  37. package/dist/src/shared/metrics.js +112 -0
  38. package/dist/src/shared/model-tuning.d.ts +9 -0
  39. package/dist/src/shared/model-tuning.js +28 -0
  40. package/dist/src/shared/paths.d.ts +19 -0
  41. package/dist/src/shared/paths.js +64 -0
  42. package/dist/src/shared/text.d.ts +4 -0
  43. package/dist/src/shared/text.js +19 -0
  44. package/dist/src/shared/time.d.ts +1 -0
  45. package/dist/src/shared/time.js +3 -0
  46. package/dist/src/state/adapters/index.d.ts +41 -0
  47. package/dist/src/state/adapters/index.js +115 -0
  48. package/dist/src/state/db.d.ts +16 -0
  49. package/dist/src/state/db.js +225 -0
  50. package/dist/src/state/ids.d.ts +8 -0
  51. package/dist/src/state/ids.js +25 -0
  52. package/dist/src/state/repo-lock.d.ts +67 -0
  53. package/dist/src/state/repo-lock.js +580 -0
  54. package/dist/src/state/schema.d.ts +2 -0
  55. package/dist/src/state/schema.js +258 -0
  56. package/dist/src/state/types.d.ts +71 -0
  57. package/dist/src/state/types.js +1 -0
  58. package/dist/src/state/workflow-repo-lock.d.ts +23 -0
  59. package/dist/src/state/workflow-repo-lock.js +83 -0
  60. package/dist/src/tools/artifacts.d.ts +18 -0
  61. package/dist/src/tools/artifacts.js +71 -0
  62. package/dist/src/tools/health.d.ts +8 -0
  63. package/dist/src/tools/health.js +88 -0
  64. package/dist/src/tools/index.d.ts +20 -0
  65. package/dist/src/tools/index.js +94 -0
  66. package/dist/src/tools/init.d.ts +17 -0
  67. package/dist/src/tools/init.js +96 -0
  68. package/dist/src/tools/injects.d.ts +53 -0
  69. package/dist/src/tools/injects.js +325 -0
  70. package/dist/src/tools/lock.d.ts +4 -0
  71. package/dist/src/tools/lock.js +78 -0
  72. package/dist/src/tools/metrics.d.ts +7 -0
  73. package/dist/src/tools/metrics.js +61 -0
  74. package/dist/src/tools/repair.d.ts +8 -0
  75. package/dist/src/tools/repair.js +26 -0
  76. package/dist/src/tools/reset.d.ts +8 -0
  77. package/dist/src/tools/reset.js +92 -0
  78. package/dist/src/tools/run.d.ts +13 -0
  79. package/dist/src/tools/run.js +54 -0
  80. package/dist/src/tools/spec.d.ts +12 -0
  81. package/dist/src/tools/spec.js +44 -0
  82. package/dist/src/tools/stage.d.ts +23 -0
  83. package/dist/src/tools/stage.js +371 -0
  84. package/dist/src/tools/status.d.ts +8 -0
  85. package/dist/src/tools/status.js +125 -0
  86. package/dist/src/tools/story.d.ts +23 -0
  87. package/dist/src/tools/story.js +85 -0
  88. package/dist/src/tools/workflow.d.ts +13 -0
  89. package/dist/src/tools/workflow.js +345 -0
  90. package/dist/src/ui/inject.d.ts +12 -0
  91. package/dist/src/ui/inject.js +107 -0
  92. package/dist/src/ui/toasts.d.ts +13 -0
  93. package/dist/src/ui/toasts.js +39 -0
  94. package/dist/src/workflow/artifacts.d.ts +24 -0
  95. package/dist/src/workflow/artifacts.js +45 -0
  96. package/dist/src/workflow/baton.d.ts +72 -0
  97. package/dist/src/workflow/baton.js +166 -0
  98. package/dist/src/workflow/context.d.ts +20 -0
  99. package/dist/src/workflow/context.js +113 -0
  100. package/dist/src/workflow/directives.d.ts +39 -0
  101. package/dist/src/workflow/directives.js +137 -0
  102. package/dist/src/workflow/repair.d.ts +8 -0
  103. package/dist/src/workflow/repair.js +99 -0
  104. package/dist/src/workflow/state-machine.d.ts +86 -0
  105. package/dist/src/workflow/state-machine.js +216 -0
  106. package/dist/src/workflow/story-helpers.d.ts +9 -0
  107. package/dist/src/workflow/story-helpers.js +13 -0
  108. package/dist/state/db.d.ts +1 -0
  109. package/dist/state/db.js +9 -0
  110. package/dist/state/repo-lock.d.ts +3 -0
  111. package/dist/state/repo-lock.js +29 -0
  112. package/dist/test/integration/db-transactions.test.d.ts +1 -0
  113. package/dist/test/integration/db-transactions.test.js +126 -0
  114. package/dist/test/integration/injection-metrics.test.d.ts +1 -0
  115. package/dist/test/integration/injection-metrics.test.js +129 -0
  116. package/dist/tools/health.d.ts +8 -0
  117. package/dist/tools/health.js +119 -0
  118. package/dist/tools/index.js +9 -0
  119. package/dist/tools/metrics.d.ts +7 -0
  120. package/dist/tools/metrics.js +61 -0
  121. package/dist/tools/reset.d.ts +8 -0
  122. package/dist/tools/reset.js +92 -0
  123. package/dist/tools/workflow.js +178 -168
  124. package/dist/ui/inject.js +21 -9
  125. package/package.json +6 -4
  126. package/src/astro/workflow-runner.ts +16 -0
  127. package/src/config/schema.ts +1 -0
  128. package/src/hooks/inject-provider.ts +94 -14
  129. package/src/index.ts +7 -0
  130. package/src/shared/metrics.ts +148 -0
  131. package/src/state/db.ts +10 -1
  132. package/src/state/schema.ts +8 -1
  133. package/src/tools/health.ts +99 -0
  134. package/src/tools/index.ts +12 -3
  135. package/src/tools/init.ts +7 -6
  136. package/src/tools/metrics.ts +71 -0
  137. package/src/tools/repair.ts +8 -4
  138. package/src/tools/reset.ts +100 -0
  139. package/src/tools/stage.ts +1 -0
  140. package/src/tools/status.ts +2 -1
  141. package/src/tools/story.ts +1 -0
  142. package/src/tools/workflow.ts +2 -0
  143. package/src/ui/inject.ts +21 -9
  144. package/src/workflow/repair.ts +2 -2
@@ -0,0 +1,94 @@
1
+ import { createAstroInitTool } from "./init";
2
+ import { createAstroStatusTool } from "./status";
3
+ import { createAstroStoryQueueTool, createAstroStoryApproveTool, createAstroStoryBoardTool, createAstroStorySetStateTool } from "./story";
4
+ import { createAstroSpecGetTool, createAstroSpecSetTool } from "./spec";
5
+ import { createAstroRunGetTool, createAstroRunAbortTool } from "./run";
6
+ import { createAstroWorkflowProceedTool } from "./workflow";
7
+ import { createAstroStageStartTool, createAstroStageCompleteTool, createAstroStageFailTool, createAstroStageResetTool } from "./stage";
8
+ import { createAstroArtifactPutTool, createAstroArtifactListTool, createAstroArtifactGetTool } from "./artifacts";
9
+ import { createAstroInjectPutTool, createAstroInjectListTool, createAstroInjectSearchTool, createAstroInjectGetTool, createAstroInjectEligibleTool, createAstroInjectDebugDueTool } from "./injects";
10
+ import { createAstroRepairTool } from "./repair";
11
+ import { createAstroHealthTool } from "./health";
12
+ import { createAstroResetTool } from "./reset";
13
+ import { createAstroMetricsTool } from "./metrics";
14
+ export function createAstroTools(opts) {
15
+ const { ctx, config, agents, runtime } = opts;
16
+ const { db } = runtime;
17
+ const hasDatabase = db !== null; // Source of truth: DB availability
18
+ const tools = {};
19
+ // Always available tools (work without database - guaranteed DB-independent)
20
+ tools.astro_status = createAstroStatusTool({ ctx, config });
21
+ tools.astro_spec_get = createAstroSpecGetTool({ ctx, config });
22
+ tools.astro_health = createAstroHealthTool({ ctx, config, db });
23
+ tools.astro_reset = createAstroResetTool({ ctx, config, db });
24
+ tools.astro_metrics = createAstroMetricsTool({ ctx, config });
25
+ // Recovery tool - available even in limited mode to allow DB initialization
26
+ tools.astro_init = createAstroInitTool({ ctx, config, runtime });
27
+ // Database-dependent tools
28
+ if (hasDatabase) {
29
+ // Ensure agents are available for workflow tools that require them
30
+ if (!agents) {
31
+ throw new Error("astro_workflow_proceed requires agents to be provided in normal mode.");
32
+ }
33
+ tools.astro_story_queue = createAstroStoryQueueTool({ ctx, config, db });
34
+ tools.astro_story_approve = createAstroStoryApproveTool({ ctx, config, db });
35
+ tools.astro_story_board = createAstroStoryBoardTool({ ctx, config, db });
36
+ tools.astro_story_set_state = createAstroStorySetStateTool({ ctx, config, db });
37
+ tools.astro_spec_set = createAstroSpecSetTool({ ctx, config, db });
38
+ tools.astro_run_get = createAstroRunGetTool({ ctx, config, db });
39
+ tools.astro_run_abort = createAstroRunAbortTool({ ctx, config, db });
40
+ tools.astro_workflow_proceed = createAstroWorkflowProceedTool({ ctx, config, db, agents });
41
+ tools.astro_stage_start = createAstroStageStartTool({ ctx, config, db });
42
+ tools.astro_stage_complete = createAstroStageCompleteTool({ ctx, config, db });
43
+ tools.astro_stage_fail = createAstroStageFailTool({ ctx, config, db });
44
+ tools.astro_stage_reset = createAstroStageResetTool({ ctx, config, db });
45
+ tools.astro_artifact_put = createAstroArtifactPutTool({ ctx, config, db });
46
+ tools.astro_artifact_list = createAstroArtifactListTool({ ctx, config, db });
47
+ tools.astro_artifact_get = createAstroArtifactGetTool({ ctx, config, db });
48
+ tools.astro_inject_put = createAstroInjectPutTool({ ctx, config, db });
49
+ tools.astro_inject_list = createAstroInjectListTool({ ctx, config, db });
50
+ tools.astro_inject_search = createAstroInjectSearchTool({ ctx, config, db });
51
+ tools.astro_inject_get = createAstroInjectGetTool({ ctx, config, db });
52
+ tools.astro_inject_eligible = createAstroInjectEligibleTool({ ctx, config, db });
53
+ tools.astro_inject_debug_due = createAstroInjectDebugDueTool({ ctx, config, db });
54
+ tools.astro_repair = createAstroRepairTool({ ctx, config, db });
55
+ }
56
+ // Create aliases for backward compatibility
57
+ const aliases = [
58
+ ["_astro_init", "astro_init"],
59
+ ["_astro_status", "astro_status"],
60
+ ["_astro_story_queue", "astro_story_queue"],
61
+ ["_astro_story_approve", "astro_story_approve"],
62
+ ["_astro_story_board", "astro_story_board"],
63
+ ["_astro_story_set_state", "astro_story_set_state"],
64
+ ["_astro_spec_get", "astro_spec_get"],
65
+ ["_astro_spec_set", "astro_spec_set"],
66
+ ["_astro_run_get", "astro_run_get"],
67
+ ["_astro_run_abort", "astro_run_abort"],
68
+ ["_astro_workflow_proceed", "astro_workflow_proceed"],
69
+ ["_astro_stage_start", "astro_stage_start"],
70
+ ["_astro_stage_complete", "astro_stage_complete"],
71
+ ["_astro_stage_fail", "astro_stage_fail"],
72
+ ["_astro_stage_reset", "astro_stage_reset"],
73
+ ["_astro_artifact_put", "astro_artifact_put"],
74
+ ["_astro_artifact_list", "astro_artifact_list"],
75
+ ["_astro_artifact_get", "astro_artifact_get"],
76
+ ["_astro_inject_put", "astro_inject_put"],
77
+ ["_astro_inject_list", "astro_inject_list"],
78
+ ["_astro_inject_search", "astro_inject_search"],
79
+ ["_astro_inject_get", "astro_inject_get"],
80
+ ["_astro_inject_eligible", "astro_inject_eligible"],
81
+ ["_astro_inject_debug_due", "astro_inject_debug_due"],
82
+ ["_astro_repair", "astro_repair"],
83
+ ["_astro_health", "astro_health"],
84
+ ["_astro_reset", "astro_reset"],
85
+ ["_astro_metrics", "astro_metrics"],
86
+ ];
87
+ // Only add aliases for tools that exist
88
+ for (const [alias, target] of aliases) {
89
+ if (tools[target]) {
90
+ tools[alias] = tools[target];
91
+ }
92
+ }
93
+ return tools;
94
+ }
@@ -0,0 +1,17 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
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
+ };
12
+ export declare function createAstroInitTool(opts: {
13
+ ctx: any;
14
+ config: AstrocodeConfig;
15
+ runtime: RuntimeState;
16
+ }): ToolDefinition;
17
+ export {};
@@ -0,0 +1,96 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { tool } from "@opencode-ai/plugin/tool";
4
+ import { ensureSchema, openSqlite, configurePragmas } from "../state/db";
5
+ import { getAstroPaths, ensureAstroDirs } from "../shared/paths";
6
+ import { nowISO } from "../shared/time";
7
+ import { sha256Hex } from "../shared/hash";
8
+ export function createAstroInitTool(opts) {
9
+ const { ctx, config, runtime } = opts;
10
+ return tool({
11
+ description: "Initialize Astrocode vNext in this repo: create .astro directories, ensure SQLite schema, and create a placeholder spec if missing. Idempotent.",
12
+ args: {
13
+ ensure_spec: tool.schema.boolean().default(true),
14
+ spec_placeholder: tool.schema.string().default("# Project Spec\n\n(Write the spec here.)\n"),
15
+ },
16
+ execute: async ({ ensure_spec, spec_placeholder }) => {
17
+ const repoRoot = ctx.directory;
18
+ const paths = getAstroPaths(repoRoot, config.db.path);
19
+ ensureAstroDirs(paths);
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);
50
+ const now = nowISO();
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 { }
92
+ }
93
+ }
94
+ },
95
+ });
96
+ }
@@ -0,0 +1,53 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ import type { AstrocodeConfig } from "../config/schema";
3
+ import type { SqliteDb } from "../state/db";
4
+ export declare function createAstroInjectPutTool(opts: {
5
+ ctx: any;
6
+ config: AstrocodeConfig;
7
+ db: SqliteDb;
8
+ }): ToolDefinition;
9
+ export declare function createAstroInjectListTool(opts: {
10
+ ctx: any;
11
+ config: AstrocodeConfig;
12
+ db: SqliteDb;
13
+ }): ToolDefinition;
14
+ export declare function createAstroInjectGetTool(opts: {
15
+ ctx: any;
16
+ config: AstrocodeConfig;
17
+ db: SqliteDb;
18
+ }): ToolDefinition;
19
+ export declare function createAstroInjectSearchTool(opts: {
20
+ ctx: any;
21
+ config: AstrocodeConfig;
22
+ db: SqliteDb;
23
+ }): ToolDefinition;
24
+ export type InjectRow = {
25
+ inject_id: string;
26
+ type: string;
27
+ title: string;
28
+ body_md: string;
29
+ tags_json: string;
30
+ scope: string;
31
+ source: string;
32
+ priority: number;
33
+ expires_at: string | null;
34
+ sha256: string;
35
+ created_at: string;
36
+ updated_at: string;
37
+ };
38
+ export declare function selectEligibleInjects(db: SqliteDb, opts: {
39
+ nowIso: string;
40
+ scopeAllowlist: string[];
41
+ typeAllowlist: string[];
42
+ limit?: number;
43
+ }): InjectRow[];
44
+ export declare function createAstroInjectEligibleTool(opts: {
45
+ ctx: any;
46
+ config: AstrocodeConfig;
47
+ db: SqliteDb;
48
+ }): ToolDefinition;
49
+ export declare function createAstroInjectDebugDueTool(opts: {
50
+ ctx: any;
51
+ config: AstrocodeConfig;
52
+ db: SqliteDb;
53
+ }): ToolDefinition;
@@ -0,0 +1,325 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { withTx } from "../state/db";
3
+ import { nowISO } from "../shared/time";
4
+ import { sha256Hex } from "../shared/hash";
5
+ const VALID_INJECT_TYPES = ["note", "policy", "reminder", "debug"];
6
+ function validateInjectType(type) {
7
+ if (!VALID_INJECT_TYPES.includes(type)) {
8
+ throw new Error(`Invalid inject type "${type}". Must be one of: ${VALID_INJECT_TYPES.join(", ")}`);
9
+ }
10
+ return type;
11
+ }
12
+ function validateTimestamp(timestamp) {
13
+ if (!timestamp)
14
+ return null;
15
+ // Strict ISO 8601 UTC with Z suffix, sortable as string.
16
+ const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
17
+ if (!isoRegex.test(timestamp)) {
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
+ }
20
+ const parsed = Date.parse(timestamp);
21
+ if (!Number.isFinite(parsed)) {
22
+ throw new Error(`Invalid timestamp: "${timestamp}" does not represent a valid date`);
23
+ }
24
+ return timestamp;
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
+ }
40
+ function newInjectId() {
41
+ return `inj_${Date.now()}_${Math.random().toString(16).slice(2)}`;
42
+ }
43
+ export function createAstroInjectPutTool(opts) {
44
+ const { db } = opts;
45
+ return tool({
46
+ description: "Create/update an inject (note/policy) stored in the DB. Useful for persistent rules.",
47
+ args: {
48
+ inject_id: tool.schema.string().optional(),
49
+ type: tool.schema.string().default("note"),
50
+ title: tool.schema.string().min(1),
51
+ body_md: tool.schema.string().min(1),
52
+ tags_json: tool.schema.string().default("[]"),
53
+ scope: tool.schema.string().default("repo"),
54
+ source: tool.schema.string().default("user"),
55
+ priority: tool.schema.number().int().default(50),
56
+ expires_at: tool.schema.string().nullable().optional(),
57
+ },
58
+ execute: async ({ inject_id, type, title, body_md, tags_json, scope, source, priority, expires_at }) => {
59
+ const id = inject_id ?? newInjectId();
60
+ const now = nowISO();
61
+ const sha = sha256Hex(body_md);
62
+ const validatedType = validateInjectType(type);
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
+ }
72
+ return withTx(db, () => {
73
+ const existing = db.prepare("SELECT inject_id FROM injects WHERE inject_id=?").get(id);
74
+ if (existing) {
75
+ db.prepare(`
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
+ )
90
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
91
+ ON CONFLICT(inject_id) DO UPDATE SET
92
+ type=excluded.type,
93
+ title=excluded.title,
94
+ body_md=excluded.body_md,
95
+ tags_json=excluded.tags_json,
96
+ scope=excluded.scope,
97
+ source=excluded.source,
98
+ priority=excluded.priority,
99
+ expires_at=excluded.expires_at,
100
+ sha256=excluded.sha256,
101
+ updated_at=excluded.updated_at
102
+ `).run(id, validatedType, title, body_md, tags_json, scope, source, priority, validatedExpiresAt, sha, now, now);
103
+ return `✅ Updated inject ${id}: ${title}`;
104
+ }
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);
106
+ return `✅ Created inject ${id}: ${title}`;
107
+ });
108
+ },
109
+ });
110
+ }
111
+ export function createAstroInjectListTool(opts) {
112
+ const { db } = opts;
113
+ return tool({
114
+ description: "List injects (optionally filtered by scope/type).",
115
+ args: {
116
+ scope: tool.schema.string().optional(),
117
+ type: tool.schema.string().optional(),
118
+ limit: tool.schema.number().int().positive().default(50),
119
+ },
120
+ execute: async ({ scope, type, limit }) => {
121
+ const where = [];
122
+ const params = [];
123
+ if (scope) {
124
+ where.push("scope = ?");
125
+ params.push(scope);
126
+ }
127
+ if (type) {
128
+ // Keep list tool permissive (debugging), but still prevents obvious garbage if used.
129
+ validateInjectType(type);
130
+ where.push("type = ?");
131
+ params.push(type);
132
+ }
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
+ `;
140
+ const rows = db.prepare(sql).all(...params, limit);
141
+ return JSON.stringify(rows, null, 2);
142
+ },
143
+ });
144
+ }
145
+ export function createAstroInjectGetTool(opts) {
146
+ const { db } = opts;
147
+ return tool({
148
+ description: "Get an inject by id (full body).",
149
+ args: {
150
+ inject_id: tool.schema.string().min(1),
151
+ },
152
+ execute: async ({ inject_id }) => {
153
+ const row = db.prepare("SELECT * FROM injects WHERE inject_id=?").get(inject_id);
154
+ if (!row)
155
+ throw new Error(`Inject not found: ${inject_id}`);
156
+ return JSON.stringify(row, null, 2);
157
+ },
158
+ });
159
+ }
160
+ export function createAstroInjectSearchTool(opts) {
161
+ const { db } = opts;
162
+ return tool({
163
+ description: "Search injects by query substring over title/body/tags. Returns matches ordered by priority/recency.",
164
+ args: {
165
+ q: tool.schema.string().min(1),
166
+ scope: tool.schema.string().optional(),
167
+ limit: tool.schema.number().int().positive().default(20),
168
+ },
169
+ execute: async ({ q, scope, limit }) => {
170
+ const like = `%${q}%`;
171
+ const where = ["(title LIKE ? OR body_md LIKE ? OR tags_json LIKE ?)"];
172
+ const params = [like, like, like];
173
+ if (scope) {
174
+ where.push("scope = ?");
175
+ params.push(scope);
176
+ }
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
+ `;
184
+ const rows = db.prepare(sql).all(...params, limit);
185
+ return JSON.stringify(rows, null, 2);
186
+ },
187
+ });
188
+ }
189
+ export function selectEligibleInjects(db, opts) {
190
+ const { nowIso, scopeAllowlist, typeAllowlist, limit = 50 } = opts;
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).
198
+ const scopeQs = scopeAllowlist.map(() => "?").join(", ");
199
+ const typeQs = typeAllowlist.map(() => "?").join(", ");
200
+ const sql = `
201
+ SELECT *
202
+ FROM injects
203
+ WHERE (expires_at IS NULL OR expires_at > ?)
204
+ AND scope IN (${scopeQs})
205
+ AND type IN (${typeQs})
206
+ ORDER BY priority DESC, updated_at DESC
207
+ LIMIT ?
208
+ `;
209
+ const params = [nowIso, ...scopeAllowlist, ...typeAllowlist, limit];
210
+ return db.prepare(sql).all(...params);
211
+ }
212
+ export function createAstroInjectEligibleTool(opts) {
213
+ const { db } = opts;
214
+ return tool({
215
+ description: "Debug: show which injects are eligible right now for injection.",
216
+ args: {
217
+ scopes_json: tool.schema.string().default('["repo","global"]'),
218
+ types_json: tool.schema.string().default('["note","policy"]'),
219
+ limit: tool.schema.number().int().positive().default(50),
220
+ },
221
+ execute: async ({ scopes_json, types_json, limit }) => {
222
+ const now = nowISO();
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);
228
+ const rows = selectEligibleInjects(db, {
229
+ nowIso: now,
230
+ scopeAllowlist: scopes,
231
+ typeAllowlist: types,
232
+ limit,
233
+ });
234
+ return JSON.stringify({ now, count: rows.length, rows }, null, 2);
235
+ },
236
+ });
237
+ }
238
+ export function createAstroInjectDebugDueTool(opts) {
239
+ const { db } = opts;
240
+ return tool({
241
+ description: "Debug: show comprehensive injection diagnostics - why injects were selected/skipped.",
242
+ args: {
243
+ scopes_json: tool.schema.string().default('["repo","global"]'),
244
+ types_json: tool.schema.string().default('["note","policy"]'),
245
+ },
246
+ execute: async ({ scopes_json, types_json }) => {
247
+ const now = nowISO();
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);
253
+ const allInjects = db.prepare("SELECT * FROM injects").all();
254
+ let total = allInjects.length;
255
+ let selected = 0;
256
+ let skippedExpired = 0;
257
+ let skippedScope = 0;
258
+ let skippedType = 0;
259
+ let skippedUnparseableExpiresAt = 0;
260
+ const excludedReasons = [];
261
+ const selectedInjects = [];
262
+ for (const inject of allInjects) {
263
+ const reasons = [];
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
+ }
275
+ }
276
+ if (!scopes.includes(inject.scope)) {
277
+ reasons.push("scope");
278
+ skippedScope++;
279
+ }
280
+ if (!types.includes(inject.type)) {
281
+ reasons.push("type");
282
+ skippedType++;
283
+ }
284
+ if (reasons.length > 0) {
285
+ excludedReasons.push({
286
+ inject_id: inject.inject_id,
287
+ title: inject.title,
288
+ reasons,
289
+ scope: inject.scope,
290
+ type: inject.type,
291
+ expires_at: inject.expires_at,
292
+ });
293
+ }
294
+ else {
295
+ selected++;
296
+ selectedInjects.push({
297
+ inject_id: inject.inject_id,
298
+ title: inject.title,
299
+ scope: inject.scope,
300
+ type: inject.type,
301
+ expires_at: inject.expires_at,
302
+ });
303
+ }
304
+ }
305
+ return JSON.stringify({
306
+ now,
307
+ scopes_considered: scopes,
308
+ types_considered: types,
309
+ summary: {
310
+ total_injects: total,
311
+ selected_eligible: selected,
312
+ excluded_total: total - selected,
313
+ skipped_breakdown: {
314
+ expired: skippedExpired,
315
+ expires_at_unparseable: skippedUnparseableExpiresAt,
316
+ scope: skippedScope,
317
+ type: skippedType,
318
+ },
319
+ },
320
+ selected_injects: selectedInjects,
321
+ excluded_injects: excludedReasons,
322
+ }, null, 2);
323
+ },
324
+ });
325
+ }
@@ -0,0 +1,4 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ export declare function createAstroLockStatusTool(opts: {
3
+ ctx: any;
4
+ }): ToolDefinition;
@@ -0,0 +1,78 @@
1
+ import path from "node:path";
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import { getLockStatus, tryRemoveStaleLock } from "../state/repo-lock";
4
+ export function createAstroLockStatusTool(opts) {
5
+ const { ctx } = opts;
6
+ return tool({
7
+ description: "Check Astrocode lock status and attempt repair. Shows diagnostics (PID, age, session) and can remove stale/dead locks.",
8
+ args: {
9
+ attempt_repair: tool.schema.boolean().default(false).describe("If true, attempt to remove stale or dead locks"),
10
+ },
11
+ execute: async ({ attempt_repair }) => {
12
+ const repoRoot = ctx.directory;
13
+ const lockPath = path.join(repoRoot, ".astro", "astro.lock");
14
+ const status = getLockStatus(lockPath);
15
+ if (!status.exists) {
16
+ return "✅ No lock file found. Repository is unlocked.";
17
+ }
18
+ const lines = [];
19
+ lines.push("# Astrocode Lock Status");
20
+ lines.push("");
21
+ lines.push("## Lock Details");
22
+ lines.push(`- **Path**: ${status.path}`);
23
+ lines.push(`- **PID**: ${status.pid} (${status.pidAlive ? '🟢 ALIVE' : '🔴 DEAD'})`);
24
+ lines.push(`- **Age**: ${status.ageMs ? Math.floor(status.ageMs / 1000) : '?'}s`);
25
+ lines.push(`- **Status**: ${status.isStale ? '⚠️ STALE' : '✅ FRESH'}`);
26
+ if (status.sessionId)
27
+ lines.push(`- **Session**: ${status.sessionId}`);
28
+ if (status.owner)
29
+ lines.push(`- **Owner**: ${status.owner}`);
30
+ if (status.instanceId)
31
+ lines.push(`- **Instance**: ${status.instanceId.substring(0, 8)}...`);
32
+ if (status.leaseId)
33
+ lines.push(`- **Lease**: ${status.leaseId.substring(0, 8)}...`);
34
+ if (status.createdAt)
35
+ lines.push(`- **Created**: ${status.createdAt}`);
36
+ if (status.updatedAt)
37
+ lines.push(`- **Updated**: ${status.updatedAt}`);
38
+ if (status.repoRoot)
39
+ lines.push(`- **Repo**: ${status.repoRoot}`);
40
+ lines.push(`- **Version**: ${status.version ?? 'unknown'}`);
41
+ lines.push("");
42
+ if (attempt_repair) {
43
+ lines.push("## Repair Attempt");
44
+ const result = tryRemoveStaleLock(lockPath);
45
+ if (result.removed) {
46
+ lines.push(`✅ **Lock removed**: ${result.reason}`);
47
+ lines.push("");
48
+ lines.push("The repository is now unlocked and ready for use.");
49
+ }
50
+ else {
51
+ lines.push(`⚠️ **Lock NOT removed**: ${result.reason}`);
52
+ lines.push("");
53
+ lines.push("**Recommendations**:");
54
+ lines.push("- If the owning process has crashed, wait 30 seconds for automatic stale detection");
55
+ lines.push("- If the process is still running, wait for it to complete");
56
+ lines.push("- As a last resort, manually stop the process and run this tool again with attempt_repair=true");
57
+ }
58
+ }
59
+ else {
60
+ lines.push("## Recommendations");
61
+ if (!status.pidAlive) {
62
+ lines.push("🔧 **Action Required**: Lock belongs to dead process. Run with `attempt_repair=true` to remove it.");
63
+ }
64
+ else if (status.isStale) {
65
+ lines.push("🔧 **Action Suggested**: Lock is stale (not updated recently). Run with `attempt_repair=true` to remove it.");
66
+ }
67
+ else {
68
+ lines.push("✅ Lock is active and healthy. The owning process is running normally.");
69
+ lines.push("");
70
+ lines.push("If you believe this is incorrect:");
71
+ lines.push("- Wait 30 seconds and check again (automatic stale detection)");
72
+ lines.push("- Run with `attempt_repair=true` only if you're certain the process has crashed");
73
+ }
74
+ }
75
+ return lines.join("\n");
76
+ },
77
+ });
78
+ }