Elevate the escalation stamp via a system subflow, not the screen flow - #1575
Merged
Conversation
…e screen flow `crm_case.is_escalated` and `escalated_date` are now `readonly: true` — the honest declaration, since only flows write them. The write that needed elevation moved into a dedicated `runAs: 'system'` sub-flow (`case_escalation_stamp`) reached by a `subflow` node, so the `escalate_case` screen flow keeps `runAs: 'user'` and the acting agent keeps their identity. The `STAMPED_NOT_TYPED` guard exemption and both of its counter-pins are deleted: the cause is gone rather than documented. Measured on a real engine, not assumed (all pinned in test/readonly-write-semantics.test.ts): - a runAs:'system' callee invoked from a runAs:'user' parent has its write to a readonly column SURVIVE, while the same callee declared runAs:'user' is STRIPPED — so the callee's own declaration decides, not the subflow hop; - the elevation is scoped to the child run: the parent's own later write to an identical readonly column is still stripped; - the reason must be written BEFORE the flag flips, or escalation_reason_required refuses the stamp and the run aborts before the reason lands either. `status: 'escalated'` stays in the user-context node — it is the transition both escalation hooks key off, so the ownership hand-off keeps firing from the acting user's write. `escalation_reason` stays writable: declaring it readonly would strip what the agent just typed. Refs #1434 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDbLQQiy7ew8sdwiCQEZwC
…ards Registering `case_escalation_stamp` is a 27th flow, and several guards count or list the shipped set. All of these are mechanical consequences of the #1434 split rather than independent choices: - `test/flow-case-actions.test.ts` runs `escalate_case` WITH its callee registered (a subflow node resolves its target off the engine registry), and gains an anti-vacuity case pinning that the parent no longer writes the stamps and the child does. Its header now states what this harness does NOT measure: it has no readonly semantics, so the strip is measured in test/readonly-write-semantics.test.ts instead. - `test/automation-docs-coverage.test.ts` learns the `autolaunched` trigger kind (new to this repo) with a word in all three locales, plus the row label. - The three automation pages gain a row; every stated flow count goes 26 -> 27 (the three pages, README banner//summary/tree, docs/STATUS.md). Refs #1434 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDbLQQiy7ew8sdwiCQEZwC
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
`...(alsoRegister as never)` is TS2698 on every TypeScript version — `never` is not an object type, so it cannot be spread. The intent is "spread a record of extra flows", so the cast now names one, matching the `flow as never` idiom already used for the single-value entry beside it. Caught by CI on 1827886, not locally: the local `typecheck` green cited in the report predated this line. `--listFiles` established that the file was in the check's SCOPE and that was allowed to stand in for the check being CURRENT — two different properties. This run of `pnpm typecheck` started 38s after the file's mtime and is the last command before this commit. Refs #1434 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YDbLQQiy7ew8sdwiCQEZwC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1434
Implements the maintainer-approved option D (director seat, decision batch #21 ②):
escalate_casekeepsrunAs: 'user', the elevated write moves into a dedicatedrunAs: 'system'sub-flow reached by asubflownode, the stamped fields becomereadonly: true, and theSTAMPED_NOT_TYPEDexemption plus its counter-pins are deleted.Nothing users see changes. The Escalate Case button behaves exactly as before; what changes is the privilege of each individual write.
⭐ Step 1 was the ruling's premise, and it is CONFIRMED
The ruling hangs on one named, falsifiable claim: a callee flow's own
runAsgoverns its writes, rather than inheriting the caller's context. It is measured against a real engine — realObjectQL, realInMemoryDriver, realAutomationEngine— intest/readonly-write-semantics.test.ts, not read off the source.Three assertions, sharing one parent shape and one child shape, with the child's declared
runAsas the only variable:runAs:'system'callee invoked from arunAs:'user'parentreadonlycolumn SURVIVESrunAs:'user'(negative control)readonlycolumn, in the run where the child's stamp landedThe second row is what makes the first a measurement of
runAsrather than of indirection: without it, a surviving system-child write would be equally well explained by "a subflow hop launders the write", which would make the elevation boundary decorative. The third row is the property the card exists to protect — the elevation is scoped to the child run and the screen flow keeps the acting user's context. Every case also asserts a control column (open_flag) landed, so a "stripped" reading can never be a harness that silently wrote nothing.The mechanism, for the record: the
subflowexecutor genuinely hands the child the parent's context ({ ...context, $subflowDepth, params }), andresolveRunContextthen re-assertsrunAs: flow.runAs ?? 'user'after that spread — last-write-wins in the callee's favour. The vendor states the intent: "a COPY, never mutating the caller's context, so the elevation is scoped to this run and the caller's identity is restored when the run returns" (ADR-0049 / #1888).The split, and the two decisions inside it
escalate_case's node wrote five fields. They are split by who may legitimately write them:escalation_reasonescalate_case(user)priorityescalate_case(user)statusescalate_case(user)is_escalatedcase_escalation_stamp(system)readonly; nobody types itescalated_datecase_escalation_stamp(system)readonly; nobody types itstatus: 'escalated'deliberately stays in the user-context node. Both escalation hooks key off it —case_escalation_reassigngates oninput.status !== 'escalated' || previous.status === 'escalated', andcase_status_side_effectsreads the same transition. Keeping it there keeps the ownership hand-off and the follow-up tasks firing from the acting user's write, exactly as before. Moving it into the elevated subflow would have silently re-attributed both to the system.escalation_reasonstays writable. Declaring itreadonlywould make the platform strip what the agent just typed — this card's own harm, inverted onto user input — andescalation_reason_requiredwould then reject the escalation outright.Splitting one
update_recordinto two creates an ordering constraint the single node did not have.escalation_reason_requiredrejects any write whose merged record hasis_escalated == truewith a blank reason; as one node the reason arrived in the same payload, but split they are two validation passes.Chosen order: the reason is written FIRST, the stamp SECOND. Measured both ways over a probe object carrying the same predicate shape:
subflownode reports its child's failure, the run aborts before the reason is written either, losing the whole escalation.The shipped edge order is pinned, walking the edges rather than trusting array order.
Guard removal
STAMPED_NOT_TYPEDheld exactly one entry,crm_case.is_escalated, and existed only because a user-context flow wrote the field. With the cause removed, the exemption and its read site are deleted:isAuthorable()now skips both fields for the right reason — they are declaredreadonly— rather than via an exemption list. Both counter-pins that existed to keep the exemption's justification honest are removed with it. ⛔ No new gate was added.Ruling item 3's still-owed half is done:
close_casenow says in the file that its flow-level elevation is a historical precedent, not a policy, so it stops being citable as a pattern for elevating the next screen flow.The dispatched surface was 6 files. This PR touches 13. Every extra file is a mechanical consequence of the ruling rather than a discretionary choice, but none of them was in the brief, and each is individually revertible:
test/case-create-form-narrowing.test.ts— a FOURTH counter-pin the site count missed. The dispatch listed three sites to keep consistent; this file holds a fourth, pinningis_escalated/escalated_dateas NOT readonly (test/readonly-write-semantics.test.ts's own header names it, so it is documented, just not enumerated). It cannot be left: it directly contradicts the ruling and fails the build. Narrowed tois_sla_violated, whose rationale never depended on the platform.src/flows/index.ts— the barrel. A new flow file that nothing exports is dead code, and thesubflownode resolves its target off the engine registry.test/flow-case-actions.test.ts— the existing runtime tests asserted the old single-node shape.test/automation-docs-coverage.test.ts+ the three automation pages +README.md+docs/STATUS.md— registering a 27th flow moves every stated flow count (26 → 27) and needs a table row in three locales.autolaunchedis also a new flow type for this repo, so the docs guard needed a trigger word for it.None of these files is claimed by the sibling agents (#1436 holds
AGENTS.md; #1481 holdstest/helpers/metadata-fixtures.tsandsrc/apps/crm.app.ts).AGENTS.mdwas not touched.Verification
Every component of
pnpm verifyrun individually on the final tree,1827886:🤖 Generated with Claude Code
https://claude.ai/code/session_01YDbLQQiy7ew8sdwiCQEZwC
Generated by Claude Code