fix(specverb, opcore): let a shadow carry the leaf's optional fields, correctly typed #327
No reviewers
Labels
No labels
burndown-2026-06
burndown-2026-08
sunday-sprint
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/umbra!327
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/1105-optional-args"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #326 and #328.
coilyco-flight-deck/agentic-os#1105restores the full flag set on top of it.The behaviour
An
inputwithoutrequiredmay legitimately be absent, but anargsentry bound to it resolved through an error, andbuildCallRequestturned that into aUserErrorthat killed the whole call. Observed on a real verb, with nothing modified:So "optional" held only while nothing bound it. The moment an
argsblock referenced it, it was required in practice, and the error told the caller to supply an input the guardfile had called optional.Why it matters beyond the message
A mount shadow replaces the generated leaf on the CLI, so it can expose only the fields it declares, and it could only declare them as required. agentic-os#1105 dropped
--assignee,--assignees,--closed,--due_date,--milestoneand--reffromissue createfor exactly this reason: carrying them would have forced every filing to pass all six.Adding a guard cost the leaf's optional surface. That is a tax on every consumer that shadows a leaf.
The fix, and why it does not fail open
ResolveArgnow wrapsErrUnsetOptional, andbuildCallRequestskips that arg so the field is simply absent from the request.The obvious worry is a typo'd
$refrencesilently vanishing instead of erroring. It cannot, because that is already caught one layer earlier:Build-time validation rejects a bare
$namethat noinputdeclares, so any reference reaching runtime is a declared input.$step.fieldis a separate path and still fails when its step is unbound.The dry plan resolves the same way, so
--dry-runno longer prints a${placeholder}for something the live call drops.Scope, and the one thing deliberately left alone
Spec dialect only. An execverb step takes positional argv tokens, where dropping one would shift every token after it and could silently weaken an
argNguard. SoResolveArgDrykeeps its original signature and the execverb path is byte-for-byte unchanged; the newUnsetInputRefhelper is what lets specverb opt in without touching it.That is the part worth reviewing hardest - the first draft did change both paths, and it should not have.
Verification
make test(34 packages, no failures),make vet,make lint(0 issues),make tidy,godoc-current.txtregenerated.code-commentsreports 18 violations onmainunchanged; this branch adds none.Two new tests: the omitted field absent from the live body rather than sent empty or as a literal placeholder, and the dry plan carrying no placeholder for it.
Second commit: scalar typing (#328)
Carrying the optional fields is only half of it.
BindSlicelowers array elements to the declared element type (#315, #319), butBinddid not do the same for a scalar, so a body field declaredintegergot the raw CLI string:booleanhad the same shape. That is #316's defect one level up, and Forgejo's answer to a wrong type is frequently a 200 that changes nothing - the whole subject ofcoilyco-flight-deck/agentic-os#1047.CoerceScalarmirrorsCoerceItems:integer,numberandbooleanparse to their JSON type, anything else stays a string, and a value that does not parse is refused rather than sent. Body fields only, since path and query values are strings on the wire.Without this, #326 would have handed consumers a way to carry
--milestonethat silently sent the wrong type - a worse outcome than not carrying it. The two belong together.Two more tests: an integer field reaching the wire as a JSON number, and a non-integer refused before the write.
fix(specverb): drop an omitted optional arg instead of failing the callto fix(specverb, opcore): let a shadow carry the leaf's optional fields, correctly typedCorrection to this PR's own description: it closed #326 only, not #328.
The body was edited to cover a second commit adding
CoerceScalar, but the merge here took the branch at60eb0d8, one commit earlier.39b76econmaincarriesErrUnsetOptionaland nothing else - confirmed by reading the merged tree rather than the diff summary:#328 is still open and carries the scalar-typing half on its own, as #329. Recording it here so nobody reads this PR's description and assumes an integer body field is already typed - it is not, until #329 lands.