Skip to content

Show workflow hand-offs under a chat answer in V2 (Phase 7b) - #1683

Merged
Paul Lizer (paullizer) merged 30 commits into
microsoft:paullizer-react-v2-uifrom
paullizer:paullizer-phase-7b-v2-handoff-card
Oct 8, 2026
Merged

Paul Lizer (paullizer) merged 30 commits into
microsoft:paullizer-react-v2-uifrom
paullizer:paullizer-phase-7b-v2-handoff-card

Conversation

@paullizer

@paullizer Paul Lizer (paullizer) commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • A hand-off card under the answer. When a plan's workflow_handoff step prepared a one-time workflow, V2 now shows a Workflow hand-off card under that answer. The card shows:

    • what the workflow covers, such as "12 documents" or "up to 500 best-matching documents", and which workspaces a search reads
    • each task, and whether it runs on the default model or an agent
    • that the workflow runs once and isn't scheduled
    • until when the offer stands

    Accept, Edit and Decline each ask for confirmation. Edit opens the prepared draft in the workflow editor, and saving it sends an edited accept. A button shows only when the server lists it in the item's actions.

  • Live run status after accept. The queued run shows as a "Handed-off workflow" row, which reuses 6b-2's LiveRunRow, so it has the same Cancel run, Retry, Review and approve, Reconnect and Open run controls.

    • The row is joined to the tracker by run.id, then checked against the conversation, step and workflow. It's never matched on the orchestration run the answer is mounted on, because the run that prepared the hand-off can be an earlier attempt.
    • The over-limit pause says why the run stopped and offers Cancel run rather than Approve.
    • With the tracker off, the card shows a static status and an Open run link.
    • 6b-1 posts the result back into the chat, as for any run started from chat.
  • The plan side.

    • workflow_handoff joins V2's approval floor, so timed and auto plans with an enabled hand-off step wait for the user even if the server's floor marker is missing.
    • The plan card shows a notice that approving the plan only prepares the workflow.
    • The Review view describes the blueprint in words: the workflow's name, what it covers and the task titles. It never shows instructions, document or workspace handles, the content filter or tags.
  • No server changes. 7a's setting, enable_chat_orchestration_workflow_handoff, stays off by default. With it off, the planner never offers the step, so answers look as they did before. An answer made while it was on shows its card as unavailable, with the reason, instead of vanishing.

Linked issue

Release Notes & Latest Features

  • New Feature
  • Bug Fix
  • UI Enhancement
  • Breaking Change
  • Internal only

Is this visible to end users?

  • Yes
  • No

Is this admin-facing (Admin Settings, governance, deployment, config)?

  • Yes
  • No

The setting, Hand Off Large Work From Chat, is 7a's. This PR changes only two rows of docs/admin/orchestration.md, which now say what V2 shows.

Should this become a Latest Feature card?

  • Yes
  • No
  • Already added

I've left that call to the coordinator. The feature is off by default and V2-only.

Screenshot needed for the card?

  • Yes
  • No
  • Attached

Version bump

V2 merges. V2 kept moving while this PR was open, so I merged it 12 times, always with a merge commit and never with a rebase or force-push.

Merge V2 commit V2 VERSION V2 PRs in it Conflicts
6faf72867 485d51aaf .273 21, including #1677 config.py, release notes
9ec0d2457 919375b41 .275 2 release notes
26e1ccbd7 705774de1 .276 1 config.py, release notes
304b91548 b3c0b7b0d .278 2 config.py, release notes
fdd33343e 7de8fcee3 .280 2 config.py, release notes
260a044c9 f1c3ddab0 .281 2, including #1691 none
d4f506c0a 01c49592b .286 3 config.py, release notes
ae2ee7c13 05fb0ced7 .287 #1687 release notes
041b96639 d4cfde733 .289 #1694, #1696 config.py, release notes
991a38825 56fd78c14 .290 #1700 release notes
79092ac83 36f0af182 .292 #1701, #1702 config.py, release notes
cd6880380 49a52bb5c .293 #1703 release notes
  • Every merge's config.py differs from V2's by at most the VERSION line. In the last three it's identical to V2's.
  • Every merge's release notes are V2's file plus this PR's 19-line section, with nothing of V2's removed.

What V2 changed near this PR. I read each batch before merging it.

Testing / validation

Which head. The build, Node, UI and functional results below are from 4417d28a8. The two commits after it change only ui_tests/fixtures/foundry_consent/.gitignore (+4 lines) and the same 20 version lines, so at this head, 1b2598f52, I ran only these light checks and didn't rerun the UI, functional or Node suites:

Check at 1b2598f52 Result
git check-ignore -v on harness.bundle.js.map, harness.bundle.css, harness.bundle.css.map and __pycache__/ in ui_tests/fixtures/foundry_consent/ each matched by that folder's .gitignore, lines 2 to 5
git status --porcelain empty
git grep 0.261.294 no lines; this PR's 20 version lines say 0.261.295, and none of its 0.261.253 floor lines changed
Release notes this PR's (v0.261.295) section is on top, above V2's (v0.261.293), and the rest of the file is V2's, byte for byte
Docs tests and guardrails below, all run at this head

Environment.

  • Python runs use the pinned Flask 3.1.3 venv, written <venv python> below, with PYTHONPATH=application\single_app;functional_tests, PYTHONIOENCODING=utf-8 and PYTHONUTF8=1.
  • <flags> is -p no:langsmith_plugin -p no:cacheprovider -W ignore.
  • Tests ran one process at a time. The UI tests drive the built V2 bundle in Playwright against stubbed routes. There was no manual browser session.

Build.

Command Result
npm --prefix .\application\v2_ui run typecheck passed
npm --prefix .\application\v2_ui run build passed
npm --prefix .\application\v2_ui run build -- --outDir ..\..\ui_tests\artifacts\orchestration-plan-editor (the bundle the UI tests load; the folder is git-ignored) passed

Node. node --test --test-concurrency=1 over these 21 functional_tests\ files: 205/205 passed.

  • test_v2_orchestration_{approval_logic,deliverables,merge_arguments,model_routing_logic,phase_groups,planner_display,planner_model_logic,progress_lane}.mjs
  • test_v2_orchestration_workflow_{deliverable,handoff_client,results_naming,run_floor,run_links}.mjs
  • test_v2_workflow_{delivery_messages,flow_node_reuse_logic,merge_task,proposal_merge,run_action_clients,run_link_routing,run_status,run_tracker}.mjs
  • Two more files that cover code next to this PR's: 14/14 passed.
    • test_v2_orchestration_run_progress_status.mjs covers describeRunProgress in orchestrationPlan.ts, a file this PR also changes.
    • test_v2_orchestration_streaming_surface.mjs covers the chat and orchestration stores and the stream controller.

The restored test helper. #1672 (e65ab0dbc) deleted the ui_tests/fixtures/agent_delegation/ folder. At V2's tip, five UI tests still import ensure_bundle from its harness_build.py at module level, so they fail at import there: test_v2_workflow_run_card.py, test_chat_workflow_results.py, test_foundry_consent_v2.py, test_v2_notifications_bell.py and test_v2_workflow_alert_notices.py.

  • This PR's run-status test reuses RunHarness from test_v2_workflow_run_card.py, so it would fail the same way.
  • bee051525 restores only harness_build.py, identical to this branch's base, 16f42e4d2.
  • Each of the five passes its own fixture's entry and bundle path, and those fixture folders have their own .gitignore. So the folder's other deleted files (harness_entry.tsx, harness.html and .gitignore) aren't needed.
  • With the helper back, test_foundry_consent_v2.py runs again, and its run left ui_tests/fixtures/foundry_consent/harness.bundle.css untracked. esbuild writes the CSS the bundle imports next to the bundle, and the test's teardown deletes only harness.bundle.js, which was the only entry in that folder's .gitignore. 0e8222f92 adds the four entries the other fixture folders already have: harness.bundle.js.map, harness.bundle.css, harness.bundle.css.map and __pycache__/. The test itself is unchanged.

