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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 99 additions & 6 deletions docs/devteam/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,12 @@
.drawer h3 { margin: 18px 0 6px; }
.hist-h { padding-top: 12px; border-top: 1px solid var(--line); }
.mini-chart svg { width: 100%; height: auto; display: block; }
.learning { margin-bottom: 14px; }
.learn-cols { display: grid; gap: 12px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 860px) { .learn-cols { grid-template-columns: minmax(0, 1fr); } }
.learn-chart h4 { font-size: 12px; font-weight: 500; margin: 0 0 4px; color: var(--fg-2); }
.learn-chart .card { margin-bottom: 2px; padding: 8px 10px; }
.learn-chart p { margin: 0 0 10px; }
.diagram-wrap { overflow-x: auto; margin: 4px 0 8px; }
.diagram-wrap figure { margin: 0; }
.md.tldr { font-size: 13px; }
Expand Down Expand Up @@ -636,6 +642,7 @@ <h3>Links</h3>

<section id="loop">
<h2>Loop <span class="sub">how the supervisor notices, makes sense of, decides and acts</span></h2>
<div id="learning" class="learning" aria-label="Is the loop making the system smarter?"></div>
<div class="tabs" role="tablist" id="loop-tabs">
<button role="tab" data-tab="observe">Observe<span class="n" id="n-observe"></span></button>
<button role="tab" data-tab="orient">Orient<span class="n" id="n-orient"></span></button>
Expand Down Expand Up @@ -2099,6 +2106,9 @@ <h4><a class="title-link" href="#goal/${encodeURIComponent(g.id)}">${esc(g.title
return empty(ask ? `No goals yet. The supervisor has asked for them: <a class="linkish" href="#q/${encodeURIComponent(ask.id)}">answer it in the Inbox</a>.` : 'No goals yet. The supervisor asks the owner for them first (<code>process/planning.md</code> §1).');
})());

// Is it learning: the chart above the tabs
$('learning').innerHTML = learningSection(o);

