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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md",
"version": "7.6.0",
"version": "7.6.1",
"source": "./plugins/agentic-engineering"
},
{
Expand Down
2 changes: 1 addition & 1 deletion .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md",
"version": "7.6.0",
"version": "7.6.1",
"source": "./plugins/agentic-engineering"
},
{
Expand Down
2 changes: 1 addition & 1 deletion plugins/agentic-engineering/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md",
"version": "7.6.0",
"version": "7.6.1",
"author": {
"name": "devantler-tech",
"url": "https://github.com/devantler-tech"
Expand Down
2 changes: 1 addition & 1 deletion plugins/agentic-engineering/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md",
"version": "7.6.0",
"version": "7.6.1",
"author": {
"name": "devantler-tech",
"url": "https://github.com/devantler-tech"
Expand Down
11 changes: 11 additions & 0 deletions plugins/agentic-engineering/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog — `agentic-engineering`

## 7.6.1 — 2026-11-11

**Fixed** — the default-branch CI classifier no longer loses a failed scheduled workflow when
`main` moves on. A scheduled run uses whatever head `main` had at its cron time, so the next merge
used to hide its failure. The classifier now also reads the branch's failed, timed-out and
startup-failed scheduled runs of the last 31 days on any head, plus each such workflow's newest
scheduled success, and keeps the workflow red until a later run of it succeeds. That costs three
reads per repository plus one per failing scheduled workflow, and an incomplete or capped answer
still means unknown. A scheduled workflow whose last failure is older than 31 days is not seen, and
manually dispatched or managed runs keep the current-head rule (#669).

## 7.6.0 — 2026-11-11

**Added** — two first-party skills for the rules around a product, both written from the
Expand Down
2 changes: 2 additions & 0 deletions plugins/agentic-engineering/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,8 @@ under the guard:
`--repo`, `--branch`, and a full `--head-sha`, and refuses the helper's offline `--input` mode.
The helper rejects unrecognized outcomes, contradictory status/conclusion pairs, repeated run
identities and invalid numeric identifiers before selecting the latest workflow result.
It also reads the branch's red scheduled runs of the last 31 days on any head, and each such
workflow's newest scheduled success, so a scheduled failure is not hidden by a later merge.
Each selector is accepted once; repeated or empty values refuse before collection.
- `scripts/count-unresolved-review-threads.sh` supplies the surveyor's unresolved-thread count. The
guard accepts only `--repo` and a positive `--pr`, and only when the helper runs alone: its
Expand Down
15 changes: 12 additions & 3 deletions plugins/agentic-engineering/agents/portfolio-surveyor.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -529,8 +529,9 @@ and decides.
### 4. CI red on the default branch (bounded, per-repo)

Judge the default branch by **its current head**, and only by runs that represent that branch's
health. Resolve the head first, using the **full-length sha** — a runs endpoint typically returns an
empty set for an abbreviated one, which reads exactly like "nothing failed". Then invoke the shipped
health. Scheduled runs are the one deliberate exception, handled inside the helper (below).
Resolve the head first, using the **full-length sha** — a runs endpoint typically returns an empty set
for an abbreviated one, which reads exactly like "nothing failed". Then invoke the shipped
[`../scripts/classify-default-branch-ci-runs.sh`](../scripts/classify-default-branch-ci-runs.sh),
using the literal absolute path supplied by preflight. If preflight supplied no path, submit
**one** bare `classify-default-branch-ci-runs.sh` probe through the active read-only guard. The
Expand All @@ -548,6 +549,12 @@ itself exits 2 on it, so every executable invocation must carry the resolved pat
flags. **Do not reimplement the helper** inline. It owns the
paginated API call in memory as well as classification, so a later-page API failure cannot be masked
by a successful consumer of partial output and the read-only role never writes an intermediate file.
The helper also reads the branch's red scheduled runs of the last 31 days, whatever head they ran
on, because a scheduled workflow runs against whatever head the branch had at its cron time. Such a
failure stays red after later merges until a later run of the same workflow succeeds: its next
successful scheduled run, or a successful branch-level run of it on the current head. A scheduled
failure older than 31 days is not seen, and `workflow_dispatch` and `dynamic` runs keep the
current-head rule. A scheduled row the helper returns is current breakage, not a stale run.
The bundled `forge-readonly-guard.sh` recognises the classifier only as this exact installed sibling;
offline `--input` remains denied. Exit 0 is a complete classification; exit 2 means `unknown`, never green.

Expand Down Expand Up @@ -588,7 +595,9 @@ different run. Red conclusions are `failure`, `timed_out`, and `startup_failure`
All four filters are load-bearing, each against a different false positive:

- **Not keyed to head** — a failed run stays attached to the sha it ran against and lingers long
after the branch moved on, surfacing days-old failures as live breakage.
after the branch moved on, surfacing days-old failures as live breakage. Scheduled runs are the
exception: the helper keeps the latest result of each scheduled workflow, so a newer success
supersedes an old failure and an unanswered failure is not stale.
- **Not a branch-level event** — a pull-request- or comment-triggered workflow can carry the same
head sha and branch name while testing a PR. Do **not** instead de-duplicate check-runs by name:
several independent comment-triggered runs coexist at one sha, so "newest per name" hides a genuine
Expand Down
2 changes: 1 addition & 1 deletion plugins/agentic-engineering/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "agentic-engineering",
"description": "The autonomous engineering system for repository portfolios and everything its agents need — engineer, read-only surveyor, meta-engineer and Flux troubleshooter agents; portfolio, product, spend and improvement workflows; GitHub, CodeRabbit review, security, library documentation, Go, Kubernetes, Flux, Talos, SvelteKit frontend, Stripe payments, engineering-practice, design, product-management, marketing, finance and productivity skills; and the MCP servers they use; configured by the consumer AGENTS.md",
"version": "7.6.0",
"version": "7.6.1",
"author": {
"name": "devantler-tech",
"url": "https://github.com/devantler-tech"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
{
"path": "scripts/classify-default-branch-ci-runs.sh",
"sha256": "9bb0ac5ae3705b7581e1da4a64d962e85a8e9de5a3c87cdc761d333625149de2",
"sha256": "0ad57f0653c45c12c8df82c1ab4e42c3787d90e656c3f0f497398b58f3dbd856",
"executable": true
},
{
Expand Down Expand Up @@ -76,7 +76,7 @@
"portfolio-surveyor": {
"enabled": true,
"mode": "delegated-read-only",
"definitionSha256": "e97a38eb100d87e852614cd87abf8eeee60501ee28b0eac323a538b8abaee99b"
"definitionSha256": "718e2058a2a547e108e807fabd53ccf7cf7ce7a21be37d7045ac4ba7c2d6f184"
},
"agent-improver": {
"enabledWhen": "Both optional consumer contract sections are present",
Expand Down
165 changes: 154 additions & 11 deletions plugins/agentic-engineering/scripts/classify-default-branch-ci-runs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,20 @@
# consumer and the read-only surveyor never writes a response file:
# classify-default-branch-ci-runs.sh --repo OWNER/REPO --branch BRANCH --head-sha FULL_SHA
#
# Remote mode also reads scheduled runs that are no longer on the current head. A scheduled
# workflow runs against whatever head the branch had at its cron time, so the next merge would
# otherwise hide its failure. Three complete reads list the branch's failed, timed-out and
# startup-failed scheduled runs of the last SCHEDULE_WINDOW_DAYS days; for each workflow found
# there, one first-page read fetches its newest successful scheduled run. Those runs join the
# current-head runs, so a scheduled workflow stays red until a later run of it succeeds.
# Cost per repository: the head pages + 3 + one per workflow with a red scheduled run.
# Blind spots: a scheduled workflow whose latest red run is older than the window, and
# non-push events other than schedule (workflow_dispatch, dynamic), which keep current-head
# semantics.
#
# Fixture/offline mode accepts a flat run array, raw page-envelope stream, or --slurp envelope array:
# classify-default-branch-ci-runs.sh --input PATH
# Offline mode classifies only the runs it is given; it reads no scheduled-run evidence.
#
# Stdout: one TSV line per current red identity —
# workflow_id conclusion html_url name event path created_at run_id run_attempt
Expand Down Expand Up @@ -63,6 +75,136 @@ observer_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P) || exit 2
. "$observer_dir/json-stream.lib.sh" || exit 2
payload_path=""
payload=""
schedule_runs='[]'
readonly SCHEDULE_WINDOW_DAYS=31

# shellcheck disable=SC2016 # jq program; dollar-prefixed names belong to jq
jq_defs='
def identifier: type == "number" and isfinite and . >= 1
and . <= 9007199254740991 and floor == .;

def valid_github_timestamp:
. as $timestamp
| type == "string"
and test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")
and (try ((fromdateiso8601 | strftime("%Y-%m-%dT%H:%M:%SZ")) == $timestamp)
catch false);

def valid_run_shape:
type == "object"
and (.workflow_id | identifier)
and (.id | identifier)
and (.run_attempt | identifier)
and (.created_at | valid_github_timestamp)
and ((.run_started_at // null) == null or (.run_started_at | valid_github_timestamp));

# Keep only the fields the classifier reads, so evidence stays small on argv.
def evidence_record:
{id, run_attempt, workflow_id, event, status, conclusion, created_at,
run_started_at, html_url, name, path, head_branch, head_sha};

# A complete paginated read: every page is an envelope, all pages agree on total_count,
# and together they hold exactly that many runs. A capped result never satisfies this.
def complete_envelope_runs:
if length == 0 then error("empty scheduled runs input") else . end
| [.[] | if type == "object" then . elif type == "array" then .[]
else error("invalid scheduled runs payload") end]
| if length == 0 then error("scheduled runs result requires page envelopes")
elif any(.[]; type != "object" or (has("workflow_runs") | not)
or (.workflow_runs | type != "array")) then
error("invalid scheduled runs page")
elif any(.[]; .total_count | type != "number" or . < 0 or floor != .) then
error("scheduled runs page is missing a valid total_count")
elif (map(.total_count) | unique | length) != 1 then
error("scheduled runs pages disagree on total_count")
elif ([.[].workflow_runs[]] | length) != .[0].total_count then
error("scheduled runs result is incomplete or capped")
else [.[].workflow_runs[]] end;
'

# Input: every page of one red-conclusion scheduled read. Output: its validated runs.
# shellcheck disable=SC2016 # jq program; dollar-prefixed names belong to jq
red_schedule_filter='
complete_envelope_runs
| if any(.[]; (valid_run_shape | not)
or .event != "schedule"
or .head_branch != $expected_branch
or .status != "completed"
or .conclusion != $expected_conclusion
or ((.created_at | fromdateiso8601) < ($since | fromdateiso8601)))
then error("scheduled red run does not match the requested branch, conclusion or window")
elif (map([.id, .run_attempt]) | unique | length) != length then
error("duplicate scheduled run identity")
else map(evidence_record) end
'

# Input: the first page of one workflow newest successful scheduled runs (one run at most).
# shellcheck disable=SC2016 # jq program; dollar-prefixed names belong to jq
success_schedule_filter='
if length != 1 or (.[0] | type != "object")
or (.[0] | has("workflow_runs") | not) or (.[0].workflow_runs | type != "array")
or (.[0].total_count | type != "number" or . < 0 or floor != .)
then error("invalid newest scheduled success page")
else .[0] end
| if (.workflow_runs | length) != ([.total_count, 1] | min) then
error("newest scheduled success page is incomplete")
else .workflow_runs end
| if any(.[]; (valid_run_shape | not)
or .workflow_id != $expected_workflow_id
or .event != "schedule"
or .head_branch != $expected_branch
or .status != "completed"
or .conclusion != "success")
then error("newest scheduled success does not match the requested workflow")
else map(evidence_record) end
'

# Read one GitHub API result into memory, refusing undecodable or ambiguous bytes.
retained_read() {
local raw
raw=$(gh api "$@" | json_stream_retain_raw) || return 2
printf '%s\n' "$raw" | json_stream_unique || return 2
printf '%s\n' "$raw"
}

collect_schedule_evidence() {
local since conclusion page found reds='' successes='' workflow_ids workflow_id
# Evidence accumulates as one JSON array per line and reaches jq on stdin, never argv,
# so a large result cannot hit the argument-size limit.
since=$(jq -nr --argjson days "$SCHEDULE_WINDOW_DAYS" 'now | floor - ($days * 86400) | todate') ||
return 2
for conclusion in failure timed_out startup_failure; do
page=$(retained_read --paginate --slurp --method GET "repos/${repo}/actions/runs" \
-f branch="$branch" \
-f event=schedule \
-f status="$conclusion" \
-f created=">=$since" \
-F per_page=100) || return 2
found=$(printf '%s\n' "$page" | jq -cs \
--arg expected_branch "$branch" \
--arg expected_conclusion "$conclusion" \
--arg since "$since" \
"$jq_defs $red_schedule_filter") || return 2
reds+="$found"$'\n'
done
workflow_ids=$(printf '%s' "$reds" | jq -rs 'add // [] | map(.workflow_id) | unique | .[]') ||
return 2
while IFS= read -r workflow_id; do
[ -n "$workflow_id" ] || continue
[[ "$workflow_id" =~ ^[1-9][0-9]*$ ]] || return 2
page=$(retained_read --method GET "repos/${repo}/actions/workflows/${workflow_id}/runs" \
-f branch="$branch" \
-f event=schedule \
-f status=success \
-F per_page=1) || return 2
found=$(printf '%s\n' "$page" | jq -cs \
--arg expected_branch "$branch" \
--argjson expected_workflow_id "$workflow_id" \
"$jq_defs $success_schedule_filter") || return 2
successes+="$found"$'\n'
done <<<"$workflow_ids"
printf '%s%s' "$reds" "$successes" | jq -cs 'add // []'
}

if [ -n "$input_path" ]; then
if [ -n "$repo" ] || [ -n "$branch" ] || [ -n "$head_sha" ]; then
Expand Down Expand Up @@ -97,6 +239,10 @@ else
echo "classify-default-branch-ci-runs: GitHub Actions pagination failed; health is unknown" >&2
exit 2
fi
if ! schedule_runs=$(collect_schedule_evidence); then
echo "classify-default-branch-ci-runs: scheduled-run evidence is incomplete or malformed; health is unknown" >&2
exit 2
fi
fi

# shellcheck disable=SC2016 # jq program; dollar-prefixed names belong to jq
Expand Down Expand Up @@ -151,9 +297,6 @@ jq_filter='
or .conclusion == "timed_out"
or .conclusion == "startup_failure";

def identifier: type == "number" and isfinite and . >= 1
and . <= 9007199254740991 and floor == .;

def valid_outcome:
has("conclusion") and
(if .conclusion == null then
Expand All @@ -167,13 +310,6 @@ jq_filter='
(if .conclusion == null then .status != "completed" else .status == "completed" end)
else true end);

def valid_github_timestamp:
. as $timestamp
| type == "string"
and test("^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")
and (try ((fromdateiso8601 | strftime("%Y-%m-%dT%H:%M:%SZ")) == $timestamp)
catch false);

if length == 0 then error("empty input") else . end
| [.[] | page_envelopes[]] as $envelopes
| [.[] | page_runs[]] as $runs
Expand Down Expand Up @@ -228,6 +364,12 @@ jq_filter='
elif any(.[]; (valid_outcome | not)) then
error("invalid workflow run outcome")
else . end
# Scheduled runs off the current head join here. A run seen by both reads must agree.
| (. + $schedule_evidence[0])
| group_by([.id, .run_attempt])
| map(if (map([.status, .conclusion]) | unique | length) != 1 then
error("contradictory observations of one workflow run attempt")
else .[0] end)
| group_by(run_identity)
| map(
sort_by([(.run_started_at // .created_at), .id, .run_attempt])
Expand Down Expand Up @@ -256,7 +398,8 @@ fi
if ! classification=$(printf '%s\n' "$payload" | jq -rs \
--arg expected_head_sha "$head_sha" \
--arg expected_branch "$branch" \
"$jq_filter"); then
--slurpfile schedule_evidence <(printf '%s\n' "$schedule_runs") \
"$jq_defs $jq_filter"); then
echo "classify-default-branch-ci-runs: malformed or incomplete runs payload; health is unknown" >&2
exit 2
fi
Expand Down
Loading
Loading