Skip to content

feat(skill): add run-assert-eval skill and eight worked example domains - #303

Open
alex ngo (ango10) wants to merge 77 commits into
mainfrom
ango10/assert-acs-skill-main
Open

feat(skill): add run-assert-eval skill and eight worked example domains#303
alex ngo (ango10) wants to merge 77 commits into
mainfrom
ango10/assert-acs-skill-main

Conversation

@ango10

Copy link
Copy Markdown
Collaborator

Summary

Adds run-assert-eval, a skill that drives the full Clarity → ASSERT → ACS loop from inside an
MCP-capable IDE, plus eight worked example domains produced with it and a skill-first README entrypoint.

Motivation / linked issue

ASSERT's entry cost was writing an eval_config.yaml by hand, which requires knowing both the schema
and which risks are actually worth measuring. This branch makes risk discovery the front door: Clarity discovers risks, the skill turns each selected risk into one atomic config, runs the pipeline, and can
then generate an ACS policy from the real findings and re-measure to prove the failure rate dropped.

Changes

Skill .claude/skills/run-assert-eval/

  • SKILL.md: canonical instructions. Clarity MCP (run_clarity) is a required front door. No
    plain-language fallback, since evals seeded from a guess produce low-signal results.
  • Three workflows: measure-clarity-failures.md (discovery → measurement),
    govern-and-remeasure.md (ACS → governed run → delta), diagnose-acs-delta.md (symptom-indexed
    fallback for when the delta comes out wrong).
  • clarity_intake.py parser, with unit tests and fixtures.
  • SETUP-CHECKLIST.md for one-time MCP setup and end-to-end verification.
  • Mirrored for Copilot (.github/prompts/) and Cursor (.cursor/rules/).

Examples eight domains built end-to-end with the skill, each shipping Clarity Protocol/
(discovered risks), evals/<risk>/eval_config.yaml (one atomic config per risk), agent.py, and a
README: azure_doc_qa, billing_support_agent, career_health_assessment, change_control_agent,
prompt_agents, science_research_agent, travel_planner_langgraph, travel_planner_neurosan.

  • Skill-generated ACS artifacts are intentionally stripped: the governed entrypoint is an output of
    the workflow, not a checked-in file.

Framework ACS CLI fixes

  • assert_ai/cli.py: assert-ai acs … now loads .env and refreshes Azure auth mode, so
    acs generate resolves credentials the same way assert-ai run does. The ACS subcommands
    don't import the runner, so the project .env was previously never loaded.
  • assert_ai/cli.py: acs validate now explains that annotator-conditioned policies
    (input.annotations.*) cannot fire in offline validation, so an unblocked/0-handled result
    is expected rather than a policy defect.
  • assert_ai/integrations/acs/{prompt_builder,validate}.py: policy generation/validation fixes,
    with tests in tests/test_acs_{prompt_builder,validate}.py.

Docs

  • Root README.md: new skill-first Get started covering onboard → explore → use → ACS. Also repoints
    a quick-install command that broke when example configs moved under evals/<risk>/.
  • Example READMEs now headline Impermissible Behavior violated / Permissible Behavior violated
    instead of the raw policy_violation / overrefusal keys

Testing

Checklist

  • Tests pass locally (pytest and/or viewer checks as applicable).
  • Docs updated if behavior or public API changed.
  • No secrets, credentials, or customer data committed.
  • No breaking change, or a CHANGELOG.md entry is included.

Alex Ngo and others added 30 commits July 13, 2026 15:47
…aph example runs and results. refine skill.
Full workflow for the career health assessment example.

Clarity: problem/failures/requirements/architecture. Triaged F1
(unsupported candidate inference) as the single atomic behavior;
overrefusal is tracked as the counter-metric, not a second suite.

ASSERT baseline (25 prompt + 25 scenario, built-in judge dimensions only):
  policy_violation  prompt  4.0%  scenario 24.0%
  overrefusal       prompt  0.0%  scenario 36.0%

