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
@@ -147,14 +147,17 @@ func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T
if err := a.Check(t0); err != nil {
t.Fatalf("the router would refuse the ask: %v", err)
}
if !strings.HasPrefix(a.Explanation, "Add instructions 'handoff-from-g14' to every Claude session on the mesh?") {
t.Errorf("the phone does not say what is asked first: %q", a.Explanation)
// At a glance (issue 639): what it does is the explanation, one line; the text is the Details, which the
// router notes collapsed after the answers.
if a.Explanation != "Every Claude session on the mesh reads these 4 lines as the mesh's own word, from its next start until they are removed the same way." ||
asks.Glance(a.Explanation) != nil {
t.Errorf("the phone does not say what is asked at a glance: %q", a.Explanation)
}
if a.Headline != "Add Claude instructions 'handoff-from-g14'?" {
t.Errorf("headline: %q", a.Headline)
}
if !strings.Contains(a.Explanation, "The whole text:\n\n"+strings.TrimRight(handover, "\n")) {
t.Errorf("a short text is shown whole: %q", a.Explanation)
if !strings.HasPrefix(a.Details, "The text:\n"+strings.TrimRight(handover, "\n")+"\nProposed through the claude-code module on g14 at ") {
t.Errorf("a short text is not shown whole: %q", a.Details)
}
if len(a.Options) != 2 {
t.Fatalf("options: %+v", a.Options)
@@ -294,7 +297,7 @@ func TestARemovalNeedsTheSameApprovalAndARecordDeletedRemovesNothing(t *testing.
t.Fatal(err)
}
rid := out["proposed"].(string)
if !strings.HasPrefix(r.published[rid].Explanation, "Remove the instructions 'handoff-from-g14' from every Claude session on the mesh?") {
if r.published[rid].Explanation != "Every Claude session on the mesh stops reading the instructions 'handoff-from-g14' from its next start." {
t.Errorf("removal ask: %q", r.published[rid].Explanation)
}
if !strings.Contains(renderedInstructions(t, p), "### handoff-from-g14") {
@@ -326,7 +329,7 @@ func TestANodeScopedSectionReachesOnlyItsNodes(t *testing.T) {
t.Fatal(err)
}
id := out["proposed"].(string)
if !strings.HasPrefix(r.published[id].Explanation, "Add instructions 'builds' to every Claude session on shanks?") {
if !strings.HasPrefix(r.published[id].Explanation, "Every Claude session on shanks reads this line as the mesh's own word") {
t.Errorf("%q", r.published[id].Explanation)
}
r.close(r.chose(id, OptionApprove, t0.Add(time.Hour)))
@@ -417,15 +420,16 @@ func TestALongTextIsShownInPartAndReadWholeAtTheTerminal(t *testing.T) {
}
id := out["proposed"].(string)
a := r.published[id]
if strings.Contains(a.Explanation, "/etc/claude-code") || !strings.Contains(a.Explanation, "[1 line(s) not shown here") {
t.Errorf("a line with a path was shown: %q", a.Explanation)
if strings.Contains(a.Details, "/etc/claude-code") || !strings.Contains(a.Details, "[1 line(s) not shown here") ||
!strings.HasPrefix(a.Details, "NOT SHOWN IN FULL here.") || !strings.HasSuffix(a.Headline, "Part shown") {
t.Errorf("a line with a path was shown, or the cut not said: %q", a.Details)
}
if !strings.Contains(a.Explanation, "mesh-cli ask claude-code claude_code_proposals") ||
!strings.Contains(a.Explanation, "fingerprint there must be "+out["fingerprint"].(string)) {
t.Errorf("the phone does not say where to read all of it: %q", a.Explanation)
if !strings.Contains(a.Details, "mesh-cli ask claude-code claude_code_proposals") ||
!strings.Contains(a.Details, "fingerprint "+out["fingerprint"].(string)) {
t.Errorf("the phone does not say where to read all of it: %q", a.Details)
}
if len(a.Explanation) > 3000 {
t.Errorf("the explanation is %d bytes", len(a.Explanation))
if len(a.Details) > 3000 || asks.Glance(a.Explanation) != nil {
t.Errorf("the details are %d bytes; the explanation %q", len(a.Details), a.Explanation)
}
list, err := ListProposals(p, store, r, id, t0)
if err != nil {
@@ -739,3 +743,33 @@ func TestAnAskTakenAfterItsRefusalIsStillAnswered(t *testing.T) {
t.Errorf("decided %q", got)
}
}
// A proposer's summary is the explanation, held to a glance; a proposal made before issue 639 is composed as it was
// asked, so the warrant for it still holds after the change.
func TestAProposalIsAskedAtAGlanceAndAnOlderOneAsItWasAsked(t *testing.T) {
p, _ := node(t, "g14")
store, r := &memProposals{m: map[string]json.RawMessage{}}, newRouter()
out, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "handoff-from-g14", Text: handover,
Summary: "Every session learns how hand-overs are written, so the next one reads them the same way."}, store, r, t0)
if err != nil {
t.Fatal(err)
}
id := out["proposed"].(string)
a := r.published[id]
if a.Explanation != "Every session learns how hand-overs are written, so the next one reads them the same way." {
t.Errorf("the proposer's summary is not the explanation: %q", a.Explanation)
}
for _, bad := range []string{strings.Repeat("word ", 70), "one\ntwo", "Read it in /etc/claude-code first."} {
if _, err := Propose(p, ProposeInput{Scope: ScopeMesh, Name: "other", Text: handover, Summary: bad}, store, r, t0); err == nil ||
!strings.Contains(err.Error(), "the summary") {
t.Errorf("the summary %q was taken: %v", bad, err)
}
}
prop, _ := readProposal(store, id)
older := prop
older.Form = 0
listed := older.Ask()
if !strings.Contains(listed.Explanation, "The whole text:") || listed.Details != "" || listed.Digest() == prop.Ask().Digest() {
t.Errorf("a proposal made before is not composed as it was asked: %q", listed.Explanation)
}
}