package main // Drafting and confirming clauses at the import (novox/hq ADR 0290 rule 8, to-be 55 §7). For a decision whose // Decision section is prose, one judgement drafts its clauses: the section in, numbered statements out, each one // sentence saying nothing the section does not. They are kept `drafted`, and never served as in force. A second // judgement, a separate call that sees only the record and the drafts, answers per clause whether the record says // exactly that: a clause it accepts becomes `confirmed`, one it rejects stays `drafted` with the reason. Which // clause of an older record a supersedes-in-part changes is judged and confirmed the same way. When every record // of a topic has been through both, the operator is told once, with the counts. `return` sends a record back to // be drafted again. // // The judgements are the agent's on this node (claude-code.claude_code_judge, ADR 0278): no tools, no session, // at most 25 seconds each, one at a time. The worker takes one step at a time in the background. import ( "context" "encoding/json" "errors" "fmt" "strconv" "strings" "time" ) // DefaultJudgeModel is the model the judgements ask unless the settings name another. const DefaultJudgeModel = "sonnet" // MostDraftTries is how many times a record's drafting is tried before it is left failed, said by its get. const MostDraftTries = 3 var draftSchema = map[string]any{ "type": "object", "properties": map[string]any{ "clauses": map[string]any{"type": "array", "items": map[string]any{ "type": "object", "properties": map[string]any{ "lead": map[string]any{"type": "string", "description": "a few words naming what is decided, ending with a full stop"}, "text": map[string]any{"type": "string", "description": "the one sentence of the statement, after the lead"}, }, "required": []string{"lead", "text"}, }}, }, "required": []string{"clauses"}, } var confirmSchema = map[string]any{ "type": "object", "properties": map[string]any{ "verdicts": map[string]any{"type": "array", "items": map[string]any{ "type": "object", "properties": map[string]any{ "clause": map[string]any{"type": "integer"}, "exact": map[string]any{"type": "boolean", "description": "true when the record says exactly this, nothing more"}, "reason": map[string]any{"type": "string", "description": "when not exact: what the record says instead"}, }, "required": []string{"clause", "exact"}, }}, }, "required": []string{"verdicts"}, } var whichSchema = map[string]any{ "type": "object", "properties": map[string]any{ "clauses": map[string]any{"type": "array", "items": map[string]any{"type": "integer"}, "description": "the numbers of the older record's clauses the newer record replaces part of"}, "reason": map[string]any{"type": "string"}, }, "required": []string{"clauses", "reason"}, } var whichConfirmSchema = map[string]any{ "type": "object", "properties": map[string]any{ "exact": map[string]any{"type": "boolean"}, "reason": map[string]any{"type": "string"}, }, "required": []string{"exact", "reason"}, } // MostDraftSource is the most text a record is drafted from in one judgement, which takes at most 64 KiB. const MostDraftSource = 48 << 10 // draftSource is what a record's clauses are drafted from: its Decision section, or, for a record that has none // (hq issue 487), what it decides across its sections — all but its preamble, consequences and references. func draftSource(r *Record) (text string, whole bool) { if section, ok := decisionSection(r); ok && strings.TrimSpace(section) != "" { return section, false } var b strings.Builder for _, s := range r.Sections { name := strings.ToLower(strings.TrimSpace(s.Name)) if name == "preamble" || strings.HasPrefix(name, "consequences") || strings.HasPrefix(name, "references") || strings.TrimSpace(s.Text) == "" { continue } fmt.Fprintf(&b, "## %s\n\n%s\n\n", s.Name, strings.TrimSpace(s.Text)) } return strings.TrimSpace(b.String()), true } // sourceName is how a question names what it hands the judgement. func sourceName(whole bool) string { if whole { return "The record's sections" } return "The Decision section" } // DraftQuestion is the first judgement's question: the record's Decision section — or, where it has none, its // sections — and nothing else of the mesh. func DraftQuestion(r *Record, section, returned string, whole bool) string { var b strings.Builder if whole { fmt.Fprintf(&b, "Below are the sections of a decision record, %s: %q. It has no Decision section: what it decides is written "+ "as prose across its sections. Write what it decides as numbered clauses: each one statement, one sentence, with a lead of a "+ "few words naming what is decided. A clause says nothing the sections do not say: no inference, no context from elsewhere, "+ "nothing added. Every decision the sections make is in one clause; a sentence that only explains, or describes what was, is "+ "in none. Keep the record's own words where you can.\n", r.Key(), r.Title) if returned != "" { fmt.Fprintf(&b, "\nAn earlier draft was sent back, with this reason: %s\n", returned) } b.WriteString("\n" + sourceName(true) + ":\n\n" + section) return b.String() } fmt.Fprintf(&b, "Below is the Decision section of a decision record, %s: %q. It is written as prose. Write what it decides as numbered "+ "clauses: each one statement, one sentence, with a lead of a few words naming what is decided. A clause says nothing the section "+ "does not say: no inference, no context from elsewhere, nothing added. Every decision the section makes is in one clause; a "+ "sentence that only explains is in none. Keep the section's own words where you can.\n", r.Key(), r.Title) if returned != "" { fmt.Fprintf(&b, "\nAn earlier draft was sent back, with this reason: %s\n", returned) } b.WriteString("\nThe Decision section:\n\n" + section) return b.String() } // ConfirmQuestion is the second judgement's question: the record and the drafts, nothing else. func ConfirmQuestion(r *Record, section string, drafts []Clause, whole bool) string { var b strings.Builder what := "the Decision section of a decision record" if whole { what = "the sections of a decision record that has no Decision section" } fmt.Fprintf(&b, "Below are %s, %s: %q, and clauses drafted from it by someone else. For each "+ "clause, answer whether the text says exactly that: exact is true only when the clause says nothing the text does not, "+ "and leaves out nothing that changes its meaning. When it is not exact, say in reason what the text says instead.\n\n"+ "%s:\n\n%s\n\nThe drafted clauses:\n\n", what, r.Key(), r.Title, sourceName(whole), section) for _, c := range drafts { fmt.Fprintf(&b, "%d. %s\n", c.N, c.Text) } return b.String() } func (k *KB) judgeModel() string { if m := strings.TrimSpace(k.settings().JudgeModel); m != "" { return m } return DefaultJudgeModel } // DraftStep takes one step of the drafting and confirming: the first record waiting to be drafted or confirmed, // then the first changes link whose clause is not yet judged. It answers what it did, or "" when nothing waits. func (k *KB) DraftStep() (string, error) { if k.Judge == nil || strings.EqualFold(strings.TrimSpace(k.settings().Drafting), "off") { return "", nil } ctx := context.Background() all, err := k.Store.All(ctx) if err != nil { return "", err } for _, e := range all { d := e.Current.Drafting if d == nil || d.State == DraftDone || (d.State == DraftFailed && d.Tries >= MostDraftTries) { continue } if d.State == DraftFailed && k.now().Sub(d.At) < time.Duration(d.Tries)*10*time.Minute { continue } return k.draftOne(ctx, e.Current.Key()) } for _, e := range all { if !e.Current.Accepted() { continue } for i, l := range e.Current.Links { if l.Kind == LinkChanges && (l.Source == LinkPending || l.Source == SourceDrafted) && !l.Unresolved { return k.judgeChange(ctx, e.Current.Key(), i) } } } return "", nil } // draftOne drafts or confirms one record's clauses, under the write lock only to read and to write. func (k *KB) draftOne(ctx context.Context, key Key) (string, error) { k.mu.Lock() e, err := k.Store.Get(ctx, key) k.mu.Unlock() if err != nil || e == nil { return "", err } r := e.Current.Clone() section, whole := draftSource(r) if section == "" || len(section) > MostDraftSource { why := fmt.Sprintf("no Decision section, and its sections hold %d bytes of what it decides, more than one judgement takes (%d): "+ "it has no clauses", len(section), MostDraftSource) if section == "" { why = "no Decision section, and no section saying what it decides: it has no clauses" } r.Drafting = &Drafting{State: DraftDone, Reason: why, At: k.now()} return k.saveDraft(ctx, e, r, EventDrafted, why) } model := k.judgeModel() fail := func(step string, err error) (string, error) { // Tried again after a pause that grows with each try, at the step it failed at, up to MostDraftTries. r.Drafting.State, r.Drafting.Reason, r.Drafting.At = DraftFailed, step+": "+err.Error(), k.now() r.Drafting.Tries++ _, serr := k.saveDraft(ctx, e, r, EventDrafted, step+" failed: "+err.Error()) if serr != nil { return "", serr } return "", fmt.Errorf("%s %s: %v", step, key, err) } if r.Drafting == nil { r.Drafting = &Drafting{State: DraftWaiting} } // A failed record goes back to the step it failed at: confirming when it holds drafts, drafting otherwise. confirming := r.Drafting.State == DraftDrafted || (r.Drafting.State == DraftFailed && hasDrafts(r)) if !confirming { answer, by, err := k.Judge(DraftQuestion(r, section, r.Drafting.Returned, whole), draftSchema, model) if err != nil { return fail("drafting", err) } var drafted struct { Clauses []struct { Lead string `json:"lead"` Text string `json:"text"` } `json:"clauses"` } if err := json.Unmarshal(answer, &drafted); err != nil || len(drafted.Clauses) == 0 { return fail("drafting", errors.New("the judgement drafted no clause")) } r.Clauses = nil for i, c := range drafted.Clauses { lead := strings.TrimSpace(strings.Trim(c.Lead, "* ")) if !strings.HasSuffix(lead, ".") { lead += "." } r.Clauses = append(r.Clauses, Clause{N: i + 1, Text: "**" + lead + "** " + strings.TrimSpace(c.Text), Status: clauseStatusOf(r.Status), Source: SourceDrafted}) } r.Drafting = &Drafting{State: DraftDrafted, Drafter: by, Returned: r.Drafting.Returned, Tries: r.Drafting.Tries, At: k.now()} return k.saveDraft(ctx, e, r, EventDrafted, fmt.Sprintf("%d clauses drafted by %s", len(r.Clauses), by)) } answer, by, err := k.Judge(ConfirmQuestion(r, section, r.Clauses, whole), confirmSchema, model) if err != nil { return fail("confirming", err) } var verdicts struct { Verdicts []struct { Clause int `json:"clause"` Exact bool `json:"exact"` Reason string `json:"reason"` } `json:"verdicts"` } if err := json.Unmarshal(answer, &verdicts); err != nil { return fail("confirming", errors.New("the judgement answered no verdicts")) } confirmed := 0 for i := range r.Clauses { r.Clauses[i].Unconfirmed = "the second judgement gave no verdict on it" for _, v := range verdicts.Verdicts { if v.Clause != r.Clauses[i].N { continue } if v.Exact { r.Clauses[i].Source, r.Clauses[i].Unconfirmed = SourceConfirmed, "" confirmed++ } else { r.Clauses[i].Unconfirmed = strings.TrimSpace(v.Reason) if r.Clauses[i].Unconfirmed == "" { r.Clauses[i].Unconfirmed = "not exact" } } } } r.Drafting.State, r.Drafting.Checker, r.Drafting.At, r.Drafting.Reason = DraftDone, by, k.now(), "" said, err := k.saveDraft(ctx, e, r, EventConfirmed, fmt.Sprintf("%d of %d drafted clauses confirmed by %s", confirmed, len(r.Clauses), by)) if err != nil { return "", err } k.tellTopic(ctx, r.Topic) return said, nil } func hasDrafts(r *Record) bool { for _, c := range r.Clauses { if c.Source == SourceDrafted { return true } } return false } func (k *KB) saveDraft(ctx context.Context, e *Entry, r *Record, event, why string) (string, error) { k.mu.Lock() defer k.mu.Unlock() by := "the knowledge base" if r.Drafting != nil && event == EventDrafted && r.Drafting.Drafter != "" { by = r.Drafting.Drafter } if r.Drafting != nil && event == EventConfirmed && r.Drafting.Checker != "" { by = r.Drafting.Checker } if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil { return "", fmt.Errorf("%s could not be kept (%v)", r.Key(), err) } k.emit(event, r, by, why, e.Versions+1) return r.Key().String() + ": " + why, nil } // tellTopic tells the operator, once, when every prose record of a topic has been drafted and confirmed. func (k *KB) tellTopic(ctx context.Context, topic string) { if k.Notify == nil || topic == "" { return } all, err := k.Store.All(ctx) if err != nil { return } records, confirmed, drafted := 0, 0, 0 for _, e := range all { r := e.Current if r.Topic != topic || r.Drafting == nil { continue } if r.Drafting.State != DraftDone { return } records++ for _, c := range r.Clauses { if c.Source == SourceConfirmed { confirmed++ } else if c.Source == SourceDrafted { drafted++ } } } if records == 0 { return } first, err := k.Store.Told(ctx, topic) if err != nil || !first { return } summary := fmt.Sprintf("The clauses of %d prose decisions on %s are drafted and checked: %d confirmed, %d still drafted.", records, topic, confirmed, drafted) explanation := "A second judgement confirmed each clause against its record; a clause still drafted is never served as in force. " + "You may send any record back with the knowledge base's return, and its clauses are drafted again. Nothing needs you otherwise." if err := k.Notify("knowledge.clauses."+strings.ReplaceAll(topic, " ", "-"), summary, "Clauses drafted for "+topic, explanation, ""); err != nil { k.logf("[mesh-knowledge] the operator could not be told about %s's clauses: %v", topic, err) } } // judgeChange judges, then confirms, which clause of an older record a supersedes-in-part of hq changes. func (k *KB) judgeChange(ctx context.Context, from Key, i int) (string, error) { k.mu.Lock() e, err := k.Store.Get(ctx, from) var t *Entry if err == nil && e != nil && i < len(e.Current.Links) { if to, ok := e.Current.Links[i].To.Record(); ok { t, err = k.Store.Get(ctx, to) } } k.mu.Unlock() if err != nil || e == nil || t == nil { return "", err } r := e.Current.Clone() l := &r.Links[i] older := t.Current served := 0 for _, c := range older.Clauses { if c.Source != SourceDrafted { served++ } } if served == 0 { return "", nil // the older record's clauses are not confirmed yet: judged once they are } model := k.judgeModel() newer, whole := draftSource(r) var b strings.Builder fmt.Fprintf(&b, "%s says it replaces part of an older record, %s. Below are the clauses of %s and %s of %s.\n\n", r.Key(), older.Key(), older.Key(), strings.ToLower(sourceName(whole)), r.Key()) for _, c := range older.Clauses { fmt.Fprintf(&b, "%d. %s\n", c.N, c.Text) } fmt.Fprintf(&b, "\n%s of %s:\n\n%s\n", sourceName(whole), r.Key(), newer) if l.Source == LinkPending { answer, by, err := k.Judge(b.String()+"\nWhich of the older record's clauses does the newer record replace part of? Name their numbers.", whichSchema, model) if err != nil { return "", fmt.Errorf("judging which clause %s changes: %v", r.Key(), err) } var which struct { Clauses []int `json:"clauses"` Reason string `json:"reason"` } if json.Unmarshal(answer, &which) != nil || len(which.Clauses) == 0 || older.Clause(which.Clauses[0]) == nil { return "", fmt.Errorf("judging which clause %s changes: the judgement named no clause of %s", r.Key(), older.Key()) } base := *l l.To.Clause, l.Source = which.Clauses[0], SourceDrafted for _, n := range which.Clauses[1:] { if older.Clause(n) != nil { extra := base extra.To.Clause, extra.Source = n, SourceDrafted r.Links = append(r.Links, extra) } } return k.saveLinks(ctx, e, r, by, fmt.Sprintf("which clause of %s it changes drafted: §%s (%s)", older.Key(), joinInts(which.Clauses), which.Reason)) } answer, by, err := k.Judge(b.String()+fmt.Sprintf("\nA first judgement said %s replaces part of clause %d of %s. Is that exact?", r.Key(), l.To.Clause, older.Key()), whichConfirmSchema, model) if err != nil { return "", fmt.Errorf("confirming which clause %s changes: %v", r.Key(), err) } var v struct { Exact bool `json:"exact"` Reason string `json:"reason"` } if json.Unmarshal(answer, &v) != nil { return "", fmt.Errorf("confirming which clause %s changes: no verdict", r.Key()) } if !v.Exact { clause := l.To.Clause l.To.Clause, l.Source = 0, SourceDrafted+": not confirmed" return k.saveLinks(ctx, e, r, by, fmt.Sprintf("that it changes %s §%d was not confirmed: %s", older.Key(), clause, v.Reason)) } l.Source = SourceConfirmed said, err := k.saveLinks(ctx, e, r, by, fmt.Sprintf("that it changes %s §%d confirmed by %s", older.Key(), l.To.Clause, by)) if err != nil { return "", err } // The older clause is changed now, as an acceptance would have made it. k.mu.Lock() defer k.mu.Unlock() t, err = k.Store.Get(ctx, older.Key()) if err != nil || t == nil { return said, err } c := t.Current.Clone() if cl := c.Clause(l.To.Clause); cl != nil && cl.Status == ClauseInForce { cl.Status = ClauseChanged cl.ChangedBy = append(cl.ChangedBy, r.Key().String()) what := fmt.Sprintf("§%d changed by %s (as hq said, its clause judged and confirmed)", l.To.Clause, r.Key()) if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: what, Record: c}, true, false, t.Revision); err != nil { return said, err } k.emit(EventChanged, c, by, what, t.Versions+1) } return said, nil } func (k *KB) saveLinks(ctx context.Context, e *Entry, r *Record, by, why string) (string, error) { k.mu.Lock() defer k.mu.Unlock() if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil { return "", fmt.Errorf("%s could not be kept (%v)", r.Key(), err) } event := EventDrafted if strings.Contains(why, "confirmed") { event = EventConfirmed } k.emit(event, r, by, why, e.Versions+1) return r.Key().String() + ": " + why, nil } func joinInts(ns []int) string { s := make([]string, len(ns)) for i, n := range ns { s[i] = strconv.Itoa(n) } return strings.Join(s, ", §") } // Return sends a record's drafted or confirmed clauses back to be drafted again, with the reason, which the next // drafting reads. func (k *KB) Return(caller string, key Key, reason string) (map[string]any, error) { ctx := context.Background() reason = strings.TrimSpace(reason) if reason == "" { return nil, errors.New("return needs reason: what is wrong with the clauses drafted") } k.mu.Lock() defer k.mu.Unlock() e, err := k.Store.Get(ctx, key) if err != nil { return nil, err } if e == nil { return nil, fmt.Errorf("the knowledge base holds no %s", key) } r := e.Current.Clone() if r.Drafting == nil { return nil, fmt.Errorf("%s's clauses were written by its author, not drafted: a written clause is changed by a new record", key) } all, err := k.Store.All(ctx) if err != nil { return nil, err } if held := clauseHolds(r, all); len(held) > 0 { return nil, fmt.Errorf("%s is not returned: drafting it again would number its clauses anew, and these hold them to their numbers: %s. "+ "A clause keeps its number once another record links it (novox/hq issue 494). To say a drafted clause is wrong "+ "or no longer holds, propose a record that changes it (changes %s §N)", key, strings.Join(held, "; "), strings.TrimPrefix(key.String(), "ADR ")) } r.Clauses = nil r.Drafting = &Drafting{State: DraftWaiting, Returned: reason, At: k.now()} why := "returned to be drafted again: " + reason if err := k.Store.Save(ctx, Version{By: caller, At: k.now(), Why: why, Record: r}, true, false, e.Revision); err != nil { return nil, err } k.emit(EventReturned, r, caller, why, e.Versions+1) return map[string]any{"record": key.String(), "says": fmt.Sprintf("%s's clauses are taken back and will be drafted again, with your reason; until then it serves none.", key)}, nil } // clauseHolds is what holds a record's clauses to their numbers: each clause another record changed or superseded, // and each link of another record, served or proposed, that names one of its clauses. A redraft numbers the clauses // anew, so a record any of these name is never returned: its links would point at other clauses. func clauseHolds(r *Record, all []*Entry) []string { var out []string for _, c := range r.Clauses { if len(c.ChangedBy) > 0 && (c.Status == ClauseChanged || c.Status == ClauseSuperseded) { out = append(out, fmt.Sprintf("§%d is %s by %s", c.N, c.Status, strings.Join(c.ChangedBy, ", "))) } } key := r.Key() seen := map[string]bool{} for _, e := range all { for _, v := range []*Record{e.Current, e.Pending} { if v == nil || v.Key() == key || v.Status == StatusWithdrawn { continue } for _, l := range v.Links { to, ok := l.To.Record() if !ok || to != key || l.To.Clause == 0 { continue } s := fmt.Sprintf("%s (%s) %s §%d", v.Key(), v.Status, l.Kind, l.To.Clause) if !seen[s] { seen[s] = true out = append(out, s) } } } } return out }