Skip to content

Pedroro/release scout - #457

Draft
pedro romero vargas (p3dr0rv) wants to merge 67 commits into
masterfrom
pedroro/release-scout
Draft

Pedroro/release scout#457
pedro romero vargas (p3dr0rv) wants to merge 67 commits into
masterfrom
pedroro/release-scout

Conversation

@p3dr0rv

Copy link
Copy Markdown
Contributor

No description provided.

… projects

- Introduced `checks.py` in the `release-agent/tools` directory, implementing various readiness checks including Azure DevOps build definition access, HTTP reachability, and pipeline variable management.
- Updated `settings.gradle` to comment out several project inclusions, including `AcaPlugin`, `LinuxBroker`, `java-linux-test-app`, `LinuxBrokerPackage`, and `NativeAuthSample`, to streamline the build configuration.
…e conditions and ensure safe parallel execution; enhance error messaging for Azure DevOps authentication issues.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

❌ Work item link check failed. Description does not contain AB#{ID}.

Click here to Learn more.

Engine (gate integrity):
- sign: remove blanket --all; CLI now requires explicit --item ids + refuses
  a bare sign, and records each attestation individually with an evidence
  --note. Closes the hole where sign --all attested every human item in one
  blind call (a release reached Phase 0 without real confirmation).
- readiness.sign() carries a per-item evidence note.

Readiness config/UX:
- silent_perms.required_servers now includes kusto + icm (the MCP servers the
  adx_access / oncall_now checks call) so they don't prompt on first run.

