Skip to content

Hand over the manual step, and talk before building a conversation-first ticket - #709

Merged
thomasluizon merged 2 commits into
mainfrom
feature/harness-manual-steps-and-conversation-gate
Aug 10, 2026
Merged

Hand over the manual step, and talk before building a conversation-first ticket#709
thomasluizon merged 2 commits into
mainfrom
feature/harness-manual-steps-and-conversation-gate

Conversation

@thomasluizon

Copy link
Copy Markdown
Owner

Two output surfaces the harness was missing. Neither is a new gate. A missing Rollout section produces silence; a conversation-first ticket under --sleep is a deferral with a reason, never a crash. Nothing here verifies the harness against itself, nothing polls GitHub, and nothing is mirrored into orbit-api.

Half 1: a manual step must survive the merge

orbit-tickets#81 closed Done on 2026-08-08 carrying, verbatim: "Rollout: merge, deploy to Render, then set PostHog:ApiKey in the Render env. The code path is inert until the key exists." Review was clean, CI was green, complete-ticket.mjs closed the issue. Nobody set the key, so AddOrbitProductAnalytics bound NoOpProductAnalytics and every signup_completed and subscription_* event for two days was discarded. Found only because Thomas asked a passing question. Every gate here measures the PULL REQUEST, and that step is not in one.

  • tools/lib/manual-steps.mjs extracts the step from the Rollout/kill-switch section, splits it into the clauses that act outside the repository ("merge" is the harness's job and is dropped), and expands each into concrete numbered actions.
  • complete-ticket.mjs posts them as a ticket comment before it closes the issue, and returns them on manualSteps. --preflight prints them and writes nothing.
  • /orchestrate step 14 and the /merge-prs final report both print them, the two moments a human is reading.

Only verified detail is expanded. The Render navigation labels come from Render's own configure-environment-variables documentation, read 2026-08-10. The __ to : mapping was proven by execution on .NET 10.0.204, not recalled. Everything else is quoted from the ticket, including the confirmation line.

Half 2: a genuinely interactive ticket must talk first

ORB-30 (#36) names Pencil as the prototyping tool in one section and Claude Design in another while saying Pencil is retired, and its acceptance criteria carry a human grant no agent can satisfy. A headless worker executes it confidently against the retired tool and fails its verdict however good the work is.

  • classifyConversationFirst in tools/lib/ticket-executability.mjs detects a human grant, a tool contradiction, a choice delegated to the implementer, and a product/brand/copy/price call.
  • Under --sleep, plan-queue.mjs defers it NEEDS_CONVERSATION with its open questions printed. Attended, it is admitted with a CONVERSATION FIRST warning and its questions attached.
  • /orchestrate step 2b now converses one topic at a time and writes the answers back to the ticket with the new comment-ticket.mjs before composing the worker prompt. That path already exists: a comment is part of the work order, the later comment wins, and compose-prompt.mjs already passes comments through.

Labels, created live on thomasluizon/orbit-tickets: needs:conversation forces it on, needs:no-conversation forces it off. The spec asked for one label overriding "in both directions"; one label cannot express both, so this follows the repo's existing parity:yes/parity:no shape. Rename either if you prefer different words. Both were created through createLabel in the ticket adapter, never a raw gh label create.


Proof

1. node tools/test-tools.mjs

TOOLS_EXIT=0

ORBIT TOOLS GATE OK

Exit code checked directly, not the tail of a pipe.

2. node .claude/hooks/test-hooks.mjs

HOOKS_EXIT=0

ORBIT HOOKS OK

This was red on main before this PR, identically: FAIL adapter wake-source: a live wake source allows the stop -> 0 got=2 want=0. The gate counted registered wake sources where the hook counts live ones, so a dead overnight process left it failing. Fixed in this PR.

3. Red-capable proof for half 1: the real orbit-tickets#81 body

--- manual sections=["Rollout / kill switch"] steps=2

1. deploy to Render
   - Open the Render Dashboard, select the service this repository deploys, and confirm it has deployed the merge commit.

2. set `PostHog:ApiKey` in the Render env
   - Open the Render Dashboard and select the service this repository deploys.
   - Click the "Environment" tab in the left sidebar.
   - Click "+ Add Environment Variable".
   - Key: `PostHog__ApiKey`
   - NOT `PostHog:ApiKey`. .NET maps `__` in an environment variable name to `:` in a configuration
     key, so `PostHog__ApiKey` is what binds `PostHog:ApiKey`. Proven by execution on .NET 10.0.204
     with Microsoft.Extensions.Configuration.EnvironmentVariables, not from memory.
   - Choose "Save, rebuild, and deploy" from the save dropdown, so the running service picks the value up.
   - Confirm it took effect. Expected behaviour says: "With `PostHog:ApiKey` set on Render: each of the
     five events appears in the PostHog US project with `distinct_id` = the user GUID and the person
     properties above."

**To reverse it**

- remove the Render env var; next restart reverts to the no-op implementation.

A ticket with no Rollout section produces (nothing at all); renderManualSteps returns null, so no comment is posted and nothing is printed. Asserted in tools/__tests__/manual-steps.mjs.

The __ mapping was proven, not assumed:

$ PostHog__ApiKey="phc_double_underscore_wins" dotnet run
PostHog:ApiKey via double underscore = 'phc_double_underscore_wins'

4. Red-capable proof for half 2

node tools/plan-queue.mjs --tickets "#36" --sleep --format markdown:

# Queue plan (0 admitted, 1 deferred)

## Deferred
- ORB-30 NEEDS_CONVERSATION: reads as conversation-first (HUMAN_GRANT, DELEGATED_CHOICE,
  TOOL_CONTRADICTION, from the body) and nobody is awake to answer. Run it attended
  - ASK Acceptance criteria carries a human grant no agent can satisfy: "* Thomas has opened the page
    and approved the direction. This is a human grant (D13); no gate and no agent may substitute for
    it, and the R tickets do not start". Split the grant into its own ticket, or accept this one
    stopping short of it?
  - ASK Scope leaves a choice to the implementer: "* Decide the stacked-CTA width-matching convention
    (user-found defect 9, open question from 2026-07-19).". Which option, and why?
  - ASK The body calls Pencil retired ("...(Pencil is retired); the three ui-skills harvest notes as
    mandatory input...") and still instructs using it ("* Then build the prototype in Pencil
    (`pencil.dev`, via the `pencil` MCP server)..."). Which one is current?

A plain code ticket does not trip it. orbit-tickets#13 (Record Astra chat token usage), run through the classifier against its real 14,863-character body:

===== #13 [Bug,repo:api] repo=api
conversationFirst=false source=null signals=none

Attended, a conversation-first ticket is admitted rather than deferred:

$ node tools/plan-queue.mjs --tickets "#296" --format markdown
# Queue plan (1 admitted, 0 deferred)

## Wave 1
- #296 `landing` (opens against main) Turnstile challenge is clipped below about 375 px, ...
  - WARNING CONVERSATION FIRST (DELEGATED_CHOICE): ask its 1 open question(s) at step 2b before any worker spawns

5. Dry run over all 166 open ticket bodies

Requested by the addendum, and not acted on. One command, two live reads.

Conversation-first: 15 of 166 (9%).

#24  DELEGATED_CHOICE                              Chat surface UX: localized tool-result cards
#36  HUMAN_GRANT,DELEGATED_CHOICE,TOOL_CONTRADICTION  Rewrite DESIGN.md (ticket 0)
#38  HUMAN_GRANT                                   Mobile screenshot capture path with Maestro
#67  DELEGATED_CHOICE                              Redesign onboarding, tour, feature guide (R14)
#72  DELEGATED_CHOICE                              Redesign upgrade, trial, referral (R17)
#79  HUMAN_GRANT,PRODUCT_CALL                      Flat SVG logo candidates, land the mark (A1)
#181 DELEGATED_CHOICE                              Bulk-move selected habits
#196 DELEGATED_CHOICE                              AI quota copy becomes daily
#203 DELEGATED_CHOICE                              Raise AppConfig.MinSupportedVersion off 0.0.0
#209 DELEGATED_CHOICE                              Landing pricing and FAQ copy realigned
#217 DELEGATED_CHOICE                              Design the first experience
#254 DELEGATED_CHOICE                              Chat habit-tool follow-ups from ORB-23
#279 DELEGATED_CHOICE                              Privacy policy discloses no cookie information
#296 DELEGATED_CHOICE                              Turnstile challenge clipped below 375 px
#298 DELEGATED_CHOICE                              Social tab does not update the URL

Carries manual steps: 13 of 166, including #144 (Stripe prices, Render variables, Play Console pricing), #82 (create the PostHog flag, flip FeatureFlags:Provider=PostHog on Render), #83 (NEXT_PUBLIC_POSTHOG_KEY in Vercel, EXPO_PUBLIC_POSTHOG_KEY), #34, #203, #208, #213.

An earlier revision flagged 19 and 15. Four conversation false positives and two extraction false positives were found by this dry run and fixed, each with the measurement in a code comment:

  • \brender\b matched the RENDER in RENDER-CORRECTNESS.md and "nothing to render". Platform names are now matched case-sensitively as the proper nouns they are.
  • set ... key matched "storing the granted scope set per key". set must now lead the clause.
  • The Linear migration footer landed inside the last section, so #144 handed over its own Linear URL because the slug contains "stripe". The footer is now excluded.
  • "It is dead debt" captured It as a retired tool, and "D28 is dead" captured a decision id. Both are now stopwords, and the instruction match must be on a different line.
  • "Manually pick the grandchild" in a Test scenarios section read as a delegated choice. Test sections are excluded from the conversation scan.
  • A comma inside a backticked SQL string split the clause and handed over half a query. Code and bracket spans are masked before splitting.

The one live instance, still outstanding

PostHog__ApiKey is genuinely not set on the Render API service right now. This is Thomas's click, not the harness's, and it is not done. This is exactly what the mechanism above would have printed on 2026-08-08:

  1. Open the Render Dashboard and select the orbit-api service.
  2. Click the Environment tab in the left sidebar.
  3. Click + Add Environment Variable.
  4. Key: PostHog__ApiKey. Not PostHog:ApiKey: .NET maps __ in an environment variable name to : in a configuration key, and a literal colon is not a usable environment variable name on Linux. Verified against src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs:329, which reads the section PostHog into PostHogSettings.ApiKey.
  5. Value: the PostHog US Cloud project API key (the phc_... write key), from that project's settings.
  6. Choose Save, rebuild, and deploy from the save dropdown. "Save only" leaves the service running without the value.
  7. Confirm. The ticket's own Expected behaviour says: "With PostHog:ApiKey set on Render: each of the five events appears in the PostHog US project with distinct_id = the user GUID and the person properties above." Sign up with a throwaway address and watch signup_completed arrive.

Events discarded between 2026-08-08 and now are gone; PostHog has no backfill for them.


Notes

  • Scope: orbit-ui-mobile only. Nothing mirrored into orbit-api, no drift check, no second copy of any contract.
  • One new tool, comment-ticket.mjs, with its test-tools.mjs coverage row and case module in this PR. sync-issue-state.mjs could not carry this: it requires --pr, --state and both SHAs, and the step 2b comment is written before any worktree exists.
  • GraphQL budget: the dry run is one gh issue list plus one gh project item-list. No poll and no per-loop call was added. The interactive proof runs above did exhaust the hourly budget near the end; that is this session's measuring, not the shipped code's behaviour.
  • Unrelated, moved not deleted: an untracked .tmp-extract-transcript.mjs (dated 2026-08-08) sat in the repository root and failed the root-allowlist pre-commit hook. It was never committed and is not mine, so it was moved to this session's scratchpad rather than deleted. Nothing references it.
  • Not merged, not pushed to main, not force-pushed.

…esign ticket

Two output surfaces the harness was missing. Neither is a new gate: a missing
Rollout section is silence, and a conversation-first ticket under --sleep is a
deferral with a reason, never a crash.

Half 1, a manual step must survive the merge. orbit-tickets#81 closed Done on
2026-08-08 carrying "Rollout: merge, deploy to Render, then set PostHog:ApiKey
in the Render env. The code path is inert until the key exists." The PR was
perfect and the key was never set, so NoOpProductAnalytics discarded every
signup_completed and subscription_* event for two days. Every gate here measures
the pull request; that step is not in one.

tools/lib/manual-steps.mjs extracts the step, expands it into concrete numbered
actions, and quotes the ticket for the confirmation. complete-ticket.mjs posts it
as a comment BEFORE it closes the issue, and returns it so /orchestrate step 14
and the /merge-prs report can print it. Only verified detail is expanded: the
Render navigation labels come from Render's own documentation, and the __ to :
mapping was proven by running .NET 10.0.204, not recalled.

Half 2, a genuinely interactive ticket must talk first. ORB-30 (#36) names Pencil
as the prototyping tool in one section and Claude Design in another while saying
Pencil is retired, and its acceptance criteria carry a human grant no agent can
satisfy. classifyConversationFirst detects that shape; plan-queue defers it
NEEDS_CONVERSATION with its open questions under --sleep, and admits it with a
warning when Thomas is awake. comment-ticket.mjs writes the answers back to the
ticket, which /orchestrate already treats as part of the work order.

Also fixes the wake-source gate test, which counted registered wake sources where
the hook counts live ones, so a dead overnight process left it red on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
orbit-ui-mobile-web Ignored Ignored Aug 10, 2026 3:59pm

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db693507e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tools/lib/manual-steps.mjs Outdated
Comment thread tools/lib/manual-steps.mjs Outdated
Comment thread tools/lib/github-issues.mjs Outdated
…versal, drop the unused label write

Three Codex findings on #709.

P1, a standalone `## Kill switch` heading. Its bullets start straight in on the
action with no label to strip, so they read as outstanding steps and the renderer
expanded "Remove PostHog:ApiKey" into "Click + Add Environment Variable": the
exact opposite of the intent. Reproduced, then fixed by classifying the whole
section as reversal when its heading is a reversal heading.

P2, a rollout organized under child headings. The flat heading filter selected
the empty parent and dropped every child, so extraction returned nothing and
completion closed the ticket with no comment at all. A matched heading now
carries its scope through its descendants, the way inScopeSections already
carries Out of scope through its own, and a sibling heading ends the region.

P1, evidence for the label write. createLabel has no callers now that the two
labels exist, so rather than record evidence for a write path nothing uses, it is
deleted. CLAUDE.md standard 2.

Also corrects the record. The premise that #81's key was never set is FALSE and
is removed from every comment and contract that asserted it. Verified live
2026-08-10 against the PostHog project: posthog-dotnet 2.12.1 has delivered
signup_completed and the four subscription_* events since 2026-07-25, 20 signups
with distinct_id set to the user GUID, plan set on all 21 people, latest event
today. Nothing was discarded. The step was never SURFACED, which is the actual
gap and still worth closing; 13 of 166 open tickets carry one of the same shape.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@thomasluizon
thomasluizon merged commit d297f56 into main Aug 10, 2026
26 checks passed
@thomasluizon
thomasluizon deleted the feature/harness-manual-steps-and-conversation-gate branch August 10, 2026 16:20
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.

1 participant