@vantageos/backend-doctor 0.1.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 +88 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +94 -0
- package/dist/contract.d.ts +18 -0
- package/dist/contract.js +29 -0
- package/dist/coverage-cli.d.ts +2 -0
- package/dist/coverage-cli.js +17 -0
- package/dist/coverage.d.ts +32 -0
- package/dist/coverage.js +77 -0
- package/dist/detectors/oracle-axis1.d.ts +86 -0
- package/dist/detectors/oracle-axis1.js +221 -0
- package/dist/detectors/oracle-axis2.d.ts +22 -0
- package/dist/detectors/oracle-axis2.js +149 -0
- package/dist/detectors/predicates.d.ts +19 -0
- package/dist/detectors/predicates.js +334 -0
- package/dist/detectors/shared.d.ts +56 -0
- package/dist/detectors/shared.js +103 -0
- package/dist/detectors/source/_scan.d.ts +43 -0
- package/dist/detectors/source/_scan.js +82 -0
- package/dist/detectors/source/backfill.d.ts +27 -0
- package/dist/detectors/source/backfill.js +211 -0
- package/dist/detectors/source/contract.d.ts +33 -0
- package/dist/detectors/source/contract.js +385 -0
- package/dist/detectors/source/identity.d.ts +47 -0
- package/dist/detectors/source/identity.js +410 -0
- package/dist/detectors/source/jobs.d.ts +10 -0
- package/dist/detectors/source/jobs.js +209 -0
- package/dist/detectors/source/lifecycle.d.ts +6 -0
- package/dist/detectors/source/lifecycle.js +147 -0
- package/dist/detectors/source/readbudget.d.ts +6 -0
- package/dist/detectors/source/readbudget.js +133 -0
- package/dist/detectors/source/refusal.d.ts +6 -0
- package/dist/detectors/source/refusal.js +125 -0
- package/dist/detectors/source/security.d.ts +37 -0
- package/dist/detectors/source/security.js +392 -0
- package/dist/detectors/source/writesafety.d.ts +14 -0
- package/dist/detectors/source/writesafety.js +345 -0
- package/dist/detectors/standard/_doc.d.ts +66 -0
- package/dist/detectors/standard/_doc.js +152 -0
- package/dist/detectors/standard/r41-frontier.d.ts +58 -0
- package/dist/detectors/standard/r41-frontier.js +176 -0
- package/dist/detectors/standard/r46-returns-validator.d.ts +61 -0
- package/dist/detectors/standard/r46-returns-validator.js +474 -0
- package/dist/detectors/standard/standard-doc.d.ts +12 -0
- package/dist/detectors/standard/standard-doc.js +271 -0
- package/dist/detectors/structural.d.ts +14 -0
- package/dist/detectors/structural.js +83 -0
- package/dist/engine-types.d.ts +10 -0
- package/dist/engine-types.js +1 -0
- package/dist/not-checkable.d.ts +22 -0
- package/dist/not-checkable.js +37 -0
- package/dist/oracle.d.ts +33 -0
- package/dist/oracle.js +133 -0
- package/dist/registry.d.ts +11 -0
- package/dist/registry.js +140 -0
- package/dist/report.d.ts +35 -0
- package/dist/report.js +138 -0
- package/dist/severity.d.ts +30 -0
- package/dist/severity.js +121 -0
- package/dist/tally.d.ts +57 -0
- package/dist/tally.js +152 -0
- package/dist/trigger.d.ts +47 -0
- package/dist/trigger.js +143 -0
- package/fixtures/backend-standard-51ec7af1.md +648 -0
- package/fixtures/backend-standard-a9d7877b.md +629 -0
- package/fixtures/mcp-app-standard-a74a042b.md +279 -0
- package/fixtures/mcp-extension-standard-a74a042b.md +259 -0
- package/fixtures/mcp-server-standard-a74a042b.md +434 -0
- package/fixtures/mcp-standard-a74a042b.md +354 -0
- package/fixtures/vp-1200-green/.pin +1 -0
- package/fixtures/vp-1200-green/convex/migrations.ts +350 -0
- package/fixtures/vp-1200-green/convex/schema.ts +1243 -0
- package/fixtures/vp-1200-red/.pin +1 -0
- package/fixtures/vp-1200-red/convex/migrations.ts +239 -0
- package/fixtures/vp-1200-red/convex/schema.ts +1243 -0
- package/fixtures/vp-by-tool-day158.csv +190 -0
- package/fixtures/vp-convex/.github/workflows/npm-publish.yml +163 -0
- package/fixtures/vp-convex/.pin +1 -0
- package/fixtures/vp-convex/bun.lock +783 -0
- package/fixtures/vp-convex/convex/auth.config.ts +8 -0
- package/fixtures/vp-convex/convex/briefingNotes.ts +321 -0
- package/fixtures/vp-convex/convex/convex.config.ts +14 -0
- package/fixtures/vp-convex/convex/credentials.ts +591 -0
- package/fixtures/vp-convex/convex/crons.ts +72 -0
- package/fixtures/vp-convex/convex/dashboard.ts +199 -0
- package/fixtures/vp-convex/convex/gumroadWebhook.ts +815 -0
- package/fixtures/vp-convex/convex/issues.ts +544 -0
- package/fixtures/vp-convex/convex/kb.ts +305 -0
- package/fixtures/vp-convex/convex/kbMutations.ts +192 -0
- package/fixtures/vp-convex/convex/kbShared.ts +56 -0
- package/fixtures/vp-convex/convex/licenses.ts +443 -0
- package/fixtures/vp-convex/convex/memories.ts +406 -0
- package/fixtures/vp-convex/convex/memoriesScoped.ts +220 -0
- package/fixtures/vp-convex/convex/messages.ts +971 -0
- package/fixtures/vp-convex/convex/migrations.ts +239 -0
- package/fixtures/vp-convex/convex/missionTemplates.ts +369 -0
- package/fixtures/vp-convex/convex/oauth.ts +1608 -0
- package/fixtures/vp-convex/convex/oauthMigrations.ts +96 -0
- package/fixtures/vp-convex/convex/okfBundleNode.ts +1031 -0
- package/fixtures/vp-convex/convex/recurringTasks.ts +386 -0
- package/fixtures/vp-convex/convex/schema.ts +1229 -0
- package/fixtures/vp-convex/convex/tasks.ts +2782 -0
- package/fixtures/vp-r31-green/.pin +1 -0
- package/fixtures/vp-r31-green/convex/migrations.ts +350 -0
- package/fixtures/vp-r31-red/.pin +1 -0
- package/fixtures/vp-r31-red/convex/migrations.ts +286 -0
- package/fixtures/vr-by-tool-day159.csv +89 -0
- package/package.json +39 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { internalMutation } from "./_generated/server";
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Migration: backfill startedAt + completedAt on tasks
|
|
5
|
+
// Run: npx convex run migrations:backfillTaskTimes
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export const backfillTaskTimes = internalMutation({
|
|
9
|
+
args: {},
|
|
10
|
+
handler: async (ctx) => {
|
|
11
|
+
const tasks = await ctx.db.query("tasks").collect();
|
|
12
|
+
let startedFixed = 0;
|
|
13
|
+
let completedFixed = 0;
|
|
14
|
+
|
|
15
|
+
for (const task of tasks) {
|
|
16
|
+
const patch: Record<string, any> = {};
|
|
17
|
+
|
|
18
|
+
// Backfill startedAt for in_progress/done/review tasks
|
|
19
|
+
if (
|
|
20
|
+
!task.startedAt &&
|
|
21
|
+
(task.status === "in_progress" || task.status === "done" || task.status === "review")
|
|
22
|
+
) {
|
|
23
|
+
patch.startedAt = task.createdAt;
|
|
24
|
+
startedFixed++;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Backfill completedAt for done tasks
|
|
28
|
+
if (!task.completedAt && task.status === "done") {
|
|
29
|
+
patch.completedAt = task.updatedAt;
|
|
30
|
+
completedFixed++;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Calculate actualMinutes if both timestamps exist
|
|
34
|
+
const startedAt = patch.startedAt ?? task.startedAt;
|
|
35
|
+
const completedAt = patch.completedAt ?? task.completedAt;
|
|
36
|
+
if (startedAt && completedAt && !task.actualMinutes) {
|
|
37
|
+
patch.actualMinutes = Math.round((completedAt - startedAt) / 60_000);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Object.keys(patch).length > 0) {
|
|
41
|
+
await ctx.db.patch(task._id, patch);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(`Backfilled: ${startedFixed} startedAt, ${completedFixed} completedAt`);
|
|
46
|
+
return { startedFixed, completedFixed, total: tasks.length };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Cleanup: delete test briefing notes (MCP Test Briefing entries)
|
|
52
|
+
// Run: npx convex run migrations:deleteTestBriefings
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export const deleteTestBriefings = internalMutation({
|
|
56
|
+
args: {},
|
|
57
|
+
handler: async (ctx) => {
|
|
58
|
+
const notes = await ctx.db.query("briefingNotes").collect();
|
|
59
|
+
let deleted = 0;
|
|
60
|
+
|
|
61
|
+
for (const note of notes) {
|
|
62
|
+
if (note.title === "MCP Test Briefing") {
|
|
63
|
+
await ctx.db.delete(note._id);
|
|
64
|
+
deleted++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`Deleted ${deleted} test briefing notes`);
|
|
69
|
+
return { deleted };
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// Cleanup: delete test tasks created by MCP tester
|
|
75
|
+
// Run: npx convex run migrations:deleteTestTasks
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export const deleteTestTasks = internalMutation({
|
|
79
|
+
args: {},
|
|
80
|
+
handler: async (ctx) => {
|
|
81
|
+
const tasks = await ctx.db.query("tasks").collect();
|
|
82
|
+
let deleted = 0;
|
|
83
|
+
|
|
84
|
+
for (const task of tasks) {
|
|
85
|
+
if (
|
|
86
|
+
task.title === "Test task from MCP tester" ||
|
|
87
|
+
task.title === "Mission-linked task from MCP tester"
|
|
88
|
+
) {
|
|
89
|
+
await ctx.db.delete(task._id);
|
|
90
|
+
deleted++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`Deleted ${deleted} test tasks`);
|
|
95
|
+
return { deleted };
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
// Cleanup: delete test missions created by MCP tester
|
|
101
|
+
// Run: npx convex run migrations:deleteTestMissions
|
|
102
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export const deleteTestMissions = internalMutation({
|
|
105
|
+
args: {},
|
|
106
|
+
handler: async (ctx) => {
|
|
107
|
+
const missions = await ctx.db.query("missions").collect();
|
|
108
|
+
let deleted = 0;
|
|
109
|
+
|
|
110
|
+
for (const mission of missions) {
|
|
111
|
+
if (
|
|
112
|
+
mission.name === "Test mission from MCP tester" ||
|
|
113
|
+
mission.name === "Updated test mission"
|
|
114
|
+
) {
|
|
115
|
+
await ctx.db.delete(mission._id);
|
|
116
|
+
deleted++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log(`Deleted ${deleted} test missions`);
|
|
121
|
+
return { deleted };
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
126
|
+
// Cleanup: delete test memories from test/mcp-tester namespace
|
|
127
|
+
// Run: npx convex run migrations:deleteTestMemories
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
export const deleteTestMemories = internalMutation({
|
|
131
|
+
args: {},
|
|
132
|
+
handler: async (ctx) => {
|
|
133
|
+
const memories = await ctx.db
|
|
134
|
+
.query("memories")
|
|
135
|
+
.withIndex("by_namespace", (q) => q.eq("namespace", "test/mcp-tester"))
|
|
136
|
+
.collect();
|
|
137
|
+
let deleted = 0;
|
|
138
|
+
|
|
139
|
+
for (const mem of memories) {
|
|
140
|
+
await ctx.db.delete(mem._id);
|
|
141
|
+
deleted++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`Deleted ${deleted} test memories from test/mcp-tester`);
|
|
145
|
+
return { deleted };
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
// Cleanup: delete test messages from MCP tester
|
|
151
|
+
// Run: npx convex run migrations:deleteTestMessages
|
|
152
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export const deleteTestMessages = internalMutation({
|
|
155
|
+
args: {},
|
|
156
|
+
handler: async (ctx) => {
|
|
157
|
+
const messages = await ctx.db.query("messages").collect();
|
|
158
|
+
let deleted = 0;
|
|
159
|
+
|
|
160
|
+
for (const msg of messages) {
|
|
161
|
+
if (msg.content === "Test message from MCP tester") {
|
|
162
|
+
// Delete receipts first
|
|
163
|
+
const receipts = await ctx.db
|
|
164
|
+
.query("messageReceipts")
|
|
165
|
+
.withIndex("by_message", (q) => q.eq("messageId", msg._id))
|
|
166
|
+
.collect();
|
|
167
|
+
for (const receipt of receipts) {
|
|
168
|
+
await ctx.db.delete(receipt._id);
|
|
169
|
+
}
|
|
170
|
+
await ctx.db.delete(msg._id);
|
|
171
|
+
deleted++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(`Deleted ${deleted} test messages and their receipts`);
|
|
176
|
+
return { deleted };
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
181
|
+
// Migration: rename memory namespace "orchestrator/pi" → "orchestrator/sigma"
|
|
182
|
+
// Run: npx convex run migrations:migrateMemoriesNamespace
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
export const migrateMemoriesNamespace = internalMutation({
|
|
186
|
+
args: {},
|
|
187
|
+
handler: async (ctx) => {
|
|
188
|
+
const memories = await ctx.db
|
|
189
|
+
.query("memories")
|
|
190
|
+
.withIndex("by_namespace", (q) => q.eq("namespace", "orchestrator/pi"))
|
|
191
|
+
.collect();
|
|
192
|
+
let migrated = 0;
|
|
193
|
+
|
|
194
|
+
for (const mem of memories) {
|
|
195
|
+
await ctx.db.patch(mem._id, { namespace: "orchestrator/sigma" });
|
|
196
|
+
migrated++;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log(`Migrated ${migrated} memories from orchestrator/pi to orchestrator/sigma`);
|
|
200
|
+
return { migrated };
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
// Migration: change diary orchestrator "pi" → "sigma" for VPS entries
|
|
206
|
+
// Run: npx convex run migrations:migrateDiaryOrchestrator
|
|
207
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
export const migrateDiaryOrchestrator = internalMutation({
|
|
210
|
+
args: {},
|
|
211
|
+
handler: async (ctx) => {
|
|
212
|
+
const entries = await ctx.db
|
|
213
|
+
.query("diary")
|
|
214
|
+
.withIndex("by_orchestrator_date", (q) => q.eq("orchestrator", "pi"))
|
|
215
|
+
.collect();
|
|
216
|
+
let migrated = 0;
|
|
217
|
+
let skipped = 0;
|
|
218
|
+
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
const content = entry.content.toLowerCase();
|
|
221
|
+
const isVpsRelated =
|
|
222
|
+
content.includes("pi-vps") ||
|
|
223
|
+
content.includes("sigma") ||
|
|
224
|
+
content.includes("vps");
|
|
225
|
+
|
|
226
|
+
if (isVpsRelated) {
|
|
227
|
+
await ctx.db.patch(entry._id, { orchestrator: "sigma" });
|
|
228
|
+
migrated++;
|
|
229
|
+
} else {
|
|
230
|
+
skipped++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log(
|
|
235
|
+
`Diary migration: ${migrated} migrated to sigma, ${skipped} kept as pi`
|
|
236
|
+
);
|
|
237
|
+
return { migrated, skipped };
|
|
238
|
+
},
|
|
239
|
+
});
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { internalMutation, mutation, query } from "./_generated/server";
|
|
3
|
+
import { creatorValidator } from "./schema";
|
|
4
|
+
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
// Shared validator for a single template step
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const stepValidator = v.object({
|
|
10
|
+
title: v.string(),
|
|
11
|
+
description: v.string(),
|
|
12
|
+
tags: v.optional(v.array(v.string())),
|
|
13
|
+
assignedTo: v.optional(v.string()),
|
|
14
|
+
assignedToInstance: v.optional(v.string()),
|
|
15
|
+
dependsOn: v.optional(v.array(v.number())),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const templateDocValidator = v.object({
|
|
19
|
+
_id: v.id("missionTemplates"),
|
|
20
|
+
_creationTime: v.number(),
|
|
21
|
+
name: v.string(),
|
|
22
|
+
description: v.optional(v.string()),
|
|
23
|
+
brief: v.optional(v.string()),
|
|
24
|
+
steps: v.array(stepValidator),
|
|
25
|
+
isDefault: v.boolean(),
|
|
26
|
+
createdBy: creatorValidator,
|
|
27
|
+
createdAt: v.number(),
|
|
28
|
+
updatedAt: v.number(),
|
|
29
|
+
deletedAt: v.optional(v.number()),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// getByName — fetch a template by its unique name
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export const getByName = query({
|
|
37
|
+
args: { name: v.string() },
|
|
38
|
+
returns: v.union(templateDocValidator, v.null()),
|
|
39
|
+
handler: async (ctx, args) => {
|
|
40
|
+
const template = await ctx.db
|
|
41
|
+
.query("missionTemplates")
|
|
42
|
+
.withIndex("by_name", (q) => q.eq("name", args.name))
|
|
43
|
+
.unique();
|
|
44
|
+
// Soft-deleted templates are invisible to reads, like a superseded memory.
|
|
45
|
+
if (template !== null && template.deletedAt !== undefined) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return template;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// listNames — the live source of truth for "which template names exist".
|
|
54
|
+
//
|
|
55
|
+
// Root cause this closes (Day 159): the mission-template guard hook
|
|
56
|
+
// (enforce-mission-template.py) can only be as good as the source it probes.
|
|
57
|
+
// Without a query exposing the LIVE table, the only sources available were a
|
|
58
|
+
// hardcoded whitelist or a cached/static manifest file — both drift the moment
|
|
59
|
+
// a new template is upserted (e.g. orchestrator-config-update-v1), refusing a
|
|
60
|
+
// template that genuinely exists. This query IS the live source: wire
|
|
61
|
+
// MISSION_TEMPLATE_PROBE_CMD at each station to a command that calls it, so
|
|
62
|
+
// the guard always interrogates the real table instead of a stale copy.
|
|
63
|
+
//
|
|
64
|
+
// Mirrors getByName's soft-delete convention: a template with deletedAt set is
|
|
65
|
+
// invisible, exactly like a superseded memory.
|
|
66
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export const listNames = query({
|
|
69
|
+
args: {},
|
|
70
|
+
returns: v.array(v.string()),
|
|
71
|
+
handler: async (ctx) => {
|
|
72
|
+
const templates = await ctx.db.query("missionTemplates").take(1000);
|
|
73
|
+
return templates
|
|
74
|
+
.filter((t) => t.deletedAt === undefined)
|
|
75
|
+
.map((t) => t.name);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
// upsert — create or update a template by name
|
|
81
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
export const upsert = mutation({
|
|
84
|
+
args: {
|
|
85
|
+
name: v.string(),
|
|
86
|
+
description: v.optional(v.string()),
|
|
87
|
+
brief: v.optional(v.string()),
|
|
88
|
+
steps: v.array(stepValidator),
|
|
89
|
+
isDefault: v.optional(v.boolean()),
|
|
90
|
+
createdBy: creatorValidator,
|
|
91
|
+
},
|
|
92
|
+
returns: v.id("missionTemplates"),
|
|
93
|
+
handler: async (ctx, args) => {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const existing = await ctx.db
|
|
96
|
+
.query("missionTemplates")
|
|
97
|
+
.withIndex("by_name", (q) => q.eq("name", args.name))
|
|
98
|
+
.unique();
|
|
99
|
+
|
|
100
|
+
if (existing !== null) {
|
|
101
|
+
await ctx.db.patch(existing._id, {
|
|
102
|
+
description: args.description,
|
|
103
|
+
brief: args.brief ?? existing.brief,
|
|
104
|
+
steps: args.steps,
|
|
105
|
+
isDefault: args.isDefault ?? existing.isDefault,
|
|
106
|
+
updatedAt: now,
|
|
107
|
+
});
|
|
108
|
+
return existing._id;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return await ctx.db.insert("missionTemplates", {
|
|
112
|
+
name: args.name,
|
|
113
|
+
description: args.description,
|
|
114
|
+
brief: args.brief,
|
|
115
|
+
steps: args.steps,
|
|
116
|
+
isDefault: args.isDefault ?? false,
|
|
117
|
+
createdBy: args.createdBy,
|
|
118
|
+
createdAt: now,
|
|
119
|
+
updatedAt: now,
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
+
// softDelete — marks a mission template as no longer active (audit-preserving)
|
|
126
|
+
// Mirrors memories.softDeleteMemory's motif: PATCH a flag rather than hard
|
|
127
|
+
// `ctx.db.delete`, so the row (and its history) survives for audit. Templates
|
|
128
|
+
// carry no RAG entries, so only the flag-patch half of that motif applies.
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export const softDelete = mutation({
|
|
132
|
+
args: {
|
|
133
|
+
templateId: v.optional(v.id("missionTemplates")),
|
|
134
|
+
name: v.optional(v.string()),
|
|
135
|
+
},
|
|
136
|
+
returns: v.null(),
|
|
137
|
+
handler: async (ctx, args) => {
|
|
138
|
+
if (args.templateId === undefined && args.name === undefined) {
|
|
139
|
+
throw new Error("Provide either templateId or name");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const template = args.templateId
|
|
143
|
+
? await ctx.db.get(args.templateId)
|
|
144
|
+
: await ctx.db
|
|
145
|
+
.query("missionTemplates")
|
|
146
|
+
.withIndex("by_name", (q) => q.eq("name", args.name as string))
|
|
147
|
+
.unique();
|
|
148
|
+
|
|
149
|
+
if (template === null || template === undefined) {
|
|
150
|
+
const ref = args.templateId ?? args.name;
|
|
151
|
+
throw new Error(`Mission template ${ref} not found`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await ctx.db.patch(template._id, {
|
|
155
|
+
deletedAt: Date.now(),
|
|
156
|
+
updatedAt: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
164
|
+
// seed — internal mutation that seeds the "issue-resolution-v2" template
|
|
165
|
+
// Call once during initial deployment or via npx convex run missionTemplates:seed
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
export const seed = internalMutation({
|
|
169
|
+
args: {},
|
|
170
|
+
returns: v.id("missionTemplates"),
|
|
171
|
+
handler: async (ctx) => {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const name = "issue-resolution-v3";
|
|
174
|
+
|
|
175
|
+
const steps = [
|
|
176
|
+
{
|
|
177
|
+
title: "Acknowledge",
|
|
178
|
+
description:
|
|
179
|
+
"Auto-posted GitHub comment confirming receipt and assignment of the issue to the AI orchestrator.",
|
|
180
|
+
tags: ["automated", "github"],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
title: "KB Search",
|
|
184
|
+
description:
|
|
185
|
+
'Search fixPatterns and episodes for similar issues using recall queries. Run: recall query="[issue keywords]" and recall query="[error message]". If match found: document which fix pattern applies in completionNote. If no match: document "No prior pattern found". completionNote is MANDATORY before proceeding.',
|
|
186
|
+
tags: ["research", "kb"],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
title: "Identify & Run Tests",
|
|
190
|
+
description:
|
|
191
|
+
"Grep test suites related to the affected component. Run identified tests and document PASS/FAIL. If the issue involves an external API: run REAL integration tests with actual API calls, not just unit tests.",
|
|
192
|
+
tags: ["testing", "analysis"],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
title: "Write Missing Tests",
|
|
196
|
+
description:
|
|
197
|
+
'Write a test that reproduces the bug. The test MUST FAIL against the current code (this proves the bug exists). Commit the failing test. completionNote MUST contain "X tests FAIL" as proof.',
|
|
198
|
+
tags: ["testing", "tdd"],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
title: "Fix",
|
|
202
|
+
description:
|
|
203
|
+
'Delegate the fix to the appropriate specialist agent. The test written in T3 MUST PASS after the fix is applied. No fix is accepted if the T3 test still fails. completionNote MUST contain "X tests PASS (including regression test)" as proof.',
|
|
204
|
+
tags: ["implementation"],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
title: "Run ALL Tests",
|
|
208
|
+
description:
|
|
209
|
+
"Run the full test suite. Zero regressions are acceptable. Document any failures and resolve them before proceeding.",
|
|
210
|
+
tags: ["testing", "qa"],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
title: "Deploy Dev + Push",
|
|
214
|
+
description:
|
|
215
|
+
"Run `npx convex dev --once` to verify Convex compilation. Push to upstream (NOT origin/fork): `git push upstream fix/issue-{number}`. Then IMMEDIATELY create a PR: `gh pr create --repo {repo} --base main --head fix/issue-{number} --title 'Fix #{number}: ...' --body '...'`. The PR is MANDATORY — never push without creating a PR in the same step.",
|
|
216
|
+
tags: ["deployment", "ci"],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
title: "Verification Preview",
|
|
220
|
+
description:
|
|
221
|
+
"Test the fix on the preview deployment. Confirm the original issue is resolved. Request human confirmation before closing.",
|
|
222
|
+
tags: ["verification", "human-review"],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
title: "Code Review",
|
|
226
|
+
description:
|
|
227
|
+
"Run the code-reviewer agent on the diff. Address all blocking feedback. Document the review outcome and any changes made. Update KB: store the fix pattern in VantagePeers fixPatterns table.",
|
|
228
|
+
tags: ["review", "quality"],
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
const existing = await ctx.db
|
|
233
|
+
.query("missionTemplates")
|
|
234
|
+
.withIndex("by_name", (q) => q.eq("name", name))
|
|
235
|
+
.unique();
|
|
236
|
+
|
|
237
|
+
if (existing !== null) {
|
|
238
|
+
await ctx.db.patch(existing._id, {
|
|
239
|
+
steps,
|
|
240
|
+
description:
|
|
241
|
+
"Issue Resolution Protocol v3 — 9-step structured process (T0-T8) for investigating, fixing, and documenting GitHub issues.",
|
|
242
|
+
isDefault: true,
|
|
243
|
+
updatedAt: now,
|
|
244
|
+
});
|
|
245
|
+
return existing._id;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return await ctx.db.insert("missionTemplates", {
|
|
249
|
+
name,
|
|
250
|
+
description:
|
|
251
|
+
"Issue Resolution Protocol v3 — 9-step structured process (T0-T8) for investigating, fixing, and documenting GitHub issues.",
|
|
252
|
+
steps,
|
|
253
|
+
isDefault: true,
|
|
254
|
+
createdBy: "system",
|
|
255
|
+
createdAt: now,
|
|
256
|
+
updatedAt: now,
|
|
257
|
+
});
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
262
|
+
// instantiateTemplateIntoMission
|
|
263
|
+
//
|
|
264
|
+
// Creates N tasks from a mission template in one call, one task per step.
|
|
265
|
+
// Each task is pre-assigned to the step's declared orchestrator (assignedTo),
|
|
266
|
+
// falling back to mission.pilot when the step has none.
|
|
267
|
+
//
|
|
268
|
+
// After all tasks are inserted (pass 1), a second pass resolves dependsOn
|
|
269
|
+
// step-indexes to actual task IDs and patches each dependent task.
|
|
270
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
export const instantiateTemplateIntoMission = mutation({
|
|
273
|
+
args: {
|
|
274
|
+
templateName: v.string(),
|
|
275
|
+
missionId: v.id("missions"),
|
|
276
|
+
context: v.optional(v.record(v.string(), v.string())),
|
|
277
|
+
titlePrefix: v.optional(v.string()),
|
|
278
|
+
callerOrchestrator: v.optional(v.string()),
|
|
279
|
+
},
|
|
280
|
+
returns: v.object({
|
|
281
|
+
taskIds: v.array(v.id("tasks")),
|
|
282
|
+
count: v.number(),
|
|
283
|
+
}),
|
|
284
|
+
handler: async (ctx, args) => {
|
|
285
|
+
// 1. Fetch template
|
|
286
|
+
const template = await ctx.db
|
|
287
|
+
.query("missionTemplates")
|
|
288
|
+
.withIndex("by_name", (q) => q.eq("name", args.templateName))
|
|
289
|
+
.unique();
|
|
290
|
+
// A soft-deleted template is invisible to reads, like a superseded memory.
|
|
291
|
+
if (!template || template.deletedAt !== undefined) {
|
|
292
|
+
throw new Error(`Template not found: "${args.templateName}"`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 2. Fetch mission
|
|
296
|
+
const mission = await ctx.db.get(args.missionId);
|
|
297
|
+
if (!mission) {
|
|
298
|
+
throw new Error(`Mission not found: ${args.missionId}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
const createdBy = args.callerOrchestrator ?? "system";
|
|
303
|
+
|
|
304
|
+
// Copy the template's brief onto the mission ONLY when the mission
|
|
305
|
+
// doesn't already carry one. Instance-specific fields supplied by the
|
|
306
|
+
// caller at mission creation (pilot, project, name, dates, ...) are
|
|
307
|
+
// never touched here — this patches `brief` alone.
|
|
308
|
+
if (template.brief !== undefined && mission.brief === undefined) {
|
|
309
|
+
await ctx.db.patch(args.missionId, {
|
|
310
|
+
brief: template.brief,
|
|
311
|
+
updatedAt: now,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Helper: simple {{key}} interpolation
|
|
316
|
+
const contextMap = args.context;
|
|
317
|
+
function interpolate(text: string): string {
|
|
318
|
+
if (!contextMap) return text;
|
|
319
|
+
return text.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
320
|
+
const val = contextMap[key];
|
|
321
|
+
return val !== undefined ? val : match;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Pass 1: insert all tasks, record ids by step index
|
|
326
|
+
const taskIds: Array<string & { __tableName: "tasks" }> = [];
|
|
327
|
+
|
|
328
|
+
for (const step of template.steps) {
|
|
329
|
+
const title = args.titlePrefix
|
|
330
|
+
? `${args.titlePrefix} ${step.title}`
|
|
331
|
+
: step.title;
|
|
332
|
+
|
|
333
|
+
const taskId = await ctx.db.insert("tasks", {
|
|
334
|
+
title,
|
|
335
|
+
description: interpolate(step.description),
|
|
336
|
+
tags: step.tags,
|
|
337
|
+
assignedTo: step.assignedTo ?? mission.pilot,
|
|
338
|
+
assignedToInstance: step.assignedToInstance,
|
|
339
|
+
missionId: args.missionId,
|
|
340
|
+
project: mission.project,
|
|
341
|
+
status: "todo",
|
|
342
|
+
priority: mission.priority,
|
|
343
|
+
createdBy,
|
|
344
|
+
createdAt: now,
|
|
345
|
+
updatedAt: now,
|
|
346
|
+
});
|
|
347
|
+
taskIds.push(taskId);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Pass 2: resolve dependsOn indexes → task ids
|
|
351
|
+
for (let i = 0; i < template.steps.length; i++) {
|
|
352
|
+
const step = template.steps[i];
|
|
353
|
+
if (!step.dependsOn || step.dependsOn.length === 0) continue;
|
|
354
|
+
|
|
355
|
+
for (const depIndex of step.dependsOn) {
|
|
356
|
+
if (depIndex < 0 || depIndex >= taskIds.length) {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`Step ${i} has dependsOn index ${depIndex} which is out of range (template has ${taskIds.length} steps)`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const resolvedDeps = step.dependsOn.map((idx) => taskIds[idx]);
|
|
364
|
+
await ctx.db.patch(taskIds[i], { dependsOn: resolvedDeps });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { taskIds, count: taskIds.length };
|
|
368
|
+
},
|
|
369
|
+
});
|