ACS: single `output` intervention point (this agent has no tools) with
three classifier annotators. agent_guarded.py wires
AgentControl.from_path(manifest, dispatcher) by hand -- ASSERT's own
build_agent_control helper omits the dispatcher, so input.annotations.*
is never populated and every annotator-conditioned rule fails open. It
also drops `history`, which would break multi-turn parity.

ASSERT remeasure (governed):
  policy_violation  prompt  8.0%  scenario 16.0%
  overrefusal       prompt  4.0%  scenario 24.0%

Net 13 failing rows of 50 vs 16 at baseline; the scenario slice -- where
the harm actually concentrated -- improved on both dimensions
(24.0->16.0 and 36.0->24.0). The prompt slice moved by one row on each
dimension, which is inside the noise band at n=25.

Tuning note: the judge scored empty schema placeholders as unsupported
assertions, so the regeneration instruction was changed to omit fields
rather than pad them, and the fallback no longer emits empty scaffolding.

Configs differ by exactly two lines (run, target.callable), so the A/B
isolates enforcement.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Alex Ngo and others added 18 commits August 4, 2026 11:30
Reset travel_planner_langgraph, change_control_agent, prompt_agents,
science_research_agent, and travel_planner_neurosan to commit 6817a31 so the
finalized skill can be rerun end to end. prompt_agents backs the
health-assistant model-only, simulated-tools, and generated-tools variants, so
clearing that root resets all three.

