Portfolio Engineering Review Skill
You are an agent that completes portfolio engineering review tasks by interacting with a shared HTTP API environment. Follow these instructions to read the task prompt and answer template, query the environment, compute results, and produce a correctly formatted JSON answer.
1. Read the Input Files
Each task directory contains:
prompt.txt — the task instructions, including scope parameters (teams, quarter, release id, as-of date, etc.)
payloads/answer_template.json — the required JSON output schema
Read both files. The prompt.txt defines the business scope. The answer_template.json is a JSON Schema that defines every required field, its type, enum constraints, and ordering/naming conventions. Your output must validate against this schema exactly.
2. Connect to the Environment
The task environment is a REST API. Read environment_access.md from the workspace root for the base URL, allowed endpoints, and credentials.
Standard configuration:
- Base URL:
http://task-env:9024/
- API endpoints are read-only GET except
POST /api/query for SQL queries
- For
POST /api/query, include header X-Env-Token: portfolio-readonly
Available GET endpoints:
| Endpoint |
Returns |
GET /api/work-items |
All work items (paginated list) |
GET /api/work-items/{item_id} |
Single work item detail |
GET /api/mix-targets |
Portfolio mix target rows |
GET /api/sla-policy |
SLA policy configuration |
GET /api/releases |
All releases |
GET /api/releases/{release_id} |
Single release detail |
GET /api/milestones |
All milestones |
GET /api/dependencies |
Dependency records between work items |
GET /api/blockers |
Blocker records on work items |
POST endpoint:
| Endpoint |
Purpose |
POST /api/query |
Run a restricted SQL query against the environment database. Send header X-Env-Token: portfolio-readonly. |
Use curl or an HTTP library to call these endpoints. Fetch all relevant data before computing results.
3. Task Type Reference
There are three distinct review types. Match your approach to the prompt's intent.
3A. Portfolio Mix Review
Goal: Classify closed work items into portfolio categories, compare the resulting count-based mix with target percentages, and identify rebalance actions.
Data sources:
GET /api/work-items — fetch in-scope work items
GET /api/mix-targets — fetch the target mix row for the given scope_id
Scope filtering: Filter work items by the scope parameters from the prompt: teams, product areas, quarter. For quarter filtering, use the work item's closed_at or other date fields — match items that closed within the specified quarter.
Portfolio categories (exactly four):
NewFeature
TechDebt
Reliability
Security
Category assignment priority for conflicting signals:
When a work item has multiple signals pointing to different categories, resolve conflicts using this priority order:
portfolio_category field on the work item (authoritative, if present and valid)
type field — maps to categories via the work item type convention
labels array — check for category-indicating label values
title — as a last resort, scan for category keywords
When two or more plausible signals conflict, prefer the highest-priority signal. If a work item lacks any signal, flag it for review but do not silently assign a category.
Inclusion rules:
- Include only closed work items matching the scope
- Exclude items with status
cancelled or duplicate
- Track excluded items separately in the exclusion flags
- Count each included item exactly once
Duplicate detection:
- A work item is a duplicate if its
status field is duplicate or its type field is duplicate, or if its duplicate_of / primary_item_id field points to another work item
- Duplicate items contribute to
excluded_duplicate_ids
- Duplicates are NOT counted in category counts or percentages
Cancelled items:
- Items with status
cancelled go to excluded_cancelled_ids
- These are NOT counted in category counts or percentages
Stale mirror fields:
- Some work items may have
mirror_status or legacy_category fields — ignore these
- Use only the authoritative work item fields (status, type, portfolio_category, labels, title)
- Always set
ignored_mirror_status_and_legacy_category to true in exclusion flags
Distractor records:
- Records matching the scope (same team/product area/quarter) but not meeting inclusion criteria go to
excluded_distractor_ids
- Common distractors: items that are not closed, items with mismatched scope, items that look in-scope but have a different actual scope
Computing the mix:
- Count included items per category →
category_counts
- Compute percentages:
(category_count / total_included) * 100, rounded to 1 decimal place → category_percentages or actual_pct
- Read target percentages from the mix-targets row for the scope →
target_pct
- Compute gaps:
gap_pct = actual_pct - target_pct, rounded to 1 decimal place
- Identify under-invested categories: those with negative
gap_pct, ordered from most negative to least negative
Follow-up / recommended action:
- If any category has a negative gap: action =
REBALANCE_CAPACITY
primary_category = the category with the largest negative gap
secondary_category = the category with the second largest negative gap (or null if only one)
rationale_code = LARGEST_NEGATIVE_GAP
- If no negative gaps: action =
MAINTAIN_CURRENT_MIX, rationale = NO_NEGATIVE_GAPS
- If data conflicts prevent confident analysis: action =
INVESTIGATE_DATA_QUALITY, rationale = DATA_CONFLICT
- When the answer template includes an
owner_team field on the recommended action: pick the team from scope with the most items in the deficit category
Output ordering:
included_work_item_ids: sort by closed_at ascending, then by id ascending
gap_table / mix_table rows: always in this fixed order: NewFeature, TechDebt, Reliability, Security
under_invested_categories: most negative gap first
excluded_*_ids: sort by closed_at ascending, then id ascending
- Team arrays: sort alphabetically
3B. SLA Aging Audit
Goal: Identify SLA-relevant work items, separate primary from duplicate records, calculate aging, find hotspots, and compute breach rate.
Data sources:
GET /api/work-items — fetch work items for the specified teams
GET /api/sla-policy — fetch SLA policy configuration (deadlines by severity, category rules)
Scope filtering:
- Filter by the teams listed in the prompt
- Filter by the SLA categories (typically
Reliability and Security)
- Use the
as_of date from the prompt as the reference date for all age calculations
- Use
recent_closed_window_days to identify recently-closed items (not overdue if closed within this window)
Primary vs. duplicate:
- Primary records are the canonical work items — use the work item's own status and fields
- Duplicate records have
status: "duplicate" or a duplicate_of / primary_item_id reference pointing to a primary
- Duplicates form clusters: each cluster has one
primary_id and a list of duplicate_ids
- Duplicates are excluded from primary counts (included_primary_ids, overdue counts, aging, missing owners, breach rate)
- Report duplicate clusters in
duplicate_clusters
SLA categories for work items:
When the prompt says to use portfolio category conventions for resolving conflicting signals, apply the same priority as in 3A: portfolio_category field > type field > labels > title.
SLA overdue determination:
- Read SLA policy to get the allowed age (in days) for each severity level (S1, S2, S3, S4)
- For each included primary work item, compute its age:
as_of_date - created_date (in days)
- An item is overdue if
age > sla_deadline_days for its severity level AND it is not closed within the recent window
- Recently closed items (closed within
recent_closed_window_days of as_of) are NOT overdue
Aging distribution:
- Compute age for all included primary items:
age = as_of_date - created_date in days
- Bucket into: 0-3, 4-7, 8-14, 15-30, 31+ days
- Count primary items in each bucket
Team overdue counts:
- Group overdue primary items by team
- Count per team
- List alphabetically by team name
Hotspot detection:
- Group overdue primary items by (team, owner) pairs
- The hotspot is the pair with the most overdue items
- If owner is missing/null/empty, use
UNASSIGNED
- Report:
team, owner, overdue_count
Overdue by severity:
- Group overdue primary items by severity level (S1, S2, S3, S4)
- Count per severity
Escalation queue:
- Sort overdue primary items into escalation order
- Priority: S1 first, then S2, then S3, then S4
- Within the same severity, order by age descending (oldest first)
Missing owners:
- Primary included items with null, empty, or missing
owner / assigned_to field
- List their IDs sorted ascending
Breach rate:
breach_rate = overdue_primary_count / included_primary_count
- Round to exactly 3 decimal places
- If
included_primary_count is 0, breach rate is 0.000
Output ordering:
included_primary_ids: sorted lexicographically ascending
overdue_primary_ids: sorted lexicographically ascending
missing_owner_ids: sorted lexicographically ascending
duplicate_clusters: sorted by primary_id ascending; within each cluster, duplicate_ids sorted lexicographically
team_overdue_counts: teams listed alphabetically
3C. Release Readiness Assessment
Goal: Evaluate whether a release is ready to ship based on milestone completion, blockers, and dependencies.
Data sources:
GET /api/releases/{release_id} — fetch the specific release
GET /api/milestones — fetch all milestones, filter to those belonging to the release
GET /api/work-items — fetch work items linked to the release and its milestones
GET /api/blockers — fetch blocker records for release work items
GET /api/dependencies — fetch dependency chains affecting release work items
Authoritative data rule:
Use release, milestone, and work item endpoints as the source of truth. Do not rely on stale mirror/export fields that may appear on work items. The release endpoint's data is authoritative for release state; the milestone endpoint's data is authoritative for milestone state.
Milestone completion:
- For each milestone in the release, identify all primary work items linked to it
- Count how many of those work items are in a completed/closed status →
complete_primary
- Count total primary work items for the milestone →
primary_total
- Compute:
completion_pct = (complete_primary / primary_total) * 100, rounded to 1 decimal place
- If
primary_total is 0, completion_pct is 100.0 (an empty milestone is trivially complete)
- Sort
milestone_completion array by milestone_id ascending
Gating work items:
- Work items linked to the release that are NOT in a completed/closed status
- These items block release readiness
- List their IDs sorted ascending, no duplicates
- An empty list is valid if all release work items are complete
Blocker analysis:
- Fetch blocker records linked to release work items
- Filter to high-impact blockers only (check blocker
impact or severity field for "high" or equivalent)
- Filter to unresolved blockers (status is not "resolved" or equivalent)
- Group by exact
cause text string
- Count occurrences per cause
- Report as
blocker_cause_counts: an object keyed by exact cause text → integer count
- An empty object
{} is valid if no unresolved high-impact blockers exist
Critical dependency chains:
- A dependency chain exists when a release work item
depends_on another work item
- Follow the chain transitively: if A depends on B, and B depends on C, the chain is [A, B, C]
- A chain is "critical" if the final item in the chain is non-complete
- Each chain is an ordered array of work item IDs: from the blocked release item → through intermediate items → to the non-complete dependency at the end
- Sort chains lexicographically by the full path (compare each element in sequence)
- An empty array
[] is valid if no critical chains exist
Ship decision:
SHIP — all milestones at 100%, no gating work items, no unresolved high-impact blockers, no critical dependency chains
NO_SHIP — one or more milestones below 100%, OR gating work items present, OR unresolved high-impact blockers present, OR critical dependency chains exist
SHIP_WITH_WATCH — all milestones at or near 100% with only minor gaps, no critical blockers, but some watch items exist (e.g., low-impact blockers or minor dependency concerns)
Readiness score:
readiness_score = total_completed_primary / total_primary_denominator
- Where
total_completed_primary = sum of complete_primary across all milestones
- Where
total_primary_denominator = sum of primary_total across all milestones
- Round to exactly 3 decimal places
- If denominator is 0, score is 1.000
4. General Data Conventions
Sorting Rules
Apply these consistently unless the answer template specifies otherwise:
- Work item ID lists: sort lexicographically (string sort, not numeric)
- Team names: sort alphabetically
- Category rows (gap_table / mix_table): always in fixed order: NewFeature, TechDebt, Reliability, Security
- Under-invested categories: most negative gap first (ascending by gap_pct)
- Duplicate clusters: by
primary_id ascending; duplicate_ids within each cluster sorted ascending
- Milestone completion: by
milestone_id ascending
- Dependency chains: lexicographically by the full ID path
- Escalation queue: by severity (S1 > S2 > S3 > S4), then by age descending within severity
Rounding Rules
- Percentages (completion_pct, actual_pct, target_pct, gap_pct): round to 1 decimal place
- Rates (breach_rate, readiness_score, sla_breach_rate): round to 3 decimal places
- Use standard rounding (half-up): e.g., 0.6666... rounds to 0.667 at 3 decimal places; 33.333... rounds to 33.3 at 1 decimal place
ID Format
Work item IDs follow the pattern WI-24024-XXXX where XXXX may be numeric or alphanumeric with a letter prefix (e.g., WI-24024-P###, WI-24024-S###, WI-24024-###). Sort these as strings.
Primary vs. Non-Primary
The environment may contain different categories of non-primary records:
- Duplicates: records with
status: "duplicate" or pointing to another item via duplicate_of / primary_item_id
- Cancelled: records with
status: "cancelled"
- Distractors: records in scope by team/quarter but not meeting all inclusion criteria
Always separate primary from non-primary before computing counts, percentages, or rates.
5. Querying with POST /api/query
When the GET endpoints don't provide sufficient filtering and you need to query across multiple dimensions, use POST /api/query:
curl -s -X POST http://task-env:9024/api/query \
-H "Content-Type: application/json" \
-H "X-Env-Token: portfolio-readonly" \
-d '{"query": "SELECT ... FROM ... WHERE ..."}'
The database schema mirrors the API resource model. Common tables include work_items, mix_targets, sla_policy, releases, milestones, dependencies, blockers. Inspect the GET responses to understand column names and types before writing queries.
Use this endpoint when you need to:
- Filter work items by multiple criteria simultaneously (team + quarter + category)
- Join across resources (e.g., work items with milestones)
- Find duplicates referencing primary items
- Compute aggregations server-side
6. Output Requirements
- Return only the JSON object — no explanatory prose before or after
- Every key in the answer template schema must be present
- No additional keys beyond what the schema defines (
additionalProperties: false)
- All
required fields must be populated
- Enum values must match exactly (case-sensitive)
- Array lengths must respect
minItems / maxItems constraints
- String patterns must match (e.g., work item ID format)
- Use
const values from the schema exactly as specified
7. Common Pitfalls
- Don't count duplicates as primary work. Always check status and duplicate_of/primary_item_id fields first.
- Don't trust stale mirror fields. Some work items have
mirror_status or legacy_category — use the authoritative fields instead.
- Don't round intermediate values. Only round the final output values as specified. Compute percentages from exact counts.
- Don't assume all items in a team's scope are primary. Filter by status (closed for portfolio mix, non-duplicate for SLA).
- Don't include cancelled items in counts. They are exclusions.
- Don't sort work item IDs numerically. Use lexicographic (string) ordering unless specifically told otherwise.
- Don't reorder fixed category arrays. NewFeature, TechDebt, Reliability, Security always in that order.
- Don't omit empty arrays/objects. If there are no duplicates, blockers, or dependency chains, return
[] or {} — never omit the key.
- Don't compute SLA age from the wrong date. Age =
as_of - created_date. Don't use updated_date or closed_date for age.
- Don't treat recently-closed items as overdue. Items closed within the recent window are resolved, not overdue.
1---2name: fewshot-attempt-02-503description: Portfolio Engineering Review Skill4---5# Portfolio Engineering Review Skill67You are an agent that completes portfolio engineering review tasks by interacting with a shared HTTP API environment. Follow these instructions to read the task prompt and answer template, query the environment, compute results, and produce a correctly formatted JSON answer.89---1011## 1. Read the Input Files1213Each task directory contains:14- `prompt.txt` — the task instructions, including scope parameters (teams, quarter, release id, as-of date, etc.)15- `payloads/answer_template.json` — the required JSON output schema1617Read both files. The `prompt.txt` defines the business scope. The `answer_template.json` is a JSON Schema that defines every required field, its type, enum constraints, and ordering/naming conventions. **Your output must validate against this schema exactly.**1819---2021## 2. Connect to the Environment2223The task environment is a REST API. Read `environment_access.md` from the workspace root for the base URL, allowed endpoints, and credentials.2425Standard configuration:26- Base URL: `http://task-env:9024/`27- API endpoints are read-only GET except `POST /api/query` for SQL queries28- For `POST /api/query`, include header `X-Env-Token: portfolio-readonly`2930**Available GET endpoints:**31| Endpoint | Returns |32|---|---|33| `GET /api/work-items` | All work items (paginated list) |34| `GET /api/work-items/{item_id}` | Single work item detail |35| `GET /api/mix-targets` | Portfolio mix target rows |36| `GET /api/sla-policy` | SLA policy configuration |37| `GET /api/releases` | All releases |38| `GET /api/releases/{release_id}` | Single release detail |39| `GET /api/milestones` | All milestones |40| `GET /api/dependencies` | Dependency records between work items |41| `GET /api/blockers` | Blocker records on work items |4243**POST endpoint:**44| Endpoint | Purpose |45|---|---|46| `POST /api/query` | Run a restricted SQL query against the environment database. Send header `X-Env-Token: portfolio-readonly`. |4748Use `curl` or an HTTP library to call these endpoints. Fetch all relevant data before computing results.4950---5152## 3. Task Type Reference5354There are three distinct review types. Match your approach to the prompt's intent.5556### 3A. Portfolio Mix Review5758**Goal:** Classify closed work items into portfolio categories, compare the resulting count-based mix with target percentages, and identify rebalance actions.5960**Data sources:**61- `GET /api/work-items` — fetch in-scope work items62- `GET /api/mix-targets` — fetch the target mix row for the given `scope_id`6364**Scope filtering:** Filter work items by the scope parameters from the prompt: teams, product areas, quarter. For quarter filtering, use the work item's `closed_at` or other date fields — match items that closed within the specified quarter.6566**Portfolio categories (exactly four):**67- `NewFeature`68- `TechDebt`69- `Reliability`70- `Security`7172**Category assignment priority for conflicting signals:**73When a work item has multiple signals pointing to different categories, resolve conflicts using this priority order:741. **`portfolio_category` field** on the work item (authoritative, if present and valid)752. **`type` field** — maps to categories via the work item type convention763. **`labels` array** — check for category-indicating label values774. **`title`** — as a last resort, scan for category keywords7879When two or more plausible signals conflict, prefer the highest-priority signal. If a work item lacks any signal, flag it for review but do not silently assign a category.8081**Inclusion rules:**82- Include only **closed** work items matching the scope83- Exclude items with status `cancelled` or `duplicate`84- Track excluded items separately in the exclusion flags85- Count each included item exactly once8687**Duplicate detection:**88- A work item is a duplicate if its `status` field is `duplicate` or its `type` field is `duplicate`, or if its `duplicate_of` / `primary_item_id` field points to another work item89- Duplicate items contribute to `excluded_duplicate_ids`90- Duplicates are NOT counted in category counts or percentages9192**Cancelled items:**93- Items with status `cancelled` go to `excluded_cancelled_ids`94- These are NOT counted in category counts or percentages9596**Stale mirror fields:**97- Some work items may have `mirror_status` or `legacy_category` fields — ignore these98- Use only the authoritative work item fields (status, type, portfolio_category, labels, title)99- Always set `ignored_mirror_status_and_legacy_category` to `true` in exclusion flags100101**Distractor records:**102- Records matching the scope (same team/product area/quarter) but not meeting inclusion criteria go to `excluded_distractor_ids`103- Common distractors: items that are not closed, items with mismatched scope, items that look in-scope but have a different actual scope104105**Computing the mix:**1061. Count included items per category → `category_counts`1072. Compute percentages: `(category_count / total_included) * 100`, rounded to 1 decimal place → `category_percentages` or `actual_pct`1083. Read target percentages from the mix-targets row for the scope → `target_pct`1094. Compute gaps: `gap_pct = actual_pct - target_pct`, rounded to 1 decimal place1105. Identify under-invested categories: those with negative `gap_pct`, ordered from most negative to least negative111112**Follow-up / recommended action:**113- If any category has a negative gap: action = `REBALANCE_CAPACITY`114- `primary_category` = the category with the largest negative gap115- `secondary_category` = the category with the second largest negative gap (or null if only one)116- `rationale_code` = `LARGEST_NEGATIVE_GAP`117- If no negative gaps: action = `MAINTAIN_CURRENT_MIX`, rationale = `NO_NEGATIVE_GAPS`118- If data conflicts prevent confident analysis: action = `INVESTIGATE_DATA_QUALITY`, rationale = `DATA_CONFLICT`119- When the answer template includes an `owner_team` field on the recommended action: pick the team from scope with the most items in the deficit category120121**Output ordering:**122- `included_work_item_ids`: sort by `closed_at` ascending, then by `id` ascending123- `gap_table` / `mix_table` rows: always in this fixed order: NewFeature, TechDebt, Reliability, Security124- `under_invested_categories`: most negative gap first125- `excluded_*_ids`: sort by `closed_at` ascending, then `id` ascending126- Team arrays: sort alphabetically127128---129130### 3B. SLA Aging Audit131132**Goal:** Identify SLA-relevant work items, separate primary from duplicate records, calculate aging, find hotspots, and compute breach rate.133134**Data sources:**135- `GET /api/work-items` — fetch work items for the specified teams136- `GET /api/sla-policy` — fetch SLA policy configuration (deadlines by severity, category rules)137138**Scope filtering:**139- Filter by the teams listed in the prompt140- Filter by the SLA categories (typically `Reliability` and `Security`)141- Use the `as_of` date from the prompt as the reference date for all age calculations142- Use `recent_closed_window_days` to identify recently-closed items (not overdue if closed within this window)143144**Primary vs. duplicate:**145- **Primary records** are the canonical work items — use the work item's own status and fields146- **Duplicate records** have `status: "duplicate"` or a `duplicate_of` / `primary_item_id` reference pointing to a primary147- Duplicates form clusters: each cluster has one `primary_id` and a list of `duplicate_ids`148- Duplicates are excluded from primary counts (included_primary_ids, overdue counts, aging, missing owners, breach rate)149- Report duplicate clusters in `duplicate_clusters`150151**SLA categories for work items:**152When the prompt says to use portfolio category conventions for resolving conflicting signals, apply the same priority as in 3A: `portfolio_category` field > `type` field > `labels` > `title`.153154**SLA overdue determination:**1551. Read SLA policy to get the allowed age (in days) for each severity level (S1, S2, S3, S4)1562. For each included primary work item, compute its age: `as_of_date - created_date` (in days)1573. An item is overdue if `age > sla_deadline_days` for its severity level AND it is not closed within the recent window1584. Recently closed items (closed within `recent_closed_window_days` of `as_of`) are NOT overdue159160**Aging distribution:**161- Compute age for all included primary items: `age = as_of_date - created_date` in days162- Bucket into: 0-3, 4-7, 8-14, 15-30, 31+ days163- Count primary items in each bucket164165**Team overdue counts:**166- Group overdue primary items by team167- Count per team168- List alphabetically by team name169170**Hotspot detection:**171- Group overdue primary items by (team, owner) pairs172- The hotspot is the pair with the most overdue items173- If owner is missing/null/empty, use `UNASSIGNED`174- Report: `team`, `owner`, `overdue_count`175176**Overdue by severity:**177- Group overdue primary items by severity level (S1, S2, S3, S4)178- Count per severity179180**Escalation queue:**181- Sort overdue primary items into escalation order182- Priority: S1 first, then S2, then S3, then S4183- Within the same severity, order by age descending (oldest first)184185**Missing owners:**186- Primary included items with null, empty, or missing `owner` / `assigned_to` field187- List their IDs sorted ascending188189**Breach rate:**190- `breach_rate = overdue_primary_count / included_primary_count`191- Round to exactly 3 decimal places192- If `included_primary_count` is 0, breach rate is 0.000193194**Output ordering:**195- `included_primary_ids`: sorted lexicographically ascending196- `overdue_primary_ids`: sorted lexicographically ascending197- `missing_owner_ids`: sorted lexicographically ascending198- `duplicate_clusters`: sorted by `primary_id` ascending; within each cluster, `duplicate_ids` sorted lexicographically199- `team_overdue_counts`: teams listed alphabetically200201---202203### 3C. Release Readiness Assessment204205**Goal:** Evaluate whether a release is ready to ship based on milestone completion, blockers, and dependencies.206207**Data sources:**208- `GET /api/releases/{release_id}` — fetch the specific release209- `GET /api/milestones` — fetch all milestones, filter to those belonging to the release210- `GET /api/work-items` — fetch work items linked to the release and its milestones211- `GET /api/blockers` — fetch blocker records for release work items212- `GET /api/dependencies` — fetch dependency chains affecting release work items213214**Authoritative data rule:**215Use release, milestone, and work item endpoints as the source of truth. Do not rely on stale mirror/export fields that may appear on work items. The release endpoint's data is authoritative for release state; the milestone endpoint's data is authoritative for milestone state.216217**Milestone completion:**2181. For each milestone in the release, identify all primary work items linked to it2192. Count how many of those work items are in a completed/closed status → `complete_primary`2203. Count total primary work items for the milestone → `primary_total`2214. Compute: `completion_pct = (complete_primary / primary_total) * 100`, rounded to 1 decimal place2225. If `primary_total` is 0, `completion_pct` is 100.0 (an empty milestone is trivially complete)2236. Sort `milestone_completion` array by `milestone_id` ascending224225**Gating work items:**226- Work items linked to the release that are NOT in a completed/closed status227- These items block release readiness228- List their IDs sorted ascending, no duplicates229- An empty list is valid if all release work items are complete230231**Blocker analysis:**232- Fetch blocker records linked to release work items233- Filter to **high-impact** blockers only (check blocker `impact` or `severity` field for "high" or equivalent)234- Filter to **unresolved** blockers (status is not "resolved" or equivalent)235- Group by exact `cause` text string236- Count occurrences per cause237- Report as `blocker_cause_counts`: an object keyed by exact cause text → integer count238- An empty object `{}` is valid if no unresolved high-impact blockers exist239240**Critical dependency chains:**241- A dependency chain exists when a release work item `depends_on` another work item242- Follow the chain transitively: if A depends on B, and B depends on C, the chain is [A, B, C]243- A chain is "critical" if the final item in the chain is non-complete244- Each chain is an ordered array of work item IDs: from the blocked release item → through intermediate items → to the non-complete dependency at the end245- Sort chains lexicographically by the full path (compare each element in sequence)246- An empty array `[]` is valid if no critical chains exist247248**Ship decision:**249- `SHIP` — all milestones at 100%, no gating work items, no unresolved high-impact blockers, no critical dependency chains250- `NO_SHIP` — one or more milestones below 100%, OR gating work items present, OR unresolved high-impact blockers present, OR critical dependency chains exist251- `SHIP_WITH_WATCH` — all milestones at or near 100% with only minor gaps, no critical blockers, but some watch items exist (e.g., low-impact blockers or minor dependency concerns)252253**Readiness score:**254- `readiness_score = total_completed_primary / total_primary_denominator`255- Where `total_completed_primary` = sum of `complete_primary` across all milestones256- Where `total_primary_denominator` = sum of `primary_total` across all milestones257- Round to exactly 3 decimal places258- If denominator is 0, score is 1.000259260---261262## 4. General Data Conventions263264### Sorting Rules265Apply these consistently unless the answer template specifies otherwise:266- **Work item ID lists**: sort lexicographically (string sort, not numeric)267- **Team names**: sort alphabetically268- **Category rows** (gap_table / mix_table): always in fixed order: NewFeature, TechDebt, Reliability, Security269- **Under-invested categories**: most negative gap first (ascending by gap_pct)270- **Duplicate clusters**: by `primary_id` ascending; `duplicate_ids` within each cluster sorted ascending271- **Milestone completion**: by `milestone_id` ascending272- **Dependency chains**: lexicographically by the full ID path273- **Escalation queue**: by severity (S1 > S2 > S3 > S4), then by age descending within severity274275### Rounding Rules276- **Percentages** (completion_pct, actual_pct, target_pct, gap_pct): round to **1 decimal place**277- **Rates** (breach_rate, readiness_score, sla_breach_rate): round to **3 decimal places**278- Use standard rounding (half-up): e.g., 0.6666... rounds to 0.667 at 3 decimal places; 33.333... rounds to 33.3 at 1 decimal place279280### ID Format281Work item IDs follow the pattern `WI-24024-XXXX` where XXXX may be numeric or alphanumeric with a letter prefix (e.g., `WI-24024-P###`, `WI-24024-S###`, `WI-24024-###`). Sort these as strings.282283### Primary vs. Non-Primary284The environment may contain different categories of non-primary records:285- **Duplicates**: records with `status: "duplicate"` or pointing to another item via `duplicate_of` / `primary_item_id`286- **Cancelled**: records with `status: "cancelled"`287- **Distractors**: records in scope by team/quarter but not meeting all inclusion criteria288289Always separate primary from non-primary before computing counts, percentages, or rates.290291---292293## 5. Querying with POST /api/query294295When the GET endpoints don't provide sufficient filtering and you need to query across multiple dimensions, use `POST /api/query`:296297```bash298curl -s -X POST http://task-env:9024/api/query \299 -H "Content-Type: application/json" \300 -H "X-Env-Token: portfolio-readonly" \301 -d '{"query": "SELECT ... FROM ... WHERE ..."}'302```303304The database schema mirrors the API resource model. Common tables include `work_items`, `mix_targets`, `sla_policy`, `releases`, `milestones`, `dependencies`, `blockers`. Inspect the GET responses to understand column names and types before writing queries.305306Use this endpoint when you need to:307- Filter work items by multiple criteria simultaneously (team + quarter + category)308- Join across resources (e.g., work items with milestones)309- Find duplicates referencing primary items310- Compute aggregations server-side311312---313314## 6. Output Requirements3153161. Return **only** the JSON object — no explanatory prose before or after3172. Every key in the answer template schema must be present3183. No additional keys beyond what the schema defines (`additionalProperties: false`)3194. All `required` fields must be populated3205. Enum values must match exactly (case-sensitive)3216. Array lengths must respect `minItems` / `maxItems` constraints3227. String patterns must match (e.g., work item ID format)3238. Use `const` values from the schema exactly as specified324325---326327## 7. Common Pitfalls328329- **Don't count duplicates as primary work.** Always check status and duplicate_of/primary_item_id fields first.330- **Don't trust stale mirror fields.** Some work items have `mirror_status` or `legacy_category` — use the authoritative fields instead.331- **Don't round intermediate values.** Only round the final output values as specified. Compute percentages from exact counts.332- **Don't assume all items in a team's scope are primary.** Filter by status (closed for portfolio mix, non-duplicate for SLA).333- **Don't include cancelled items in counts.** They are exclusions.334- **Don't sort work item IDs numerically.** Use lexicographic (string) ordering unless specifically told otherwise.335- **Don't reorder fixed category arrays.** NewFeature, TechDebt, Reliability, Security always in that order.336- **Don't omit empty arrays/objects.** If there are no duplicates, blockers, or dependency chains, return `[]` or `{}` — never omit the key.337- **Don't compute SLA age from the wrong date.** Age = `as_of` - `created_date`. Don't use `updated_date` or `closed_date` for age.338- **Don't treat recently-closed items as overdue.** Items closed within the recent window are resolved, not overdue.