Skip to content

Commit 032143f

Browse files
committed
Add Burn persona attribution to CLI launches
1 parent 877d04b commit 032143f

11 files changed

Lines changed: 605 additions & 23 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ corepack pnpm --filter agentworkforce link --global
204204
### Usage
205205

206206
```
207-
agentworkforce create [--to <target>] [--save-default]
208-
agentworkforce agent <persona>[@<tier>]
207+
agentworkforce create [--to <target>] [--save-default] [--install-in-repo] [--no-burn]
208+
agentworkforce agent [--install-in-repo] [--no-burn] <persona>[@<tier>]
209209
agentworkforce list [flags]
210210
agentworkforce install [flags] <pkg|path>
211211
agentworkforce sources <list|add|remove>
@@ -223,6 +223,8 @@ agentworkforce --version
223223
2. Configured persona source dirs. Default:
224224
`~/.agentworkforce/workforce/personas/*.json`
225225
3. Built-in personas in `/personas/`
226+
- Burn attribution tags are written by default through `@relayburn/sdk`;
227+
opt out with `--no-burn` or `AGENTWORKFORCE_BURN=0`.
226228
- `list` — print the catalog of personas from the cascade (cwd →
227229
configured dirs → library). Columns: persona, source, harness, model,
228230
rating, description.
@@ -260,6 +262,18 @@ export POSTHOG_API_KEY=phx_…
260262
agentworkforce agent posthog@best
261263
```
262264

265+
Persona launches are attributed by default when the installed `@relayburn/sdk`
266+
supports launcher tagging. AgentWorkforce stamps generic Burn tags such as
267+
`agentworkforce=1`, `persona=<id>`, `personaTier=<tier>`,
268+
`personaVersion=<sha256>`, and `personaSource=<cwd|user|dir:n|library>`.
269+
Use `--no-burn` or `AGENTWORKFORCE_BURN=0` to skip stamp writing and ingest.
270+
Query the ledger with Burn's generic tag flags:
271+
272+
```bash
273+
burn summary --tag persona=code-reviewer
274+
burn summary --group-by-tag persona
275+
```
276+
263277
### Persona pack installs
264278

265279
Install a persona pack into the current project:

‎packages/agentworkforce/CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `agentworkforce agent` now records default Burn attribution tags through
13+
the CLI package; opt out with `--no-burn` or `AGENTWORKFORCE_BURN=0`.
1214
- `agentworkforce create` now opens `persona-maker@best`, supports target
1315
selection, and passes `TARGET_DIR` / `CREATE_MODE` persona inputs.
1416
- `agentworkforce --version` now prints the installed package version.

‎packages/agentworkforce/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ npm i -g agentworkforce
99
Then:
1010

1111
```sh
12-
agentworkforce create [--to <target>] [--save-default]
13-
agentworkforce agent <persona>[@<tier>]
12+
agentworkforce create [--to <target>] [--save-default] [--install-in-repo] [--no-burn]
13+
agentworkforce agent [--install-in-repo] [--no-burn] <persona>[@<tier>]
1414
agentworkforce list [flags]
1515
agentworkforce show <persona>
1616
agentworkforce sources <list|add|remove>

‎packages/cli/CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `agentworkforce agent` now wraps direct harness launches with default Burn
13+
attribution tags via `@relayburn/sdk`; opt out with `--no-burn` or
14+
`AGENTWORKFORCE_BURN=0`.
1215
- `agentworkforce create` now opens `persona-maker@best`, supports `--to` and
1316
`--save-default`, and passes `TARGET_DIR` / `CREATE_MODE` persona inputs.
1417
- Persona source config supports `defaultCreateTarget` for the implicit create target.

‎packages/cli/README.md‎

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ built-in one from `/personas/`, or an installed/local one that extends a lower
66
source.
77

88
```
9-
agentworkforce create [--to <target>] [--save-default]
10-
agentworkforce agent <persona>[@<tier>]
9+
agentworkforce create [--to <target>] [--save-default] [--install-in-repo] [--no-burn]
10+
agentworkforce agent [--install-in-repo] [--no-burn] <persona>[@<tier>]
1111
agentworkforce list [flags]
1212
agentworkforce show <persona>[@<tier>]
1313
agentworkforce install [flags] <pkg|path>
@@ -68,7 +68,7 @@ Unknown persona prints the full catalog with each entry's origin.
6868
## Create
6969

7070
```
71-
agentworkforce create [--to <target>] [--save-default] [--install-in-repo]
71+
agentworkforce create [--to <target>] [--save-default] [--install-in-repo] [--no-burn]
7272
```
7373

7474
`create` is the persona-authoring entry point. It runs `persona-maker@best`
@@ -860,7 +860,7 @@ persona session, add it to the persona's `mcpServers` block.
860860
## Interactive
861861

862862
```sh
863-
agentworkforce agent [--install-in-repo] <persona>[@<tier>]
863+
agentworkforce agent [--install-in-repo] [--no-burn] <persona>[@<tier>]
864864
```
865865

866866
By default, claude and opencode sessions run inside a sandbox mount — see
@@ -887,10 +887,44 @@ By default, claude and opencode sessions run inside a sandbox mount — see
887887
initial argument.
888888
5. Runs the skill cleanup command on exit, regardless of exit status. In
889889
stage-dir mode this is a single `rm -rf <stage-dir>`.
890-
6. Propagates the harness's exit code.
890+
6. Writes generic Burn attribution tags through `@relayburn/sdk`, ingests
891+
harness session logs while the child runs, and runs a final ingest after
892+
exit. The harness is still launched directly; AgentWorkforce does not call
893+
`burn run`.
894+
7. Propagates the harness's exit code.
891895

892896
Signals (SIGINT, SIGTERM) are forwarded to the child.
893897

898+
### Burn attribution
899+
900+
Persona launches are attributed by default when the installed `@relayburn/sdk`
901+
supports launcher tagging. AgentWorkforce stamps generic enrichment tags:
902+
`agentworkforce=1`, `persona=<id>`, `personaTier=<tier>`,
903+
`personaVersion=<sha256>`, and `personaSource=<cwd|user|dir:n|library>`.
904+
`personaVersion` is the SHA-256 of the fully resolved persona spec after
905+
cascade/extends merge and before prompt input substitution.
906+
907+
Opt out for one launch:
908+
909+
```sh
910+
agentworkforce agent --no-burn code-reviewer@best
911+
```
912+
913+
Opt out through the environment:
914+
915+
```sh
916+
AGENTWORKFORCE_BURN=0 agentworkforce agent code-reviewer@best
917+
```
918+
919+
After Burn's generic tag summary support is available, query the ledger with
920+
tag filters rather than persona-specific Burn commands:
921+
922+
```sh
923+
burn summary --tag persona=code-reviewer
924+
burn summary --group-by-tag persona
925+
burn summary --tag agentworkforce=1 --group-by-tag personaTier
926+
```
927+
894928
## Skill staging
895929

896930
By default, interactive `claude` sessions stage skills under the user's home

‎packages/cli/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"dependencies": {
1313
"@agentworkforce/harness-kit": "workspace:*",
1414
"@agentworkforce/workload-router": "workspace:*",
15+
"@relayburn/sdk": "^2.3.0",
1516
"@relayfile/local-mount": "^0.6.1",
1617
"ora": "^9.4.0"
1718
},
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import { setTimeout as sleep } from 'node:timers/promises';
4+
5+
import type { PersonaSelection, PersonaSpec } from '@agentworkforce/workload-router';
6+
import {
7+
buildBurnEnrichment,
8+
burnIngestHarness,
9+
canonicalJson,
10+
personaVersionHash,
11+
shouldEnableBurnAttribution,
12+
startBurnAttribution,
13+
type BurnIngestOptions,
14+
type BurnPendingStampOptions
15+
} from './burn-attribution.js';
16+
17+
function fakeSelection(): Pick<PersonaSelection, 'personaId' | 'tier' | 'runtime'> {
18+
return {
19+
personaId: 'code-reviewer',
20+
tier: 'best',
21+
runtime: {
22+
harness: 'codex',
23+
model: 'openai-codex/gpt-5.3-codex',
24+
systemPrompt: 'Review the diff.',
25+
harnessSettings: {
26+
reasoning: 'high',
27+
timeoutSeconds: 1200
28+
}
29+
}
30+
};
31+
}
32+
33+
function fakeSpec(overrides: Partial<PersonaSpec> = {}): PersonaSpec {
34+
return {
35+
id: 'code-reviewer',
36+
intent: 'review',
37+
tags: ['review'],
38+
description: 'Reviews code.',
39+
skills: [],
40+
tiers: {
41+
best: fakeSelection().runtime,
42+
'best-value': {
43+
harness: 'opencode',
44+
model: 'opencode/gpt-5-nano',
45+
systemPrompt: 'Review concisely.',
46+
harnessSettings: {
47+
reasoning: 'medium',
48+
timeoutSeconds: 900
49+
}
50+
},
51+
minimum: {
52+
harness: 'opencode',
53+
model: 'opencode/minimax-m2.5-free',
54+
systemPrompt: 'Review blockers.',
55+
harnessSettings: {
56+
reasoning: 'low',
57+
timeoutSeconds: 600
58+
}
59+
}
60+
},
61+
...overrides
62+
};
63+
}
64+
65+
test('personaVersionHash canonicalizes object keys and changes with effective content', () => {
66+
assert.equal(canonicalJson({ b: 1, a: { d: 4, c: 3 } }), '{"a":{"c":3,"d":4},"b":1}');
67+
68+
const left = {
69+
id: 'p',
70+
tiers: {
71+
best: { model: 'm', harness: 'codex' }
72+
},
73+
tags: ['review']
74+
};
75+
const right = {
76+
tags: ['review'],
77+
tiers: {
78+
best: { harness: 'codex', model: 'm' }
79+
},
80+
id: 'p'
81+
};
82+
assert.equal(personaVersionHash(left), personaVersionHash(right));
83+
assert.notEqual(personaVersionHash(left), personaVersionHash({ ...right, tags: ['testing'] }));
84+
});
85+
86+
test('buildBurnEnrichment emits the required AgentWorkforce tags', () => {
87+
const spec = fakeSpec();
88+
const tags = buildBurnEnrichment({
89+
selection: fakeSelection(),
90+
personaSpec: spec,
91+
personaSource: 'dir:1'
92+
});
93+
94+
assert.deepEqual(tags, {
95+
agentworkforce: '1',
96+
persona: 'code-reviewer',
97+
personaTier: 'best',
98+
personaVersion: personaVersionHash(spec),
99+
personaSource: 'dir:1'
100+
});
101+
});
102+
103+
test('startBurnAttribution writes a pending stamp and runs periodic plus final ingest', async () => {
104+
const stamps: BurnPendingStampOptions[] = [];
105+
const ingests: BurnIngestOptions[] = [];
106+
const run = await startBurnAttribution({
107+
selection: fakeSelection(),
108+
personaSpec: fakeSpec(),
109+
personaSource: 'cwd',
110+
cwd: '/tmp/project',
111+
intervalMs: 5,
112+
now: () => new Date('2026-05-08T12:00:00.000Z'),
113+
sdk: {
114+
writePendingStamp: (opts) => {
115+
stamps.push(opts);
116+
},
117+
ingest: (opts) => {
118+
if (opts) ingests.push(opts);
119+
}
120+
}
121+
});
122+
123+
assert.equal(run.enabled, true);
124+
await sleep(25);
125+
await run.stop();
126+
127+
assert.equal(stamps.length, 1);
128+
assert.equal(stamps[0]?.harness, 'codex');
129+
assert.equal(stamps[0]?.cwd, '/tmp/project');
130+
assert.equal(stamps[0]?.spawnStartTs, '2026-05-08T12:00:00.000Z');
131+
assert.equal(stamps[0]?.enrichment.persona, 'code-reviewer');
132+
assert.ok(ingests.length >= 2, 'expected at least one periodic ingest plus final ingest');
133+
assert.deepEqual(ingests.at(-1), { harness: 'codex' });
134+
});
135+
136+
test('startBurnAttribution skips SDK loading and ingest when opted out', async () => {
137+
let loads = 0;
138+
const run = await startBurnAttribution({
139+
selection: fakeSelection(),
140+
personaSpec: fakeSpec(),
141+
personaSource: 'library',
142+
cwd: '/tmp/project',
143+
noBurn: true,
144+
sdk: async () => {
145+
loads += 1;
146+
throw new Error('should not load');
147+
}
148+
});
149+
150+
await run.stop();
151+
assert.equal(run.enabled, false);
152+
assert.equal(loads, 0);
153+
assert.equal(
154+
shouldEnableBurnAttribution({ env: { AGENTWORKFORCE_BURN: '0' } }),
155+
false
156+
);
157+
});
158+
159+
test('burnIngestHarness maps AgentWorkforce claude to Burn claude-code', () => {
160+
assert.equal(burnIngestHarness('claude'), 'claude-code');
161+
assert.equal(burnIngestHarness('codex'), 'codex');
162+
assert.equal(burnIngestHarness('opencode'), 'opencode');
163+
});

0 commit comments

Comments
 (0)