Skip to content
This text was generated by an LLM (how cgwalters uses LLMs). cgwalters has not reviewed it yet.

Operator interaction

How a person answers the board and acts on it. The commands here print a proposal by default and write only with --apply and the operator's own token; none is delegated to an agent.

Asking the operator

The needs-human label is the one marker that the operator must act (architecture): the operator answers on the labelled item and takes the label off, or on the question issue ask create opens, which ask resolve closes and unlabels. The pass derives the Needs human status from the label, never set by hand, and holds labelled work. The decisions document of ask add and ask read is superseded by it.

Operator actions

An operator action is a proposal, not permission to write. The schema in operator-actions.schema.json is shared with the review site; the Rust checker also enforces configured repository scope and comment provenance. This implements the schema half of tracker#582, not its execution UI.

One unquoted, unindented agent-board-action fence holds one object. Its schema is agent-board/operator-action/v1, title is one line (200 characters), why is a nonblank reason (4,000 characters), and actions holds 1–50 mutations in execution order. Unknown fields and nulls are refused, except the expected prior state of the organisation kinds. Numbers are positive JavaScript-safe integers. Text lengths count Unicode characters.

Every mutation has type. Those on one issue or pull request also have repo (OWNER/NAME) and number; the two organisation kinds have org instead, and neither repo nor number. The types are:

TypeAdditional required fieldsIntended mutation
close_issuereasonPost the reason comment, then close the issue
reopen_issuenoneReopen the issue
add_label, remove_labellabelAdd or remove this exact label
set_project_fieldproject, field, valueSet Priority on the item's GitHub project URL; the schema still accepts Status, which is derived, not set by hand (architecture)
add_sub_issueparent_repo, parent_numberAttach the issue to this parent
move_sub_issueparent_repo, parent_number, previous_parent_repo, previous_parent_numberRemove from the named old parent, then attach to the new one
approve_pull_requesthead_shaApprove exactly this 40-character lowercase hex head
merge_pull_requesthead_sha, methodMerge exactly this head using merge, squash or rebase
set_org_variableorg, name, value, visibility, repositories with selected, expected_currentCreate or update this Actions variable of the organisation
apply_org_rulesetorg, source (repo, path, sha), ruleset_name, expected_current_sha256Create or replace the organisation ruleset of this name with the body of a reviewed file

Labels and field values are nonblank, at most 200 characters; reasons are at most 4,000. Project URLs use the existing GitHub project identity rules. Project numbers have at most 15 decimal digits, without leading zeroes. Integer numbers accept equivalent JSON spellings such as 1.0 and 1e0, as JSON Schema and JavaScript do. Each parent repository must also be on the allowlist. These are typed mutations, not arbitrary API requests; a consumer must not infer an issue is a PR or vice versa from its number.

Organisation actions

