messageKind knew only the tmux preamble, so a prompt opening with the T3 header [from: role (id) -> to: role (id)] counted as human in Table 2. The first line now matches either form; anything short of the full header stays human. Filbert approved R1 against the frozen hashes. Proof: packages/ledger/tests/ledger.test.mjs, 22/22; against HEAD's ledger.mjs it fails exactly the two new tests. No suite runs it. Eight suites green on the staged tree. The fix changes zero current counts: no Pi log under .pi/state contains a T3 header, and the ledger does not read T3 transcripts. Gate F waits on a T3 thread source. Co-Authored-By: Claude Opus 5.5 <[email protected]>
90 lines
5.9 KiB
Diff
90 lines
5.9 KiB
Diff
diff --git a/packages/ledger/README.md b/packages/ledger/README.md
|
|
index a998a76c..da0ab1c5 100644
|
|
--- a/packages/ledger/README.md
|
|
+++ b/packages/ledger/README.md
|
|
@@ -36,11 +36,15 @@ No install, build, service restart, or configuration change is needed.
|
|
duplicated entries in copied logs are not deduplicated. No transcript content
|
|
leaves the parser. Assistant messages and logs outside repo seats do not count.
|
|
Symlink source directories are refused and symlink files are not followed.
|
|
-- The first text line alone classifies a message. A bracketed addressing
|
|
- preamble whose source session is `control-board` is board; any other valid
|
|
- addressing preamble is agent; otherwise human. This is a format count, not
|
|
- proof of who typed the message. Text blocks are joined with newlines.
|
|
- The entry timestamp is used, falling back to the message timestamp.
|
|
+- The first text line alone classifies a message. Two addressing forms count:
|
|
+ the tmux preamble `[host:session -> host:session]` that `agent-send.sh`
|
|
+ writes, and the T3 header `[from: role (thread-id) -> to: role (thread-id)]`
|
|
+ from `docs/guides/T3-AGENT-COMMS.md`. Either may carry ` class=<class>` before
|
|
+ the closing bracket. A preamble whose sender is `control-board` (tmux session
|
|
+ or T3 role) is board; any other valid preamble is agent; otherwise human.
|
|
+ This is a format count, not proof of who typed the message. Text blocks are
|
|
+ joined with newlines. The entry timestamp is used, falling back to the
|
|
+ message timestamp.
|
|
- Seats with no in-range user messages are omitted. Issue seats come from `#N`
|
|
mentions anywhere in in-range user text, including quoted text.
|
|
- Human messages per closed issue divides Table 2's human sum by issues closed
|
|
diff --git a/packages/ledger/src/ledger.mjs b/packages/ledger/src/ledger.mjs
|
|
index dc8a3a69..b09e95f5 100644
|
|
--- a/packages/ledger/src/ledger.mjs
|
|
+++ b/packages/ledger/src/ledger.mjs
|
|
@@ -77,8 +77,12 @@ export function messageText(content) {
|
|
}
|
|
export function messageKind(text) {
|
|
const firstLine = text.split(/\r?\n/, 1)[0];
|
|
- const match = firstLine.match(/^\[([^\s:\[\]]+):([^\s\[\]]+) -> ([^\s:\[\]]+):([^\s\[\]]+)(?: class=[a-z-]+)?\](?:\s|$)/);
|
|
- return !match ? 'human' : match[2] === 'control-board' ? 'board' : 'agent';
|
|
+ // tmux preamble from agent-send.sh: [host:session -> host:session class=x]
|
|
+ const tmux = firstLine.match(/^\[([^\s:\[\]]+):([^\s\[\]]+) -> ([^\s:\[\]]+):([^\s\[\]]+)(?: class=[a-z-]+)?\](?:\s|$)/);
|
|
+ // T3 header (docs/guides/T3-AGENT-COMMS.md): [from: role (id) -> to: role (id) class=x]
|
|
+ const t3 = firstLine.match(/^\[from: ([^\s()\[\]]+) \(([^()\[\]]+)\) -> to: ([^\s()\[\]]+) \(([^()\[\]]+)\)(?: class=[a-z-]+)?\](?:\s|$)/);
|
|
+ const sender = tmux ? tmux[2] : t3 ? t3[1] : null;
|
|
+ return sender === null ? 'human' : sender === 'control-board' ? 'board' : 'agent';
|
|
}
|
|
async function directories(dir, optional = false) {
|
|
try {
|
|
diff --git a/packages/ledger/tests/ledger.test.mjs b/packages/ledger/tests/ledger.test.mjs
|
|
index 7b4e4d33..175f5d5e 100644
|
|
--- a/packages/ledger/tests/ledger.test.mjs
|
|
+++ b/packages/ledger/tests/ledger.test.mjs
|
|
@@ -110,6 +110,19 @@ test('invalid dates, reverse dates and duplicate options refuse', t => {
|
|
assert.throws(() => dateRange('2026-02-30')); assert.throws(() => dateRange('2026-09-12', '2026-09-06'));
|
|
const f = fixture(t); assert.equal(f.run(['--since', '2026-09-01']).status, 1);
|
|
});
|
|
+test('T3 agent assignments do not count as human in Table 2', t => {
|
|
+ const f = fixture(t);
|
|
+ f.put('.pi/state/bob/sessions/t3.jsonl', [
|
|
+ f.entry('[from: sage (1ef1e4f8) -> to: bob (9cb9731e) class=actionable]\nassign #1'),
|
|
+ f.entry('[from: sage (1ef1e4f8) -> to: bob (9cb9731e)]\nfollow-up #1'),
|
|
+ f.entry('Jason: go ahead'),
|
|
+ ].map(x => JSON.stringify(x)).join('\n') + '\n');
|
|
+ const result = f.run(['--json']);
|
|
+ assert.equal(result.status, 0, result.stderr);
|
|
+ const r = JSON.parse(result.stdout);
|
|
+ assert.deepEqual(r.seats, [{ seat: 'alice', board: 1, agent: 1, human: 1 }, { seat: 'bob', board: 0, agent: 2, human: 1 }]);
|
|
+ assert.equal(r.totals.humanMessagesPerClosedIssue, 2);
|
|
+});
|
|
test('preamble parsing and issue number boundaries', () => {
|
|
assert.equal(messageKind('[h:control-board -> h:seat] hi'), 'board');
|
|
assert.equal(messageKind('[h:seat -> h:seat class=actionable] hi'), 'agent');
|
|
@@ -117,6 +130,20 @@ test('preamble parsing and issue number boundaries', () => {
|
|
assert.equal(messageKind(' [h:seat -> h:seat] quoted'), 'human');
|
|
assert.deepEqual(issueNumbers('fix #1 #2 #2 abc#3 #0 #4x'), [1, 2]);
|
|
});
|
|
+test('T3 header: agent, or board from control-board; anything short of the full header is human', () => {
|
|
+ const sage = 'sage (1ef1e4f8-3ead-4208-beca-38f9f1add079)', filbert = 'filbert (9cb9731e-a10f-4c8f-a212-c4fa1f5f4731)';
|
|
+ assert.equal(messageKind(`[from: ${sage} -> to: ${filbert}]\nbuild #1506`), 'agent');
|
|
+ assert.equal(messageKind(`[from: ${sage} -> to: ${filbert} class=actionable]\nbuild`), 'agent');
|
|
+ assert.equal(messageKind(`[from: ${sage} -> to: ${filbert}] same line`), 'agent');
|
|
+ assert.equal(messageKind(`[from: darkwing (thread-id: unknown) -> to: reviewer (new-thread)]\nreview`), 'agent');
|
|
+ assert.equal(messageKind(`[from: control-board (b) -> to: ${filbert}]\nhi`), 'board');
|
|
+ assert.equal(messageKind(`Jason here\n[from: ${sage} -> to: ${filbert}]\nquoted`), 'human');
|
|
+ assert.equal(messageKind(` [from: ${sage} -> to: ${filbert}]`), 'human');
|
|
+ assert.equal(messageKind(`[from: sage -> to: filbert]\nno thread ids`), 'human');
|
|
+ assert.equal(messageKind(`[from: ${sage} -> to: ${filbert} class=Actionable]`), 'human');
|
|
+ assert.equal(messageKind(`[from: ${sage} -> to: ${filbert}]trailing`), 'human');
|
|
+ assert.equal(messageKind(`[From: ${sage} -> to: ${filbert}]`), 'human');
|
|
+});
|
|
test('no closed issues with human messages means undefined ratio, not invented zero', () => {
|
|
const r = summarize(range, [], [], { rows: [{ seat: 'a', human: 1, board: 0, agent: 0 }], mentions: new Map() });
|
|
assert.equal(r.totals.humanMessagesPerClosedIssue, 'unknown');
|