You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Schemas for Task, Result, Effect and Disposition #19
changed the title [-]Schemas the first slice uses and the repo lacks: Task, Result, Effect, Disposition, Role manifest, pass record[/-][+]Schemas for Task, Result, Effect and Disposition[/+]on Oct 11, 2026
Solution Architect, delegated. On main at c094a8b (#170). Read with P-13's answer: CLI 2.1.302, origin/trials/p-13-cli at a0fa60b, and the redacted answer's fields. Also read trials/P-17.json, the working ruling on #161 (record 0034) and design §3.1, §3.3, §3.5 to §3.10 and §4.3.
What this story decides. Four schemas and what each field may hold. Which fields the model writes and which the Factory writes. Three gaps in the design shape the answer.
A Role's Finding has no surface yet when its Result is written. §3.7 derives a Finding's id from its source and its surface key. A Role's Finding gets its surface, a review thread or an annotation, only when apply posts it. §3.3 still reads a thread's category and severity "from that Lens's own Finding record, in its Run's result.json, joined by surface.key". So the Result cannot hold the key the join needs.
Some fields in §3.5 and §3.8 claim an identity. A Disposition's by, its content_id, and a Finding's source are things a model would be asserting. Each is already known to the Factory from the Task.
The validator has no oneOf, anyOf or if. P-13 proved only the keywords the validator implements (type, properties, required, additionalProperties, enum, const, pattern, items), handed to the CLI with no $ref. A list of Effects shaped {type, ...} can require no field of any one type.
P-13 adds one fact. The answer's usage gave input_tokens 2, cache_creation_input_tokens 20475 and output_tokens 194. Its total_cost_usd was 0.167688, with num_turns, duration_ms, session_id and the model as the one key of modelUsage. Input tokens alone would count about one ten-thousandth of the input.
1. The Result asks, and names no one. A Result holds what the Role judged, and only that. The Harness fills every field that says who the Role is, what it ran on, what Release ran it and what it cost. It takes them from the Task and from the CLI's own answer.
Written by the Engine: schema, run, status (completed, incomplete or refused only), reason, dispositions, findings, effects, coverage.
Filled by the Harness: release and usage. On an answer that does not validate, it writes the whole Result itself: statusfailed, reasonresult_invalid, empty lists, and usage. superseded is also written only by the Harness or apply.
No field in a Result holds a Role, an App, an actor, a content_id or a commit. The schema allows no such field, so a model cannot claim one.
Why: decision 0004 says a model "is able to ask, in a form a schema can refuse". A by: pr-review written by the model is the model saying who it is. apply already knows the Role from the Task. Taking it from the Result would let text the Run read decide who owns a Finding.
2. The four schemas. Each one's own fields are below. "Code" means held by enum, const or an anchored pattern with no space. "Text" means any string. Only a field named body may be text, and no text reaches a decision (ruling 5).
schemas/task.schema.json, task/1, a document. It is built by the Harness from the Decision, and no field comes from the model.
Field
Holds
schema
consttask/1
run
^r_[0-9A-Z]{6,26}$, the Run's identity, minted by the Controller before dispatch (0034)
{repo, type, number}, item as in decision.schema.json
content_id
^sha256:[0-9a-f]{4,64}$
findings
Finding ids, ^f_[0-9a-f]{4,64}$
texts
{id, trust, source, body}: id^[a-z]+:[A-Za-z0-9_-]+$; trust one of maintainer, role, advisory, outside; source a login, ^[A-Za-z0-9-]+(\[bot\])?$; body is text. It goes to the model as data and to nothing else
limits
turns, repair_turns, seconds: integers, at least 1 (repair_turns at least 0); cost_ceiling_usd: a number, at least 0
tier
standard or deep
permitted_effects
each {"$ref": "effect.schema.json#/$defs/type"}
result_schema
constresult/1
schemas/result.schema.json, result/1, a document. It is what run uploads as result.json.
Field
Holds
schema, run, release
As in the Task. They must equal the Task's, and apply checks that
^[a-z_]+$ or null: why a Run did not complete; null when it did
dispositions
items {"$ref": "disposition.schema.json"}
findings
raised Findings, $defs/raised, ruling 3
effects
{"$ref": "effect.schema.json"}
coverage
{"$ref": "coverage.schema.json"}; [] for an Intent that reviews nothing (0029)
usage
engineconstclaude-cli; engine_version^[0-9]+\.[0-9]+\.[0-9]+$; model^[a-z0-9.-]+$; session a UUID pattern; input_tokens, cache_creation_input_tokens, cache_read_input_tokens, output_tokens and turns, each an integer at least 0 or null; cost_usd and seconds, each a number at least 0 or null. All required. A count is null only where the CLI never answered, and then seconds is the measured duration (§3.10)
bundle and preflight (§3.5) are left out. Slice 1's review Intent changes no code. They come with the author Intents in slice 2. A field added later is optional, or the major version goes up, because the control room reads old artifacts with new code.
schemas/effect.schema.json, effect/1, a part. It is the Effects one Result asks for, as one list for each type. It is an object with additionalProperties: false, every property optional, each property an array of one strict shape. $defs/type is the enum of its property names. Every Effect has an id, ^e[0-9]{1,3}$, and create_check_run's external_id is built from it (§3.6).
Type
Fields
submit_review
id; comments: {finding, body}, where finding is a raised Finding's ref and body is text; body, text or null. It has no event and no commit. apply submits it as a comment on the commit the Decision admitted, so a Role's review never approves. Approval stays the act Run's (§3.10)
create_check_run
id; annotations: {finding, body}. The name and the conclusion are apply's, taken from the Task's Role and how the Run ended
resolve_thread
id; thread, ^PRRT_[A-Za-z0-9_-]+$
comment
id; on, ^(item|PRRT_[A-Za-z0-9_-]+)$; body, text
The other types in §3.6 come with the Intents that need them: push_commits, open_pull_request, mark_ready, request_review, add_reaction. add_label, assign, enqueue_merge and dequeue_merge are the Controller's acts. They are never a Result's Effects, so effect/1 has no property for them. Why one list for each type: with no oneOf, it is the only shape in which "a resolve_thread with no thread" is something the schema can refuse. A Result that fails validation applies nothing (0004), so the schema is the guard. It also fixes the order in which apply writes.
schemas/disposition.schema.json, disposition/1, a part. It has finding (a Finding id) and one half: proposal, {outcome, reason}, where outcome is fixed, declined, deferred or not_applicable; or decision, {result, reason}, where result is accepted or rejected. Each reason is optional, ^[a-z_]+$. There is no by and no content_id: the Run's Role and content are the Task's. This is §3.5's shape, which P-13 validated. "Exactly one half, and the half this Run's Role may make" depends on the Task's Role and the authority table, so apply checks it and the schema does not (ruling 7).
3. A raised Finding, and where its record lives. In a Result, findings items are $defs/raised: {ref, location, category, severity, criterion, adopted_from}.
ref^n[0-9]{1,3}$, local to the Result.
location as finding/1, its path^[^\s]+$.
category and severity are finding/1's enums, copied.
criterion^[A-Z]+-[0-9]+$ or null.
adopted_from as finding/1.
No id, source, surface or content_id.
The one Effect that posts it names its ref: a submit_review comment for a Finding that gates, an annotation for one that does not (§4.3). After apply reads the posted surface back, it writes the full finding/1 into applied.json. The id is derived from the source and the surface key, as §3.7 says. The source comes from the Task's Role and its App, and content_id from the Task. The facts port joins a thread's category and severity from that record by surface.key. Why: the Finding record is written once, when all of it is known. finding/1 does not change. kernel/coverage.py's covered reads only criterion, which a raised Finding has under the same name.
4. One definition, referenced by file.tools/jsonschema_lite.py's $ref resolves <kind>.schema.json and <kind>.schema.json#/$defs/<name>. It looks them up in a mapping of kind to schema that the caller passes: validate(value, schema, ..., schemas=None). The referenced schema becomes the root for its own #/$defs references. A reference with no mapping, or to a kind the mapping lacks, raises SchemaError: it fails closed, as an unknown keyword does. tools/check_examples.py passes every schema in schemas/. The validator still reads no file. Why: decision 0029 asks for the Result to "take coverage/1 by reference". #28's pass record must carry facts/1, at 627 lines, which a copy would double. The Harness hands the CLI a copy with the references resolved, which waits on harness/. That keeps P-13's proven form, no $ref, in front of the model.
5. Only a body is text. In tools/check_examples.py, CODES_ONLY names task, result, effect, disposition and run-name. In those schemas, every node whose type admits a string has an enum, a const or a pattern anchored at both ends, unless it is a property named body. #28 adds its new schemas to the set. Why: this is the rule "no text from outside reaches a decision", held structurally for what one Run carries. A body goes out to GitHub or in to the model. The facts port never reads one back (§3.3).
6. The run-name, for #161.#161 left the pattern to this story. schemas/run-name.schema.json, a part, is a string schema: ^sf [A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[1-9][0-9]* [a-z][a-z-]*[a-z] [a-z][a-z-]*[a-z] sha256:[0-9a-f]{4,64} r_[0-9A-Z]{6,26}$.
The fields are repo and item, Intent, Role, the full content_id and run, each copied from the Task, with no c: or r: tag. Both values carry their own prefix. Why the full content id: the records port fills the run record's content_id from the name (0034), and the Kernel compares it whole with the facts'. A shortened one would never match. GitHub's limit on a run-name's length has not been tried. With a 64-digit content id and a 26-character run id it is about 160 characters, and P-17's were shorter. That is recorded as unverified, not a blocker.
7. Examples and counter-examples. Each counter-example is wrong in exactly one way: the validator must give exactly one error.
Examples:
task.json: §3.5's, adjudicate-findings.
result.json: a review-change that raises a major Finding on a thread and a minor one as an annotation, with coverage. Its model is not a full model id, which tools/check_repo.py keeps to fixtures.
result.adjudication.json: §3.5's, two Dispositions and a resolve_thread.
result.failed.json: written by the Harness, with null counts.
effect.json: all four types.
disposition.json: an owner's decision. disposition.proposal.json: an author's proposal.
run-name.json.
invalid/task.: text-of-no-trust-class, with trust trusted; a-permitted-effect-a-result-cannot-request, with enqueue_merge; a-run-id-in-another-form, 01JA7Q; no-release.
invalid/result.:
sentence-as-reason: the issue's third criterion.
carries-a-commit-hash.
a-finding-that-names-its-source.
a-status-the-design-does-not-know.
usage-without-the-tokens-written-to-the-cache: drop cache_creation_input_tokens alone.
invalid/effect.: an-act-of-the-controller, enqueue_merge; a-review-that-approves, an event on submit_review; a-thread-named-in-words; a-check-run-with-a-conclusion.
invalid/disposition.: sentence-as-reason; names-who-decided, a by; a-result-in-words; an-outcome-the-author-cannot-propose, a proposal of accepted.
invalid/run-name.: the-workflows-own-name, the workflow's name: alone, as P-17 saw it listed; a-run-id-in-another-form, §3.10's old r:01JA7Q.
"Neither half, or both" cannot be a counter-example, because the schema accepts it. It is a Boundary row waiting on harness/ (ruling 9).
8. Tests and mutants, written first.
In tests/test_tools.py:
test_a_reference_to_another_schema_is_followed: valid and invalid through a file reference and through a #/$defs fragment. A referenced schema's own #/$defs reference resolves against that schema, not the referrer.
test_a_reference_to_a_schema_not_given_is_refused: SchemaError, with no mapping, and with a kind the mapping lacks.
test_a_string_that_is_neither_a_code_nor_a_body_is_caught: a bare string, an unanchored pattern, and a body that passes.
tools/check_authority.py also holds the Result's raised-Finding category and severity enums to finding/1's, as it holds the facts' thread categories. Test: test_a_raised_finding_with_a_category_nobody_owns_is_caught.
Mutants, appended, each with text that appears once:
"a referenced schema is read against the referrer's root": the root is not switched. The first test kills it.
"a reference to a schema not given accepts anything". The second test kills it.
"a free string in a Run's schema passes": the CODES_ONLY condition becomes False. The third test kills it.
"a raised Finding may carry a category nobody owns". The check_authority test kills it.
9. Record and design. A rule changes: where a Role's Finding record lives, and that a Result names no one. So this takes a new record, 0035, "A Result asks and names no one; apply writes the Finding it posted". It merges after 0033 and 0034. Its Decision is rulings 1, 3, 4 and 6.
Its Boundaries name:
the counter-examples above, and the four tests;
waiting on harness/:
the Harness hands the CLI result/1 with its references resolved and the fields the Harness fills removed, status narrowed to completed, incomplete and refused, and effects narrowed to the Task's permitted_effects;
the Harness fills release and usage from the CLI's own answer, and writes a failed Result, result_invalid, for an answer that does not validate;
apply refuses a Result whose run or release is not the Task's, an Effect type the Task does not permit, an Effect id or a Finding ref given twice, a raised Finding posted other than exactly once, or one that does not gate posted on a thread;
apply refuses a Disposition with neither half or both, or a half its Role may not make: an author proposes and the owner of the category decides (config/authority.json);
apply submits a Role's review as a comment on the admitted commit, never an approval;
apply writes each posted Finding's finding/1 into applied.json, its source from the Task;
waiting on adapters/: the facts port joins a thread's category and severity from applied.json; the Controller names a Run by run-name/1, and the records port parses it.
Rows elsewhere:
0029's row "The Result's schema takes coverage/1 by reference …" changes its Check cell to schemas/result.schema.json, tools/check_examples.py.
0004 gains a row: "A Result's schema has no field for a commit, a Role, an actor or a content id", naming invalid/result.carries-a-commit-hash.json and invalid/disposition.names-who-decided.json.
Design edits, in the same change:
§3.3, the threads row: "in its Run's result.json" becomes "in its Run's applied.json".
§3.5: the Result example and who writes what. usage gains its fields, from P-13.
§3.6: Effects listed by type, and which ones a Result may ask for.
§3.7: a Role raises a Finding in its Result, and apply writes its record.
§3.8: a Disposition in a Result has no by and no content_id.
§3.10: the run-name's form, and applied.json holds the posted Findings.
Can #19 and #28 run in parallel? No, not whole. #28 builds on this story: its pass record $refs facts.schema.json through ruling 4. Its role manifest refs effect.schema.json#/$defs/type. Its envelope check must classify task, result, effect, disposition and run-name.
Shared files: tools/jsonschema_lite.py, tools/check_examples.py, tests/test_tools.py, tools/mutants.py, schemas/AGENTS.md, examples/AGENTS.md, and the design's §3.7, which both edit.
Needs the Maintainer? None open. The one point below was answered on 2026-10-18: option (a) taken. applied.json and outcome.json are #28's fourth criterion, and its rulings define them. Ruling 3's applied.json is that schema.
The point as it was put:applied.json and outcome.json are artifacts of every slice-1 Run: the Kernel reads outcome.json, and with ruling 3 the facts port reads applied.json. Slice 0's exit asks for an example of every schema slice 1 uses. Neither has a story, and #19 already has four acceptance criteria.
Decision number, updated: this ruling's record is 0036, not 0035. #165 (PR #176) needs a record of its own and takes 0034, the next free number, because it merges sooner. Records merge in number order: 0032 #175, 0033 P-13, 0034 #165, 0035 #161, 0036 #19, 0037 #28. #26 takes a number when it is built.
Decision number, updated: this story's record is 0037 (it was 0036). #26's P-39 reach trial passed and is ready to build now, so it takes 0036, ahead of the stories still being ruled or built. Order: 0034 #165 (PR #176), 0035 #161, 0036 #26, 0037 #19, 0038 #28, 0039 #174 PR B.
Decision numbers change rule (Maintainer, 2026-10-19). A decision record takes its number when its pull request is clean and green and about to be enqueued, as the next free number. It is no longer fixed when the rulings are posted. The number posted on this issue is provisional. The builder renames the record at enqueue if an earlier PR took that number. CI's gapless decision check still holds the order. Boundary rows in other records name tests, never numbers, so nothing else moves. This ends the renumbering, and a story with a record no longer waits behind an unrelated story that holds a lower number.
Decision numbers are taken at enqueue (the Maintainer, 2026-10-19, on the plan review's recommendation). A record takes the next free number when its PR is clean and green and about to be enqueued, not when its ruling is posted. Rulings say "the next free number at enqueue". Boundary rows in other records name tests, never numbers, so nothing else moves. The order posted on 2026-10-19 (0035 #161, 0036 #26, 0037 #19, 0038 #28, 0039 #174 PR B) is the expected order, not a reservation: #161 keeps 0035 as the next to enqueue, and each of the others takes the next free number in the order it is actually enqueued. A rows-only PR still merges any time.
Dependency (plan review, 2026-10-19). This story waits on #161's record being on main, not on its code: nothing here reads unidentified_runs (that is #28's context table). Start now in lane s; cite trials/P-13.json, which is on main (#177).
Activity
[-]Schemas the first slice uses and the repo lacks: Task, Result, Effect, Disposition, Role manifest, pass record[/-][+]Schemas for Task, Result, Effect and Disposition[/+]don-petry commentedon Oct 18, 2026
Working rulings (overturnable in review)
Solution Architect, delegated. On main at c094a8b (#170). Read with P-13's answer: CLI 2.1.302,
origin/trials/p-13-cliat a0fa60b, and the redacted answer's fields. Also readtrials/P-17.json, the working ruling on #161 (record 0034) and design §3.1, §3.3, §3.5 to §3.10 and §4.3.What this story decides. Four schemas and what each field may hold. Which fields the model writes and which the Factory writes. Three gaps in the design shape the answer.
applyposts it. §3.3 still reads a thread's category and severity "from that Lens's own Finding record, in its Run'sresult.json, joined bysurface.key". So the Result cannot hold the key the join needs.by, itscontent_id, and a Finding'ssourceare things a model would be asserting. Each is already known to the Factory from the Task.oneOf,anyOforif. P-13 proved only the keywords the validator implements (type,properties,required,additionalProperties,enum,const,pattern,items), handed to the CLI with no$ref. A list of Effects shaped{type, ...}can require no field of any one type.P-13 adds one fact. The answer's
usagegaveinput_tokens2,cache_creation_input_tokens20475 andoutput_tokens194. Itstotal_cost_usdwas 0.167688, withnum_turns,duration_ms,session_idand the model as the one key ofmodelUsage. Input tokens alone would count about one ten-thousandth of the input.1. The Result asks, and names no one. A Result holds what the Role judged, and only that. The Harness fills every field that says who the Role is, what it ran on, what Release ran it and what it cost. It takes them from the Task and from the CLI's own answer.
schema,run,status(completed,incompleteorrefusedonly),reason,dispositions,findings,effects,coverage.releaseandusage. On an answer that does not validate, it writes the whole Result itself:statusfailed,reasonresult_invalid, empty lists, andusage.supersededis also written only by the Harness orapply.content_idor a commit. The schema allows no such field, so a model cannot claim one.by: pr-reviewwritten by the model is the model saying who it is.applyalready knows the Role from the Task. Taking it from the Result would let text the Run read decide who owns a Finding.2. The four schemas. Each one's own fields are below. "Code" means held by
enum,constor an anchoredpatternwith no space. "Text" means any string. Only a field namedbodymay be text, and no text reaches a decision (ruling 5).schemas/task.schema.json,task/1, a document. It is built by the Harness from the Decision, and no field comes from the model.schemaconsttask/1run^r_[0-9A-Z]{6,26}$, the Run's identity, minted by the Controller before dispatch (0034)release^factory/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$role,intent^[a-z][a-z-]*[a-z]$item{repo, type, number},itemas indecision.schema.jsoncontent_id^sha256:[0-9a-f]{4,64}$findings^f_[0-9a-f]{4,64}$texts{id, trust, source, body}:id^[a-z]+:[A-Za-z0-9_-]+$;trustone ofmaintainer,role,advisory,outside;sourcea login,^[A-Za-z0-9-]+(\[bot\])?$;bodyis text. It goes to the model as data and to nothing elselimitsturns,repair_turns,seconds: integers, at least 1 (repair_turnsat least 0);cost_ceiling_usd: a number, at least 0tierstandardordeeppermitted_effects{"$ref": "effect.schema.json#/$defs/type"}result_schemaconstresult/1schemas/result.schema.json,result/1, a document. It is whatrunuploads asresult.json.schema,run,releaseapplychecks thatstatuscompleted,incomplete,failed,refused,supersededreason^[a-z_]+$or null: why a Run did not complete; null when it diddispositions{"$ref": "disposition.schema.json"}findings$defs/raised, ruling 3effects{"$ref": "effect.schema.json"}coverage{"$ref": "coverage.schema.json"};[]for an Intent that reviews nothing (0029)usageengineconstclaude-cli;engine_version^[0-9]+\.[0-9]+\.[0-9]+$;model^[a-z0-9.-]+$;sessiona UUID pattern;input_tokens,cache_creation_input_tokens,cache_read_input_tokens,output_tokensandturns, each an integer at least 0 or null;cost_usdandseconds, each a number at least 0 or null. All required. A count is null only where the CLI never answered, and thensecondsis the measured duration (§3.10)bundleandpreflight(§3.5) are left out. Slice 1's review Intent changes no code. They come with the author Intents in slice 2. A field added later is optional, or the major version goes up, because the control room reads old artifacts with new code.schemas/effect.schema.json,effect/1, a part. It is the Effects one Result asks for, as one list for each type. It is an object withadditionalProperties: false, every property optional, each property an array of one strict shape.$defs/typeis theenumof its property names. Every Effect has anid,^e[0-9]{1,3}$, andcreate_check_run'sexternal_idis built from it (§3.6).submit_reviewid;comments:{finding, body}, wherefindingis a raised Finding'srefandbodyis text;body, text or null. It has no event and no commit.applysubmits it as a comment on the commit the Decision admitted, so a Role's review never approves. Approval stays the act Run's (§3.10)create_check_runid;annotations:{finding, body}. The name and the conclusion areapply's, taken from the Task's Role and how the Run endedresolve_threadid;thread,^PRRT_[A-Za-z0-9_-]+$commentid;on,^(item|PRRT_[A-Za-z0-9_-]+)$;body, textThe other types in §3.6 come with the Intents that need them:
push_commits,open_pull_request,mark_ready,request_review,add_reaction.add_label,assign,enqueue_mergeanddequeue_mergeare the Controller's acts. They are never a Result's Effects, soeffect/1has no property for them. Why one list for each type: with nooneOf, it is the only shape in which "aresolve_threadwith no thread" is something the schema can refuse. A Result that fails validation applies nothing (0004), so the schema is the guard. It also fixes the order in whichapplywrites.schemas/disposition.schema.json,disposition/1, a part. It hasfinding(a Finding id) and one half:proposal,{outcome, reason}, whereoutcomeisfixed,declined,deferredornot_applicable; ordecision,{result, reason}, whereresultisacceptedorrejected. Eachreasonis optional,^[a-z_]+$. There is nobyand nocontent_id: the Run's Role and content are the Task's. This is §3.5's shape, which P-13 validated. "Exactly one half, and the half this Run's Role may make" depends on the Task's Role and the authority table, soapplychecks it and the schema does not (ruling 7).3. A raised Finding, and where its record lives. In a Result,
findingsitems are$defs/raised:{ref, location, category, severity, criterion, adopted_from}.ref^n[0-9]{1,3}$, local to the Result.locationas finding/1, itspath^[^\s]+$.categoryandseverityare finding/1's enums, copied.criterion^[A-Z]+-[0-9]+$or null.adopted_fromas finding/1.id,source,surfaceorcontent_id.The one Effect that posts it names its
ref: asubmit_reviewcomment for a Finding that gates, an annotation for one that does not (§4.3). Afterapplyreads the posted surface back, it writes the fullfinding/1intoapplied.json. Theidis derived from the source and the surface key, as §3.7 says. Thesourcecomes from the Task's Role and its App, andcontent_idfrom the Task. The facts port joins a thread's category and severity from that record bysurface.key. Why: the Finding record is written once, when all of it is known.finding/1does not change.kernel/coverage.py'scoveredreads onlycriterion, which a raised Finding has under the same name.4. One definition, referenced by file.
tools/jsonschema_lite.py's$refresolves<kind>.schema.jsonand<kind>.schema.json#/$defs/<name>. It looks them up in a mapping of kind to schema that the caller passes:validate(value, schema, ..., schemas=None). The referenced schema becomes the root for its own#/$defsreferences. A reference with no mapping, or to a kind the mapping lacks, raisesSchemaError: it fails closed, as an unknown keyword does.tools/check_examples.pypasses every schema inschemas/. The validator still reads no file. Why: decision 0029 asks for the Result to "takecoverage/1by reference". #28's pass record must carryfacts/1, at 627 lines, which a copy would double. The Harness hands the CLI a copy with the references resolved, which waits onharness/. That keeps P-13's proven form, no$ref, in front of the model.5. Only a
bodyis text. Intools/check_examples.py,CODES_ONLYnamestask,result,effect,dispositionandrun-name. In those schemas, every node whosetypeadmits a string has anenum, aconstor apatternanchored at both ends, unless it is a property namedbody. #28 adds its new schemas to the set. Why: this is the rule "no text from outside reaches a decision", held structurally for what one Run carries. Abodygoes out to GitHub or in to the model. The facts port never reads one back (§3.3).6. The run-name, for #161. #161 left the pattern to this story.
schemas/run-name.schema.json, a part, is a string schema:^sf [A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[1-9][0-9]* [a-z][a-z-]*[a-z] [a-z][a-z-]*[a-z] sha256:[0-9a-f]{4,64} r_[0-9A-Z]{6,26}$.The fields are repo and item, Intent, Role, the full
content_idandrun, each copied from the Task, with noc:orr:tag. Both values carry their own prefix. Why the full content id: the records port fills the run record'scontent_idfrom the name (0034), and the Kernel compares it whole with the facts'. A shortened one would never match. GitHub's limit on a run-name's length has not been tried. With a 64-digit content id and a 26-character run id it is about 160 characters, and P-17's were shorter. That is recorded as unverified, not a blocker.7. Examples and counter-examples. Each counter-example is wrong in exactly one way: the validator must give exactly one error.
task.json: §3.5's,adjudicate-findings.result.json: areview-changethat raises a major Finding on a thread and a minor one as an annotation, with coverage. Itsmodelis not a full model id, whichtools/check_repo.pykeeps to fixtures.result.adjudication.json: §3.5's, two Dispositions and aresolve_thread.result.failed.json: written by the Harness, with null counts.effect.json: all four types.disposition.json: an owner's decision.disposition.proposal.json: an author's proposal.run-name.json.invalid/task.:text-of-no-trust-class, with trusttrusted;a-permitted-effect-a-result-cannot-request, withenqueue_merge;a-run-id-in-another-form,01JA7Q;no-release.invalid/result.:sentence-as-reason: the issue's third criterion.carries-a-commit-hash.a-finding-that-names-its-source.a-status-the-design-does-not-know.usage-without-the-tokens-written-to-the-cache: dropcache_creation_input_tokensalone.invalid/effect.:an-act-of-the-controller,enqueue_merge;a-review-that-approves, aneventonsubmit_review;a-thread-named-in-words;a-check-run-with-a-conclusion.invalid/disposition.:sentence-as-reason;names-who-decided, aby;a-result-in-words;an-outcome-the-author-cannot-propose, a proposal ofaccepted.invalid/run-name.:the-workflows-own-name, the workflow'sname:alone, as P-17 saw it listed;a-run-id-in-another-form, §3.10's oldr:01JA7Q."Neither half, or both" cannot be a counter-example, because the schema accepts it. It is a Boundary row waiting on
harness/(ruling 9).8. Tests and mutants, written first.
tests/test_tools.py:test_a_reference_to_another_schema_is_followed: valid and invalid through a file reference and through a#/$defsfragment. A referenced schema's own#/$defsreference resolves against that schema, not the referrer.test_a_reference_to_a_schema_not_given_is_refused:SchemaError, with no mapping, and with a kind the mapping lacks.test_a_string_that_is_neither_a_code_nor_a_body_is_caught: a bare string, an unanchored pattern, and abodythat passes.test_each_effect_type_is_requested_by_its_own_list:$defs/type's enum equalsproperties' keys.tools/check_authority.pyalso holds the Result's raised-Findingcategoryandseverityenums to finding/1's, as it holds the facts' thread categories. Test:test_a_raised_finding_with_a_category_nobody_owns_is_caught.CODES_ONLYcondition becomesFalse. The third test kills it.check_authoritytest kills it.9. Record and design. A rule changes: where a Role's Finding record lives, and that a Result names no one. So this takes a new record, 0035, "A Result asks and names no one;
applywrites the Finding it posted". It merges after 0033 and 0034. Its Decision is rulings 1, 3, 4 and 6.Its Boundaries name:
harness/:result/1with its references resolved and the fields the Harness fills removed,statusnarrowed tocompleted,incompleteandrefused, andeffectsnarrowed to the Task'spermitted_effects;releaseandusagefrom the CLI's own answer, and writes afailedResult,result_invalid, for an answer that does not validate;applyrefuses a Result whoserunorreleaseis not the Task's, an Effect type the Task does not permit, an Effectidor a Findingrefgiven twice, a raised Finding posted other than exactly once, or one that does not gate posted on a thread;applyrefuses a Disposition with neither half or both, or a half its Role may not make: an author proposes and the owner of the category decides (config/authority.json);applysubmits a Role's review as a comment on the admitted commit, never an approval;applywrites each posted Finding'sfinding/1intoapplied.json, its source from the Task;adapters/: the facts port joins a thread's category and severity fromapplied.json; the Controller names a Run byrun-name/1, and the records port parses it.Rows elsewhere:
coverage/1by reference …" changes its Check cell toschemas/result.schema.json,tools/check_examples.py.invalid/result.carries-a-commit-hash.jsonandinvalid/disposition.names-who-decided.json.Design edits, in the same change:
threadsrow: "in its Run'sresult.json" becomes "in its Run'sapplied.json".usagegains its fields, from P-13.applywrites its record.byand nocontent_id.applied.jsonholds the posted Findings.10. Out of scope.
applied.jsonandoutcome.json, which are Schemas for the Role manifest, the pass record and the feed; and one envelope for all #28's (below).bundle,preflightand the slice-2 Effect types.harness/.Can #19 and #28 run in parallel? No, not whole. #28 builds on this story: its pass record
$refsfacts.schema.jsonthrough ruling 4. Its role manifest refseffect.schema.json#/$defs/type. Its envelope check must classifytask,result,effect,dispositionandrun-name.tools/jsonschema_lite.py,tools/check_examples.py,tests/test_tools.py,tools/mutants.py,schemas/AGENTS.md,examples/AGENTS.md, and the design's §3.7, which both edit.main.trials/P-13.jsononce 0033's fixture is onmain.Needs the Maintainer? None open. The one point below was answered on 2026-10-18: option (a) taken.
applied.jsonandoutcome.jsonare #28's fourth criterion, and its rulings define them. Ruling 3'sapplied.jsonis that schema.The point as it was put:
applied.jsonandoutcome.jsonare artifacts of every slice-1 Run: the Kernel readsoutcome.json, and with ruling 3 the facts port readsapplied.json. Slice 0's exit asks for an example of every schema slice 1 uses. Neither has a story, and #19 already has four acceptance criteria.run, from 0034).harness/story in slice 1, and slice 0 closes without them.Recommend (a).
don-petry commentedon Oct 18, 2026
Decision number, updated: this ruling's record is 0036, not 0035. #165 (PR #176) needs a record of its own and takes 0034, the next free number, because it merges sooner. Records merge in number order: 0032 #175, 0033 P-13, 0034 #165, 0035 #161, 0036 #19, 0037 #28. #26 takes a number when it is built.
don-petry commentedon Oct 19, 2026
Decision number, updated: this story's record is 0037 (it was 0036). #26's P-39 reach trial passed and is ready to build now, so it takes 0036, ahead of the stories still being ruled or built. Order: 0034 #165 (PR #176), 0035 #161, 0036 #26, 0037 #19, 0038 #28, 0039 #174 PR B.
don-petry commentedon Oct 19, 2026
Decision numbers change rule (Maintainer, 2026-10-19). A decision record takes its number when its pull request is clean and green and about to be enqueued, as the next free number. It is no longer fixed when the rulings are posted. The number posted on this issue is provisional. The builder renames the record at enqueue if an earlier PR took that number. CI's gapless decision check still holds the order. Boundary rows in other records name tests, never numbers, so nothing else moves. This ends the renumbering, and a story with a record no longer waits behind an unrelated story that holds a lower number.
don-petry commentedon Oct 19, 2026
Decision numbers are taken at enqueue (the Maintainer, 2026-10-19, on the plan review's recommendation). A record takes the next free number when its PR is clean and green and about to be enqueued, not when its ruling is posted. Rulings say "the next free number at enqueue". Boundary rows in other records name tests, never numbers, so nothing else moves. The order posted on 2026-10-19 (0035 #161, 0036 #26, 0037 #19, 0038 #28, 0039 #174 PR B) is the expected order, not a reservation: #161 keeps 0035 as the next to enqueue, and each of the others takes the next free number in the order it is actually enqueued. A rows-only PR still merges any time.
don-petry commentedon Oct 19, 2026
Dependency (plan review, 2026-10-19). This story waits on #161's record being on main, not on its code: nothing here reads
unidentified_runs(that is #28's context table). Start now in lane s; citetrials/P-13.json, which is on main (#177).Schemas: the codes-only walk refuses a reference cycle and a keyword …
Mutants: each of decision 0038's names the test that kills it, and th…