Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .harness/docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2047,6 +2047,22 @@ story-128 changes how the board block learns what a board already says about the

**The stub `gh` in `tests/test_filed_query.py` grew the read and lost the listing**, and the assertions moved with it: the ledger records the graphql call as it records the `project` subcommands, `OMIT_VARIABLE` now makes the node read report no such item rather than making the listing report nothing, and the read failing is driven beside it so both routes to *unobtainable* are exercised. The assertions new in kind rather than repointed are that a filing writing every field records no project item-list call at all, that the transient stderr carries no number, and that a board seeded past the retired bound files an item added at the end of its order. A scan holds both sync scripts to mentioning neither `gh project item-list` nor `ITEM_LIST_LIMIT`, with a control that plants a listing back into a script and requires it reported, so the retirement is held by something that reddens rather than by this paragraph.

## A field name is matched by one rule wherever it is matched

story-129 gives the sync script's field-name comparison a single home. The script asked "is this the field this target configured?" in three places and the three did not agree: the read of the item's current value normalized both sides — spaces removed, case ignored — while the lookup that resolves a field's id and the field-name half of the lookup that resolves an option's id matched the configured name against the board's **verbatim**. A target configuring `STATUS_FIELD="status"` against a board titled `Status` therefore read the field successfully, found it empty, and then resolved no id to write into.

**The two halves failed differently, and neither failure named its cause.** For Status a field resolving to no id is transient: the filing exits 75, the outbox entry stays pending, and every later sweep retries a configuration that cannot land — the same never-lands shape story-128's bounded listing had, arrived at from a different direction. For the five classification fields it is quiet: the field is skipped with a line on stderr, the filing exits 0, and the board is simply missing a value nobody was told about. Any difference in case or internal spacing that the read tolerated and the write did not produced one of those two.

**The fix is one definition rather than three sites made to agree.** `SAME_FIELD_NAME` holds a `jq` definition of `same_field_name($a; $b)` — both names compared with their spaces removed and their case ignored — and `board_value`, `field_id_for` and `option_id_for` each prepend it to their own program and call it. It is a `jq` function rather than a shell one because the comparison happens inside `jq` in all three places. This widens only: a configured name that already matched the board exactly still matches, since equality survives removing spaces and lowering case on both sides. One edit reaches all six configured names, because every write resolves its id through `field_id_for`.

**The option-name half is deliberately untouched and stays verbatim.** An option is the value a person reads off the board, so the board's own spelling is the one that goes there: an option value that does not match the board exactly still resolves to nothing, and still costs the Status filing a transient exit and a classification field that field alone. The script says which three sites share the rule and that option names sit outside it, so the sharing does not have to be inferred from the code. Nothing else moved — the write is still made only where the item reports the field empty, the two failure classifications are unchanged, and the project, its field list and the item's field values are each still read at most once per filing.

**The existing coverage could not have caught this, and the reason is worth knowing before writing the next board test.** `THIS_TARGETS_STATUS_FIELD` supplied both the configured name and, through `seeded_board()`, the board's own name, so the two sides were the same string *by construction*; the older test for a field name carrying a space renamed both halves together for the same reason. A test that derives the board's state from the value under test cannot exhibit a disagreement between them. The new cases in `tests/test_filed_query.py` derive the configured name from the board's by `swapcase()` and assert both that it differs and that it differs in case alone, over both sync copies — one for Status, one for a classification field, and one holding the option-name rule still narrow.

**Beside the filings sits a scan that holds the sharing rather than the agreement.** The filings assert the three lookups agree today; `test_every_field_name_lookup_defers_to_one_statement_of_sameness` asserts they cannot come to disagree tomorrow — the script states the rule once, no line outside that statement states it again, and each of the three defers to it. The shared definition is matched by its shape rather than its name, so the assertion is about stating-the-rule-once and not about a chosen identifier. Its control renders two templates this repository does not ship — one whose id lookup matches verbatim, one whose read spells the normalization inline — and requires each reported.

`.harness/sync/github.sh` carries the identical edit, since the installed copy is held to differing from its template in nothing but the constant assignments at the top. This is the shape recorded under "A planned story is published onto the item it was planned from" above: **an edit to a shipped script is an edit to two files.**

## Run directory anatomy

.harness/runs/story-001/
Expand Down
1 change: 1 addition & 0 deletions .harness/history/execution-log.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@
{"story_id": "story-123", "status": "completed", "retry_count": 0, "timestamp": "2026-09-08 21:27:35"}
{"story_id": "story-126", "status": "completed", "retry_count": 0, "timestamp": "2026-09-09 01:42:55"}
{"story_id": "story-128", "status": "completed", "retry_count": 0, "timestamp": "2026-09-09 08:32:13"}
{"story_id": "story-129", "status": "completed", "retry_count": 0, "timestamp": "2026-09-09 09:48:56"}
1 change: 1 addition & 0 deletions .harness/history/inspection-log.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
{"story_id": "story-123", "mode": "narrow", "cost_usd": 1.7901849999999995, "scope_files": 60, "invocations": 1, "timestamp": "2026-09-08 21:32:25", "findings": 3, "filed": 3, "dropped": 0}
{"story_id": "story-126", "mode": "narrow", "cost_usd": 1.9372045000000002, "scope_files": 60, "invocations": 1, "timestamp": "2026-09-09 01:47:09", "findings": 3, "filed": 3, "dropped": 0}
{"story_id": "story-128", "mode": "narrow", "cost_usd": 1.633331, "scope_files": 60, "invocations": 1, "timestamp": "2026-09-09 08:37:45", "findings": 2, "filed": 2, "dropped": 0}
{"story_id": "story-129", "mode": "narrow", "cost_usd": 1.3884794999999999, "scope_files": 60, "invocations": 1, "timestamp": "2026-09-09 09:52:45", "findings": 1, "filed": 1, "dropped": 0}
1 change: 1 addition & 0 deletions .harness/history/mandate-log.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@
{"story_id": "story-123", "timestamp": "2026-09-08 20:30:12", "conferred_by": "jerod.wilkerson <[email protected]>", "source_kind": "human", "recorded_by": "l5-plan", "discarded_session_block": false}
{"story_id": "story-126", "timestamp": "2026-09-09 00:38:53", "conferred_by": "jerod.wilkerson <[email protected]>", "source_kind": "human", "recorded_by": "l5-plan", "discarded_session_block": false}
{"story_id": "story-128", "timestamp": "2026-09-09 08:02:40", "conferred_by": "jerod.wilkerson <[email protected]>", "source_kind": "human", "recorded_by": "l5-plan", "discarded_session_block": false}
{"story_id": "story-129", "timestamp": "2026-09-09 09:14:32", "conferred_by": "jerod.wilkerson <[email protected]>", "source_kind": "human", "recorded_by": "l5-plan", "discarded_session_block": false}
1 change: 1 addition & 0 deletions .harness/history/retry-log.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@
{"story_id": "story-118", "timestamp": "2026-09-08 19:14:16", "stage": "tester", "retry_stage": "tester"}
{"story_id": "story-126", "timestamp": "2026-09-09 01:13:04", "stage": "tester", "retry_stage": "tester"}
{"story_id": "story-126", "timestamp": "2026-09-09 01:20:52", "stage": "tester", "retry_stage": "tester"}
{"story_id": "story-129", "timestamp": "2026-09-09 09:28:14", "stage": "tester", "retry_stage": "tester"}
118 changes: 118 additions & 0 deletions .harness/stories/story-129.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
story:
id: story-129
title: The Status field's name is matched the same way when it is read and when it is written
description: |
Planned from the brief filed under key 117.

The sync script asks "is this the field this target configured?" in three
places, and they do not agree. Reading what the board currently says
normalizes both sides — spaces removed, case ignored — so a target that
configures STATUS_FIELD="status" against a board whose field is titled
"Status" reads the value successfully. Resolving the id to write with does
not normalize at all: it matches the configured name verbatim against the
names the board reports. So the read succeeds, finds the field empty, and
the write then resolves no id.

For Status that ends the filing: the guard exits 75, the entry stays
pending, and it is retried for ever without ever landing. The same two
lookups serve the five classification fields, where the failure is quieter
— the field is skipped with a line on stderr, the filing exits 0, and the
board is simply missing the value. Any difference in case or internal
spacing that the read tolerates and the write does not produces one of
those two outcomes.

This story gives field-name matching a single definition and has all three
lookups refer to it, so the three sites cannot drift apart again. The
option-name half of the option lookup is untouched: options continue to
match the board verbatim.

The suite cannot see the defect today because the one configuration it
drives cannot exhibit it. THIS_TARGETS_STATUS_FIELD names the configured
value, and seeded_board() names the board's field from that same constant,
so the two are the same string by construction; the existing test for a
field name carrying a space renames both halves together for the same
reason. The new test deliberately makes them differ.

workflow: story-workflow

tasks:
- Define, once, in templates/sync/github.sh, what makes two field names the same — the configured name and the board's name compared with their spaces removed and their case ignored — and have the read of the item's current value and both id lookups refer to that one definition rather than each carrying its own rule.
- Leave the option-name comparison in the option lookup matching the board verbatim; only the field-name half of that lookup adopts the shared rule.
- Apply the identical edit to .harness/sync/github.sh, which is this repository's installed copy of that template and is held to differing from it in nothing but the constant assignments at the top.
- Add coverage to tests/test_filed_query.py driving a configured field name that differs from the board's in case, for the Status field and for one classification field, over both sync copies.
- Record the change in .harness/docs/ARCHITECTURE.md.

acceptance_criteria:
- templates/sync/github.sh carries one definition of what makes two field names the same, and the lookup that reads the item's current value, the lookup that resolves a field's id, and the field-name half of the lookup that resolves an option's id each refer to that definition rather than spelling a rule of their own.
- The option-name comparison in the option lookup still matches the configured option value against the board's option names verbatim, unchanged by this story.
- .harness/sync/github.sh carries the same edit, and the standing assertion that the installed copy differs from its template only in constant assignments still holds.
- A filing driven with the Status field name configured in a case the board does not use resolves the field, sets the item's Status to the configured option, and exits 0 — where before this story it exited 75 saying the project has no field of that name.
- A filing driven with a classification field name configured in a case the board does not use writes that field's value onto the board — where before this story the field was skipped and the value was written nowhere.
- Both of those cases are driven against the template and against the installed copy, in the shape the module's existing board tests use for that pairing.
- The new test fails against the code as it stands before this story and passes after it, so it holds the rule rather than describing it.
- The full suite passes.

technical_plan:
implementation_steps:
- In templates/sync/github.sh, introduce a single named definition of the field-name comparison beside the three lookups, expressing the rule the read already applies: both the board's name and the configured name have their spaces removed and their case ignored before they are compared.
- Rewrite the lookup that reads the item's current field value to use that definition in place of the normalization it spells inline today, so the rule has one home rather than one of its three.
- Rewrite the lookup that resolves a field's id to compare under that definition instead of matching the configured name verbatim. This lookup serves both the Status write and every classification-field write, so one edit reaches all six configured names.
- Rewrite the field-name half of the lookup that resolves an option's id the same way, and leave its option-name half comparing verbatim.
- Say in the script's own comments what the rule is and that all three lookups share it, so the next reader does not have to infer the sharing from the code.
- Copy the edit into .harness/sync/github.sh unchanged, leaving its constant assignments as they are — the two files are held byte-identical outside those constants, so an edit to one is an edit to both.
- In tests/test_filed_query.py, add a case that seeds the board with the field names it already uses, drives a filing with the Status field name configured in a different case, and asserts the item lands in the configured column and the script exits 0.
- Add the companion case for one classification field, driven the same way, asserting the value reaches the board rather than being skipped.
- Drive both cases over the template and the installed copy, following the existing parametrization over the two sync copies, and drive the configured-name override through the mechanism the module already uses to override a constant for one filing.
- Record in .harness/docs/ARCHITECTURE.md what the two halves disagreed about, what one rule replaced them, and why the existing tests could not have caught it.
likely_file_changes:
- file: templates/sync/github.sh
stage: implementer
reason: Holds the three lookups that disagree; the shared definition and all three call sites are added here.
- file: .harness/sync/github.sh
stage: implementer
reason: This repository's installed copy of that template, held to differing from it only in constant assignments, so the same edit must land here or the standing assertion reddens.
- file: tests/test_filed_query.py
stage: tester
reason: Where the sync script's board behaviour is driven; gains the cases for a configured name differing from the board's in case, for Status and for a classification field.
- file: .harness/docs/ARCHITECTURE.md
stage: documenter
reason: Records the change and why the existing coverage could not exhibit the defect.

scope:
modify:
- templates/sync/github.sh
- .harness/sync/github.sh
- tests/test_filed_query.py
- .harness/docs/ARCHITECTURE.md
do_not_modify:
- templates/query/github.sh
- .harness/query/github.sh
- .harness/config.yaml
- orchestration/
- schemas/
- prompts/
- workflows/

verification_requirements:
- Confirm that templates/sync/github.sh states the field-name rule once and that all three lookups refer to that one statement, rather than three sites that happen to agree today.
- Confirm the option-name comparison still matches verbatim against the board's option names, and that the story changed only the field-name half of that lookup.
- Confirm .harness/sync/github.sh carries the same edit and that the existing assertion comparing it to its template passes.
- Confirm the new test cases drive a configured field name that differs from the board's in case for both the Status field and a classification field, and that they are driven over both sync copies.
- Confirm the new cases fail against the pre-story scripts — the Status case by the filing exiting 75, the classification case by the value not reaching the board — so the coverage holds the rule rather than restating it.
- Confirm the full suite passes.

constraints:
- A configured name and a board name that already match exactly must keep resolving exactly as they do today; this story widens what matches and narrows nothing.
- The option-name rule is untouched. An option value that does not match the board's option names verbatim must still resolve to nothing, and must still cost the Status filing an exit 75 and cost a classification field that field alone.
- The two failure classifications are unchanged: a Status field that resolves to no id remains transient, and a classification field that resolves to no id remains a line on stderr with the filing exiting 0.
- The board is still written only where the item reports the field empty; nothing in this story writes over a value a person set.
- The key marker, the path marker prefix and the payload marker prefix are untouched, as is the pairing between the sync script and templates/query/github.sh.
- The project, its field list and the item's field values are still each read at most once per filing.
- .harness/sync/github.sh keeps its own constant values; only the lines it shares with its template change.

mandate:
source:
kind: human
conferred_at: 2026-09-09 09:14:32
conferred_by: jerod.wilkerson <[email protected]>
recorded_by: l5-plan
Loading
Loading