The organisation kinds exist for org-wide settings that the bot, an org member only, may propose but not make (cgwalters-forge/actions#129); the operator, an org owner, executes them. org must be the configured one (check). There is no secret kind, by design: a proposal is a public comment, and a secret's value cannot be shown for review.

A variable name is upper case, as GitHub stores it: letters, digits and underscores, not starting with a digit or GITHUB_, at most 100 characters. value is shown in full, never trimmed or truncated: 1–49,152 characters in the schema, and the checkers also refuse more than 49,152 UTF-8 bytes, GitHub's 48 KB limit. A GitHub comment holds at most 65,536 characters, so a value and expected_current both near the limit cannot be proposed together. visibility is all, or selected with repositories (1–100 repositories of org, each on the allowlist, none twice); there is no private.

A ruleset body is never inline. source names a JSON file at a 40-hex commit of an allowlisted repository: a relative /-separated path of [A-Za-z0-9_.-] segments, none . or .., ending in .json, at most 200 characters. The executor applies it only if that commit is reachable from the repository's default branch, so the body is reviewed code, not a branch a member pushed. The file holds the writable body only: name (equal to ruleset_name), target (branch, tag or push), enforcement (evaluate or active; disabled is refused), rules (objects with a string type), and optionally conditions and bypass_actors, with integer numbers only, at most 64 KiB. It may not add a bypass actor: each actor must already be in the live ruleset, compared as normalised objects, so a create has none. Removing actors is allowed. check_ruleset_source in board-forge implements these file checks; agent-board itself never fetches the file.

Unlike the other kinds, these two carry the prior state the proposer saw: expected_current (the variable's value, or null for unset) and expected_current_sha256 (the live ruleset's digest, or null for none of that name). The proposal replaces org state that is itself its input, and there is no issue or head to re-read; the operator must know the value being replaced is still the one the proposal was written against, and the review site flags the action stale otherwise. These are the only fields that may be null. A proposer that is only an org member can read a variable's current value through a repository it is visible to, but generally not the owner's view of a ruleset (its bypass_actors are hidden). So expected_current_sha256 for an update normally comes from the operator: the review site shows the live digest when it refuses a stale action. A wrong expectation only makes the action stale; it never applies it.

A ruleset is normalised by keeping only its writable fields, name, target, enforcement, bypass_actors, conditions and rules, that are present (dropping id, source, source_type, node_id, _links, created_at, updated_at, current_user_can_bypass and anything else), sorting object keys recursively by UTF-16 code unit (JavaScript's default sort, so "10" precedes "2"; JSON.stringify of an object would not keep that order), keeping arrays in order, and serialising as compact JSON with JSON.stringify string escaping. Numbers must be integers in JavaScript's safe range, spelled as integers. The digest is the lowercase hex SHA-256 of its UTF-8 bytes (ruleset_digest in board-forge); ruleset_digests in the shared fixture pins it for both sides.

Check

Terminal window
agent-board action check "$ISSUE" --config /trusted-checkout/operator-actions.json

The repository-owned file, read from a protected default-branch checkout, has writers (operator and bot logins) and repositories (explicit allowed repositories), both nonempty arrays of at most 100 strings, and optionally org: the one organisation the organisation kinds may change, compared without case. Absent or empty, they are refused. There is no implicit organisation repository allowlist, no default configuration and no execution switch. Do not source this file or its path from proposal content or dispatcher workflow inputs. To widen repository scope, the operator changes that file through normal review.

The checker reads at most 1,000 issue comments using the job-plan reader and applies the same newest-writer and foreign-edit rules as job plans. This stops a visitor's copied proposal or a collaborator's edit being accepted as the operator's proposal; a foreign edit that removes the fence still blocks fallback. It prints each validated mutation as JSON Lines, preserving order, and identifies the source comment on stderr. No writes occur.

Checking does not inspect current target state, project membership/options, old parent relationships, PR heads, live organisation variables or rulesets, or ruleset source files and their reachability. The review site must check these before acting, flag stale actions, use the viewer's credentials, stop on the first failure and record execution once. Project scope is not write authority: repository scope alone does not authorize changes to any project. Execution and result markers are deliberately left to that separate implementation.

fixtures/operator-actions.json contains shared accepted/rejected schema cases and configuration. Both validators should run every case; provenance checks are separate because they require GitHub comment metadata.

Run just test-action-schema with Ajv 8 available on NODE_PATH for the JavaScript schema fixture check, and cargo test --locked -p board-forge action:: for the Rust contract and provenance tests.

Project status updates

agent-board project status-update --owner my-org --project 7 --status on-track publishes one update using the operator's project-write token. Status is required: on-track, at-risk, or off-track. This is an explicit operator write; the supervisor's status_update (requests) is the only automatic one.

Pass --body FILE to publish a UTF-8 body verbatim. Without it, the command posts the project's changes since its latest update (the week before now for a first update): agent-board changes --since TIME prints the same list, as Markdown or --format json. Its sections are Landed (pull requests merged, with the issues each closes), Closed (issues and pull requests closed without a merge), Opened, and Held (a needs-human or needs-design label added or removed, marked released when none is left). Besides the project's items, it searches each repository they are in for pull requests merged or opened in the window. Every merged one counts as Landed; a new one counts as Opened only when the token's own login or the operator of --board-config wrote it. Apps' and dependency bots' pull requests (a Bot author, a login ending in [bot], renovate or dependabot) get one count line at the end of their section instead of lines of their own; project items are always listed. Landed is grouped by repository past eight lines. Each item appears once, in the first of Landed, Closed, Opened and Held it fits, and every line is [owner/repo#N](url) title, sorted by repository and number. Empty sections are left out; with no changes the body says No changes since TIME.

For a list that is published somewhere other than the project, such as the weekly notes on the organization site, changes --public-only leaves out the issues and pull requests of private repositories, does not search those repositories, and drops a private issue from a landing's closes. It needs no extra request: the reads already carry each repository's isPrivate, and one that was not read counts as private. --max-chars N trims the Markdown to at most N characters as a generated status update is trimmed (the sections with the most lines lose their last lines first, ending with +K more). Status updates themselves stay unfiltered: they are posted on the project. --until TIME ends the window there instead of at the read, for a fixed period such as a week: what merged, closed, opened or changed hold after it is left out, and the searches take the window as a range. Labels are read as they are now, so a hold released after the end may show as released.

Apart from those pull requests, the list describes current members, not a historical board diff: removed members, project-field changes and draft issues are not in it. Incomplete reads, redacted members and invalid timestamps fail rather than publish a misleading list. Reads use the existing pagination request limit. Bodies are at most 4,000 Unicode characters; a longer list trims its longest sections, each ending with +K more.

The command rejects offline snapshots and JSON rendering and prints the created update node ID. It requires project write permission, checks the requested project identity before writing, and checks the returned body/status receipt. It does not add a reporting-interval gate, retry a failed mutation, or prevent concurrent operator publications. No workflow, credential store or delegated authority is introduced.