From d1b8d80c9b222cebde6756ede2c729d00e251b07 Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Wed, 11 Nov 2026 13:56:28 -0500 Subject: [PATCH 1/8] Decisions cite the knowledge they rest on (decide --on) A call that rests on a lesson, a fact or a problem left no trace of it: the lineage couldn't draw knowledge into a decision, and the learning record couldn't count it. --on stores the ids as decision.knowledge, a new top-level field kept apart from refs (an act stays within refs) and from approved_by (authority is unchanged). Only accepted knowledge counts; a candidate, a retired lesson, an id in both lists or anything but knowledge or a problem is refused. The decision's sidepeek lists what it rests on in its own words. Co-Authored-By: Claude Opus 5.5 --- docs/devteam/dashboard/index.html | 14 ++++-- docs/devteam/schema/log.schema.json | 9 ++++ docs/devteam/scripts/devteam.mjs | 21 +++++++-- test/ooda-knowledge.test.mjs | 66 +++++++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 6 deletions(-) create mode 100644 test/ooda-knowledge.test.mjs diff --git a/docs/devteam/dashboard/index.html b/docs/devteam/dashboard/index.html index 130514d..e4ac0d4 100644 --- a/docs/devteam/dashboard/index.html +++ b/docs/devteam/dashboard/index.html @@ -2193,7 +2193,7 @@

