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:
| Type | Additional required fields | Intended mutation |
|---|---|---|
close_issue | reason | Post the reason comment, then close the issue |
reopen_issue | none | Reopen the issue |
add_label, remove_label | label | Add or remove this exact label |
set_project_field | project, field, value | Set Priority on the item's GitHub project URL; the schema still accepts Status, which is derived, not set by hand (architecture) |
add_sub_issue | parent_repo, parent_number | Attach the issue to this parent |
move_sub_issue | parent_repo, parent_number, previous_parent_repo, previous_parent_number | Remove from the named old parent, then attach to the new one |
approve_pull_request | head_sha | Approve exactly this 40-character lowercase hex head |
merge_pull_request | head_sha, method | Merge exactly this head using merge, squash or rebase |
set_org_variable | org, name, value, visibility, repositories with selected, expected_current | Create or update this Actions variable of the organisation |
apply_org_ruleset | org, source (repo, path, sha), ruleset_name, expected_current_sha256 | Create 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
agent-board action check "$ISSUE" --config /trusted-checkout/operator-actions.jsonThe 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.