fix(cli): a step's extent is only as strong as the evidence that closed it - #777
Merged
blafourcade merged 2 commits intoSep 6, 2026
Merged
Conversation
…ed it Reading every `step_start` as the end of whatever was open assumes a session only ever runs one skill after another. An orchestrating skill's whole job is to invoke others, so that assumption cuts it short at its first child. Measured on the one orchestrated session captured, 2026-09-04: `aidd-orchestrator:01-sdlc` opened at 05:56:27 and `aidd-pm:04-spec` at 05:59:53, so the orchestration read as 206 seconds against a session that ran until 09:27:21. `by_flow`, reading the same journal under the rule this adopts, named 1,052 records for that skill while `by_step` named 1. `buildStepIntervals` is now two walks over the same lines. The orchestrating half *is* `buildFlowIntervals` — a flow is an orchestrating step, and calling it rather than restating it is what keeps the two axes from drifting apart again. The other half opens on a non-orchestrating `step_start` and keeps the rule it always had: any later `step_start` closes it, because a session that starts orchestrating is no longer running the plain skill it was running before. `attributeMoment` now answers with the innermost interval around a moment rather than the first one in the array. Both claims are true of a record inside an invoked step; the inner one says more, and the outer one goes on answering for every moment the inner one does not cover. Array order decides nothing, which matters now that two separate walks build the list. Which skills orchestrate is `ORCHESTRATING_SKILLS`'s declaration, never inferred: nesting and sequence produce the identical journal, so no rule read off the boundaries alone separates them. The limit is stated in code — a skill that invokes another without being declared an orchestrator is still cut short by its own child. **Measured effect on today's data: none.** `by_step` reads `aidd-dev:01-plan=972, aidd-orchestrator:01-sdlc=1` before and after. The tail of that session is owned by `aidd-dev:01-plan`, an invoked step nothing closed, which runs to the journal's last witnessed moment and wins on having started later. Telling an interval closed by evidence from one merely bounded is the next change, and this two-walk shape is what it needs to fall back to. Mutations run, both killed: the outermost interval winning, and a single walk opening on every `step_start`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
An interval nothing ever closed ends at the journal's own last witnessed moment. That end is a bound, not a measurement, and reading it as an extent is how a step that opened shortly before a long session went on working comes to be credited with all of it — purely for having opened later than the orchestration around it. Measured on the one orchestrated session captured, 2026-09-04: `aidd-dev:01-plan` opened at 06:00:50 inside an orchestration opened at 05:56:27. Neither was ever closed, so both ran to 09:27:21 and the innermost-start rule gave the invoked step every record in between. `buildClosedIntervals` now tells its caller which of the two ended an interval — a moment the journal witnessed, or the cap standing in for one it never did — and `answersFor` reads it: an interval closed by the journal's end yields to one that encloses it and was never closed either. Two values and not three. "Closed by its own `step_end`" and "closed by a later `step_start`" are different strengths of evidence and both were weighed; both are nevertheless a moment the journal witnessed, and no caller separates them, so a third value would be structure nothing reads. The yielding is that narrow and no wider. Where the enclosing interval states its own end, the inner one runs past it, nothing encloses it, and the innermost claim stands — the same answer it gets when both ends are witnessed. An unclosed interval nothing encloses still answers for its own moments: what is refused is preferring a bound over a wider claim covering the same moment, never the bound. No tie between two unclosed sibling steps can arise, and it is not this rule that prevents it: any `step_start` closes whichever plain step was open, so at most one invoked step is ever left unclosed at a time. The test says so by demonstration rather than a comment. Containment between two unclosed intervals is compared on the start alone. They all end at the same cap, so comparing the ends would be a clause no input can make false — the kind of default that hides a wrong number. Measured on the real sink, 30 days, 31,025 requests, every axis reconciling: by_step aidd-orchestrator:01-sdlc journal-interval 1 -> 973 by_step aidd-dev:01-plan journal-interval 972 -> 0 973 is the whole of what the two walks can agree on. 1,052 records fall inside that flow interval; 79 of them carry a step their own tool stated (12 `aidd-orchestrator:01-sdlc`, 61 `aidd-dev:01-plan`, 6 `aidd-pm:04-spec`) and keep that stronger claim on the step axis. 1,052 - 79 = 973, and `by_flow` reads 1,052 for the same interval. This moves attribution within the 1,052 records the journal covers. It does not move the 27,393 unattributed ones: those predate the journal, from one session resumed since 2026-08-13, and no interval rule reaches them. Mutations run, three killed by three distinct guards: never yielding, yielding to any earlier start, and letting a closed interval yield too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
blafourcade
enabled auto-merge (squash)
September 6, 2026 02:41
blafourcade
deleted the
fix/a-nested-step-does-not-close-the-one-that-invoked-it
branch
September 6, 2026 06:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two faults in
buildStepIntervals, one commit each, both about the same thing: the step axis read an interval's extent without asking what established it.Measured on the one orchestrated session captured, 2026-09-04,
aidd-orchestrator:01-sdlcread as 206 seconds against a session that ran until 09:27:21, andby_stepnamed 1 record for it whileby_flow, reading the same journal, named 1,052.1. An invoked step does not close the orchestration that invoked it
Reading every
step_startas the end of whatever was open assumes a session only ever runs one skill after another. An orchestrating skill's whole job is to invoke others, so the assumption cut it short at its first child — 05:56:27 to 05:59:53.buildStepIntervalsbecomes two walks over the same lines. The orchestrating half isbuildFlowIntervals: a flow is an orchestrating step, and calling it rather than restating its rule is what keeps the two axes from drifting apart again. The other half opens on a non-orchestratingstep_startand keeps the rule it always had — any laterstep_startcloses it, because a session that starts orchestrating is no longer running the plain skill it was running before.attributeMomentanswers with the innermost interval around a moment rather than the first in the array. Both claims are true of a record inside an invoked step; the inner one says more, and the outer one goes on answering for every moment the inner one does not cover. Array order decides nothing, which matters now that two separate walks build the list.Which skills orchestrate is
ORCHESTRATING_SKILLS's declaration, never inferred: nesting and sequence produce the identical journal, so no rule read off the boundaries alone separates them. The limit is stated in code — a skill that invokes another without being declared an orchestrator is still cut short by its own child.On its own this commit changed nothing on the real data, and its message says so. The tail of that session was still owned by
aidd-dev:01-plan, an invoked step nothing closed, which ran to the journal's last moment and won on having started later. That is the second fault.2. A step nothing closed yields to the one that encloses it
An interval nothing closed ends at the journal's own last witnessed moment. That end is a bound, not a measurement.
buildClosedIntervalsnow tells its caller which of the two ended an interval — a moment the journal witnessed, or the cap standing in for one it never did — andanswersForreads it: an interval closed by the journal's end yields to one that encloses it and was never closed either. The enclosing one is at least still known to have been open at that moment; the inner one's extent rests on nothing.Two values and not three. "Closed by its own
step_end" and "closed by a laterstep_start" are genuinely different strengths of evidence, and both were weighed; both are nevertheless a moment the journal witnessed, and no caller separates them, so a third value would be structure nothing reads.The yielding is that narrow and no wider:
step_startcloses whichever plain step was open, so at most one invoked step is ever left unclosed at a time. The test demonstrates that rather than a comment asserting itContainment between two unclosed intervals is compared on the start alone, because they all end at the same cap — comparing the ends would be a clause no input can make false.
Measured on the real sink
30 days, 31,025 requests, every axis reconciling to the same total:
by_step·aidd-orchestrator:01-sdlc· journal-intervalby_step·aidd-dev:01-plan· journal-interval973 is the whole of what the two walks can agree on, and the difference from
by_flowis accounted for line by line: 1,052 records fall inside that flow interval, and 79 of them carry a step their own tool stated — 12aidd-orchestrator:01-sdlc, 61aidd-dev:01-plan, 6aidd-pm:04-spec— which is the stronger claim and the one the step axis keeps. 1,052 − 79 = 973.Twelve of those 79 are tool-stated
aidd-orchestrator:01-sdlc, whichby_flowstill counts inside its own journal-interval row — so the same records readjournal-intervalon one axis andtool-statedon the other. That asymmetry predates this change, both axes still reconcile to the period total, and nothing here touches it; it is named so the arithmetic above does not read as tidier than it is.Read this as attribution moving, not as coverage rising. It moves records within the 1,052 the journal covers. The 27,393 unattributed ones predate the journal, from a single session resumed since 2026-08-13, and no interval rule reaches them.
prompt-matchedis still 0.0% and untouched by this.What this rule's life expectancy is
It is the right answer for the journals that already exist, where a session's end is never written down. It is not the permanent answer. A
SessionEndhook — available on all five tools, and the next change afterUserPromptSubmit— replaces the inferred cap with an observed end, and every interval that gains one stops being ajournal-endinterval and stops yielding. The rule fires less often the more the capture improves, which is the direction it should go.Tests
Seven cases, each written before the code that satisfies it; four were red at the start.
step_startanswersForanswersForMutations run, five killed:
step_startopenspnpm test3478 passed / 309 files. Typecheck clean, biomeciexit 0, knip clean, jscpd exit 0, bundle 596.6 / 598 KB, layering clean, 371 repository script tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp