diff --git a/src/Polyphony/Commands/BranchCommands.LoadTree.cs b/src/Polyphony/Commands/BranchCommands.LoadTree.cs
index 78ae7b6c..0d37b91c 100644
--- a/src/Polyphony/Commands/BranchCommands.LoadTree.cs
+++ b/src/Polyphony/Commands/BranchCommands.LoadTree.cs
@@ -8,9 +8,11 @@ namespace Polyphony.Commands;
///
/// polyphony branch load-tree: discover the work-item hierarchy
-/// rooted at the given epic, group children into PR groups via PG-N tags,
-/// match each PG to its merged PR (if any), and emit completion +
-/// reconciliation summaries. Migrated from scripts/load-work-tree.ps1.
+/// rooted at the given epic, group children into merge groups via
+/// PG-N tags (legacy tag format until prompt-migration PR
+/// lands), match each merge group to its merged PR (if any), and emit
+/// completion + reconciliation summaries. Migrated from
+/// scripts/load-work-tree.ps1.
///
public sealed partial class BranchCommands
{
@@ -19,7 +21,8 @@ public sealed partial class BranchCommands
///
/// Loads the work-item tree rooted at ,
- /// discovers PR groups, and reports completion + reconciliation status.
+ /// discovers merge groups, and reports completion + reconciliation
+ /// status.
///
/// ADO work item ID — root of the hierarchy (typically an epic).
/// Cancellation token.
@@ -48,14 +51,14 @@ public async Task LoadTree(int workItem, CancellationToken ct = default)
var repoSlug = await TryResolveRepoSlugAsync(ct).ConfigureAwait(false);
var mergedPrs = await TryListMergedPrsAsync(repoSlug, ct).ConfigureAwait(false);
- var prGroups = BuildPrGroups(hierarchy, allItems, mergedPrs);
+ var mergeGroups = BuildMergeGroups(hierarchy, allItems, mergedPrs);
- var completed = prGroups.Where(p => p.Completed).Select(p => p.Name).ToList();
- var pending = prGroups.Where(p => !p.Completed).Select(p => p.Name).ToList();
- var nextPg = pending.Count > 0 ? pending[0] : "";
- var reconcile = prGroups
+ var completed = mergeGroups.Where(p => p.Completed).Select(p => p.Name).ToList();
+ var pending = mergeGroups.Where(p => !p.Completed).Select(p => p.Name).ToList();
+ var nextMergeGroup = pending.Count > 0 ? pending[0] : "";
+ var reconcile = mergeGroups
.Where(p => p.NeedsReconciliation)
- .Select(p => new PgReconciliation
+ .Select(p => new MergeGroupReconciliation
{
Name = p.Name,
NonDoneChildIds = p.NonDoneChildIds,
@@ -64,7 +67,7 @@ public async Task LoadTree(int workItem, CancellationToken ct = default)
})
.ToList();
- var taggedCount = allItems.Count(i => ExtractPgTag(i.Tags) is not null);
+ var taggedCount = allItems.Count(i => ExtractLegacyPgTag(i.Tags) is not null);
var totalTasks = allItems.Count(i =>
i.Facets.Contains("implementable") && !i.Facets.Contains("plannable"));
var totalIssues = allItems.Count(i => i.Facets.Contains("plannable"));
@@ -76,11 +79,11 @@ public async Task LoadTree(int workItem, CancellationToken ct = default)
result = new BranchLoadTreeResult
{
WorkTree = workTree,
- PrGroups = prGroups,
- CompletedPgs = completed,
- PendingPgs = pending,
- NextPg = nextPg,
- PgsNeedingReconciliation = reconcile,
+ MergeGroups = mergeGroups,
+ CompletedMergeGroups = completed,
+ PendingMergeGroups = pending,
+ NextMergeGroup = nextMergeGroup,
+ MergeGroupsNeedingReconciliation = reconcile,
TotalTasks = totalTasks,
TotalIssues = totalIssues,
TaggedItems = taggedCount,
@@ -137,23 +140,23 @@ private static WorkTree BuildWorkTree(HierarchyResult root)
};
}
- private static IReadOnlyList BuildPrGroups(
+ private static IReadOnlyList BuildMergeGroups(
HierarchyResult root,
IReadOnlyList allItems,
IReadOnlyList mergedPrs)
{
- // Group items by their PG-N tag — replicates Group-ByPG semantics
- // from scripts/lib/pg-helpers.ps1.
- var pgMap = new Dictionary Implementable, List Container)>(StringComparer.Ordinal);
+ // Group items by their PG-N tag (legacy planner-emitted format) —
+ // replicates Group-ByPG semantics from scripts/lib/pg-helpers.ps1.
+ var groupMap = new Dictionary Implementable, List Container)>(StringComparer.Ordinal);
foreach (var item in allItems)
{
- var tag = ExtractPgTag(item.Tags);
+ var tag = ExtractLegacyPgTag(item.Tags);
if (tag is null) continue;
- if (!pgMap.TryGetValue(tag, out var bucket))
+ if (!groupMap.TryGetValue(tag, out var bucket))
{
bucket = (new List(), new List());
- pgMap[tag] = bucket;
+ groupMap[tag] = bucket;
}
var isImplementable = item.Facets.Contains("implementable");
@@ -170,8 +173,8 @@ private static IReadOnlyList BuildPrGroups(
}
}
- var groups = new List();
- if (pgMap.Count == 0)
+ var groups = new List();
+ if (groupMap.Count == 0)
{
// Fallback: no PG tags found → synthesize a single PG-1.
var slug = Slugify(root.Title);
@@ -181,16 +184,16 @@ private static IReadOnlyList BuildPrGroups(
var issueIds = allItems
.Where(i => i.Facets.Contains("plannable"))
.Select(i => i.WorkItemId).ToList();
- groups.Add(BuildOnePg("PG-1", taskIds, issueIds, NewBranchName($"pg-1-{slug}"),
+ groups.Add(BuildOneMergeGroup("PG-1", taskIds, issueIds, NewBranchName($"pg-1-{slug}"),
allItems, mergedPrs, isFallback: true));
}
else
{
- foreach (var name in pgMap.Keys.OrderBy(SortKeyForPg))
+ foreach (var name in groupMap.Keys.OrderBy(SortKeyForLegacyPgTag))
{
- var bucket = pgMap[name];
+ var bucket = groupMap[name];
var branch = NewBranchName(Slugify(name));
- groups.Add(BuildOnePg(name, bucket.Implementable, bucket.Container, branch,
+ groups.Add(BuildOneMergeGroup(name, bucket.Implementable, bucket.Container, branch,
allItems, mergedPrs, isFallback: false));
}
}
@@ -198,7 +201,7 @@ private static IReadOnlyList BuildPrGroups(
return groups;
}
- private static PullRequestGroup BuildOnePg(
+ private static MergeGroup BuildOneMergeGroup(
string name,
IReadOnlyList taskIds,
IReadOnlyList issueIds,
@@ -211,12 +214,13 @@ private static PullRequestGroup BuildOnePg(
var mergedPr = match?.Number ?? 0;
var allIds = taskIds.Concat(issueIds).ToHashSet();
- var pgItems = allItems.Where(i => allIds.Contains(i.WorkItemId)).ToList();
- var allDone = pgItems.Count > 0 && pgItems.All(i => IsTerminalCategory(i.State));
+ var groupItems = allItems.Where(i => allIds.Contains(i.WorkItemId)).ToList();
+ var allDone = groupItems.Count > 0 && groupItems.All(i => IsTerminalCategory(i.State));
- // Fallback PG only counts as "completed" once both the PR is merged
- // AND every item is terminal — preserves the load-work-tree.ps1
- // semantic where ungrouped scopes need both signals to close.
+ // Fallback merge group only counts as "completed" once both the PR
+ // is merged AND every item is terminal — preserves the
+ // load-work-tree.ps1 semantic where ungrouped scopes need both
+ // signals to close.
var completed = isFallback ? mergedPr > 0 && allDone : mergedPr > 0;
IReadOnlyList nonDoneChildren = [];
@@ -238,7 +242,7 @@ private static PullRequestGroup BuildOnePg(
.Select(i => i.WorkItemId).ToList();
}
- return new PullRequestGroup
+ return new MergeGroup
{
Name = name,
ChildIds = taskIds,
@@ -253,11 +257,11 @@ private static PullRequestGroup BuildOnePg(
};
}
- private static int SortKeyForPg(string pg)
+ private static int SortKeyForLegacyPgTag(string tag)
{
// "PG-3" → 3; non-conforming names sort last.
- if (pg.StartsWith("PG-", StringComparison.Ordinal)
- && int.TryParse(pg[3..], out var n))
+ if (tag.StartsWith("PG-", StringComparison.Ordinal)
+ && int.TryParse(tag[3..], out var n))
{
return n;
}
@@ -309,11 +313,11 @@ private async Task> TryListMergedPrsAsync(
private static BranchLoadTreeResult EmptyLoadTreeResult(int workItem, string error, string adoWorkspace) => new()
{
WorkTree = new WorkTree { EpicId = workItem, EpicTitle = "", EpicType = "", WorkItems = [] },
- PrGroups = [],
- CompletedPgs = [],
- PendingPgs = [],
- NextPg = "",
- PgsNeedingReconciliation = [],
+ MergeGroups = [],
+ CompletedMergeGroups = [],
+ PendingMergeGroups = [],
+ NextMergeGroup = "",
+ MergeGroupsNeedingReconciliation = [],
TotalTasks = 0,
TotalIssues = 0,
TaggedItems = 0,
diff --git a/src/Polyphony/Commands/BranchCommands.NextTask.cs b/src/Polyphony/Commands/BranchCommands.NextTask.cs
index 92745cf3..d4cd7f42 100644
--- a/src/Polyphony/Commands/BranchCommands.NextTask.cs
+++ b/src/Polyphony/Commands/BranchCommands.NextTask.cs
@@ -7,19 +7,20 @@ namespace Polyphony.Commands;
///
/// polyphony branch next-task: select the next implementable item
-/// in a PG and transition it to its in-progress state.
+/// in a merge group and transition it to its in-progress state.
/// Migrated from scripts/task-router.ps1.
///
public sealed partial class BranchCommands
{
///
- /// Picks the next non-terminal implementable item in the named PG,
- /// transitions it via begin_implementation, and emits the
- /// branch name + workspace metadata the workflow needs to start work.
+ /// Picks the next non-terminal implementable item in the named merge
+ /// group, transitions it via begin_implementation, and emits
+ /// the branch name + workspace metadata the workflow needs to start
+ /// work.
///
/// ADO work item ID — root of the hierarchy.
- /// PG name (e.g. "PG-1"). Either this or pg-number is required.
- /// PG number (e.g. 1). Convenience for callers that track PG as int.
+ /// Merge-group name (e.g. "PG-1"). Either this or pg-number is required. Operator-facing flag name preserved as --pg-name until the workflow rewire PR ships.
+ /// Merge-group number (e.g. 1). Convenience for callers that track merge groups as ints.
/// Cancellation token.
[Command("next-task")]
public async Task NextTask(
@@ -28,11 +29,11 @@ public async Task NextTask(
int pgNumber = 0,
CancellationToken ct = default)
{
- var resolvedPg = string.IsNullOrEmpty(pgName) && pgNumber > 0
+ var resolvedMergeGroup = string.IsNullOrEmpty(pgName) && pgNumber > 0
? $"PG-{pgNumber}"
: pgName;
- if (string.IsNullOrEmpty(resolvedPg))
+ if (string.IsNullOrEmpty(resolvedMergeGroup))
{
EmitNextTask(EmptyNextTaskResult("Either --pg-name or --pg-number must be provided.", "", ""));
return ExitCodes.Success;
@@ -46,7 +47,7 @@ public async Task NextTask(
var hierarchy = await walker.WalkAsync(workItem, maxDepth: 3, ct).ConfigureAwait(false);
if (hierarchy is null)
{
- EmitNextTask(EmptyNextTaskResult($"Work item {workItem} not found", resolvedPg,
+ EmitNextTask(EmptyNextTaskResult($"Work item {workItem} not found", resolvedMergeGroup,
await TryResolveAdoWorkspaceAsync(ct).ConfigureAwait(false)));
return ExitCodes.Success;
}
@@ -59,23 +60,23 @@ public async Task NextTask(
var implementable = nodes.Where(n => n.Node.Facets.Contains("implementable")).ToList();
// Same fallback ladder as task-router.ps1:
- // 1. items directly tagged with the PG
- // 2. items whose parent container is tagged with the PG
+ // 1. items directly tagged with the merge group
+ // 2. items whose parent container is tagged with the merge group
// 3. issue-as-task: plannable+implementable, tagged, no children
// 4. all implementable items
var candidates = implementable
- .Where(n => string.Equals(ExtractPgTag(n.Node.Tags), resolvedPg, StringComparison.Ordinal))
+ .Where(n => string.Equals(ExtractLegacyPgTag(n.Node.Tags), resolvedMergeGroup, StringComparison.Ordinal))
.ToList();
if (candidates.Count == 0)
{
- var pgContainerIds = nodes
+ var mergeGroupContainerIds = nodes
.Where(n => n.Node.Facets.Contains("plannable")
- && string.Equals(ExtractPgTag(n.Node.Tags), resolvedPg, StringComparison.Ordinal))
+ && string.Equals(ExtractLegacyPgTag(n.Node.Tags), resolvedMergeGroup, StringComparison.Ordinal))
.Select(n => n.Node.WorkItemId)
.ToHashSet();
candidates = implementable
- .Where(n => n.Parent is not null && pgContainerIds.Contains(n.Parent.Node.WorkItemId))
+ .Where(n => n.Parent is not null && mergeGroupContainerIds.Contains(n.Parent.Node.WorkItemId))
.ToList();
}
@@ -85,7 +86,7 @@ public async Task NextTask(
.Where(n =>
n.Node.Facets.Contains("plannable")
&& n.Node.Facets.Contains("implementable")
- && string.Equals(ExtractPgTag(n.Node.Tags), resolvedPg, StringComparison.Ordinal)
+ && string.Equals(ExtractLegacyPgTag(n.Node.Tags), resolvedMergeGroup, StringComparison.Ordinal)
&& (n.Node.Children is null || n.Node.Children.Length == 0))
.ToList();
}
@@ -110,7 +111,7 @@ public async Task NextTask(
ContainerTitle = "",
ContainerType = "",
RemainingCount = 0,
- CurrentPg = resolvedPg,
+ CurrentMergeGroup = resolvedMergeGroup,
BranchName = "",
AdoWorkspace = workspace,
};
@@ -144,12 +145,13 @@ public async Task NextTask(
// Walk up to find the nearest plannable ancestor (the container).
var (containerId, containerTitle, containerType) = FindNearestPlannableAncestorWithType(next);
- // Resolve branch name: prefer config-driven workspace_hint.pg_branch
- // pattern, fall back to feature/{rootId}-{slug-of-pg}.
+ // Resolve branch name: prefer config-driven workspace_hint
+ // merge-group-branch pattern (legacy JSON wire key
+ // "pg_branch"), fall back to feature/{rootId}-{slug-of-mg}.
var rootItem = await repository.GetByIdAsync(workItem, ct).ConfigureAwait(false);
var hint = rootItem is not null ? BranchNameResolver.Resolve(processConfig, rootItem) : null;
- var branchName = await ResolveBranchNameAsync(hint, resolvedPg, workItem, ct).ConfigureAwait(false);
+ var branchName = await ResolveBranchNameAsync(hint, resolvedMergeGroup, workItem, ct).ConfigureAwait(false);
result = new BranchNextTaskResult
{
@@ -161,7 +163,7 @@ public async Task NextTask(
ContainerTitle = containerTitle,
ContainerType = containerType,
RemainingCount = nonTerminal.Count,
- CurrentPg = resolvedPg,
+ CurrentMergeGroup = resolvedMergeGroup,
BranchName = branchName,
AdoWorkspace = workspace,
};
@@ -170,7 +172,7 @@ public async Task NextTask(
catch (Exception ex)
{
result = EmptyNextTaskResult($"Error routing next task: {ex.Message}",
- resolvedPg, await TryResolveAdoWorkspaceAsync(ct).ConfigureAwait(false));
+ resolvedMergeGroup, await TryResolveAdoWorkspaceAsync(ct).ConfigureAwait(false));
}
EmitNextTask(result);
@@ -178,21 +180,23 @@ public async Task NextTask(
}
private async Task ResolveBranchNameAsync(
- WorkspaceHint? hint, string pgName, int rootId, CancellationToken ct)
+ WorkspaceHint? hint, string mergeGroupName, int rootId, CancellationToken ct)
{
string expected;
- if (hint is { PgBranch: { Length: > 0 } pgBranchTemplate })
+ if (hint is { MergeGroupBranch: { Length: > 0 } template })
{
- // Substitute {n} OR {pg} (the PG number) into the configured
- // pg_branch template — accept both conventions used in the wild.
- var pgNum = ExtractPgNumber(pgName);
- expected = pgBranchTemplate
- .Replace("{n}", pgNum, StringComparison.OrdinalIgnoreCase)
- .Replace("{pg}", pgNum, StringComparison.OrdinalIgnoreCase);
+ // Substitute {n} OR {pg} (the merge-group number) into the
+ // configured branch template — accept both conventions used
+ // in the wild. Legacy template tokens preserved until the
+ // workflow rewire PR ships.
+ var num = ExtractLegacyPgNumber(mergeGroupName);
+ expected = template
+ .Replace("{n}", num, StringComparison.OrdinalIgnoreCase)
+ .Replace("{pg}", num, StringComparison.OrdinalIgnoreCase);
}
else
{
- var slug = Slugify(pgName);
+ var slug = Slugify(mergeGroupName);
expected = $"feature/{rootId}-{slug}";
if (expected.Length > 60) expected = expected[..60];
}
@@ -203,10 +207,16 @@ private async Task ResolveBranchNameAsync(
return string.Equals(current, expected, StringComparison.Ordinal) ? current : expected;
}
- private static string ExtractPgNumber(string pgName)
+ ///
+ /// Parses the legacy PG-N merge-group name format to extract
+ /// the numeric suffix. "Legacy" because the planner-emitted format
+ /// is scheduled to flip in the prompt-migration PR; until then this
+ /// reader only accepts PG-N.
+ ///
+ private static string ExtractLegacyPgNumber(string mergeGroupName)
{
- if (pgName.StartsWith("PG-", StringComparison.Ordinal)
- && int.TryParse(pgName[3..], out var n))
+ if (mergeGroupName.StartsWith("PG-", StringComparison.Ordinal)
+ && int.TryParse(mergeGroupName[3..], out var n))
{
return n.ToString();
}
@@ -247,7 +257,7 @@ private static IEnumerable FlattenWithParents(HierarchyResult no
private sealed record NodeWithParent(HierarchyResult Node, NodeWithParent? Parent);
- private static BranchNextTaskResult EmptyNextTaskResult(string error, string pg, string adoWorkspace) => new()
+ private static BranchNextTaskResult EmptyNextTaskResult(string error, string mergeGroup, string adoWorkspace) => new()
{
Action = "error",
PrimaryId = 0,
@@ -257,7 +267,7 @@ private sealed record NodeWithParent(HierarchyResult Node, NodeWithParent? Paren
ContainerTitle = "",
ContainerType = "",
RemainingCount = 0,
- CurrentPg = pg,
+ CurrentMergeGroup = mergeGroup,
BranchName = "",
AdoWorkspace = adoWorkspace,
Error = error,
diff --git a/src/Polyphony/Commands/BranchCommands.Route.cs b/src/Polyphony/Commands/BranchCommands.Route.cs
index a6ff1170..c99c1033 100644
--- a/src/Polyphony/Commands/BranchCommands.Route.cs
+++ b/src/Polyphony/Commands/BranchCommands.Route.cs
@@ -9,7 +9,7 @@
namespace Polyphony.Commands;
///
-/// polyphony branch route: classifies every PR group in the
+/// polyphony branch route: classifies every merge group in the
/// hierarchy and reports the next action the workflow should take —
/// create_branch, submit_pr, or all_complete. Migrated from
/// scripts/pg-router.ps1.
@@ -17,14 +17,17 @@ namespace Polyphony.Commands;
public sealed partial class BranchCommands
{
///
- /// Routes a work-item hierarchy to its next PR-group action.
+ /// Routes a work-item hierarchy to its next merge-group action.
///
/// ADO work item ID — root of the hierarchy.
///
- /// Optional PG number (e.g. 2) to scope routing to. When supplied,
- /// the named PG is selected as current_pg regardless of
- /// whether earlier PGs are still incomplete — required for parallel
- /// for_each dispatch where each invocation must route on its own PG.
+ /// Optional merge-group number (e.g. 2) to scope routing to. When
+ /// supplied, the named merge group is selected as current_pg
+ /// (legacy JSON wire key) regardless of whether earlier merge groups
+ /// are still incomplete — required for parallel for_each dispatch
+ /// where each invocation must route on its own merge group.
+ /// Operator-facing flag name preserved as --pg-number until
+ /// the workflow rewire PR ships.
///
/// Cancellation token.
[Command("route")]
@@ -47,7 +50,7 @@ public async Task Route(int workItem, int pgNumber = 0, CancellationToken c
var rootItem = await repository.GetByIdAsync(workItem, ct).ConfigureAwait(false);
var hint = rootItem is not null ? BranchNameResolver.Resolve(processConfig, rootItem) : null;
- // Build PG groups (or a single fallback PG-1 when no tags exist).
+ // Build merge groups (or a single fallback PG-1 when no tags exist).
var groups = BuildRouteGroups(workItem, hierarchy, allItems, hint);
// Resolve repo + remote branches + PR lists; degrade gracefully on failure.
@@ -58,8 +61,8 @@ public async Task Route(int workItem, int pgNumber = 0, CancellationToken c
var classified = ClassifyGroups(groups, allItems, remoteBranches, mergedPrs, openPrs);
- // PG scoping: parallel dispatch overrides "first non-completed".
- ClassifiedPg? current;
+ // Merge-group scoping: parallel dispatch overrides "first non-completed".
+ ClassifiedMergeGroup? current;
if (pgNumber > 0)
{
current = classified
@@ -75,23 +78,23 @@ public async Task Route(int workItem, int pgNumber = 0, CancellationToken c
}
var workspace = await ResolveAdoWorkspaceAsync(ct).ConfigureAwait(false);
- var completedPgs = classified.Where(c => c.Completed).Select(c => c.Group.Name).ToList();
- var remainingPgs = classified.Where(c => !c.Completed).Select(c => c.Group.Name).ToList();
+ var completedMergeGroups = classified.Where(c => c.Completed).Select(c => c.Group.Name).ToList();
+ var remainingMergeGroups = classified.Where(c => !c.Completed).Select(c => c.Group.Name).ToList();
if (current is null)
{
result = new BranchRouteResult
{
Action = "all_complete",
- CurrentPg = "",
+ CurrentMergeGroup = "",
BranchName = "",
WorkItemIds = [],
ChildIds = [],
PrNumber = 0,
PrUrl = "",
- CompletedPgs = completedPgs,
- RemainingPgs = [],
- TotalPgs = classified.Count,
+ CompletedMergeGroups = completedMergeGroups,
+ RemainingMergeGroups = [],
+ TotalMergeGroups = classified.Count,
AdoWorkspace = workspace,
};
}
@@ -100,15 +103,15 @@ public async Task Route(int workItem, int pgNumber = 0, CancellationToken c
result = new BranchRouteResult
{
Action = current.Action,
- CurrentPg = current.Group.Name,
+ CurrentMergeGroup = current.Group.Name,
BranchName = current.Group.BranchName,
WorkItemIds = current.Group.WorkItemIds,
ChildIds = current.Group.ChildIds,
PrNumber = current.PrNumber,
PrUrl = current.PrUrl,
- CompletedPgs = completedPgs,
- RemainingPgs = remainingPgs,
- TotalPgs = classified.Count,
+ CompletedMergeGroups = completedMergeGroups,
+ RemainingMergeGroups = remainingMergeGroups,
+ TotalMergeGroups = classified.Count,
AdoWorkspace = workspace,
};
}
@@ -116,7 +119,7 @@ public async Task Route(int workItem, int pgNumber = 0, CancellationToken c
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
- result = EmptyRouteResult($"Error routing PG: {ex.Message}",
+ result = EmptyRouteResult($"Error routing merge group: {ex.Message}",
await TryResolveAdoWorkspaceAsync(ct).ConfigureAwait(false));
}
@@ -124,22 +127,22 @@ public async Task Route(int workItem, int pgNumber = 0, CancellationToken c
return ExitCodes.Success;
}
- private List BuildRouteGroups(
+ private List BuildRouteGroups(
int rootId,
HierarchyResult root,
IReadOnlyList allItems,
WorkspaceHint? hint)
{
- var pgMap = new Dictionary Implementable, List Container)>(StringComparer.Ordinal);
+ var mergeGroupMap = new Dictionary Implementable, List Container)>(StringComparer.Ordinal);
foreach (var item in allItems)
{
- var tag = ExtractPgTag(item.Tags);
+ var tag = ExtractLegacyPgTag(item.Tags);
if (tag is null) continue;
- if (!pgMap.TryGetValue(tag, out var bucket))
+ if (!mergeGroupMap.TryGetValue(tag, out var bucket))
{
bucket = (new List(), new List());
- pgMap[tag] = bucket;
+ mergeGroupMap[tag] = bucket;
}
var isImplementable = item.Facets.Contains("implementable");
@@ -156,8 +159,8 @@ private List BuildRouteGroups(
}
}
- var groups = new List();
- if (pgMap.Count == 0)
+ var groups = new List();
+ if (mergeGroupMap.Count == 0)
{
// Fallback: synthesize a single PG-1 from the whole tree.
var taskIds = allItems
@@ -178,7 +181,7 @@ private List BuildRouteGroups(
return pln && (!imp || hc);
})
.Select(i => i.WorkItemId).ToList();
- groups.Add(new RoutePgGroup(
+ groups.Add(new RouteMergeGroup(
Name: "PG-1",
BranchName: ResolveFeatureBranch(hint, rootId, root.Title),
ChildIds: taskIds,
@@ -186,12 +189,12 @@ private List BuildRouteGroups(
}
else
{
- foreach (var name in pgMap.Keys.OrderBy(SortKeyForPg))
+ foreach (var name in mergeGroupMap.Keys.OrderBy(SortKeyForLegacyPgTag))
{
- var bucket = pgMap[name];
- groups.Add(new RoutePgGroup(
+ var bucket = mergeGroupMap[name];
+ groups.Add(new RouteMergeGroup(
Name: name,
- BranchName: ResolvePgBranch(hint, rootId, name),
+ BranchName: ResolveLegacyMergeGroupBranch(hint, rootId, name),
ChildIds: bucket.Implementable,
WorkItemIds: bucket.Container));
}
@@ -200,28 +203,28 @@ private List BuildRouteGroups(
return groups;
}
- private static List ClassifyGroups(
- IReadOnlyList groups,
+ private static List ClassifyGroups(
+ IReadOnlyList groups,
IReadOnlyList allItems,
IReadOnlyList remoteBranches,
IReadOnlyList mergedPrs,
IReadOnlyList openPrs)
{
- var classified = new List(groups.Count);
- foreach (var pg in groups)
+ var classified = new List(groups.Count);
+ foreach (var mg in groups)
{
var mergedPr = mergedPrs.FirstOrDefault(p =>
- string.Equals(p.HeadRefName, pg.BranchName, StringComparison.Ordinal));
+ string.Equals(p.HeadRefName, mg.BranchName, StringComparison.Ordinal));
var openPr = openPrs.FirstOrDefault(p =>
- string.Equals(p.HeadRefName, pg.BranchName, StringComparison.Ordinal));
+ string.Equals(p.HeadRefName, mg.BranchName, StringComparison.Ordinal));
if (mergedPr is not null)
{
// Stale-branch defense: a merged PR with all containers still
// in the proposed/initial category is most likely a leftover
// from a prior failed run.
- var stale = pg.WorkItemIds.Count > 0
- && !pg.WorkItemIds.Any(id =>
+ var stale = mg.WorkItemIds.Count > 0
+ && !mg.WorkItemIds.Any(id =>
{
var item = allItems.FirstOrDefault(i => i.WorkItemId == id);
if (item is null) return false;
@@ -231,35 +234,35 @@ private static List ClassifyGroups(
if (!stale)
{
- classified.Add(new ClassifiedPg(pg, "all_complete", true,
+ classified.Add(new ClassifiedMergeGroup(mg, "all_complete", true,
mergedPr.Number, mergedPr.Url ?? ""));
continue;
}
- classified.Add(new ClassifiedPg(pg, "create_branch", false, 0, ""));
+ classified.Add(new ClassifiedMergeGroup(mg, "create_branch", false, 0, ""));
continue;
}
if (openPr is not null)
{
- classified.Add(new ClassifiedPg(pg, "submit_pr", false,
+ classified.Add(new ClassifiedMergeGroup(mg, "submit_pr", false,
openPr.Number, openPr.Url ?? ""));
continue;
}
// No merged or open PR — fall back to ADO-state-only completion.
// Prefer issue states when present; else use task states.
- var allDone = pg.WorkItemIds.Count > 0
- ? pg.WorkItemIds.All(id => IsItemTerminal(id, allItems))
- : pg.ChildIds.Count > 0 && pg.ChildIds.All(id => IsItemTerminal(id, allItems));
+ var allDone = mg.WorkItemIds.Count > 0
+ ? mg.WorkItemIds.All(id => IsItemTerminal(id, allItems))
+ : mg.ChildIds.Count > 0 && mg.ChildIds.All(id => IsItemTerminal(id, allItems));
if (allDone)
{
- classified.Add(new ClassifiedPg(pg, "all_complete", true, 0, ""));
+ classified.Add(new ClassifiedMergeGroup(mg, "all_complete", true, 0, ""));
}
else
{
- classified.Add(new ClassifiedPg(pg, "create_branch", false, 0, ""));
+ classified.Add(new ClassifiedMergeGroup(mg, "create_branch", false, 0, ""));
}
}
return classified;
@@ -271,16 +274,24 @@ private static bool IsItemTerminal(int id, IReadOnlyList allIte
return item is not null && IsTerminalCategory(item.State);
}
- private static string ResolvePgBranch(WorkspaceHint? hint, int rootId, string pgName)
+ ///
+ /// Resolves the merge-group branch name by substituting the legacy
+ /// {n}/{pg} placeholders in the user's
+ /// pg_branch template against the PG-N tag suffix.
+ /// "Legacy" because the template surface and tag format both flip in
+ /// the workflow rewire PR; until then this still consumes
+ /// WorkspaceHint.MergeGroupBranch as a free-form template.
+ ///
+ private static string ResolveLegacyMergeGroupBranch(WorkspaceHint? hint, int rootId, string mergeGroupName)
{
- if (hint is { PgBranch: { Length: > 0 } template })
+ if (hint is { MergeGroupBranch: { Length: > 0 } template })
{
- var pgNum = ExtractPgNumber(pgName);
+ var num = ExtractLegacyPgNumber(mergeGroupName);
return template
- .Replace("{n}", pgNum, StringComparison.OrdinalIgnoreCase)
- .Replace("{pg}", pgNum, StringComparison.OrdinalIgnoreCase);
+ .Replace("{n}", num, StringComparison.OrdinalIgnoreCase)
+ .Replace("{pg}", num, StringComparison.OrdinalIgnoreCase);
}
- var slug = Slugify(pgName);
+ var slug = Slugify(mergeGroupName);
var b = $"feature/{rootId}-{slug}";
return b.Length > 60 ? b[..60] : b;
}
@@ -338,15 +349,15 @@ private async Task TryListRemoteSlugAsync(CancellationToken ct)
private static BranchRouteResult EmptyRouteResult(string error, string adoWorkspace) => new()
{
Action = "error",
- CurrentPg = "",
+ CurrentMergeGroup = "",
BranchName = "",
WorkItemIds = [],
ChildIds = [],
PrNumber = 0,
PrUrl = "",
- CompletedPgs = [],
- RemainingPgs = [],
- TotalPgs = 0,
+ CompletedMergeGroups = [],
+ RemainingMergeGroups = [],
+ TotalMergeGroups = 0,
AdoWorkspace = adoWorkspace,
Error = error,
};
@@ -355,14 +366,14 @@ private static void EmitRoute(BranchRouteResult result)
=> Console.WriteLine(JsonSerializer.Serialize(
result, PolyphonyJsonContext.Default.BranchRouteResult));
- private sealed record RoutePgGroup(
+ private sealed record RouteMergeGroup(
string Name,
string BranchName,
IReadOnlyList ChildIds,
IReadOnlyList WorkItemIds);
- private sealed record ClassifiedPg(
- RoutePgGroup Group,
+ private sealed record ClassifiedMergeGroup(
+ RouteMergeGroup Group,
string Action,
bool Completed,
int PrNumber,
diff --git a/src/Polyphony/Commands/BranchCommands.cs b/src/Polyphony/Commands/BranchCommands.cs
index 3bc4c0a6..a9c9b3d1 100644
--- a/src/Polyphony/Commands/BranchCommands.cs
+++ b/src/Polyphony/Commands/BranchCommands.cs
@@ -228,13 +228,13 @@ private static void Emit(BranchCheckDepsResult result)
PolyphonyJsonContext.Default.BranchCheckDepsResult));
///
- /// Close all non-terminal items in a PG scope by validating each
- /// transition against the process config and transitioning valid items
- /// to their target state. Replaces scripts/scope-closer.ps1.
+ /// Close all non-terminal items in a merge-group scope by validating
+ /// each transition against the process config and transitioning valid
+ /// items to their target state. Replaces scripts/scope-closer.ps1.
///
/// ADO work item ID — root of the hierarchy.
- /// PG name (e.g. "PG-1"). Either this or pg-number is required.
- /// PG number (e.g. 1). Convenience for callers that track PG as int.
+ /// Merge-group name (e.g. "PG-1"). Either this or pg-number is required. Operator-facing flag name preserved as --pg-name until the workflow rewire PR ships.
+ /// Merge-group number (e.g. 1). Convenience for callers that track merge groups as ints.
/// PR number associated with this scope closure (echoed in output).
/// Cancellation token.
[Command("close-scope")]
@@ -245,11 +245,11 @@ public async Task CloseScope(
int prNumber = 0,
CancellationToken ct = default)
{
- var resolvedPg = string.IsNullOrEmpty(pgName) && pgNumber > 0
+ var resolvedMergeGroup = string.IsNullOrEmpty(pgName) && pgNumber > 0
? $"PG-{pgNumber}"
: pgName;
- if (string.IsNullOrEmpty(resolvedPg))
+ if (string.IsNullOrEmpty(resolvedMergeGroup))
{
EmitClose(EmptyClose("", prNumber, "Either --pg-name or --pg-number must be provided.", ""));
return ExitCodes.Success;
@@ -263,18 +263,18 @@ public async Task CloseScope(
var hierarchy = await walker.WalkAsync(workItem, maxDepth: 3, ct).ConfigureAwait(false);
if (hierarchy is null)
{
- EmitClose(EmptyClose(resolvedPg, prNumber,
+ EmitClose(EmptyClose(resolvedMergeGroup, prNumber,
$"Work item {workItem} not found", await ResolveAdoWorkspaceAsync(ct).ConfigureAwait(false)));
return ExitCodes.Success;
}
var allItems = Flatten(hierarchy).ToList();
- var pgItemIds = ItemsForPg(allItems, resolvedPg);
- var pgItems = allItems.Where(i => pgItemIds.Contains(i.WorkItemId)).ToList();
+ var mergeGroupItemIds = ItemsForMergeGroup(allItems, resolvedMergeGroup);
+ var mergeGroupItems = allItems.Where(i => mergeGroupItemIds.Contains(i.WorkItemId)).ToList();
// P5-compliant terminal check via twig.Domain (replaces hardcoded
// 'Done' string in scope-closer.ps1:63).
- var nonTerminal = pgItems.Where(i => !IsTerminalCategory(i.State)).ToList();
+ var nonTerminal = mergeGroupItems.Where(i => !IsTerminalCategory(i.State)).ToList();
var closed = new List();
var failed = new List();
@@ -308,7 +308,7 @@ public async Task CloseScope(
result = new BranchCloseScopeResult
{
- PgName = resolvedPg,
+ MergeGroupName = resolvedMergeGroup,
PrNumber = prNumber,
ClosedItems = closed,
FailedClosures = failed,
@@ -323,7 +323,7 @@ public async Task CloseScope(
}
catch (Exception ex)
{
- result = EmptyClose(resolvedPg, prNumber, $"Error closing scope: {ex.Message}",
+ result = EmptyClose(resolvedMergeGroup, prNumber, $"Error closing scope: {ex.Message}",
await TryResolveAdoWorkspaceAsync(ct).ConfigureAwait(false));
}
@@ -362,26 +362,33 @@ private async Task TryResolveAdoWorkspaceAsync(CancellationToken ct)
}
///
- /// Group-by-PG logic from scripts/lib/pg-helpers.ps1:
- /// - Parse the work item's tags for the first PG-N entry.
+ /// Group-by-merge-group logic from scripts/lib/pg-helpers.ps1:
+ /// - Parse the work item's tags for the first PG-N entry
+ /// (legacy tag format — see ).
/// - Classify by facet: implementable items go to implementables,
/// plannable-with-children items go to containers.
/// "Issue-as-task" (plannable+implementable with no children) is treated
/// as implementable.
///
- private static HashSet ItemsForPg(IEnumerable items, string pg)
+ private static HashSet ItemsForMergeGroup(IEnumerable items, string mergeGroup)
{
var ids = new HashSet();
foreach (var item in items)
{
- var tag = ExtractPgTag(item.Tags);
- if (!string.Equals(tag, pg, StringComparison.Ordinal)) continue;
+ var tag = ExtractLegacyPgTag(item.Tags);
+ if (!string.Equals(tag, mergeGroup, StringComparison.Ordinal)) continue;
ids.Add(item.WorkItemId);
}
return ids;
}
- private static string? ExtractPgTag(string? tags)
+ ///
+ /// Parses the legacy PG-N tag format from a work item's tag
+ /// string. Named "legacy" because the planner-emitted format is
+ /// scheduled to flip in the prompt-migration PR; until then this
+ /// reader only accepts PG-N.
+ ///
+ private static string? ExtractLegacyPgTag(string? tags)
{
if (string.IsNullOrEmpty(tags)) return null;
foreach (var raw in tags.Split(';'))
@@ -417,9 +424,9 @@ private static bool IsTerminalCategory(string state)
return category == StateCategory.Completed || category == StateCategory.Removed;
}
- private static BranchCloseScopeResult EmptyClose(string pg, int pr, string error, string adoWorkspace) => new()
+ private static BranchCloseScopeResult EmptyClose(string mergeGroup, int pr, string error, string adoWorkspace) => new()
{
- PgName = pg,
+ MergeGroupName = mergeGroup,
PrNumber = pr,
ClosedItems = [],
FailedClosures = [],
diff --git a/src/Polyphony/Models/BranchCloseScopeResult.cs b/src/Polyphony/Models/BranchCloseScopeResult.cs
index 249b9e5f..b364834b 100644
--- a/src/Polyphony/Models/BranchCloseScopeResult.cs
+++ b/src/Polyphony/Models/BranchCloseScopeResult.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Serialization;
+
namespace Polyphony;
/// An item that was successfully transitioned to a terminal state.
@@ -18,11 +20,16 @@ public sealed record FailedClosure
///
/// Result of polyphony branch close-scope. Mirrors the JSON shape
-/// emitted by the legacy scripts/scope-closer.ps1.
+/// emitted by the legacy scripts/scope-closer.ps1. JSON wire key
+/// pg_name is preserved via
+/// until the workflow rewire PR
+/// ships.
///
public sealed record BranchCloseScopeResult
{
- public required string PgName { get; init; }
+ [JsonPropertyName("pg_name")]
+ public required string MergeGroupName { get; init; }
+
public required int PrNumber { get; init; }
public required IReadOnlyList ClosedItems { get; init; }
public required IReadOnlyList FailedClosures { get; init; }
diff --git a/src/Polyphony/Models/BranchLoadTreeResult.cs b/src/Polyphony/Models/BranchLoadTreeResult.cs
index a8a61ca4..6550c599 100644
--- a/src/Polyphony/Models/BranchLoadTreeResult.cs
+++ b/src/Polyphony/Models/BranchLoadTreeResult.cs
@@ -34,8 +34,16 @@ public sealed record WorkTree
public required IReadOnlyList WorkItems { get; init; }
}
-/// A discovered PR group with completion + reconciliation metadata.
-public sealed record PullRequestGroup
+///
+/// A discovered merge group (the unit of mergeable work — formerly "PG" /
+/// "Pull-request Group") with completion + reconciliation metadata. The
+/// JSON wire format still emits legacy snake_case keys (pr_groups,
+/// completed_pgs, etc.) via
+/// pinning so workflow YAMLs and reference scripts keep binding correctly
+/// — the wire flip will land in the follow-up PR alongside workflow
+/// rewires.
+///
+public sealed record MergeGroup
{
[JsonPropertyName("child_ids")]
public required IReadOnlyList ChildIds { get; init; }
@@ -54,8 +62,11 @@ public sealed record PullRequestGroup
public required bool NeedsReconciliation { get; init; }
}
-/// Reconciliation summary for a PG that has a merged PR but stale items.
-public sealed record PgReconciliation
+///
+/// Reconciliation summary for a merge group that has a merged PR but
+/// stale items.
+///
+public sealed record MergeGroupReconciliation
{
[JsonPropertyName("non_done_child_ids")]
public required IReadOnlyList NonDoneChildIds { get; init; }
@@ -64,22 +75,37 @@ public sealed record PgReconciliation
[JsonPropertyName("non_done_work_item_ids")]
public required IReadOnlyList NonDoneWorkItemIds { get; init; }
public required string Name { get; init; }
-
}
///
/// Result of polyphony branch load-tree. Mirrors the JSON shape of
/// the legacy scripts/load-work-tree.ps1 so existing workflow YAML
/// refs continue to bind correctly.
+///
+/// JSON wire format: every *MergeGroup* property below is pinned
+/// to its legacy *pg* snake_case key by
+/// . The wire flip is deferred to
+/// a follow-up PR that lands alongside the workflow YAML rewires.
///
public sealed record BranchLoadTreeResult
{
public required WorkTree WorkTree { get; init; }
- public required IReadOnlyList PrGroups { get; init; }
- public required IReadOnlyList CompletedPgs { get; init; }
- public required IReadOnlyList PendingPgs { get; init; }
- public required string NextPg { get; init; }
- public required IReadOnlyList PgsNeedingReconciliation { get; init; }
+
+ [JsonPropertyName("pr_groups")]
+ public required IReadOnlyList MergeGroups { get; init; }
+
+ [JsonPropertyName("completed_pgs")]
+ public required IReadOnlyList CompletedMergeGroups { get; init; }
+
+ [JsonPropertyName("pending_pgs")]
+ public required IReadOnlyList PendingMergeGroups { get; init; }
+
+ [JsonPropertyName("next_pg")]
+ public required string NextMergeGroup { get; init; }
+
+ [JsonPropertyName("pgs_needing_reconciliation")]
+ public required IReadOnlyList MergeGroupsNeedingReconciliation { get; init; }
+
public required int TotalTasks { get; init; }
public required int TotalIssues { get; init; }
public required int TaggedItems { get; init; }
diff --git a/src/Polyphony/Models/BranchNextTaskResult.cs b/src/Polyphony/Models/BranchNextTaskResult.cs
index e654a42c..a412ec26 100644
--- a/src/Polyphony/Models/BranchNextTaskResult.cs
+++ b/src/Polyphony/Models/BranchNextTaskResult.cs
@@ -1,8 +1,13 @@
+using System.Text.Json.Serialization;
+
namespace Polyphony;
///
/// Result of polyphony branch next-task. Mirrors the JSON shape of
-/// the legacy scripts/task-router.ps1.
+/// the legacy scripts/task-router.ps1. JSON wire key
+/// current_pg is preserved via
+/// until the workflow rewire PR
+/// ships.
///
public sealed record BranchNextTaskResult
{
@@ -14,7 +19,10 @@ public sealed record BranchNextTaskResult
public required string ContainerTitle { get; init; }
public required string ContainerType { get; init; }
public required int RemainingCount { get; init; }
- public required string CurrentPg { get; init; }
+
+ [JsonPropertyName("current_pg")]
+ public required string CurrentMergeGroup { get; init; }
+
public required string BranchName { get; init; }
public required string AdoWorkspace { get; init; }
public string? Error { get; init; }
diff --git a/src/Polyphony/Models/BranchRouteResult.cs b/src/Polyphony/Models/BranchRouteResult.cs
index 5f528e9d..f9c6a9de 100644
--- a/src/Polyphony/Models/BranchRouteResult.cs
+++ b/src/Polyphony/Models/BranchRouteResult.cs
@@ -16,38 +16,54 @@ public sealed record BranchRouteResult
public required string Action { get; init; }
///
- /// Name of the PG the action targets (e.g. "PG-1"). Empty when
- /// is all_complete or error.
+ /// Name of the merge group the action targets (e.g. "PG-1"). Empty
+ /// when is all_complete or error.
+ /// JSON wire key remains current_pg until the workflow rewire
+ /// PR ships.
///
- public required string CurrentPg { get; init; }
+ [JsonPropertyName("current_pg")]
+ public required string CurrentMergeGroup { get; init; }
///
- /// Branch name suggestion for the PG. Empty when no PG is active.
+ /// Branch name suggestion for the merge group. Empty when no merge
+ /// group is active.
///
public required string BranchName { get; init; }
- /// Container item IDs (issues, etc.) belonging to the PG.
+ /// Container item IDs (issues, etc.) belonging to the merge group.
[JsonPropertyName("work_item_ids")]
public required IReadOnlyList WorkItemIds { get; init; }
- /// Implementable item IDs (children) belonging to the PG.
+ /// Implementable item IDs (children) belonging to the merge group.
[JsonPropertyName("child_ids")]
public required IReadOnlyList ChildIds { get; init; }
- /// Existing PR number associated with the PG branch (0 when none).
+ /// Existing PR number associated with the merge-group branch (0 when none).
public required int PrNumber { get; init; }
/// Existing PR URL when one was matched, else empty.
public required string PrUrl { get; init; }
- /// Names of PGs already complete (merged or all items terminal).
- public required IReadOnlyList CompletedPgs { get; init; }
+ ///
+ /// Names of merge groups already complete (merged or all items
+ /// terminal). JSON wire key stays completed_pgs.
+ ///
+ [JsonPropertyName("completed_pgs")]
+ public required IReadOnlyList CompletedMergeGroups { get; init; }
- /// Names of PGs still in flight.
- public required IReadOnlyList RemainingPgs { get; init; }
+ ///
+ /// Names of merge groups still in flight. JSON wire key stays
+ /// remaining_pgs.
+ ///
+ [JsonPropertyName("remaining_pgs")]
+ public required IReadOnlyList RemainingMergeGroups { get; init; }
- /// Total PG count discovered (1 in the unstructured fallback).
- public required int TotalPgs { get; init; }
+ ///
+ /// Total merge-group count discovered (1 in the unstructured
+ /// fallback). JSON wire key stays total_pgs.
+ ///
+ [JsonPropertyName("total_pgs")]
+ public required int TotalMergeGroups { get; init; }
/// Resolved ADO workspace identifier ("org/project").
public required string AdoWorkspace { get; init; }
diff --git a/src/Polyphony/Models/RouteResult.cs b/src/Polyphony/Models/RouteResult.cs
index 326e4664..f8823c3b 100644
--- a/src/Polyphony/Models/RouteResult.cs
+++ b/src/Polyphony/Models/RouteResult.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Serialization;
+
namespace Polyphony;
public sealed record RouteResult
@@ -9,8 +11,15 @@ public sealed record RouteResult
public WorkspaceHint? WorkspaceHint { get; init; }
}
+///
+/// Branch-name hints emitted alongside routing decisions. JSON wire key
+/// pg_branch is preserved via
+/// until the workflow rewire PR ships.
+///
public sealed record WorkspaceHint
{
public string? FeatureBranch { get; init; }
- public string? PgBranch { get; init; }
+
+ [JsonPropertyName("pg_branch")]
+ public string? MergeGroupBranch { get; init; }
}
diff --git a/src/Polyphony/PolyphonyJsonContext.cs b/src/Polyphony/PolyphonyJsonContext.cs
index fddc7835..910733f7 100644
--- a/src/Polyphony/PolyphonyJsonContext.cs
+++ b/src/Polyphony/PolyphonyJsonContext.cs
@@ -33,8 +33,8 @@ namespace Polyphony;
[JsonSerializable(typeof(WorkTree))]
[JsonSerializable(typeof(WorkTreeIssue))]
[JsonSerializable(typeof(WorkTreeTask))]
-[JsonSerializable(typeof(PullRequestGroup))]
-[JsonSerializable(typeof(PgReconciliation))]
+[JsonSerializable(typeof(MergeGroup))]
+[JsonSerializable(typeof(MergeGroupReconciliation))]
[JsonSerializable(typeof(BranchNextTaskResult))]
[JsonSerializable(typeof(PlanSeedChildrenResult))]
[JsonSerializable(typeof(SeedReconciliation))]
diff --git a/src/Polyphony/Routing/BranchNameResolver.cs b/src/Polyphony/Routing/BranchNameResolver.cs
index 194a12d2..c9886ce7 100644
--- a/src/Polyphony/Routing/BranchNameResolver.cs
+++ b/src/Polyphony/Routing/BranchNameResolver.cs
@@ -27,7 +27,12 @@ public static class BranchNameResolver
return new WorkspaceHint
{
FeatureBranch = SubstitutePlaceholders(config.BranchStrategy.FeatureBranch, rootItem.Id, slug),
- PgBranch = SubstitutePlaceholders(config.BranchStrategy.PgBranch, rootItem.Id, slug),
+ // BranchStrategy.PgBranch is the YAML wire key on the user's
+ // process-config.yaml — preserved until the workflow rewire
+ // PR ships. WorkspaceHint exposes it under the new
+ // MergeGroupBranch C# name; the JSON output still emits
+ // "pg_branch" via [JsonPropertyName].
+ MergeGroupBranch = SubstitutePlaceholders(config.BranchStrategy.PgBranch, rootItem.Id, slug),
};
}
diff --git a/tests/Polyphony.Tests/Commands/BranchCommandsCloseScopeTests.cs b/tests/Polyphony.Tests/Commands/BranchCommandsCloseScopeTests.cs
index 33f30c49..2b9e1940 100644
--- a/tests/Polyphony.Tests/Commands/BranchCommandsCloseScopeTests.cs
+++ b/tests/Polyphony.Tests/Commands/BranchCommandsCloseScopeTests.cs
@@ -70,7 +70,7 @@ public async Task CloseScope_PgNumberDerivesPgName()
var (_, output) = await CaptureConsoleAsync(() => cmd.CloseScope(workItem: 100, pgNumber: 3, prNumber: 42));
var result = Deserialize(output);
- result.PgName.ShouldBe("PG-3");
+ result.MergeGroupName.ShouldBe("PG-3");
result.PrNumber.ShouldBe(42);
result.AdoWorkspace.ShouldBe("org/proj");
}
@@ -97,7 +97,7 @@ public async Task CloseScope_PgItemsTransitioned_RecordedInClosedItems()
exit.ShouldBe(ExitCodes.Success);
var result = Deserialize(output);
- result.PgName.ShouldBe("PG-1");
+ result.MergeGroupName.ShouldBe("PG-1");
result.PrNumber.ShouldBe(7);
result.TotalClosed.ShouldBe(2);
result.TotalFailed.ShouldBe(0);
diff --git a/tests/Polyphony.Tests/Commands/BranchCommandsLoadTreeTests.cs b/tests/Polyphony.Tests/Commands/BranchCommandsLoadTreeTests.cs
index 1dd1b0d2..f0daadcf 100644
--- a/tests/Polyphony.Tests/Commands/BranchCommandsLoadTreeTests.cs
+++ b/tests/Polyphony.Tests/Commands/BranchCommandsLoadTreeTests.cs
@@ -88,12 +88,12 @@ public async Task LoadTree_NoPgTags_FallsBackToSinglePg1()
var (_, output) = await CaptureConsoleAsync(() => cmd.LoadTree(100));
var result = Deserialize(output);
- result.PrGroups.Count.ShouldBe(1);
- result.PrGroups[0].Name.ShouldBe("PG-1");
- result.PrGroups[0].MergedPr.ShouldBe(0);
- result.PrGroups[0].Completed.ShouldBeFalse();
- result.PendingPgs.ShouldContain("PG-1");
- result.NextPg.ShouldBe("PG-1");
+ result.MergeGroups.Count.ShouldBe(1);
+ result.MergeGroups[0].Name.ShouldBe("PG-1");
+ result.MergeGroups[0].MergedPr.ShouldBe(0);
+ result.MergeGroups[0].Completed.ShouldBeFalse();
+ result.PendingMergeGroups.ShouldContain("PG-1");
+ result.NextMergeGroup.ShouldBe("PG-1");
result.TaggedItems.ShouldBe(0);
result.UntaggedItems.ShouldBe(3);
}
@@ -121,12 +121,12 @@ public async Task LoadTree_PgTags_GroupsItemsByPg()
var (_, output) = await CaptureConsoleAsync(() => cmd.LoadTree(100));
var result = Deserialize(output);
- result.PrGroups.Count.ShouldBe(2);
+ result.MergeGroups.Count.ShouldBe(2);
// Sorted by N (PG-1 before PG-2).
- result.PrGroups[0].Name.ShouldBe("PG-1");
- result.PrGroups[1].Name.ShouldBe("PG-2");
- result.PrGroups[0].BranchNameSuggestion.ShouldStartWith("feature/pg-1");
- result.NextPg.ShouldBe("PG-1");
+ result.MergeGroups[0].Name.ShouldBe("PG-1");
+ result.MergeGroups[1].Name.ShouldBe("PG-2");
+ result.MergeGroups[0].BranchNameSuggestion.ShouldStartWith("feature/pg-1");
+ result.NextMergeGroup.ShouldBe("PG-1");
result.TaggedItems.ShouldBe(4);
result.WorkTree.EpicId.ShouldBe(100);
result.WorkTree.WorkItems.Count.ShouldBe(2);
@@ -149,11 +149,11 @@ public async Task LoadTree_MergedPr_MarksPgCompleted()
var (_, output) = await CaptureConsoleAsync(() => cmd.LoadTree(100));
var result = Deserialize(output);
- var pg = result.PrGroups.Single();
+ var pg = result.MergeGroups.Single();
pg.MergedPr.ShouldBe(42);
pg.Completed.ShouldBeTrue();
- result.CompletedPgs.ShouldContain("PG-1");
- result.NextPg.ShouldBe("");
+ result.CompletedMergeGroups.ShouldContain("PG-1");
+ result.NextMergeGroup.ShouldBe("");
}
[Fact]
@@ -173,12 +173,12 @@ public async Task LoadTree_CompletedPgWithStaleDoingTasks_NeedsReconciliation()
var (_, output) = await CaptureConsoleAsync(() => cmd.LoadTree(100));
var result = Deserialize(output);
- var pg = result.PrGroups.Single();
+ var pg = result.MergeGroups.Single();
pg.Completed.ShouldBeTrue();
pg.NeedsReconciliation.ShouldBeTrue();
pg.StaleDoingChildIds.ShouldContain(300);
- result.PgsNeedingReconciliation.Count.ShouldBe(1);
- result.PgsNeedingReconciliation[0].StaleDoingChildIds.ShouldContain(300);
+ result.MergeGroupsNeedingReconciliation.Count.ShouldBe(1);
+ result.MergeGroupsNeedingReconciliation[0].StaleDoingChildIds.ShouldContain(300);
}
[Fact]
@@ -199,8 +199,8 @@ public async Task LoadTree_NoGitHubRemote_PgsRemainPending()
var (_, output) = await CaptureConsoleAsync(() => cmd.LoadTree(100));
var result = Deserialize(output);
- result.PrGroups.Single().MergedPr.ShouldBe(0);
- result.PrGroups.Single().Completed.ShouldBeFalse();
+ result.MergeGroups.Single().MergedPr.ShouldBe(0);
+ result.MergeGroups.Single().Completed.ShouldBeFalse();
}
[Fact]
diff --git a/tests/Polyphony.Tests/Commands/BranchCommandsNextTaskTests.cs b/tests/Polyphony.Tests/Commands/BranchCommandsNextTaskTests.cs
index 3b78d714..568e067d 100644
--- a/tests/Polyphony.Tests/Commands/BranchCommandsNextTaskTests.cs
+++ b/tests/Polyphony.Tests/Commands/BranchCommandsNextTaskTests.cs
@@ -94,7 +94,7 @@ public async Task NextTask_HappyPath_TransitionsFirstNonDoneTask()
result.ContainerTitle.ShouldBe("Issue 1");
result.ContainerType.ShouldBe("Issue");
result.RemainingCount.ShouldBe(2);
- result.CurrentPg.ShouldBe("PG-1");
+ result.CurrentMergeGroup.ShouldBe("PG-1");
result.AdoWorkspace.ShouldBe("org/proj");
result.BranchName.ShouldStartWith("feature/100-");
}
@@ -143,7 +143,7 @@ public async Task NextTask_PgNumberDerivesPgName()
() => cmd.NextTask(workItem: 100, pgNumber: 3));
var result = Deserialize(output);
- result.CurrentPg.ShouldBe("PG-3");
+ result.CurrentMergeGroup.ShouldBe("PG-3");
result.PrimaryId.ShouldBe(300);
}
diff --git a/tests/Polyphony.Tests/Commands/BranchCommandsRouteTests.cs b/tests/Polyphony.Tests/Commands/BranchCommandsRouteTests.cs
index 7cd6734b..b4241736 100644
--- a/tests/Polyphony.Tests/Commands/BranchCommandsRouteTests.cs
+++ b/tests/Polyphony.Tests/Commands/BranchCommandsRouteTests.cs
@@ -87,9 +87,9 @@ public async Task Route_NoPg_NoBranchNoMergedPr_EmitsCreateBranchPg1()
var result = Deserialize(output);
result.Action.ShouldBe("create_branch", $"Output was: {output}");
- result.CurrentPg.ShouldBe("PG-1");
+ result.CurrentMergeGroup.ShouldBe("PG-1");
result.ChildIds.ShouldContain(200);
- result.TotalPgs.ShouldBe(1);
+ result.TotalMergeGroups.ShouldBe(1);
}
[Fact]
@@ -114,7 +114,7 @@ public async Task Route_OpenPrMatchesBranch_EmitsSubmitPr()
result.Action.ShouldBe("submit_pr", $"Output was: {output}");
result.PrNumber.ShouldBe(42);
result.PrUrl.ShouldBe("https://example.com/pr/42");
- result.CurrentPg.ShouldBe("PG-1");
+ result.CurrentMergeGroup.ShouldBe("PG-1");
}
[Fact]
@@ -139,8 +139,8 @@ public async Task Route_MergedPrAndItemsTerminal_EmitsAllComplete()
var result = Deserialize(output);
result.Action.ShouldBe("all_complete", $"Output was: {output}");
- result.CompletedPgs.ShouldContain("PG-1");
- result.RemainingPgs.ShouldBeEmpty();
+ result.CompletedMergeGroups.ShouldContain("PG-1");
+ result.RemainingMergeGroups.ShouldBeEmpty();
}
[Fact]
@@ -166,7 +166,7 @@ public async Task Route_MergedPrButContainerStillProposed_EmitsCreateBranchAsSta
var result = Deserialize(output);
result.Action.ShouldBe("create_branch", $"Output was: {output}");
- result.CurrentPg.ShouldBe("PG-1");
+ result.CurrentMergeGroup.ShouldBe("PG-1");
}
[Fact]
@@ -190,9 +190,9 @@ public async Task Route_PgNumberScoping_OverridesFirstNonComplete()
var (_, output) = await CaptureConsoleAsync(() => cmd.Route(workItem: 100, pgNumber: 2));
var result = Deserialize(output);
- result.CurrentPg.ShouldBe("PG-2", $"Output was: {output}");
+ result.CurrentMergeGroup.ShouldBe("PG-2", $"Output was: {output}");
result.ChildIds.ShouldContain(201);
- result.TotalPgs.ShouldBe(2);
+ result.TotalMergeGroups.ShouldBe(2);
}
[Fact]
@@ -215,8 +215,8 @@ public async Task Route_AllPgsComplete_EmitsAllComplete()
var result = Deserialize(output);
result.Action.ShouldBe("all_complete", $"Output was: {output}");
- result.CurrentPg.ShouldBe("");
- result.CompletedPgs.ShouldContain("PG-1");
+ result.CurrentMergeGroup.ShouldBe("");
+ result.CompletedMergeGroups.ShouldContain("PG-1");
}
[Fact]
diff --git a/tests/Polyphony.Tests/Commands/JsonOutputContractTests.cs b/tests/Polyphony.Tests/Commands/JsonOutputContractTests.cs
index 4924f4aa..108e7da2 100644
--- a/tests/Polyphony.Tests/Commands/JsonOutputContractTests.cs
+++ b/tests/Polyphony.Tests/Commands/JsonOutputContractTests.cs
@@ -125,6 +125,9 @@ await SeedAsync(
AssertNoPascalCase(output, "Message");
AssertNoPascalCase(output, "WorkspaceHint");
AssertNoPascalCase(output, "FeatureBranch");
+ // The C# property is now named MergeGroupBranch but the JSON wire
+ // key is still "pg_branch" — assert the new C# name doesn't leak.
+ AssertNoPascalCase(output, "MergeGroupBranch");
AssertNoPascalCase(output, "PgBranch");
}
@@ -143,8 +146,9 @@ await SeedAsync(
var result = JsonSerializer.Deserialize(output, PolyphonyJsonContext.Default.RouteResult);
result.ShouldNotBeNull();
- // If pg_branch is null, it should be absent from the raw JSON
- if (result.WorkspaceHint?.PgBranch is null)
+ // If MergeGroupBranch (legacy JSON key "pg_branch") is null, it
+ // should be absent from the raw JSON.
+ if (result.WorkspaceHint?.MergeGroupBranch is null)
{
output.ShouldNotContain("\"pg_branch\"");
}
@@ -482,7 +486,7 @@ await SeedAsync(
public void SchemaRenames_JsonContract_FieldsPresent()
{
// Arrange: create dummy objects for serialization
- var prGroup = new Polyphony.PullRequestGroup
+ var mergeGroup = new Polyphony.MergeGroup
{
ChildIds = new[] { 1, 2 },
WorkItemIds = new[] { 10, 20 },
@@ -495,7 +499,7 @@ public void SchemaRenames_JsonContract_FieldsPresent()
Completed = true,
NeedsReconciliation = false
};
- var pgRecon = new Polyphony.PgReconciliation
+ var mergeGroupRecon = new Polyphony.MergeGroupReconciliation
{
NonDoneChildIds = new[] { 5 },
StaleDoingChildIds = new[] { 6 },
@@ -517,46 +521,57 @@ public void SchemaRenames_JsonContract_FieldsPresent()
var routeResult = new Polyphony.BranchRouteResult
{
Action = "create_branch",
- CurrentPg = "PG-1",
+ CurrentMergeGroup = "PG-1",
BranchName = "feature/pg-1",
WorkItemIds = new[] { 10, 20 },
ChildIds = new[] { 1, 2 },
PrNumber = 1,
PrUrl = "url",
- CompletedPgs = new[] { "PG-1" },
- RemainingPgs = new[] { "PG-2" },
- TotalPgs = 2,
+ CompletedMergeGroups = new[] { "PG-1" },
+ RemainingMergeGroups = new[] { "PG-2" },
+ TotalMergeGroups = 2,
AdoWorkspace = "org/proj",
Error = null
};
// Act
- var prGroupJson = JsonSerializer.Serialize(prGroup, PolyphonyJsonContext.Default.PullRequestGroup);
- var pgReconJson = JsonSerializer.Serialize(pgRecon, PolyphonyJsonContext.Default.PgReconciliation);
+ var mergeGroupJson = JsonSerializer.Serialize(mergeGroup, PolyphonyJsonContext.Default.MergeGroup);
+ var mergeGroupReconJson = JsonSerializer.Serialize(mergeGroupRecon, PolyphonyJsonContext.Default.MergeGroupReconciliation);
var seedReconJson = JsonSerializer.Serialize(seedRecon, PolyphonyJsonContext.Default.SeedReconciliation);
var seedErrorJson = JsonSerializer.Serialize(seedError, PolyphonyJsonContext.Default.SeedError);
var routeResultJson = JsonSerializer.Serialize(routeResult, PolyphonyJsonContext.Default.BranchRouteResult);
- // Assert: JSON field names are stable and correct
- prGroupJson.ShouldContain("\"child_ids\"");
- prGroupJson.ShouldContain("\"work_item_ids\"");
- prGroupJson.ShouldContain("\"non_done_child_ids\"");
- prGroupJson.ShouldContain("\"stale_doing_child_ids\"");
- prGroupJson.ShouldContain("\"non_done_work_item_ids\"");
- pgReconJson.ShouldContain("\"non_done_child_ids\"");
- pgReconJson.ShouldContain("\"stale_doing_child_ids\"");
- pgReconJson.ShouldContain("\"non_done_work_item_ids\"");
+ // Assert: JSON field names are stable and correct (legacy snake_case
+ // wire keys preserved via [JsonPropertyName] until the workflow
+ // rewire PR ships).
+ mergeGroupJson.ShouldContain("\"child_ids\"");
+ mergeGroupJson.ShouldContain("\"work_item_ids\"");
+ mergeGroupJson.ShouldContain("\"non_done_child_ids\"");
+ mergeGroupJson.ShouldContain("\"stale_doing_child_ids\"");
+ mergeGroupJson.ShouldContain("\"non_done_work_item_ids\"");
+ mergeGroupReconJson.ShouldContain("\"non_done_child_ids\"");
+ mergeGroupReconJson.ShouldContain("\"stale_doing_child_ids\"");
+ mergeGroupReconJson.ShouldContain("\"non_done_work_item_ids\"");
seedReconJson.ShouldContain("\"child_id\"");
seedErrorJson.ShouldContain("\"child_id\"");
routeResultJson.ShouldContain("\"work_item_ids\"");
routeResultJson.ShouldContain("\"child_ids\"");
+ // Bridge proof: the renamed C# properties still emit the legacy
+ // snake_case keys consumers expect.
+ routeResultJson.ShouldContain("\"current_pg\"");
+ routeResultJson.ShouldContain("\"completed_pgs\"");
+ routeResultJson.ShouldContain("\"remaining_pgs\"");
+ routeResultJson.ShouldContain("\"total_pgs\"");
// Assert: C# property names are not leaked
- prGroupJson.ShouldNotContain("NonDoneChildIds");
- prGroupJson.ShouldNotContain("NonDoneWorkItemIds");
- prGroupJson.ShouldNotContain("StaleDoingChildIds");
- prGroupJson.ShouldNotContain("ChildIds");
+ mergeGroupJson.ShouldNotContain("NonDoneChildIds");
+ mergeGroupJson.ShouldNotContain("NonDoneWorkItemIds");
+ mergeGroupJson.ShouldNotContain("StaleDoingChildIds");
+ mergeGroupJson.ShouldNotContain("ChildIds");
routeResultJson.ShouldNotContain("WorkItemIds");
routeResultJson.ShouldNotContain("ChildIds");
+ routeResultJson.ShouldNotContain("CurrentMergeGroup");
+ routeResultJson.ShouldNotContain("CompletedMergeGroups");
+ routeResultJson.ShouldNotContain("RemainingMergeGroups");
}
// =========================================================================
diff --git a/tests/Polyphony.Tests/Routing/BranchNameResolverTests.cs b/tests/Polyphony.Tests/Routing/BranchNameResolverTests.cs
index 2b78ff94..fe0c5db3 100644
--- a/tests/Polyphony.Tests/Routing/BranchNameResolverTests.cs
+++ b/tests/Polyphony.Tests/Routing/BranchNameResolverTests.cs
@@ -56,7 +56,7 @@ public void Resolve_WithSlugPlaceholder_GeneratesSlugFromTitle()
}
[Fact]
- public void Resolve_PgBranch_SubstitutesPlaceholders()
+ public void Resolve_MergeGroupBranch_SubstitutesPlaceholders()
{
var config = new ProcessConfigBuilder()
.WithType("Epic", ["plannable"])
@@ -67,7 +67,10 @@ public void Resolve_PgBranch_SubstitutesPlaceholders()
var hint = BranchNameResolver.Resolve(config, item);
hint.ShouldNotBeNull();
- hint.PgBranch.ShouldBe("feature/50-pg-test");
+ // The YAML config key (BranchStrategy.PgBranch) is preserved per
+ // the compatibility-bridge plan; the WorkspaceHint surface is now
+ // MergeGroupBranch even though the JSON wire key remains pg_branch.
+ hint.MergeGroupBranch.ShouldBe("feature/50-pg-test");
}
[Fact]