UI tests.

  • <venv python> -m pytest <flags> ui_tests\test_v2_orchestration_workflow_handoff_card.py ui_tests\test_v2_orchestration_workflow_handoff_plan.py ui_tests\test_v2_orchestration_workflow_handoff_run_status.py ui_tests\test_v2_orchestration_workflow_proposal_card.py ui_tests\test_v2_orchestration_workflow_run_floor.py ui_tests\test_v2_orchestration_workflow_run_links.py ui_tests\test_v2_orchestration_plan_card.py

    • 178 passed, 6 failed.
    • All 6 failures are in the unchanged test_v2_orchestration_plan_card.py. It sets _PAGE only in main(), so it fails the same way under pytest at the base.
    • <venv python> ui_tests\test_v2_orchestration_plan_card.py: 7/7 passed.
  • <venv python> -O -m pytest <flags> over the three new hand-off UI files: 92 passed.

  • <venv python> -m pytest <flags>, one file at a time, over seven more UI files: 218 passed, 11 errors.

    File Result
    test_v2_workflow_run_card.py 41 passed
    test_v2_workflow_run_tracker_spa.py 6 passed
    test_v2_orchestration_workflow_handoff_run_status.py, again on its own 18 passed
    test_chat_workflow_results.py, the rerun the coordinator required after Show a workflow's reply once in the conversation it created (0.261.253) #1637 merged 26 passed
    test_foundry_consent_v2.py 20 passed
    test_v2_notifications_bell.py 47 passed, 10 errors
    test_v2_workflow_alert_notices.py 60 passed, 1 error
  • The 11 errors aren't this PR's. All 11 tests pass, then fail their console check at teardown.

    • The check fails on five Settings page requests that these tests don't stub, so they get 404: /api/m365/preferences, /api/m365/chat/connection, /api/m365/connections, /api/m365/bindings?page_size=20 and /api/profile/fact-memory. Bring V2 User Settings to profile parity with an admin-style layout #1691 added those requests, in M365Cards.tsx and FactMemoryBench.tsx.
    • At V2's tip, 49a52bb5c, with only the restored test helper added, the bell and alerts files give the same result: 107 passed and 11 errors, on the same 11 tests. A temporary pytest plugin logged the same five URLs there.
  • Fix orchestrated action charts that were drawn but never shown #1703's tests. Fix orchestrated action charts that were drawn but never shown #1703 added or changed these. I ran them because it changed which plan steps must feed the answer and re-baselined 7a's off golden:

    • <venv python> -m pytest <flags> functional_tests\test_orchestration_action_chart_delivery.py: 15 passed, and 15 again as a script.
    • <venv python> -m pytest <flags> functional_tests\test_orchestration_workflow_results_capability.py: 70 passed.
    • <venv python> -m pytest <flags> ui_tests\test_v2_orchestration_dependency_plans.py: 34 passed.

Functional tests.

  • <venv python> -m pytest <flags> over 19 functional_tests\ files: 525 passed. The files:
    • test_v2_orchestration_workflow_handoff_xss_guardrail.py, test_v2_orchestration_workflow_run_links_xss_guardrail.py, test_v2_workflow_run_tracking_xss_guardrail.py, test_xss_guardrails_checker.py and test_orchestration_workflow_run_approval_floor.py
    • test_orchestration_workflow_handoff_editor_round_trip.py, test_orchestration_workflow_proposal_editor_round_trip.py and test_workflow_handoff_status_route.py
    • 7a's test_orchestration_workflow_handoff_{adapter,capability,imports,off_golden,planner,routes}.py and test_workflow_handoff_{builder,end_to_end,lifecycle,origin,result_reader}.py
  • The three new functional files also pass under -O -m pytest, as scripts, and as -O scripts:
    • the XSS guardrail (8 tests)
    • the editor round trip (4 tests)
    • the status route (3 tests)
  • <venv python> functional_tests\test_v2_notifications_bell.py: 10/10 passed.

Docs, run through __main__.

Test Result
test_docs_app_surface_coverage.py 7/7
test_docs_site_quality.py 6/6
test_docs_latest_release_catalog_parity.py 6/6
test_docs_link_integrity.py 2/5
test_docs_release_notes_integrity.py 0/1
  • The last two fail the same way at V2's tip, 49a52bb5c, and the brief lists both as known base failures:
    • Link integrity: the same 11 broken relative links (of 1521 here and 1519 at the tip), the same 15 broken site links (of 855 and 853) and the same 2 broken features.yml links (of 76). The broken sets are identical, so none of this PR's links is broken.
    • Release-notes integrity: the generated archive is stale. It's missing 223 releases here and 222 at the tip; the extra one is this PR's .295. Per the brief, I didn't regenerate it.
  • Screenshots are 72 of 133 filled here and 72 of 132 at the tip. The new slot is the hand-off card's, reference/chat-controls-workflow-handoff-card.png, which shows a placeholder until someone captures it. Outstanding media doesn't fail the test.
  • python .\scripts\build_docs_inventory.py regenerated the inventory with no content change.

Also: mutation testing killed 37/37, all six guardrails reported no failures at this head, and CodeQL reports no new alerts at this head. All three are below.

Documentation

  • Release notes updated, or not needed

  • Feature documentation updated, or not needed

  • Fix documentation updated, or not needed

  • Release notes: a new ### **(v0.261.295)** section above (v0.261.293) in docs/explanation/release_notes.md.

  • Feature doc: a new "The V2 hand-off card" section in docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md, plus additions to Usage, the test table and Known limitations. f41c26cef also corrects two of 7a's sentences there: only an all_matches query pauses over its limit (see Follow-ups).

  • Fix doc: not needed, because there's no bug fix.

  • Other pages:

    • docs/admin/orchestration.md: the setting's row now names the V2 card. The over-limit pause row points to Cancel run on the card and says only an all-matches query pauses.
    • docs/guides/trigger-a-workflow.md: a new "Hand large work off to a workflow" section, and six hand-off rows in the "Other run problems" table.
    • docs/reference/chat-controls.md: a new "Workflow hand-offs (V2 interface)" section, with a screenshot slot.
  • Checked and left unchanged: docs/_features/workflow-results-in-chat.md, docs/_features/workflow-ai-assistant.md and docs/guides/ask-about-workflow-results.md. The roadmap is untouched.

Security checklist

  • New Flask routes include @swagger_route(security=get_auth_security())

  • Settings sent to non-admin frontends use sanitize_settings_for_user()

  • Browser JavaScript is served from local SimpleChat static assets only; no CDN-hosted JS

  • No secrets, keys, connection strings, or local-only artifacts are included

  • Routes: no Flask routes were added or changed, so the guardrail's swagger check skipped itself.

  • Settings: no settings are sent anywhere new.

  • Browser code: no new dependencies, CDN references or dynamic imports. Everything ships in the V2 bundle.

  • Fixtures: they use fake ids and http://simplechat.test. The build output under ui_tests/artifacts/ is git-ignored.

  • XSS:

    • Workflow, task, agent and workspace names, and every server sentence, render as React text only. Nothing uses dangerouslySetInnerHTML.
    • Links come only from the reviewed builders workflowProposalLink and workflowRunHref.
    • Request paths encode their ids with encodeURIComponent.

Decisions made autonomously

  1. Reuse 6b-2's run row. WorkflowRunCard.tsx now exports LiveRunRow, with two optional props: heading (default "Started workflow") and waitingText (default workflowWaitingText). A hand-off's run reads and acts exactly like 6b-2's, and 6b-2's own rows are unchanged.
  2. Describe the blueprint in words. The Review view shows the workflow's name, what it covers and the task titles. It never shows instructions, handles, the content filter or tags.
  3. Place the plan notice after the workflow-run notice, styled like it.
  4. No pre-accept count. A count would take two requests per card: the draft, then a loop preview. The card shows the bound the server discloses instead. This is listed under Follow-ups.
  5. Give code-less refusals V2's own text. Decorator refusals carry no code, and the decorator 403's error is "Forbidden", so ApiError.message is never shown for them.
  6. Show the static fallback whenever the tracker is off or hasn't found the run.
  7. Don't gate the mount on the setting. The card mounts on capabilities_used, and the server's list decides what it shows. With a gate closed, every item reads unavailable, so the card says why instead of vanishing.
  8. Keep the draft when a save is refused. The editor's save path discards the draft on a 401, 403 or 404, so every refusal except a 401 is rethrown as a 400. A 401 still reaches the editor's onAccessLost.
  9. Explain the edit limits up front. EDIT_LIMITS shows under the buttons, and a refusal adds up to 3 of the server's errors[]. There are no field locks, because adding them would change the editor for every caller.
  10. Put the user guide in docs/guides/trigger-a-workflow.md, after "Run a workflow from chat".
  11. After an edited accept, the editor closes, the card shows queued, and focus returns to the card.
  12. Hide the proposal and run cards on a hand-off answer. This is defensive, because the server already refuses mixed plans (_reject_mixed_workflow_handoff). It also keeps WorkflowRunCard's extra from listing the hand-off's run a second time with the generic pause text.
  13. Label the step's chip "Hand off large work", the registry's label.
  14. Stack the save confirm on the editor, without restating the original disclosure, because an edited loop may differ.

A finding about task_prompt.

  • workflowForSave sets a v3 workflow's task_prompt from its first task's instructions, but the server set the hand-off's from its name. Sending the editor's value back would make an untouched save count as an edit.
  • So before an edited accept, workflowHandoffEdit drops id and, for v3, task_prompt.
  • functional_tests/test_orchestration_workflow_handoff_editor_round_trip.py::test_saving_the_handoff_untouched_is_not_an_edit covers it. The test bundles the real normalizeWorkflowDefinition and workflowHandoffEdit with the app's esbuild, runs them under node, and passes the result between the real draft and accept routes.

Error and state mapping

States

A button shows only when the item's actions list it. The Buttons column shows what the server lists for each state.

State Chip What the card says Buttons
pending "Awaiting your decision" "Nothing runs until you choose." and "You can decide until <local date and time>." Accept, Edit, Decline
creating "Creating" "Creating the workflow." See the polling note below the table. none
created "Workflow created" "The workflow was created, but its run has not started." Start its run, Open workflow
queued "Run queued" (ok tone) "The workflow was created and its run was queued.", then the run (see "The queued run" below). With chat_delivery, it adds "The run's outcome is posted in this chat when it ends." the run row's controls
denied "Declined" "You declined this hand-off. Nothing was created." none
expired "Expired" (warn) "This hand-off expired. Ask again in chat for a new one." none
unavailable "Unavailable" (warn) its reason's text (see "Reasons" below), or "This hand-off is not available." Decline, when listed
invalid "Can't be used" (warn) its reason's text, or the same fallback Decline, when listed
created or queued with reason workflow_deleted "Workflow deleted" (warn) "The workflow this hand-off created was deleted." none, with no Open workflow link and no run

Polling while creating. The card checks again every 3 seconds, at most 40 times or for 120 seconds. It skips checks while the tab is hidden and checks again when the tab is shown. After that it says "This is taking longer than expected." with Check again.

