Spine: fan-out rows indent under a parent that isn't drawn, and the review loop rows have no actor prefix #25

Closed
opened 2026-09-14 11:10:02 -04:00 by cmoriarty · 1 comment
Owner

Two things in the spine read wrong since #20 made every step name say who acts.

1. Fan-out rows are indented under a parent that is not there

Fan-out spec rows indented under openspec.propose.proposal

openspec.propose.spec[run-spine] and openspec.propose.spec[agent-view] are shifted right. They look like children of openspec.propose.proposal, but they are not. The space reads as a layout bug.

Why it happens. ui/src/components/Spine.tsx:417 indents any row that has a fanout_key by one level (const indent = step.fanout_key ? 1 : 0). A fan-out node has no row of its own: openspec.propose.spec is replaced by its children. So the indent points at a parent the spine never draws. The same applies to openspec.review[spec|scope|test|risk] when the review loop is open, and to openspec.apply[<group>].

The indent is deliberate. ui/e2e/spine-alignment.spec.ts pins it with "but the label still indents, so depth is still visible". Change that test with the fix.

Options

  • Remove the indent. The [key] suffix already says the rows are siblings of one step.
  • Or make the tree visible. Draw a row for the fan-out step itself, e.g. openspec.propose.spec ×2 with its own status and done count. Put its children under it with a visible connector (a tree guide such as ├/└, or a left rule). It could fold the way the review loop does.

Related, on the same rows: the node name and the key truncate separately, so a narrow spine shows openspec.propose.sp… [agent-vie…. Under a group row, a child could show just its key ([run-spine], [agent-view]), which would also fix the truncation.

2. The review loop's rows have no prefix

Every spine row starts with who acts (git., script., openspec., human.) except the review loop's two rows, which read just review and revise (Spine.tsx:755 renders the side's name). Each side folds steps by different actors:

  • review folds openspec.review[lens] and script.review-digest.
  • revise folds openspec.revise and git.commit.revision.

So no single existing prefix fits either row.

Proposal: an agent. prefix for steps that are a prompt and an opencode session doing the work, as opposed to a script, git, the OpenSpec CLI, or an agent writing OpenSpec's own artifacts. Today every agent step is openspec.*:

step prompt done when touches the OpenSpec change?
openspec.explore explore .osf/explore.md exists no
openspec.propose.proposal openspec-artifact proposal sidecar + openspec-artifact:proposal writes proposal.md
openspec.propose.spec[cap] openspec-artifact openspec-artifact:specs:{cap} writes a spec
openspec.propose.design openspec-artifact openspec-artifact:design writes design.md
openspec.propose.tasks openspec-artifact openspec-artifact:tasks writes tasks.md
openspec.review[lens] (.r2, .r3) review-lens .osf/review/{lens}.json matches its schema no, reads it
openspec.revise (.r2) propose-revise .osf/review/revision-N.md exists rewrites the change's artifacts
openspec.apply[group] apply-group tasks-complete:{group} ticks tasks.md
openspec.reconcile-tasks tasks-reconcile tasks-complete ticks tasks.md
openspec.test.unit test-suite .osf/test/unit.json exists no
openspec.summarize summary .osf/summary.md exists no

Candidates for agent.* are the rows that do not touch the change: agent.review[lens], agent.test.unit, agent.summarize, and possibly agent.explore. Explore is a judgement call, because #20 asked for OpenSpec's explore, propose and apply phases to stand out as OpenSpec steps.

The loop rows could then carry the name of the agent step on each side, e.g. agent.review and openspec.revise.

A rename has the same cost #20 had:

  • runs started before the deploy cannot continue, and a repository's committed .osf/pipeline.yaml keeps the old names;
  • the UI keeps legacy entries so recorded runs still read.

Changing only the displayed label would avoid that, but it would split the name on screen from the id in the pipeline file, logs and API. #20 chose one name everywhere.

Done when

  • No spine row is indented without a visible reason, and spine-alignment.spec.ts checks whatever replaces the indent.
  • Every spine row, the review loop's included, starts with who acts.
  • If ids change, they change everywhere at once, as in #20: src/osf/pipeline/nodes.py, this repository's .osf/pipeline.yaml, the UI's phase, purpose and loop tables (keeping the old ids for recorded runs), docs/guide.md, the README's naming row, the fake osfd and the tests.
Two things in the spine read wrong since #20 made every step name say who acts. ## 1. Fan-out rows are indented under a parent that is not there ![Fan-out spec rows indented under openspec.propose.proposal](https://forgejo.underthere.xyz/attachments/fc5e6735-245e-43db-86e2-dc35a9be4169) `openspec.propose.spec[run-spine]` and `openspec.propose.spec[agent-view]` are shifted right. They look like children of `openspec.propose.proposal`, but they are not. The space reads as a layout bug. **Why it happens.** `ui/src/components/Spine.tsx:417` indents any row that has a `fanout_key` by one level (`const indent = step.fanout_key ? 1 : 0`). A fan-out node has no row of its own: `openspec.propose.spec` is replaced by its children. So the indent points at a parent the spine never draws. The same applies to `openspec.review[spec|scope|test|risk]` when the review loop is open, and to `openspec.apply[<group>]`. The indent is deliberate. `ui/e2e/spine-alignment.spec.ts` pins it with "but the label still indents, so depth is still visible". Change that test with the fix. **Options** - **Remove the indent.** The `[key]` suffix already says the rows are siblings of one step. - **Or make the tree visible.** Draw a row for the fan-out step itself, e.g. `openspec.propose.spec ×2` with its own status and done count. Put its children under it with a visible connector (a tree guide such as `├`/`└`, or a left rule). It could fold the way the review loop does. Related, on the same rows: the node name and the key truncate separately, so a narrow spine shows `openspec.propose.sp… [agent-vie…`. Under a group row, a child could show just its key (`[run-spine]`, `[agent-view]`), which would also fix the truncation. ## 2. The review loop's rows have no prefix Every spine row starts with who acts (`git.`, `script.`, `openspec.`, `human.`) except the review loop's two rows, which read just `review` and `revise` (`Spine.tsx:755` renders the side's name). Each side folds steps by different actors: - `review` folds `openspec.review[lens]` and `script.review-digest`. - `revise` folds `openspec.revise` and `git.commit.revision`. So no single existing prefix fits either row. **Proposal: an `agent.` prefix** for steps that are a prompt and an opencode session doing the work, as opposed to a script, git, the OpenSpec CLI, or an agent writing OpenSpec's own artifacts. Today every agent step is `openspec.*`: | step | prompt | done when | touches the OpenSpec change? | |---|---|---|---| | `openspec.explore` | `explore` | `.osf/explore.md` exists | no | | `openspec.propose.proposal` | `openspec-artifact` | proposal sidecar + `openspec-artifact:proposal` | writes `proposal.md` | | `openspec.propose.spec[cap]` | `openspec-artifact` | `openspec-artifact:specs:{cap}` | writes a spec | | `openspec.propose.design` | `openspec-artifact` | `openspec-artifact:design` | writes `design.md` | | `openspec.propose.tasks` | `openspec-artifact` | `openspec-artifact:tasks` | writes `tasks.md` | | `openspec.review[lens]` (`.r2`, `.r3`) | `review-lens` | `.osf/review/{lens}.json` matches its schema | no, reads it | | `openspec.revise` (`.r2`) | `propose-revise` | `.osf/review/revision-N.md` exists | rewrites the change's artifacts | | `openspec.apply[group]` | `apply-group` | `tasks-complete:{group}` | ticks `tasks.md` | | `openspec.reconcile-tasks` | `tasks-reconcile` | `tasks-complete` | ticks `tasks.md` | | `openspec.test.unit` | `test-suite` | `.osf/test/unit.json` exists | no | | `openspec.summarize` | `summary` | `.osf/summary.md` exists | no | Candidates for `agent.*` are the rows that do not touch the change: `agent.review[lens]`, `agent.test.unit`, `agent.summarize`, and possibly `agent.explore`. Explore is a judgement call, because #20 asked for OpenSpec's explore, propose and apply phases to stand out as OpenSpec steps. The loop rows could then carry the name of the agent step on each side, e.g. `agent.review` and `openspec.revise`. A rename has the same cost #20 had: - runs started before the deploy cannot continue, and a repository's committed `.osf/pipeline.yaml` keeps the old names; - the UI keeps legacy entries so recorded runs still read. Changing only the displayed label would avoid that, but it would split the name on screen from the id in the pipeline file, logs and API. #20 chose one name everywhere. ## Done when - No spine row is indented without a visible reason, and `spine-alignment.spec.ts` checks whatever replaces the indent. - Every spine row, the review loop's included, starts with who acts. - If ids change, they change everywhere at once, as in #20: `src/osf/pipeline/nodes.py`, this repository's `.osf/pipeline.yaml`, the UI's phase, purpose and loop tables (keeping the old ids for recorded runs), `docs/guide.md`, the README's naming row, the fake osfd and the tests.
Author
Owner

Shipped with #52 as the OpenSpec change agent-step-names, archived in #84. Live on production since bb4d708.

1. Fan-out rows: the indent is gone, so openspec.propose.spec[<cap>], the review lenses and openspec.apply[<group>] start on the same column as every other row; the [key] says which siblings belong together. On a narrow spine the step name gives way first, down to two characters and an ellipsis, and the key is kept whole: ope… [discovery-feed] rather than openspec.propose.sp… [agent-vie…. Only a key too long to fit beside that stub is shortened. spine-alignment.spec.ts now checks that labels share one column and that no key that fits is cut.

2. Review loop rows: they read agent.review and openspec.revise, named for who acts in each, for new runs and for runs recorded with the old ids.

PRs: #82 (the change), #83 (a key-width fix found on production: a 75% cap cut [discovery-feed] at 1024px), #84 (archive).

Shipped with #52 as the OpenSpec change `agent-step-names`, archived in #84. Live on production since `bb4d708`. **1. Fan-out rows**: the indent is gone, so `openspec.propose.spec[<cap>]`, the review lenses and `openspec.apply[<group>]` start on the same column as every other row; the `[key]` says which siblings belong together. On a narrow spine the step name gives way first, down to two characters and an ellipsis, and the key is kept whole: `ope… [discovery-feed]` rather than `openspec.propose.sp… [agent-vie…`. Only a key too long to fit beside that stub is shortened. `spine-alignment.spec.ts` now checks that labels share one column and that no key that fits is cut. **2. Review loop rows**: they read `agent.review` and `openspec.revise`, named for who acts in each, for new runs and for runs recorded with the old ids. PRs: #82 (the change), #83 (a key-width fix found on production: a 75% cap cut `[discovery-feed]` at 1024px), #84 (archive).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cmoriarty/braid#25
No description provided.