@mainahq/core 0.6.0 → 1.0.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/package.json +3 -3
- package/src/cloud/__tests__/client.test.ts +168 -0
- package/src/cloud/client.ts +54 -0
- package/src/cloud/types.ts +42 -0
- package/src/feedback/__tests__/sync.test.ts +104 -0
- package/src/feedback/sync.ts +54 -0
- package/src/index.ts +5 -0
- package/src/init/index.ts +45 -1
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mainahq/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Maina core engines — Context, Prompt, and Verify for verification-first development",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://mainahq.com/",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "https://github.com/mainahq/maina.git",
|
|
11
11
|
"directory": "packages/core"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
@@ -493,4 +493,172 @@ describe("createCloudClient", () => {
|
|
|
493
493
|
expect(result.error).toBe("Job not found");
|
|
494
494
|
}
|
|
495
495
|
});
|
|
496
|
+
|
|
497
|
+
// ── postFeedbackBatch ─────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
test("postFeedbackBatch sends events in snake_case", async () => {
|
|
500
|
+
mockFetch.mockImplementation(() =>
|
|
501
|
+
Promise.resolve(jsonResponse({ data: { received: 3 } })),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const client = setupClient();
|
|
505
|
+
const result = await client.postFeedbackBatch([
|
|
506
|
+
{
|
|
507
|
+
promptHash: "hash-1",
|
|
508
|
+
command: "commit",
|
|
509
|
+
accepted: true,
|
|
510
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
promptHash: "hash-2",
|
|
514
|
+
command: "review",
|
|
515
|
+
accepted: false,
|
|
516
|
+
context: "user edited",
|
|
517
|
+
diffHash: "diff-abc",
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
promptHash: "hash-3",
|
|
521
|
+
command: "fix",
|
|
522
|
+
accepted: true,
|
|
523
|
+
},
|
|
524
|
+
]);
|
|
525
|
+
|
|
526
|
+
expect(result.ok).toBe(true);
|
|
527
|
+
if (result.ok) {
|
|
528
|
+
expect(result.value.received).toBe(3);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const call = mockFetch.mock.calls[0] as unknown[];
|
|
532
|
+
const url = call[0] as string;
|
|
533
|
+
expect(url).toBe("https://api.test.maina.dev/feedback/batch");
|
|
534
|
+
|
|
535
|
+
const requestInit = call[1] as RequestInit;
|
|
536
|
+
expect(requestInit.method).toBe("POST");
|
|
537
|
+
|
|
538
|
+
const body = JSON.parse(requestInit.body as string);
|
|
539
|
+
expect(body.events).toHaveLength(3);
|
|
540
|
+
// Verify snake_case mapping
|
|
541
|
+
expect(body.events[0].prompt_hash).toBe("hash-1");
|
|
542
|
+
expect(body.events[0].command).toBe("commit");
|
|
543
|
+
expect(body.events[0].accepted).toBe(true);
|
|
544
|
+
expect(body.events[0].timestamp).toBe("2026-01-01T00:00:00Z");
|
|
545
|
+
expect(body.events[1].diff_hash).toBe("diff-abc");
|
|
546
|
+
expect(body.events[1].context).toBe("user edited");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("postFeedbackBatch returns error on failure", async () => {
|
|
550
|
+
mockFetch.mockImplementation(() =>
|
|
551
|
+
Promise.resolve(jsonResponse({ error: "Unauthorized" }, 401)),
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
const client = setupClient();
|
|
555
|
+
const result = await client.postFeedbackBatch([]);
|
|
556
|
+
|
|
557
|
+
expect(result.ok).toBe(false);
|
|
558
|
+
if (!result.ok) {
|
|
559
|
+
expect(result.error).toBe("Unauthorized");
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// ── getFeedbackImprovements ──────────────────────────────────────────
|
|
564
|
+
|
|
565
|
+
test("getFeedbackImprovements returns improvements with camelCase mapping", async () => {
|
|
566
|
+
mockFetch.mockImplementation(() =>
|
|
567
|
+
Promise.resolve(
|
|
568
|
+
jsonResponse({
|
|
569
|
+
data: {
|
|
570
|
+
improvements: [
|
|
571
|
+
{
|
|
572
|
+
command: "commit",
|
|
573
|
+
prompt_hash: "hash-abc",
|
|
574
|
+
samples: 50,
|
|
575
|
+
accept_rate: 0.85,
|
|
576
|
+
status: "healthy",
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
command: "review",
|
|
580
|
+
prompt_hash: "hash-def",
|
|
581
|
+
samples: 30,
|
|
582
|
+
accept_rate: 0.4,
|
|
583
|
+
status: "needs_improvement",
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
team_totals: {
|
|
587
|
+
total_events: 200,
|
|
588
|
+
accept_rate: 0.72,
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
}),
|
|
592
|
+
),
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const client = setupClient();
|
|
596
|
+
const result = await client.getFeedbackImprovements();
|
|
597
|
+
|
|
598
|
+
expect(result.ok).toBe(true);
|
|
599
|
+
if (result.ok) {
|
|
600
|
+
expect(result.value.improvements).toHaveLength(2);
|
|
601
|
+
expect(result.value.improvements[0]?.command).toBe("commit");
|
|
602
|
+
expect(result.value.improvements[0]?.promptHash).toBe("hash-abc");
|
|
603
|
+
expect(result.value.improvements[0]?.samples).toBe(50);
|
|
604
|
+
expect(result.value.improvements[0]?.acceptRate).toBe(0.85);
|
|
605
|
+
expect(result.value.improvements[0]?.status).toBe("healthy");
|
|
606
|
+
expect(result.value.improvements[1]?.status).toBe("needs_improvement");
|
|
607
|
+
expect(result.value.teamTotals.totalEvents).toBe(200);
|
|
608
|
+
expect(result.value.teamTotals.acceptRate).toBe(0.72);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const call = mockFetch.mock.calls[0] as unknown[];
|
|
612
|
+
const url = call[0] as string;
|
|
613
|
+
expect(url).toBe("https://api.test.maina.dev/feedback/improvements");
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("getFeedbackImprovements handles camelCase response", async () => {
|
|
617
|
+
mockFetch.mockImplementation(() =>
|
|
618
|
+
Promise.resolve(
|
|
619
|
+
jsonResponse({
|
|
620
|
+
data: {
|
|
621
|
+
improvements: [
|
|
622
|
+
{
|
|
623
|
+
command: "fix",
|
|
624
|
+
promptHash: "hash-ghi",
|
|
625
|
+
samples: 10,
|
|
626
|
+
acceptRate: 0.95,
|
|
627
|
+
status: "excellent",
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
teamTotals: {
|
|
631
|
+
totalEvents: 100,
|
|
632
|
+
acceptRate: 0.9,
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
}),
|
|
636
|
+
),
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
const client = setupClient();
|
|
640
|
+
const result = await client.getFeedbackImprovements();
|
|
641
|
+
|
|
642
|
+
expect(result.ok).toBe(true);
|
|
643
|
+
if (result.ok) {
|
|
644
|
+
expect(result.value.improvements[0]?.promptHash).toBe("hash-ghi");
|
|
645
|
+
expect(result.value.improvements[0]?.acceptRate).toBe(0.95);
|
|
646
|
+
expect(result.value.teamTotals.totalEvents).toBe(100);
|
|
647
|
+
expect(result.value.teamTotals.acceptRate).toBe(0.9);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("getFeedbackImprovements returns error on failure", async () => {
|
|
652
|
+
mockFetch.mockImplementation(() =>
|
|
653
|
+
Promise.resolve(jsonResponse({ error: "Forbidden" }, 403)),
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
const client = setupClient();
|
|
657
|
+
const result = await client.getFeedbackImprovements();
|
|
658
|
+
|
|
659
|
+
expect(result.ok).toBe(false);
|
|
660
|
+
if (!result.ok) {
|
|
661
|
+
expect(result.error).toBe("Forbidden");
|
|
662
|
+
}
|
|
663
|
+
});
|
|
496
664
|
});
|
package/src/cloud/client.ts
CHANGED
|
@@ -10,6 +10,9 @@ import type {
|
|
|
10
10
|
ApiResponse,
|
|
11
11
|
CloudConfig,
|
|
12
12
|
CloudFeedbackPayload,
|
|
13
|
+
CloudPromptImprovement,
|
|
14
|
+
FeedbackEvent,
|
|
15
|
+
FeedbackImprovementsResponse,
|
|
13
16
|
PromptRecord,
|
|
14
17
|
SubmitVerifyPayload,
|
|
15
18
|
TeamInfo,
|
|
@@ -74,6 +77,16 @@ export interface CloudClient {
|
|
|
74
77
|
payload: CloudFeedbackPayload,
|
|
75
78
|
): Promise<Result<{ recorded: boolean }, string>>;
|
|
76
79
|
|
|
80
|
+
/** Upload a batch of feedback events to the cloud. */
|
|
81
|
+
postFeedbackBatch(
|
|
82
|
+
events: FeedbackEvent[],
|
|
83
|
+
): Promise<Result<{ received: number }, string>>;
|
|
84
|
+
|
|
85
|
+
/** Fetch feedback-based improvement suggestions from the cloud. */
|
|
86
|
+
getFeedbackImprovements(): Promise<
|
|
87
|
+
Result<FeedbackImprovementsResponse, string>
|
|
88
|
+
>;
|
|
89
|
+
|
|
77
90
|
/** Submit a diff for cloud verification. */
|
|
78
91
|
submitVerify(
|
|
79
92
|
payload: SubmitVerifyPayload,
|
|
@@ -201,6 +214,47 @@ export function createCloudClient(config: CloudConfig): CloudClient {
|
|
|
201
214
|
postFeedback: (payload) =>
|
|
202
215
|
request<{ recorded: boolean }>("POST", "/feedback", payload),
|
|
203
216
|
|
|
217
|
+
postFeedbackBatch: async (events) => {
|
|
218
|
+
// Map camelCase → snake_case for cloud API
|
|
219
|
+
const snakeEvents = events.map((e) => ({
|
|
220
|
+
prompt_hash: e.promptHash,
|
|
221
|
+
command: e.command,
|
|
222
|
+
accepted: e.accepted,
|
|
223
|
+
context: e.context,
|
|
224
|
+
diff_hash: e.diffHash,
|
|
225
|
+
timestamp: e.timestamp,
|
|
226
|
+
}));
|
|
227
|
+
return request<{ received: number }>("POST", "/feedback/batch", {
|
|
228
|
+
events: snakeEvents,
|
|
229
|
+
});
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
getFeedbackImprovements: async () => {
|
|
233
|
+
// biome-ignore lint/suspicious/noExplicitAny: snake_case API mapping
|
|
234
|
+
const result = await request<any>("GET", "/feedback/improvements");
|
|
235
|
+
if (!result.ok) return result;
|
|
236
|
+
const d = result.value;
|
|
237
|
+
const rawImprovements = d.improvements ?? [];
|
|
238
|
+
const improvements: CloudPromptImprovement[] = rawImprovements.map(
|
|
239
|
+
// biome-ignore lint/suspicious/noExplicitAny: snake_case API mapping
|
|
240
|
+
(i: any) => ({
|
|
241
|
+
command: i.command,
|
|
242
|
+
promptHash: i.promptHash ?? i.prompt_hash,
|
|
243
|
+
samples: i.samples,
|
|
244
|
+
acceptRate: i.acceptRate ?? i.accept_rate,
|
|
245
|
+
status: i.status,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
const totals = d.teamTotals ?? d.team_totals ?? {};
|
|
249
|
+
return ok({
|
|
250
|
+
improvements,
|
|
251
|
+
teamTotals: {
|
|
252
|
+
totalEvents: totals.totalEvents ?? totals.total_events ?? 0,
|
|
253
|
+
acceptRate: totals.acceptRate ?? totals.accept_rate ?? 0,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
|
|
204
258
|
submitVerify: async (payload) => {
|
|
205
259
|
// biome-ignore lint/suspicious/noExplicitAny: snake_case API mapping
|
|
206
260
|
const result = await request<any>("POST", "/verify", {
|
package/src/cloud/types.ts
CHANGED
|
@@ -156,3 +156,45 @@ export interface CloudFeedbackPayload {
|
|
|
156
156
|
/** Optional context about the feedback. */
|
|
157
157
|
context?: string;
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
// ── Feedback Batch (learn --cloud) ─────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export interface FeedbackEvent {
|
|
163
|
+
/** Prompt hash the feedback refers to. */
|
|
164
|
+
promptHash: string;
|
|
165
|
+
/** Command that generated the output. */
|
|
166
|
+
command: string;
|
|
167
|
+
/** Whether the user accepted the output. */
|
|
168
|
+
accepted: boolean;
|
|
169
|
+
/** Optional context about the feedback. */
|
|
170
|
+
context?: string;
|
|
171
|
+
/** Optional diff hash for traceability. */
|
|
172
|
+
diffHash?: string;
|
|
173
|
+
/** ISO-8601 timestamp. */
|
|
174
|
+
timestamp?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface FeedbackBatchPayload {
|
|
178
|
+
/** Array of feedback events to upload. */
|
|
179
|
+
events: FeedbackEvent[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface CloudPromptImprovement {
|
|
183
|
+
/** Command the improvement applies to. */
|
|
184
|
+
command: string;
|
|
185
|
+
/** Prompt hash that was analysed. */
|
|
186
|
+
promptHash: string;
|
|
187
|
+
/** Number of feedback samples analysed. */
|
|
188
|
+
samples: number;
|
|
189
|
+
/** Accept rate (0–1). */
|
|
190
|
+
acceptRate: number;
|
|
191
|
+
/** Health status based on accept rate. */
|
|
192
|
+
status: "needs_improvement" | "healthy" | "excellent";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface FeedbackImprovementsResponse {
|
|
196
|
+
/** Per-command improvement assessments. */
|
|
197
|
+
improvements: CloudPromptImprovement[];
|
|
198
|
+
/** Aggregated team-wide feedback totals. */
|
|
199
|
+
teamTotals: { totalEvents: number; acceptRate: number };
|
|
200
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { recordOutcome } from "../../prompts/engine";
|
|
5
|
+
import { exportFeedbackForCloud } from "../sync";
|
|
6
|
+
|
|
7
|
+
let tmpDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
tmpDir = join(
|
|
11
|
+
import.meta.dir,
|
|
12
|
+
`tmp-sync-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
13
|
+
);
|
|
14
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
try {
|
|
19
|
+
const { rmSync } = require("node:fs");
|
|
20
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
21
|
+
} catch {
|
|
22
|
+
// ignore
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("exportFeedbackForCloud", () => {
|
|
27
|
+
test("returns empty array when no feedback exists", () => {
|
|
28
|
+
const events = exportFeedbackForCloud(tmpDir);
|
|
29
|
+
expect(events).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("exports accepted feedback events", () => {
|
|
33
|
+
recordOutcome(tmpDir, "hash-abc", {
|
|
34
|
+
accepted: true,
|
|
35
|
+
command: "commit",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const events = exportFeedbackForCloud(tmpDir);
|
|
39
|
+
|
|
40
|
+
expect(events).toHaveLength(1);
|
|
41
|
+
expect(events[0]?.promptHash).toBe("hash-abc");
|
|
42
|
+
expect(events[0]?.command).toBe("commit");
|
|
43
|
+
expect(events[0]?.accepted).toBe(true);
|
|
44
|
+
expect(events[0]?.timestamp).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("exports rejected feedback events", () => {
|
|
48
|
+
recordOutcome(tmpDir, "hash-def", {
|
|
49
|
+
accepted: false,
|
|
50
|
+
command: "review",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const events = exportFeedbackForCloud(tmpDir);
|
|
54
|
+
|
|
55
|
+
expect(events).toHaveLength(1);
|
|
56
|
+
expect(events[0]?.accepted).toBe(false);
|
|
57
|
+
expect(events[0]?.command).toBe("review");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("exports context when present", () => {
|
|
61
|
+
recordOutcome(tmpDir, "hash-ctx", {
|
|
62
|
+
accepted: true,
|
|
63
|
+
command: "commit",
|
|
64
|
+
context: "user edited the message",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const events = exportFeedbackForCloud(tmpDir);
|
|
68
|
+
|
|
69
|
+
expect(events).toHaveLength(1);
|
|
70
|
+
expect(events[0]?.context).toBe("user edited the message");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("omits context when not present", () => {
|
|
74
|
+
recordOutcome(tmpDir, "hash-no-ctx", {
|
|
75
|
+
accepted: true,
|
|
76
|
+
command: "commit",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const events = exportFeedbackForCloud(tmpDir);
|
|
80
|
+
|
|
81
|
+
expect(events).toHaveLength(1);
|
|
82
|
+
expect(events[0]?.context).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("exports multiple events in chronological order", () => {
|
|
86
|
+
recordOutcome(tmpDir, "hash-1", { accepted: true, command: "commit" });
|
|
87
|
+
recordOutcome(tmpDir, "hash-2", { accepted: false, command: "review" });
|
|
88
|
+
recordOutcome(tmpDir, "hash-3", { accepted: true, command: "fix" });
|
|
89
|
+
|
|
90
|
+
const events = exportFeedbackForCloud(tmpDir);
|
|
91
|
+
|
|
92
|
+
expect(events).toHaveLength(3);
|
|
93
|
+
expect(events[0]?.promptHash).toBe("hash-1");
|
|
94
|
+
expect(events[1]?.promptHash).toBe("hash-2");
|
|
95
|
+
expect(events[2]?.promptHash).toBe("hash-3");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("returns empty array on invalid db path", () => {
|
|
99
|
+
const events = exportFeedbackForCloud(
|
|
100
|
+
"/nonexistent/path/that/does/not/exist",
|
|
101
|
+
);
|
|
102
|
+
expect(events).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback sync — exports local feedback records for cloud upload.
|
|
3
|
+
*
|
|
4
|
+
* Reads from the local SQLite feedback.db and maps records to the
|
|
5
|
+
* cloud-compatible FeedbackEvent format for batch upload.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { FeedbackEvent } from "../cloud/types";
|
|
9
|
+
import { getFeedbackDb } from "../db/index";
|
|
10
|
+
|
|
11
|
+
/** Raw row shape from the feedback table. */
|
|
12
|
+
interface FeedbackRow {
|
|
13
|
+
prompt_hash: string;
|
|
14
|
+
command: string;
|
|
15
|
+
accepted: number;
|
|
16
|
+
context: string | null;
|
|
17
|
+
created_at: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Export all local feedback records in the cloud-compatible format.
|
|
22
|
+
*
|
|
23
|
+
* Reads from the feedback table in the SQLite database at `mainaDir/feedback.db`
|
|
24
|
+
* and maps each row to a `FeedbackEvent` object ready for batch upload.
|
|
25
|
+
*/
|
|
26
|
+
export function exportFeedbackForCloud(mainaDir: string): FeedbackEvent[] {
|
|
27
|
+
const dbResult = getFeedbackDb(mainaDir);
|
|
28
|
+
if (!dbResult.ok) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { db } = dbResult.value;
|
|
33
|
+
|
|
34
|
+
const rows = db
|
|
35
|
+
.query(
|
|
36
|
+
"SELECT prompt_hash, command, accepted, context, created_at FROM feedback ORDER BY created_at ASC",
|
|
37
|
+
)
|
|
38
|
+
.all() as FeedbackRow[];
|
|
39
|
+
|
|
40
|
+
return rows.map((row) => {
|
|
41
|
+
const event: FeedbackEvent = {
|
|
42
|
+
promptHash: row.prompt_hash,
|
|
43
|
+
command: row.command,
|
|
44
|
+
accepted: row.accepted === 1,
|
|
45
|
+
timestamp: row.created_at,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (row.context) {
|
|
49
|
+
event.context = row.context;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return event;
|
|
53
|
+
});
|
|
54
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -64,7 +64,11 @@ export type {
|
|
|
64
64
|
ApiResponse,
|
|
65
65
|
CloudConfig,
|
|
66
66
|
CloudFeedbackPayload,
|
|
67
|
+
CloudPromptImprovement,
|
|
67
68
|
DeviceCodeResponse,
|
|
69
|
+
FeedbackBatchPayload,
|
|
70
|
+
FeedbackEvent,
|
|
71
|
+
FeedbackImprovementsResponse,
|
|
68
72
|
PromptRecord,
|
|
69
73
|
SubmitVerifyPayload,
|
|
70
74
|
TeamInfo,
|
|
@@ -168,6 +172,7 @@ export {
|
|
|
168
172
|
type RulePreference,
|
|
169
173
|
savePreferences,
|
|
170
174
|
} from "./feedback/preferences";
|
|
175
|
+
export { exportFeedbackForCloud } from "./feedback/sync";
|
|
171
176
|
export {
|
|
172
177
|
analyzeWorkflowTrace,
|
|
173
178
|
type PromptImprovement,
|
package/src/init/index.ts
CHANGED
|
@@ -177,7 +177,7 @@ function buildAgentsMd(stack: DetectedStack): string {
|
|
|
177
177
|
|
|
178
178
|
return `# AGENTS.md
|
|
179
179
|
|
|
180
|
-
This repo uses [Maina](https://github.com/
|
|
180
|
+
This repo uses [Maina](https://github.com/mainahq/maina) for verification-first development.
|
|
181
181
|
|
|
182
182
|
## Quick Start
|
|
183
183
|
\`\`\`bash
|
|
@@ -204,6 +204,7 @@ maina commit # verify + commit
|
|
|
204
204
|
|------|---------|-----------|
|
|
205
205
|
| \`.maina/constitution.md\` | Project DNA — stack, rules, gates | Team (stable, rarely changes) |
|
|
206
206
|
| \`AGENTS.md\` | Agent instructions — commands, conventions | Team |
|
|
207
|
+
| \`.github/copilot-instructions.md\` | Copilot agent instructions + MCP tools | Team |
|
|
207
208
|
| \`CLAUDE.md\` | Claude Code specific instructions | Optional, Claude Code users |
|
|
208
209
|
| \`.maina/prompts/*.md\` | Prompt overrides for review/commit/etc | Maina (via \`maina learn\`) |
|
|
209
210
|
|
|
@@ -213,6 +214,45 @@ maina commit # verify + commit
|
|
|
213
214
|
`;
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
function buildCopilotInstructions(stack: DetectedStack): string {
|
|
218
|
+
const runCmd = stack.runtime === "bun" ? "bun" : "npm";
|
|
219
|
+
return `# Copilot Instructions
|
|
220
|
+
|
|
221
|
+
You are working on a codebase verified by [Maina](https://mainahq.com), the verification-first developer OS. Maina MCP tools are available — use them.
|
|
222
|
+
|
|
223
|
+
## Workflow
|
|
224
|
+
|
|
225
|
+
1. **Get context** — call \`maina getContext\` to understand codebase state
|
|
226
|
+
2. **Write tests first** — TDD always. Write failing tests, then implement
|
|
227
|
+
3. **Verify your work** — call \`maina verify\` before requesting review
|
|
228
|
+
4. **Check for slop** — call \`maina checkSlop\` on changed files
|
|
229
|
+
5. **Review your code** — call \`maina reviewCode\` with your diff
|
|
230
|
+
|
|
231
|
+
## Available MCP Tools
|
|
232
|
+
|
|
233
|
+
| Tool | When to use |
|
|
234
|
+
|------|-------------|
|
|
235
|
+
| \`getContext\` | Before starting — understand branch state and verification status |
|
|
236
|
+
| \`verify\` | After changes — run the full verification pipeline |
|
|
237
|
+
| \`checkSlop\` | On changed files — detect AI-generated slop patterns |
|
|
238
|
+
| \`reviewCode\` | On your diff — two-stage review (spec compliance + code quality) |
|
|
239
|
+
| \`suggestTests\` | When implementing — generate TDD test stubs |
|
|
240
|
+
| \`getConventions\` | Understand project coding conventions |
|
|
241
|
+
|
|
242
|
+
## Conventions
|
|
243
|
+
|
|
244
|
+
- Runtime: ${stack.runtime}
|
|
245
|
+
- Test: \`${runCmd} test\`
|
|
246
|
+
- Commits: conventional commits (feat, fix, refactor, test, docs, chore)
|
|
247
|
+
- No \`console.log\` in production code
|
|
248
|
+
- Diff-only: only fix issues on changed lines
|
|
249
|
+
|
|
250
|
+
## When Working on Audit Issues
|
|
251
|
+
|
|
252
|
+
Issues labeled \`audit\` come from maina's daily verification. Fix the specific findings listed — don't refactor unrelated code.
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
216
256
|
const REVIEW_PROMPT_TEMPLATE = `# Review Prompt
|
|
217
257
|
|
|
218
258
|
Review the following code changes for:
|
|
@@ -288,6 +328,10 @@ function getFileManifest(stack: DetectedStack): FileEntry[] {
|
|
|
288
328
|
relativePath: ".github/workflows/maina-ci.yml",
|
|
289
329
|
content: buildCiWorkflow(stack),
|
|
290
330
|
},
|
|
331
|
+
{
|
|
332
|
+
relativePath: ".github/copilot-instructions.md",
|
|
333
|
+
content: buildCopilotInstructions(stack),
|
|
334
|
+
},
|
|
291
335
|
];
|
|
292
336
|
}
|
|
293
337
|
|