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.7.2 — 2026-10-16

- `ado_pr merged=<branch>` (`adolens pr --merged staging --since 7d`): pull requests completed into a branch, newest
first, each with its work items, their type, state and tags - "what went into UAT this week"; `since` takes a time
too ("2026-10-15 14:30").
- `ado_pipeline deployments=true` (`adolens pipeline --deployments --definition <release> --stage UAT`): recent release
deployments with date and time, release and build - the moment to use as `since` ("PRs to staging after deployment
XYZ").
- Dates given as `since=yyyy-MM-dd` are local days (before, a day earlier in Polish time was shown and used).

## 0.7.1 — 2026-10-15

Fixes from use in a process with many required fields.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ What each MCP tool answers, when Claude should reach for it, and the CLI command
| `ado_backlog` | a team's backlog in priority order (requirement, portfolio, task, or by name); `move` + `above`/`below` reorders | refinement, priorities | `adolens backlog --top 20`, `adolens backlog --move 123 --above 456` |
| `ado_trace` | was it done as asked, and where is it: the acceptance criteria (from the parent for a task) and whether they changed after coding started; changesets, commits and pull requests linked to the item and its tasks with the X++ objects they touch; builds (also those deleted by retention); every release stage of the code's branches with **YES / NOT YET**, labelled by the environment map (`staging` → UAT); for Git, whether the change is merged into each mapped branch; tests, tasks and warning signals | "did they do what I asked", "is it on UAT yet" | `adolens trace 123` |
| `ado_changes` | the files of a changeset (`523`), pull request (`PR 482`) or commit (sha + `repo`) with X++ objects and their added / changed / removed methods; `file` gives the diff of one file — for AOT XML per method plus the metadata (fields, controls, properties) without raw XML noise | reading what exactly changed | `adolens changes 523`, `adolens changes 523 --file AxClass/SalesTable_Extension.xml` |
| `ado_pr` | without id: pull requests waiting for my review and mine (`list=active`: all); with id: status, reviewers and votes, checks (build, work item linking, reviewers), linked work items, open comment threads written by people (not system notes) | code review, "what is blocking my PR" | `adolens pr`, `adolens pr 482` |
| `ado_pr` | without id: pull requests waiting for my review and mine (`list=active`: all); `merged=<branch>`: those completed into a branch (`since`, `top`, `repo`) with their work items; with id: status, reviewers and votes, checks (build, work item linking, reviewers), linked work items, open comment threads written by people (not system notes) | code review, "what is blocking my PR" | `adolens pr`, `adolens pr 482` |
| `ado_pr_update` | `create` (repo, source, target, title, markdown text, work items, reviewers, draft), `comment` (general or on a file and line), `reply`, `resolve`, `vote`, `publish`, `complete`, `abandon`; `@[name]` notifies | reviewing, opening and finishing PRs | — |
| `ado_pipeline` | recent builds (of a pipeline, a branch); with `build`: the failed stage, job and steps, their errors, and the lines of the log that report errors (MSBuild noise cut); `artifacts=true` lists the build's artifacts, `artifact=<name>` the files in one (`part=` reads a file), `save=true` saves it to disk | "why is the build red", getting a package or a log | `adolens pipeline 900`, `adolens pipeline 900 --artifact drop --save` |
| `ado_pipeline_run` | queues a pipeline by name or id, optionally on a branch, with variables | re-running a build | — |
Expand Down
2 changes: 1 addition & 1 deletion README.pl.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ Na co odpowiada każde narzędzie MCP, kiedy Claude powinien po nie sięgnąć i
| `ado_backlog` | backlog zespołu w kolejności priorytetu (wymagania, portfolio, zadania albo po nazwie); `move` + `above`/`below` zmienia kolejność | refinement, priorytety | `adolens backlog --top 20`, `adolens backlog --move 123 --above 456` |
| `ado_trace` | czy zrobiono to, o co proszono, i gdzie to jest: kryteria akceptacji (dla zadania — z elementu nadrzędnego) i to, czy zmieniły się po rozpoczęciu prac; changesety, commity i pull requesty podpięte pod element i jego zadania, z obiektami X++, których dotyczą; buildy (także usunięte przez retencję); każdy etap release'u gałęzi tego kodu z **TAK / JESZCZE NIE** i etykietą z mapy środowisk (`staging` → UAT); dla Gita — czy zmiana jest scalona do każdej zmapowanej gałęzi; testy, zadania i sygnały ostrzegawcze | „czy zrobili to, o co prosiłem”, „czy to już jest na UAT” | `adolens trace 123` |
| `ado_changes` | pliki changesetu (`523`), pull requesta (`PR 482`) albo commita (sha + `repo`) z obiektami X++ i ich dodanymi / zmienionymi / usuniętymi metodami; `file` daje diff jednego pliku — dla AOT XML per metoda plus metadane (pola, kontrolki, właściwości) bez szumu surowego XML-a | czytanie, co dokładnie się zmieniło | `adolens changes 523`, `adolens changes 523 --file AxClass/SalesTable_Extension.xml` |
| `ado_pr` | bez id: pull requesty czekające na mój przegląd i moje (`list=active`: wszystkie); z id: stan, recenzenci i głosy, kontrole (build, podpięty work item, recenzenci), podpięte work itemy, otwarte wątki pisane przez ludzi (bez wpisów systemowych) | code review, „co blokuje mój PR” | `adolens pr`, `adolens pr 482` |
| `ado_pr` | `merged=<gałąź>`: pull requesty scalone do gałęzi (`since`, `top`, `repo`) z ich work itemami; bez id: pull requesty czekające na mój przegląd i moje (`list=active`: wszystkie); z id: stan, recenzenci i głosy, kontrole (build, podpięty work item, recenzenci), podpięte work itemy, otwarte wątki pisane przez ludzi (bez wpisów systemowych) | code review, „co blokuje mój PR” | `adolens pr`, `adolens pr 482` |
| `ado_pr_update` | `create` (repo, źródło, cel, tytuł, opis w markdownie, work itemy, recenzenci, szkic), `comment` (ogólny albo na pliku i linii), `reply`, `resolve`, `vote`, `publish`, `complete`, `abandon`; `@[imię]` wysyła powiadomienie | przegląd, zakładanie i kończenie PR-ów | — |
| `ado_pipeline` | ostatnie buildy (pipeline'u, gałęzi); z `build`: nieudany etap, zadanie i kroki, ich błędy i linie logu zgłaszające błędy (bez szumu MSBuild); `artifacts=true` lista artefaktów buildu, `artifact=<nazwa>` pliki w nim (`part=` czyta plik), `save=true` zapisuje go na dysk | „czemu build jest czerwony”, pobranie paczki lub logu | `adolens pipeline 900`, `adolens pipeline 900 --artifact drop --save` |
| `ado_pipeline_run` | uruchamia pipeline po nazwie albo id, opcjonalnie na gałęzi, ze zmiennymi | ponowne puszczenie buildu | — |
Expand Down
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.7.1</Version>
<Version>0.7.2</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
11 changes: 10 additions & 1 deletion src/AdoLens/Cli.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ board [<board>] --team <t> --move <id> --column <name> --detail summary|norma
trace <id> was it done as asked, and where is it (DEV / UAT / PROD) --detail summary|full
changes <523 | C523 | "PR 482" | sha --repo <r>> files, X++ methods; --file <name> for a diff
pr [<id>] PRs waiting for my review and mine (--list active: all); with id: reviewers, checks, threads
--merged <branch> [--since 7d] [--repo <r>] [--top 30]: completed into a branch, with work items
pipeline [<build id>] recent builds (--definition <name> --branch <b> --top 10); with id: why it failed
--artifacts (list) | --artifact <name> [--part <file>] [--save [--to <folder>]]
--deployments [--definition <release>] [--stage <name>]: release deployments with dates
wiki [<page path>] --search <text> --depth 2 --detail full --wiki <name>
wiki-write <page path> --file <page.md> (or standard input) --mode create|append|prepend|replace
--version <v> (for replace, from 'wiki <path>') --comment <text>
Expand Down Expand Up @@ -843,7 +845,9 @@ public async Task<int> PrAsync()
var lens = NewLens();
Print(Int(cl.Positional.FirstOrDefault()) is { } id
? await PullRequestQueries.ShowAsync(lens, cl.Opt("connection"), id, cl.Opt("repo"), cl.Opt("detail"))
: await PullRequestQueries.ListAsync(lens, cl.Opt("connection"), cl.Opt("project"), cl.Opt("list")));
: cl.Opt("merged") is { } branch
? await PullRequestQueries.MergedAsync(lens, cl.Opt("connection"), cl.Opt("project"), branch, cl.Opt("repo"), cl.Opt("since"), Int(cl.Opt("top")) ?? 30)
: await PullRequestQueries.ListAsync(lens, cl.Opt("connection"), cl.Opt("project"), cl.Opt("list")));
return 0;
}