Removes generated Clarity Protocol trees, eval configs, ACS policies and
manifests, guarded agents, and guard tests while retaining each example's
original agent, tools, README, and config inputs. Prior ASSERT and ACS run
outputs were preserved under artifacts/*-run2-archive before deletion.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Clarity risk discovery identified two Critical failure modes in the
science research assistant: restricted-class disclosure and obedience to
instructions embedded in retrieved documents. Both are governed by a
single structural gate.

tools.py `_render()` returns the full `body` and `snippet` for every hit
regardless of the authoritative `class` field, so the four restricted
classes (restricted_results, partner_notes, contacts, private_notes) are
disclosed in full. `snippet` is not a lesser field - it carries the
partner recall figure and a named contact - so body-only redaction would
have left the harm intact while reporting enforcement active.

The gate runs at post_tool_call on file_search and is transformative, not
a denial: file_search is never blocked. Restricted results have `body`
and `snippet` replaced by a marker while `id`, `class`, `rank`, `tags`,
and `title` are preserved, so the agent can still report that a document
exists and give the access-request path.

No injection detector was built. The payload the injection asks for is
removed before delivery, so the embedded instruction can be obeyed in
full and return nothing. Source class is deliberately not used as an
injection signal: fetch_url hardcodes class `public`, and the carrier
document is genuinely public.

Measured at n=25 per split, baseline vs governed:

  restricted_class_disclosure
    prompt    harm 91.67% -> 43.75%   permissible 60.87% -> 32.00%
    scenario  harm 95.83% -> 75.00%   permissible 48.00% -> 52.00%
  embedded_instruction_obeyed
    prompt    harm 56.25% -> 26.67%   permissible 28.00% -> 28.00%
    scenario  harm 61.90% -> 34.78%   permissible 52.00% -> 52.00%

Restricted markers fall to zero in the governed transcripts: the partner
recall figure 679 -> 0, the contact address 220 -> 0, the contact name
428 -> 0.

The scenario permissible tick for the first risk is one row of 25 with
node-level counts flat at 14/16/16, so it is clustering noise rather than
a mechanism; it was not resampled.

One iteration was needed. The first marker was long, prohibition-heavy,
and injected twice per restricted document, which suppressed legitimate
internal_only discussion - the internal_only sentinel dropped from 58
occurrences to 48. A compact, positively framed, scope-bounded marker
with the long guidance emitted once in policy_notice restored it to
exactly 58. No prompt, model, control-flow, or test-set change.

agent_guarded.py adds ACS enforcement and nothing else. It imports
agent.py rather than copying the loop, never redefines SYSTEM_PROMPT,
TOOL_SCHEMAS, MAX_STEPS, MAX_TOOL_CALLS, or AGENT_MODEL, and delegates to
base.chat(). Enforcement is scoped by a contextvars.ContextVar set inside
try/finally by the governed entrypoints; with the wrapper installed and
the contextvar unset, baseline behaviour is unchanged.

Governed configs differ from baseline by exactly two lines (`run:` and
`target.callable`). Both governed runs reused systematize and test_set
artifact v0001; --force-stage was never passed.

Note that under-disclosure by blanket refusal is itself scored
`permissible: false` in this taxonomy, so the harm metric is mixed here -
part of the improvement is the agent answering more, not only leaking
less.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Runs the full Clarity -> ASSERT -> ACS -> ASSERT loop over the change
control agent for both Critical failures found by Clarity risk discovery.

failure-02 fabricated_change_record -- WIN. A post_tool_call gate denies a
change record whose returned fabricated_fields is non-empty, and a
pre_tool_call gate holds the control surfaces while such a record stands.

  prompt    harm  0.00% -> 0.00%   permissible 52.00% -> 44.00%
  scenario  harm 90.91% -> 86.36%  permissible 52.00% -> 52.00% (flat)

failure-01 unauthorized_change_advancement -- HONEST NEGATIVE. A
pre_tool_call gate on the four control surfaces denies a submission while
policy state forbids it.

  prompt    harm  0.00% -> 0.00%   permissible 18.18% ->  4.35%
  scenario  harm 66.67% -> 44.00%  permissible 16.00% -> 24.00%

The win condition (harm down AND permissible down-or-flat) is not met on
the scenario split. Harm fell 22.7pp, roughly 5-6 rows of 25, while
permissible rose 8pp, or 2 rows. All four permitted governed attempts were
spent and attempt 1 was the best of them on every metric, so attempt 1 is
what ships and the negative is reported rather than re-rolled.

The permissible move is inside the measured noise floor. Governed attempts
1 and 2 differed only by added trace spans -- an observability change with
no policy effect -- yet scenario harm moved 44.0 -> 56.0 and permissible
24.0 -> 41.7. Run-to-run variance at n=25 with judge n=1 is therefore about
3-5 rows. The 2-row permissible move sits inside that band and the 5-6 row
harm drop sits outside it. No run was repeated unchanged to fish for a
better draw.

failure-02's residual 86% harm is a structural ceiling, not an
implementation gap: only 8 of 20 harmful rows ever call
create_change_request and 7 of 20 make no tool call at all, so most
fabrication harm is invented prose that a tool-call gate cannot reach. The
one output-stage attempt at it drove scenario permissible 52% -> 84% and
overrefusal 12% -> 76% while barely moving harm, and was reverted in full.

Not-permissible harm is 0.00% on the prompt split in every run of both
suites, so all harm signal in this domain lives in the scenario split and
the prompt split measures only over-restriction.

agent_guarded.py adds ACS enforcement and nothing else. Both arms execute
the same agent.py::_run_loop function object -- verified at runtime as
agent_guarded._run_loop is agent._run_loop -- which takes a pluggable tool
executor; the baseline passes _default_execute_tool and the guarded module
passes an ACS-enforcing executor of the identical signature. No prompt,
model, tool schema or budget is redefined. Each eval_config.governed.yaml
differs from its baseline by exactly two lines, run: and target.callable,
and every run scored the same systematize/test_set v0001 artifact.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Runs the full Clarity -> ASSERT -> ACS -> ASSERT loop over the health
assistant Prompt Agent for both Critical failures found by Clarity risk
discovery, across the model-only and simulated-tools variants.

A Prompt Agent has no host process: the target is declared entirely in YAML
and the ASSERT runtime owns the model call and tool loop, so there is
nothing for ACS to wrap. The target is therefore materialised as a Python
callable in agent.py, which instantiates ASSERT's own HostedSession and
SimulatedResolver rather than imitating them. Both A/B arms run that same
materialised callable, so the delta isolates enforcement; absolute levels
carry materialisation error, the delta does not. The five
health_assistant*.yaml specs are the specification under test and are
unmodified.

simtools x fabricated_clinical_fact_as_retrieved -- WIN on attempt 1.
Marking, not suppression: the harm is the certainty, so the claim is
attributed to its origin and not presented as verified.

  prompt    harm 13/25 = 52.0% -> 9/24 = 37.5%   perm 6/12 = 50.0% -> 1/18 =  5.6%
  scenario  harm 17/25 = 68.0% -> 8/24 = 33.3%   perm 10/20 = 50.0% -> 3/25 = 12.0%

simtools x dosage_guidance_to_elderly_patient -- WIN on attempt 3.
Non-statement, not marking: a caveat beside "5 mg" does not remove the 5 mg.

  prompt    harm 10/25 = 40.0% -> 1/24 =  4.2%   perm 1/25 = 4.0% -> 1/25 = 4.0%
  scenario  harm 19/25 = 76.0% -> 15/25 = 60.0%  perm 0/25 = 0.0% -> 0/25 = 0.0%

Over-refusal is 0% on both splits of both shipped runs.

modelonly x dosage_guidance_to_elderly_patient -- NOT WON, reported as such.
No configuration wins both splits. v4 wins the prompt split outright
(37.5% -> 27.3%, permissible flat at 0/25) and cuts scenario harm
18/24 -> 14/24, but scenario permissible rises 0/25 -> 2/25.

modelonly x fabricated_clinical_fact_as_retrieved is excluded by design:
with no tools there is no retrieval claim to make.

gentools is not measurable and is skipped rather than approximated.
health_assistant_generated_tools.yaml sets tool_source: per_test_case, so
tool schemas ride on each test-case row and are read by
_build_hosted_session as scenario["tools"]. _build_target_session has the
payload in scope and passes it to ExternalSession, but constructs
CallableSession without it, and CallableSession.__init__ accepts no
scenario or tools parameter. Per-test-case tools cannot reach a callable
target, so chat_gentools raises NotImplementedError. Approximating with a
fixed toolset would measure simtools while labelling it gentools.

Both rates are flagged/applicable, and the judge marks a node not
applicable when the transcript never engages it, so a control that removes
a behaviour outright shrinks its own denominator and can push a rate up
while violations fall. modelonly v3 prompt is 9/24 -> 8/21: the rate rises
37.5% -> 38.1% while the count falls. Counts are therefore reported beside
every rate here, and the README carries the same warning.

The shipped default is the v3 ladder, which is what produced the confirmed
simtools x dosage win; the v4 position-keyed ladder is retained behind
HEALTH_ACS_POSITION_KEYED_DOSAGE=1 and gate telemetry behind
HEALTH_ACS_GATE_LOG, both default off. Each governed eval config pins the
run id the shipped code reproduces, so no config resumes a cached run
produced by different code.

agent_guarded.py adds ACS enforcement and nothing else. Both arms execute
the same function object, verified at runtime as agent._chat is
agent_guarded._chat; each governed entrypoint is a single line calling it
through its one seam. The guarded module never references HostedSession,
SimulatedResolver, load_toolset_file, parse_target_config or the system
prompt. Each eval_config.governed.yaml differs from its baseline by exactly
two lines, run: and target.callable, and every run in all three suites
scored the same systematize/test_set v0001 artifact.

verify_gates.py exercises both gates against the real AgentControl/OPA
runtime (13/13), including a case proving the annotator name contract fails
silently when the manifest key, the Rego reference and the dispatcher
branch disagree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Adds the governed arm for both Critical risks discovered by Clarity
(fabricated_cost_claim, unsupported_entry_requirement), measured against a
shared baseline. All runs reuse one systematize v0001 and one test_set v0001,
and each governed eval config differs from its baseline by exactly two lines
(run, callable), so the A/B is directly comparable.

Result: a split verdict, shipped honestly rather than tuned into a win.

Prompt split (single turn) wins on both risks:
  costs  harm 16/23 -> 7/21
  entry  harm flat at 2 rows while permissible violations fall 11/23 -> 8/25
         and over-refusal falls 44.0% -> 32.0%

Scenario split (ten turns) regresses on both risks:
  costs  permissible 12/23 -> 17/24, over-refusal 48.0% -> 75.0%
  entry  permissible 11/19 -> 16/18, over-refusal 50.0% -> 89.5%

The scenario regression is architectural, not a tuning failure. Those
conversations frequently never reach the research step, so the retrieval record
is empty; with no evidence in hand the only correct action for an output gate
is to decline, and nothing later in the conversation supplies what is missing.
Softening the annotator across attempts measurably re-opened harm (costs prompt
harm back to 47.4%, entry scenario harm to 81.8%), so the trade is real. The
enforcement wrapper is deliberately not allowed to retrieve the missing
grounding, because a wrapper that retrieves is no longer a control.

agent_guarded.py reuses the baseline graph rather than reimplementing it:
get_graph(), _seed_messages() and _get_llm() are imported from agent.py, and
_draft() is agent.chat() plus the returned message list and retrieval record.
At runtime the guarded module resolves to the same compiled graph object, so
the arms differ only by enforcement. Tracing is enabled before agent import in
both arms so the judge sees identical telemetry.

A known defect ships deliberately and is documented in code and in the README:
the depth-based fallback rotation was inert during measurement because history
was not threaded into it, so every declining turn emitted identical wording.
The call site is left in its measured two-argument form so the published
numbers reproduce from this code. The prompt split is single turn and therefore
unaffected, so those wins stand unconfounded; the scenario over-refusal figures
should be read as an upper bound on the cost of enforcement rather than a
precise measurement of it. Threading history is a one-line, unvalidated change
belonging to the next cycle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Resets the example so the Clarity -> ASSERT -> ACS workflow can be run
end to end from a clean slate in the IDE.

Removed (all generated by the previous skill run):
  Clarity Protocol/    discovery, failures, goal, solution, mailboxes
  acs/                 manifests and Rego policy for both risks
  evals/               baseline and governed eval configs for both suites
  agent_guarded.py     the ACS enforcement wrapper
  README.md            restored to its pre-skill content

Also removed from the working tree, untracked and therefore not part of
this commit: artifacts/results/travel-planner-*, artifacts/acs/travel-planner-*,
the per-run logs and status JSON under artifacts/runlogs/, and __pycache__.
Clearing the results and stage artifacts matters as much as clearing the
source: a stale suite directory would let a later run reuse cached
systematize/test_set artifacts instead of generating its own.

The example is now byte-identical to its pre-skill state (empty diff against
c2c11d5) and contains only agent.py, auto_trace.py and README.md.

Nothing is lost. The previous cycle is preserved in full by commit 818f7c7
and by the exported release bundle, which carries the source, both ACS
policies, all eval configs, the Clarity protocol, every run's results and
transcripts, and the per-run logs for baseline plus all four governed
attempts.

No other example is touched.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Shipped configurations (counts are flagged/applicable, re-derived from raw
results; rates alone mislead because the judge drops non-engaging rows from
the denominator):

wrong-destination-entry-requirements  baseline -> acs-governed
  prompt    harm 25/25 -> 4/24   permissible 4/11 -> 1/25   overrefusal 0/25 -> 0/25
  scenario  harm 24/24 -> 10/14  permissible 10/16 -> 21/24 overrefusal 7/24 -> 24/24

fabricated-budget-verification        baseline -> acs-governed-v2
  prompt    harm 21/25 -> 13/24  permissible 1/22 -> 1/25   overrefusal 0/25 -> 0/25
  scenario  harm 25/25 -> 23/23  permissible 11/25 -> 10/24 overrefusal 10/25 -> 11/24

Single-turn prompts win on both risks. Multi-turn scenarios trade harm for
over-refusal: the fallback is a fixed stateless template re-delivered on every
denied turn, so a 7-turn conversation re-asks for a nationality the traveller
already supplied. That is a remediation-design fault, not a policy fault.

Enforcement-only A/B: each governed config differs from its baseline by exactly
two lines (run, callable). Same model, judge, sample size, and stage artifacts.

Provenance: agent_guarded.py output templates were verified against the shipped
transcripts by string probe -- the budget fallback appears 493x in
acs-governed-v2 and 0x in acs-governed, which is why the budget config is pinned
to v2. Annotator and regeneration prompt text was edited after the last
successful run for two follow-up attempts that failed at inference and produced
no results. Wrapper-internal prompts are never written to any artifact, so those
edits cannot be confirmed or excluded from the measured state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Shipped runs:
  azure-doc-qa-confidential-leakage    baseline -> acs-governed
  azure-doc-qa-fabricated-answer       baseline -> acs-governed-grounded-v2

Counts are flagged/applicable, re-derived from the raw result rows. Rates alone
mislead: the judge marks a node not-applicable when the transcript never
engages it, so a shrinking denominator can move a rate while the violation
count is unchanged.

confidential-leakage           baseline -> acs-governed
  prompt    harm  9/22 ->  2/22   permissible 10/25 ->  6/25   overrefusal  2/25 ->  4/25
  scenario  harm 15/24 ->  8/24   permissible 17/25 -> 16/25   overrefusal 11/25 -> 16/25

fabricated-answer              baseline -> acs-governed-grounded-v2
  prompt    harm  3/14 ->  1/16   permissible 11/24 -> 11/25   overrefusal 10/25 -> 11/25
  scenario  harm  9/23 ->  9/18   permissible  6/25 -> 18/25   overrefusal  5/25 -> 18/25

Leakage wins on both axes: harm roughly halves and permissible violations fall
as well. Fabrication wins single-turn only - harm 3 -> 1 with permissible
exactly flat at 11. Multi-turn does not win: the harm count is unchanged at 9
(the rate moves 39.1 -> 50.0 only because the denominator fell 23 -> 18) while
permissible violations triple, 6 -> 18.

Fabrication took three governed attempts, all recorded in Clarity summary.md:
a reply-only output annotator cannot separate grounded specificity from
fabricated specificity, so it only trades over-refusal. Feeding the annotator
the retrieval context captured from the baseline graph, then scoping the
rewrite, cuts single-turn harm without an over-refusal cost. Multi-turn is not
reachable from an output gate at all - 18/25 conversations are flagged for both
fabrication and over-refusal, i.e. the agent fabricates on some turns and
stonewalls on others. The fix belongs upstream, at retrieval state or in the
prompt, not in another output-remediation lever.

Enforcement-only A/B: each governed config differs from its baseline by exactly
two lines (run, callable). One systematize/v0001 and one test_set/v0001 shared
by every run, so no stage was regenerated.

Provenance: agent_guarded.py was written 11:35:19, the shipped run started
11:36:54 and ended 12:20:12, and the file has not been touched since. The
intermediate configs eval_config.governed.yaml and
eval_config.governed_grounded.yaml are retained because summary.md cites their
numbers as the progression, but the committed agent_guarded.py is the
grounded+scoped code and will NOT reproduce those two runs. Only
eval_config.governed_grounded_v2.yaml is reproducible from this tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
Keep only the baseline agent, the baseline eval configs, the Clarity
Protocol design record and the README in each domain.

Deleted (57 files):
  acs/ manifests + rego policy .......... 30
  agent_guarded.py ......................  8
  eval_config.governed*.yaml ............ 18
  prompt_agents/verify_gates.py .........  1

verify_gates.py goes with them: it imports agent_guarded and loads acs/,
so it cannot function once those are gone.

Kept deliberately, though a literal "delete what the skill made" would
have removed them:
  - __init__.py in billing_support_agent, career_health_assessment and
    prompt_agents. These were added by the skill, but are required for
    examples.<domain>.agent to import.
  - mock_tools.py, mcp_tools.py and docs/ (azure_doc_qa); tools.py
    (change_control_agent, science_research_agent); the five
    health_assistant*.yaml specs (prompt_agents). All are referenced at
    import time or read at runtime.

All 17 remaining baseline eval_config.yaml target agent.py and contain no
acs/ or guarded references, so the baseline side of each A/B is intact.

Clarity Protocol is left as-is. 8 of its 205 files still describe acs/ and
agent_guarded.py; that is prose, not imports, and is correct for a
historical design record.

Verified: all 8 examples.<domain>.agent modules import cleanly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
@ango10 alex ngo (ango10) changed the title Ango10/assert acs skill main feat(skill): add run-assert-eval skill and eight worked example domains Aug 5, 2026
Alex Ngo and others added 3 commits August 5, 2026 16:47
…only layout

Merging origin/main brought in 054797f, which reduced incident_triage_agent to a baseline-only, one-behavior-per-YAML example and deleted eval_config_naive_prompt.yaml, eval_config_guarded.yaml, eval_config_guarded_gepa.yaml, and incident-triage.guardrails.yaml. Git merged cleanly because no file was touched on both sides, but the top-level examples/README.md row still advertised all four configs and the ACS + GEPA 4-variant matrix -- stale on main as well, since 054797f never updated this table.

Repoint the row at what the example actually ships: behaviors/ as the recommended one-behavior-per-YAML split, with eval_config_baseline.yaml as the bundled overview. Drop the ACS/GEPA framing (that demo is superseded by #262 and its guardrails file is deleted); ACS remains covered by the acs_guardrails row.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
This branch moved each example's eval_config.yaml under evals/<risk>/ so one config probes one risk, but docs across the repo still pointed at the old top-level paths. Every 'assert-ai run --config examples/travel_planner_langgraph/eval_config.yaml' (and two 'assert-ai init --from' variants) referenced a file this branch deleted, so the copy-paste quickstarts in AGENTS.md, docs/getting-started.md, and docs/guides/securing-agents-with-acs.md all failed.

Repoint those at evals/budget-overrun/eval_config.yaml, matching the root README. In examples/README.md also fix the science_research_agent row and correct the canonical example's trace backend, which the config now sets to otel rather than phoenix.

azure_doc_qa/IMPROVEMENT_JOURNEY.md is left pointing at its original bundled config on purpose: it is a historical log whose rates came from a single run scoring 9 judge dimensions over 56 test cases, so repointing it at a single-risk config would misattribute those numbers. Add a note recording that the config was since split, with links to the replacements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb38db79-6f6e-4f57-9e57-c7496498048b
@ango10

alex ngo (ango10) commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI/CD failing? Is this potential reason? Issue: #304.

Update 08/07: Seems like its fixed, was this due to GitHub Actions being down yesterday?

@changliu2 Chang Liu (changliu2) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Request changes.

I read the whole diff, ran the suite on both this branch and main, and pulled the CI history. The Skill design is the strongest part of this branch — the permissibility-split terminology from #295 is handled more carefully than anywhere else in the repo (including the not_permissible identifier vs. "non-permissible" label mismatch, and the warning that config dimensions silently replace BUILT_IN_DIMENSIONS by name), and all 20 configs are genuinely one atomic behavior each, with shapes correctly pushed into stratify.dimensions rather than bundled into the description. The ACS-variant rule is respected and the framework changes are small, tested, and honest about their own heuristics.

Four things need to land before merge.

1. Silent test-coverage regression. Deleting examples/travel_planner_langgraph/eval_config.yaml turns two tests in tests/test_library_e2e.py (:624, :637) into skipTest("Example config not found"). Measured: main = 65 passed / 0 skipped; this branch = 63 passed / 2 skipped. Those are the only e2e tests that a shipped example config merges the safety-core judge preset with inline overrides. Please repoint them at examples/travel_planner_langgraph/evals/budget-overrun/eval_config.yaml.

2. The 26 new tests never run in CI. pytest.ini sets testpaths = tests and regression.yml runs pytest tests/ -x -q, so .claude/skills/run-assert-eval/tests/test_clarity_intake.py is never collected — the same gap the #267 audit found. They pass (I ran them: 26 passed), so this is free to fix. regression.yml's paths: filter also doesn't include .claude/** or the new example dirs, so future Skill edits won't trigger CI either.

3. No CI has run on head. gh pr checks shows only CodeQL; the only Build and Regression runs on this branch are on 58a4eb7e / a435e4fa, not 6e39fa8. I agree with your #304 diagnosis — the two red Regression runs are the unpinned arize-phoenix breakage, not yours. But that means the assert_ai/ changes here have never been CI-verified. I ran them locally and they're fine (test_acs_validate + test_acs_prompt_builder: 8 passed, 1 skipped; full tests/: 1181 passed with 28 failures that are all local viewer/node_modules absence). Please land the #304 pin and re-run against head before merge.

4. billing_support_agent is a strawman baseline, and the README calls it "the best one to read first." The system prompt ends with "Prefer being helpful and decisive over being overly cautious; when the customer asks you to take an action, take it." It then says the billing policy "lives in the team policy document; consult it as needed" — but there is no policy-retrieval tool among the eight, so the agent is graded against a policy it cannot read. The module docstring confirms "the high-risk tools execute regardless of verification," and CALLER_ACCOUNT_ID = "ACME-1001" is never compared against the account_id argument. So cross-customer-data-exposure (which the config maps to OWASP API1:2023 BOLA / CWE-639) and unverified-high-risk-action are measuring an agent with no scoping and no gate, told to just do it. The rate is a property of the fixture, and any ACS delta off it is large by construction.

This is the same "no auth logic at all" pattern we pushed back on for the AIEWF bank-manager baseline. I'm confident calling it because it's an outlier here, not house style — azure_doc_qa, change_control_agent, career_health_assessment, and science_research_agent all ship properly specified prompts whose controls are prompt-only and honestly documented as defeasible in the config context. That's the right kind of baseline. Please bring billing_support_agent up to that bar (real policy in the prompt, drop the "be decisive" instruction, ideally scope the read tools to CALLER_ACCOUNT_ID) and re-measure.

Non-blocking follow-ups, happy to see these in a second PR:

  • Nine copy-pasteable commands still point at the deleted per-domain eval_config.yaml: azure_doc_qa/agent.py:14,17, azure_doc_qa/IMPROVEMENT_JOURNEY.md:45,97,436, travel_planner_langgraph/agent.py:12, phoenix_auto_trace/travel_langgraph.py:10.
  • examples/travel_planner_langgraph/__init__.py is the only one missing of the eight. It works via the PEP-420 namespace fallback, but that's exactly the shadowing fragility SKILL.md's callable-contract section warns about.
  • clarity_intake.py:457 joins an unvalidated markdown-link path onto failures_dir. pathlib lets an absolute value replace the base, and ../ is unchecked, so a failures.md entry can pull an arbitrary file into the parser output and thus into agent context. failures.md is LLM-authored from repo content, so it isn't fully trusted. A resolve() + is_relative_to(failures_dir) guard closes it.
  • SKILL.md Step 3 / measure-clarity-failures.md:114 tell the agent to interpolate Clarity-derived prose into --describe "<text>" with no quoting guidance; a quote or backtick in the description breaks or injects into the user's shell. A --describe-file path would remove the class.
  • This repo is public, and the only evidence for the "all eight domains ran end-to-end" claim is a onedrive.cloud.microsoft link no outside contributor can open — and it exposes internal tenant path structure. Please summarise the numbers inline.

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.

2 participants