From f08813956c7d866a5bd42e642950dd206b77eb6c Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:34:20 -0400 Subject: [PATCH 1/7] store: a delivery gate anchor names when a job was first found done --- internal/store/gate.go | 69 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/internal/store/gate.go b/internal/store/gate.go index 1ef12e2310..323c129e2e 100644 --- a/internal/store/gate.go +++ b/internal/store/gate.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "strings" + "time" ) // DeliveryGate is the final judge's evidence about one job. Pass is the first @@ -441,6 +442,21 @@ func (g DeliveryGate) Whole() bool { return g.Pass || g.PolishClosed || g.Overturned } +// Done is whether this gate's own row reports the requested work as done: the +// judgement passed, or its only shortfall was behaviours the request states +// that no check exercises or asserts — the work is there, the checks for it are +// not. +// +// It is the positive fact a delivery gate had no way to state before, and two +// readers turn on it rather than on their own reading of the fields. The +// envelope's `core_done_seconds` is the moment it first became true for a run, +// and the share-of-spend rail measures growth from the same moment — one +// definition, because a rail and a receipt that disagreed about when the work +// was done would be two answers to one question. +func (g DeliveryGate) Done() bool { + return g.Pass || len(g.Unexercised) > 0 || len(g.Unasserted) > 0 +} + // Cited is the gate's citations however they were written down. A row recorded // before the list existed carries only the joined line, and reading it as one // citation is the honest reading of it: that is exactly what it was when it was @@ -657,6 +673,59 @@ func (s *Store) DeliveryGateLineage(baseID string) ([]DeliveryGate, error) { return gates, nil } +// DeliveryGateAnchor returns where in the journal — and when — a job's +// requested work was FIRST found done: the oldest gate recorded for the node +// and everything spliced beneath its id whose verdict passed or left only a +// coverage finding. ok is false while no such gate has been recorded, which is +// a job whose core work has not been found done. +// +// The SEQUENCE is the point and not the payload. A spend rail measured from the +// moment the work was done reads the journal by sequence (SpendSinceSeq), and a +// caller given the payload alone would have to walk the whole event log to get +// back to the row it came from. +func (s *Store) DeliveryGateAnchor(baseID string) (seq int64, at time.Time, ok bool, err error) { + baseID = strings.TrimSpace(baseID) + if baseID == "" { + return 0, time.Time{}, false, nil + } + namespace := baseID + SplitNamespace + ceiling, ordered := idPrefixCeiling(namespace) + if !ordered { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w: prefix has no ordered ceiling", baseID, ErrInvalid) + } + rows, err := s.db.Query(` + SELECT seq, ts, payload FROM events + WHERE kind = ? AND (node_id = ? OR (node_id >= ? AND node_id < ?)) + ORDER BY seq`, EventDeliveryGate, baseID, namespace, ceiling) + if err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + defer rows.Close() + for rows.Next() { + var rowSeq int64 + var stamp, payload string + if err := rows.Scan(&rowSeq, &stamp, &payload); err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + var gate DeliveryGate + if err := json.Unmarshal([]byte(payload), &gate); err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + if !gate.Done() { + continue + } + when, err := parseTime(stamp) + if err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + return rowSeq, when, true, nil + } + if err := rows.Err(); err != nil { + return 0, time.Time{}, false, fmt.Errorf("read delivery gate anchor %q: %w", baseID, err) + } + return 0, time.Time{}, false, nil +} + // EventAcceptance is the acceptance checklist journaled against the piece of // work it will be used to judge: the behaviours the person's own request states, // read from the request before any work began. From ab32d99af6daddfd0a2d36c293439dcc92e02858 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:35:02 -0400 Subject: [PATCH 2/7] revision: a second unexercised-only finding buys no coverage round --- internal/revision/judge.go | 74 +++++++++++++++++ internal/revision/unexercisedbound_test.go | 97 ++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 internal/revision/unexercisedbound_test.go diff --git a/internal/revision/judge.go b/internal/revision/judge.go index 2eaa6343d9..7064504e96 100644 --- a/internal/revision/judge.go +++ b/internal/revision/judge.go @@ -2127,6 +2127,12 @@ func GapHandover(gaps string, revised bool, refused string) string { return handover } +// maxUnexercisedRounds bounds how many rounds one unexercised-only finding may +// buy. One is enough: the round exists to add checks for the behaviours the +// request states, and a second round aimed at the same finding measures nothing +// the first did not. +const maxUnexercisedRounds = 1 + // ExtendForGap is the authority the delivery gate never had. // // The judgement at the job root was already the right one and its maximum power @@ -2231,6 +2237,22 @@ func ExtendForGap(ctx context.Context, graph *store.Store, node store.Node, part if settled, met := metExtension(ctx, extension, unmet); met { return settled } + // ONE COVERAGE ROUND, AND NO MORE. An unexercised-only finding names + // behaviours the request states that no check exercises or asserts, and the + // round it buys exists to write those checks. That it STANDS until a + // measurement closes it is right — a behaviour an earlier round proved + // nothing exercises does not become exercised because this round took no + // reading — but it is also what let the same finding raise the same round at + // every later gate with nothing to bound it: a run whose named fix was + // committed minutes in spent the rest of its wall and most of its bill + // writing more tests, because a coverage round could be bought again and + // again. One is enough. See maxUnexercisedRounds. + if unexercisedOnly(unmet) && coverageRoundAlreadyBought(graph, base) { + extension.Refused = "the coverage round was already bought once, and the behaviours " + + "with no check that exercises them are left to the person" + extension.Unclosed = true + return extension + } if refusal := outOfWall(ctx, node); refusal != "" { extension.Refused, extension.Unclosed = refusal, true return extension @@ -2280,6 +2302,58 @@ func ExtendForGap(ctx context.Context, graph *store.Store, node store.Node, part return extension } +// unexercisedOnly says this finding is the coverage measurement and nothing +// else: behaviours the request states that no check exercises or asserts, with +// no other gap a round could be aimed at. +// +// A MODEL JUDGE'S OWN PROSE IS NOT ONE OF THESE. A judgement that still carries +// one is unsourced — the citation it names has not been weighed — so it is not +// unexercised-only however many behaviours ride on it; the round it buys is for +// the prose, and the coverage finding travels along. measuredHalf is what turns +// such a judgement into an unexercised-only one, and it is the same door every +// other measured finding goes through. +func unexercisedOnly(unmet Judgment) bool { + if len(unmet.Unexercised) == 0 && len(unmet.Unasserted) == 0 { + return false + } + if !unmet.Sourced { + return false + } + return !unmet.Mechanical && len(unmet.Constraint) == 0 && len(unmet.Consumers) == 0 && + len(unmet.Unbound) == 0 && len(unmet.OwnFailing) == 0 +} + +// coverageRoundAlreadyBought answers whether one of this job's earlier gates +// already grew work for an unexercised-only finding. The gate row carries the +// finding in Unexercised and Unasserted, and Extended says a round actually +// landed for it — the round counter is derived from these rows, so the ledger +// that bounds the next round is the one every other reader already replays. +// A read that fails answers no, which is the fail-safe direction: it buys the +// round rather than refusing one on evidence it could not see. +func coverageRoundAlreadyBought(graph *store.Store, base string) bool { + if graph == nil { + return false + } + gates, err := graph.DeliveryGateLineage(base) + if err != nil { + log.Printf("note: could not read the coverage ledger for %s: %v", base, err) + return false + } + bought := 0 + for _, gate := range gates { + if !gate.Extended || gate.Mechanical { + continue + } + if len(gate.Consumers) > 0 || len(gate.Unbound) > 0 || len(gate.OwnFailing) > 0 || len(gate.Constraint) > 0 { + continue + } + if len(gate.Unexercised) > 0 || len(gate.Unasserted) > 0 { + bought++ + } + } + return bought >= maxUnexercisedRounds +} + // coverageRefused reads the growth journal for the refusal that has just // happened and answers whether it was the coverage question, so the delivery // can say which governor declined in that governor's own terms. diff --git a/internal/revision/unexercisedbound_test.go b/internal/revision/unexercisedbound_test.go new file mode 100644 index 0000000000..42aad6b3ec --- /dev/null +++ b/internal/revision/unexercisedbound_test.go @@ -0,0 +1,97 @@ +package revision + +// One coverage round, and no more. + +import ( + "context" + "path/filepath" + "strings" + "testing" + + "github.com/Agent-Field/codeaf/internal/store" +) + +// coverageRoundJob splices one job into a private store so a gate can be read +// back off its ledger. +func coverageRoundJob(t *testing.T, session string) *store.Store { + t.Helper() + graph, err := store.Open(filepath.Join(t.TempDir(), "coverage-round.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { graph.Close() }) + if err := graph.Splice(store.RootID, store.Subtree{Nodes: []store.NodeSpec{{ + ID: "task-1", Brief: "add the flag", + }}}, store.Provenance{Origin: store.OriginUser, Intent: "add the flag", SessionID: session}); err != nil { + t.Fatal(err) + } + return graph +} + +// anUnexercisedFinding is the finding the acceptance gate raises on its own: +// behaviours the request states that no check exercises, sourced because nobody +// has to ask for them to be checked. +func anUnexercisedFinding() Judgment { + return Judgment{Pass: false, Checked: true, Sourced: true, + Gaps: "1 the request states has no check that exercises it.", + Unexercised: []string{"the flag turns on"}} +} + +// A SECOND UNEXERCISED-ONLY FINDING BUYS NO ROUND. The finding stands until a +// measurement closes it, so without this bound it raises the same round at every +// later gate — the run whose named fix was committed minutes in and spent the +// rest of its wall writing more tests. One round is bought for it and no more, +// and the finding is recorded unclosed so the delivery says why. +func TestASecondUnexercisedFindingBuysNoRound(t *testing.T) { + graph := coverageRoundJob(t, "s1") + // The earlier round: the request's behaviours had no check, a round was + // bought for it, and the row says so. + if err := graph.RecordDeliveryGate("task-1", store.DeliveryGate{ + Gap: "2 the request states have no check that exercises them.", + Unexercised: []string{"the flag turns on", "the flag turns off"}, + Round: 1, Extended: true, + }); err != nil { + t.Fatal(err) + } + + node := store.Node{ID: "task-1", Provenance: store.Provenance{Intent: "add the flag"}} + planned := 0 + extension := ExtendForGap(context.Background(), graph, node, "the flag is added", + anUnexercisedFinding(), nil, 10, + func(context.Context, string, string) (store.Subtree, error) { + planned++ + return store.Subtree{}, nil + }) + + if planned != 0 { + t.Fatalf("a second coverage round was planned: %d calls", planned) + } + if extension.Spliced != 0 || !extension.Unclosed { + t.Fatalf("the finding did not stay open: %+v", extension) + } + if !strings.Contains(extension.Refused, "coverage round was already bought once") { + t.Fatalf("the refusal does not say what it refused: %q", extension.Refused) + } +} + +// THE BOUND IS THE SECOND ROUND. A job that has never bought a coverage round +// buys its first, which is the whole point of the round existing; the bound may +// not refuse work the finding has not already paid for. +func TestAFirstUnexercisedFindingStillBuysItsRound(t *testing.T) { + graph := coverageRoundJob(t, "s2") + node := store.Node{ID: "task-1", Provenance: store.Provenance{Intent: "add the flag"}} + planned := 0 + extension := ExtendForGap(context.Background(), graph, node, "the flag is added", + anUnexercisedFinding(), nil, 10, + func(context.Context, string, string) (store.Subtree, error) { + planned++ + return store.Subtree{}, nil + }) + + if planned != 1 { + t.Fatalf("the first coverage round was not even planned: %d calls", planned) + } + if strings.Contains(extension.Refused, "coverage round was already bought once") { + t.Fatalf("the bound refused a round the finding had not paid for: %q", extension.Refused) + } +} From 3dd68b873e54d1d114b5c6e879d75ced65152e6b Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:35:52 -0400 Subject: [PATCH 3/7] resident: growth is refused once its spend outweighs the requested work --- internal/resident/grow.go | 72 +++++++++++++++++++++++++++++ internal/resident/growshare_test.go | 67 +++++++++++++++++++++++++++ internal/resident/overrun.go | 2 + 3 files changed, 141 insertions(+) create mode 100644 internal/resident/growshare_test.go diff --git a/internal/resident/grow.go b/internal/resident/grow.go index 16274281b8..a3e7f4c2bb 100644 --- a/internal/resident/grow.go +++ b/internal/resident/grow.go @@ -113,6 +113,14 @@ const ( // is not a cap and not a count: it is this job's OWN measured pace read // against the clock it is actually running under. CauseOutOfWall = "out-of-wall" + + // CauseSpendShare is the round refused because growth has already spent more + // than the requested work itself cost, measured from the moment a gate first + // found that work done. It is a bound against the bill rather than against a + // count of rounds, and it exists because a count could not stop a run whose + // named fix landed at minute eleven from spending the rest of its wall and + // most of its bill on growth. + CauseSpendShare = "spend-share" ) // The refusals in the words a person reads. They are constants because two of @@ -142,8 +150,19 @@ const ( // it was going to end with: a judgement on what it did. So the job stops // growing while there is still time to finish and be judged. RefusedOutOfWall = "there is not enough time left on this run to finish another round of work, so this is handed over while there is still time to check it" + + // The refusal that bounds growth against the bill. Once the requested work + // is done, a round that would take the total past twice what that work cost + // is refused and the job is handed over: the work the person asked for is + // finished, and what is left is growth they did not ask for. + RefusedSpendShare = "the requested work is done and growth has already doubled what it cost — handing over what's done" ) +// growthSpendShare bounds how much a job may spend on growth after its +// requested work is first found done: one means growth may at most double the +// bill that got the work done. +const growthSpendShare = 1.0 + // GrowthStopped reports whether a refusal cause is one of the two the governor // reaches by READING THE WORLD rather than by counting — the round before this // one changed nothing and neither did the work before that, or the remaining @@ -652,6 +671,20 @@ func growJob(ctx context.Context, graph *store.Store, ask Satisfier, req GrowReq } } + // 3b. Share of spend. Once the requested work is first found done, growth is + // bounded against the bill: a round is refused when more than + // [growthSpendShare] times the spend that got the work done has gone on + // growth since. The moment is the delivery gate's own first "done" — a + // pass or a coverage finding — and not this round's, so the rail holds + // across every later round. A job whose gate has never found its core + // work done, and a job with no session to bill, have no anchor and the + // rail does not apply: it refuses nothing it cannot measure. + if reach, err := growthShareReached(graph, req); err != nil { + return GrowVerdict{Round: round}, err + } else if reach { + return refuse(CauseSpendShare, RefusedSpendShare) + } + // 4. The only question that can say "there is nothing left to do". // // IT DOES NOT GET TO REFUSE THE FIRST ROUND AFTER A REVIEW FOUND @@ -715,6 +748,45 @@ func growJob(ctx context.Context, graph *store.Store, ask Satisfier, req GrowReq return GrowVerdict{Allow: true, Round: round, CoveredDespite: covered, Spent: req.spent}, nil } +// growthShareReached answers whether a job has already spent more on growth +// than [growthSpendShare] times what the requested work cost, measured from the +// moment a gate first found that work done. The read is the one the daily rail +// uses — SpendSinceSeq over the errand's session — so the two rails bill the +// same rows and a run cannot read cheap here and dear there. A job with no gate +// that found its work done, or with no session to bill, has no anchor and the +// rail does not apply; a read that fails is returned rather than guessed at. +func growthShareReached(graph *store.Store, req GrowRequest) (bool, error) { + if graph == nil { + return false, nil + } + jobRoot := strings.TrimSpace(req.JobRoot) + if jobRoot == "" { + jobRoot = req.Node.ID + } + session := strings.TrimSpace(req.Node.Provenance.SessionID) + if session == "" { + return false, nil + } + anchor, _, ok, err := graph.DeliveryGateAnchor(jobRoot) + if err != nil || !ok { + return false, err + } + // The share is read against the spend UP TO the anchor: the total billed so + // far minus what has been billed since the gate. One query per window, both + // off the rail's own read, so the two halves cannot come from two different + // readings of the same journal. + total, err := graph.SpendSinceSeq(session, 0) + if err != nil { + return false, err + } + after, err := graph.SpendSinceSeq(session, anchor) + if err != nil { + return false, err + } + before := total.Cost() - after.Cost() + return after.Cost() > growthSpendShare*before, nil +} + // admitGrowth journals a round that actually landed. It is called after the // splice rather than at the verdict, because the round counter is derived from // these events and an admission that never spliced is a round nobody spent — diff --git a/internal/resident/growshare_test.go b/internal/resident/growshare_test.go new file mode 100644 index 0000000000..ec0764724b --- /dev/null +++ b/internal/resident/growshare_test.go @@ -0,0 +1,67 @@ +package resident + +import ( + "context" + "testing" + + "github.com/Agent-Field/codeaf/internal/store" +) + +// shareRequest is the ordinary overrun round every cap is tested against: one +// node's worth of work, measured, that the free checks would otherwise admit. +func shareRequest(t *testing.T, graph *store.Store) GrowRequest { + t.Helper() + node := jobNode(t, graph, "job") + node.Provenance.SessionID = "s1" + return GrowRequest{JobRoot: "job", Node: node, Reason: GrowOverrun, Adding: 1, Measured: true, Produced: 1} +} + +// GROWTH IS REFUSED ONCE IT HAS SPENT MORE THAN THE REQUESTED WORK COST. The +// rail is measured from the moment a gate first found the work done, and it is +// a share of the bill rather than a count of rounds — the bound a count could +// not give a run whose named fix landed early and whose growth came after. +func TestGrowthIsRefusedWhenSpendAfterTheGateDoublesTheBill(t *testing.T) { + graph := crowdedJob(t, "s1", 3) + // The spend that got the requested work done, and the gate that found it + // done. + if err := graph.RecordUsage(store.NodeUsage{NodeID: "job", Cost: 1.0}); err != nil { + t.Fatal(err) + } + if err := graph.RecordDeliveryGate("job", store.DeliveryGate{Pass: true}); err != nil { + t.Fatal(err) + } + // And the growth since, which has already spent twice the work cost. + if err := graph.RecordUsage(store.NodeUsage{NodeID: "job", Cost: 2.0}); err != nil { + t.Fatal(err) + } + + verdict, err := growJob(context.Background(), graph, nil, shareRequest(t, graph)) + if err != nil { + t.Fatal(err) + } + if verdict.Allow || verdict.Cause != CauseSpendShare { + t.Fatalf("growth past the share was admitted: %+v", verdict) + } + if verdict.Refused != RefusedSpendShare { + t.Fatalf("the refusal is not the share rail's own words: %q", verdict.Refused) + } +} + +// THE RAIL ONLY APPLIES WHERE IT CAN BE MEASURED. A job whose gate has never +// found its core work done has no anchor, so growth is bounded by the caps and +// the rail refuses nothing — the fail-safe direction, because a rail that +// guessed at an anchor would stop work it cannot account for. +func TestAJobWithNoGateIsNotRefusedByTheShareRail(t *testing.T) { + graph := crowdedJob(t, "s2", 3) + if err := graph.RecordUsage(store.NodeUsage{NodeID: "job", Cost: 5.0}); err != nil { + t.Fatal(err) + } + + verdict, err := growJob(context.Background(), graph, nil, shareRequest(t, graph)) + if err != nil { + t.Fatal(err) + } + if !verdict.Allow || verdict.Cause == CauseSpendShare { + t.Fatalf("a job with no gate was refused by the share rail: %+v", verdict) + } +} diff --git a/internal/resident/overrun.go b/internal/resident/overrun.go index fd4fbb6475..ab5763f73d 100644 --- a/internal/resident/overrun.go +++ b/internal/resident/overrun.go @@ -543,6 +543,8 @@ func governorPhrase(cause string) string { return "no more room" case CauseCovered: return "already covered" + case CauseSpendShare: + return "the bill has doubled" } return "handing over" } From 996f6fbf645704adad547ca4f518d1b984f70f81 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:36:44 -0400 Subject: [PATCH 4/7] do: the envelope says when the requested work was first found done --- cmd/codeaf/coredone_test.go | 51 ++++++++++++ cmd/codeaf/do.go | 80 +++++++++++++++---- cmd/codeaf/envelope.go | 50 +++++++----- .../manual/chat/running-from-the-terminal.md | 1 + 4 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 cmd/codeaf/coredone_test.go diff --git a/cmd/codeaf/coredone_test.go b/cmd/codeaf/coredone_test.go new file mode 100644 index 0000000000..32e3a251f4 --- /dev/null +++ b/cmd/codeaf/coredone_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "path/filepath" + "testing" + "time" + + "github.com/Agent-Field/codeaf/internal/store" +) + +// THE ENVELOPE SAYS WHEN THE REQUESTED WORK WAS FIRST FOUND DONE, in seconds +// from the run's start, off the journal's own gate event — and leaves the key +// out when no gate ever said so, because a fabricated instant is worse than an +// absent one. +func TestTheEnvelopeSaysWhenTheRequestedWorkWasDone(t *testing.T) { + graph, err := store.Open(filepath.Join(t.TempDir(), "coredone.db")) + if err != nil { + t.Fatal(err) + } + defer graph.Close() + + const gated, ungated = "session-gated", "session-ungated" + spliceForErrand(t, graph, gated, []store.NodeSpec{{ID: "task-1", Brief: "add the flag"}}) + spliceForErrand(t, graph, ungated, []store.NodeSpec{{ID: "task-2", Brief: "add the other flag"}}) + if err := graph.RecordDeliveryGate("task-1", store.DeliveryGate{Pass: true}); err != nil { + t.Fatal(err) + } + _, at, ok, err := graph.DeliveryGateAnchor("task-1") + if err != nil || !ok { + t.Fatalf("the passing gate has no anchor: ok=%t err=%v", ok, err) + } + started := at.Add(-7 * time.Second) + + gatedOutcome := headlessOutcome{started: started} + priceErrand(graph, gated, 0, &gatedOutcome) + fields := envelopeFields(t, errandEnvelope(gatedOutcome)) + got, present := fields["core_done_seconds"] + if !present { + t.Fatal("a gate found the work done and the envelope says nothing about when") + } + if seconds, ok := got.(float64); !ok || seconds < 6.9 || seconds > 7.1 { + t.Fatalf("core_done_seconds = %v, want about 7", got) + } + + // And absent when no gate ever said the work was done. + ungatedOutcome := headlessOutcome{started: started} + priceErrand(graph, ungated, 0, &ungatedOutcome) + if _, present := envelopeFields(t, errandEnvelope(ungatedOutcome))["core_done_seconds"]; present { + t.Fatal("a run whose gate never found the work done reported when it was") + } +} diff --git a/cmd/codeaf/do.go b/cmd/codeaf/do.go index 9ce388f972..9c377e9fa2 100644 --- a/cmd/codeaf/do.go +++ b/cmd/codeaf/do.go @@ -110,7 +110,14 @@ type headlessOutcome struct { Nodes int `json:"nodes"` Seconds float64 `json:"seconds"` - Settled bool `json:"settled"` + // started is when this invocation opened, and coreDoneSeconds is how long + // it took to finish the requested work: the first moment a delivery gate + // found that work done. Both are unexported — they reach a caller only + // through the envelope's `core_done_seconds`, which is the one spelling + // every reader shares. + started time.Time + coreDoneSeconds float64 + Settled bool `json:"settled"` // unfinishedTree is the finished-tree reading's own sentence on the one run // that cannot be called settled: its checks failed to collect. It stays // unexported because the sentence leaves through Deliverable, while Settled @@ -616,6 +623,7 @@ func errandRun(request doRequest, seats config.Seats, started time.Time) (outcom return headlessOutcome{}, err } outcome.Seconds = time.Since(started).Seconds() + outcome.started = started // The wall is the case that made this necessary. A leaf cancelled by the // timeout journals its usage row on the way down, which is after the // watcher has returned and — until this line moved the shutdown ahead of @@ -656,6 +664,47 @@ func priceErrand(graph *store.Store, session string, openedAt int64, outcome *he // work is read from what was written down, for the reason the bill is: a // counter in this process could not see a round a resident spliced. outcome.rounds = errandRounds(graph, session) + // AND WHEN THE REQUESTED WORK WAS FIRST FOUND DONE, off the same journal. + outcome.coreDoneSeconds = errandCoreDoneSeconds(graph, session, outcome.started) +} + +// errandCoreDoneSeconds is how long it took this errand to finish the requested +// work, measured from the run's start to the first delivery gate on one of its +// jobs that found the work done — a pass, or a coverage finding. It is the +// EARLIEST such moment across the errand's jobs, so a run that finished one part +// early and another late reports the first; a run whose gate never said so +// reports zero, which the envelope carries as an absent key rather than a +// fabricated instant. +func errandCoreDoneSeconds(graph *store.Store, session string, started time.Time) float64 { + if graph == nil || started.IsZero() { + return 0 + } + nodes, err := graph.SessionMemberNodes(session) + if err != nil { + return 0 + } + var earliest int64 + var at time.Time + for _, node := range nodes { + if node.Parent != store.RootID { + continue + } + seq, when, ok, err := graph.DeliveryGateAnchor(node.ID) + if err != nil || !ok { + continue + } + if earliest == 0 || seq < earliest { + earliest, at = seq, when + } + } + if earliest == 0 { + return 0 + } + seconds := at.Sub(started).Seconds() + if seconds < 0 { + return 0 + } + return seconds } // errandRounds is how many times this errand bought MORE WORK: every growth @@ -2970,20 +3019,21 @@ func reportErrand(request doRequest, outcome headlessOutcome) error { // `exec` and `run` from publishing three different objects again. func errandEnvelope(outcome headlessOutcome) resultEnvelope { return buildResultEnvelope(runResult{ - Stop: outcome.resolvedStop(), - Answer: outcome.Deliverable, - Files: outcome.Artifacts, - Error: outcome.Error, - SpendUSD: outcome.Spend, - TokensIn: outcome.tokensIn, - TokensOut: outcome.tokensOut, - Seconds: outcome.Seconds, - Model: outcome.Model, - Steps: outcome.Nodes, - Run: outcome.run, - Calls: outcome.calls, - Rounds: outcome.rounds, - Extra: legacyErrandFields(outcome), + Stop: outcome.resolvedStop(), + Answer: outcome.Deliverable, + Files: outcome.Artifacts, + Error: outcome.Error, + SpendUSD: outcome.Spend, + TokensIn: outcome.tokensIn, + TokensOut: outcome.tokensOut, + Seconds: outcome.Seconds, + CoreDoneSeconds: outcome.coreDoneSeconds, + Model: outcome.Model, + Steps: outcome.Nodes, + Run: outcome.run, + Calls: outcome.calls, + Rounds: outcome.rounds, + Extra: legacyErrandFields(outcome), }) } diff --git a/cmd/codeaf/envelope.go b/cmd/codeaf/envelope.go index f191944547..147ac5be32 100644 --- a/cmd/codeaf/envelope.go +++ b/cmd/codeaf/envelope.go @@ -294,6 +294,13 @@ type resultEnvelope struct { Tokens envelopeTokens `json:"tokens"` // Seconds is how long it took, wall clock. Seconds float64 `json:"seconds"` + // CoreDoneSeconds is when the requested work was FIRST found done, in + // seconds from the run's start: the first delivery gate that passed or left + // only a coverage finding — the fix committed, the checks for it still to + // come. It is omitted when no gate ever said so, and it is the figure a + // person reads to see how much of the run was the work and how much came + // after it. + CoreDoneSeconds float64 `json:"core_done_seconds,omitempty"` // Model is the model the work ran on, as the seat ladder resolved it. Model string `json:"model"` // Steps is how many pieces of work ran: `do`'s nodes, `exec`'s turns. A @@ -392,11 +399,15 @@ type runResult struct { TokensIn int TokensOut int Seconds float64 - Model string - Steps int - Run string - Calls int - Rounds int + // CoreDoneSeconds is `do`'s fact: when its gate first found the requested + // work done, read off the journal. `exec` and `run` have no delivery gate + // and leave it zero, which omits the key. + CoreDoneSeconds float64 + Model string + Steps int + Run string + Calls int + Rounds int // Extra is this verb's own fields: its old spellings, and whatever it knows // that the contract has no room for. Nil for a verb with neither. Extra map[string]any @@ -421,20 +432,21 @@ func buildResultEnvelope(result runResult) resultEnvelope { files = []string{} } return resultEnvelope{ - OK: exitFor(stop) == exitDone, - Stop: stop, - Answer: result.Answer, - Files: files, - Error: result.Error, - SpendUSD: result.SpendUSD, - Tokens: envelopeTokens{In: result.TokensIn, Out: result.TokensOut}, - Seconds: result.Seconds, - Model: result.Model, - Steps: result.Steps, - Run: result.Run, - Calls: result.Calls, - Rounds: result.Rounds, - extra: result.Extra, + OK: exitFor(stop) == exitDone, + Stop: stop, + Answer: result.Answer, + Files: files, + Error: result.Error, + SpendUSD: result.SpendUSD, + Tokens: envelopeTokens{In: result.TokensIn, Out: result.TokensOut}, + Seconds: result.Seconds, + CoreDoneSeconds: result.CoreDoneSeconds, + Model: result.Model, + Steps: result.Steps, + Run: result.Run, + Calls: result.Calls, + Rounds: result.Rounds, + extra: result.Extra, } } diff --git a/internal/manual/chat/running-from-the-terminal.md b/internal/manual/chat/running-from-the-terminal.md index cb6bc70c71..58f40ddaca 100644 --- a/internal/manual/chat/running-from-the-terminal.md +++ b/internal/manual/chat/running-from-the-terminal.md @@ -186,6 +186,7 @@ stdout, always parseable, printed even when the run failed**: | `spend_usd` | what it cost, whole, in dollars | | `tokens` | `{"in": …, "out": …}` | | `seconds` | wall clock | +| `core_done_seconds` | when the requested work was first found done, in seconds from the start; absent when the gate never said so | | `model` | the model the work ran on | | `steps` | how many pieces of work ran — `do`'s nodes, `exec`'s turns. A saved program does not measure it: the key is still there, holding `0`, and that `0` is a measurement nobody took rather than a count of none | | `run` | this invocation's id. It names the folder `--debug` writes into, and every row this run wrote into `~/.codeaf/logs/calls.jsonl` carries it too — so `codeaf logs --run ` is how you get from this object to the calls behind it. Empty on a verb that opened no run of its own | From 2c9fafa7c15304948f73f8d6b8afb101865cb787 Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:36:55 -0400 Subject: [PATCH 5/7] docs: the change note for the bounded coverage round and the core-done figure --- .../unreleased/1095-coverage-round-buys-once.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docs/changes/unreleased/1095-coverage-round-buys-once.md diff --git a/docs/changes/unreleased/1095-coverage-round-buys-once.md b/docs/changes/unreleased/1095-coverage-round-buys-once.md new file mode 100644 index 0000000000..e04870fc24 --- /dev/null +++ b/docs/changes/unreleased/1095-coverage-round-buys-once.md @@ -0,0 +1,16 @@ +--- +kind: changed +title: the coverage round buys once, and the envelope says when the requested work was done +pr: 1095 +surface: [resident, docs] +invalidates: + - "An unexercised-only finding bought a repair round at every later delivery gate, because the finding stands until a measurement closes it and nothing bounded how many rounds `no check exercises this behaviour` could raise. A job buys at most one such round now (`maxUnexercisedRounds`), and a second finding is recorded unclosed rather than funded." + - "Growth was bounded only by the round counter, the job-size ceiling, the wall and the daily dollar rail. A share-of-spend rail (`growthSpendShare`) refuses a round once growth has spent more than the requested work itself cost, counted from the gate that first found that work done, and journaled as `CauseSpendShare`." + - "`codeaf do --json` carried no figure for when the requested work was finished. `core_done_seconds` now appears, in seconds from the start, when a delivery gate first passed or found only a coverage gap, and is absent when no gate said so." +--- + +An unexercised-only finding is a measurement, so it stands until a measurement +closes it — right, but unbounded, it raised the same test-writing round at every +gate. A run whose named fix was committed minutes in spent the rest of its wall +and most of its bill writing more tests. The round is bounded to one, and growth +past the point the work was done is bounded against the bill that got it done. From 489423252be59332f18d5f25ad2c8e369c4e832c Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:48:25 -0400 Subject: [PATCH 6/7] store: a gate is done only when the checks are all that is missing Done read any gate carrying a coverage finding as the work being done, including one that also named a failing check of the work's own, an unbound consumer, a constraint or a mechanical failure. Anchoring the spend rail and the core-done figure there measured from before the work was finished. A test holds each shape. --- internal/store/gate.go | 17 ++++++++++++++++- internal/store/gate_done_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 internal/store/gate_done_test.go diff --git a/internal/store/gate.go b/internal/store/gate.go index 323c129e2e..ba1adbee15 100644 --- a/internal/store/gate.go +++ b/internal/store/gate.go @@ -454,7 +454,22 @@ func (g DeliveryGate) Whole() bool { // definition, because a rail and a receipt that disagreed about when the work // was done would be two answers to one question. func (g DeliveryGate) Done() bool { - return g.Pass || len(g.Unexercised) > 0 || len(g.Unasserted) > 0 + if g.Pass { + return true + } + // A COVERAGE FINDING BESIDE ANOTHER SHORTFALL IS NOT DONE. The work is + // there only when the checks for it are the whole of what is missing; a + // gate that also names a consumer left unbound, a check of the work's own + // that fails, a constraint unmet or a mechanical failure has found the work + // itself short, and anchoring "done" there would measure growth from before + // the work was finished. + if g.Mechanical || g.Unreadable || g.Unjudged { + return false + } + if len(g.Consumers) > 0 || len(g.Unbound) > 0 || len(g.OwnFailing) > 0 || len(g.Constraint) > 0 { + return false + } + return len(g.Unexercised) > 0 || len(g.Unasserted) > 0 } // Cited is the gate's citations however they were written down. A row recorded diff --git a/internal/store/gate_done_test.go b/internal/store/gate_done_test.go new file mode 100644 index 0000000000..ea1b2826a8 --- /dev/null +++ b/internal/store/gate_done_test.go @@ -0,0 +1,28 @@ +package store + +import "testing" + +// A GATE IS DONE ON A PASS, OR WHEN THE CHECKS ARE ALL THAT IS MISSING. A +// coverage finding that stands beside any other shortfall has found the work +// itself short, and the moment it anchors would come before the work was +// finished. +func TestAGateIsDoneOnlyWhenTheChecksAreAllThatIsMissing(t *testing.T) { + for _, c := range []struct { + name string + gate DeliveryGate + want bool + }{ + {"a pass", DeliveryGate{Pass: true}, true}, + {"an unexercised finding alone", DeliveryGate{Unexercised: []string{"the flag turns on"}}, true}, + {"an unasserted finding alone", DeliveryGate{Unasserted: []string{"the flag turns off"}}, true}, + {"nothing found at all", DeliveryGate{}, false}, + {"a coverage finding beside a failing check of its own", DeliveryGate{Unexercised: []string{"a"}, OwnFailing: []string{"TestX"}}, false}, + {"a coverage finding beside an unbound consumer", DeliveryGate{Unexercised: []string{"a"}, Unbound: []string{"caller"}}, false}, + {"a coverage finding beside a constraint", DeliveryGate{Unasserted: []string{"a"}, Constraint: []string{"no new dependency"}}, false}, + {"a coverage finding on a mechanical failure", DeliveryGate{Unexercised: []string{"a"}, Mechanical: true}, false}, + } { + if got := c.gate.Done(); got != c.want { + t.Errorf("%s: Done() = %t, want %t", c.name, got, c.want) + } + } +} From 0710b5cd4ae4efea6174c8d1d25bef862456a02d Mon Sep 17 00:00:00 2001 From: santoshkumarradha Date: Thu, 17 Sep 2026 06:48:33 -0400 Subject: [PATCH 7/7] docs: the change note carries this change's pull request number --- ...rage-round-buys-once.md => 1100-coverage-round-buys-once.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename docs/changes/unreleased/{1095-coverage-round-buys-once.md => 1100-coverage-round-buys-once.md} (99%) diff --git a/docs/changes/unreleased/1095-coverage-round-buys-once.md b/docs/changes/unreleased/1100-coverage-round-buys-once.md similarity index 99% rename from docs/changes/unreleased/1095-coverage-round-buys-once.md rename to docs/changes/unreleased/1100-coverage-round-buys-once.md index e04870fc24..09a3ed0453 100644 --- a/docs/changes/unreleased/1095-coverage-round-buys-once.md +++ b/docs/changes/unreleased/1100-coverage-round-buys-once.md @@ -1,7 +1,7 @@ --- kind: changed title: the coverage round buys once, and the envelope says when the requested work was done -pr: 1095 +pr: 1100 surface: [resident, docs] invalidates: - "An unexercised-only finding bought a repair round at every later delivery gate, because the finding stands until a measurement closes it and nothing bounded how many rounds `no check exercises this behaviour` could raise. A job buys at most one such round now (`maxUnexercisedRounds`), and a second finding is recorded unclosed rather than funded."