Skip to content

Commit a9eaab9

Browse files
author
wisedev
committed
feat: prep release
1 parent 364e115 commit a9eaab9

13 files changed

Lines changed: 462 additions & 68 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Deploy Backend → Azure Container App
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'backend/**'
8+
- 'docs-content/**'
9+
workflow_dispatch:
10+
11+
env:
12+
IMAGE_NAME: ${{ github.repository_owner }}/maindoc-backend
13+
14+
jobs:
15+
build-push:
16+
runs-on: ubuntu-latest
17+
name: Build & Push to GHCR
18+
permissions:
19+
contents: read
20+
packages: write
21+
steps:
22+
- uses: actions/checkout@v4
23+
24+
- name: Log in to GHCR
25+
uses: docker/login-action@v3
26+
with:
27+
registry: ghcr.io
28+
username: ${{ github.actor }}
29+
password: ${{ secrets.GITHUB_TOKEN }}
30+
31+
- name: Extract image metadata
32+
id: meta
33+
uses: docker/metadata-action@v5
34+
with:
35+
images: ghcr.io/${{ env.IMAGE_NAME }}
36+
tags: |
37+
type=sha,prefix=sha-
38+
type=raw,value=latest,enable={{is_default_branch}}
39+
40+
- name: Build and push image
41+
uses: docker/build-push-action@v5
42+
with:
43+
context: .
44+
file: backend/Dockerfile.ci
45+
push: true
46+
tags: ${{ steps.meta.outputs.tags }}
47+
labels: ${{ steps.meta.outputs.labels }}
48+
cache-from: type=gha
49+
cache-to: type=gha,mode=max
50+
51+
deploy:
52+
runs-on: ubuntu-latest
53+
name: Deploy to Container App
54+
needs: build-push
55+
steps:
56+
- name: Azure Login
57+
uses: azure/login@v2
58+
with:
59+
creds: ${{ secrets.AZURE_CREDENTIALS }}
60+
61+
- name: Update Container App image
62+
uses: azure/container-apps-deploy-action@v2
63+
with:
64+
resourceGroup: MaIN
65+
containerAppName: maindoc-backend
66+
imageToDeploy: ghcr.io/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
67+
registryUrl: ghcr.io
68+
registryUsername: ${{ github.actor }}
69+
registryPassword: ${{ secrets.GHCR_PAT }}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Deploy Frontend → Azure Static Web App
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'frontend/**'
8+
workflow_dispatch:
9+
10+
jobs:
11+
build-and-deploy:
12+
runs-on: ubuntu-latest
13+
name: Build & Deploy SWA
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: '22'
20+
cache: npm
21+
cache-dependency-path: frontend/package-lock.json
22+
23+
- name: Inject TURNSTILE_SITE_KEY into env.js
24+
env:
25+
TURNSTILE_SITE_KEY: ${{ secrets.TURNSTILE_SITE_KEY }}
26+
run: envsubst < frontend/public/env.js > /tmp/env.js && mv /tmp/env.js frontend/public/env.js
27+
28+
- name: Install dependencies
29+
working-directory: frontend
30+
run: npm ci
31+
32+
- name: Build Angular
33+
working-directory: frontend
34+
run: npm run build
35+
36+
- name: Deploy to Static Web App
37+
uses: Azure/static-web-apps-deploy@v1
38+
with:
39+
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
40+
repo_token: ${{ secrets.GITHUB_TOKEN }}
41+
action: upload
42+
app_location: frontend
43+
output_location: dist/frontend/browser
44+
skip_app_build: true