${esc(g.title hide: ['review', 'result', 'secrets', 'secrets_posted', 'choices', 'status'], fmt: { why_asked: (w) => `${w.rung ? `${esc(RUNG_WORDS[w.rung])}` : ''}${w.ceiling ? `, at most ${esc(RUNG_WORDS[w.ceiling])}` : ''}${(w.reasons || []).length ? ` · ${esc(w.reasons.map(words).join(', '))}` : ''}`, autonomy: (a) => `${esc(words(a.class))}: ${esc(RUNG_WORDS[a.rung])}${a.scope ? ` (${esc(a.scope)})` : ''}`, goal: (v) => glink(v), re: (v) => linkify(v) }, }, - decision: { order: ['what', 'class', 'scope', 'authority', 'approved_by', 'rung', 'ceiling', 'options', 'rationale', 'confidence', 'refs', 'reversible'], fmt: { authority: (v) => esc(AUTHORITY_LABEL[v] || v) } }, + decision: { order: ['what', 'class', 'scope', 'authority', 'approved_by', 'knowledge', 'rung', 'ceiling', 'options', 'rationale', 'confidence', 'refs', 'reversible'], label: { knowledge: 'Rests on' }, fmt: { authority: (v) => esc(AUTHORITY_LABEL[v] || v), knowledge: linkList } }, act: { order: ['what', 'class', 'scope', 'decision', 'approved_by', 'rung', 'refs', 'reversible'], fmt: { id_block: (b) => `${esc(b.feature)}: ${esc(b.from)} to ${esc(b.to)}` } }, knowledge: { order: ['statement', 'kind', 'status', 'scope', 'confidence', 'valence', 'evidence'], fmt: { valence: (v) => (v < 0 ? `${esc(v)} (something to avoid)` : esc(v)) } }, problem: { order: ['title', 'status', 'severity', 'realized', 'threatens', 'assigned_to', 'detail', 'refs'], fmt: { realized: (v) => (v ? 'yes: an issue' : 'no: a risk'), assigned_to: (v) => linkify(v), severity: (v) => `${esc(v)}` } }, @@ -2836,9 +2836,16 @@

Changelog

${merged.length ? `
    ${merged.map((t) => `
  • ${tlink(t.id)} const k = (DATA.ooda.knowledge || []).find((x) => x.id === approvedBy); return k ? `

    The standing rule it rests on

    ${esc(k.statement)} · ${esc(scopeWords(k.scope))}
    ` : ''; } -// Records an object cites, in their own words (from its peek). +// The knowledge a decision rests on (decide --on), or a start's prompt carried, in its own words +// (from its peek): what the call or the session had to go on. +function knowledgeCited(p, title) { + const ks = p?.record?.fields?.knowledge || []; + return ks.length ? `

    ${esc(title)}

      ${ks.map((id) => { const x = p.cited?.[id]; return `
    • ${linkify(id)}${x ? ` ${esc(words(x.kind ?? x.type))}${x.text ? `: ${linkify(String(x.text).slice(0, 160))}` : ''}` : ''}
    • `; }).join('')}
    ` : ''; +} +// Records an object cites, in their own words (from its peek); what it rests on or was given has its own list. function citedBlock(p, o) { - const c = Object.entries(p?.cited || {}).filter(([id]) => !id.startsWith('q-') || !o?.approved_by || id !== o.approved_by); + const given = new Set(p?.record?.fields?.knowledge || []); + const c = Object.entries(p?.cited || {}).filter(([id]) => (!id.startsWith('q-') || !o?.approved_by || id !== o.approved_by) && !given.has(id)); return c.length ? `

    What it cites

      ${c.map(([id, x]) => `
    • ${linkify(id)} ${esc(words(x.type))}${x.kind ? ` (${esc(words(x.kind))})` : ''}${x.text ? `: ${linkify(String(x.text).slice(0, 160))}` : ''}
    • `).join('')}
    ` : ''; } function openDecision(id) { @@ -2855,6 +2862,7 @@

    Changelog

    ${merged.length ? `
      ${merged.map((t) => `
    • ${tlink(t.id)} ] })} ${lineageBlock(p)} ${restsOn(r.under?.approved_by)} + ${knowledgeCited(p, 'What it rests on')} ${r.kind === 'decision' ? `

      Carried out by

      ${acts.length ? `
        ${acts.map((a) => `
      1. ${a.what ? actView(a).head : esc(a.id)}
      2. `).join('')}
      ` : '

      Nothing yet.

      '}` : ''} ${r.correction ? '' : '

      To correct it, use Correct on it in Loop → Decide.

      '} ${historySection(p, id)}`); diff --git a/docs/devteam/schema/log.schema.json b/docs/devteam/schema/log.schema.json index f71d188..3c5fc01 100644 --- a/docs/devteam/schema/log.schema.json +++ b/docs/devteam/schema/log.schema.json @@ -1780,6 +1780,15 @@ "type": "string", "pattern": "^[qk]-" }, + "knowledge": { + "description": "The knowledge or problems this decision rests on (decide --on); never copied into refs, and never its authority (approved_by is).", + "type": "array", + "items": { + "type": "string", + "pattern": "^(k|p)-" + }, + "x-tip": "The lessons, facts or problems the call rests on." + }, "corrected": { "description": "Your correction, if the call was wrong; it drops the class to rung 1. On a goal-impact decision (constitutional, so no rung drops) it may carry the impact you give the goal instead.", "type": "object", diff --git a/docs/devteam/scripts/devteam.mjs b/docs/devteam/scripts/devteam.mjs index aef7f2c..7b15488 100644 --- a/docs/devteam/scripts/devteam.mjs +++ b/docs/devteam/scripts/devteam.mjs @@ -985,6 +985,9 @@ const RUNG = { 1: 'ask', 2: 'ask with a default', 3: 'act and report' }; const SCHEMA = readJson(path.join(kitDir, 'schema', 'log.schema.json')); const ENVELOPE = ['v', 'id', 'type', 'op', 'at', 'by', 'channel']; const LINE_MAX = 4096; +// The most knowledge ids a decision (--on) or a start (the lessons its prompt carried) records, so its +// line stays under LINE_MAX. +const KNOWLEDGE_IDS_MAX = 60; const TYPES = Object.keys(SCHEMA.$defs.types); const RANK = SCHEMA['x-authority'].rank; @@ -7242,12 +7245,24 @@ const citing = () => flagStr('decision') ?? undefined; function cmdDecide() { const what = rest ? rest.join(' ') : flagStr('what'); const cls = flagStr('class'); - if (!cls || !what) die('usage: decide --class c [--scope s] [--approved qid|kid] [--options "a" "b"] [--rationale "…"] [--confidence 0-1] [--refs ids] [--irreversible] -- ""'); + if (!cls || !what) die('usage: decide --class c [--scope s] [--approved qid|kid] [--options "a" "b"] [--rationale "…"] [--confidence 0-1] [--refs ids] [--on k-…|p-…] [--irreversible] -- ""'); supervisorOnly('decide'); if (cls === 'improvement-trial') upkeepAllows('a new trial'); let scope = flagStr('scope') ?? null; const approved = flagStr('approved') ?? null; const refs = asList(flags.refs); + // --on: the knowledge or problems the call rests on, a link for the lineage and the learning + // record, apart from refs (an act stays within refs) and from approved_by (authorise reads only + // that). Knowledge must be accepted: a candidate isn't the project's yet, and a retired one no + // longer is. An id in both would read as what the call is about and what it rests on at once. + const on = flags.on === true ? die('--on names the knowledge or problems the decision rests on: --on …') : asList(flags.on); + if (on.length > KNOWLEDGE_IDS_MAX) die(`--on takes at most ${KNOWLEDGE_IDS_MAX} ids (a log line is at most ${LINE_MAX} bytes): cite the ones the call turns on`); + for (const k of on) { + const o = logGet(k) ?? die(`--on ${k}: no such knowledge or problem`); + if (!['knowledge', 'problem'].includes(o.type)) die(`--on ${k} is ${o.type === 'observation' ? 'an' : 'a'} ${o.type}: a decision rests on knowledge or a problem (cite what it is about in --refs)`); + if (o.type === 'knowledge' && o.status !== 'accepted') die(`${k} is ${o.status}: a decision rests only on accepted knowledge`); + if (refs.includes(k)) die(`${k} is in both --refs and --on: --refs is what the decision is about, --on what it rests on`); + } // a remediation decision names the remediation it runs if (cls === 'production-remediation') { upkeepAllows('a remediation', { noForce: true }); @@ -7295,7 +7310,7 @@ function cmdDecide() { if (a.doorCited && a.approval.ticket !== refs[0] && !(features().get(tickets().get(refs[0])?.feature)?.goals ?? []).includes(a.approval.goal)) die(`${approved} answered a one-way door for ${a.approval.ticket ?? a.approval.goal ?? 'something else'}, not for ${refs[0]} or a goal its feature serves`); const line = appendLog({ type: 'decision', class: cls, scope: scope ?? undefined, what, rationale: flagStr('rationale'), options: flags.options !== undefined ? listFlag('options') : undefined, - confidence, refs, reversible: !flags.irreversible, rung: a.rung, ceiling: a.ceiling, authority: a.authority, approved_by: approved ?? undefined, + confidence, refs, reversible: !flags.irreversible, rung: a.rung, ceiling: a.ceiling, authority: a.authority, approved_by: approved ?? undefined, knowledge: on.length ? [...new Set(on)] : undefined, }, 'supervisor'); out(line.id); } @@ -14779,7 +14794,7 @@ function peekView(kind, id) { } else { const o = logGet(id) ?? die(`no record ${id}`); v.history = historyOf([id]); - v.cited = cited([...(o.refs ?? []), ...(o.evidence ?? []), ...(o.approved_by ? [o.approved_by] : [])]); + v.cited = cited([...(o.refs ?? []), ...(o.evidence ?? []), ...(o.approved_by ? [o.approved_by] : []), ...(Array.isArray(o.knowledge) ? o.knowledge : [])]); if (o.type === 'problem' || o.type === 'knowledge') v.related = logObjects('observation').filter((x) => (x.refs ?? []).includes(id) || x.subject === id || (o.evidence ?? []).includes(x.id)).sort(byTime).map((x) => ({ id: x.id, at: x.created_at, by: x.created_by, kind: x.kind, detail: x.detail, severity: x.severity ?? null })); // an incident: its view, what its fingerprints raised, and the latest event of each // (production data, scrubbed; the drawer frames it as untrusted) diff --git a/test/ooda-knowledge.test.mjs b/test/ooda-knowledge.test.mjs new file mode 100644 index 0000000..51fa3de --- /dev/null +++ b/test/ooda-knowledge.test.mjs @@ -0,0 +1,66 @@ +// Knowledge in decisions and prompts (the OODA spec, PR 4): a decision cites the knowledge or +// problems it rests on, a worker's or planner's start records the lessons its prompt carried, and a +// fixed incident nobody learned from is orientation debt until a lesson or a noise ruling cites it. +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fleetFixture, json, line } from './lib/kit.mjs'; + +const SUP = { DEVTEAM_ROLE: 'supervisor' }; +const stamp = (d) => `${d.toISOString().slice(0, 10).replace(/-/g, '')}-${d.toISOString().slice(11, 23).replace(/[:.]/g, '')}`; +// A line as the kit would have written it agoMs ago, in its writer's file. Ids follow the kit's +// form, so each line picks a distinct agoMs; lines go in time order. +function handFor(F) { + const logDir = path.join(F.old, 'docs/devteam/run/log'); + return (by, o, agoMs = 1000) => { + const d = new Date(Date.now() - agoMs); + const id = o.id ?? `${{ observation: 'o', problem: 'p', decision: 'd', act: 'a', knowledge: 'k' }[o.type]}-${stamp(d)}-${by}`; + const file = ['owner', 'supervisor'].includes(by) ? `${by}-${d.toISOString().slice(0, 7)}.jsonl` : `${by}.jsonl`; + fs.mkdirSync(logDir, { recursive: true }); + fs.appendFileSync(path.join(logDir, file), line({ op: 'new', ...(by === 'owner' ? { channel: 'cli' } : {}), ...o, id, at: d.toISOString(), by })); + return id; + }; +} +const peek = (F, kind, id) => json(F.ok(F.old, ['peek', kind, id, '--json'])); +const decisionOf = (F, id) => json(F.ok(F.old, ['log', '--type', 'decision', '--json'])).find((d) => d.id === id); + +test('decide --on: a decision cites the accepted knowledge or problems it rests on, apart from its refs', () => { + const F = fleetFixture('ooda-on'); + const { old, ok, run } = F; + try { + const o1 = ok(old, ['observe', '--subject', 'app', '--kind', 'note', '--detail', 'the registry has a scratch copy'], SUP).trim(); + const rule = ok(old, ['knowledge', 'add', '--kind', 'preference', '--statement', 'Keep scratch copies out of the registry', '--scope', 'project', '--owner']).trim(); + const cand = ok(old, ['knowledge', 'add', '--kind', 'lesson', '--statement', 'Scratch copies confuse workers', '--scope', 'project', '--evidence', o1], SUP).trim(); + const old1 = ok(old, ['knowledge', 'add', '--kind', 'fact', '--statement', 'The registry was hand-kept', '--scope', 'project', '--owner']).trim(); + ok(old, ['knowledge', 'set', old1, '--status', 'retired', '--owner']); + const p = ok(old, ['problem', 'add', '--title', 'registry drift', '--severity', '1'], SUP).trim(); + + // without --on, then with it: the same call, the same authority; knowledge stored, refs as given + const plain = ok(old, ['decide', '--class', 'registry-edit', '--refs', o1, '--', 'the registry stays as it is'], SUP).trim(); + const on = ok(old, ['decide', '--class', 'registry-edit', '--refs', o1, '--on', rule, p, '--', 'the registry stays as it is'], SUP).trim(); + const d0 = decisionOf(F, plain), d1 = decisionOf(F, on); + assert.equal(d0.knowledge, undefined, 'no --on, no field'); + assert.deepEqual(d1.knowledge, [rule, p]); + assert.deepEqual(d1.refs, [o1], 'never copied into refs'); + assert.equal(d1.authority, d0.authority, 'citing a standing rule with --on is not resting on it for authority'); + assert.equal(d1.approved_by, undefined); + // an act under it stays within its refs, as before + ok(old, ['act', '--decision', on, '--', 'left the registry as it is'], SUP); + assert.match(run(old, ['act', '--decision', on, '--refs', rule, '--', 'x'], SUP).err, /outside it/); + + // refused: a candidate, a retired lesson, an observation, an unknown id, one in both lists, a bare --on + assert.match(run(old, ['decide', '--class', 'registry-edit', '--refs', o1, '--on', cand, '--', 'x'], SUP).err, new RegExp(`${cand} is candidate: a decision rests only on accepted knowledge`)); + assert.match(run(old, ['decide', '--class', 'registry-edit', '--refs', o1, '--on', old1, '--', 'x'], SUP).err, new RegExp(`${old1} is retired: a decision rests only on accepted knowledge`)); + assert.match(run(old, ['decide', '--class', 'registry-edit', '--refs', o1, '--on', o1, '--', 'x'], SUP).err, /--on .*knowledge or a problem/); + assert.match(run(old, ['decide', '--class', 'registry-edit', '--refs', o1, '--on', 'k-nope', '--', 'x'], SUP).err, /--on k-nope: no such/); + assert.match(run(old, ['decide', '--class', 'registry-edit', '--refs', o1, p, '--on', p, '--', 'x'], SUP).err, new RegExp(`${p} is in both --refs and --on`)); + assert.match(run(old, ['decide', '--class', 'registry-edit', '--refs', o1, '--on', '--', 'x'], SUP).err, /--on names/); + + // lineage draws what it rests on before it + const L = peek(F, 'decision', on).lineage; + for (const k of [rule, p]) assert.ok(L.edges.some((e) => e.from === k && e.to === on && e.why === 'rests on'), `${k} → ${on}: ${JSON.stringify(L.edges)}`); + // the peek names them in their own words + assert.equal(peek(F, 'decision', on).cited[rule].text, 'Keep scratch copies out of the registry'); + } finally { F.done(); } +}); From 7593a734c630fbd2608c2a37eb459ac2ac00157f Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Wed, 11 Nov 2026 13:58:14 -0500 Subject: [PATCH 2/8] Starts record the knowledge they were given A lesson's effect can't be measured if nothing says which sessions were told it. A fresh worker or planner start now records, on its worker-slots act, the ids of the accepted knowledge its prompt carried (act.knowledge, top-level, newest first, at most 60 so the line fits), never in what, which LAUNCH_ACT parses. A resumed session reads no new prompt, so its act records none; an empty list is omitted. The act's sidepeek lists what its instructions carried. Co-Authored-By: Claude Opus 5.5 --- docs/devteam/dashboard/index.html | 3 +- docs/devteam/schema/log.schema.json | 9 +++++ docs/devteam/scripts/devteam.mjs | 6 +++- test/ooda-knowledge.test.mjs | 51 ++++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/docs/devteam/dashboard/index.html b/docs/devteam/dashboard/index.html index e4ac0d4..cfb9479 100644 --- a/docs/devteam/dashboard/index.html +++ b/docs/devteam/dashboard/index.html @@ -2194,7 +2194,7 @@

      ${esc(g.title fmt: { why_asked: (w) => `${w.rung ? `${esc(RUNG_WORDS[w.rung])}` : ''}${w.ceiling ? `, at most ${esc(RUNG_WORDS[w.ceiling])}` : ''}${(w.reasons || []).length ? ` · ${esc(w.reasons.map(words).join(', '))}` : ''}`, autonomy: (a) => `${esc(words(a.class))}: ${esc(RUNG_WORDS[a.rung])}${a.scope ? ` (${esc(a.scope)})` : ''}`, goal: (v) => glink(v), re: (v) => linkify(v) }, }, decision: { order: ['what', 'class', 'scope', 'authority', 'approved_by', 'knowledge', 'rung', 'ceiling', 'options', 'rationale', 'confidence', 'refs', 'reversible'], label: { knowledge: 'Rests on' }, fmt: { authority: (v) => esc(AUTHORITY_LABEL[v] || v), knowledge: linkList } }, - act: { order: ['what', 'class', 'scope', 'decision', 'approved_by', 'rung', 'refs', 'reversible'], fmt: { id_block: (b) => `${esc(b.feature)}: ${esc(b.from)} to ${esc(b.to)}` } }, + act: { order: ['what', 'class', 'scope', 'decision', 'approved_by', 'rung', 'refs', 'knowledge', 'reversible'], label: { knowledge: 'Given' }, fmt: { id_block: (b) => `${esc(b.feature)}: ${esc(b.from)} to ${esc(b.to)}`, knowledge: linkList } }, knowledge: { order: ['statement', 'kind', 'status', 'scope', 'confidence', 'valence', 'evidence'], fmt: { valence: (v) => (v < 0 ? `${esc(v)} (something to avoid)` : esc(v)) } }, problem: { order: ['title', 'status', 'severity', 'realized', 'threatens', 'assigned_to', 'detail', 'refs'], fmt: { realized: (v) => (v ? 'yes: an issue' : 'no: a risk'), assigned_to: (v) => linkify(v), severity: (v) => `${esc(v)}` } }, message: { fmt: { to: (v) => esc(who(v)), re: (v) => linkify(v) } }, @@ -2824,6 +2824,7 @@

      Changelog

      ${merged.length ? `
        ${merged.map((t) => `
      • ${tlink(t.id)} ] })} ${v.detail ? `

        ${v.detail}

        ` : ''} ${lineageBlock(p)} + ${knowledgeCited(p, 'What its instructions carried')} ${d ? `

        The decision it carried out

        ${dlink(d.id, d.what)}
        ${d.rationale ? `
        ${linkify(d.rationale)}
        ` : ''}
        ${authorityWords(d.under)} · ${esc(when(d.at))}${d.correction ? ` · corrected ${esc(d.correction.reason)}` : ''}
        ` : ''} ${citedBlock(p, a)} ${a.correction ? '' : a.decision ? '

        It carried out a decision: correct the decision if the call was wrong (Loop → Decide). If the call was sound and the act went wrong, that is a problem, not a trust drop.

        ' : '

        To correct it, use Correct on it in Loop → Act.

        '} diff --git a/docs/devteam/schema/log.schema.json b/docs/devteam/schema/log.schema.json index 3c5fc01..2011fa4 100644 --- a/docs/devteam/schema/log.schema.json +++ b/docs/devteam/schema/log.schema.json @@ -1876,6 +1876,15 @@ "pattern": "^d-", "x-tip": "The decision it carried out." }, + "knowledge": { + "description": "On a worker-slots act that started a worker or planner fresh: the accepted knowledge its prompt carried (the knowledge that applies to its ticket or feature), newest first, at most 60. Never in what, which the kit parses. A resumed session reads no new prompt, so its act carries none.", + "type": "array", + "items": { + "type": "string", + "pattern": "^k-" + }, + "x-tip": "The lessons and facts its instructions carried when it started." + }, "corrected": { "description": "Your correction, if it was its own call and wrong.", "type": "object", diff --git a/docs/devteam/scripts/devteam.mjs b/docs/devteam/scripts/devteam.mjs index 7b15488..218ed96 100644 --- a/docs/devteam/scripts/devteam.mjs +++ b/docs/devteam/scripts/devteam.mjs @@ -12025,6 +12025,10 @@ function makeSession(role) { seedWorktree(wt, { repo: R }); const promptFile = path.join(runDir, 'prompts', `${name}.txt`); const knowledge = knowledgeBlock(id); + // What the prompt's knowledge block carries, for the start's act (a lesson's effect is measured + // from it): only on a fresh start, since a resumed session never reads the new prompt; the newest + // first, as many as fit a line. + const given = resume ? [] : knowledgeFor(id).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, KNOWLEDGE_IDS_MAX).map((k) => k.id); const links = linksBlock(id); const arts = artifactsBlock(id); const held = block ?? (role === 'planner' ? idBlockFor(id) : null); @@ -12086,7 +12090,7 @@ function makeSession(role) { if (role === 'worker' && !resume) appendLog({ type: 'phase', from: null, to: 'starting', note: `started in ${shown}` }, id); // a planner parked by a park is running again if (role === 'planner' && lastPhaseOf(name)?.to === 'parked') appendLog({ type: 'phase', from: 'parked', to: 'starting', note: `started in ${shown}` }, name); - appendLog({ type: 'act', decision: citing(), class: 'worker-slots', what: `${resume ? 'resumed' : 'made'} ${role} ${name} in ${shown}${block ? `, ticket numbers ${padNN(block.from)}-${padNN(block.to)}` : ''}${refused ? ` while its goal is ${holdWords(mode)}, with --force` : ''}`, refs: role === 'worker' ? [id] : goalIds, reversible: true, id_block: block ?? undefined }, 'supervisor'); + appendLog({ type: 'act', decision: citing(), class: 'worker-slots', what: `${resume ? 'resumed' : 'made'} ${role} ${name} in ${shown}${block ? `, ticket numbers ${padNN(block.from)}-${padNN(block.to)}` : ''}${refused ? ` while its goal is ${holdWords(mode)}, with --force` : ''}`, refs: role === 'worker' ? [id] : goalIds, reversible: true, id_block: block ?? undefined, knowledge: given.length ? given : undefined }, 'supervisor'); out(`${resume ? 'resumed' : 'made'} ${role} ${name} in ${shown} at ${wt}${block ? `; ticket numbers ${padNN(block.from)}-${padNN(block.to)} reserved` : ''}`); }); } catch (e) { diff --git a/test/ooda-knowledge.test.mjs b/test/ooda-knowledge.test.mjs index 51fa3de..339159d 100644 --- a/test/ooda-knowledge.test.mjs +++ b/test/ooda-knowledge.test.mjs @@ -5,7 +5,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; -import { fleetFixture, json, line } from './lib/kit.mjs'; +import { fleetFixture, json, line, hasTmux } from './lib/kit.mjs'; const SUP = { DEVTEAM_ROLE: 'supervisor' }; const stamp = (d) => `${d.toISOString().slice(0, 10).replace(/-/g, '')}-${d.toISOString().slice(11, 23).replace(/[:.]/g, '')}`; @@ -64,3 +64,52 @@ test('decide --on: a decision cites the accepted knowledge or problems it rests assert.equal(peek(F, 'decision', on).cited[rule].text, 'Keep scratch copies out of the registry'); } finally { F.done(); } }); + +test('a start records the accepted knowledge its prompt carried: fresh starts only, newest first, at most 60', { skip: !hasTmux && 'no tmux' }, () => { + const F = fleetFixture('ooda-given'); + const { old, ok } = F; + const hand = handFor(F); + const ticket = (id, component) => ok(old, ['ticket', 'add', id, '--json', JSON.stringify({ title: id, feature: 'chores', component, kind: 'chore', size: 'S', risk: 'normal', serves: [], depends_on: [], touches: [`${component}/**`], done_when: ['the tidy check passes in CI'] })], SUP); + const starts = () => json(ok(old, ['log', '--type', 'act', '--json'])).filter((a) => a.class === 'worker-slots'); + const startOf = (name) => starts().filter((a) => a.what.includes(` ${name} in `)).pop(); + const rawLine = (id) => fs.readdirSync(path.join(old, 'docs/devteam/run/log')).flatMap((f) => fs.readFileSync(path.join(old, 'docs/devteam/run/log', f), 'utf8').split('\n')).find((l) => l.includes(`"id":"${id}"`)); + try { + // 61 accepted lessons for one component, written hours ago, in time order + const many = Array.from({ length: 61 }, (_, i) => hand('owner', { type: 'knowledge', kind: 'lesson', statement: `lesson ${i} for the mn component`, scope: 'component:mn', status: 'accepted', evidence: [] }, (100 - i) * 60000)); + ticket('ch-01-tidy', 'ch'); + ticket('ot-01-other', 'ot'); + ticket('mn-01-many', 'mn'); + const lesson = ok(old, ['knowledge', 'add', '--kind', 'lesson', '--statement', 'Run the tidy check before pushing', '--scope', 'component:ch', '--owner']).trim(); + ok(old, ['knowledge', 'add', '--kind', 'lesson', '--statement', 'Tidy checks are slow on Mondays', '--scope', 'component:ch', '--evidence', many[0]], SUP); // a candidate: not given + + // given: the one accepted lesson in its scope, in its prompt and on its act + ok(old, ['worker', 'ch-01-tidy'], SUP); + const a = startOf('ch-01-tidy'); + assert.match(a.what, /^made worker ch-01-tidy in /, 'what is as LAUNCH_ACT reads it'); + assert.deepEqual(a.knowledge, [lesson]); + assert.ok(fs.readFileSync(path.join(old, 'docs/devteam/run/prompts/ch-01-tidy.txt'), 'utf8').includes(`(${lesson})`)); + // none in scope: no field + ok(old, ['worker', 'ot-01-other'], SUP); + assert.equal('knowledge' in startOf('ot-01-other'), false); + assert.ok(!rawLine(startOf('ot-01-other').id).includes('"knowledge"'), 'omitted, not empty'); + // a resumed session never reads a new prompt: nothing recorded as given + ok(old, ['worker', 'ch-01-tidy', '--resume'], SUP); + const r = startOf('ch-01-tidy'); + assert.match(r.what, /^resumed worker ch-01-tidy in /); + assert.equal(r.knowledge, undefined); + // at most 60, newest first; the line stays under 4,096 bytes + ok(old, ['worker', 'mn-01-many'], SUP); + const m = startOf('mn-01-many'); + assert.equal(m.knowledge.length, 60); + assert.deepEqual(m.knowledge, many.slice(1).reverse()); + assert.ok(Buffer.byteLength(rawLine(m.id)) < 4096, `${Buffer.byteLength(rawLine(m.id))} bytes`); + // a planner's start too + const G = ok(old, ['goal', 'add', '--title', 'Tidy everything', '--outcome', 'nothing untidy', '--measure', 'untidy files', '--target', '0', '--unit', 'files', '--direction', 'down', '--source', 'prod', '--check', 'echo 0', '--non-goals', 'none', '--preconditions', 'a'], SUP).trim(); + const plan = ok(old, ['knowledge', 'add', '--kind', 'fact', '--statement', 'Plans name their goal', '--scope', 'project', '--owner']).trim(); + ok(old, ['planner', 'beta', '--goal', G], SUP); + assert.deepEqual(startOf('plan-beta').knowledge, [plan]); + // the lineage draws a lesson into the start it was given to + const L = peek(F, 'act', a.id).lineage; + assert.ok(L.edges.some((e) => e.from === lesson && e.to === a.id && e.why === 'rests on'), JSON.stringify(L.edges)); + } finally { F.done(); } +}); From df5b4c2e4b2abd6d402f792669fae89b54197190 Mon Sep 17 00:00:00 2001 From: Hugh Francis Date: Wed, 11 Nov 2026 14:01:47 -0500 Subject: [PATCH 3/8] Orientation debt: what fixed incidents taught A fast fix verified quiet closed its incident and taught nothing: no step asked what it showed. A resolved incident with an outcome citing it, in the last 30 days, that no knowledge cites in its evidence and no live noise ruling names, is now orientation debt: orient lists it after the recurring signals with the lesson and the noise ruling it could take (values quoted), dashboard-data carries it as ooda.debt.incidents, and the Orient tab lists it under the signals. A noise ruling may now name such a problem; a corrected one owes the lesson again, and an incident whose status the owner set owes nothing. The G1 golden gains only the empty incidents key. Co-Authored-By: Claude Opus 5.5 --- docs/devteam/dashboard/index.html | 8 ++++-- docs/devteam/scripts/devteam.mjs | 36 ++++++++++++++++++++---- test/fixtures/g1/dashboard-data.txt | 3 +- test/ooda-knowledge.test.mjs | 43 +++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/docs/devteam/dashboard/index.html b/docs/devteam/dashboard/index.html index cfb9479..43205c0 100644 --- a/docs/devteam/dashboard/index.html +++ b/docs/devteam/dashboard/index.html @@ -635,7 +635,7 @@

        Loop how the supervisor notices, makes sense of, decides a