Skill (A: dedupe, B: modular split):
- Split the 48KB monolith SKILL.md into a lean 7KB core (golden rules +
  behaviour dispatch + reference routing table) plus skill/reference/*.md read
  on demand (readiness-gate, starting-and-scheduling, presenting-status,
  commands, phases/preflight, phases/_TEMPLATE). Core is now well under Scout's
  inline limit, so it no longer spills / gets skimmed.
- Fix regression where the readiness table was suppressed: render the checklist
  table FIRST, then handle silent-runs (was "before showing the checklist").
- Anti-assumption rule: never attest/approve on an echoed m_ask_user result.
- Scale convention: add a phase = phases.yaml + phases/agents/<id>.py +
  reference/phases/<id>.md + one routing-table row.

bootstrap.ps1: auto-install Python via winget + silent pyyaml; verify Scout
present + skill copy; restart Scout by default (guarded when run inside a Scout
session); UTF-8 + detached-launch fixes; clearer folder/run instructions.

Tests: 101/101 (added regressions for bare-sign refusal, no --all, evidence note).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Recurring regression: the model ran checklist (which auto-logs) but did not
paste the table into its reply, then asked for attestations in a table-less
message — losing the context the user needs.

- Core golden rule #2: running a command != showing it; the user only sees what
  you paste.
- New golden rule #2b: NEVER ask for a gate decision or attestation in a message
  that doesn't contain the freshly-rendered table (in the SAME message).
- readiness-gate.md step 3b: paste the table verbatim into the same message as
  the attestation question.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ecks

Two UX nits from testing:

1. Table rendered twice (once mid-auto-checks, once after). Restructured the
   readiness flow to render the checklist table ONCE, in step 3b, right before
   the attestations — steps 1-3a are now quiet setup (verify + silent-runs +
   scout checks) with no half-done table. Step 1 uses �erify (terse pass/fail)
   instead of checklist --verify (big table).

2. Model asked the user permission to run the ADX/Kusto query even though the
   kusto MCP server is auto-approved (verified: the query runs silently). The
   prompt was the model asking, not Scout. Step 3a now explicitly forbids asking
   permission to run the scout-assisted auto checks — they're auto-approved and
   must run silently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…uctural fix)

Prompt-level 'render the table then ask' instructions were fragile — the model
would run checklist (auto-logged) but not paste it, then ask attestations in a
table-less message. Fix structurally in the engine instead:

- render.readiness_table now appends a '✋ Your confirmation needed' section
  listing the outstanding attest items, but ONLY once every auto item is
  satisfied. The table and the attestation prompt are now a single deterministic
  output — you can't show one without the other, and the prompt can't appear
  before the auto checks are done. Engine controls the ordering, not the model.

Skill readiness flow simplified to match: run all auto checks silently first
(never ask permission to run the ADX/on-call MCP calls — they're auto-approved),
then render checklist ONCE (its output carries the confirmation section) and
fire m_ask_user in the same message. Fixes both nits: single table render, and
no spurious 'can I run the query?' prompt.

Tests: 102/102 (added test_attest_prompt_welded_only_after_auto_done).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…plays)

Root-cause of the recurring 'no table, just a list' regression: it's controlled
by WHERE the checklist render sits in the tool-call sequence, not by wording.

- A checklist table render that is the model's FIRST action is pasted verbatim.
- A render placed AFTER a chain of silent tool calls (verify/record-check/
  m_get_settings) gets summarized into a plain list (model switches to wrap-up
  mode). The first render also primes verbatim-pasting for the second.

So the double-render is the control mechanism, not waste. Codified as a DESIGN
INVARIANT in readiness-gate.md so future edits don't collapse to a single
end-of-flow render and reintroduce the regression.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…wo-render flow)

Now that we understand the render-first mechanism, re-apply the engine welded
prompt WITHOUT the flow reorder that broke display:

- render.readiness_table appends '✋ Your confirmation needed' only once all auto
  items are satisfied. In the working two-render flow this means render #1
  (checklist --verify, scout items still pending) stays a clean table with NO
  prompt, and render #2 (after the silent scout checks) carries the welded
  attestation list. Verified: render1 no prompt, render2 has prompt.
- Keeps the proven display-first two-render flow (render #1 primes verbatim
  paste); adds deterministic attestation context to render #2 from the CLI so
  the model doesn't hand-build the list.
- step 3b notes the confirmation section comes from the CLI.

Tests: 102/102.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lock

User wants the table shown once, not twice. The two outputs are inherent (one
before the silent auto-checks, one after) — the fix is to make the SECOND output
a compact confirmation block, not the full table again.

- render.py: full table no longer carries the welded prompt; new public
  render.attest_prompt(chk) returns just the '✋ Your confirmation needed' block
  (or a status line if auto checks aren't done / gate cleared).
- new CLI mode: checklist --attest-prompt emits ONLY that block (no table).
- Flow: render #1 = full table (display-first, primes verbatim paste); silent
  auto checks; render #2 = checklist --attest-prompt = block only. One table
  total. Verified E2E: render1 has table/no block, render2 has block/no table.
- Invariant doc + step 3b updated to the single-table design.

Tests: 102/102.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mplete table once + deterministic attestation card

Fixes the recurring readiness-display problems with a two-part approach:

1. Deterministic attestation card (removes the LLM from the critical path).
   - render.attest_prompt_payload(chk, release) builds the exact m_ask_user
     payload the engine owns: question + answer cards (1 confirm_all + one
     decline per outstanding attest item) + confirm_items + recommendedIndex.
     ready:false until all auto checks pass. On-call window dates surfaced in
     the confirm_all description.
   - `checklist --attest-prompt --json` returns that payload; the skill passes
     it straight to m_ask_user. The always-rendered Scout card is the guaranteed
     source of truth even if the markdown table doesn't render.

2. Table shown ONCE, AFTER full evaluation (the requested UX).
   - Skill flow reordered: steps 1-3 run all auto checks SILENTLY (Python
     verifiers, silent_perms, on-call, ADX) with no table/partial renders; step
     4 renders the complete checklist table (all [auto] items resolved to the
     final state) with explicit "paste verbatim, do not summarize" framing;
     step 5 presents the deterministic card; steps 6-8 map the answer to
     sign/decline. No more early/incomplete table, no spurious ADX permission
     prompt.

Gate integrity preserved throughout: no --all, explicit per-item sign, never
attest on assumption.

Tests: 103/103 (added test_attest_prompt_payload_is_deterministic_card,
test_attest_prompt_is_separate_render_never_in_table).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The phase map and current-phase steps tables used an empty-header first column
just for the status icon (| | # | Phase | Done |). Scout's table renderer gives
that empty column a huge width, leaving a big gap before the # column.

Fix: drop the standalone icon column and prefix the icon onto the Phase/Step
name — '| 0 | 🗓 Pre-flight … |' instead of '| 🗓 | 0 | Pre-flight … |'. Clean
3-/2-column tables, no empty gap. (Readiness table left as-is — user-validated.)

Tests: 103/103.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Give every Phase-0 step ONE home under steps/<phase>/ authoring a single
build(state) -> Outcome, replacing the split where scout steps were scattered
across phases.yaml + stub_runner + commands/*.py + templates + skill md.

Outcomes (orchestrator/outcomes.py): Done | Blocked | NeedsHuman | NeedsSkill.
Generic dispatcher `step-action` serializes a migrated step's outcome as JSON so
the skill reacts uniformly (needs_skill -> run tool + record-step; needs_human ->
show prompt + done). Agent steps run in-process during `next` (step-action
refuses them).

Migrated (steps/preflight/):
  scout : notice, flight_reminder, lockdown (gather-then-decide via check-lockdown)
  attest: confirm_reminders, vitals (NeedsHuman prompts)
  agent : breaking, cg, cron, wiki (Done/Blocked, engine-run)

Shared libs: steps/lib/{context,templating,agent}. Legacy homes
(phases/agents/preflight.py, commands/notice.py, commands/lockdown.py) reduced to
thin compatibility/recorder shims so the engine + 103 tests are unchanged.

Also fixes:
- render.py: phase-map legend was glued into the table as a one-column row
  (missing blank line) -> now a separate italic caption.
- skill docs: render the status table ONCE per advance pass (after the work),
  not an interim table before running the scout steps.

103/103 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ils + knowledge

Replace the dry-run boolean with a real-by-default model plus a personal,
gitignored mocks.local.yaml overlay for safe testing:

- Remove dry_run entirely (state/engine/render/cli/steps). Every run makes real
  calls; the fake "[dry-run] Would…" branches are gone.
- Mock overlay (orchestrator/mocks.py + steps/lib/mockctx.py): mocks.local.yaml
  can, per step, force outcome (done|blocked), redirect a real send (send_to),
  or inject inputs so the step's REAL logic runs on your data (cg alerts,
  breaking changelog, cron run, wiki page_name/name_suffix, notice variant).
  Readiness entry-gate auto checks are mockable too (readiness.<item>).
- Each step DECLARES its mockable knobs (MOCKABLE); `mock-spec` lists exactly
  what every step exposes (steps + readiness), with typo detection.

Status "Details" column (render): a third column captures each step's execution
outcome from its stored note — where a lockdown clashed, the breaking change, CG
alerts found, the created wiki link — generic for all current/future steps, with
rich multi-line notes expanding below. Durable refs are now first-class:
StepState.links [{name,url}] (wiki page, CG alerts page + per-alert deep links),
threaded Done/Blocked -> StepResult -> StepState -> render.

Step knowledge base (config/knowledge.yaml + orchestrator/knowledge.py +
`step-info`): curated per-step help (what/where/how/links/faqs) so the skill
answers step questions accurately instead of guessing. Fixes the Play Console
vitals navigation. SKILL.md consults step-info for step questions.

Also: readiness table groups auto items before attest (oncall_now before
play_console_access); breaking-change entries no longer double their bullet.

106/106 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…registries

Make a step a single self-contained module so adding one touches ~2 files and can
never silently drift:

- Auto-discover step modules: steps/__init__.discover() scans steps/<phase>/*.py.
  Remove the hand-maintained _STEPS list.
- Engine resolves agent steps via steps.get_step() (module.run when KIND==agent),
  stub fallback otherwise. DELETE phases/agents/ (package + REGISTRY) and the dead
  `agent:` field from config/phases.yaml — no runner registry, no agent-id indirection.
- Co-locate step CONFIG into each module (CONFIG constant); remove config/preflight.yaml
  and orchestrator/phase_config.py. The module is now the single home for a step's
  logic + mock knobs + config.
- Structural guardrail test: every discovered module maps to a phases.yaml step and
  its KIND matches the config flags — drift fails LOUDLY.

config/phases.yaml is now pure COMPOSITION (which steps, order, deps, flags); a
step's logic/config lives in its module; knowledge stays in config/knowledge.yaml
(a module may override via KNOWLEDGE). README documents the 2-file "adding a step"
contract. Tests point at steps.preflight.* directly; block-path tests use the mock
overlay instead of registry patching.

107/107 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Correct the Component Governance alerts link to the real One/AD-MFA
  phonefactor-phoneApp-android componentGovernance page (in the cg module
  CONFIG and knowledge.yaml).
- Pending scout steps (e.g. lockdown) now render as 'Scout runs this'
  instead of 'Your action' and are no longer flagged as needing the user;
  they only escalate to a blocked user action when a scout step records an
  attention/overlap.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The knowledge base had no ownership fact for the CG step, so 'who fixes
this?' was answered by guessing (wrongly deferring to the component owner).
Record the real rule: the RELEASE OWNER remediates Component Governance
alerts, including authoring and landing the fix PR. Add a 'who' field to
the knowledge schema/renderer and populate it (plus a matching FAQ) for
preflight.cg.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Free-form step questions (what/why/who/how about a step) are exactly the
'interaction' the event log is meant to capture, but nothing journaled
them, so surfacing missing/inaccurate knowledge (e.g. CG ownership) left
no trace. Add a thin two-sided 'step_qa' event: EventLog.qa(), a
'journal --kind qa --question ... --answer ...' path, log-viewer rendering,
and a questions_answered rollup in summarize(). Wire the skill so that,
during an active release, answering a step question from step-info silently
journals the pair (skipped when no release run exists).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ocalization

Build out the first three CCD steps as self-contained scout modules under
steps/ccd/ (auto-discovered), each doing the real work idempotently:

- final_reminder  (email)  — CCD-day 'code complete is TODAY' notice to the
  Android DL (the 'update' variant of the Phase-0 notice template).
- pr_reminder     (Teams)  — NEW step; posts to the 'Code reviews' chat: merge
  every required PR before the 11 PM auto branch-cut; a CCD delay or post-CCD
  cherry-pick both need Moumita Ghosh's (moghosh@) approval; merge string PRs
  before the noon localization run.
- localization    (trigger)— fires pipeline 405133 (msazure/One) with
  isCreatePrSelected=true; PR created => strings to merge, no PR => none.

Each module carries CONFIG (incl. fire_at_local timing), MOCKABLE knobs,
knowledge.yaml entries, and tests. phases.yaml marks the three source: scout
and inserts pr_reminder (P1-1b); branch_cut stays the human gate. Timing is
encoded per-step (09:00 / 12:00) for a per-release CCD automation to drive;
the engine stays date-based. EXTERNAL-REFERENCES catalogs the new chat,
pipeline, and approver. Test harness gains _clear_ccd_scout so flow tests
reach the branch_cut gate. 114/114.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire the timed Phase-1 automations into release-start provisioning, and close
the traceability gap: the registry knew an automation's id/release/purpose but
not WHICH STEPS it drives.

- config/automations.yaml: declarative source of truth mapping each per-release
  automation to the steps it drives. Timing is DERIVED from each step module's
  fire_at_local (no duplication).
- orchestrator/automations.py: plan(release, ccd) emits concrete specs
  (name/schedule/steps/prompt/registration) for the skill to create; validate()
  enforces every fire_at_local step is owned by exactly one automation and each
  automation's steps exist + share one fire time.
- registry: entries now carry steps[]; list(step=...) is the reverse lookup
  (which automation owns a step). �utomation plan CLI action + --step /
  --step-filter flags.
- Guardrail test test_automations_cover_every_scheduled_step makes the mapping
  self-enforcing; plus registry-linkage and plan tests.
- SKILL/commands/scheduling docs: provision timed automations at start
  (register WITH --step), reverse/forward lookup, runtime journaling. 117/117.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'Release push reminders' drives no steps — it advances the whole release via
hourly tick. An empty steps[] read the same as 'not filled in', so label the
category explicitly. Registry entries now carry a 'kind': step-driving (owns
steps) or release-level (whole-release, no steps), auto-derived from whether
steps are present, with a guard that the two can't contradict. kind_of()
derives it for legacy entries. CLI: --kind on register (override) and list
(filter); list shows [kind] and '(release-level - no steps)'. Docs note push
reminders is release-level. 118/118.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Code Complete Day is now three scout steps (final_reminder, pr_reminder,
localization) with NO gate:
- branch_cut: removed — the release branch is cut AUTOMATICALLY at 11 PM,
  there is no manual cut action, so a human 'cut the branch' gate was wrong.
- precheck_prs: removed — there's no reliable signal for which PRs are
  'required' for a release, so an automated pre-check can't be built honestly.
- verify_trigger: removed from Phase 1 — verifying the overnight/auto build
  fits Phase 2 (Build & Lib Verification), not Code Complete Day.

Consequence: the first human gate is now go_test (Phase 2). Updated the flow
tests (first gate go_test, second stop the bug-bash ui_failures hold), the
phase-shape test, the mocks example, and stale comments. 118/118.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make localization a small state machine instead of fire-and-describe:
- TRIGGER: build() runs pipeline 405133 (isCreatePrSelected=true); the runner
  calls record-localization-run to store the build id + start time and leaves
  the step IN-FLIGHT (StepState gains a durable data dict).
- POLL: a per-release "every 10 minutes" poller automation calls
  check-localization; the pure decide() applies:
    * running and under 3h  -> wait, poll again
    * running and over 3h   -> email the release engineer (with the manual-steps
      localization doc) and hold the step
    * complete              -> parse the OneLocBuild@3 log for the
      "Pull request created with ID" line; if found, post that PR to the Code
      reviews chat for review and mark done (with the PR link); else done (no
      strings).
All decision logic is pure/tested; IO is done via NeedsSkill/decision payloads.

Adds: steps/ccd/localization.py rewrite (helpers, decide, KNOWLEDGE incl. the
eng.ms localization doc), orchestrator/commands/localization.py
(record-localization-run, check-localization), StepState.data, an interval
"every N" automation kind in automations.yaml + planner/validate (pollers are
exempt from fire_at_local single-owner accounting; may share a step with a
time-of-day trigger), and docs/refs. 124/124.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ocument orgs

Real-tenant check against build 176407869 (msazure/One) confirmed the ADO MCP
can't reach that org (TF200016) but az can. So:

- localization CONFIG carries the exact az reads (az_read: status via
  `az pipelines build show`; OneLocBuild@3 log id via `az devops invoke ...
  timeline`; log content via `... logs`). build()._trigger and the poller
  automation prompt now spell these out for msazure/One.
- extract_pr() prefers the full PR URL the OneLocBuild@3 log prints after the
  id (real form includes /DefaultCollection/), falling back to the template;
  pr_url_template updated to the DefaultCollection form.
- KNOWLEDGE explains MCP-can't-reach-msazure and the az read path.

Also document the two-org reality in EXTERNAL-REFERENCES: Engineering lives in
identitydivision (ADO MCP bound here); One lives in msazure.visualstudio.com
(az only). 125/125.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ore EOD)

When the completed localization run's PR is posted to the Code reviews chat,
the message now @mentions the release engineer (release owner) and asks them
to ensure the PR is merged before EOD. _review_post builds the Teams mention:
an <at id="0"> tag in the html content matched by a mentions[] entry
(id=owner UPN, userIdentityType=aadUser) that workiq_send_chat_message
consumes. Falls back to a plain (unmentioned) post when no owner email is set.
Tests + KNOWLEDGE updated. 126/126.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two per-step mock knobs (exercised in a real msazure/One test run, build
177042471):
- create_pr (input): overrides isCreatePrSelected on the trigger; set false to
  run the pipeline WITHOUT creating a PR.
- send_to (post): redirects the completion PR post to your own chat ('me').
  Applied by check-localization, since the post happens in the poll decider,
  not build().

step_action's payload-override now only touches kind:payload knobs, leaving
input (build) and post (poller) knobs to their proper stages. 126/126.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… user action cue

Regression from 8d43c03: that commit relabeled pending scout steps as
status="scout" / needs_owner=False (so the user isn't told "Your action"), but
it also removed the signal the SKILL used to know it must EXECUTE them. Result:
scout steps (notice, flight_reminder, lockdown) just waited — the skill saw
"Scout handles this passively, nothing to do" and stalled. The digest even
wrongly announced "Action needed now: Send early release notice" (a scout step
surfaced as a user action).

Fix separates "the skill's automated work" from "a user hold":
- engine: status_report now exposes `scout_pending` (ready scout step ids the
  skill runs via step-action). The parallel dispatch picks a genuine USER hold
  (attest/blocked) for current_step, not a scout step; and `action` is never a
  scout step (guarded), so the digest stops mislabeling it.
- SKILL.md: new dispatch rule — when scout_pending is non-empty, run each via
  step-action (perform the send/scrape, record-step) SILENTLY as Scout's work,
  then re-run next; only then surface the remaining user holds.
- tests: rewrote test_engine_holds_scout_assisted_lockdown to the new contract;
  added scout_pending exposure + digest-no-mislabel regression guards.

138/138. Verified live on release 2026-08: scout_pending=[notice,flight_reminder,
lockdown], digest action is now the real blocked cg step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…atch

Every phase is CCD-relative, so a wrong Code Complete Date misaligns the whole
release. Add a required entry-gate item that reconciles the release CCD against
pipeline 3038's override BEFORE signing — a divergence now blocks (human-resolved),
not just a soft warning.

- New readiness item `ccd_confirmed` (verify: auto, source: scout, NOT opt_out),
  slotted foundational (position 2, after build_access).
- New read-only `check-ccd --json` command: re-reads the pipeline override and
  classifies status match|conflict|unreadable|unset (distinguishes unreadable from
  no-conflict, which refresh_conflict could not) so the skill decides deterministically.
- Skill flow (readiness-gate.md step 3): match -> record pass; conflict -> present
  override vs CCD, user picks, set-ccd --confirm, re-check; unreadable -> attest
  fallback (confirm CCD manually); unset -> gate stays closed.
- Knowledge: readiness.ccd_confirmed (what/who/where/how/faqs). Docs: commands.md.
- Tests: ccd_confirmed added to _pass_scout_checks + the 6 manual gate-clearing
  tests; 2 focused tests (required-scout-item behavior; check-ccd 4-way classify).

140/140 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… warn)

The ccd_confirmed gate only reconciled the CCD against the pipeline — it was
blind to the calendar. Two real defects surfaced in testing:

  * Start a release in the CURRENT month and the 2nd-Wednesday default can be in
    the PAST (e.g. today Aug 18, default Aug 12). You can't code-complete in the
    past, and --default can't fix it (the default IS the past date).
  * Phase 0 opens at CCD-7; a near CCD collapses the 7-day prep window (CCD
    tomorrow => Phase 0 in one day) with no warning.

Make ccd_confirmed the complete "is this CCD good?" gate — temporal + pipeline —
with one reusable source of truth, not a patch:

  * schedule.ccd_viability(ccd, as_of, open_spec="CCD-7"): pure helper (reuses
    anchor_offset) → {days_to_ccd, past, phase0_open, normal_window, runway_days,
    compressed}. past = ccd < today; compressed = inside the CCD-7 window.
  * check-ccd: layers temporal on reconciliation. New status priority
    unset → past → conflict → unreadable → match, always emitting the calendar
    picture. `past` BLOCKS (overrides recon, but keeps a future override so the
    resolver can offer to adopt it); `compressed` is an advisory that rides on any
    non-past status (WARN, never blocks). New --as-of for a testable clock.
  * Gate flow (readiness-gate.md): past → reschedule to a future date (adopt a
    future override if present) then re-check; compressed → warn with runway_days,
    proceed or move the CCD. Item text + knowledge broadened; commands.md updated.
  * Tests: ccd_viability unit (past/compressed/healthy/boundaries) + check-ccd
    past & compressed cases, clock pinned via --as-of.

141/141 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Symptom (release 2026-08, Phase 0): the owner got the daily phase digest while
Scout's own steps — Send early release notice, feature-owner reminders, Detect
lockdown/holiday overlap — hadn't run yet. The email was premature and half-run.

Root cause: the hourly `tick` automation advances only AGENT steps (deterministic,
in-process). Phase-0 SCOUT steps need MCP and are parked as scout_pending for the
skill to run. But the digest's silence gate (_digest_model) only checked
readiness/halt/phase-due — not scout_pending — so it fired as soon as the phase
opened, listing Scout's own undone work as "Scout runs this" while asking the user
to act.

Fix (design-fit, mirrors the existing "unsigned readiness → silent" rule): the
daily digest reports the SETTLED "here's what needs YOU" picture, so it stays silent
while the open phase still has un-run Scout steps. Once Scout drains notice/
reminders/lockdown (interactively or via a scout-capable path), the digest fires
with the true state. A scout step that BLOCKS becomes a real user task (status
'blocked', not 'scout') and still pushes — so a genuine hold is never hidden.

  * render._digest_model: return None while r["scout_pending"] is non-empty (after
    the phase-due check). One line; all three renderers (plain/markdown/html) and
    the HTML email inherit it since they share this gate.
  * skill/reference/starting-and-scheduling.md: document the new silence rule.
  * tests: new test_digest_silent_while_scout_pending (canonical guard); a new
    _drain_phase0_scout_only helper (runs the 3 scout steps, leaves human holds);
    updated the notify/tick/renderer tests that previously asserted the OLD
    premature behavior to drain scout first (and test_tick_advances_and_reports now
    asserts a headless tick is silent until scout is drained, then fires).

142/142 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…of outbound actions

Option 2 (chosen over keep-interactive): the hourly "Release push reminders"
automation now DRIVES the release end-to-end instead of only advancing agent steps.
Each run it: (1) advances, (2) drains scout_pending for the open phase — performing
each step-action itself (send the early-release notice, feature-owner reminders,
lockdown check, localization trigger) via MCP and recording it, exactly as the
interactive skill would, (3) delivers the daily digest. So Phase 0 comms go out on
schedule at CCD-7 even with nobody watching, and the digest reports the true state.

Safety valve — because these outbound actions happen headless with no preview:
every outbound scout action drops a ONE-LINE courtesy copy in the owner's Scout DM
("🤖 [release <id>] Autonomous: <summary>") so they see what went out.

Made "outbound" a deterministic, declared property rather than guessing from tool
names:
  * NeedsSkill gains `outbound: bool = False` (auto-serialized by as_dict, so it
    surfaces in step-action JSON with no extra plumbing).
  * The five external-effect steps set outbound=True — notice + final_reminder
    (email), flight_reminder + pr_reminder (Teams), localization (pipeline trigger).
    lockdown stays False (its check-lockdown is a local follow-up — nothing leaves
    the box), so it never triggers a DM copy.
  * skill/reference/starting-and-scheduling.md: rewrote the push-reminder
    provisioning prompt + description to the autonomous 3-step flow (status → drain
    scout_pending with the outbound DM copy → tick+deliver).
  * The live "Release push reminders" automation prompt was updated to match.
  * New test_scout_steps_declare_outbound_effect guards the per-step flag.

Testing stays safe: mocks.local.yaml redirects the outbound sends to the user, so an
autonomous run during a test emails/pings the tester, not the real DLs.

143/143 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…er approval

The `cg` step blocks on active High/Critical Component Governance alerts. Clarified
in config/knowledge.yaml (preflight.cg) that the release engineer OWNS the fix, and
that shipping WITHOUT fixing is an override that is NOT the engineer's call alone —
it requires their MANAGER's explicit approval, recorded in the skip reason (name the
approver + link the follow-up fix). Updated what/who/how + FAQs accordingly.

Knowledge-only change (no engine enforcement); 143/143 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…his", not "Do this"

Reported: on Phase 1 (Code Complete Day) the current-phase table showed the first
step — "CCD final code-complete reminder" — as "📌 Do this — then mark done" while
the other two showed "Pending". All three are SCOUT steps (source: scout), so none
is a user to-do; Scout runs them automatically.

Cause: _current_steps() classified the current cursor step under awaiting_action as
"reminder" (the human-todo label) without ever checking source == scout. The other
scout steps fell through to "pending" for the same reason.

Fix: _current_steps() now classifies a scout step (source==scout, not attest) as a
distinct "scout" state (and a blocked step as "blocked"), before the current-step/
awaiting_action branch. render gains the "scout" pill (🤖 "Scout runs this —
automatic") and "blocked" (⛔ "Blocked — needs you"). Scout steps never carry the
reminder flag, so no stray 📌 tag. The headline already treated them correctly
(action=None, scout_pending populated) — this was purely the per-step table label.

New regression guard test_current_steps_label_scout_not_user_action. 144/144 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ate scout_pending

From the live test log: on Aug 19 (CCD-7), the moment Phase 0 completed, the engine
opened Phase 1 and the autonomous automation drained all three Code Complete Day
scout steps (final_reminder email, pr_reminder, localization trigger) — 7 days
before the CCD (Aug 26). In test-redirect mode it was low-impact (comms went to the
owner, localization found no strings), but semantically the CCD-day comms fired early.

Two-part root cause + fix:

1. The `ccd` phase had NO time anchor. `_phase_due` treats a missing anchor as
   "always due", so Phase 1 opened the instant Phase 0 finished. FIX: anchor the
   ccd phase to "CCD" (offset 0) in config/phases.yaml — it opens ON the Code
   Complete Date, exactly as preflight is anchored to CCD-7. Now _phase_due(ccd) is
   as_of >= CCD, so the engine holds it "scheduled" until the CCD.

2. The anchor alone was insufficient: the autonomous automation drains
   status_report()["scout_pending"], and that list was built from the first
   incomplete phase's scout steps REGARDLESS of whether the phase was due — so the
   automation would still run them early, bypassing the engine's due-check. FIX:
   gate scout_pending on active_phase["due"] — a not-yet-open phase exposes NO
   pending scout work. (A blocked scout step surfaces via status, not scout_pending,
   so real holds are unaffected.)

Tests: new test_ccd_phase_not_due_before_ccd_and_no_scout_pending guards both halves
(scheduled + empty scout_pending before CCD; steps appear on the CCD). Updated
test_local_mock_applies_to_later_phases to advance the clock to the CCD (Phase 1 is
now date-gated). 145/145 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…etails box

Reported: the localization step completed and was marked passed, but the Details box
had no evidence of what ran — because when the pipeline creates NO OneLoc PR
(no new strings, or create_pr disabled in test mode), decide() returned complete_none
with no links, so nothing showed. Only the complete_pr branch carried a link (the PR).

Fix: decide()'s terminal branches now ALWAYS carry a proof `links` entry so the
Details box always has evidence of the run:
  * new _run_link(state, cfg): prefers the exact run_url stored at trigger, else
    builds the standard build-results URL from the recorded build id + org/project
    (e.g. https://msazure.visualstudio.com/One/_build/results?buildId=177219192 —
    verified to match the real run URL).
  * complete_pr → links = [PR link, run link].
  * complete_none → links = [run link]  (previously empty).
The check-localization command already persists decision["links"] onto the step, so
the render's Details box picks these up with no extra plumbing.

Tests: _loc_state now seeds a build_id; test_localization_decide_branches asserts the
run link appears in BOTH terminal branches (and alongside the PR link). 145/145 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…bloat

Audit review of events.jsonl found two issues: (1) step_ran recorded only
{phase,step,name} — never the OUTCOME, so "what did the step find / why did it block"
lived ONLY inside rendered scout_output text blobs, not in queryable fields (e.g. cg
ran 6x, all byte-identical, none recording the CVE it blocked on); (2) scout_output
was ~60% of the file, much of it repeated multi-KB rendered status tables from the
advance/status emits — presentation duplicated into the audit log.

Fixes:
- log_actions(el, actions, state=...) now enriches phase/step events with the step's
  recorded status + note (the finding / block reason), and a ran step that recorded a
  block is logged as a distinct `step_blocked` (queryable) instead of step_ran. All 4
  call sites (next/approve/done/tick) pass state.
- emit() gains an optional log_text so callers can journal a COMPACT form while still
  printing the full block. New advance_log_summary() logs the per-action outcome lines
  WITHOUT the appended status table. Wired into next/approve/done (advance), status,
  and deny — dropping the ~3.4KB table snapshot per advance to ~1.2KB of real content
  (and the structured step_ran/step_blocked now carry the finding anyway).

The log is now self-contained: you can reconstruct what each step did/found and why a
block happened from structured fields, without scraping rendered blobs.

Tests: test_log_actions_records_step_outcome_and_blocks (status+note capture,
step_blocked on a block) and test_advance_log_summary_is_compact_no_status_table.
147/147 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t Wednesday)

Reported: on Aug 19 the user got the Code-Complete-Day email + PR-merge Teams message
from the automation, a week before the real CCD (Aug 26). Root cause: automations.plan()
built the one-shot CCD-day automations with schedule "every {weekday} at {time}", where
weekday = the CCD's weekday. "every wednesday" resolves to the NEXT Wednesday — Aug 19,
not the CCD Aug 26 — so the one-shot fired on the wrong Wednesday. (The comment even
claimed "one-shot on the CCD date", but the NL didn't encode the date.)

Scout's scheduler has no absolute-date form, but it DOES support cron. Fix: pin the
schedule to the exact CCD via cron `M H D Mo *` (e.g. 'cron: 0 9 26 8 *' = 09:00 on
Aug 26). A one-shot cron fires ON the CCD regardless of how far in advance it's
provisioned. Verified Scout parses this to a single-fire on the 26th.

- new _ccd_cron(ccd_date, hhmm); plan() uses it for the (non-interval) CCD automations.
  Interval pollers ('every 10 minutes') unchanged.
- removed now-dead _fmt_time_nl.
- doc (starting-and-scheduling.md) updated to explain the cron/CCD-pinning and why
  'every wednesday' is wrong.
- tests: updated test_automation_plan_derives_specs_from_ccd to assert the cron form;
  new test_ccd_cron_pins_to_exact_date (times, day/month, invalid-input guards).

148/148 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hen the CCD moves

Two gaps behind the early-fire report: (1) CCD-day automations were provisioned before
the CCD was confirmed, and (2) nothing updated their (CCD-pinned cron) schedules when
the CCD changed mid-flight — so a moved CCD left them firing on the old date.

Fixes:
- Skill ordering: provision the timed phase automations ONLY after the entry gate's
  ccd_confirmed settles the CCD (SKILL.md "start a release" reordered; the provisioning
  section now gates on a settled CCD / no ccd_conflict).
- New `automation sync --release <id> --json`: matches each REGISTERED step-driving
  automation to its desired schedule for the CURRENT CCD and reports {id, name, slug,
  current_schedule, desired_schedule, changed} so the skill re-applies the ones that
  moved via m_update_automation. Matching is by SLUG (stable) — matching by steps alone
  is ambiguous because the noon trigger and the poller both drive ccd.localization.
- Registry now stores `slug` + `schedule` per entry (register gains --slug/--schedule);
  plan()'s registration output + the printed `register:` line include them.
- set-ccd prints a ⚠ reminder to run `automation sync` when step-driving automations are
  registered; starting-and-scheduling.md documents the post-CCD-change re-sync flow.

Tests: test_automation_sync_repins_on_ccd_change (slug disambiguation + re-pin on a
within-month CCD move); plan/registration assertions for slug+schedule. 149/149 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tubs

Implements Phase 2 as read-only verification + report of the Engineering release
pipelines, per the reviewed design. Replaces the 6 placeholder stubs (stages_ok/
retain/health/ui_auto/payload/mrwp_rc — the action stubs dropped as out of scope)
with 4 deterministic agent steps + the existing go_test human gate:

  checker_fired       — the checker (3038) fired the release: scans the month's daily
                        runs for the one whose 'Trigger Monthly Release' JOB succeeded
                        (skipped on ordinary days, runs on the CCD).
  orchestrator_health — finds the orchestrator (2828) run by its AuthenticatorBranch
                        self-tag; confirms Validate/Create Branches/Trigger RC all
                        succeeded and it is PARKED at 'Remove RC Tags' (pending = the
                        healthy expected state — this step NEVER approves that gate).
                        Reports the RC versions.
  mrwp_ecs / mrwp_local — each MRWP (2519) RC-testing run 'ran to completion': every
                        stage executed (state completed, result succeeded/yellow/red);
                        skipped/canceled/pending = block (aborted pipeline). Red/yellow
                        stages and failed tests do NOT block (triaged later). Attaches
                        the Test-tab summary (unit/instrumented/UI-automation).

New tools/pipelines.py holds the ADO reads (find_orchestrator_run, find_checker_runs,
get_timeline/get_stages, named_record, stage_completion, get_test_summary,
mrwp_run_ids). MRWP ids come from the orchestrator's RC-ECS=/RC-Local= self-tags
(PR #26375) with a trigger-log-parse fallback for runs predating those tags — so
verification works today. Test summary + build logs use a token-backed REST GET where
`az devops invoke` mis-routes the endpoint. Every block surfaces the recovery TSG +
escalation chat; auth failures block with a `run az login` hint (not a false 'not
triggered'). All reads verified against the live 2026-08 release.

Knowledge entries (build_verify.*) added; go_test relabeled 'RC verified — proceed to
bug bash'. Tests: stage-completion rule + 5 step/phase tests + _SAFE_AGENTS offline
profile. 155/155 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires the "report the status of the pipeline and the test" view as a read-only CLI
command the skill calls on demand — distinct from the build_verify steps (which gate).

- tools/pipelines.py: new release_report() aggregator — a pure read over the existing
  helpers that resolves the whole chain (checker 3038 → orchestrator 2828 → the two
  MRWP 2519 runs) and returns a structured model: checker fired?, orchestrator healthy
  + parked at Remove RC Tags + versions, and per-MRWP-run stage completion + Test-tab
  summary (unit/instrumented/UI-automation). Read failures become `error` notes +
  `problems` entries (never crashes, never gates).
- orchestrator/commands/rc_report.py: `rc-report --release <id> [--json]` — prints the
  model as JSON for the skill, or a formatted chain + test-breakdown report (with the
  top failing suites) for humans. Registered in REGISTRARS.
- SKILL.md + commands.md: trigger on "how are the RC pipelines / did the RC tests pass
  / phase 2 status" → run rc-report, paste verbatim. Red/yellow + failed tests are
  expected (triaged in bug bash); only a never-ran stage shows under Issues.
- tests: release_report aggregation + formatter (offline via monkeypatched helpers) and
  the never-ran-stage → problem path. Verified live against the 2026-08 release.

157/157 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- pipelines.mrwp_run_ids: collect ALL RC-<provider> tags (and all log-parse
  matches) and pick the newest = max build id, so a re-triggered Trigger RC
  Testing stage's fresh MRWP run wins over the stale failed one. Added
  _tag_values / _newest_id helpers (kept _tag_value for single-value reads).
- ReleaseState.pipeline_runs: new dict recording checker/orchestrator/mrwp_ecs/
  mrwp_local/versions/resolved_at; round-trips via save/load.
- steps/build_verify: _common.stash_runs() helper; checker_fired,
  orchestrator_health, and verify_mrwp stash their resolved ids on the Done path.
  rc-report also persists the resolved ids (best-effort).
- status_report exposes pipeline_runs; status_view shows a "Pipelines:" line and
  the daily digest (text + markdown) adds a one-line RC summary while build_verify
  is the active phase (reads state, no live az call in the render path).
- Tests: newest-id selection on re-trigger, persistence round-trip, digest RC line.
  160/160 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… any phase

Testing a late phase from scratch is expensive. The simulator fast-forwards the
REAL engine to a declared mid-release target so what you validate is exactly what
production produces — no parallel reimplementation of the state machine.

- orchestrator/sim.py: run_scenario/load_scenario/list_scenarios. Builds a mock
  profile (outcome:done for steps in phases BEFORE the target + readiness auto-pass),
  signs the entry gate offline, then drives the real engine's public transitions
  (step_once/approve_gate/complete_step/record_scout_step), stopping at target modes
  open|gate|done. Target phase runs for real: data:live hits az; data:mock uses the
  scenario's fine-grained input mocks. --freeze snapshots produced state to
  tests/fixtures/<name>.json (a cache generated BY the driver, so it can't drift).
- orchestrator/commands/sim.py: `sim list` + `sim run --scenario <n> [--freeze]`,
  writing to a dedicated .sim-runs root so a scenario can't clobber a real release.
- engine.current_phase_id(): public derived phase position for the driver.
- config/scenarios/: mid_build_verify_open (live, at entry) + at_rc_gate (offline,
  holds at go_test with the real 2026-08 run ids).
- Robustness (code-review): a blocked auto step in a PARALLEL target phase is
  surfaced as blocked (threaded attempted set + ran-branch block detection) instead
  of spinning to the iteration cap; at:gate on a gateless phase reports a problem.
- Tests: 8 sim tests (rc-gate offline, open/done positioning, blocked target in
  sequential + parallel phases, gateless-gate, freeze round-trip, scheduled guard).
  168/168 pass. Docs: SKILL.md + reference/commands.md. .gitignore: .sim-runs/.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… pipelines

Fast-forwards Phases 0-1 offline, then runs the four build_verify steps LIVE
(real az reads of the 2026-08 checker/orchestrator/ECS/Local MRWP runs) and halts
at the go_test gate. Complements at_rc_gate (offline) and mid_build_verify_open
(positions at entry).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…om real release

The sim writes to .sim-runs and never touches the real release, so a normal
`status` (which reads the real runs-root) won't reflect a sim run — by design.
Add `sim status` to render the sandbox directly (auto-discovers the sim release),
and make the SKILL/commands docs call out the isolation explicitly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…L routing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… it normally

Per user feedback: the essential machinery is just a correct state.json (produced
by the fast-forward, not hand-authored) + mocks. The isolated .sim-runs sandbox +
sim status was unnecessary ceremony. Now:
- sim run seeds the REAL runs-root by default (DEFAULT_SEED_RUNS), backing up any
  existing state to release-state.pre-seed-<ts>.json first.
- Removed the sim status subcommand — a normal status reflects the seed now.
- After a seed you use the ordinary skill commands (status/rc-report/next/approve).
- --runs-root still lets you target a throwaway sandbox on purpose.
- Docs (SKILL.md, commands.md, scenario headers) updated; +backup round-trip test.
169/169 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- build_verify now anchored CCD+1 — it opens the day after Code Complete, so the
  engineer wakes to a resume (the 8:00 phase-open push is delivered by the daily
  digest automation; the engine gates on the date).
- New scout step rc_report (before go_test) emails the RC verification report to the
  release owner: pipeline health (checker/orchestrator + versions) and both MRWP runs'
  stage completion + top failing test suites, composed from LIVE data. Frames failures
  as bug-bash triage unless a stage never ran. Blocks if no owner email (set-owner).
  Scout composes; the skill sends via workiq_send_email. Sim marks it done w/o sending.
- _common.py: rc_email/_rc_email_html/_rc_email_plain/rc_report_model formatters.
- knowledge.yaml: build_verify.rc_report entry. Tests: rc_report email + block, updated
  phase-shape (6 steps, CCD+1) + gate-count + CCD+1 due timing. 170/170 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The phase-open 'resume' must land in the morning, not overnight. The daily digest
automation ran 'every hour' (first tick could be 1 AM). Pinned to cron '0 8-18 * * *'
so the first tick of each day is 08:00 local — a phase opening (e.g. Phase 2 on CCD+1)
reaches the owner as an 8 AM resume — with hourly same-day catch-ups (idempotent +
once/day de-dup) and nothing overnight. Updated the live 2026-08 automation and the
provisioning doc so future releases provision it pinned.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…uites; drop dismissive line

Addresses report-quality feedback:
- The '(x2 more)' confusion was duplicate suite names: the test API returns the same
  suite as several runs. New pipelines.get_failed_tests aggregates by suite base name
  (strips the ' # <buildlabel>' run suffix), summing failures and deduping test names.
- Lists the actual failing TEST names under each suite bullet (fetched from
  test/Runs/<id>/results?outcomes=Failed), bounded by max_result_calls/per_suite_cap.
- release_report gains failed_suites per MRWP provider (with_failed_tests, default on).
- Removed the 'No blocking issues - ... triaged in bug bash' line from both the CLI
  report and the email; the email drops the green triage box. Blocking-issues section
  still shows only when a stage never ran.
- Both the rc-report CLI (_format) and the rc_report email render the new detail.
- Tests: suite aggregation/dedup + failing-test-name rendering; updated rc-report test.
  171/171 pass. Verified live against 2026-08 (real failing test names surfaced).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The autonomous worker shouldn't wait: 'every hour' (24x/day, incl. overnight) catches
finished pipelines/steps as soon as they land. The 08:00-18:00 pin optimized for the
digest email arriving in the morning but throttled the autonomous engine — wrong
tradeoff. Reverts doc commit 9c9219f; the live automation was also reset to 'every hour'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… auto-detect at init

Two root causes made Phase 1 comms fire ~13h early in testing:

1) Due-ness used the HOST date (date.today()). On a UTC automation host the date
   rolled at UTC-midnight (evening before, Pacific), opening phases early. Now the
   engine evaluates due-ness + every fire_at_local on the OWNER's timezone.
   - schedule.py: get_tz/now_local + today() is now tz-aware.
   - engine.__init__ computes self.now_local + self.as_of from a tz/now; tz precedence
     is arg -> state.timezone -> config/schedule.yaml -> DEFAULT_TZ (America/Los_Angeles).

2) The every-hour worker drained timed steps the instant a phase went due, ignoring
   their fire_at_local. New Orchestrator._step_time_ready gates a timed step until its
   wall-clock time (owner tz) on its fire day; wired into scout_pending, _step_sequential,
   and _step_parallel. The dedicated cron automation calls step-action (build) directly,
   so it still fires the step at the pinned time; missed steps catch up next day.

3) init now auto-detects the owner's IANA timezone (tzlocal) and PERSISTS it on the
   release (state.timezone), so later headless runs — even in a UTC process — use the
   owner's clock. --timezone overrides; config/schedule.yaml carries the default.

Deps: tzdata (IANA db on Windows) + tzlocal (detect). Recorded in requirements.yaml.
automations.fire_at() exposes a step's fire_at_local to the engine.
Tests: +5 (PT today, UTC-evening due-ness, timed-step gate + catch-up, state-tz
precedence, init capture). 176/176 pass. Code-reviewed clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…re %, visual dashboard

Report-quality improvements to the Phase-2 RC report (email, plain text, and CLI):

- Test-run classification: new pipelines.classify_test_run buckets each run into THREE
  categories — unit (*_UnitTests), instrumented (*_InstrumentedTests), and UI automation
  (everything else, incl. the (API NN) device suites and Lab Api Tests; "the rest are UI").
  No "Other" bucket. get_test_summary now returns per-category {total,passed,failed};
  get_failed_tests tags each failing suite with its category.
- The headline metric is now the UI-automation failure rate (the RC-critical bucket),
  computed over UI tests only — not all tests. This also corrects an inflated count:
  failed is total-passed-notApplicable per category, so skipped/NA tests no longer count
  as failures (e.g. ECS UI 30 failed / 181, ~16.6% — not 104/5871).
- Visual dashboard (email): header banner, summary strip (UI failure rate / checker /
  orchestrator), per-provider cards with a big UI-failure %, pass bars, a per-category
  breakdown table (Unit/Instrumented/UI ← RC gate), and failing suites tagged by category
  with per-suite % and test names.
- CLI rc-report + plain-text email show the same per-category breakdown.
- Tests: classifier (3 buckets, Lab Api → UI), suite aggregation w/ category, updated
  rc_report email assertions (per-category headline). 177/177 pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cheduler is local, not UTC)

Empirically verified 2026-08-20: a cron '37 9' fired at 09:37 PDT / 16:37 UTC. Docstring-only note so a UTC conversion is never added, which would shift every CCD-day comm by the host's UTC offset. No logic change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ent three-tier UI automation quality gate

- Updated build_verify_live.yaml to auto-advance rc_report and position at bug-bash entry.
- Modified mocks.local.example.yaml to reflect changes in RC report handling.
- Enhanced rc_report.py to apply a three-tier UI gate, determining pass/warn/attention outcomes.
- Adjusted sim.py to change auto-approve gates from go_test to bash_done.
- Revised SKILL.md and commands.md to update user guidance on RC verification and command usage.
- Updated build_verify.md to clarify the removal of the go_test gate and the new RC report process.
- Enhanced _common.py to summarize UI failing suites for better reporting.
- Updated tests to reflect the removal of the go_test gate and ensure correct behavior in the new flow.
Design-audit follow-up to the go_test removal — eliminate the same 'step id hardcoded across modules' smell in two more places:

1) automations planner no longer special-cases ccd.localization. A step module may now declare automation_prompt(release, spec) (single source of truth, like fire_at_local); localization's bespoke trigger/poller prompts move into steps/ccd/localization.py and _prompt_for() delegates generically.

2) render/digest no longer hardcode the build_verify phase id for the RC pipeline-run line — driven by a data flag (show_pipeline_runs) in phases.yaml, propagated via engine _active_phase_report.

Engine core confirmed free of hardcoded step ids. 181/181 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant