Roundtable

Proposals, Patch Validation, and Transactions

Agents submit patch-bearing proposals. Roundtable records proposal metadata and affected resources, validates the patch against the current repository and governance state, and exposes an orchestrator-side apply operation. A successful apply creates a transaction record and a rollback artifact. This is the repository’s governed mutation path; it is not a single ACID transaction across SQLite and filesystem writes.

Proposal lifecycle

proposal.create stores task/agent/title/summary/risk, affected-resource associations, and a patch artifact beneath .roundtable/patches/. The local handler does not persist its advertised expected-tests/rollback-notes inputs. proposal.attach_patch can attach or replace patch content for an existing proposal. Review requests, votes, decisions, security reviews, and human approvals are persisted separately. Proposal status is updated through proposal/patch operations; exact eligible states and gates should be read from the service, not inferred from the design task pack.

Local Creation Fields and Persistence

Creation requires actual nonempty string task_id, agent_id, title, summary_md, and patch. These checks do not trim whitespace. It parses patch paths, requires the task to exist, and validates ownership of the merged affected-resource list. It does not verify that the supplied agent exists/is enabled, owns the task, or is the authenticated caller. Risk defaults to normal for missing/empty/non-string input, with no enum check. Unadvertised proposal_id and status are accepted; defaults are generated P- timestamp ID and pending. A supplied status is not constrained to a creation-state enum.

Optional affected_resources are deduplicated by exact ID, with empty entries omitted. Explicit file/symbol IDs cover their parsed file paths; directory IDs cover matching descendant paths; uncovered parsed patch paths add file:<path> resources. Ownership requires an exact resource-ID match in the supplied agent’s active claims, excluding read/review claims. It does not check claim expiry, task/session/workspace association, or treat an unrelated overlapping lease as an exact match. Later patch validation separately checks resource coverage and current governance.

Creation writes .roundtable/patches/<proposal-id>.diff before proposal persistence, requesting parent mode 0755 and file mode 0644. Supplied IDs are used in pathname construction without a separate ID/containment check here. Existing artifact paths can be overwritten. The proposal upsert can replace an existing ID’s local metadata, rather than rejecting it as duplicate creation. It then ensures resources, replaces links, and reloads both for the response. These are separate operations: a failure can leave an artifact, changed proposal, or incomplete associations. No durable request deduplication or encompassing filesystem/database transaction is supplied.

Unknown argument keys can reach this local handler but are not automatically stored: expected tests, rollback notes, HTTP base revision, workspace/substate fields, and caller-selected execution controls do not gain behavior merely by appearing in input. Creation does not request review, clear old acceptance evidence on ID reuse, run tests, create an approval, or append a dedicated creation event in this handler. Title/summary/patch content are not generally redacted. It returns saved proposal and proposal_resources; use explicit current validation and review before apply.

Declared Resource Merge and ID Interpretation

Creation and attachment retain nonempty explicit IDs in input order, deduplicating exact strings, then append uncovered parsed file paths as file:<path>. IDs are not trimmed, case-folded, path-cleaned, or symlink-resolved. Explicit IDs remain unchanged as lease/link keys even if another spelling describes the same path.

Exact symbol: IDs split at the first # into path and optional symbol name. Other IDs containing a colon split at the first colon into type/path; bare IDs become file metadata but retain the bare ID. Only parsed types file, symbol, and directory suppress automatic file insertion. A symbol suppresses insertion for its entire parsed file path without resolving the name or checking spans; final coverage can still fail.

Directory matching is literal equality or prefix directory-path + "/"; an empty directory path covers every parsed path. Trailing slash is not removed. Thus directory:src suppresses insertion for src/main.go, whereas directory:src/ also requires the automatically added file:src/main.go. Bare src/main.go suppresses insertion but requires ownership of that exact bare key, not file:src/main.go. This helper does not establish filesystem containment or least privilege.

Resource-record creation skips IDs whose lookup succeeds, preserving existing metadata. Any lookup error, not only a confirmed missing row, instead leads to an upsert using metadata parsed from the ID and detected language. Per-resource writes are separate and can remain after a later failure. Existing metadata is not verified against ID spelling here; final coverage reads stored metadata, so an ID label alone is not authoritative scope evidence.

Local Proposal Reads

proposal.list accepts optional actual string task_id and status. Empty/missing or non-string values impose no filter; nonempty values compare exactly without whitespace trimming or case normalization. It loads all database proposals ordered by creation time then ID and filters in memory. There is no run/workspace/agent restriction, cursor, page size, newest-first mode, or automatic exclusion of terminal records. The response is {proposals: [...]} with an empty array when no matches survive, not a shared HTTP page envelope.

The underlying list selects ID, task/agent IDs, title, summary Markdown, patch path, status, risk, and creation time. It does not populate the newer HTTP control-plane substates, workspace metadata, base revision, or validation summary merely because those database columns exist. Local stored-record serialization is distinct from the HTTP response schema; do not infer field parity from shared tables. List query, scan, and final iteration errors propagate rather than returning a verified empty result.

proposal.get requires a nonempty string proposal ID, loads that record by ID, and separately reads its resource links sorted by resource ID. It returns proposal and proposal_resources; an empty association slice can be null. The links contain proposal/resource IDs, not expanded resource definitions, claims, or symbols. A resource-read error fails the call after proposal lookup. These independent reads are not an atomic snapshot.

Neither read validates artifact existence, downloads patch bytes, redacts all stored metadata, recalculates policy, loads votes/decisions/security/human/test evidence, or proves current apply eligibility. Use the corresponding tools and current validation for those tasks. Proposal path/status metadata alone is not proof of a readable patch or a completed transaction; HTTP list/detail membership and pagination are separately described below.

Local Patch Attachment and Replacement

proposal.attach_patch requires actual nonempty string proposal_id and patch, loads the proposal by ID, parses touched files, merges optional affected_resources with parsed file resources, and validates those claims against the stored proposal author. It does not authenticate that the caller is that author or require a pending/in-review proposal status. Whitespace is not independently trimmed by these required-string checks. Malformed patch/resource arguments or missing claims fail before the file write; this is not full apply validation, policy acceptance, or test execution.

If the proposal already has a patch path, attachment reuses it; otherwise it constructs .roundtable/patches/<proposal-id>.diff and saves that path. The helper joins the stored path to the service root without a separate symlink/containment check at this write site. It creates parent directories with requested mode 0755 and writes complete patch bytes with requested mode 0644, subject to umask/existing modes. This is an overwrite, not append, exclusive creation, immutable artifact revision, or atomic temporary-file rename. Preserve the previous patch yourself through the governed evidence workflow when comparison is needed.

After writing, it upserts the existing proposal, ensures resource records, replaces proposal-resource links with the merged list, and reloads proposal/resources for the response. These steps are not one filesystem/database transaction. Failure can leave new patch bytes with old or partial resource metadata; a late response error does not restore the previous artifact. No expected proposal revision, patch hash, or compare-and-swap guards concurrent replacements.

Attachment does not clear existing votes, decisions, human approvals, security-review records, test runs, or prior transaction evidence; it does not reset status/risk/base metadata or request fresh review. Those records are not automatically rebound to the new bytes. Reassess existing acceptance evidence after every replacement rather than treating an earlier approval or passing test as approval of the changed artifact. The response contains the saved proposal and resource list, without an immutable patch-version ID or dedicated replacement event from this handler.

Votes and decision records

Local Review Requests and Rejection

proposal.request_review requires a nonempty proposal ID, loads proposal/resources/votes, and reads/parses the current artifact to evaluate policy. Missing/unreadable/malformed artifacts fail the request; it does not perform temporary apply or execute tests. Target roles always include Reviewer, add configured required roles from detected profiles, and add Security for exact high/critical risk or high-risk paths, extending it with configured security roles. Role labels are deduplicated case-sensitively and sorted; candidate matching to stored agent roles is case-insensitive but does not trim stored roles.

Every enabled matching agent except the exact stored proposal author is returned with role, agent ID, adapter, and stored status. Readiness, session availability, workspace association, concurrency limits, or a current turn are not checked. When a role has no candidate, its response entry contains only the role; this is not an error or proof that review occurred. Proposal status changes to in_review only from empty or exact pending; other statuses still return review context without a transition. The response includes proposal/resources, roles/candidates, policy result, and human/security requirement flags.

This local handler does not persist review assignments, append a review-request event, contact candidates, create approval records, run a security scan, schedule turns, or apply a patch. Its role selection does not enforce voter eligibility. A successful response is a computed review plan, not completed review evidence or an asynchronous job receipt. HTTP request-review uses its separately documented transition behavior rather than this full local response.

patch.reject requires nonempty proposal_id and reason_md, loads the proposal, and unconditionally upserts status rejected without prior-state or caller-ownership checks. It then upserts a rejected decision associated with the proposal/task. Unadvertised decision_id defaults to a generated D- timestamp ID; decided_by defaults to orchestrator. Supplied nonempty values and reason text are stored without general text redaction or authenticated actor validation. Reusing a decision ID updates that row rather than creating an immutable history entry.

Status write and decision write/reloads are separate operations, not one transaction. A decision failure can leave the proposal rejected without the intended decision record; repeated calls can create additional decisions. Rejection does not revert applied files, delete patch/resources, release claims, invalidate votes/approvals, cancel sessions, or create compensation. It returns saved proposal and decision, with no dedicated rejection event emitted by this handler. In particular, rejecting an already accepted proposal changes metadata, not the repository mutation it previously represented.

vote.cast requires an existing proposal_id and nonempty agent_id, vote, and reason_md. It returns the saved vote. confidence defaults to 0 and is not range-clamped by the local handler. The handler also accepts vote_id (not advertised in the registry); missing/empty generates a V- timestamp ID, while an existing ID replaces that vote row’s fields and retains its creation time. Caller agent IDs are not authenticated or checked against review roles by this handler. Policy counts stored recognized vote strings, without using confidence or policy-weight metadata in its current approval count.

Use approve, approve_with_notes, revise, reject, veto, or abstain as the workflow vocabulary. The local handler accepts any nonempty string; unrecognized or differently cased strings are stored but do not contribute to approval/blocker counts. vote.list requires a proposal ID and returns its votes in ascending creation-time/ID order. The returned list can be null when empty. These tools do not automatically apply a patch after votes arrive.

decision.record requires nonempty decision, rationale_md, and decided_by; proposal_id and task_id are optional associations. It additionally recognizes an unadvertised decision_id, generating a D- timestamp ID when empty. An existing ID updates its row rather than creating an immutable revision. The handler stores decision strings and actor metadata directly, without checking the actor identity or loading the associated task/proposal first. It returns decision; recording it does not itself update task/proposal status, grant approval, or apply a patch. Lifecycle operations such as patch.reject have their own associated state changes.

Validation performed by patch.validate

Validation reports separate results for:

  1. The author’s active-claim freshness scan, followed by exact ownership checks for declared affected resources.
  2. Patch artifact existence and unified-diff parse success.
  3. Resource coverage: the touched paths must be covered by proposal resources.
  4. Applying the patch to a temporary workspace copy of the current project.
  5. Policy evaluation based on proposal risk, touched paths, and stored votes.
  6. Human approval satisfaction and whether a security review is required/satisfied.

The returned valid field covers claim validation, artifact presence, patch parsing, resource coverage, and successful temporary apply. Policy approvability and security/human gates are reported separately and are enforced again by patch.apply.

Claim Freshness and Validation Side Effects

The validator lists all claims in the local database and selects those whose agent_id exactly matches the stored proposal author and whose status is exactly active, excluding exact claim types read and review. It does not restrict this scan to the proposal’s affected resources. A stale lease on an unrelated resource can therefore invalidate this proposal. Task, run, session, workspace, expiry, heartbeat, and renewal settings do not narrow selection; validation is not caller authentication or automatic expiration.

For every selected claim with nonempty base_hash, it loads the resource, computes its current content hash, and persists resource.current_hash, including an empty result. A nonempty current hash different from the base hash causes an immediate claim upsert to status suspended; that claim is excluded from the active ownership map and collected in stale_claims. An empty base hash skips hashing entirely. An empty current hash leaves the claim active, so passing this check does not prove that missing or unresolved content is unchanged. See Resource Hash Algorithms for hash formats and filesystem limitations.

After scanning, any collected stale claim produces claim_error beginning stale claim base hash: and names the first collected resource. This takes precedence over missing affected-resource ownership. Only when no stale claims were collected does the validator require an exact active resource-ID match for every affected resource; the first missing match produces unclaimed affected resource:. Overlapping paths or a broader directory lease do not substitute for that exact ID check.

This is a state-mutating validation operation, not a read-only eligibility probe. Resource updates and suspensions are separate writes without a transaction around the loop, and this helper appends no dedicated event. A later resource lookup, hashing, or persistence failure returns immediately while earlier writes remain. Such an early failure returns no stale-claim slice even if earlier claims were already suspended: inspect stored claims rather than interpreting empty stale_claims as proof that nothing changed. Subsequent validation excludes those suspended claims and can report missing ownership instead of the original stale-hash error. Creation and patch attachment use a separate ownership-only helper and do not perform this freshness scan.

Patch Metadata Parser

repo.ParseTouchedFiles splits the entire supplied string on LF and recognizes exact line prefixes, not a complete Git/unified-diff grammar. Each diff --git line is split with strings.Fields; fewer than four fields produces invalid diff header: ..., while additional fields are not independently rejected. The third/fourth tokens supply old/new paths, stripping one matching a/ or b/ prefix and applying platform slash normalization. Exact /dev/null becomes an empty path. This does not decode Git C-style quoted names, escaped bytes, or filenames containing whitespace; it is not a containment or traversal validator.

The returned Files list contains deduplicated nonempty old/new paths, sorted lexicographically. FileChanges retains encountered header order. Exact new file mode and deleted file mode lines set flags without validating the mode value. rename from and rename to replace the respective path with trimmed slash-normalized text, set the rename flag, and add those paths to Files; earlier header paths remain in that list. These metadata lines before any diff header are ignored. Ordinary ---/+++ headers do not independently establish a file or creation/deletion flag.

For a recognized @@ line, the parser requires a current path and the prefix pattern @@ -<digits>[,<digits>] +<digits>[,<digits>] @@; trailing heading text is permitted. A hunk before a usable diff path fails, and a malformed recognized hunk produces invalid hunk header: .... Changes are indexed by the current old path when nonempty, otherwise the new path, including after rename metadata.

Recorded ranges describe old-side hunk spans, including context, not individual changed lines. Omitted old count defaults to one; otherwise end is old start plus old count minus one, with nonpositive start/end normalization. Zero old count becomes a one-line anchor at positive old start, then positive new start, otherwise line one. New-side count is matched but not used to derive the range. Integer-conversion errors are ignored by this helper, so very large numeric headers are not a reliable validation boundary.

Unrecognized lines, hunk bodies, line-count consistency, binary payloads, mode-only semantics, copy metadata, and combined-diff syntax are not fully validated. A nonempty file list can yield parser success without any hunk ranges or executable changes; no recognized files yields patch contains no diff headers. Parser output feeds declared resources, coverage, preview, policy paths, and rollback capture, but actual executability is separately tested by the external executor. Preserve the original artifact and do not equate metadata parse success with complete path safety or correct patch application.

Resource Coverage Algorithm

Coverage loads each proposal-resource record and groups exact stored types file, directory, and symbol by slash-normalized path. Other resource types do not cover file changes. Missing resource or failed symbol resolution fails the check. This is distinct from lease ownership: resource links define the declared coverage, while claim validation checks the proposal author’s claims separately.

For every parsed file operation, coverage considers unique nonempty old/new paths. An exact file match or matching directory for any of those paths accepts the operation. Directory matching is lexical equality or directory + "/" prefix; an empty directory path covers everything in this helper. It does not resolve symlinks or revalidate directory semantics here. Consequently, a rename can pass on coverage of only one side, not necessarily both source and destination. Create/delete/rename without file/directory coverage is rejected even if symbol resources exist.

For ordinary modifications lacking file/directory coverage, each parsed change range must overlap at least one valid symbol span for a considered path. Overlap is not full containment: a range extending beyond the claimed symbol can pass when it intersects that span. Different ranges can overlap different symbols. Missing ranges or symbols do not establish coverage, and invalid symbol bounds are ignored. This check uses parser/indexer line evidence, not a complete semantic dependency or mutation-boundary analysis. Do not rely on it as proof that every changed line belongs entirely to the claimed declaration.

If detailed file operations are absent, the checker synthesizes ordinary old/new-same-path changes from parsed file paths. It returns on the first coverage failure, not a complete issue inventory. Exact error messages distinguish special-operation coverage, nonoverlapping symbol ranges, and uncovered files. The algorithm does not grant new claims, enforce unique ownership, inspect external dependency effects, or repair malformed resource records. Review actual touched paths/ranges alongside claims before application.

Validation Response Interpretation

The handler requires nonempty proposal_id and returns an error for failed proposal/resource/vote/approval/security-record reads. Claim, patch-parser, coverage, and temporary-apply errors are instead carried in the successful result envelope. Read every relevant field rather than equating transport ok: true with valid: true or apply authorization.

Field Meaning and limitation
proposal, proposal_resources Separately loaded stored metadata/links, not an immutable patch-version snapshot.
patch_exists os.Stat succeeded for the joined artifact path; any stat error makes it false. This does not prove regular-file type or successful read.
patch_files, patch_parse_error Parsed touched paths and parser error text. Missing artifact skips parsing, leaving empty/nil paths and an empty error.
claims_valid, claim_error, stale_claims Claim validator outcome and stale-claim evidence; not authentication of the supplied/stored author.
resource_coverage, resource_error Coverage validator outcome when parsing succeeds. If skipped, nil error still yields true; it is not an independently executed-check marker.
temp_apply_error Copy/executor error if parsing and coverage succeed. Empty also occurs when skipped, so it is not sufficient proof of a completed dry run.
policy Runtime policy evaluation using stored risk, parsed paths, and stored votes. It can still be evaluated with no parsed paths after an artifact failure.
human_approvals, human_approval, human_approval_applied All proposal approvals plus the selected satisfying record/boolean under runtime rules, not a newly requested or verified human action.
security_reviews, security_review Stored history and latest selected review, not an automatic scan performed by validation.
security_review_required, security_review_satisfied Separate runtime gate; satisfied defaults true when review is not required.
valid Conjunction of no claim error, artifact stat success, no parser error, no coverage error, and no temporary-apply error. Excludes policy/human/security acceptance and tests.

If stat succeeds but reading fails, the read error is not returned directly: patch text remains empty and later parser/executor behavior supplies the failure evidence. This path has no dedicated patch_read_error or executed/skipped status per stage. Validation does not itself persist a test run, phase, validation revision, or main event, and does not update proposal status. It reads current state across several queries/filesystem operations rather than under one snapshot or lock. HTTP staged validation maps this result into its own projection and persists a separate test record; see Tests and Evidence.

Apply sequence

Temporary Workspace Copy

Validation creates an OS temporary directory named roundtable-patch-*; apply separately creates roundtable-apply-* and repeats the copy/application check. Both schedule recursive cleanup on return, with cleanup errors ignored. There is no configured temporary-root, copy-size limit, disk-space preflight, or retained dry-run workspace for inspection.

CopyWorkspace walks the current filesystem, not a Git commit/index export. It skips directories whose basename is exactly .git or .roundtable at any depth, but does not read .gitignore, skip dependency/build trees, filter secrets, or exclude ordinary hidden files. Untracked and already-modified files are copied. Large node_modules, build output, and local environment files can therefore increase work and copy sensitive material into temporary storage. A file named .git (as in some worktree layouts) is not excluded by the directory-only test.

Directories are created with requested mode 0755; files are opened and copied into newly created regular files. Original executable bits, permissions, ownership, timestamps, and extended attributes are not preserved. File symlinks are opened through their targets and copied as regular file contents; directory symlinks are not traversed as directories by this walk and can cause a copy failure when opened as a file. There is no symlink-containment check in this helper. Other special filesystem entries are not explicitly filtered. Copy/close errors can fail the dry run for reasons unrelated to patch validity.

The walk is not a snapshot or a context-aware bounded operation: concurrent external writes can produce a mixed-time copy, and request cancellation does not independently interrupt its file-copy loop. Successful patch application to this copy proves applicability to that copied view, not preservation of filesystem metadata, clean Git state, test success, or absence of secrets. It also does not eliminate the race before authoritative application. Review the actual worktree and recorded gates separately.

patch.apply repeats validation immediately before mutation. It refuses invalid patches, policy results that are not approvable without the required human override, and unsatisfied required security review. It then:

  1. Reads the stored patch and parses touched files.
  2. Captures pre-apply file contents for a rollback artifact.
  3. Copies the workspace into a temporary directory and verifies patch application there.
  4. Computes a before-state hash, applies the patch to the authoritative workspace, and computes an after-state hash.
  5. Marks the proposal accepted, writes a rollback JSON artifact under .roundtable/patches/, and persists a transaction with proposal/run/applier, hashes, status, timestamps, rollback path, and metadata.

repo.RepoStateHash combines Git HEAD (when available) with a SHA-256 of sorted workspace files, excluding .git and .roundtable; outside a Git repository it uses the workspace hash. This is not solely a Git tree hash and may include pre-existing uncommitted files.

Local Apply Inputs and Persistence Ordering

patch.apply requires an actual nonempty string proposal_id, without trimming whitespace. It runs patch.validate first, so a refused apply can already have persisted resource hashes or suspended stale claims. Invalid validation becomes the generic error proposal validation failed; inspect explicit validation results and stored claims for details. A missing typed policy result produces policy evaluation unavailable. A nonapprovable policy is refused unless the returned human-approval satisfaction flag is true; the required security-review gate is checked afterward and is not bypassed by that human flag.

Input Local service behavior
transaction_id Nonempty string retained as supplied; otherwise generated TX- timestamp ID. Used as the transaction upsert key and rollback-artifact filename component.
run_id Nonempty string retained; otherwise RUN-UNSPECIFIED. No run-existence or proposal/run association check here.
applied_by Nonempty string retained; otherwise orchestrator. Attribution, not authenticated identity.

This method does not independently restrict proposal prior status, require a new transaction ID, enforce idempotency, or verify a caller’s author/applier ownership. Reusing an ID can replace a transaction row and overwrite its rollback artifact rather than create an immutable new history entry. These are local service semantics, distinct from HTTP middleware or transaction-control handlers.

After validation, the method reloads the proposal and rereads/reparses its patch; it does not compare those bytes with a validated digest. It captures rollback contents, makes another temporary copy/application, computes the before hash, applies authoritatively, and computes the after hash. It then saves proposal status accepted, writes rollback JSON, upserts the transaction, and reloads proposal and transaction for the response. No transaction encompasses this sequence, and no dedicated event, vote, decision, test run, claim release, Git commit, or HTTP phase is created by this method.

Failure point Evidence or effects that may already remain
Validation or gate refusal Validation-time resource updates and claim suspensions; no authoritative patch execution yet.
Patch reread/parse, rollback capture, temporary copy/apply, or before hash No authoritative patch application from this attempt; earlier validation writes remain. Temporary directory removal is deferred and its error ignored.
Authoritative executor error Earlier file/hunk changes and executor-created artifacts can remain; rollback entries exist only in memory and have not yet been written.
After-state hash or proposal save error Authoritative files have changed, but rollback JSON and transaction upsert have not yet occurred.
Rollback write error Files changed and proposal saved as accepted; artifact may be absent/partial and transaction is not yet upserted.
Transaction upsert error Files changed, proposal accepted, rollback artifact written; no successful transaction persistence is established.
Final proposal/transaction reload error All preceding writes may have succeeded even though the tool returns an error instead of its success response.

Successful transaction metadata contains mode: applied, policy summary, parsed patch files, and selected human-approval/security-review IDs (which may be empty); applied time is current UTC RFC3339. The response returns proposal, transaction, policy, human_approval, and security_review. Neither an error nor response loss proves that mutation did not happen. Preserve evidence and inspect current files/rows before retrying; this method performs no compensating restoration.

Repository-State Hash Format

WorkspaceHash walks the selected root and skips directories named exactly .git or .roundtable at any depth. It includes other nondirectory paths regardless of Git tracking/ignore rules, slash-normalizes and sorts their relative names, then hashes each relative path, a NUL byte, full file bytes, and another NUL byte in that order with SHA-256. The result is lowercase hexadecimal. Empty directories contribute nothing. Permissions, executable bits, ownership, timestamps, extended attributes, symlink identity, and Git staging state are not encoded. File symlinks are read through their targets; unreadable paths or directory symlinks read as files can fail hashing.

RepoStateHash first runs Git discovery/HEAD commands, then computes that filesystem hash. Outside Git it returns only the hexadecimal workspace hash. Inside Git it returns git:<HEAD>:<workspace-hash>; an unborn HEAD recognized by its error-message checks uses no-head in place of HEAD. Those checks inspect specific command-error text rather than a structured Git status. Other Git failures abort hashing. Git commands have no context timeout in this helper, and the discovered top-level path is not used to broaden hashing beyond the supplied root. A root inside a larger repository can therefore combine that repository’s HEAD with only the selected subtree’s files.

Branch name, remote/upstream, merge state, reflog, and clean-worktree status are not separate hash components. Two branches at the same commit with identical included files can match. Mode-only changes or empty-directory changes can match too. Conversely, ignored build/dependency/environment files can change the hash without a proposal modification. A .git file is not excluded by the directory-only rule. Treat transaction before_git_hash/after_git_hash as these mixed Git/filesystem labels, not pure Git object IDs, signatures, or a full filesystem backup.

Hashing reads the current filesystem sequentially with no snapshot lock, size cap, cancellation checks, or per-file manifest output. Concurrent changes can yield mixed-time evidence, and a failure after authoritative apply can prevent recording the after hash. Hash equality does not prove tests passed, files are safe, or later work may be overwritten. Preserve current diffs, transaction records, and recovery artifacts alongside hashes before any manual restoration.

Important guarantees and gaps

External Patch Executor

Temporary and authoritative application both call exec.Command("patch", "-p1") with the chosen root as working directory and the full patch text on stdin. The executable must be available on the Roundtable process’s PATH. This is the system patch utility, not git apply, a Go-native diff engine, a configured adapter command, or a shell pipeline. -p1 strips one leading pathname component according to that installed utility’s behavior. No explicit batch, forward-only, fuzz, reject-file, backup-file, or binary-mode options are passed; parser recognition of a change does not guarantee that the installed executor supports all its Git diff metadata.

The command inherits the process environment and has no context cancellation, timeout, resource limit, sandbox, or output-byte cap in this helper. It collects combined stdout/stderr in memory. Success discards that output and returns only nil error; failure includes the exit error and trimmed combined output in patch apply failed: ..., without general secret redaction here. Review who can read tool errors and artifacts. Different platform implementations/versions can behave differently; a documentation build does not probe this executable.

The helper does not stage files, commit, reset, switch branches, enforce a clean worktree, run tests, or inspect/reverse partial effects. A nonzero exit can follow earlier file/hunk changes; the caller performs no automatic filesystem rollback. Temporary checks reduce ordinary context failures but do not establish an atomic authoritative apply or eliminate concurrent-write races. Inspect changed files and any executor-created artifacts after an uncertain failure before retrying. Missing patch fails command startup rather than activating a fallback implementation.

Recovery

Before retrying an uncertain apply, inspect the workspace diff, proposal state, transaction rows, and .roundtable/patches/*-rollback.json. Preserve the rollback artifact and database/WAL files. Restore only after reviewing the exact changed paths and confirming no later work would be overwritten. The service does not expose a general automatic rollback command today.

Rollback Artifact Format

Runtime apply captures rollback entries before authoritative mutation, using the parsed touched-file list. The eventual .roundtable/patches/<transaction-id>-rollback.json is an indented JSON array, not a reverse diff or an automatically executable restore plan:

[
  {"path": "src/example.go", "existed": true, "content": "previous text\n"},
  {"path": "src/new.go", "existed": false, "content": ""}
]

path is the supplied parsed relative path. Successful os.ReadFile records existed: true and converts all bytes to a Go string. A not-exist error records false with empty content; other read errors abort capture. Empty existing files are distinguishable from missing files through the flag. This helper does not capture directory trees, file permissions, ownership, timestamps, executable bits, symlink identity/target metadata, extended attributes, or Git index state. Reading a symlink follows its target. JSON string serialization can replace invalid UTF-8, so the artifact is not a byte-exact binary-file backup.

Writing creates parent directories with requested mode 0755 and writes the artifact with requested mode 0644, subject to umask/existing permissions. It uses an ordinary direct write rather than exclusive creation, atomic rename, fsync, checksum validation, encryption, or restrictive secret-specific permissions. Reusing a transaction ID can reuse/overwrite its artifact path. File contents are not redacted; preserve access controls and do not publish recovery artifacts in documentation or Git.

Capture is in memory before apply, but artifact writing occurs after files have changed and the proposal has been marked accepted. A later artifact-write or transaction-persistence failure can therefore leave mutation without a complete saved recovery record. There is no schema version, before/after hash embedded in this array, later-work conflict check, or general restore handler. Compare transaction hashes, current diffs, and every affected path before creating a governed recovery patch. The existed: false flag describes the capture-time state, not authorization to delete a file now containing later work.

HTTP phase history and recovery advice

The API exposes GET /api/v1/workspaces/{id}/transactions/{transaction_id}/phases and /recovery. Both resolve the workspace and require a transaction associated with that workspace; lookup failure returns 404 transaction_not_found. Neither endpoint executes a phase, retries an apply, restores files, or invokes the runtime transaction manager.

Phase history reads transaction_phases rows for the transaction, ordered by created_at then ID ascending. Items include ID, transaction ID, phase, status, actor, optional reason/request ID, inputs/outputs objects, optional log reference/failure code, recovery state, optional start/end timestamps, and creation time. Null recovery state becomes recoverable; this fallback is not a verified recovery assessment. Null, malformed, or non-object inputs/outputs become empty objects. Reason receives text redaction, but inputs/outputs are decoded without payload redaction. Actor, log reference, failure code, and other phase columns are not independently text-redacted here. Do not place secrets or internal reasoning in these records. A log reference is metadata, not an automatic log download.

All phase rows are loaded before shared offset pagination, with default limit 50 and accepted range 1 through 200. Query/scan failures return phase_list_failed; final iteration errors are not separately checked. A page boundary is not a transaction snapshot, and stored phases are not proof that filesystem changes and audit records were committed atomically.

Recovery advice returns transaction_id, stored status, repository_state, an actions array, and optional blocked_reason. Its entire decision is a fixed mapping from transaction status:

Stored status Advertised actions Repository state
pending stage, validate, cancel unknown
staged validate, apply, cancel unknown
failed retry, cancel, compensate unknown
cancelled compensate unknown
applied Empty verified
Any other value Empty; reason transaction state has no safe recovery action unknown

The literal verified for applied does not result from inspecting Git, file hashes, rollback artifacts, phase outputs, or current repository integrity. Action labels are advice, not authorization, capability discovery, or a guarantee that an executable endpoint exists for each label. An unfamiliar stored state, including a runtime-specific terminal state, can receive no recommended action even if manual investigation remains possible. Always inspect actual repository state and relevant runtime recovery evidence before retrying or compensating.

HTTP Transaction Control

Transaction list/detail membership is determined by joining the transaction’s proposal to the selected workspace, not by transaction run_id. Orphaned transactions or transactions whose proposal belongs elsewhere are not returned. List orders by transaction ID and loads all rows before shared offset pagination; there are no status/run filters in this handler. Query/scan errors return HTTP 500 transaction_list_failed; final iteration errors are not checked separately. Detail lookup errors become HTTP 404 transaction_not_found, including underlying database failures.

The projection contains ID, selected workspace ID, proposal/run IDs, before hash, optional after hash, status, applier, optional application time and rollback path, and metadata. Metadata uses object decoding with {} for null/malformed/non-object JSON, without payload redaction. Hash fields retain their historical git_hash names but have the runtime hash semantics explained above.

Every control action requires human authorization and a nonempty Idempotency-Key header, plus common authentication. This handler checks header presence rather than trimmed content and has no local replay cache. It does not decode a JSON request body. reason for status actions comes exclusively from the URL query and receives limited text redaction; avoid sensitive reasons in URLs, which can also appear in access logs.

Status Actions and Validation

stage, retry, and cancel map to stored statuses staged, pending, and cancelled. They do not copy a patch, schedule a worker, rerun validation, stop a process, restore files, or update proposal transaction substates. The SQL update excludes rows already applied or cancelled, but does not check affected-row count. Thus an action on either terminal status can succeed, leave status unchanged, and still add a completed phase and audit row. Other existing statuses are not subjected to action-specific preconditions.

The status update, phase insertion, and transaction.<action> audit insertion share one SQL transaction. Phase actor is the supplied actor header; start/end timestamps are the same current UTC RFC3339Nano value. No executable inputs/outputs or failure record are populated by this action. Begin errors use HTTP 500 transaction_transition_failed; write/commit failures use HTTP 409. A commit-only failure can produce an uninformative detail because its error is not retained. Post-commit reload errors are ignored; success returns HTTP 200 with the transaction projection and does not append a main event.

validate loads the workspace-owned transaction and calls runtime PatchValidate for its stored proposal ID. Empty proposal ID returns HTTP 409 transaction_invalid; service errors return transaction_validation_failed. Success returns the runtime validation object with HTTP 200, not the transaction projection. It does not advance status, persist a phase, run expected tests, or guarantee apply eligibility; inspect the separate policy/security/human fields described above. Unknown actions, after transaction lookup, return HTTP 400 invalid_transaction_action.

Apply Routing and Authorization Boundaries

apply additionally requires exact X-Roundtable-Orchestrator: true. This supplied header is a routing guard, not independent verification of a transaction-manager identity. Proposal ID comes from a route parameter when present, otherwise the proposal_id query parameter; missing ID returns HTTP 400 proposal_id_required. JSON body fields are ignored.

Unlike other actions, apply branches before the workspace-scoped transaction lookup. It passes the supplied proposal ID and route transaction ID directly to runtime PatchApply, using the selected workspace root, workspace ID as run_id, and actor header as applied_by. This HTTP branch does not verify that the supplied proposal belongs to the selected workspace, matches an existing transaction, or is associated with the route transaction ID. The service loads proposals by ID and runs its own governance checks; this is not equivalent to the missing HTTP association checks. Do not rely on list/detail membership filtering as authorization for apply.

The service performs the actual repository mutation sequence documented above, not a queued phase transition. It can create/upsert a transaction using the supplied ID rather than requiring a previously staged transaction. Success returns HTTP 200 with runtime proposal, transaction, policy, human-approval, and security-review objects. Every returned service error maps to HTTP 409 transaction_apply_blocked; that label does not prove files remained unchanged, since failures can occur after authoritative mutation. Preserve and inspect repository state before retrying.

Compensation Is Proposal Creation Only

compensate requires a workspace-owned transaction and a nonempty stored rollback path, otherwise HTTP 409 compensation_unavailable. It does not check transaction status, current hashes, artifact existence, artifact contents, or whether later work would be overwritten. It generates compensating-<UnixNano> and inserts a new high-risk, pending proposal with empty task ID, actor header as agent ID, title/summary naming the original transaction, approval state pending, transaction state not_started, and patch path copied verbatim from the rollback path. No affected-resource rows are created here.

The runtime’s rollback artifact is JSON containing recovery entries, not a unified reverse patch. This endpoint does not convert it; the resulting proposal therefore does not by itself provide a patch suitable for normal validation/application. Review recovery evidence and prepare a valid governed patch with resources before attempting restoration. No automatic rollback occurs and the original transaction is unchanged.

Proposal insertion and audit are separate writes, with audit errors ignored. No main event, phase, approval request, or deduplication is created. Repetition can create multiple compensating proposals. Insert failure returns HTTP 409 compensation_failed; success returns HTTP 201 with proposal ID, original transaction ID, copied patch path, and literal pending status.

HTTP Patch Preview Boundaries

Proposal patch metadata, files, diff, and symbols endpoints load a workspace-owned proposal and read its stored patch path from disk on each request. The path must be nonblank and repository-relative; lexical normalization rejects root/parent escapes and absolute paths. The helper does not resolve symlinks, so a lexically contained path is not proof that the actual read target stays inside the workspace. Patch contents are read entirely into memory and parsed before pagination; there is no configured patch-byte limit or immutable preview snapshot.

Proposal lookup failures map to 404 proposal_not_found; other path/read/parser failures map to 400 patch_unavailable. A successful preview does not perform git apply --check, verify leases, evaluate policy, run tests, or authorize transaction application.

Metadata returns proposal ID, stored patch path/base revision, inspected current HEAD, stale-base flag, file count, added/removed counts, and binary count. Repository inspection errors are ignored. Stale-base is true when a nonempty stored base differs from the returned HEAD, including an empty HEAD after inspection failure. The line-count labels are not actual diff-line statistics: added lines counts parsed change-range entries, removed lines counts deleted-file entries, and binary count remains zero in this handler. Do not use these fields as code churn or risk measurements.

File items include path, old/new paths, operation, binary flag, and added/removed counts. Path prefers new path, falling back to old. Operation precedence is rename, create, delete, then modify. Added/removed counts are left at zero. Binary detection is a simple search for Binary files a/<path> or Binary files b/<path> in the whole patch, not comprehensive Git binary-patch decoding. Lists use shared offset pagination, default 50, limits 1 through 200.

Diff returns paginated strings from splitting the text-redacted patch on newline, not structured hunks or HTML. A trailing newline produces a final empty string. Limited text redaction can modify diff content, so the response is not an exact downloadable patch or a suitable source for applying changes. It does not sanitize arbitrary secret content or validate Markdown/HTML rendering.

Symbols indexes every currently readable touched file in the existing worktree, not the proposed post-patch contents or only declarations overlapping changed ranges. Unsafe paths and indexing errors are silently skipped. Deleted, unsupported, or not-yet-created files can contribute no symbols without an error response. Items expose path, resource ID, kind, name, and line range, potentially using absolute indexer paths. Treat them as current-file context subject to Symbol Index limitations, not a verified semantic impact analysis.

Separate preview requests can observe different patch bytes, repository HEADs, or file contents. Preserve authoritative patch artifacts and use the actual validation/application pipeline before any governed mutation.

proposal.create, proposal.attach_patch, proposal.request_review, vote.cast, decision.record, security.review, human.request_approval, patch.validate, patch.apply, and patch.reject are documented in MCP Tools. Policy syntax is documented in Governance Policies.

HTTP Proposal State Transitions

Creation and Listing

Internal HTTP creation requires exact X-Roundtable-Orchestrator: true and a nonblank trimmed idempotency key in addition to common authentication. This supplied marker is not proof of a verified coordinator identity. Title and patch path are trimmed; summary is text-redacted but not trimmed, so whitespace-only summary can pass. All three must be nonempty after that processing. Path checks reject any .. substring (including benign filenames containing it) or leading /; they do not inspect patch existence, resolve symlinks, parse the patch, or establish claim coverage. Preview and actual validation perform separate checks.

Omitted proposal ID receives a timestamp-derived value; supplied ID is used without trimming. Empty risk defaults to normal, while arbitrary nonempty risk strings are accepted. Deliberation ID, proposer session ID, base revision, and resource IDs are stored without handler-level association/eligibility checks. Task and agent IDs are inserted as empty strings. Initial main status and vote/policy/approval substates are pending; transaction state is not_started. No vote, policy evaluation, approval request, review assignment, or execution begins automatically.

Proposal insert, supplied resource-link inserts, and creation audit share a transaction. Resource IDs are not deduplicated by the handler, so database constraints can reject duplicates or invalid links. Begin failure uses proposal_create_failed; insert/link/audit/commit failures use proposal_create_conflict. After commit, reload and separate main-event errors are ignored. HTTP 201 is therefore not proof of a populated response or workspace event delivery; the event run ID is the proposal ID.

List selects exact workspace membership, oldest creation time then ID, and performs a separate detail read per row before offset pagination. It has no status/risk/agent/search filters. Per-row detail errors silently omit records, while initial query/scan/final-iteration failures return proposal_list_failed. Default limit is 50, maximum 200; small pages do not bound the initial collection load. Detail lookup errors use proposal_not_found. Neither read recalculates governance substates or verifies the stored patch artifact.

Transition Effects

The HTTP proposal transition handler requires human authorization, a nonblank trimmed Idempotency-Key, and exact proposal/workspace membership. It recognizes only these actions:

Action Allowed source states Target state
request-review pending in_review
reject pending, in_review rejected
withdraw pending, in_review withdrawn

Unknown actions return invalid_proposal_transition; disallowed source states return 409 illegal_proposal_transition. This handler does not decode a request body or require a rejection/withdrawal reason. Request-review does not assign reviewers, cast votes, evaluate policy, acquire claims, or start tests. Reject/withdraw do not release claims or restore files. Approval/application use separate surfaces; action labels alone are not runtime transaction operations.

State update and before/after audit insertion share a SQL transaction. The update includes the previously read status in its predicate, but affected-row count is not checked: a concurrent state change can cause zero updated rows while audit is still inserted and the response succeeds. Begin failure uses HTTP 500 proposal_transition_failed; SQL/commit failure uses HTTP 409 proposal_transition_conflict. Vote, policy, approval, and transaction substate columns are not reset or recalculated by this handler.

After commit, proposal reload and separate main-event append errors are ignored. Main event run ID is the proposal ID, and event type uses the raw hyphenated action (proposal.request-review), while audit action replaces hyphens with underscores (proposal.request_review). This is not guaranteed workspace live-stream membership. HTTP 200 returns the reloaded projection when available, not evidence that review work or event delivery occurred. Proposal reads expose stored substates and text-redact title, summary, and patch path; those values are labels, not verified up-to-date consensus or policy outcomes.