The details, which show only for a disclosed item:

  • the description
  • Covers: disclosure.text
  • Workspaces: the scope names, joined with ", "
  • Runs: "Once, when you accept. It is not scheduled."
  • Alerts: "A notification after every run, severity Info."
  • Durable: "Each run saves checkpoints and can resume after an interruption."
  • Tasks, in a list labelled "Workflow tasks": "N. <title>" ("Untitled task" when empty), then either "Runs with the agent <name>." ("Runs with the agent chosen for it." when there's no name) or "Runs with the default model."

The card's name is workflow.name, else summary.name, else "Workflow hand-off". The eyebrow above it reads "Workflow hand-off".

Buttons:

  • Accept reads "Accept", or "Start its run" when the item is created, and "Starting…" while busy.
  • Edit reads "Edit", and "Opening…" while the draft loads. Its accessible name is "Edit <name> before accepting it".
  • Decline uses the danger style.
  • Open workflow (from workflowProposalLink) shows when the workflow exists, isn't deleted, and the hand-off isn't queued with a run.
  • When Edit is offered, EDIT_LIMITS shows under the buttons: "Edits keep the trigger manual and keep the For each over documents or a workspace search. Tasks can use only your own local agents, and URL Access and Run as aren't available."

Reasons

The first seven are the server's WORKFLOW_HANDOFF_REASON_TEXT, verbatim. The server has no text for the rest, so V2 has its own.

Reason Text
workflow_handoff_invalid "The hand-off was planned in a way SimpleChat can't run. Ask again, naming the documents or the workspace to review."
workflow_context_unavailable "Your documents and workspaces could not be checked for this request. Ask again in a new message."
handoff_loop_limit "The request covers more documents than one hand-off can review. Ask again with a narrower request."
handoff_agent_unsupported "A hand-off runs its tasks on the default model or on a local agent. Ask again without naming that agent."
handoff_unavailable "Handing work off to a workflow isn't available with this deployment's workflow settings."
handoff_sources_unavailable "A document, workspace or agent the hand-off named is no longer available to you. Ask again in a new message."
handoff_prepare_failed "The hand-off couldn't be prepared. Ask again in a new message."
workflow_handoff_disabled (V2) "Handing work off to a workflow is turned off, so this hand-off is not available."
workflow_results_disabled (V2) "Workflow results in chat are turned off, so this hand-off is not available."
workflow_role_required (V2) "You need workflow access to use this hand-off."
workflow_shared_conversation (V2) "Workflow hand-offs are available only in your own conversations."
content_review (V2) "This response is in content review, so its hand-off cannot be used."
workflow_deleted (V2) "The workflow this hand-off created was deleted."
anything else "This hand-off is not available."

Refusals with a code

The text is V2's copy of the server's ERROR_MESSAGES, verbatim. legacy_plan uses LEGACY_PLAN_MESSAGE.

Status Code What the card says Try again
400 invalid_request "The request is not valid." no
400 handoff_edit_invalid "The edited workflow is not valid. Review the task, runner and document inputs." no
403 workflow_handoff_disabled "Workflow hand-off is not available here." no
403 workflow_role_required "You need the workflow role to hand work off to a workflow." no
403 workflow_shared_conversation "Workflow hand-off is available only in your own private chats." no
403 handoff_results_off "Workflow hand-off needs workflow results in chat, which is turned off." no
403 handoff_access_lost "You no longer have access to a document, workspace or agent this workflow uses." no, even with state:'created'
404 run_not_found "Run not found." no
404 handoff_not_found "Workflow hand-off not found." no
409 handoff_unavailable "This workflow hand-off is no longer available." no, even with state:'created'
409 handoff_expired "This workflow hand-off expired. Ask again to get a new one." no
409 handoff_denied "This workflow hand-off was declined." no
409 handoff_accepted "This workflow hand-off was already accepted." no
409 handoff_busy "This workflow hand-off is being accepted. Try again in a moment." yes
409 handoff_kind_mismatch "This is not a workflow hand-off." no
409 handoff_run_conflict "The workflow was created, but its run could not be started.", then the reason's text (see the next table; the default is workflow_run_not_started) yes, with state:'created'
409 workflow_deleted "The workflow this hand-off created was deleted." no
409 handoff_limit_changed "The document limit changed since this hand-off was prepared. Ask again to get a new one." no
409 legacy_plan "This plan was created by an earlier orchestration version and can't be opened or rerun. Start a new request." no
422 handoff_invalid "This workflow hand-off is not valid. Ask again to get a new one." no
422 handoff_agent_unsupported "A hand-off workflow can use only your own agents." no
429 handoff_daily_limit "You reached the daily limit for workflow hand-offs. Try again later." no
503 handoff_queue_failed "The workflow was created, but its run could not be queued. Try again." yes, with state:'created'
503 service_unavailable "The service is temporarily unavailable. Try again." no

handoff_run_conflict reasons (V2's copy of the server's WORKFLOW_RUN_REASON_TEXT):

Reason Text
workflow_unavailable "The workflow can't be started from chat. It may have been deleted, or it can't run as saved. Open it in Workflows to check it."
workflow_already_running "The workflow is already running. You can follow that run in Workflows and start the workflow again once it finishes."
workflow_definition_changed "The workflow changed while it was being started. Ask again in a new message to start its current version."
workflow_run_tombstoned "The workflow couldn't be started. Ask again in a new message."
workflow_run_not_started "The workflow couldn't be started. Open it in Workflows to run it."

How a coded refusal reads. The text above comes first, followed by up to 3 of the server's errors[] messages. Each is cut to 240 characters and ends with a period, and repeats are dropped. An unknown code reads the fallback for where it happened.

Try again (workflowHandoffRetryable) is offered only for the three codes marked yes, and only while the item is creating or still offers accept. Decline and draft errors never offer it (coordinator note 1). It sends the same accept again. With state:'created', the server queues the same run, because the run's request id comes from the user and the hand-off.

Refusals without a code, and other failures

Case What the card says
401 "Sign in again to continue."
400 with no code (the allow_user_workflows or personal-workflows decorators) "Personal workflows are turned off in SimpleChat right now."
403 with no code (the WorkflowUser or User role decorators) "You need workflow access to use this hand-off. Ask your administrator." It never shows "Forbidden".
any other status with no code the fallback for where it happened
a network failure (TypeError) "SimpleChat could not be reached. Check your connection and try again."
a response that fails its check "The workflow hand-off returned an invalid response."

Response checks. Any other response reads as invalid.

  • The list must name the run it was asked for.
  • An accept must return the same handoff_id with state:'queued', and its created must agree with the status: true for 201, false for 200.
  • A decline must return the same handoff_id with state:'denied'.
  • A draft must return the same handoff_id and a workflow object.

Fallbacks:

  • an action: "The hand-off could not be updated. Try again."
  • the list: "The hand-off could not be loaded. Try again."
  • opening the editor: "The workflow editor could not be opened. Try again."

Where errors show:

  • Action errors appear in a role="alert" danger box.
  • List errors appear in a role="status" warning banner, with Try again to read the list again.
  • A 404 list, or an empty one, renders nothing.
  • The cards sit in a section labelled "Workflow hand-offs".

Editing

  • Edit loads …/draft and opens WorkflowEditorDialog on it.
  • Save asks for confirmation, then sends {conversation_id, mode:'edited', workflow}, where workflow is the editor's payload without id and, for v3, without task_prompt. An as-proposed accept sends {conversation_id, mode:'as_proposed'}.
  • A draft with URL Access on is refused before anything is sent: "URL Access is not available for workflows created from chat." followed by the server's url_access_note.
  • A refused edited accept (any status except 401) goes back to the editor as ApiError("<text> Your draft has been retained.", 400), so the editor keeps the draft. The list is then refreshed. A 401 still reaches the editor's onAccessLost.
  • Cancelling the save confirm reads "Not saved. Your draft has been retained."

The queued run

  • Tracked: this applies when the tracker is live and its row matches the run's id, conversation, step and workflow. The run shows as LiveRunRow headed "Handed-off workflow", with 6b-2's controls, "Checked <time>" and "Check now".
  • Otherwise: "Run: <label>" and an "Open run" link (accessible name "Open run of <name>"), with "Status when this message loaded. Open the run for its progress and results."
  • A halted tracker: "Live status isn't available right now." (WORKFLOW_STATUS_HALTED_TEXT), in role="status".
  • The over-limit pause (waiting reason paused): "Paused before reviewing any documents because more matched than one hand-off can review. Cancel it, then ask again with a narrower request." The row offers Cancel run and Open run, with no Approve and no Retry. A pause past the deadline reads as 6b-2's "Timed out", with "It reached its time limit.", instead.
Run status (static label) Label
queued Queued
running, ready_to_resume, resuming, waiting_recovery, cancelling Running
waiting_approval, waiting_output, paused, awaiting_approval, awaiting_sharing_approval, awaiting_analysis_approval, awaiting_run_as_approval, awaiting_sign_in Needs you
completed Completed
completed_partial Partly completed
failed, invalid, incomplete, skipped Failed
cancelled, canceled Cancelled
anything else, or no status Status unavailable

Confirmations

Action Title Description Body Button
Accept "Accept this workflow hand-off?" "SimpleChat creates <name> and starts its one run now." the Covers and Workspaces lines Accept
Start its run (created) "Start the run of this workflow?" "SimpleChat starts the one run of <name> now." "It runs once and is not scheduled." Start its run
Decline "Decline this workflow hand-off?" "SimpleChat will not create <name>." "You can ask again in chat at any time." Decline
Save, in the editor "Save and start this workflow?" "Saving creates this workflow and starts its one run now." the edit limits Save and start

The plan card and Review view

  • The notice has role="note" and the label "Workflow hand-off in this plan". It shows when any enabled step is workflow_handoff, and reads: "This plan hands work off to a one-time workflow, so it always waits for your approval." and "Approving this plan prepares a one-time workflow. Nothing runs until you accept it on the hand-off card that appears under the answer."

  • The approval floor: APPROVAL_FLOOR_CAPABILITIES is ['workflow_run', 'workflow_handoff'], so a timed plan never counts down and an auto plan never runs itself, with or without the server's floor marker. A plan whose hand-off step is turned off, and that has no floor marker, counts down and runs as before.

  • The chip reads "Hand off large work".

  • The step's arguments show three entries:

    • workflow: the name
    • documents: "N named documents" ("1 named document"), or "a search of N workspaces" followed by ", all matches", ", the best match", ", the N best matches" or ", the best matches"
    • tasks: the titles, joined with ", "

    The name and titles render as text and have a matching title.

  • Sentinels (coordinator note 3). These tests assert that no instruction, handle, filter, tag, raw key, hand-off id, document id, digest, sidecar text or result_outputs reaches the page as text or markup:

    • ui_tests/test_v2_orchestration_workflow_handoff_plan.py::test_the_card_explains_the_hand_off_and_only_a_click_runs_it_once covers the Review view and the step's Named outputs, which show only the handoff declaration and "Kind: structured-v1".
    • ::test_a_streamed_hand_off_step_shows_only_its_summary and ::test_a_stored_hand_off_step_shows_only_its_summary send the step's retained result and the server-only sidecar on the step itself, and only the adapter's summary shows.

Delivery footer, running tag and bell

6b-1 and 6b-2 are unchanged. A hand-off run is a chat-started run: accept queues it with trigger_source='chat_orchestration' and a chat_invocation naming the producer plan run, the step and the hand-off. Here is what's checked, and by which test.

functional_tests/test_v2_orchestration_workflow_handoff_client.mjs → "a hand-off run posts back like any chat-started run: its footer, running tag and bell notice all find it". It runs the server's own status-route and delivery code through an inline Python probe, then V2's own functions on the result.

Delivery footer.

  • Metadata. The posted message's workflow_delivery metadata equals what 6b-1's build_delivery_metadata writes from accept's chat_invocation. parseWorkflowDeliveryMetadata round-trips it, including a null orchestration_run_id, step_id or requested_at.
  • Matching (unit test). workflowDeliveryRun finds the tracked row and the delivered row, whose orchestration_run_id is the producer. It finds nothing for another chat, another workflow, a row that names the plan run the answer shows, another step, another conversation or an empty snapshot.
  • Rendered. ui_tests/test_v2_orchestration_workflow_handoff_run_status.py::test_a_handoff_result_posted_to_the_open_chat_reloads_it_and_names_the_workflow:
    • the open chat reloads, announces once and marks the reply read
    • it shows "Results posted below for <name>", the footer's "Open run of <name>" and "Follow up on <name>"
    • there's no plain retry, the result chip is selected, and there's one live row

Running tag.

  • Unit test:
    • workflowRunningLabel and workflowRunningTagLabel read "Running <name>" while the run is queued, running or paused, and nothing once it's delivered.
    • Two runs read "Running 2 workflows".
    • A halted or stopped tracker clears the tag but keeps the label.
  • Rendered. …run_status.py::test_the_running_tag_names_the_handoff_workflow_and_gives_way_to_the_unread_dot: the tag and its title show on the other chat's row, and the unread dot replaces them once the result is posted.

Bell.

  • Unit test:
    • In the server's NOTIFICATION_TYPES, workflow_chat_delivery is {bi-activity, info}, which V2's describeNotification reads as "Workflow results" (info).
    • chat_response_complete is {bi-chat-dots, success}, which V2 reads as "AI responded" (ok).
    • The test also pins the server lines that pick each type: _send_chat_notice calls create_chat_response_notification, which sends chat_response_complete, and _send_notice sends NOTIFICATION_TYPE.
  • Rendered. …run_status.py::test_a_handoff_result_posted_to_another_chat_marks_it_unread_and_notifies_once:
    • one reply announcement, {conversationId, messageId, runId, source:'workflow'}
    • the unread dot
    • a refreshed bell count
    • one desktop notification tagged simplechat-conversation-<id>
    • no mark-read, and nothing repeated after the next global check

Gap. No test renders a hand-off's own row in the bell panel. The rendered "Workflow results" row is covered only by the generic ui_tests/test_v2_notifications_bell.py::test_a_chat_run_notice_reads_as_workflow_results_and_opens_its_run_in_v2, and "AI responded" by ::test_bell_sits_beside_the_brand_and_its_panel_lists_every_kind_of_notice. It's listed under Follow-ups.

The rest of the run row is covered by …run_status.py:

  • test_the_queued_handoff_run_shows_live_from_the_run_that_prepared_it
  • test_an_over_limit_pause_says_why_and_offers_cancel_rather_than_approve
  • test_a_pause_past_the_deadline_reads_as_timed_out_not_as_the_over_limit_pause
  • test_a_handoff_answer_shows_its_run_once_and_never_the_proposal_or_run_cards, for both the mounted run and an earlier attempt
  • test_a_tracked_run_the_handoff_did_not_queue_is_not_shown_as_its_run
  • test_with_workflow_runs_off_the_list_says_unavailable_and_nothing_reads_status
  • test_with_personal_workflows_off_the_card_says_so_and_nothing_reads_status
  • test_a_page_loaded_with_a_tracker_flag_off_shows_the_queued_run_statically
  • test_handoff_and_run_names_render_as_text

functional_tests/test_workflow_handoff_status_route.py runs the real status route for an accepted hand-off's run (test_the_status_route_lists_the_accepted_hand_off_run_for_the_card, test_a_later_attempt_shows_the_run_its_producer_names).

Mutation testing

37/37 killed.

When. These ran on the code commit, e86d282dd, and weren't rerun after the merges, because the code lines they mutate are unchanged (see "This PR's code is the same as before the merges"). The only test change since then is the CodeQL fix, f7e4033a9. It empties the run-links list that RunHarness builds for each instance instead of replacing it, so the run-status test checks the same thing.

Method:

  • Each mutation was applied to the source, the V2 bundle was rebuilt where a UI test needed it, the named tests ran, and the source was restored.

  • A mutation counts as killed only when a test fails on an assertion. A setup error doesn't count.

  • The first runs of M10a, M10b, M14a, M14b, M15 and M18 stopped at the UI tests' stale-bundle check. Each was rebuilt and re-run, and every final record is an assertion failure.

  • Every baseline passed with no mutation:

    Suite Tests
    client .mjs 19
    run_floor.mjs 13
    XSS guardrail 8
    card UI 59
    plan UI 15
    run-status UI 18

The test files:

  • client: functional_tests/test_v2_orchestration_workflow_handoff_client.mjs
  • floor: functional_tests/test_v2_orchestration_workflow_run_floor.mjs
  • xss: functional_tests/test_v2_orchestration_workflow_handoff_xss_guardrail.py
  • card, plan and status: the three ui_tests/test_v2_orchestration_workflow_handoff_*.py files
ID Mutation Caught by
M1 The list parser accepts any run's list client
M2a Accept takes any response client
M2b Decline takes any response client
M2c created needn't agree with 201 client
M3a An as-proposed accept omits mode client, card
M3b Save accepts as proposed card
M4a Accept skips its confirm card
M4b Decline skips its confirm card
M4c The accept confirm drops what the hand-off covers card
M4d Save skips its confirm card
M5a Decline is offered regardless of actions card
M5b Accept is offered based on the state instead card
M6a The Review view renders the raw arguments client, plan
M6b The summary shows instructions client, plan
M6c The summary lists document handles client, plan
M7 The floor list drops workflow_handoff client, floor, plan
M8a, M8b A refused edit drops the draft card
M9a, M9b The URL Access refusal is weakened client, card
M10a, M10b The tracker is joined by the mounted orchestration run client, status
M11a, M11b The creating poll is unbounded, or runs while the tab is hidden card
M12a–d The card mounts on any answer, in a shared chat, on a masked answer, or on another conversation card
M12e Setting-off reads as generic client, card
M13a, M13b "Forbidden" is shown, or the card hides on a code-less refusal client, card
M14a, M14b The run card or the proposal card also mounts status
M15 The pause text is generic client, status
M16 An edited accept re-sends the v3 task_prompt client, card
M17 The notice is dropped plan
M18 A name renders as HTML xss, client, status

Guardrails

I ran the coordinator's runner against V2's tip, the base of this PR's diff:

<venv python> run_v2_guardrails.py <worktree> 49a52bb5c841d98781030b5da49418a3738edb7e HEAD 7b

At this head, 1b2598f52, it found 25 changed files, and all six checks reported no failures.

Check Result
broken-access-control passed (1 file)
xss-sinks passed (9 files)
swagger-routes skipped itself, because no changed Python file defines a Flask route
python-syntax passed (529 files)
malicious-pr-review 0 blockers; 839 keyword findings (767 Important, 72 Moderate)
v2-ts-sinks passed (8 files)

Keyword findings by category:

Category Count
AI, plugin, agent or workspace boundary 334
security control 275
dynamic execution 81
obfuscation 72
secret or sensitive data 70
external connection 7

The most findings are in the client .mjs (236), the card UI test (184), the run-status UI test (84), workflowHandoffs.ts (67) and the plan UI test (65).

I reviewed the findings. All process use is in tests:

  • subprocess.run runs esbuild and node on a temporary bundle in the editor round trip, and the bundle is deleted afterwards.
  • subprocess.run in the restored harness_build.py runs the app's own esbuild under node, with an argument list and no shell, to bundle a UI test fixture.
  • execFileSync runs the inline Python status-route probe in the client .mjs.
  • ast.literal_eval reads string constants from the server source in the card and plan UI tests.

The only URL host is http://simplechat.test.

In CI, malicious-pr-security-review passed. It posts 10 of its keyword findings as warning annotations, and I read each line:

  • two lines of the header comment in WorkflowHandoffCard.tsx (L6 and L11)
  • the notice's role="note" and its icon's aria-hidden in OrchestrationWorkflowHandoffNotice.tsx (L21 and L26)
  • a comment in OrchestrationRunView.tsx (L114)
  • the card's mount comment and gate in MessageList.tsx (L1181–1185)

CodeQL

No new alerts. CodeQL has analyzed refs/pull/1683/merge for three languages on each of five merge refs, 15 analyses in all.

Merge ref V2 base This PR's head Python JavaScript/TypeScript Actions
1896aaf36 b3c0b7b0d 92dc0e1d9 (.279) 1 alert 0 0
99e5fcf28 f1c3ddab0 ba4706b50 (.281) 0 0 0
f9d03f7ed d4cfde733 b8b49f121 (.290) 0 0 0
729b69c93 49a52bb5c 4417d28a8 (.294) 0 0 0
aa8180bed 49a52bb5c 1b2598f52 (.295) no new alerts no new alerts no new alerts

The first four rows come from the code-scanning analyses list, where every analysis ran the same number of rules: 172 for Python, 201 for JavaScript/TypeScript and 27 for Actions. For the fifth, the code-scanning API refused this session's token (HTTP 403), so its result is the CodeQL check run's, below. Each of that ref's three Analyze jobs checked out aa8180b and finished uploading its results. The two commits since 4417d28a8 change only a .gitignore and 20 version lines.

The one alert, fixed. Code-scanning alert 4087, py/overwritten-inherited-attribute, flagged ui_tests/test_v2_orchestration_workflow_handoff_run_status.py L231. There, HandoffRunHarness.__init__ replaced the link_items list that RunHarness.__init__ sets with a new empty one.

  • f7e4033a9 empties that list with self.link_items.clear() instead. RunHarness.__init__ builds a fresh list for each instance, so the test checks the same thing.
  • The alert's instance on this PR's merge ref reads fixed as of 729b69c93, the last merge ref the API let me read. I didn't dismiss any alert.

At this head, 1b2598f52, on merge ref aa8180bed:

  • The CodeQL check run (113048454937) succeeded with "No new alerts in code changed by this pull request" and no annotations.
  • Analyze (python) (113048080207), Analyze (javascript-typescript) (113048080266) and Analyze (actions) (113048080066) succeeded. Each has one annotation, a notice that the ubuntu-latest label moves to Ubuntu 26 from October 19, 2026.
  • All 11 checks on the PR pass, and GitHub reports it mergeable with no conflicts.

Follow-ups

  1. The pre-accept count (decision 4). A real count before accept would need the draft's iterable and a loop preview, which is two requests per card.
  2. The bell gap. No rendered bell row is specific to a hand-off (see "Delivery footer, running tag and bell").
  3. How the pause text is chosen. The pause text keys on the waiting reason paused, because the over-limit pause has no reason code. Any other pause of a hand-off run without a reason reads the same. A server reason code would make it exact.
  4. Retry and the capability allowlist.
    • Retry on a failed hand-off run follows the status route's available flag. That flag comes from workflow_run_settings_gate (functions_orchestration_workflow_context.py L283–296), which also needs workflow_run on the capability allowlist.
    • So an allowlist that admits workflow_handoff but not workflow_run hides Retry, and 6b-2 shows "Starting workflows from chat is turned off, so Retry isn't available here." The resume route (route_backend_workflows.py L1571) would accept the retry.
    • This is untested. It's server or 6b-2 behavior, and isn't changed here.
  5. The chip and the live row. The chip stays "Run queued" while the live row shows the run's real state, and a paused run's static label is "Needs you". This is documented in Known limitations.
  6. The editor round trip needs node_modules. It fails, rather than skips, when application/v2_ui/node_modules is missing. CI would need npm ci first.
  7. The footnote next to Check now. While the tracker is live but hasn't found the run, the static footnote sits next to Check now.
  8. The static Open run link isn't gated on allow_user_workflows. The run page itself refuses.
  9. Pre-existing, and not changed here:
    • 6b-2's workflowDeliveryRun looks a row up by key (snapshot.runs[delivery.run_id]) without comparing its run_id.
    • Named outputs shows only the handoff declaration.
    • workflowForSave rewrites a v3 task_prompt on regular saves.
    • ui_tests/test_v2_orchestration_plan_card.py works only through __main__.
    • The release-notes archive check and 3 link-integrity checks fail the same way at V2's tip.
    • V2's to fix, from Bring V2 User Settings to profile parity with an admin-style layout #1691: the bell and alerts UI tests end with the same 11 teardown errors at V2's tip, because they don't stub five Settings page requests that Bring V2 User Settings to profile parity with an admin-style layout #1691 added in M365Cards.tsx and FactMemoryBench.tsx (see Testing). The fix is stubs in those V2 tests; this PR doesn't change them.
  10. 7a's docs.
    • 7a's docs said in three places that any workspace query matching more documents than its limit pauses: two sentences in CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md, in "The one-time lifecycle" and Known limitations, and the pause row of docs/admin/orchestration.md's troubleshooting table. Only an all_matches query pauses; a best_n query reviews only its best N. f41c26cef corrects all three, along with the same wording in this PR's own card section and the guide's pause row.
    • 7a's v0.261.250 release notes still say "The V2 hand-off card is a separate change; until it ships, users can't accept a hand-off in the browser, so leave the setting off." The coordinator decided to leave that entry as it is: it's in a historical section that V2 owns, and this PR's release-notes section and the admin row already say users can accept hand-offs on the card.
  11. No screenshot is committed. The slot is in docs/reference/chat-controls.md, and the guide section has none.
  12. Test version headers. Coordinator note 9 keeps the floor at .253: the six new Python tests check assert_app_version_at_least("0.261.253"), directly or through IMPLEMENTED_IN. The headers, as renumbered after the merges:
    • The six new Python tests and the .ts probe say Version: 0.261.253 and Implemented in: 0.261.295.
    • The client .mjs says .295 for both.
    • test_v2_orchestration_workflow_run_floor.mjs, which this PR extends, says Version: 0.261.295 and keeps Implemented in: 0.261.212.
    • The card test's bootstrap stub reports version 0.261.295.
    • The restored harness_build.py keeps its own .093 header.
  13. Keeping up with V2.

Paul Lizer (paullizer) and others added 11 commits October 7, 2026 05:30
A plan's workflow_handoff step now gets a hand-off card under its answer:
what the one-time workflow will review, its tasks, and when the offer
expires, with Accept, Edit and Decline. Accept sends mode as_proposed.
Edit opens the prepared draft in the workflow editor, and saving sends an
edited accept that keeps the draft open on a coded refusal. After accept,
the card follows the run through the run tracker, joined by the accepted
run's id, and shows the hand-off's own over-limit pause text. Hand-off
plans need manual approval, show a plan-card notice, and show the
blueprint as a summary instead of raw arguments.

Part of microsoft#1549. Part of microsoft#1543.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Describe the hand-off card under a plan's answer: what it discloses,
Accept, Edit with Save and start, Decline, Start its run, live run status,
the over-limit pause, refusal text and Try again. Add the user guide
section and troubleshooting rows, the chat controls reference, the admin
setting's V2 behavior, the feature doc's V2 section, files, tests and
known limitations, and the v0.261.256 release notes.

Part of microsoft#1549. Part of microsoft#1543.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Part of microsoft#1549
Part of microsoft#1543

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 485d51a (VERSION 0.261.273) so the PR can be checked.

Conflicts:
- application/single_app/config.py: took V2's file (VERSION 0.261.273).
  This branch changed only the VERSION line; the renumber commit sets
  the final number.
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.256) section inserted at the very top, above V2's
  first section, (v0.261.271). Every V2 section is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
7a's feature doc said any workspace query that matches more documents
than its limit pauses before reviewing any (in "The one-time lifecycle"
and in "Known limitations"). Only a query for all matching documents
does:

- functions_workflow_loop_inputs.py L459-462: only an unranked
  (all_matches) query checks its count against the limit; a best_n
  query trims to its own count instead.
- functions_workflow_flow_runner.py L432-433: that limit error is what
  pauses the run before any item.
- functions_workflow_handoff_builder.py L180-186: the card's disclosure
  says the same, limit_behavior 'best_n' versus 'pause'.

Corrected those two lines, and made the pause's cause name the
all-matches query in the V2 card section, the trigger-a-workflow
troubleshooting row and the admin troubleshooting row this branch
already changes. Docs only, so no version bump.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 919375b (microsoft#1682, VERSION 0.261.275), which also
includes microsoft#1681 (VERSION 0.261.274).

Conflicts:
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.256) section inserted at the very top, above V2's
  first section, (v0.261.275). Every V2 section is unchanged.
- application/single_app/config.py merged cleanly to V2's file (VERSION
  0.261.275), because this branch hadn't changed it since the last
  merge. The renumber commit sets the final number.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
e65ab0d removed the agent delegation card and, with it, deleted
ui_tests/fixtures/agent_delegation/harness_build.py, which other V2
browser tests still import. Without it, these 7 files fail at
collection with ModuleNotFoundError:

- ui_tests/test_chat_workflow_results.py
- ui_tests/test_foundry_consent_v2.py
- ui_tests/test_v2_notifications_bell.py
- ui_tests/test_v2_workflow_alert_notices.py
- ui_tests/test_v2_workflow_run_card.py
- ui_tests/test_v2_workflow_run_tracker_spa.py, which imports the bell
  and run card tests
- ui_tests/test_v2_orchestration_workflow_handoff_run_status.py, this
  PR's run status test, which imports the same two

Restore only that file, byte-identical to e65ab0d^ (blob 9dd4f84,
1,539 bytes). The five direct importers pass entry= and bundle= into
their own fixture directories, each with its own .gitignore, so
harness.html, harness_entry.tsx and the old .gitignore stay deleted.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Renumber only this PR's own references to 0.261.277 after merging
paullizer-react-v2-ui at 919375b (0.261.275): config.py, the
release-notes section header, the doc lines that give the V2 card's
version, the Version/Implemented in headers of this PR's new tests, and
the card test's fake bootstrap version. 0.261.277 sits above the
numbers that open PRs already claim, so landing them doesn't force
another renumber. V2's own 0.261.256, 0.261.274 and 0.261.275
references are unchanged, and the test floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 705774d (microsoft#1679, VERSION 0.261.276).

Conflicts:
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.277) section inserted at the very top, above V2's
  first section, (v0.261.276). Every V2 section is unchanged.
- application/single_app/config.py: kept this branch's VERSION
  0.261.277, which is above V2's 0.261.276. The two files differed only
  on the VERSION line.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at b3c0b7b (microsoft#1684, VERSION 0.261.277, and microsoft#1685,
VERSION 0.261.278).

Conflicts:
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.277) section inserted at the very top, above V2's
  first section, (v0.261.278). Every V2 section is unchanged. The next
  commit renumbers this branch's section, because microsoft#1684 already uses
  0.261.277.
- application/single_app/config.py: kept this branch's file (VERSION
  0.261.277) for the merge. The two files differed only on the VERSION
  line. The next commit renumbers it above V2's 0.261.278.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Renumber only this PR's own references from 0.261.277 to 0.261.279
after merging paullizer-react-v2-ui at b3c0b7b (0.261.278), because
microsoft#1684 landed on V2 with 0.261.277: config.py, the release-notes section
header, the doc lines that give the V2 card's version, the
Version/Implemented in headers of this PR's new tests, and the card
test's fake bootstrap version. These are the same 20 lines 82377db
renumbered. 0.261.279 is the next number above V2's 0.261.278 and every
open PR's claim. V2's own 0.261.256 and 0.261.274 to 0.261.278
references are unchanged, and the test floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread ui_tests/test_v2_orchestration_workflow_handoff_run_status.py Fixed
Paul Lizer (paullizer) and others added 18 commits October 7, 2026 10:53
CodeQL flagged HandoffRunHarness.__init__ for overwriting RunHarness's
link_items attribute (py/overwritten-inherited-attribute). Clearing the
per-instance list keeps the same behavior: a hand-off plan has no
workflow_run step, so Phase 5's run-links list stays empty.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 7de8fce (microsoft#1689, VERSION 0.261.279, and microsoft#1686,
VERSION 0.261.280).

Conflicts:
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.279) section inserted at the very top, above V2's
  first section, (v0.261.280). Every V2 section is unchanged. The next
  commit renumbers this branch's section, because microsoft#1689 already uses
  0.261.279.
- application/single_app/config.py: kept this branch's file (VERSION
  0.261.279) for the merge. The two files differed only on the VERSION
  line. The next commit renumbers it above V2's 0.261.280.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 now uses 0.261.279 (microsoft#1689) and 0.261.280 (microsoft#1686), so this branch's
