solicitation-review
Score a solicitation's responses and recommend an awardee; after human approval, record the award. Needs a solicitation with responses.
SKILL.mdskills/solicitation-review/SKILL.md
GitHubSolicitation Review
Manual skill — never runs in default /ace:run. Only via:
/ace:step solicitation-review --opp <opp-name>
This is the only skill that calls award_response (irreversible) and the
only skill that populates selected_llo (which gates Phase 9). Writes to
phases.solicitation-management.products.selected_llo in the current run's
run_state.yaml only.
Inputs
| Parameter | Used for | Resolver |
|---|---|---|
--solicitation-id |
the call being reviewed and awarded | default: the run's products.solicitation.solicitation_id; outside a run, explicit |
--rubric <doc id> |
evaluation criteria | default: the run's solicitation-create_published.md; else the call's own criteria via get_solicitation |
| responses | what is scored | default: the run's solicitation-monitor_responses/*.md; else live list_responses + get_response |
--out-folder-id |
where rubric / recommendation / award record go | default: the run's 8-solicitation-management/ |
optional_steps.ace_winner |
write selected_llo.synthetic_winner: true |
default: the run's optional_steps block; outside a run, absent |
The HITL award gate (Step 6) is unchanged in every mode: the human approves at
level 0 before award_response. Step 10's run_state.yaml write happens only
when a run is bound; outside a run, record the award (response id, org, budget,
awardee_notified) in the program context doc's Decisions.
Default resolution inside a run — read from the current run's run_state.yaml.phases.solicitation-management.products.solicitation:
solicitation_idpublic_urllabs_program_id— labs integer program ID cached bysolicitation-create(also available atopp.yaml.connect.program.connect_int_idas the durable opp-level cache). Used only as the optionallist_solicitationsfilter.get_solicitation,list_responses,get_response,update_solicitationandaward_responsedo NOT need it, even for a private (is_public: false) call: since connect-labs#2567, labs authorizes them by owning-org membership, and the live schema marksprogram_id"Accepted for compatibility; not needed" (2026-10-18). Note: this is not the Connect program UUID. If both locations are empty and you need the filter, fall back to the resolution recipe insolicitation-createstep 5 (labs_contextlookup by program name).ACE/<opp-name>/runs/<run-id>/8-solicitation-management/solicitation-create_published.md(rubric)ACE/<opp-name>/runs/<run-id>/8-solicitation-management/solicitation-monitor_responses/*.md(all responses)
Process
Pull all responses fresh. Call:
mcp__connect-labs__list_responses(solicitation_id: <id>)For each response, call
get_responseeven if the local cache exists (responses may have been edited).Score each response. Read the rubric from
published.md(theevaluation_criteriablock). For each response, score every criterion on its declared scale (typically 1-10) and compute a weighted total. Use the same archetype-aware judgment thatsolicitation-create-evaluses on the rubric itself.Optionally write to labs. For each response, call:
mcp__connect-labs__create_review( public_record_acknowledged: true, response_id: <id>, llo_entity_id: <the response's llo_entity_id, or "individual">, criteria_scores: { <criterion_id>: <score>, ... }, notes: "<reasoning>", demonstration: true # ONLY for the ACE winner on an ace_winner run -- see below )This puts ACE's scores in the labs audit trail. Idempotent — call
list_reviewsfirst and skip if a review byace@dimagi-ai.comalready exists for this response.demonstration: true— the ACE winner only. Labs refuses a review or award of a response from the caller's own side, and the rule keys on the USER: ace@ belongs to the winner org (connect_orgs.llo_org), so every plain call on the ACE winner's response is refused (ace#3257). Labs' one exception is a demonstration: honoured only when the response was createdsynthetic(bysolicitation-seed-responses), the call is unlisted, and the caller manages the call. Pass it only when BOTH hold:optional_steps.ace_winneris on (isOptionalStepEnabled(runState, 'ace_winner')), and this response is the seeded ACE winner (the roster's winner row:llo_entity_id=llo_org,synthetic: trueonget_response). Never on any other response, never on a real call, never outside anace_winnerrun. If the winner'sget_responsereadssynthetic: falseit was filed before labs had the marker and can never be demonstration-awarded: re-runsolicitation-seed-responses(it files a fresh, marked winner) before reviewing it.Write
scoring-rubric.md. Save the per-response, per-criterion scores to:ACE/<opp-name>/runs/<run-id>/8-solicitation-management/solicitation-review_scoring-rubric.mdMarkdown table per response, with columns: criterion, weight, score (0-10), notes.
Write
recommendation.md. Save:ACE/<opp-name>/runs/<run-id>/8-solicitation-management/solicitation-review_recommendation.mdBody: ranked list of candidates with reasoning. Top candidate gets a
> Recommended awardeecallout block. Include differentiators (what sets the top candidate apart from #2/#3) and any flagged-as-unscoreable responses.
5b. Required-before gate — a HARD STOP (owner decision 2026-10-04). Before asking anyone to approve an award, call:
mcp__ace-decisions__decisions_open_asks(
runFolderId: <run folder id>, opportunity: <opp>, run_id: <run-id>,
neededBy: 'award',
solicitationAnswered: [<question ids the top-ranked response answered, non-empty>]
)
If requiredBefore is non-empty, do not present the award prompt and do
not call award_response — not even if the human asks for it in this
session. Each entry is a decision row with review_ask: required-before
and needed_by: award: the design says its answer changes who may be
awarded or what the award commits to, and nobody has answered it. Add a
> Award blocked callout to recommendation.md listing each entry's
question, owner and answer_channel, write award-record.md with
status: blocked and the same list, and stop. The way to clear it is to
ANSWER the ask: the owner rules in the ace-web decisions review (saved to
inputs/decision-overrides.yaml with decided_by), or — for an ask whose
answer_channel is solicitation:<question-id> — the chosen response
answers that question (those land in closedBySolicitation; name them in
recommendation.md with the response's answer quoted). Then re-run this
skill. A recommended-confirmation ask never blocks the award. Contract:
docs/decisions-contract.md § Open asks.
HITL gate. First resolve the awardee's Connect organisation id (
org_id). This is the Connect org that will hold the partner's opportunity in Phase 9. Labs stores it on the response as "the Connect organisation id the awarder named for the contract". It is an owner-only field, soget_responsereturns it to the call's owner, and it is usually empty unless the response was created with one. Use the response'sorg_idwhen it is set. Otherwise propose the org from the response (llo_entity_id/org_name) and have the human confirm or supply the id in the same reply. Never invent one: Phase 9'sconnect_send_llo_inviteneeds a real Connect org.Present
recommendation.mdto the human and ask:"Confirm awarding response_id=<top> (reward_budget <amount> <currency>) to <org_name>, Connect org
<org_id>? Labs will email the awardee if labs email is on, and an award cannot be undone or repeated. Reply withaward <response_id> <amount> <org_id>to confirm, orcancelto halt."Wait for an explicit reply. Do not call
award_responsewithout one. If the human picks a different response_id, amount or org, use those. If they pick a different response, re-run step 5b with THAT response's answered question ids first.Call
award_response. On confirm (the live schema requiresresponse_id,reward_budgetandorg_id; there is noamountfield, and extra keys are rejected — ace#3146):mcp__connect-labs__award_response( response_id: <chosen>, reward_budget: <approved amount, integer, in the call's currency>, org_id: <confirmed Connect organisation id>, fund_id: <labs fund id>, # optional: only when the award should append a fund allocation demonstration: true # ONLY when optional_steps.ace_winner AND <chosen> is the seeded ACE winner )demonstration: truefollows exactly Step 3's rule: only on anace_winnerrun, only for the seeded ACE winner'ssyntheticresponse, never for a real award. Labs then recordsdemonstration_award: true, emails nobody and allocates no fund (so leavefund_idout — labs refuses it). Labs refusesdemonstration: trueon any response that is not synthetic, on a listed call, or to a caller who cannot manage the call, and refuses a plain award of a synthetic response — neither ever falls back to the other. Say "demonstration award" in the Step 6 prompt on such a run.The call marks the response
awarded, and moves anactiveorclosedsolicitation toawardeditself. It emails the awardee only when labs email is on (awardee_notifiedin the result says whether it was sent). A response that is already awarded is refused withFAILED_PRECONDITION, and you get no second email or allocation. Withfund_id, the fund is checked first; if the allocation then fails, the award still stands and the result carriesallocation_error. Do NOT retry, because a retry is refused as already awarded.Write
award-record.md.ACE/<opp-name>/runs/<run-id>/8-solicitation-management/solicitation-review_award-record.mdBody:
response_id,awarded_at,awarded_org_slug,awarded_org_name,awarded_contact_email,award_amount(thereward_budgetsent),org_id,awardee_notified(from the result, so a reader knows whether labs emailed the awardee),demonstration_award(from the result), andallocation_errorwhen present. On labs-side error:status: failed+ the error envelope.Confirm the labs-side status is
awarded.award_responsealready flips anactive/closedsolicitation toawarded. Read it back withget_solicitation, and only if it does not sayawardedcall:mcp__connect-labs__update_solicitation( solicitation_id: <id>, update_data: { status: 'awarded' }, )No
program_id, even for a private call: the owning org's members may update it, and the live schema marksprogram_id"Accepted for compatibility; not needed". Treat 4xx here as non-fatal:award_responsealready succeeded, so write astatus_update_failednote intoaward-record.mdand continue. The award is durable; the labs-side status flip can be retried out-of-band via the labs UI.Populate
selected_lloand update solicitation status. Only on a successful award, and only when a run is bound (outside a run see § Inputs), write to the current run'srun_state.yamlviaupdate_yaml_file+merge: 'deep'. Read the existingproducts.solicitationblock first, setstatus: awardedand populate theawarded.*block, then write bothproducts.solicitationandproducts.selected_lloin one consolidated payload.deepmerges these under thesolicitation-managementphase block while preserving itsstatus,steps, etc. —two-levelwould replace the whole phase block wholesale and drop them (#572/#587):phases: solicitation-management: products: solicitation: # preserved from solicitation-create + the read above... status: awarded awarded: response_id: <chosen> awarded_at: <ISO> awarded_org_slug: <returned> awarded_org_name: <returned> awarded_contact_email: <returned> award_amount: <approved $> selected_llo: org_slug: <returned> contact_email: <returned> source: solicitation response_id: <chosen> synthetic_winner: true # ONLY when optional_steps.ace_winner — omit otherwiseThe ACE-owned winner (
optional_steps.ace_winner). When the run enabledace_winner(isOptionalStepEnabled(runState, 'ace_winner'),lib/optional-steps.ts), the awarded response is the onesolicitation-seed-responsesfiled for ACE's own winner org (connect_orgs.llo_org, resolved only bylib/connect-orgs.ts). Writeselected_llo.synthetic_winner: true. Every later consumer (opp-closeout,llo-feedback,timeline-monitor, Phase 9's simulated mode) reads that flag and sends nothing outbound, so the award never reaches a real LLO. Never set it on a real award. On anace_winnerrun the orchestrator runs this skill inline at level 0, so the Step 6 approval still reaches the operator. The award is still irreversible and still real on labs, so the HITL gate is unchanged.No write to
opp.yaml.selected_llooropp.yaml.solicitation. These are per-run only. Phase 9 (llo-onboarding) readsselected_llofrom the samerun_state.yaml.
Error handling
- Required-before ask unanswered (step 5b): do not call
award_response; recordstatus: blockedwith the open asks. This is not an error to retry around — the ask must be answered first. - HITL gate timeout / no reply: do not call
award_response. Do not mutaterun_state.yamloropp.yaml. Exit cleanly so the human can re-run the skill. - Human replies
cancel: halt; do not writeaward-record.md. award_responsereturns 4xx after approval: writeaward-record.mdwithstatus: failedand the error envelope. Do not populateselected_llo(Phase 9 stays gated). Surface the error to the human and suggest contacting a labs admin if the award call must succeed out-of-band.list_reviewsshows ACE already reviewed all responses: skip the scoring step (we don't re-score), proceed to step 4 from the existing reviews.- No responses at all: write
recommendation.mdwithstatus: no_responses, do not callaward_response. Suggest extending the deadline (futuresolicitation-monitor --extend-deadlineskill).
Output
ACE/<opp-name>/runs/<run-id>/8-solicitation-management/solicitation-review_scoring-rubric.mdACE/<opp-name>/runs/<run-id>/8-solicitation-management/solicitation-review_recommendation.mdACE/<opp-name>/runs/<run-id>/8-solicitation-management/solicitation-review_award-record.mdphases.solicitation-management.products.selected_llo.*populated (only on success; per-run only)phases.solicitation-management.products.solicitation.{status: awarded, awarded.*}populated (only on success; per-run only)
MCP Tools Used
connect-labs:list_responses,get_response,list_reviews,create_review,award_response(response_id+reward_budget+org_id, optionalfund_id),get_solicitation,update_solicitation(fallback status flip toawarded; noprogram_idneeded)ace-gdrive:drive_create_file,drive_update_file,drive_read_file,drive_list_folderace-decisions:decisions_open_asks(step 5b — the required-before award gate)