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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
hard-coded `0.1.0`, and `baseline.version` / `baseline.specification` derive
from the framework TOML metadata (`OSPS v2026.02.19`) instead of `2025.10.10`
(Refs #494).
- `get_pending_data` gives every question a selector, so a batch of up to 4
questions is asked in one round of the client's question tool (#650, #655).
After 0.1.1, free-text questions and choice questions with more than 4
values had none, so Claude Code switched between the question tool and
plain text within a batch. (In 0.1.1 a free-text question with two or more
examples offered the examples as answers, and a choice question showed
only its first 4 values.) A choice question now offers its first 4 values
and names every allowed value in the question, any of which can be typed
under "Other"; a free-text question offers "Skip for now" and "Answer in
chat" and never a value. `confirm_project_data` refuses those two labels,
and the `answer_mapping` markers `SKIP` and `ASK_USER_FOR_VALUE`, as a
value, so an agent that forwards the label its question tool returned
records nothing for that key. Selector headers are cut at a word boundary
("Compiled", not "Compiled Ass"), and a yes/no question's hint is in the
question instead of next to "Yes".
- The skills ask the person to confirm which repository is being audited
and name it on every darnit call, the audit included. An audit called
without `owner` and `repo` does not apply the person's confirmed answers or
stored PASS candidates, so after answering the data questions a re-audit
showed the same results. `darnit-comply` re-runs the audit after data is
confirmed and shows what changed.

- `get_pending_data` and `get_judgment_requests` return a `procedure`: the
steps a coding agent follows with the result (how to show candidates and
ask the questions, the exact `confirm_project_data` or `submit_judgment`
call, and the next call), naming only the repository the caller named.
A `confirm_project_data` result on the OpenSSF Baseline server ends with
the next `get_pending_data` call. The `darnit-data`, `darnit-audit`,
`darnit-comply`, and `darnit-remediate` skills defer to these procedures
instead of restating them or telling the agent to follow another skill,
which a client may not load, so `darnit-comply` collected data
differently from `darnit-data`. The comply and remediate skills now pass
`owner` and `repo` to `remediate_audit_findings`, and `darnit-comply`
always calls `get_pending_data` (it used to only when WARN controls
were waiting on data); with nothing pending it returns `complete`.
The data procedure's `confirm_project_data` call names `host` as a
placeholder for repositories not on github.com, maps answers by
`answer_mapping` (a No on a candidate is the value the person typed,
never `false`), and keeps the caller's `control_ids` on the next
`get_pending_data` call.
- **BREAKING:** `register_handlers()` is the one handler registration hook
for plugins (#451). `darnit-gittuf` and `darnit-reproducibility` rename
`register_sieve_handlers()` to `register_handlers()`, and their `register()`
Expand Down
8 changes: 5 additions & 3 deletions docs/architecture/framework-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1405,7 +1405,9 @@ get_judgment_requests(
) -> {status: "ok", files, requests, remaining, oversized?} | {status: "rejected" | "error", rejection | error}
```

A coding agent obtains what it judges through `get_judgment_requests`, registered beside `submit_judgment` and requiring the same named repository identity. It audits `local_path` the same way and returns one request per control left `PENDING` (`llm_judgment`), optionally limited to `control_ids`: the control's `prompt`, `analysis_hints`, `target_key` when set, `evidence_digest`, and the keys of its files in a shared `files` map. Each `files` entry holds `path`, `digest`, and `content`, where `content` is exactly the content `submit_judgment` checks the control's citations against (including its caps); a file read by several controls appears once, under a distinct key only if its content differs. `gathered_evidence` and the rest of the control's evidence are not returned. A control with a stored candidate for its current digest (`PENDING`, `confirmation`) is left out unless `include_judged` is true. Requests are ordered by control id and paged: a page holds whole requests and its serialized result stays under 40,000 bytes (UTF-8), below the size at which a coding agent's client writes a tool result to a file instead of showing it, and `remaining` lists the controls not yet returned, to be passed as `control_ids` on the next call. A single request larger than the page limit is returned alone, with `oversized = true`. The tool records nothing, and `submit_judgment` never trusts its output.
A coding agent obtains what it judges through `get_judgment_requests`, registered beside `submit_judgment` and requiring the same named repository identity. It audits `local_path` the same way and returns one request per control left `PENDING` (`llm_judgment`), optionally limited to `control_ids`: the control's `prompt`, `analysis_hints`, `target_key` when set, `evidence_digest`, and the keys of its files in a shared `files` map. Each `files` entry holds `path`, `digest`, and `content`, where `content` is exactly the content `submit_judgment` checks the control's citations against (including its caps); a file read by several controls appears once, under a distinct key only if its content differs. `gathered_evidence` and the rest of the control's evidence are not returned. A control with a stored candidate for its current digest (`PENDING`, `confirmation`) is left out unless `include_judged` is true, and is listed in `skipped` so the agent does not request it again. Requests are ordered by control id and paged: a page holds whole requests and its serialized result stays under 40,000 bytes (UTF-8), below the size at which a coding agent's client writes a tool result to a file instead of showing it, and `remaining` lists the controls not yet returned, to be passed as `control_ids` on the next call. A single request larger than the page limit is returned alone, with `oversized = true`. The tool records nothing, and `submit_judgment` never trusts its output.

Each `get_judgment_requests` page also carries `procedure`: the ordered steps for judging the page (judge each request from its prompt, hints, and listed files only; call `submit_judgment` once per request with passages copied verbatim; a pass is a PASS candidate and a fail a suggestive finding, never the audit result; fetch the next page with `remaining`). The steps name the repository and checkout the caller passed and hold placeholders for everything else. The procedure text lives in one place in the framework (`darnit.server.procedures`), so a skill or client defers to it instead of restating it.

The compact `summary` output of an audit carries, on each `PENDING` row, `pending.kind` and, for a candidate, `pending.candidate` (its model, reasoning, and number of cited excerpts, never the excerpts), so a client can tell a control awaiting a judgment from one awaiting confirmation without the full evidence.

Expand Down Expand Up @@ -1757,8 +1759,8 @@ Each context key has one canonical name and one vocabulary, taken from the frame

The observable payloads of `get_pending_data` and `confirm_project_data` are defined in `specs/042-candidate-integrity/contracts/context-confirmation-tools.md`:

- `get_pending_data` is read-only. Each question carries its candidate as data (`candidate: {value, origin, digest, label}`, where `digest` is the value digest (7.5) and `label` marks it unconfirmed); the candidate's value appears nowhere else in the response. `command_template` holds placeholders only (`<the person's answer>`, `<candidate digest if the person accepts it>`, `owner=..., repo=...`), never a candidate value or a configuration example. `answer_mapping.value_map["Yes"]` for a candidate is `{"accept_candidates": {"<key>": "<candidate.digest>"}}`, a placeholder the agent fills from the question's `candidate.digest` only after the person answered yes. Enum questions list every allowed value in `allowed_values`; when there are more than 4, the selector (`ask_user`) is omitted and the prompt lists them all. `examples` appear only as `format_hint`. Every question in the batch is returned under `questions`, with or without a selector. The response lists `stored_unconfirmed` values with their locations.
- `confirm_project_data` takes a parameter for every context key that the server's bound framework defines, and no other framework's keys (generated from the definitions; an enum parameter accepts only its `values`), plus `accept_candidates` (`{key: candidate digest}`: detection runs again and the current candidate, or a value concluded in this run, is confirmed only if its digest still matches, recording its value and origin as the basis; a mismatch, a key with no candidate, or a key given both an answer and a digest is refused and nothing is written for it), `confirm_stored` and `reject_stored` (review of stored values: a rejected `.project/darnit.yaml` value is deleted; a rejected `.project/project.yaml` value is reported with its file and field and the file is left unchanged), `expires_at` (`{key: date}`), and `owner`, `repo`, `host`, which are required and decide the record location (7.5). `confirm_stored` records a confirmation of the value currently stored for each key (basis: that value, origin `stored_unconfirmed` or `expired_confirmation`). Like a confirmation, a rejection writes to `.project/darnit.yaml` only when the operator trusts the named repository; otherwise it is refused and names the file and field for the person to edit. The result names, per key, `confirmed (in-repository)` with the file written or `confirmed (operator-side)`, `rejected`, `edit required: <file>:<field>`, or `refused: <reason>`.
- `get_pending_data` is read-only. Each question carries its candidate as data (`candidate: {value, origin, digest, label}`, where `digest` is the value digest (7.5) and `label` marks it unconfirmed); the candidate's value appears nowhere else in the response. `command_template` holds placeholders only (`<the person's answer>`, `<candidate digest if the person accepts it>`, `owner=..., repo=...`), never a candidate value or a configuration example. `answer_mapping.value_map["Yes"]` for a candidate is `{"accept_candidates": {"<key>": "<candidate.digest>"}}`, a placeholder the agent fills from the question's `candidate.digest` only after the person answered yes. Every question carries a selector (`ask_user`), so one batch is asked in one round of the client's question tool, which offers at most 4 options and adds a free-text "Other". Enum questions list every allowed value in `allowed_values`; when there are more than 4, the selector offers the first 4 and its question names them all, and any of them can be typed under Other (every allowed value stays reachable). A free-text question's selector offers "Skip for now" and "Answer in chat" and never a value; the answer is typed under Other. A yes/no question's options carry neutral descriptions, and its hint is part of the question. A selector header is at most 12 characters, cut at a word boundary. `examples` appear only as `format_hint`, never as an option. Every question in the batch is returned under `questions`. The response lists `stored_unconfirmed` values with their locations. The response also carries `procedure`: the ordered steps for collecting the data (show each candidate with its origin, labelled unconfirmed; ask every question, passing `ask_user_batch` verbatim in one round; per-question skip; one `confirm_project_data` call per batch, mapping each answer by `answer_mapping` (Yes/No on a yes/no question is a boolean; No on a candidate, or Other, is the value the person typed), with `accept_candidates` filled only after a yes; call `get_pending_data` again, with the caller's `control_ids`, until `complete`; review `stored_unconfirmed`; summarize). Its calls name only the `owner` and `repo` the caller passed, never ones detected from the checkout's remote (placeholders otherwise), and hold no candidate or detected value. `get_pending_data` takes no `host`, so its `confirm_project_data` call names `host` as a placeholder to fill when the repository is not on github.com (7.10). A `confirm_project_data` result that handled context values on a server with `get_pending_data` ends with the next `get_pending_data` call, named the same way.
- `confirm_project_data` takes a parameter for every context key that the server's bound framework defines, and no other framework's keys (generated from the definitions; an enum parameter accepts only its `values`), plus `accept_candidates` (`{key: candidate digest}`: detection runs again and the current candidate, or a value concluded in this run, is confirmed only if its digest still matches, recording its value and origin as the basis; a mismatch, a key with no candidate, or a key given both an answer and a digest is refused and nothing is written for it), `confirm_stored` and `reject_stored` (review of stored values: a rejected `.project/darnit.yaml` value is deleted; a rejected `.project/project.yaml` value is reported with its file and field and the file is left unchanged), `expires_at` (`{key: date}`), and `owner`, `repo`, `host`, which are required and decide the record location (7.5). `confirm_stored` records a confirmation of the value currently stored for each key (basis: that value, origin `stored_unconfirmed` or `expired_confirmation`). Like a confirmation, a rejection writes to `.project/darnit.yaml` only when the operator trusts the named repository; otherwise it is refused and names the file and field for the person to edit. A value that is one of the free-text selector's labels ("Skip for now", "Answer in chat") or an `answer_mapping` marker (`"SKIP"`, `"ASK_USER_FOR_VALUE"`), alone or inside a list, is not an answer and is refused, since an agent may forward the label its question tool returned. The result names, per key, `confirmed (in-repository)` with the file written or `confirmed (operator-side)`, `rejected`, `edit required: <file>:<field>`, or `refused: <reason>`.
- **Which server exposes it**: each server's `confirm_project_data` covers only the keys of the framework the server is bound to. The OpenSSF Baseline server defines its own (which also carries `confirm_not_applicable` and `confirm_pass_candidate`). Every other framework server that defines context keys and whose TOML does not define a tool named `confirm_project_data` gets a framework-neutral builtin `confirm_project_data` (a framework with no context keys gets none), registered by the factory the same way as `submit_judgment` and `confirm_pass_candidate` (5.4) and bound to that framework's context definitions. It records context values only: the per-key parameters, `accept_candidates`, `confirm_stored`, `reject_stored`, `expires_at`, `owner`, `repo`, `host`, and `local_path`. Claim confirmations and PASS candidates stay on their existing tools.
- Remediation prompts (the preflight and `format_context_prompt`) show a candidate as labelled, unconfirmed data with its origin and digest, name the key's hint source files that exist without parsing values out of them, and hold placeholders only in their commands.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4640,18 +4640,11 @@ Each question carries its candidate, if any, as data: `candidate: {value, origin
labelled UNCONFIRMED. Command templates and answer mappings hold placeholders only;
`allowed_values` lists every allowed value of an enum; `format_hint` is a format, never an answer.

MANDATORY WORKFLOW - your next action after calling this tool MUST be calling AskUserQuestion:
1. Call this tool. It returns "questions" and "ask_user_batch" (AskUserQuestion-compatible objects).
2. Show the person each question's candidate value and origin, labelled unconfirmed. Then call
AskUserQuestion with questions=<ask_user_batch>, VERBATIM. Ask the questions that have no
ask_user entry after that, listing every allowed value.
3. After the person answers, use "answer_mapping" to call confirm_project_data(). A "Yes" on a
candidate becomes accept_candidates with that question's candidate.digest, filled in only now.
4. Call this tool again for the next batch. Repeat until status is "complete".

`stored_unconfirmed` lists values stored in .project/ without a confirmation (key, value,
location). Show them to the person; confirm or reject the ones the person names in one
confirm_project_data call with `confirm_stored` / `reject_stored`.
The result's `procedure` lists the steps to follow, in order: show candidates, ask the
questions (`ask_user_batch` is AskUserQuestion-compatible; pass it verbatim), record the answers
with confirm_project_data, call this tool again until status is "complete", review
`stored_unconfirmed` values, and summarize. Your next action after calling this tool MUST be
asking the person the questions.

Do NOT pre-fill or suggest answers. Never type a detected value as an answer.
"""
Expand Down
Loading
Loading