version moves above them. Renumbers the same 20 lines in 15 files as the
previous bump: config.py, this branch's release-notes section, the
documentation version notes and the new tests' headers. The tests' version
floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at f1c3dda (microsoft#1688, which kept VERSION 0.261.280, and
microsoft#1691, VERSION 0.261.281).

No textual conflicts. Both sides set config.py's VERSION to 0.261.281,
so git took it as the same change. The release notes merged with this
branch's (v0.261.281) section at the very top, above V2's first section,
(v0.261.280). microsoft#1691 added its entries to V2's existing sections, so V2's
file has no (v0.261.281) section of its own. The next commit renumbers
this branch to 0.261.282, because V2's VERSION is now 0.261.281.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 now uses 0.261.281 (microsoft#1691), so this branch's version moves above it.
Renumbers the same 20 lines in 15 files as the previous bump: config.py,
this branch's release-notes section, the documentation version notes and
the new tests' headers. The tests' version floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 01c4959 (microsoft#1692, VERSION 0.261.282 and 0.261.283,
microsoft#1695, VERSION 0.261.284, and microsoft#1693, VERSION 0.261.285 and 0.261.286).

Conflicts:
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.282) section inserted at the very top, above V2's
  first section, (v0.261.286). Every V2 section is unchanged. The next
  commit renumbers this branch's section, because microsoft#1692 already uses
  0.261.282.
- application/single_app/config.py: V2's file, including its new
  activity-log indexing policy, with this branch's VERSION line
  (0.261.282) kept for the merge. The VERSION line was the only
  conflicting hunk. The next commit renumbers it above V2's 0.261.286.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 now uses 0.261.286 (microsoft#1693), and microsoft#1692 already uses 0.261.282, so this
branch's version moves above both. Renumbers the same 20 lines in 15
files as the previous bump: config.py, this branch's release-notes
section, the documentation version notes and the new tests' headers.
V2's own (v0.261.282) release-notes section is untouched. The tests'
version floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 05fb0ce (microsoft#1687, VERSION 0.261.287).

Conflicts:
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.287) section inserted at the very top, above V2's
  first section, microsoft#1687's own (v0.261.287). Every V2 section is unchanged.
  The next commit renumbers this branch's section, because microsoft#1687 already
  uses 0.261.287.

application/single_app/config.py merged cleanly: both sides set VERSION
to 0.261.287, so the merged file equals V2's. The next commit renumbers
it above V2's 0.261.287.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 now uses 0.261.287 (microsoft#1687), so this branch's version moves above it.
Renumbers the same 20 lines in 15 files as the previous bump: config.py,
this branch's release-notes section, the documentation version notes and
the new tests' headers. V2's own (v0.261.287) release-notes section is
untouched. The tests' version floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at d4cfde7 (microsoft#1694, microsoft#1696, VERSION 0.261.289).

Conflicts:
- application/single_app/config.py: only the VERSION line. Took V2's
  file (0.261.289).
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.288) section inserted at the very top, above V2's
  first section, its own (v0.261.288). Every V2 section is unchanged.

docs/admin/orchestration.md merged cleanly: V2's edits and this branch's
two lines don't touch each other.

The next commit renumbers this branch's section and VERSION to
0.261.290, above V2's 0.261.289.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 now uses 0.261.289 (microsoft#1694, microsoft#1696) and has its own 0.261.288 release
notes section, so this branch's version moves above both. Renumbers the
same 20 lines in 15 files as the previous bump: config.py, this branch's
release-notes section, the documentation version notes and the new
tests' headers. V2's own (v0.261.288) release-notes section is
untouched. The tests' version floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 56fd78c (microsoft#1700, VERSION 0.261.290).

Conflicts:
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.290) section inserted at the very top, above V2's
  first section, its own (v0.261.290). Every V2 section is unchanged.

application/single_app/config.py merged cleanly: V2 and this branch both
set VERSION to 0.261.290.

V2's other changes, to Sidebar.tsx plus microsoft#1700's fix doc and two tests,
don't touch this branch's files.

The next commit renumbers this branch's section and VERSION to
0.261.291, above V2's 0.261.290.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 now uses 0.261.290 (microsoft#1700) and has its own 0.261.290 release notes
section, so this branch's version moves above it. Renumbers the same 20
lines in 15 files as the previous bump: config.py, this branch's
release-notes section, the documentation version notes and the new
tests' headers. V2's own (v0.261.290) release-notes section and microsoft#1700's
fix doc and tests are untouched. The tests' version floor stays
0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 36f0af1 (microsoft#1701, VERSION 0.261.291, and microsoft#1702,
VERSION 0.261.292).

Conflicts:
- application/single_app/config.py: V2's file byte-for-byte (VERSION
  0.261.292). This branch changes only that line.
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.291) section inserted at the very top, above V2's
  first section, its (v0.261.292). Every V2 section is unchanged,
  including microsoft#1701's own (v0.261.291).

docs/admin/orchestration.md merged cleanly: microsoft#1701's notes and
troubleshooting rows and this branch's settings and hand-off pause rows
are separate hunks.

V2's other changes, to the Ask an agent step's access check, the V2
Control Center and their docs and tests, don't touch this branch's
files.

The next commit renumbers this branch's section and VERSION to
0.261.293, above V2's 0.261.292.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 now uses 0.261.291 (microsoft#1701) and 0.261.292 (microsoft#1702), with their own
release notes sections, so this branch's version moves above them.
Renumbers the same 20 lines in 15 files as the previous bump: config.py,
this branch's release-notes section, the documentation version notes and
the new tests' headers. V2's own (v0.261.292) and (v0.261.291)
release-notes sections and microsoft#1701's 0.261.291 notes in orchestration.md
are untouched. The tests' version floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in V2 at 49a52bb (microsoft#1703, VERSION 0.261.293).

Conflicts:
- docs/explanation/release_notes.md: V2's file byte-for-byte, with this
  branch's own (v0.261.293) section inserted at the very top, above V2's
  first section, microsoft#1703's (v0.261.293). Every V2 section is unchanged.

application/single_app/config.py merged cleanly: both sides set VERSION
to 0.261.293.

OrchestrationRunView.tsx, orchestrationPlan.ts and
docs/admin/orchestration.md merged cleanly. microsoft#1703's changes there pass
the server's after-run chart and diagram checks to the deliverables
list and add its own notes. This branch's changes there are the
hand-off approval floor, the hand-off step's argument text and its
capability label, and its own settings and pause rows. They are
separate hunks and don't interact.

The next commit renumbers this branch's section and VERSION to
0.261.294, above V2's 0.261.293.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 now uses 0.261.293 (microsoft#1703), with its own release notes section, so
this branch's version moves above it. Renumbers the same 20 lines in 15
files as the previous bump: config.py, this branch's release-notes
section, the documentation version notes and the new tests' headers.
V2's own (v0.261.293) release-notes section and microsoft#1703's 0.261.293 notes
are untouched. The tests' version floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The foundry_consent fixture's .gitignore listed only harness.bundle.js.
esbuild also writes harness.bundle.css beside the bundle, and the test's
teardown removes only the .js, so a run left the CSS untracked in the
working tree.

The gap was exposed by bee0515, which restored the agent_delegation
harness_build.py helper that test_foundry_consent_v2.py imports. Without
that helper the test failed at import and never built anything.

Add the four entries every sibling fixture's .gitignore has:
harness.bundle.js.map, harness.bundle.css, harness.bundle.css.map and
__pycache__/.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
microsoft#1704, an open draft into V2, also claims 0.261.294, so this branch's
version moves above every open pull request's claim. V2 is still at
0.261.293. Renumbers the same 20 lines in 15 files as the previous bump:
config.py, this branch's release-notes section, the documentation version
notes and the new tests' headers. V2's own release-notes sections are
untouched. The tests' version floor stays 0.261.253.

Refs microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) marked this pull request as ready for review October 8, 2026 01:31
@paullizer
Paul Lizer (paullizer) merged commit 167096e into microsoft:paullizer-react-v2-ui Oct 8, 2026
11 checks passed
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 8, 2026
Phase 7b (microsoft#1683, v0.261.295) merged, so both halves of Phase 7 are in V2 and the capability works end to end. Record 7b's scope and follow-ups, mark 6c and Phase 8 optional, add three V2 test follow-ups and the deferred count-before-Accept decision, and refresh the stale release-notes count. Docs only; VERSION unchanged.

Refs microsoft#1543 microsoft#1546 microsoft#1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit that referenced this pull request Oct 8, 2026
The base reached 0.261.295 with the workflow hand-off card (#1683) and the
orchestration workflows capability (#1648), so this change moves to
0.261.296. config.py and the release notes conflicted on the version;
the base's v0.261.295 section is kept as is, with this change's entry in
a new v0.261.296 section above it, and the change's test headers, version
floor and docs are renumbered to match.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants