story-091: One command files a story - #89
Merged
Merged
Conversation
Implemented by the l5 harness story workflow.
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.
Story
One command files a story.
The outbox had a queue and a drain but nothing that could file an entry. This adds the transport: one command, invoked once per entry, through which the harness knows nothing else about the tracker. No provider abstraction, no client library, no tracker vocabulary anywhere in harness source.
The contract
key,identity,stateandpayload.L5_SYNC_KEY— the entry's key, taken from the same field as the copy on stdin so the two cannot disagree.last_error, bounded to a tail, so a failed or pending entry says why in its own file.A zero exit naming no reference is read as transient, not as success, because it establishes nothing about whether the request arrived.
75 is EX_TEMPFAIL, and the choice is recorded rather than arbitrary: 1 and 2 are ordinary failures, 126 and 127 mean the command could not be executed, 128+N is a signal. "A small number would collide with all of those; this one collides with nothing."
The timeout is not optional
sync_timeout_secondsdefaults to 60 rather than 0, and the reason is written where a reader meets it: unlikemax_pause_wait_seconds, a zero here would mean no timeout, which is the failure the bound exists to prevent. So the one duration written in harness source is a real one and every path through the transport is bounded in time.The kill reaches the command's process group — the command is spawned with
start_new_session=Trueand killed withos.killpg— so a command that spawned children of its own leaves none behind. A command killed this way leaves its entry pending rather than failed, because running past a timeout established nothing about whether the request arrived.The reference read from stdout is length-bounded too: it is written into a durable file and validated against the entry schema, so an unbounded one would be an unbounded write of somebody else's stdout into the queue.
Idempotency is the command's promise, and the harness says it cannot check it
The command must search the tracker for the key before it creates anything, which is what makes the ambiguous write — created, response lost — safe to retry. The harness cannot enforce this, and the schema says so rather than implying a check that does not exist. Same for a sync command must not commit: it writes, and a human or a run commits.
templates/sync/github.shis a reference implementation of the whole contract, installed byl5-initinto.harness/sync/and executable. It writes the key into the issue body as a marker and creates only when a search for that marker finds nothing; a search that fails is transient rather than terminal, because creating on a failed search is exactly the duplicate the mechanism exists to avoid. Its board step is opt-in and its own business — the harness knows nothing about it.Evidence
suite-run-result.json, exit 0)clean-clone-result.json, exit 0)tests/test_outbox.py::test_no_module_a_run_executes_reaches_the_outbox— exit 1 reverted, exit 0 appliedtests/test_command_transport.pyTested with fixture commands and never a network: one exiting 0 with a canned reference, one transient, one terminal, one hanging past the timeout, one not executable, one invoked twice with the same key.
Still no producer, deliberately
test_no_module_a_run_executes_reaches_the_outboxholdsstory_coordinator.pyto reaching the queue not at all — the only module that reaches it iscommand_transport.py, the drain side — with a planted-call-site control and an assertion that the single exemption has not gone stale.l5-syncremains the only drain site and says why in its own docstring: nothing enqueues anything yet, so there is no queue accumulating unattended, and the story that adds a producer is the first point at which an automatic sweep earns its call site.Running
l5-syncon this repository today reportslanded 0, pending 0, failed 0, poisoned 0.Notes for review
This repository configures
sync_commandfor itself, so the script the harness hands to every target is the script this repository is exercised by. The template ships both keys commented out, andtest_a_freshly_initialized_target_sets_neither_new_keyasserts a fresh target opts in to filing rather than discovering that it files — with this repo's own config as the control, so "unset" is a fact about the template rather than about a reader finding nothing.On the two copies of
github.sh: they are byte-identical, and a test pins them so.templates/sync/github.shis mode 100644 in git while.harness/sync/github.shis 100755, which does not matter —l5-initdoes an explicitchmod(0o755)after copying rather than relying on git's mode bit, andtest_l5_init_installs_the_reference_command_executableasserts the installed copy is executable.The verifier's second
unverifiedis resolved by evidence it did not have. It could not confirm the new sync scripts would survive into a clean clone, because they were untracked when it read the tree. They are in the story commit, and the clean-clone suite passed afterwards.Two operational gaps worth knowing before a producer lands, neither a defect in this story:
--label l5.gh issue createfails if that label does not exist, and the script maps that to exit 75 — so entries would pend and retry indefinitely rather than failing loudly. Create the label, or setL5_SYNC_LABEL, before the first producer merges.gh project item-addputs an item on a project and sets no field; choosing a column needs anitem-editcarrying project, field and option ids, which the script neither resolves nor accepts. Per-target settings — board, label, repository — come from ambient shell environment today and are recorded nowhere in the repo. Worth a story, sequenced after the producer so it can be verified against a real entry.🤖 Generated with Claude Code
https://claude.ai/code/session_01HkujNrgMcsRBFWNEKWYnmA