An ask reads at a glance on the phone: a headline, a summary and its points, the rest in a note (issues 383 and 639)

The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold,
no summary. A settings proposal was the same wall of text, its values masked so the approval was blind.

- messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each
  answer does, the expiry, where else it is answered and the asker's Details travel as the message's note
  (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram ·
  <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind
  that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone
  (from the earlier work on issue 383, #178).
- telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in
  a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message
  shown before is still compared by the digest it was kept under.
- desk-channel: the note follows the body.
- mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three
  points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is
  composed again as it was asked (form), so its warrant still holds.
- claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the
  module's), its text in the Details; a question's context is at most 300 characters and its recommendation
  one point. Older proposals and questions keep their form.

The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and
asks.Glance.
This commit is contained in:
jochen
2026-10-13 17:16:50 +02:00
parent 9c166398f2
commit eadf301f9f
46 changed files with 1276 additions and 162 deletions
@@ -309,9 +309,11 @@ type Got struct {
Source string `json:"source,omitempty"`
Reserved string `json:"reserved,omitempty"`
NeedsOperat bool `json:"needs-operator,omitempty"`
Cut string `json:"cut,omitempty"`
Bound int `json:"bound"`
Read string `json:"read,omitempty"`
// Brief is what the operator's ask shows of it (issue 639).
Brief *Brief `json:"brief,omitempty"`
Cut string `json:"cut,omitempty"`
Bound int `json:"bound"`
Read string `json:"read,omitempty"`
// Reconstructed, Amended and Touches are the record's fields of those names; Fields, any other it holds as
// written (hq issue 487).
@@ -349,7 +351,7 @@ func (k *KB) Get(a GetAsk) (*Got, error) {
bound := boundOf(a.Bound)
g := &Got{Record: key.String(), Title: r.Title, Status: r.Status, Topic: r.Topic, Date: r.Date, Deciders: r.Deciders,
Version: version, Versions: e.Versions, Insights: r.Insights, Reviews: r.Reviews, Drafting: r.Drafting, Source: r.Source,
NeedsOperat: r.NeedsOperator, Bound: bound, Since: k.since(ctx, key),
NeedsOperat: r.NeedsOperator, Brief: r.Brief, Bound: bound, Since: k.since(ctx, key),
Reconstructed: r.Reconstructed, Amended: r.Amended, Touches: r.Touches, Fields: r.Extra}
if r.Status == StatusReserved || r.Status == StatusWithdrawn {
g.Reserved = fmt.Sprintf("reserved by %s at %s for %q", r.ReservedBy, r.ReservedAt.Format("2006-01-02 15:04 UTC"), r.ReservedFor)
@@ -0,0 +1,111 @@
package main
import (
"fmt"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// novox/hq issue 639: the operator would not read the ask for ADR 0343 — thirty plain lines, every clause one per
// line. A record that needs the operator is asked with its brief: a headline that says what it changes for them, a
// summary, at most three points; who proposed it, the fingerprint and how to read the clauses are the Details.
// adr0343 is ADR 0343 as it was asked on 2026-10-13: thirteen clauses, and the brief its author would now give.
func adr0343() *Record {
r := &Record{Kind: KindDecision, Number: 343, Status: StatusProposed, Date: "2026-10-13", NeedsOperator: true,
Title: "A session's memory follows the operator: the agent module carries each project's memory folder to every node, and a file changed on two keeps both",
Author: "fix-367@g14",
Brief: &Brief{Headline: "Your agents' memory follows you to every machine",
Summary: "What an agent learns in a project's memory is copied to every machine that runs the agent, so a session " +
"anywhere starts with it. A file changed on two machines keeps both versions and warns you; secrets in memory " +
"files are screened, though not perfectly.",
Points: []string{"Only memory folders travel: links and files over 128 KiB stay where they are",
"A conflict reaches you as a warning that needs you",
"A deleted file is marked, and a newer change elsewhere outlives the deletion"}}}
leads := []string{"Only the memory folders are carried", "Some memory files are not carried, and each is listed with why",
"The memory folders become paths the module's own process writes", "Memory moves only between homes of the same kind",
"The module's process on each node notices a change and puts it in the module's state",
"Every other node writes the new version where its own file holds no change it did not carry, and keeps the version replaced",
"When a file changed on two nodes, both versions are kept, the newer first",
"A conflict is sent to the operator as a warning that needs them",
"A deletion is carried as a removal marker, and a change outlives it",
"On a node's first carrying, every version learned before is kept", "One read-only tool shows what is carried",
"A value put straight into the state is not yet told apart from one the module put there", "How it is checked"}
for i, l := range leads {
r.Clauses = append(r.Clauses, Clause{N: i + 1, Text: "**" + l + ".** The statement.", Status: ClauseProposed, Source: SourceWritten})
}
return r
}
func TestADecisionAskReadsAtAGlance(t *testing.T) {
r := adr0343()
at := time.Date(2026, 10, 13, 14, 5, 0, 0, time.UTC)
a := composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12, Form: AskGlance})
if err := a.Check(at); err != nil {
t.Fatal(err)
}
if a.Headline != "Your agents' memory follows you to every machine" {
t.Errorf("the headline is not what it changes for the operator: %q", a.Headline)
}
if err := asks.Glance(a.Explanation); err != nil {
t.Errorf("%v:\n%s", err, a.Explanation)
}
if n := strings.Count(a.Explanation, "\n• "); n != 3 || !strings.HasPrefix(a.Explanation, "What an agent learns") {
t.Errorf("the summary and its %d points:\n%s", n, a.Explanation)
}
for _, c := range r.Clauses {
if strings.Contains(a.Explanation+a.Details, c.Lead()) {
t.Errorf("the ask lists the clause %d: %q", c.N, c.Lead())
}
}
details := strings.Split(a.Details, "\n")
if len(details) != 2 || !strings.Contains(details[0], "ADR 0343, proposed by fix-367@g14; the knowledge base's own review approved it.") ||
!strings.Contains(details[0], "Fingerprint "+fingerprint(r.Digest())+".") ||
details[1] != "Read its 13 clauses with the knowledge base's get, number 343." {
t.Errorf("the details:\n%s", a.Details)
}
// The ask made before the brief is composed again as it was, so its warrant still holds after the change.
old := composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12})
if !strings.Contains(old.Explanation, "Its 13 clause(s):") || old.Headline != "Accept ADR 0343?" || old.Details != "" || old.Digest() == a.Digest() {
t.Errorf("an ask made before the brief is not composed as it was asked:\n%s", old.Explanation)
}
// Without a brief (a record proposed before the rule), the glance is the title, and no clause.
r.Brief = nil
a = composeAsk(r, &OperatorAsk{ID: "kb-decision-343-0d1e2f3a", At: at, Version: 12, Form: AskGlance})
if a.Headline != "Approve ADR 0343?" || asks.Glance(a.Explanation) != nil || strings.Contains(a.Explanation, "Only the memory folders") {
t.Errorf("without a brief: %q\n%s", a.Headline, a.Explanation)
}
}
// A brief is held to what the phone reads at a glance, each way it fails named.
func TestABriefIsHeldToAGlance(t *testing.T) {
good := adr0343().Brief
if got := briefProblems(good); len(got) != 0 {
t.Fatalf("the brief of ADR 0343 is refused: %v", got)
}
for name, c := range map[string]struct {
change func(b *Brief)
says string
}{
"no headline": {func(b *Brief) { b.Headline = "" }, "headline is empty"},
"long headline": {func(b *Brief) { b.Headline = strings.Repeat("memory ", 10) }, "longer than 60"},
"no summary": {func(b *Brief) { b.Summary = " " }, "summary is empty"},
"long summary": {func(b *Brief) { b.Summary = strings.Repeat("A short sentence. ", 20) }, "summary is"},
"four points": {func(b *Brief) { b.Points = append(b.Points, "one more") }, "4 points"},
"a path": {func(b *Brief) { b.Points[0] = "Carried from the agent's home under .claude/projects" }, "names a path"},
"two lines": {func(b *Brief) { b.Summary = "One.\nTwo." }, "more than one line"},
} {
b := *good
b.Points = append([]string{}, good.Points...)
c.change(&b)
if got := fmt.Sprint(briefProblems(&b)); !strings.Contains(got, c.says) {
t.Errorf("%s: %s", name, got)
}
}
if got := briefProblems(nil); len(got) != 1 || !strings.Contains(got[0], "carries a brief") {
t.Errorf("no brief: %v", got)
}
}
@@ -238,6 +238,8 @@ type ProposeAsk struct {
Links []LinkAsk
Homes []string
NeedsOperator bool
// Brief is what the operator's ask shows (novox/hq issue 639).
Brief *Brief
// Status is a research effort's: active, graduated or abandoned.
Status string
// Insight, for an accepted record, is what stood before: the proposal is a progressive insight.
@@ -280,7 +282,7 @@ func (k *KB) Propose(caller string, a ProposeAsk) (*Proposed, error) {
}
r := &Record{Kind: a.Kind, Number: a.Number, Title: strings.TrimSpace(a.Title), Topic: strings.TrimSpace(a.Topic),
Status: StatusProposed, Date: strings.TrimSpace(a.Date), Deciders: a.Deciders, Effort: a.Status,
NeedsOperator: a.NeedsOperator, Sections: a.Sections, Homes: a.Homes, Author: caller}
NeedsOperator: a.NeedsOperator, Brief: a.Brief, Sections: a.Sections, Homes: a.Homes, Author: caller}
if r.Date == "" {
r.Date = k.now().Format("2006-01-02")
}
@@ -914,8 +914,8 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
Description: "Propose a record whole under a reserved number: its title, topic, date and deciders; for a decision its " +
"clauses, each opening with a bold lead (\"**What is decided.** …\"), and its reasoning in sections (Context, " +
"Considered Options, Consequences, How it is checked, References); for a research effort its status and documents; its " +
"links (supersedes, changes \"0276 §4\", extends, became, cites), and whether it needs the operator. Refused, naming " +
"every reason at once and writing nothing, when it breaks one of the 29 rules that judge one record or its prose — a " +
"links (supersedes, changes \"0276 §4\", extends, became, cites), and whether it needs the operator, with the brief its ask shows. Refused, naming " +
"every reason at once and writing nothing, when it breaks one of the 30 rules that judge one record or its prose — a " +
"link to nothing, a clause without a bold lead, a superseded record cited alone, an unknown topic, a retired word. A " +
"proposal is revised by proposing it again; an accepted record changes only by a progressive insight (insight: what " +
"stood before). The knowledge base then reviews each version itself, in a judgement that sees only the record, the " +
@@ -937,13 +937,19 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
"to": text("the target"), "from": integer("the clause of this record it goes out from")}, "required": []string{"kind", "to"}}},
"homes": texts("hq documents that cite it, such as a design's path; a decision needs one unless an accepted record links it"),
"needs-operator": flag("it holds only once the operator approves it"),
"status": map[string]any{"type": "string", "enum": ResearchStatuses, "description": "a research effort's status"},
"insight": text("for an accepted record: a progressive insight, saying what stood before"),
"why": text("one line: why this version"),
"brief": map[string]any{"type": "object", "description": "required with needs-operator: what the operator's phone shows " +
"in place of the clauses (issue 639) — a headline of at most 60 characters saying what it changes for the operator, " +
"not the title; a summary of two or three short sentences, at most 300 characters, on what changes, why and what it " +
"costs or risks; at most three points of at most 160 characters. No path, address, dotted name or long word",
"properties": map[string]any{"headline": text("what it changes for the operator"), "summary": text("two or three short sentences"),
"points": texts("at most three, only where they add something")}, "required": []string{"headline", "summary"}},
"status": map[string]any{"type": "string", "enum": ResearchStatuses, "description": "a research effort's status"},
"insight": text("for an accepted record: a progressive insight, saying what stood before"),
"why": text("one line: why this version"),
}),
Run: func(m map[string]any) (any, error) {
a, err := readArgs("propose", m, "kind", "number", "title", "topic", "date", "deciders", "clauses", "sections",
"links", "homes", "needs-operator", "status", "insight", "why")
"links", "homes", "needs-operator", "brief", "status", "insight", "why")
if err != nil {
return nil, err
}
@@ -977,6 +983,12 @@ func Tools(k *KB, caller func(map[string]any) string) []stdio.Tool {
if err := decode(m["links"], &p.Links, "links is a list of {kind, to, from}"); err != nil {
return nil, err
}
if m["brief"] != nil {
p.Brief = &Brief{}
if err := decode(m["brief"], p.Brief, "brief is {headline, summary, points}"); err != nil {
return nil, err
}
}
return k.Propose(caller(m), p)
}},
{Name: Seat + ".import",
@@ -84,9 +84,48 @@ func fingerprint(digest string) string {
return strings.Join(g, " ")
}
// AskGlance is the form of an ask read at a glance (OperatorAsk.Form, novox/hq issue 639).
const AskGlance = 1
// composeAsk is the ask for a record, composed from the record and its ask alone: asked once, and composed
// again to hold the warrant to it.
// again to hold the warrant to it — in the form it was asked in, so an ask made before a form changed is still
// held to the words the operator was shown.
func composeAsk(r *Record, a *OperatorAsk) asks.Ask {
if a.Form == AskGlance {
return glanceAsk(r, a)
}
return listedAsk(r, a)
}
// glanceAsk is the ask read at a glance on the phone (novox/hq issue 639): the brief's headline, its summary and
// points; who proposed it, its review, its fingerprint and how to read every clause are its Details, which the
// router notes after the answers. A record without a brief (one proposed before the rule) is asked with its
// title as its summary. The clauses are never listed.
func glanceAsk(r *Record, a *OperatorAsk) asks.Ask {
name := r.Key().String()
headline, explanation := "Approve "+name+"?", name+" needs your approval before it holds."
if t := strings.TrimSuffix(strings.TrimSpace(r.Title), "."); shown(t) && len([]rune(t)) < asks.SummaryLength {
explanation = t + "."
}
if r.Brief != nil && len(briefProblems(r.Brief)) == 0 {
headline, explanation = strings.TrimSpace(r.Brief.Headline), r.Brief.Explanation()
}
author := r.Author
if author == "" {
author = "an agent session"
}
clauses := fmt.Sprintf("Read its %d clauses with the knowledge base's get, number %d.", len(r.Clauses), r.Number)
if len(r.Clauses) == 0 {
clauses = fmt.Sprintf("Read it whole with the knowledge base's get, number %d.", r.Number)
}
details := fmt.Sprintf("%s, proposed by %s; the knowledge base's own review approved it. Fingerprint %s.\n%s",
name, author, fingerprint(r.Digest()), clauses)
return withOptions(r, a, asks.Ask{ID: a.ID, Headline: headline, Explanation: explanation, Details: details})
}
// listedAsk is the ask as it was composed before the brief: every clause's lead listed. Composed only to hold
// the warrant of an ask made then.
func listedAsk(r *Record, a *OperatorAsk) asks.Ask {
name := r.Key().String()
headline := "Accept " + name + "?"
var b strings.Builder
@@ -111,9 +150,15 @@ func composeAsk(r *Record, a *OperatorAsk) asks.Ask {
}
}
fmt.Fprintf(&b, "\n\nRead it whole with the knowledge base's get, number %d; its fingerprint is %s.", r.Number, fingerprint(r.Digest()))
out := asks.Ask{ID: a.ID, Headline: headline, Explanation: b.String(), Who: asks.Operator,
Expires: a.At.Add(AskLasts), OnExpiry: "the record stays proposed; nothing of it is in force",
About: "knowledge." + r.Kind + "." + strconv.Itoa(r.Number)}
return withOptions(r, a, asks.Ask{ID: a.ID, Headline: headline, Explanation: b.String()})
}
// withOptions is an ask with what every form of it carries: who answers, its expiry, what it is about, and its
// two options, each binding its act.
func withOptions(r *Record, a *OperatorAsk, out asks.Ask) asks.Ask {
out.Who, out.Expires = asks.Operator, a.At.Add(AskLasts)
out.OnExpiry = "the record stays proposed; nothing of it is in force"
out.About = "knowledge." + r.Kind + "." + strconv.Itoa(r.Number)
for _, o := range []struct{ id, label, does string }{
{OptionApprove, "Approve", "the record is accepted and its clauses are in force"},
{OptionDecline, "Decline", "the record stays proposed; nothing of it is in force"},
@@ -157,7 +202,7 @@ func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string,
if k.Asks == nil {
return unasked("the operator cannot be asked from here")
}
oa := &OperatorAsk{ID: newAskID(w), At: k.now(), Version: version, Said: AskWaiting}
oa := &OperatorAsk{ID: newAskID(w), At: k.now(), Version: version, Said: AskWaiting, Form: AskGlance}
ask := composeAsk(w, oa)
if err := ask.Check(k.now()); err != nil {
return unasked(fmt.Sprintf("the ask could not be made (%v)", err))
@@ -248,6 +248,9 @@ type OperatorAsk struct {
ID string `json:"id"`
At time.Time `json:"at"`
Version int `json:"version"`
// Form is how the ask was composed, so it is composed again the same way to hold the warrant to it: 0 for
// an ask made before the brief (the clause leads listed), AskGlance for one read at a glance (issue 639).
Form int `json:"form,omitempty"`
// Said is what came of it: waiting, approved, declined, expired, refused (and why).
Said string `json:"said"`
Warrant string `json:"warrant,omitempty"`
@@ -291,11 +294,14 @@ type Record struct {
// written; what is one is a touches link.
Touches []string `json:"touches,omitempty"`
// Effort is the status a proposed research effort takes when it is accepted.
Effort string `json:"effort,omitempty"`
NeedsOperator bool `json:"needs-operator,omitempty"`
Clauses []Clause `json:"clauses,omitempty"`
Links []Link `json:"links,omitempty"`
Sections []Section `json:"sections,omitempty"`
Effort string `json:"effort,omitempty"`
NeedsOperator bool `json:"needs-operator,omitempty"`
// Brief is what the operator's phone shows when the record asks for their Approve: written by its author,
// in the operator's words (novox/hq issue 639). Required of a record that needs the operator (the rule brief).
Brief *Brief `json:"brief,omitempty"`
Clauses []Clause `json:"clauses,omitempty"`
Links []Link `json:"links,omitempty"`
Sections []Section `json:"sections,omitempty"`
// Homes are the documents of hq that cite the record (a design, a playbook), while those live in hq.
Homes []string `json:"homes,omitempty"`
// Extra holds the fields of an imported record this slice has no field of its own for, as written.
@@ -329,6 +335,25 @@ type Insight struct {
By string `json:"by,omitempty"`
}
// Brief is a record as the operator is asked about it, on a phone, at a glance (novox/hq issue 639): a headline
// that says what the record changes for the operator — not its title — a summary of two or three short
// sentences on what changes, why, and what it costs or risks, and at most three points, only where they add
// something. The clauses are never listed in the ask; it says how to read them.
type Brief struct {
Headline string `json:"headline"`
Summary string `json:"summary"`
Points []string `json:"points,omitempty"`
}
// Explanation is the brief as the ask explains it: the summary, then a line for each point.
func (b Brief) Explanation() string {
out := strings.TrimSpace(b.Summary)
for _, p := range b.Points {
out += "\n• " + strings.TrimSpace(p)
}
return out
}
// PendingState marks a version that is a proposal over an accepted record: a progressive insight waiting for
// its review. The accepted version stays the one served until it is accepted.
type PendingState struct {
@@ -442,6 +467,12 @@ func Render(r *Record) string {
if r.NeedsOperator {
field("needs operator", "yes")
}
if r.Brief != nil {
field("brief", r.Brief.Headline)
for _, l := range strings.Split(r.Brief.Explanation(), "\n") {
field("brief", l)
}
}
field("homes", strings.Join(r.Homes, ", "))
keys := make([]string, 0, len(r.Extra))
for k := range r.Extra {
@@ -18,6 +18,10 @@ func proposed(t *testing.T, w *world, author string, needsOperator bool) int {
n := w.reserved(t, author)
p := valid(n)
p.NeedsOperator = needsOperator
if needsOperator {
p.Brief = &Brief{Headline: "Merges land after ninety seconds of quiet",
Summary: "A merge window closes after ninety seconds without a merge, so merges land sooner."}
}
if _, err := w.k.Propose(author, p); err != nil {
t.Fatal(err)
}
@@ -245,7 +249,8 @@ func TestARecordThatNeedsTheOperatorIsAcceptedOnlyOnTheWarrant(t *testing.T) {
}
ask := w.router.asked[0]
if err := ask.Check(w.clock); err != nil || ask.Options[0].Level != asks.Approve || ask.Options[1].Level != asks.Approve ||
!strings.Contains(ask.Explanation, "the knowledge base's own review") {
!strings.Contains(ask.Details, "the knowledge base's own review") || ask.Headline != "Merges land after ninety seconds of quiet" ||
asks.Glance(ask.Explanation) != nil || strings.Contains(ask.Explanation, "The window closes") {
t.Fatalf("the ask: %v %+v", err, ask)
}
// Without the warrant: nothing is accepted, however often the router's record is read or the reviews run.
@@ -2,9 +2,10 @@ package main
// The rules a record is held to (novox/hq ADR 0290 rule 4, to-be 55 §4 and §5).
//
// **29 rules judge one record or its prose**, and `propose` refuses a record that breaks any of them, naming every
// **30 rules judge one record or its prose**, and `propose` refuses a record that breaks any of them, naming every
// one it breaks at once. They are hq's checks (00-META/checks/records.py, cycle.py, index.py, words.py) as they
// judge one record, moved to the write: 25 on its structure, 4 on its prose. Each has an id, said in the refusal,
// judge one record, moved to the write: 25 on its structure, 4 on its prose; and the brief a record that needs
// the operator is asked with (novox/hq issue 639). Each has an id, said in the refusal,
// and one case in the table test (TestEveryPerRecordRuleRefusesAProposal).
//
// **4 rules judge the whole record set** (wholeSetRules) and run after every acceptance, and in the check
@@ -15,6 +16,8 @@ import (
"fmt"
"regexp"
"strings"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Rule is one rule that judges one record: its id and what it holds.
@@ -25,7 +28,7 @@ type Rule struct {
From string
}
// PerRecordRules are the 29, in the order they are judged.
// PerRecordRules are the 30, in the order they are judged.
var PerRecordRules = []Rule{
{"kind", "a record is a decision or a research effort", "cycle.py"},
{"reserved", "its number was reserved for its kind and holds no other record", "cycle.py (duplicate numbers)"},
@@ -56,6 +59,7 @@ var PerRecordRules = []Rule{
{"daemon-line", "it writes no daemon's line", "words.py lines"},
{"deferral", "a paragraph that puts work off names the issue that tracks it", "cycle.py deferrals"},
{"insight-form", "a progressive insight its text mentions is in the dated, marked form", "records.py insights"},
{"brief", "a record that needs the operator carries a brief its ask reads at a glance: a headline, a summary, at most three points, each fit for the phone", "novox/hq issue 639"},
}
// Refusal is one rule a record breaks, and how.
@@ -116,7 +120,7 @@ func allText(r *Record) string {
return b.String()
}
// JudgeRecord holds a proposed record to the 29 rules. held is the entry its number holds (a reservation, the
// JudgeRecord holds a proposed record to the 30 rules. held is the entry its number holds (a reservation, the
// proposal being revised, or the accepted record an insight corrects); lookup reads every other record.
func JudgeRecord(r *Record, held *Entry, lookup Lookup, ref *Reference) []Refusal {
var out []Refusal
@@ -349,6 +353,11 @@ func JudgeRecord(r *Record, held *Entry, lookup Lookup, ref *Reference) []Refusa
refuse("insight-form", "a progressive insight dated %s predates the record (%s)", mk[1], r.Date)
}
}
if r.NeedsOperator || r.Brief != nil {
for _, why := range briefProblems(r.Brief) {
refuse("brief", "%s", why)
}
}
return out
}
@@ -597,6 +606,7 @@ var RuleFrom = map[string]string{
"consequences": "2026-10-11",
"checked": "2026-10-11",
"live-citation": "2026-10-11",
"brief": "2026-10-13",
}
// DecisionFrom overrides RuleFrom for a decision record: hq's words check never read 02-DECISIONS ("a decision
@@ -615,3 +625,35 @@ func Holds(rule, kind, date string) bool {
}
return !dated || !isDay.MatchString(date) || date >= from
}
// briefProblems are the ways a brief would not read at a glance on the operator's phone (novox/hq issue 639):
// missing; a headline empty or longer than an ask's; a summary or a point the router could refuse, or that
// names a path, an address or a long word (shown); more than three points, or one too long (asks.Glance).
func briefProblems(b *Brief) []string {
if b == nil {
return []string{"a record that needs the operator carries a brief (headline, summary, points): what it changes for the operator, " +
"in their words, which its ask shows in place of its clauses"}
}
var out []string
h := strings.TrimSpace(b.Headline)
if h == "" || strings.Contains(h, "\n") || len([]rune(h)) > asks.HeadlineLength {
out = append(out, fmt.Sprintf("the brief's headline is empty, more than one line, or longer than %d characters", asks.HeadlineLength))
}
if s := strings.TrimSpace(b.Summary); s == "" || strings.Contains(s, "\n") {
out = append(out, "the brief's summary is empty or more than one line")
}
for i, p := range b.Points {
if p = strings.TrimSpace(p); p == "" || strings.Contains(p, "\n") {
out = append(out, fmt.Sprintf("the brief's point %d is empty or more than one line", i+1))
}
}
if err := asks.Glance(b.Explanation()); err != nil {
out = append(out, "the brief "+strings.TrimPrefix(err.Error(), "the explanation "))
}
for _, line := range append([]string{h}, strings.Split(b.Explanation(), "\n")...) {
if !shown(line) {
out = append(out, fmt.Sprintf("the brief's line %q names a path, an address, a dotted name or a long word, which the operator's phone is not shown", line))
}
}
return out
}
@@ -90,6 +90,9 @@ func TestEveryPerRecordRuleRefusesAProposal(t *testing.T) {
{"insight-form", func(p *ProposeAsk, w *world, t *testing.T) {
p.Sections[2].Text = "A progressive insight corrected the count."
}},
{"brief", func(p *ProposeAsk, w *world, t *testing.T) {
p.Date, p.NeedsOperator = "2026-10-13", true
}},
}
covered := map[string]bool{}
for _, c := range cases {
@@ -100,8 +103,8 @@ func TestEveryPerRecordRuleRefusesAProposal(t *testing.T) {
t.Errorf("the rule %s has no case", r.ID)
}
}
if len(PerRecordRules) != 29 {
t.Errorf("%d per-record rules; ADR 0290 counts 29", len(PerRecordRules))
if len(PerRecordRules) != 30 {
t.Errorf("%d per-record rules; ADR 0290 counts 29, and issue 639 adds the brief", len(PerRecordRules))
}
for _, c := range cases {
t.Run(c.rule, func(t *testing.T) {