Expand All @@ -852,6 +856,11 @@ public async Task<int> PipelineAsync()
var lens = NewLens();
if (cl.Opt("to") is { } to) lens.Settings.DownloadDir = to;
var save = cl.Flag("save") || cl.Peek("to") is not null;
if (cl.Flag("deployments"))
{
Print(await PipelineQueries.DeploymentsAsync(lens, cl.Opt("connection"), cl.Opt("project"), cl.Opt("definition"), cl.Opt("stage"), Int(cl.Opt("top")) ?? 10));
return 0;
}
Print(Int(cl.Positional.FirstOrDefault()) is { } build
? cl.Flag("artifacts") || cl.Peek("artifact") is not null
? await PipelineQueries.ArtifactsAsync(lens, cl.Opt("connection"), cl.Opt("project"), build, cl.Opt("artifact"), cl.Opt("part"), save)
Expand Down
4 changes: 2 additions & 2 deletions src/AdoLens/CommandLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ public sealed partial class CommandLine
["logout"] = ["connection=", "all", "remove"],
["connections"] = [],
["trace"] = ["connection=", "detail="],
["pr"] = ["connection=", "project=", "repo=", "list=", "detail="],
["pipeline"] = ["connection=", "project=", "definition=", "branch=", "top=", "detail=", "artifacts", "artifact=", "part=", "save", "to="],
["pr"] = ["connection=", "project=", "repo=", "list=", "detail=", "merged=", "since=", "top="],
["pipeline"] = ["connection=", "project=", "definition=", "branch=", "top=", "detail=", "artifacts", "artifact=", "part=", "save", "to=", "deployments", "stage="],
["changes"] = ["connection=", "repo=", "file=", "detail="],
["config"] = ["env=", "remove-env=", "connection=", "default-connection=", "project=", "team=", "mode=", "usage-log=", "add-toolset=", "remove-toolset=", "confirm-above=", "show-tokens=", "language=", "label-languages=", "download-dir="],
["item"] = ["connection=", "include=", "detail="],
Expand Down
6 changes: 4 additions & 2 deletions src/AdoLens/Hygiene.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,10 @@ public static async Task<string> ReleaseNotesAsync(Lens lens, string? connection
string title, condition;
if (since is not null)
{
var from = Formatting.ParseSince(since, day);
var to = until is null ? day : Formatting.ParseSince(until, day);
// A date ("2026-10-01") comes back in UTC; the query and the title work with local days.
static DateTime LocalDay(DateTime d) => (d.Kind == DateTimeKind.Utc ? d.ToLocalTime() : d).Date;
var from = LocalDay(Formatting.ParseSince(since, day));
var to = until is null ? day : LocalDay(Formatting.ParseSince(until, day));
title = $"{from:yyyy-MM-dd} - {to:yyyy-MM-dd}";
condition = $" AND [Microsoft.VSTS.Common.StateChangeDate] >= '{from:yyyy-MM-dd}' AND [Microsoft.VSTS.Common.StateChangeDate] < '{to.AddDays(1):yyyy-MM-dd}'" +
await AreaAsync(boards, name, t, ct);
Expand Down
22 changes: 15 additions & 7 deletions src/AdoLens/McpTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,17 +321,22 @@ public static Task<string> ReadFile(Lens lens, string path,
public static class PrTools
{
[McpServerTool(Name = "ado_pr", ReadOnly = true, Idempotent = true)]
[Description("Pull requests: without id the ones waiting for my review and mine (list=active: all active); with id: status, reviewers and votes, checks, work items, open comment threads.")]
[Description("Pull requests: without id the ones waiting for my review and mine (list=active: all active); merged=<branch>: those completed into a branch (since, top), with their work items; with id: status, reviewers and votes, checks, work items, open comment threads.")]
public static Task<string> Pr(Lens lens,
int? id = null,
[Description("Repository name (optional with id).")] string? repo = null,
[Description("Repository name (optional).")] string? repo = null,
[Description("review (default) | active.")] string? list = null,
[Description("Target branch, e.g. staging.")] string? merged = null,
[Description("With merged: 7d, 2w or yyyy-MM-dd.")] string? since = null,
int top = 30,
[Description("full: all threads, whole description.")] string? detail = null,
string? project = null,
string? connection = null,
CancellationToken ct = default)
=> McpTools.Run("ado_pr", new { id, repo, list, detail, project, connection },
() => id is { } n ? PullRequestQueries.ShowAsync(lens, connection, n, repo, detail, ct) : PullRequestQueries.ListAsync(lens, connection, project, list, ct));
=> McpTools.Run("ado_pr", new { id, repo, list, merged, since, top, detail, project, connection },
() => id is { } n ? PullRequestQueries.ShowAsync(lens, connection, n, repo, detail, ct)
: merged is not null ? PullRequestQueries.MergedAsync(lens, connection, project, merged, repo, since, top, ct)
: PullRequestQueries.ListAsync(lens, connection, project, list, ct));

[McpServerTool(Name = "ado_pr_update")]
[Description("Acts on pull requests: create (repo, source, target, title, text, workItems, reviewers, draft), comment (text, optional file and line), reply (thread, text), resolve (thread, status), vote (approve|suggestions|wait|reject|reset), publish, complete, abandon. @[name] in text notifies.")]
Expand Down Expand Up @@ -368,9 +373,11 @@ public static Task<string> PrUpdate(Lens lens,
public static class PipelineTools
{
[McpServerTool(Name = "ado_pipeline", ReadOnly = true, Idempotent = true)]
[Description("Builds: recent runs (optionally of one pipeline or branch); with build: why it failed - failed steps, errors and the error lines of the log; artifacts=true lists its artifacts, artifact=<name> its files (part=<file> reads one, save=true saves it to disk).")]
[Description("Builds: recent runs (optionally of one pipeline or branch); with build: why it failed - failed steps, errors and the error lines of the log; artifacts=true lists its artifacts, artifact=<name> its files (part=<file> reads one, save=true saves it to disk); deployments=true: recent release deployments (definition, stage) with dates.")]
public static Task<string> Pipeline(Lens lens,
int? build = null,
bool deployments = false,
[Description("With deployments: stage name, e.g. UAT.")] string? stage = null,
bool artifacts = false,
string? artifact = null,
string? part = null,
Expand All @@ -382,8 +389,9 @@ public static Task<string> Pipeline(Lens lens,
string? project = null,
string? connection = null,
CancellationToken ct = default)
=> McpTools.Run("ado_pipeline", new { build, artifacts, artifact, part, save, definition, branch, top, detail, project, connection },
() => build is { } b
=> McpTools.Run("ado_pipeline", new { build, deployments, stage, artifacts, artifact, part, save, definition, branch, top, detail, project, connection },
() => deployments ? PipelineQueries.DeploymentsAsync(lens, connection, project, definition, stage, top, ct)
: build is { } b
? artifacts || artifact is not null ? PipelineQueries.ArtifactsAsync(lens, connection, project, b, artifact, part, save, ct)
: PipelineQueries.BuildAsync(lens, connection, project, b, detail, ct)
: PipelineQueries.ListAsync(lens, connection, project, definition, branch, top, ct));
Expand Down
37 changes: 37 additions & 0 deletions src/AdoLens/Pipelines.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,43 @@ internal static string Excerpt(string log, int max)
return string.Join('\n', hits.Select(x => $"{x.i + 1}: {Formatting.Collapse(x.l, 300)}"));
}

/// <summary>
/// Recent deployments of classic releases, newest first: definition, stage, release name, when it finished, the
/// build deployed - "when did XYZ reach UAT" (the date then goes into since= of other tools).
/// </summary>
public static async Task<string> DeploymentsAsync(Lens lens, string? connection, string? project, string? definition, string? stage, int top,
CancellationToken ct = default)
{
var session = lens.For(connection);
var name = session.Project(project);
var vsrm = session.Client.ServiceUrl("vsrm") + Uri.EscapeDataString(name) + "/";
var filter = "";
if (!string.IsNullOrWhiteSpace(definition))
{
var defs = await session.Client.GetListAsync($"{vsrm}_apis/release/definitions?searchText={Uri.EscapeDataString(definition.Trim())}", ct);
var def = defs.FirstOrDefault(d => string.Equals(Json.Str(d, "name"), definition.Trim(), StringComparison.OrdinalIgnoreCase));
if (def.ValueKind != JsonValueKind.Object) def = defs.FirstOrDefault();
if (def.ValueKind != JsonValueKind.Object) return $"no release definition like '{definition}' in {name}";
filter = $"&definitionId={def.GetProperty("id").GetInt32()}";
}
top = Math.Clamp(top, 1, 100);
var list = await session.Client.GetListAsync($"{vsrm}_apis/release/deployments?queryOrder=descending&$top={(stage is null ? top : 200)}{filter}", ct);
var rows = list.Where(d => stage is null || (Json.Str(Json.Obj(d, "releaseEnvironment"), "name") ?? "").Contains(stage.Trim(), StringComparison.OrdinalIgnoreCase))
.Take(top).ToList();
if (rows.Count == 0) return $"no deployments found in {name}{(definition is null ? "" : $" for {definition}")}{(stage is null ? "" : $", stage {stage}")}";
var sb = new StringBuilder($"deployments in {name}, newest first:\n");
foreach (var d in rows)
{
var when = DateTime.TryParse(Json.Str(d, "completedOn") ?? Json.Str(d, "startedOn") ?? Json.Str(d, "queuedOn"), CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal, out var w) ? w.ToLocalTime().ToString("yyyy-MM-dd HH:mm") : "?";
var artifact = Json.Arr(Json.Obj(d, "release"), "artifacts").FirstOrDefault(a => Json.Bool(a, "isPrimary"));
var version = Json.Str(Json.Obj(Json.Obj(artifact, "definitionReference"), "version"), "name");
sb.AppendLine($"- {when} {Json.Str(Json.Obj(d, "releaseDefinition"), "name")} / {Json.Str(Json.Obj(d, "releaseEnvironment"), "name")}: " +
$"{Json.Str(Json.Obj(d, "release"), "name")} {Json.Str(d, "deploymentStatus")}{(version is null ? "" : $", build {version}")}");
}
return sb.Append("A time here works as since= (e.g. ado_pr merged=staging since=\"2026-10-15 14:30\").").ToString();
}

const long MaxArtifactRead = 20 * 1024 * 1024;

/// <summary>
Expand Down
Loading