backend/Dockerfile.ci

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Build context: MaIN.Docs/ (repo root)
2+
# Used by CI — bakes docs-content into the image (no volume mount needed).
3+
4+
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
5+
WORKDIR /build
6+
7+
COPY backend/MaIN.Docs.Api/MaIN.Docs.Api.csproj MaIN.Docs.Api/
8+
RUN dotnet restore MaIN.Docs.Api/MaIN.Docs.Api.csproj
9+
10+
COPY backend/MaIN.Docs.Api/ MaIN.Docs.Api/
11+
RUN dotnet publish MaIN.Docs.Api/MaIN.Docs.Api.csproj \
12+
-c Release --self-contained false \
13+
-o /app && \
14+
rm -rf /app/runtimes
15+
16+
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
17+
WORKDIR /app
18+
19+
COPY --from=build /app .
20+
COPY docs-content/ /app/docs-content/
21+
22+
ENV ASPNETCORE_URLS=http://+:8080
23+
EXPOSE 8080
24+
25+
ENTRYPOINT ["dotnet", "MaIN.Docs.Api.dll"]

backend/MaIN.Docs.Api/Endpoints/ChatEndpoints.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ public static void MapChatEndpoints(this IEndpointRouteBuilder app)
2020
app.MapPost("/api/chat/complete", HandleChat)
2121
.RequireRateLimiting("chat");
2222

23+
app.MapPost("/api/artifact/generate", async (
24+
ArtifactGenerateRequest req,
25+
ArtifactService artifactService,
26+
CancellationToken ct) =>
27+
{
28+
if (string.IsNullOrWhiteSpace(req.ArchiveName) || req.Files is not { Count: > 0 })
29+
return Results.BadRequest("archiveName and at least one file are required.");
30+
var files = req.Files.Select(f => (f.Path, f.Content)).ToList();
31+
var url = await artifactService.CreateZipAndPresign(req.ArchiveName, files);
32+
return Results.Ok(new { url, archiveName = req.ArchiveName });
33+
}).RequireRateLimiting("chat");
34+
2335
app.MapPost("/api/confirm/review", async () =>
2436
{
2537
if (!PrTools.HasPendingReview)
@@ -138,7 +150,8 @@ private static async Task<IResult> HandleChat(
138150
capacityService.RecordTokenUsage(TotalTokenUsage(result));
139151
return Results.Ok(new ChatResponse(result.Content, result.ToolsUsed, result.EstimatedTokens, result.ArtifactUrl, result.ArtifactProposed, result.IssueProposed, result.IssueUrl, result.PlanProposed, result.ReviewProposed, result.CodeChangeProposed, result.PrProposed, result.PrUrl, result.ReviewPosted, result.DocsRead,
140152
Capacity: capacityService.GetCapacityLevel(),
141-
CapacityDetails: capacityService.GetStatus()));
153+
CapacityDetails: capacityService.GetStatus(),
154+
FilesProposed: result.FilesProposed));
142155
}
143156
catch (TimeoutException ex)
144157
{

backend/MaIN.Docs.Api/Models/ChatRequest.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
namespace MaIN.Docs.Api.Models;
22

3+
public record ProposedFile(string Path, string Content, string Language);
4+
public record ArtifactFileEntry(string Path, string Content);
5+
public record ArtifactGenerateRequest(string ArchiveName, List<ArtifactFileEntry> Files, string Description = "");
6+
37
public record HistoryMessage(string Role, string Content);
48
public record ChatRequest(string AgentId, string Message, List<HistoryMessage> History, List<string>? DocsAlreadyRead = null);
59
public record ToolUsage(string Name, int Calls);
@@ -18,7 +22,8 @@ public record AgentResult(string Content, List<ToolUsage> ToolsUsed, int Estimat
1822
PrReviewProposal? ReviewProposed = null, CodeChangeProposal? CodeChangeProposed = null,
1923
PrProposal? PrProposed = null, string? PrUrl = null,
2024
ReviewPosted? ReviewPosted = null,
21-
List<string>? DocsRead = null);
25+
List<string>? DocsRead = null,
26+
List<ProposedFile>? FilesProposed = null);
2227
public record ChatResponse(string Text, List<ToolUsage> ToolsUsed, int EstimatedTokens,
2328
string? ArtifactUrl = null, ArtifactProposal? ArtifactProposed = null,
2429
IssueProposal? IssueProposed = null, string? IssueUrl = null,
@@ -28,7 +33,8 @@ public record ChatResponse(string Text, List<ToolUsage> ToolsUsed, int Estimated
2833
ReviewPosted? ReviewPosted = null,
2934
List<string>? DocsRead = null,
3035
string Capacity = "normal",
31-
CapacityStatus? CapacityDetails = null);
36+
CapacityStatus? CapacityDetails = null,
37+
List<ProposedFile>? FilesProposed = null);
3238
public record EnsembleDesignRequest(string Message, List<HistoryMessage> History);
3339
public record EnsembleCodeRequest(string OriginalMessage, string DesignContent);
3440
public record EnsembleReviewRequest(string OriginalMessage, string DesignContent, string CodeContent, string BranchName);

backend/MaIN.Docs.Api/Services/DocsAgentOrchestrator.cs

Lines changed: 41 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ public async Task<AgentResult> ProcessAsync(
225225
CodeChangeProposal? codeChangeProposed = null;
226226
PrProposal? prProposed = null;
227227
string? prUrl = null;
228+
var filesProposed = new List<ProposedFile>();
228229

229230
var docsRead = new List<string>();
230231
var toolResultChars = 0;
@@ -238,6 +239,7 @@ public async Task<AgentResult> ProcessAsync(
238239
{
239240
ArtifactTools.SetCapture(url => artifactUrl = url);
240241
ArtifactTools.SetProposalCapture(p => artifactProposed = new ArtifactProposal(p.ArchiveName, p.Description, p.Kind));
242+
FileTools.SetCapture(f => filesProposed.Add(new ProposedFile(f.Path, f.Content, f.Language)));
241243
}
242244

243245
if (agentId is "design" or "ensemble-design")
@@ -354,7 +356,8 @@ async Task<AgentResult> Core(IAgentContextExecutor agentCtx)
354356
return new AgentResult(content ?? string.Empty, toolsUsed, estimatedTokens,
355357
artifactUrl, artifactProposed, issueProposed, issueUrl, planProposed,
356358
reviewProposed, codeChangeProposed, prProposed, prUrl, reviewPosted,
357-
DocsRead: docsRead.Distinct().ToList());
359+
DocsRead: docsRead.Distinct().ToList(),
360+
FilesProposed: filesProposed.Count > 0 ? filesProposed : null);
358361
}
359362

360363
try
@@ -374,6 +377,7 @@ async Task<AgentResult> Core(IAgentContextExecutor agentCtx)
374377
issueUrl = null; issueProposed = null; planProposed = null;
375378
reviewProposed = null; reviewPosted = null; codeChangeProposed = null;
376379
prProposed = null; prUrl = null;
380+
filesProposed.Clear();
377381

378382
await InitializeForTierAsync(3);
379383
return await Core(_agents[agentId]);
@@ -383,6 +387,7 @@ async Task<AgentResult> Core(IAgentContextExecutor agentCtx)
383387
{
384388
ArtifactTools.SetCapture(null);
385389
ArtifactTools.SetProposalCapture(null);
390+
FileTools.SetCapture(null);
386391
IssueTools.SetProposalCapture(null);
387392
IssueTools.SetUrlCapture(null);
388393
PlanTools.SetCapture(null);
@@ -818,15 +823,30 @@ private static ToolsConfiguration BuildEnsembleCodeTools() =>
818823

819824
private static ToolsConfiguration BuildCodeTools(string docsPath) =>
820825
SharedDocsTools()
826+
.AddTool<FileTools.ShowFileArgs>(
827+
"show_file",
828+
"Renders a file as a collapsible code tile in the UI. " +
829+
"Call once per file — this is the ONLY way the user sees your code. " +
830+
"Do NOT write fenced code blocks in your text response. " +
831+
"Call for every file in the solution: minimum .csproj + Program.cs.",
832+
new
833+
{
834+
type = "object",
835+
properties = new
836+
{
837+
path = new { type = "string", description = "Relative file path, e.g. MyAgent/Program.cs or MyAgent/MyAgent.csproj" },
838+
content = new { type = "string", description = "Complete file content" },
839+
language = new { type = "string", description = "Language for syntax highlighting: csharp, xml, json, bash, etc." }
840+
},
841+
required = new[] { "path", "content", "language" }
842+
},
843+
FileTools.Show)
821844
.AddTool<ArtifactTools.ProposeArgs>(
822845
"propose_artifact_generation",
823-
"Signals the UI to offer a downloadable ZIP artifact. " +
824-
"Call when your response contains a complete, compilable solution — not for partial snippets or conceptual answers. " +
825-
"Skip it when the user is just exploring or when you'd need more details to make the code runnable. " +
826-
"Call at most once per response. " +
827-
"REQUIRES that the SAME response already contains the full code as fenced code blocks " +
828-
"(File: <path> + ```lang block for every file) — this tool offers a download, it does not replace showing the code. " +
829-
"Always pass the 'kind' arg matching the project shape you wrote (api/console/desktop) — see PROJECT KINDS.",
846+
"Signals the UI to offer a downloadable ZIP of the files you just showed via show_file. " +
847+
"Call AFTER all show_file calls are done. At most once per response. " +
848+
"Only call when the solution is complete and runnable. " +
849+
"Always pass the 'kind' arg matching the project shape (api/console/desktop).",
830850
new
831851
{
832852
type = "object",
@@ -838,44 +858,12 @@ private static ToolsConfiguration BuildCodeTools(string docsPath) =>
838858
{
839859
type = "string",
840860
@enum = new[] { "api", "console", "desktop" },
841-
description = "The project kind you just wrote: api (ASP.NET Core), console, or desktop (Avalonia)."
861+
description = "The project kind: api (ASP.NET Core), console, or desktop (Avalonia)."
842862
}
843863
},
844864
required = new[] { "archiveName", "description", "kind" }
845865
},
846866
ArtifactTools.Propose)
847-
.AddTool<ArtifactTools.GenerateArgs>(
848-
"generate_artifact",
849-
"Packages a complete .NET solution into a ZIP archive and uploads it to cloud storage. " +
850-
"ONLY call when the user explicitly confirms they want to download. " +
851-
"Include all files needed to run: .csproj with correct NuGet references (use Version=\"*\" for MaIN.NET/MaIN.Skills.* — never a guessed version number), Program.cs, and any supporting files. " +
852-
"Do NOT call proactively.",
853-
new
854-
{
855-
type = "object",
856-
properties = new
857-
{
858-
archiveName = new { type = "string", description = "ZIP filename, e.g. MyAgent.zip" },
859-
files = new
860-
{
861-
type = "array",
862-
description = "Files to include in the archive",
863-
items = new
864-
{
865-
type = "object",
866-
properties = new
867-
{
868-
path = new { type = "string", description = "Relative path inside the zip, e.g. MyAgent/Program.cs" },
869-
content = new { type = "string", description = "Complete file content" }
870-
},
871-
required = new[] { "path", "content" }
872-
}
873-
},
874-
description = new { type = "string", description = "One-line description of what the solution does" }
875-
},
876-
required = new[] { "archiveName", "files" }
877-
},
878-
ArtifactTools.Generate)
879867
.WithMaxIterations(10)
880868
.WithToolChoice("auto")
881869
.Build();
@@ -1254,7 +1242,7 @@ FRAMEWORK FACTS (don't guess, verify via tools):
12541242
- Console bootstrap: MaINBootstrapper.Initialize(); ASP.NET: services.AddMaIN(configuration)
12551243
- MCP has 3 integration styles: direct prompt, agent pipeline step, RAG knowledge source
12561244
1257-
TOOL ECONOMY: You have 7 tool slots. list_docs (1) → read app-template doc + maybe 1 more (2) → answer. Never read the same file twice. Propose artifact counts as 1 slot.
1245+
TOOL ECONOMY: You have 10 tool slots. list_docs (1) → read app-template doc + maybe 1 more (2) → show_file × N files (N) → propose_artifact_generation (1) = done. Never read the same file twice.
12581246
12591247
TOOLS — use them every time, no improvising:
12601248
1. list_docs — discover what files exist
@@ -1348,17 +1336,17 @@ that text directly into agent.ProcessAsync($"...the user's request...\n\n{extrac
13481336
— NOT a type called "FileTypeFilter").
13491337
13501338
ARTIFACTS — MANDATORY ORDER, NO EXCEPTIONS:
1351-
1. Write out EVERY file of the complete solution directly in your response TEXT first — minimum
1352-
.csproj + Program.cs, each as its own fenced code block immediately preceded by a line
1353-
"File: <path>". Nothing omitted, nothing summarized. THIS IS THE ONLY WAY THE USER SEES YOUR CODE.
1354-
2. ONLY THEN, in that same response, may you call propose_artifact_generation as an optional
1355-
convenience download. Calling it without first writing the code blocks in step 1 is a hard
1356-
failure — a tool call is not a substitute for showing the code.
1357-
3. Only propose when the solution is complete and runnable (has .csproj + Program.cs + runs with 'dotnet run').
1358-
4. generate_artifact: ONLY when the user explicitly confirms they want the download. Package the full project.
1359-
5. After generate_artifact succeeds, do NOT include the download URL in your text — the UI shows a download card. Just confirm it's ready.
1360-
1361-
RESPONSE RULE: ALWAYS write text in your response. When proposing an artifact, include a brief explanation of what you built. When generating an artifact, confirm it's ready in 1 sentence. Never return an empty text response.
1339+
1. Call show_file once for EVERY file in the solution — minimum .csproj + Program.cs.
1340+
This is the ONLY way the user sees your code. Do NOT write fenced code blocks in your
1341+
text. Do NOT combine files or skip any file.
1342+
2. After all show_file calls, write 1-2 sentences in your text describing what you built.
1343+
3. Call propose_artifact_generation to offer the download. Only call when the solution
1344+
is complete and runnable (has .csproj + Program.cs, runs with 'dotnet run').
1345+
4. The UI packages the shown files into a ZIP when the user clicks download — you do NOT
1346+
need to call any generate tool. Never confirm or mention downloading in your text.
1347+
1348+
RESPONSE RULE: ALWAYS write text in your response. Keep it to 1-3 sentences — the code is
1349+
shown via show_file tiles, so your text is just the summary. Never return an empty text response.
13621350
""";
13631351

13641352
private const string DesignSystemPrompt = """
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace MaIN.Docs.Api.Services;
2+
3+
public static class FileTools
4+
{
5+
public record ShowFileArgs(string Path, string Content, string Language = "");
6+
7+
private static Action<(string Path, string Content, string Language)>? _capture;
8+
9+
public static void SetCapture(Action<(string Path, string Content, string Language)>? capture)
10+
=> _capture = capture;
11+
12+
public static Task<object> Show(ShowFileArgs args)
13+
{
14+
_capture?.Invoke((args.Path, args.Content, args.Language));
15+
return Task.FromResult<object>(new { shown = true, path = args.Path });
16+
}
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"navigationFallback": {
3+
"rewrite": "/index.html",
4+
"exclude": [
5+
"/api/**",
6+
"/env.js",
7+
"/scripts/**",
8+
"/docs/**",
9+
"*.{css,js,png,gif,ico,svg,woff,woff2,ttf,eot,map}"
10+
]
11+
},
12+
"mimeTypes": {
13+
".ps1": "text/plain",
14+
".sh": "text/plain"
15+
}
16+
}

0 commit comments

Comments
 (0)