Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.10.1 — 2026-10-26

- A Polish installation shows Polish texts people read: the descriptions of the skills in Claude Code and in the ZIPs
for Claude Desktop (from `description.pl.txt` next to each skill; the instructions for Claude stay in English), and
the titles and descriptions of the prompts. `adolens skills` writes both - the installer and `install.ps1` call it,
and changing the language in the settings menu writes them again.
- `ado_create` / `ado_update` with a parent: a parent of a lower or the same backlog level (a requirement under a task,
a task under a task) gets a note "unusual parent" - the link is still made, but it is usually a mistaken id.
- A test for the warning about a required label language without a label file.

## 0.10.0 — 2026-10-26

Environments per repository, code in another project, a tidier installer page.
Expand Down
12 changes: 2 additions & 10 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,9 @@ Head '5/6 Skills for Claude'
# Settings -> Capabilities -> Skills, so the ZIPs are prepared next to the program.
$skillSrc = Join-Path $InstallDir 'skills'
if (Test-Path $skillSrc) {
$desktopZips = Join-Path $InstallDir 'skills-for-claude-desktop'
New-Item -ItemType Directory -Force -Path $desktopZips | Out-Null
Add-Type -AssemblyName System.IO.Compression.FileSystem
foreach ($skill in Get-ChildItem $skillSrc -Directory) {
$zip = Join-Path $desktopZips "$($skill.Name).zip"
if (Test-Path $zip) { Remove-Item $zip -Force }
[IO.Compression.ZipFile]::CreateFromDirectory($skill.FullName, $zip, [IO.Compression.CompressionLevel]::Optimal, $true)
}
# Claude Code loads every installed skill in each session: only the skills of the mode go there.
# Claude Code loads every installed skill in each session: only the skills of the mode go there. The ZIPs for
# Claude Desktop are written next to the program; both with descriptions in the language of the installation.
Say (& $exe skills | Out-String).Trim()
Say "Claude Desktop: upload the ZIPs you want from $desktopZips in Settings -> Capabilities -> Skills"
}
else { Say 'no skills in this package' }

Expand Down
3 changes: 2 additions & 1 deletion packaging/inno/ado-lens.iss
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,8 @@ begin
RunAdoLens(Exe, 'config --mode ' + Mode, Output);
end
else Mode := SavedMode;
InstallCodeSkills(Mode);
{ The skills of the mode for Claude Code and the ZIPs for Claude Desktop, with descriptions in the chosen language. }
if RunAdoLens(Exe, 'skills', Output) <> 0 then InstallCodeSkills(Mode);
{ 7: Claude Desktop is running and would write its old configuration back. }
Code := RunAdoLens(Exe, 'register', Output);
while (Code = 7) and not WizardSilent do
Expand Down
1 change: 1 addition & 0 deletions skills/dev-build/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Diagnozuje nieudany build albo release dla programisty - który krok padł, prawdziwe linie błędów, kod za nimi i propozycja poprawki. Użyj, gdy ktoś mówi „build jest czerwony”, „dlaczego padł build 900”, „pipeline się wysypał”, „co zepsuło build”. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/dev-pr/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pisze opis pull requesta na podstawie work itemu albo przegląda pull request względem kryteriów akceptacji work itemu i kontroli projektu. Użyj, gdy programista prosi „napisz opis PR”, „otwórz PR dla #123”, „zrób review PR 482”, „czy ten PR jest gotowy”. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/dev-start/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Rozpoczyna pracę programisty nad work itemem - co jest do zrobienia (kryteria z elementu i jego rodzica, załączniki), gdzie w kodzie to wypada, plan na każde kryterium, otwarte pytania. Użyj, gdy programista mówi „zaczynam #123”, „co mam zrobić w tym tasku”, „zaplanuj tę historyjkę”, „gdzie to zmienić”. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/dev-why/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Wyjaśnia, dlaczego kod wygląda tak, a nie inaczej - historia pliku albo obiektu X++, zmiany, które go ukształtowały, work itemy i decyzje za nimi. Użyj, gdy programista pyta „czemu to tak jest”, „kto to zmienił i po co”, „do czego to było”, „czy mogę to usunąć”. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/po-explain/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Wyjaśnia językiem biznesu, jak działa funkcja i dlaczego tak działa - z work itemów, wiki i kodu w Azure DevOps. Użyj, gdy ktoś pyta „jak działa X”, „co się dzieje, gdy…”, „czemu system robi…”, „gdzie to jest opisane”, albo potrzebuje opisu funkcji dla użytkowników, testerów czy nowej osoby w zespole. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/po-refine/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Przygotowuje refinement backlogu dla Product Ownera - co nie jest gotowe do planowania (bez kryteriów akceptacji, bez estymacji, za duże, niejasne), konkretne poprawki i pytania do zespołu, podział dużych elementów. Użyj, gdy ktoś prosi „przygotuj refinement”, „uporządkuj backlog”, „co nie jest gotowe na sprint”, „podziel tę historyjkę”, „o co zapytać zespół”. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/po-sprint/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Podsumowuje sprint albo okres dla Product Ownera i interesariuszy - postęp, ryzyka, co dostarczono i gdzie jest wdrożone - oraz pisze release notes. Użyj, gdy ktoś pyta „jak idzie sprint”, „przygotuj sprint review”, „co dostarczyliśmy”, „release notes”, „status dla interesariuszy”, „co nowego dzisiaj”. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/po-story/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pisze work item dla Product Ownera - wymaganie (user story, product backlog item, requirement), błąd albo task - krótko i kompletnie, z testowalnymi kryteriami akceptacji, na prawdziwych typach i polach projektu. Użyj, gdy ktoś prosi „załóż historyjkę”, „napisz wymaganie”, „zgłoś błąd”, „rozpisz na taski”, albo opisuje potrzebę, która ma trafić do Azure DevOps. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/po-test/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pisze przypadki testowe akceptacji z kryteriów work itemu i pokazuje stan jego testów, dla Product Ownera albo testera. Użyj, gdy ktoś prosi „napisz przypadki testowe”, „przygotuj scenariusze UAT”, „jak to przetestować”, „czy to jest przetestowane”, „co nie przeszło w testach”. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/po-verify/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sprawdza, czy work item zrobiono tak, jak proszono, i gdzie jest zmiana (DEV / UAT / PROD), dla Product Ownera. Użyj, gdy ktoś pyta „czy zrobili to, o co prosiłem”, „czy #123 jest zrobione dobrze”, „czy to już jest na UAT”, „czy mogę to zamknąć”, albo chce odbioru historyjki, błędu czy taska w Azure DevOps. Wymaga serwera MCP ado-lens.
1 change: 1 addition & 0 deletions skills/po-wiki/description.pl.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Pisze albo aktualizuje stronę dokumentacji na wiki projektu dla Product Ownera - jak działa funkcja, co dostarczona zmiana znaczy dla użytkowników, opis procesu - z work itemów, załączników i kodu. Użyj, gdy ktoś prosi „udokumentuj to”, „napisz stronę wiki”, „zaktualizuj dokumentację po tej historyjce”, „opisz proces dla nowych osób”. Wymaga serwera MCP ado-lens.
2 changes: 1 addition & 1 deletion src/AdoLens/AdoLens.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<AssemblyTitle>ADO Lens for Azure DevOps</AssemblyTitle>
<Description>Azure DevOps (work items, boards, wiki, Git and TFVC, pipelines) served to AI assistants over MCP</Description>
<Copyright>Copyright © 2026 WillowDev</Copyright>
<Version>0.10.0</Version>
<Version>0.10.1</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions src/AdoLens/Cli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,9 @@ async Task<int> SetupMenuAsync()
settings.Language = l?.Trim() switch { "1" => "pl", "2" => "en", "3" => null, _ => settings.Language };
settings.Save(SettingsPath);
Ui.Polish = settings.Language == "pl" || settings.Language is null && Ui.Polish;
// Skill descriptions follow the language.
Console.WriteLine(" " + Skills.Sync(settings.Mode));
if (Skills.DesktopZips(Ui.Polish) is { Length: > 0 } zips) Console.WriteLine(" " + zips);
break;
case "9":
Console.WriteLine(Ui.T(" 1. auto - on demand in Claude Code, all at start in other clients (Claude Desktop)",
Expand Down
6 changes: 5 additions & 1 deletion src/AdoLens/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
return await cli.SearchAsync();
case "skills":
Console.WriteLine(Skills.Sync(Settings.Load(Settings.DefaultPath).Mode));
if (Skills.DesktopZips(Ui.Polish) is { Length: > 0 } zips) Console.WriteLine(zips);
return 0;
case "review":
return await cli.ReviewAsync();
Expand Down Expand Up @@ -169,7 +170,10 @@ async Task<int> RunServerAsync()
return next(context, ct);
})
.AddCallToolFilter(ToolArguments.Errors))
.WithPrompts(Prompts.TypesFor(lens.Settings));
;
// Prompts one by one (not WithPrompts) so a Polish installation gets Polish titles and descriptions.
foreach (var method in Prompts.Methods(lens.Settings))
builder.Services.AddSingleton(sp => Prompts.Create(method, sp, Ui.Polish));
await builder.Build().RunAsync();
return 0;
}
Expand Down
31 changes: 31 additions & 0 deletions src/AdoLens/Prompts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,37 @@ public static class Prompts
{
/// <summary>The prompt classes of a mode: developer prompts (dev_*) only in the dev and full modes.</summary>
/// IEnumerable on purpose: an array would bind to the generic WithPrompts&lt;T&gt;(target) and register nothing.
/// <summary>Title and description of each prompt for a Polish installation (what people see in the prompt list).</summary>
public static readonly Dictionary<string, (string Title, string Description)> Polish = new()
{
["verify"] = ("Sprawdź work item: czy zrobione jak trzeba i gdzie jest", "Czy work item zrobiono tak, jak proszono, i gdzie jest (DEV / UAT / PROD) - kryterium po kryterium."),
["explain"] = ("Wyjaśnij, jak działa funkcja", "Jak działa funkcja i dlaczego, językiem biznesu, ze źródłami."),
["story"] = ("Napisz work item", "Wymaganie, błąd albo task - krótko i kompletnie, z kryteriami akceptacji; zakładany po twojej zgodzie."),
["sprint"] = ("Stan sprintu albo review", "Jak idzie sprint, co jest zagrożone, co dostarczono i gdzie to jest."),
["refine"] = ("Refinement backlogu", "Co w backlogu trzeba poprawić przed planowaniem: brak kryteriów, estymacji, za duże elementy, pytania do zespołu."),
["test"] = ("Przypadki testowe z kryteriów akceptacji", "Przypadki testowe napisane z kryteriów akceptacji work itemu i stan jego testów."),
["wiki"] = ("Napisz stronę dokumentacji", "Strona wiki o funkcji albo dostarczonej zmianie, na podstawie work itemów i kodu."),
["label"] = ("Skąd jest ten komunikat", "Komunikat D365 wklejony tak, jak widać go na ekranie: jego etykieta, tłumaczenia oraz gdzie i w jakim procesie się pojawia."),
["dev_start"] = ("Zacznij pracę nad work itemem", "Programista: co zrobić, gdzie w kodzie, plan z kryteriów akceptacji."),
["dev_pr"] = ("Przygotuj albo sprawdź pull request", "Programista: opis PR z work itemu albo przegląd PR względem jego work itemu."),
["dev_build"] = ("Dlaczego build jest czerwony", "Programista: błędy nieudanego builda i propozycja poprawki."),
["dev_why"] = ("Dlaczego ten kod tak wygląda", "Programista: historia pliku albo obiektu - kto zmieniał, kiedy, dla jakich work itemów i po co."),
};

/// <summary>The prompt methods of the mode.</summary>
public static IEnumerable<System.Reflection.MethodInfo> Methods(Settings settings) =>
TypesFor(settings).SelectMany(t => t.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static))
.Where(m => m.GetCustomAttributes(typeof(McpServerPromptAttribute), false).Length > 0);

/// <summary>A prompt, with its Polish title and description when the installation is Polish.</summary>
public static McpServerPrompt Create(System.Reflection.MethodInfo method, IServiceProvider services, bool polish)
{
var name = ((McpServerPromptAttribute)method.GetCustomAttributes(typeof(McpServerPromptAttribute), false)[0]).Name ?? method.Name;
var options = new McpServerPromptCreateOptions { Services = services };
if (polish && Polish.TryGetValue(name, out var pl)) { options.Title = pl.Title; options.Description = pl.Description; }
return McpServerPrompt.Create(method, options: options);
}

public static IEnumerable<Type> TypesFor(Settings settings) =>
settings.Mode == "po" ? [typeof(Prompts)] : [typeof(Prompts), typeof(DevPrompts)];

Expand Down
11 changes: 8 additions & 3 deletions src/AdoLens/Review.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,7 @@ static async Task<string> ReviewOneAsync(Session session, string change, string?
var folder = folders.GetValueOrDefault(fileId) ?? await FindFolderAsync(session, c, fileId, ct);
if (folder is null) continue; // a label file of another model (Microsoft's): not checked
var languages = await LanguageFilesAsync(session, c, folder, fileId, ct);
// A required language with no label file at all: every label is missing in it.
foreach (var lang in session.Config.LabelLanguages.Where(l => !languages.Any(x => x.Language.Equals(l, StringComparison.OrdinalIgnoreCase))))
missing.Add($"{lang}: no label file {fileId}.{lang}.label.txt in the model - none of its labels are translated");
missing.AddRange(MissingLanguageFiles(session.Config.LabelLanguages, languages.Select(x => x.Language), fileId));
foreach (var (lang, path) in languages)
{
if (!session.Config.LabelLanguageRequired(lang)) continue;
Expand All @@ -136,6 +134,13 @@ static async Task<string> ReviewOneAsync(Session session, string change, string?
return sb.ToString().TrimEnd();
}

/// <summary>A required language with no label file at all: every label is missing in it.</summary>
internal static IEnumerable<string> MissingLanguageFiles(IEnumerable<string> required, IEnumerable<string> present, string fileId)
{
var have = present.ToHashSet(StringComparer.OrdinalIgnoreCase);
return required.Where(l => !have.Contains(l)).Select(l => $"{l}: no label file {fileId}.{l}.label.txt in the model - none of its labels are translated");
}

static void Add(Dictionary<string, HashSet<string>> map, string file, string id)
{
if (!map.TryGetValue(file, out var set)) map[file] = set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Expand Down
52 changes: 50 additions & 2 deletions src/AdoLens/Skills.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,56 @@ public static class Skills
_ => skill.StartsWith("po-", StringComparison.OrdinalIgnoreCase),
};

/// <summary>
/// A skill's SKILL.md in the language of the installation: in Polish, the description (what people see in the list
/// of skills, and what Claude matches their words against) comes from description.pl.txt. The instructions stay in
/// English - Claude answers in the language the user writes in.
/// </summary>
public static string Text(string skillDir, bool polish)
{
var text = File.ReadAllText(Path.Combine(skillDir, "SKILL.md"));
var pl = Path.Combine(skillDir, "description.pl.txt");
if (!polish || !File.Exists(pl)) return text;
var description = File.ReadAllText(pl).Replace("\r", "").Replace('\n', ' ').Trim();
return System.Text.RegularExpressions.Regex.Replace(text, @"(?m)^description:[^\r\n]*", _ => "description: " + description, System.Text.RegularExpressions.RegexOptions.None, TimeSpan.FromSeconds(1));
}

/// <summary>Folder of the ZIPs for Claude Desktop next to an installed program, or null when running from the sources.</summary>
public static string? DesktopDir(string? skillsDir = null)
{
var source = skillsDir ?? Prompts.SkillsDir();
if (source is null || Path.GetDirectoryName(source) is not { } app || !File.Exists(Path.Combine(app, "bin", "adolens.exe"))) return null;
return Path.Combine(app, "skills-for-claude-desktop");
}

/// <summary>One ZIP per skill ("name/SKILL.md"), in the language of the installation, for upload in Claude Desktop.</summary>
public static string DesktopZips(bool polish, string? skillsDir = null, string? outDir = null)
{
var source = skillsDir ?? Prompts.SkillsDir();
var target = outDir ?? DesktopDir(source);
if (source is null || target is null) return "";
Directory.CreateDirectory(target);
var made = 0;
foreach (var dir in Directory.GetDirectories(source).Where(d => File.Exists(Path.Combine(d, "SKILL.md"))))
{
var name = Path.GetFileName(dir);
var zip = Path.Combine(target, name + ".zip");
if (File.Exists(zip)) File.Delete(zip);
using var archive = System.IO.Compression.ZipFile.Open(zip, System.IO.Compression.ZipArchiveMode.Create);
foreach (var file in Directory.GetFiles(dir, "*", SearchOption.AllDirectories).Where(f => !Path.GetFileName(f).StartsWith("description.", StringComparison.OrdinalIgnoreCase)))
{
var entry = archive.CreateEntry(name + "/" + Path.GetRelativePath(dir, file).Replace('\\', '/'));
using var w = new StreamWriter(entry.Open(), new System.Text.UTF8Encoding(false));
if (Path.GetFileName(file).Equals("SKILL.md", StringComparison.OrdinalIgnoreCase)) w.Write(Text(dir, polish));
else { w.Flush(); using var r = File.OpenRead(file); r.CopyTo(w.BaseStream); }
}
made++;
}
return $"Claude Desktop: {made} skill ZIP(s) in {target} - upload the ones you want in Settings > Capabilities > Skills";
}

/// <summary>Copies the mode's skills into Claude Code and removes this program's others; returns what it did.</summary>
public static string Sync(string mode, string? skillsDir = null, string? codeDir = null)
public static string Sync(string mode, string? skillsDir = null, string? codeDir = null, bool? polish = null)
{
var source = skillsDir ?? Prompts.SkillsDir();
var target = codeDir ?? CodeDir;
Expand All @@ -34,7 +82,7 @@ public static string Sync(string mode, string? skillsDir = null, string? codeDir
if (ForMode(name, mode))
{
Directory.CreateDirectory(dst);
File.Copy(file, Path.Combine(dst, "SKILL.md"), overwrite: true);
File.WriteAllText(Path.Combine(dst, "SKILL.md"), Text(dir, polish ?? Ui.Polish), new System.Text.UTF8Encoding(false));
added.Add(name);
}
else if (File.Exists(Path.Combine(dst, "SKILL.md")))
Expand Down
Loading