Repository navigation
Commit 14c2307
authored
* 🔎 Let Plan ask for read-only XMD information (#762)
An agent writing a Plan could not look at the project first. It can now answer
any Plan-producing turn with a read-only XMD program instead of a draft; the
workflow evaluates it under the host's own read authority, hands the rendered
findings back as inert context, and asks again.
Classification is lexical and happens before anything else touches the
response. A draft is a response whose first nonempty body block, after optional
lexically closed frontmatter, is a nonempty level-one heading; everything else
is an information candidate. The frontmatter is never parsed, so
closed-but-invalid YAML stays a draft and reaches the repair path that exists
to explain it — parsing it there would classify that draft as a request and
evaluate program text nobody approved.
The rule lives in core rather than beside `Plan.md`, exported through
`core/host` alone. It has to agree with core about where a Markdown body begins
and what a heading is, and both the `---` delimiters and the parser are there;
a classifier that drew the boundary differently from the structural check would
send a draft to evaluation. It is pure, carries no authority, and reaches no
document.
Recovery is one narrow class. `generated-candidate.ts` marks a failure at the
throw site that raises it and reads it back through a namespaced non-enumerable
property that survives a separately loaded copy. Marking per site rather than
per class is the point: `GeneratedXmdError` is raised for a refused construct
and for a retained admission whose ceilings moved, and recovering by
`instanceof` would recover stale history along with a typo. Anything unmarked is
terminal, so a failure added later is not recoverable until someone decides it
is.
An unknown name given to `<Syntax names={…}>` is one of those recoverable
mistakes, and it cannot be marked where it is raised: `<Syntax>` persists the
lookup, and a failure crossing that durable boundary is rebuilt without its
class or any non-enumerable property. Recognizing it by the declared name that
does survive is not enough either — a symbols provider that throws can use that
same name, and recovering one would hand a broken installation back to an agent
as retry context. So core raises a private `SyntaxSelectionRefusal` around the
selection it performs itself, after the provider has already returned, and the
component recognizes that class *inside* the executor by `instanceof`, where the
original is still in hand. Only the conclusion travels out, in a variable core's
own closure owns. A provider that throws stays terminal whatever it names its
error.
`<PlanInformation>` is the only place that turns a failure back into a turn. It
projects its child through `tryContent()`, which hands back the child's
*original* failure rather than a boundary's account of it — `content()` would
wrap it in a `ContentError` and collapse the distinction this depends on. It is
paired and value-returning, so `as` is mandatory and its closed
`{ status, text }` envelope is never rendered into a Plan. A refusal discards
partial output; every unmarked failure is rethrown unchanged.
Settled information text crosses a pre-disclosure secret check before that
envelope is bound. The following Prompt is a disclosure destination like the
progress stream and the journal, and unlike them it is not durable — it reaches
the agent before any event of it is appended, so the serialized pre-append gate
cannot be what protects it. Canonical execution owns the policy and the scanner;
`<PlanInformation>` asks them and appends nothing. The policy is resolved live,
so an absent or unauthentic one fails rather than reporting "off", and a host
that explicitly disabled detection is followed exactly rather than given a
second always-on policy. Both outcomes are scanned, a finding raises the
existing `SecretDetectedError`, and because it throws, nothing binds: no
findings progress, no following Prompt, no review, no artifact.
`Plan.md` binds `requests = 8` beside its ten drafts and three repairs and
applies one response-handling shape after each of the three Plan-producing
Prompt sites, inserted at their three identical endings so they cannot drift.
Successes and refusals each spend one request; drafts and repairs are untouched
in both directions; the ninth candidate is not evaluated and starts no turn.
The explanation turn stays outside the loop. The command host installs
`ordinaryEvaluationProfile()`; `<Plan>` installs nothing.
The fake agent now fails loudly on a turn nobody scripted, naming the turn and
quoting the prompt, instead of synthesizing an empty reply. That fiction let a
case whose script and expectations disagreed pass anyway — a document with two
`<Plan>` sites silently gave its second site nothing — and removing it
immediately found four more fixtures relying on untitled Plans.
Ten fixtures across five suites replied with untitled programs, which are
information requests by this contract. Each received the level-one title every
Plan is required to have, so each row still tests what it tested. One row's
premise genuinely changed: a fully fenced reply has no heading and is now a
request, so "nothing is stripped" is re-expressed as a draft carrying a fence
reaching review with that fence intact. The tenth was in a suite none of the
focused commands run, and only `--changed=origin/main` found it.
The disclosure-waits-for-cleanup evidence is a held barrier rather than a
recorder: an admitted `component-answer` — the same profile arm canonical
protected `<Syntax />` is admitted through — whose body registers cleanup that
blocks. It is written last in the request on purpose. Cleanup inside a fragment
is per element, so a barrier written first is released before the read even
happens and proves nothing; the first version of that control was vacuous for
exactly that reason, and the assertion that the read had already run is what
catches it. The refusal path cannot hold a barrier at all — element cleanup
cannot straddle the failure that produces the refusal, and a provider handler
cannot register scope-bound cleanup because its claim window is deliberately
synchronous. Its ordering is settled instead by `CE22`, which proves
projection-owned work is still live where a failure is reported and already torn
down by the time recovery runs, plus a Plan-level row for recovery reaching the
following turn and `FE34` for a cleanup failure winning and staying terminal.
The distribution evidence is an executable journey rather than asset hashes.
`smoke-test/plan-information/` runs a mixed request-to-approved-Plan exchange —
one request refused, one answered, then a Plan — through the source CLI, the
emitted npm bin and the compiled binary. The tripwire is in the scenario rather
than the assertions: its agent answers only prompts carrying the refusal and
then the selected documentation, so a build that lost the packaged assets or the
protected tier writes no Plan to assert about. A compiled control covers
`plan-command.md`, which has no catalog entry to digest, by running the command
until it fails on an unresolvable agent — reaching that point at all proves the
document, the declaration and the protected tier are embedded.
* 🔒 Retain a Syntax selection refusal as a value, not a marked error (#762)
The classification was applied to whichever error left the durable operation,
decided by a variable set inside it. That is wrong in two directions, and the
second one only appears on a continuation.
If the selection refused and *publication* of the result then failed, the error
leaving the operation was the persistence, journal or secret failure — and it
left wearing the classification. `<PlanInformation>` would have offered that
back as retry context and started another Agent turn on a run whose journal had
already stopped accepting entries.
On replay the executor is skipped, so the variable was never set. The retained
refusal was rebuilt without its class and without the non-enumerable mark, and
came back unclassified. The same request was recoverable live and terminal after
a partial continuation.
Both follow from signalling a durable fact out of band. The refusal is now
retained: core's own `SyntaxSelectionRefusal` becomes `{ refused }` in the
record, beside the unchanged `{ symbols }`, and the closed value is interpreted
after `createDurableOperation` returns — the same code on a live run and on a
replay. A publication failure throws before that interpretation, so a result
that was never recorded is never acted on. A provider that throws is not caught
at all and fails the operation, which is what keeps infrastructure terminal
whatever it names its error.
Existing successful records are untouched, so a history written before this
still reads as exactly what it meant.
`markGeneratedCandidate` and `generatedCandidateReason` become
`markGeneratedRequestRefusal` and `generatedRequestRefusal`, in
`generated-request-refusal.ts`. The old names read as core granting a Plan
permission to retry, which is not core's decision to make: core states the fact
that it refused the generated request, and `<Plan>` alone decides that a refused
request earns another turn. `<PlanInformation>` translates the fact into its
existing refused result, unchanged.
One existing case changed its premise rather than its expectation. `SYN31` held
that a refusal retains no record. It now retains one, deliberately — that is
what makes a replayed refusal mean what the live one meant — so the row asserts
the record is exactly `{ refused }` and that a continuation reaches the same
refusal. Its five malformed-list cases are unchanged and still retain nothing:
those refuse before the occurrence is claimed. `PC27` gained the shapes the
second alternative makes possible — both members present, a non-string refusal,
and an empty reason.
Both new controls were checked against the defect they exist for: with the
closure restored, the publication row reports the classification it must not
carry, and the continuation row calls the live provider it must not reach.
* 📝 Say what core refused, not what a caller may do about it (#762)
The classification was renamed but its prose was not. Core still described
itself as granting permission — "a failure a trusted host may offer the
candidate another chance at", "a mistake the candidate can correct", a private
helper called `candidate()` — which is the framing the rename was meant to
remove. Core knows one thing and should say only that: the generated request
itself was refused.
The `core/host` export documentation now states the fact and names its limits
explicitly: core says nothing about whether a refusal is correctable, whether a
host may ask again, or whether another turn should happen. `fragment-
capabilities.ts` renames `candidate()` to `refusedRequest()`, because what it
builds is a refusal of what the fragment asked for and not a verdict about who
can fix it. The two `generated-xmd.ts` comments that read as retry policy now
say which thing was refused instead.
`syntax-refusal.ts` was also describing a mechanism that no longer exists. It
still explained the closure variable the previous commit replaced, so it now
describes what actually happens: the refusal is recognized inside the durable
executor, retained as `{ refused }`, and re-raised when that value is read back
— which is why a refusal that could not be published is never interpreted.
Plan keeps its own voice, because the decision genuinely is Plan's.
`<PlanInformation>` remains "the only place in the workflow that turns a failure
back into another turn"; what changed is the one clause that had core answering
that question. It now asks core the factual question and states, in the same
breath, that turning a refused request into another turn is this workflow's
policy and this is where it is made.
Behavior, durable representation and record schema are unchanged; every edit
here is a comment, a doc block, or the name of one private helper and its two
local call sites.
* 🏷️ Name the Plan writer, and state the whole syntax_symbols payload (#762)
"Authorship profile" and `<PlanAuthorship>` were generic names for one specific
thing: the constrained Agent and execution frame a Plan is written inside. The
generic name made every reader work out which authorship was meant — the frame,
or the drafting-and-review process that happens within it — and the two are not
the same subject.
The abstraction is now called the Plan writer throughout: the module, the
private component, its origin, the profile, frame, placement, policy,
observation, provider inputs, stack, roots, and the constants and helpers that
name any of them. No compatibility alias is left behind for any of the former
private names, so nothing can go on referring to the old abstraction by
accident.
Ordinary prose still says "authorship" where it means the process — a turn that
fails "ends authorship", the command "reports authorship as it happens", the
title requirement is "an authorship and human-review requirement". Those are
about drafting, repair, review and approval, which is what the word is for.
Two things were deliberately left alone and are worth naming. `AuthorshipFlags`
is the base of `AgentFlags`, which `xmd run` uses, so it describes agent
selection rather than the Plan writer; renaming it would leave `xmd run`
extending a plan-writer type. And `git-host`'s own `authorship()` is a commit's
author and committer, an unrelated word.
`<PlanAuthorship>` becoming `<PlanWriter>` changes two lines of `Plan.md` and
therefore its declared digest. That is accepted rather than worked around: this
branch already changes those bytes, so a continuation from previously shipped
source already crosses the stale-source boundary, and adding an alias, a dual
registration or a replay exception would create a second way to be that
Component. Every digest expectation recomputes the hash from the source file at
test time, so what needed updating was the private-name inventories rather than
any recorded constant. Public `<Plan>` keeps its name, origin and identity.
The retained-Syntax documentation was also still describing the payload as it
was before the durability correction. Four generic descriptions said an
occurrence retains exactly `{ symbols }`; the value has carried a second
alternative since a refusal became something the record distinguishes. They now
state the closed protocol — `{ symbols: string } | { refused: non-empty
string }`, exactly one member — along with what makes it closed: both
alternatives are durable values, the component interprets the value only after
publication succeeds, a retained refusal therefore means the same thing live and
on replay, a publication failure prevents interpretation and stays terminal, a
missing, additional, mistyped, empty or simultaneous member is stale input, and
existing `{ symbols }` histories remain readable.
Two statements that are specifically about `Plan.md`'s own bare `<Syntax />`
still say `{ symbols }`, because that occurrence names no component and cannot
produce a named-selection refusal. Both were reworded so they read as being
about that occurrence rather than as the protocol.
This extends one effect's closed payload description. It adds no durable record
and does not touch the generic durable-event envelope.
* 🏷️ Finish naming the Plan writer, and stop overstating the Syntax payload (#762)
Five identifiers still called the Plan writer "authorship". The frame's field
held the capability that installs a provider, and two commands held the settled
stack in a local of the same name, so reading any of them meant deciding which
authorship was meant — the writer, or the drafting the writer does.
`PlanWriterFrame.authorship` is now `PlanWriterFrame.planWriter`, with the
construction in `plan-component.ts` and the `frame.planWriter.installProvider`
call that reads it, plus the two locals in `agent-stack.ts` and `cli.ts`.
`planWriter` rather than `profile`, because the value is the capability that
installs a provider; `PlanWriterProfile` is the constrained execution profile
that results from it.
The two legitimate uses stay: `AuthorshipFlags` describes Agent selection shared
with `xmd run`, and `git-host`'s `authorship()` is a commit's author and
committer. So does every sentence using the word for the drafting, repair,
review and approval activity — including the `--agent-provider` help text, which
says "agent provider for Plan authorship" and is describing exactly that.
The Syntax suite's own documentation was also still promising more than the
payload delivers. It introduced the behavior as recording "exactly
`{ symbols }`", which stopped being true when a named selection refusal became
something the record carries. It now states the closed payload — one member,
either the rendered symbols or a refused selection — and says why the refusal is
a value rather than a failure: a continuation has to reach the same refusal it
reached live. `SYN19` exercises a bare occurrence that rendered, so its title now
says so instead of claiming to close the protocol.
No behavior, authority, classification or durable representation changes here,
and no compatibility alias is added.
1 parent 03a2168 commit 14c2307
39 files changed
Lines changed: 3181 additions & 536 deletions
File tree
- packages
- acp
- cli
- src
- documents
- tests
- document-suites/plan
- support
- core
- src
- components
- tests
- scripts/tests
- smoke-test/plan-information
- agents
- specs
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
57 | | - | |
| 57 | + | |
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
| 61 | + | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| |||
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
73 | | - | |
| 73 | + | |
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| |||
82 | 82 | | |
83 | 83 | | |
84 | 84 | | |
85 | | - | |
| 85 | + | |
86 | 86 | | |
87 | 87 | | |
88 | | - | |
| 88 | + | |
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | | - | |
| 114 | + | |
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
118 | | - | |
119 | | - | |
| 118 | + | |
| 119 | + | |
120 | 120 | | |
121 | | - | |
| 121 | + | |
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
137 | | - | |
| 137 | + | |
138 | 138 | | |
139 | 139 | | |
140 | 140 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
98 | | - | |
| 98 | + | |
99 | 99 | | |
100 | | - | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| |||
888 | 888 | | |
889 | 889 | | |
890 | 890 | | |
891 | | - | |
| 891 | + | |
892 | 892 | | |
893 | 893 | | |
894 | 894 | | |
| |||
1015 | 1015 | | |
1016 | 1016 | | |
1017 | 1017 | | |
1018 | | - | |
| 1018 | + | |
1019 | 1019 | | |
1020 | 1020 | | |
1021 | 1021 | | |
| |||
1024 | 1024 | | |
1025 | 1025 | | |
1026 | 1026 | | |
1027 | | - | |
1028 | | - | |
1029 | | - | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
1030 | 1030 | | |
1031 | | - | |
| 1031 | + | |
1032 | 1032 | | |
1033 | 1033 | | |
1034 | 1034 | | |
1035 | 1035 | | |
1036 | | - | |
| 1036 | + | |
1037 | 1037 | | |
1038 | | - | |
| 1038 | + | |
1039 | 1039 | | |
1040 | 1040 | | |
1041 | 1041 | | |
| |||
2503 | 2503 | | |
2504 | 2504 | | |
2505 | 2505 | | |
2506 | | - | |
| 2506 | + | |
2507 | 2507 | | |
2508 | 2508 | | |
2509 | 2509 | | |
2510 | | - | |
2511 | | - | |
| 2510 | + | |
| 2511 | + | |
2512 | 2512 | | |
2513 | 2513 | | |
2514 | 2514 | | |
| |||
2520 | 2520 | | |
2521 | 2521 | | |
2522 | 2522 | | |
2523 | | - | |
| 2523 | + | |
2524 | 2524 | | |
2525 | 2525 | | |
2526 | 2526 | | |
| |||
0 commit comments