// Observe
$('observations').innerHTML = o.observations.length ? o.observations.slice(0, 40).map((x) => `
<div class="card small"${subj('observation', x.id, `${words(x.kind)}: ${x.detail}`)} data-open="observation/${esc(encodeURIComponent(x.id))}"><span class="badge ${sevBadge(x.severity ?? 0)}">${esc(words(x.kind))}</span>${subjectLink(x.subject)}
Expand Down Expand Up @@ -2535,10 +2545,13 @@ <h4><a class="title-link" href="#goal/${encodeURIComponent(g.id)}">${esc(g.title
return `<h3>${esc(label)}: the TL;DR</h3><div class="md tldr">${md}</div><p class="small"><a class="linkish" href="${esc(href)}">Read the whole design</a></p>`;
}
const reviewList = (rs) => (rs?.length ? `<ul>${rs.map((r) => `<li>${esc(words(r.stage))} review, pass ${esc(r.pass)}: <b>${r.verdict === 'APPROVE' ? 'approved' : r.verdict === 'CHANGES' ? 'changes asked' : esc(r.verdict || 'not done')}</b>${r.reviewer ? ` by ${esc(r.reviewer)}` : ''}</li>`).join('')}</ul>` : '');
/* learning start */
// A small time chart: one line per series over time, optional horizontal marks (targets).
// span: [from, to] in ms to fix the x axis (a sensor's last 24h); width: the drawing's own width,
// so a narrow card draws a narrow chart with readable text; a series may name its color.
function lineChart(series, { marks = [], unit = '', height = 160, yMin = 0, span = null, width = 640 } = {}) {
// so a narrow card draws a narrow chart with readable text; a series may name its color. A point
// is [ts, value] or [ts, value, { hollow, href, title }]: hollow for too few to say, href to open
// what it counts. xmarks: [{ at, label, href }] draws dashed vertical lines (a retro, a rung change).
function lineChart(series, { marks = [], xmarks = [], unit = '', height = 160, yMin = 0, span = null, width = 640 } = {}) {
const pts = series.flatMap((s) => s.points);
if (!pts.length) return '';
const W = width, H = height, L = 40, R = 12, T = 12, B = 22;
Expand All @@ -2564,10 +2577,71 @@ <h4><a class="title-link" href="#goal/${encodeURIComponent(g.id)}">${esc(g.title
series.forEach((s, i) => {
const ps = s.points.slice().sort((a, b) => a[0] - b[0]);
svg += `<polyline fill="none" stroke="${s.color || `var(--series-${(i % 4) + 1}, var(--blue))`}" stroke-width="2" points="${ps.map((p) => `${x(p[0]).toFixed(1)},${y(p[1]).toFixed(1)}`).join(' ')}"/>`;
if (ps.length <= 60) for (const p of ps) svg += `<circle cx="${x(p[0]).toFixed(1)}" cy="${y(p[1]).toFixed(1)}" r="3" fill="${s.color || `var(--series-${(i % 4) + 1}, var(--blue))`}"><title>${esc(`${s.name}: ${p[1]}${unit} · ${fmtDate(p[0])}`)}</title></circle>`;
const c = s.color || `var(--series-${(i % 4) + 1}, var(--blue))`;
if (ps.length <= 60) for (const p of ps) {
const o = p[2] || {};
const dot = `<circle cx="${x(p[0]).toFixed(1)}" cy="${y(p[1]).toFixed(1)}" r="3" ${o.hollow ? `fill="none" stroke="${c}" stroke-width="1.5" pointer-events="all" data-hollow` : `fill="${c}"`}><title>${esc(o.title ?? `${s.name}: ${p[1]}${unit} · ${fmtDate(p[0])}`)}</title></circle>`;
svg += o.href ? `<a href="${esc(o.href)}">${dot}</a>` : dot;
}
});
for (const m of xmarks) {
const ts = Date.parse(m.at);
if (!(ts >= t0 && ts <= t1)) continue;
const X = x(ts).toFixed(1);
const mark = `<g class="xmark"><line x1="${X}" x2="${X}" y1="${T}" y2="${H - B}" stroke="var(--muted)" stroke-dasharray="3 3"/><line x1="${X}" x2="${X}" y1="${T}" y2="${H - B}" stroke="transparent" stroke-width="8"/><title>${esc(`${m.label} · ${fmtDate(ts)}`)}</title></g>`;
svg += m.href ? `<a href="${esc(m.href)}">${mark}</a>` : mark;
}
return `<div class="card chart mini-chart">${series.length > 1 ? `<div class="legend">${series.map((s, i) => `<span><i style="border-top-color:${s.color || `var(--series-${(i % 4) + 1}, var(--blue))`}"></i>${esc(s.name)}</span>`).join('')}</div>` : ''}${svg}</svg></div>`;
}
// Is the loop making the system smarter? Six small charts, two per column (Informed, Autonomous,
// Trusted), from the weekly learning records, oldest first. Three columns, never one score: a
// correction in one class shows as a dip, not averaged away by a promotion in another. A count and
// a share never share a chart. A point whose denominator is under LEARN_MIN is hollow: too few to say.
const LEARN_MIN = 5;
const learnShare = (a, b) => (b ? Math.round((100 * a) / b) : 0);
const LEARN_CHARTS = [
['Informed', [
{ title: 'Accepted knowledge', note: 'lessons, facts and rules in force at the week\'s end', series: [{ name: 'accepted knowledge', v: (f) => f.informed.knowledge }] },
{ title: 'Explained, and given', note: 'recurring signals explained, of those formed; fresh starts given a lesson', unit: '%', series: [
{ name: 'signals explained', v: (f) => learnShare(f.informed.signals_explained, f.informed.signals_formed), n: (f) => f.informed.signals_formed },
{ name: 'starts given a lesson', v: (f) => learnShare(f.informed.starts_given, f.informed.starts), n: (f) => f.informed.starts },
] },
]],
['Autonomous', [
{ title: 'Ladder calls made on trust', note: 'of the calls in a trust-ladder class, the ones made on its rung or a standing rule, not asked', unit: '%', series: [{ name: 'on trust', v: (f) => learnShare(f.autonomous.trusted_calls, f.autonomous.ladder_calls), n: (f) => f.autonomous.ladder_calls }] },
{ title: 'Questions per merged ticket', note: 'decisions put to you, for each ticket merged', series: [{ name: 'questions per merged ticket', v: (f) => +(f.autonomous.questions / Math.max(1, f.autonomous.merged)).toFixed(2), n: (f) => f.autonomous.merged, none: 'no ticket merged' }] },
]],
['Trusted', [
{ title: 'Agreement', note: 'your answers that went with the recommendation', unit: '%', series: [{ name: 'agreement', v: (f) => learnShare(f.trusted.agreed, f.trusted.answered), n: (f) => f.trusted.answered }] },
{ title: 'Corrections per 100 calls', note: 'calls you corrected, for every hundred the supervisor made', series: [{ name: 'corrections per 100 calls', v: (f) => +((100 * f.trusted.corrections) / Math.max(1, f.trusted.calls)).toFixed(1), n: (f) => f.trusted.calls }] },
]],
];
// a record whose three columns aren't objects (a hand-written line) is left out, not drawn
const learnable = (r) => !!r && !!r.figures && ['informed', 'autonomous', 'trusted'].every((k) => r.figures[k] && typeof r.figures[k] === 'object');
function learningSection(o) {
const recs = (o.learning || []).filter(learnable);
const T = o.learning_totals || {};
const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
if (recs.length < 3) {
const since = T.since ? fmtDate(Date.parse(T.since)) : 'the start';
return `<div class="learn-young" data-learn="young"><div class="grid">${[['calls', T.calls], ['lessons kept', T.lessons], ['corrections', T.corrections]].map(([k, v]) => `<div class="stat"><span>${esc(`${k} since ${since}`)}</span><b>${esc(v ?? 0)}</b></div>`).join('')}</div>
<p class="small muted">Since ${esc(since)}: ${esc(plural(T.calls ?? 0, 'call'))}, ${esc(plural(T.lessons ?? 0, 'lesson'))} kept, ${esc(plural(T.corrections ?? 0, 'correction'))}. The loop needs a few weeks of calls, answers and lessons before trends mean anything.</p></div>`;
}
const xmarks = (o.learning_marks || []).map((m) => ({ at: m.at, label: m.label, href: m.href }));
const span = [Date.parse(recs[0].from), Date.parse(recs[recs.length - 1].from) + 7 * DAY];
const chart = (c) => {
const series = c.series.map((s, i) => ({ name: s.name, color: i ? 'var(--amber)' : 'var(--series-1)', points: recs.map((r) => {
const v = s.v(r.figures);
const n = s.n ? s.n(r.figures) : null;
const few = n !== null && n < LEARN_MIN;
return [Date.parse(r.from), v, { hollow: few, href: `#learning/${encodeURIComponent(r.id)}`, title: few ? `${s.name}, week ${r.week}: ${n === 0 && s.none ? s.none : `too few to say (n = ${n})`}` : `${s.name}, week ${r.week}: ${v}${c.unit || ''}${n !== null ? ` (n = ${n})` : ''}` }];
}) }));
return `<div class="learn-chart" data-chart="${esc(c.title)}"><h4>${esc(c.title)}</h4>${lineChart(series, { unit: c.unit || '', height: 120, width: 360, span, xmarks })}<p class="small muted">${esc(c.note)}</p></div>`;
};
return `<div class="learn-cols" data-learn="charts">${LEARN_CHARTS.map(([col, charts]) => `<div class="learn-col"><h3>${esc(col)}</h3>${charts.map(chart).join('')}</div>`).join('')}</div>
<p class="small muted">Weekly, from each week's learning record: open a point for what it counted. A hollow point is too few to say (under ${LEARN_MIN}). Dashed lines are retros and rung changes.</p>`;
}
/* learning end */
// A set's progress: its meter and forecast, its burn-up, then its tickets as the board.
function progressBlock(items) {
const now = Date.parse(DATA.progress.now);
Expand Down Expand Up @@ -3170,15 +3244,34 @@ <h3>Its observations</h3>${items.length ? `<ul>${items.map((i) => `<li><a class=
${s?.explain?.length ? `<details><summary>How the supervisor explains it</summary><p class="small muted">With what it teaches, or a ruling that it is noise; you can correct either.</p>${s.explain.map((x) => `<pre>${esc(x)}</pre>`).join('')}</details>` : ''}
${historySection(p, key)}`);
}
// The weekly learning record (PR 5 adds its figures).
// The weekly learning record: its figures, every record each counts (recomputed from the log as it
// stood at the week's end; the line's refs are capped), and what each lesson given did since.
function openLearning(id) {
drawerFor('learning', id);
const p = peekOf('learning', id);
const f = p?.record?.fields;
const card = (DATA.ooda.learning || []).find((x) => x.id === id);
const f = p?.record?.fields ?? (card ? { detail: card.detail, week: card.week, figures: card.figures } : null);
if (!f) return notFound('learning', id, p, p?.error ? 'No such learning record.' : '');
showDrawer(`<h2 id="drawer-title">${esc(clip(f.detail || 'Learning', 80))}</h2>
const L = p?.learning;
const F = L?.figures ?? f.figures;
const ids = (xs) => (xs?.length ? linkify(xs.join(' ')) : '<span class="muted">none</span>');
const of = (a, b) => `${esc(a)} of ${esc(b)}${b ? ` <span class="muted">(${esc(Math.round((100 * a) / b))}%)</span>` : ''}${b < 5 ? ' <span class="muted">· too few to say</span>' : ''}`;
const rows = F ? [
['Informed', 'Accepted knowledge in force', esc(F.informed.knowledge), L?.lists.knowledge],
['', 'Recurring signals explained, of those formed', of(F.informed.signals_explained, F.informed.signals_formed), L?.lists.signals.filter((x) => x.explained).flatMap((x) => x.by.slice(0, 1))],
['', 'Fresh starts given a lesson', of(F.informed.starts_given, F.informed.starts), L?.lists.starts.map((x) => x.id)],
['Autonomous', 'Ladder calls made on trust', of(F.autonomous.trusted_calls, F.autonomous.ladder_calls), L?.lists.ladder_calls.map((x) => x.id)],
['', 'Questions put to you, for merged tickets', `${esc(F.autonomous.questions)} for ${esc(F.autonomous.merged)}`, L ? [...L.lists.questions, ...L.lists.merged] : null],
['Trusted', 'Answers that went with the recommendation', of(F.trusted.agreed, F.trusted.answered), L?.lists.answered.map((x) => x.id)],
['', 'Corrections, in the calls made', `${esc(F.trusted.corrections)} in ${esc(F.trusted.calls)}`, L?.lists.corrections],
] : [];
const eff = L?.efficacy ?? [];
showDrawer(`<h2 id="drawer-title">The week of ${esc(L?.from ? fmtDate(Date.parse(L.from)) : f.week)}</h2>
${attrs({ id, peek: p, type: 'observation' })}
${f.detail ? `<p class="ctx measure">${linkify(f.detail)}</p>` : ''}
<h3>The figures</h3>${rows.length ? `<div class="scroll"><table class="nums" data-learning-figures><tr><th></th><th>Figure</th><th>Count</th><th>What it counted</th></tr>${rows.map(([g, k, v, xs]) => `<tr><td>${esc(g)}</td><td>${esc(k)}</td><td>${v}</td><td class="small">${L ? ids(xs) : '<span class="loading">Loading…</span>'}</td></tr>`).join('')}</table></div>` : empty('No figures on this record.')}
<h3>Lesson efficacy</h3><p class="small muted">For each lesson a worker or planner was given: what was seen on the signals it came from in the four weeks before it was first given, and since. Counts and records, not a score.</p>
${L ? (eff.length ? `<div class="scroll"><table class="nums" data-efficacy><tr><th>Lesson</th><th>First given</th><th>Before</th><th>Since</th></tr>${eff.map((e) => `<tr><td><a class="linkish" href="#lesson/${encodeURIComponent(e.id)}">${esc(clip(e.statement || e.id, 80))}</a>${e.keys.length ? `<div class="small muted">${e.keys.map((k) => `<code>${esc(k)}</code>`).join(' ')}</div>` : '<div class="small muted">its evidence names no observation</div>'}</td><td>${esc(when(e.first_given))}</td><td>${esc(e.before.count)}<div class="small">${ids(e.before.ids)}</div></td><td>${esc(e.since.count)}<div class="small">${ids(e.since.ids)}</div></td></tr>`).join('')}</table></div>` : empty('No lesson was given to a worker or planner by the end of this week.')) : p === undefined ? '<p class="loading">Loading…</p>' : ''}
${lineageBlock(p)}
${historySection(p, id)}`);
}
Expand Down
2 changes: 2 additions & 0 deletions docs/devteam/process/operator.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ Leave out what isn't the supervisor choosing to ask: the kinds whose job is to a

Write down the question ids each check caught. On a kit too old for `search --type question`, compare by reading `questions --all --json` alone.

Each repo's weekly counts help with the first three: `log --type observation --json` holds one record of kind `learning` per ended week, with its `week` and `figures` (informed, autonomous, trusted). Compare the last few weeks rather than one; a count under five says little.

**Signals.** Some of what you see is a symptom, and relaying it is the failure this review exists to catch. Each of these gets a "why?" answered before anything goes in `NOTES.md`, at the review and at any check-in that sees one:
- a supervisor asking the owner in chat instead of with an Inbox question;
- a goal whose latest reading is at or past its target but whose status isn't `met` (`goal --json`: `status`, and the last of `readings` against `measure.target` in the way `measure.direction` says);
Expand Down
1 change: 1 addition & 0 deletions docs/devteam/process/supervisor.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ A reading (a sensor's daily rollup, a goal reading, a `spend` line, a `gate` run

**Improve and record**
10. `devteam.mjs retro --due`: if due, draft it now (§10) and finish it on the next quiet tick, or first thing once the draft is a day old. On quiet ticks run the improvement loop (§9).
- **Is the loop learning?** Once a week has ended (UTC), `observe-metrics` writes it as one observation of kind `learning` (`source_key: learning:<yyyy-Www>`, with its `week` and `figures`); on its first run it back-fills every past week since the log began (up to two years), each counted from the log as it stood at that week's end. Never write one yourself. The figures are counts in three columns: informed (accepted knowledge in force; recurring signals explained, of those formed; fresh starts given a lesson), autonomous (ladder calls made on trust, of all ladder calls; decisions put to the owner; tickets merged), trusted (answers that went with your recommendation, of those that had one; corrections, of your calls). The Loop charts them, hollow where fewer than five were counted; `peek learning <id> --json` lists every record a week counted and, for each lesson a start was given, what its evidence's signals did in the four weeks before and since. At a retro, read the weeks since the last one: a share falling, or corrections rising, is a signal to orient on, not a score to defend.
11. Notify the owner only on a new blocking question or something they must act on now.
12. `devteam.mjs log-sync` (pushes the chore-log branch; merge it with `merge --chore log` once a day, after the tick's merges, and before merging a plan that names a newly approved goal).
13. Spend is measured from every agent's session log (settings `usage_sources`: `claude-code` and `codex` are built in; `usage_command` adds any other harness; `model_prices` prices a model the kit doesn't know). An `unpriced-model` finding means spend undercounts until the owner adds a price.
Expand Down
Loading
Loading