@paxnax/berliner 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/agents/claude.js +1 -1
- package/dist/agents/codex.js +25 -9
- package/dist/agents/read.js +44 -8
- package/dist/args.js +3 -0
- package/dist/client.js +4 -2
- package/dist/commands/body.js +22 -6
- package/dist/commands/hook.js +48 -5
- package/dist/commands/nudge.js +64 -0
- package/dist/commands/report.js +7 -3
- package/dist/commands/session.js +3 -0
- package/dist/commands/sessions.js +2 -0
- package/dist/commands/setup.js +4 -1
- package/dist/commands/team.js +21 -0
- package/dist/commands/update.js +6 -2
- package/dist/commands/upgrade.js +25 -13
- package/dist/config.js +17 -2
- package/dist/gh.js +6 -4
- package/dist/index.js +16 -2
- package/dist/setup/hooks.js +36 -18
- package/dist/team.js +99 -0
- package/dist/tests/check.js +108 -0
- package/dist/tests/command.js +72 -0
- package/dist/tests/nudge.js +68 -0
- package/dist/tests/runs.js +195 -0
- package/opencode/berliner.js +10 -2
- package/package.json +10 -1
- package/skill/SKILL.md +32 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { jsonl } from "../agents/read.js";
|
|
6
|
+
import { nudgeEnabled } from "../commands/nudge.js";
|
|
7
|
+
import { stateDir } from "../config.js";
|
|
8
|
+
import { checkTests } from "./check.js";
|
|
9
|
+
const run = promisify(execFile);
|
|
10
|
+
const BUDGET_MS = 500;
|
|
11
|
+
// The session's start without reading the whole record: both tools date their first entries.
|
|
12
|
+
async function firstTimestamp(file, deadline) {
|
|
13
|
+
for await (const e of jsonl(file)) {
|
|
14
|
+
if (Date.now() > deadline)
|
|
15
|
+
return null;
|
|
16
|
+
if (typeof e.timestamp === "string")
|
|
17
|
+
return e.timestamp;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
async function repoRoot(cwd, deadline) {
|
|
22
|
+
try {
|
|
23
|
+
return (await run("git", ["rev-parse", "--show-toplevel"], { cwd, timeout: Math.max(1, deadline - Date.now()) })).stdout.trim() || null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// The Stop hook's line asking the agent to run the tests, or null. Asked once per set of changes: the state is saved first.
|
|
30
|
+
export async function nudge(tool, p, env) {
|
|
31
|
+
const deadline = Date.now() + BUDGET_MS;
|
|
32
|
+
if (tool === "opencode" || p.stop_hook_active === true)
|
|
33
|
+
return null;
|
|
34
|
+
if (typeof p.transcript_path !== "string" || !p.transcript_path || typeof p.cwd !== "string")
|
|
35
|
+
return null;
|
|
36
|
+
// A reply ending in a question waits on the user; it doesn't hand the work back. Markdown may close after the ?: `?**`, `?)`.
|
|
37
|
+
const last = typeof p.last_assistant_message === "string" ? p.last_assistant_message.trimEnd().split("\n").at(-1) : "";
|
|
38
|
+
if (/\?[*_)"`]*$/.test(last ?? ""))
|
|
39
|
+
return null;
|
|
40
|
+
const root = await repoRoot(p.cwd, deadline);
|
|
41
|
+
if (!root || !nudgeEnabled(env, root))
|
|
42
|
+
return null;
|
|
43
|
+
const startedAt = await firstTimestamp(p.transcript_path, deadline);
|
|
44
|
+
const check = await checkTests({ tool, transcriptPath: p.transcript_path, cwd: p.cwd, root, startedAt, deadline, quick: true });
|
|
45
|
+
if (!check || check.verdict === "verified")
|
|
46
|
+
return null;
|
|
47
|
+
const dir = join(stateDir(env), "nudge");
|
|
48
|
+
const file = join(dir, `${p.session_id}.json`);
|
|
49
|
+
const state = JSON.stringify({ changed: check.changed, changedAt: check.changedAt });
|
|
50
|
+
if (existsSync(file) && readFileSync(file, "utf8") === state)
|
|
51
|
+
return null;
|
|
52
|
+
// The changed paths name the user's files: only the user may read them.
|
|
53
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
54
|
+
// shortcut: nudge state files are never removed, one small file per session, like the hook's payload files; prune old ones if they pile up.
|
|
55
|
+
writeFileSync(file, state, { mode: 0o600 });
|
|
56
|
+
const files = check.changed
|
|
57
|
+
.slice(0, 3)
|
|
58
|
+
.map((f) => `\`${f}\``)
|
|
59
|
+
.join(", ");
|
|
60
|
+
// No file changed since the last pass means the run after it failed.
|
|
61
|
+
const note = check.verdict === "zero_tests"
|
|
62
|
+
? `\`${check.command}\` ran no tests. Please check the test filter and run the full suite before handing this back.`
|
|
63
|
+
: !files
|
|
64
|
+
? `\`${check.command}\` failed on its last run. Please get it passing, or say why, before handing this back.`
|
|
65
|
+
: `This repo has tests and ${files} changed since they last passed. Please run \`${check.command}\` before handing this back.`;
|
|
66
|
+
// Codex expects JSON, not plain text, on Stop's stdout; block is how it continues the turn.
|
|
67
|
+
return JSON.stringify(tool === "codex" ? { decision: "block", reason: note } : { hookSpecificOutput: { hookEventName: "Stop", additionalContext: note } });
|
|
68
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { subagentFiles } from "../agents/claude.js";
|
|
5
|
+
import { jsonl, jsonlBackward } from "../agents/read.js";
|
|
6
|
+
// Words that change how a run reports, not which tests it runs. Kept short on purpose: anything else makes the run partial.
|
|
7
|
+
const FLAGS = new Set(["--", "--silent", "--color", "--no-color", "-v", "--verbose", "--coverage", "2>&1"]);
|
|
8
|
+
const ZERO = [
|
|
9
|
+
/No test files found/,
|
|
10
|
+
/No tests found/,
|
|
11
|
+
/\b0 tests\b/,
|
|
12
|
+
/\b0 passing\b/,
|
|
13
|
+
/no tests ran/,
|
|
14
|
+
/collected 0 items/,
|
|
15
|
+
/no tests to run/,
|
|
16
|
+
/Tests:\s+0 total/,
|
|
17
|
+
];
|
|
18
|
+
// Non-zero counts only: cargo prints "0 failed" when it passes and "0 passed" when nothing ran.
|
|
19
|
+
const FAIL_COUNT = /\b[1-9]\d* (?:failed|failing)\b/;
|
|
20
|
+
const PASS_COUNT = /\b[1-9]\d* (?:passed|passing)\b/;
|
|
21
|
+
// node:test prints `# pass N` and `# fail N` (TAP) or `ℹ pass N` (spec); TAP's per-test `ok N - name` is no summary, so Go's
|
|
22
|
+
// marker takes its package and time.
|
|
23
|
+
const FAILED = [FAIL_COUNT, /^FAIL\b/m, /test result: FAILED/, /^\s*not ok\b/m, /^[#ℹ] fail [1-9]/m];
|
|
24
|
+
const PASSED = [PASS_COUNT, /^ok\s+\S+\s+(?:[\d.]+s|\(cached\))\s*$/m, /^[#ℹ] pass [1-9]/m];
|
|
25
|
+
const real = (dir) => {
|
|
26
|
+
try {
|
|
27
|
+
return realpathSync(dir);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const sameDir = (a, b) => a === b || (real(a) ?? a) === real(b);
|
|
34
|
+
// A full run's shape, or null. Only a run in the repo root is full: in a monorepo, a workspace's own test script is part of
|
|
35
|
+
// the suite. The directory is the call's, moved by each `cd <dir>` before it; one it can't work out (none, `-`, `~`, a
|
|
36
|
+
// variable, quotes, pushd) makes the run partial. Piped: something after the test command owns the exit status, so only
|
|
37
|
+
// the output can tell.
|
|
38
|
+
function parse(command, main, cwd, root) {
|
|
39
|
+
const segments = command.split("&&");
|
|
40
|
+
const mainWords = main.split(" ");
|
|
41
|
+
let dir = cwd;
|
|
42
|
+
for (const [i, segment] of segments.entries()) {
|
|
43
|
+
const end = segment.search(/[|;\n]/);
|
|
44
|
+
const head = end < 0 ? segment : segment.slice(0, end);
|
|
45
|
+
const words = head.trim().split(/\s+/);
|
|
46
|
+
while (/^\w+=\S*$/.test(words[0] ?? ""))
|
|
47
|
+
words.shift();
|
|
48
|
+
if (words[0] === "cd" || words[0] === "pushd") {
|
|
49
|
+
const plain = words[0] === "cd" && words.length === 2 && /^[^-~$"'`\\][^$"'`\\]*$/.test(words[1]);
|
|
50
|
+
dir = dir && plain ? path.resolve(dir, words[1]) : undefined;
|
|
51
|
+
}
|
|
52
|
+
if (mainWords.every((w, j) => words[j] === w) && onlyFlags(words.slice(mainWords.length)) && dir && sameDir(dir, root)) {
|
|
53
|
+
// Splitting removed the &&s, so an & left over backgrounds the chain or redirects.
|
|
54
|
+
const tail = [segment.slice(head.length), ...segments.slice(i + 1)].join(" ").replaceAll("2>&1", "");
|
|
55
|
+
return { piped: /[|;&\n]/.test(tail) };
|
|
56
|
+
}
|
|
57
|
+
// A cd later in the step, after ; & || or a newline, moves the steps after it somewhere this doesn't follow.
|
|
58
|
+
if (/[;|&\n{]\s*(cd|pushd|popd)\b/.test(segment))
|
|
59
|
+
dir = undefined;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function onlyFlags(words) {
|
|
64
|
+
for (let k = 0; k < words.length; k++) {
|
|
65
|
+
if (words[k] === "--reporter")
|
|
66
|
+
k++;
|
|
67
|
+
else if (!FLAGS.has(words[k]) && !words[k].startsWith("--reporter="))
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
export const isFullRun = (command, main, cwd, root) => parse(command, main, cwd, root) !== null;
|
|
73
|
+
function outcome(output, isError, piped) {
|
|
74
|
+
// A "nothing ran" line only means zero when no part of the run counted a test: cargo's doc-tests print "running 0 tests" after the unit tests.
|
|
75
|
+
if (ZERO.some((r) => r.test(output)) && !PASS_COUNT.test(output) && !FAIL_COUNT.test(output))
|
|
76
|
+
return "zero";
|
|
77
|
+
if (!piped)
|
|
78
|
+
return isError === false ? "passed" : isError === true ? "failed" : "unknown";
|
|
79
|
+
if (FAILED.some((r) => r.test(output)))
|
|
80
|
+
return "failed";
|
|
81
|
+
return PASSED.some((r) => r.test(output)) ? "passed" : "unknown";
|
|
82
|
+
}
|
|
83
|
+
const text = (c) => typeof c === "string" ? c : Array.isArray(c) ? c.map((p) => (p?.type === "text" && typeof p.text === "string" ? p.text : "")).join("\n") : "";
|
|
84
|
+
const parts = (e) => (Array.isArray(e.message?.content) ? e.message.content.filter((p) => p && typeof p === "object") : []);
|
|
85
|
+
const time = (ts) => (typeof ts === "string" ? Date.parse(ts) : Number.NaN);
|
|
86
|
+
function bashCall(e, p, main, root) {
|
|
87
|
+
if (e.type !== "assistant" || p.type !== "tool_use" || p.name !== "Bash" || typeof p.input?.command !== "string")
|
|
88
|
+
return null;
|
|
89
|
+
const shape = parse(p.input.command, main, typeof e.cwd === "string" ? e.cwd : undefined, root);
|
|
90
|
+
return shape && { command: p.input.command, ...shape, background: p.input.run_in_background === true };
|
|
91
|
+
}
|
|
92
|
+
function toolResult(e, p) {
|
|
93
|
+
const at = time(e.timestamp);
|
|
94
|
+
return e.type === "user" && p.type === "tool_result" && !Number.isNaN(at) ? { at, content: p.content, isError: p.is_error } : null;
|
|
95
|
+
}
|
|
96
|
+
// A background call's result only says it started.
|
|
97
|
+
const run = (call, r) => ({
|
|
98
|
+
command: call.command,
|
|
99
|
+
at: r.at,
|
|
100
|
+
outcome: call.background ? "unknown" : outcome(text(r.content), r.isError, call.piped),
|
|
101
|
+
});
|
|
102
|
+
async function forward(file, main, root, deadline) {
|
|
103
|
+
const calls = new Map();
|
|
104
|
+
const runs = [];
|
|
105
|
+
for await (const e of jsonl(file)) {
|
|
106
|
+
if (Date.now() > deadline)
|
|
107
|
+
return null;
|
|
108
|
+
for (const p of parts(e)) {
|
|
109
|
+
const call = bashCall(e, p, main, root);
|
|
110
|
+
if (call)
|
|
111
|
+
calls.set(p.id, call);
|
|
112
|
+
const r = toolResult(e, p);
|
|
113
|
+
const made = r && calls.get(p.tool_use_id);
|
|
114
|
+
if (r && made)
|
|
115
|
+
runs.push(run(made, r));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return runs;
|
|
119
|
+
}
|
|
120
|
+
// Last entry first, so a result is seen before its call. Stops at the first entry older than since.
|
|
121
|
+
async function backward(file, main, root, deadline, since) {
|
|
122
|
+
const results = new Map();
|
|
123
|
+
const runs = [];
|
|
124
|
+
for await (const e of jsonlBackward(file)) {
|
|
125
|
+
if (Date.now() > deadline)
|
|
126
|
+
return null;
|
|
127
|
+
if (time(e.timestamp) < since)
|
|
128
|
+
break;
|
|
129
|
+
for (const p of parts(e)) {
|
|
130
|
+
const r = toolResult(e, p);
|
|
131
|
+
if (r)
|
|
132
|
+
results.set(p.tool_use_id, r);
|
|
133
|
+
const call = bashCall(e, p, main, root);
|
|
134
|
+
const got = call && results.get(p.id);
|
|
135
|
+
if (call && got)
|
|
136
|
+
runs.push(run(call, got));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return runs;
|
|
140
|
+
}
|
|
141
|
+
// The full runs of main in the repo root in the session and its subagents, in time order; null once Date.now() passes the
|
|
142
|
+
// deadline. With since (ms), only the runs from then on, read from the end of each file.
|
|
143
|
+
export async function claudeRuns(transcriptPath, main, root, deadline, since) {
|
|
144
|
+
const runs = [];
|
|
145
|
+
for (const file of [transcriptPath, ...(await subagentFiles(transcriptPath))]) {
|
|
146
|
+
const found = since === undefined ? await forward(file, main, root, deadline) : await backward(file, main, root, deadline, since);
|
|
147
|
+
if (!found)
|
|
148
|
+
return null;
|
|
149
|
+
runs.push(...found);
|
|
150
|
+
}
|
|
151
|
+
if (Date.now() > deadline)
|
|
152
|
+
return null;
|
|
153
|
+
return runs.sort((a, b) => a.at - b.at);
|
|
154
|
+
}
|
|
155
|
+
// Codex records the directory as a file URL.
|
|
156
|
+
function codexDir(cwd) {
|
|
157
|
+
if (typeof cwd !== "string")
|
|
158
|
+
return undefined;
|
|
159
|
+
try {
|
|
160
|
+
return cwd.startsWith("file:") ? fileURLToPath(cwd) : cwd;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// A finished command as a run of main, or null. Codex records the argv; the script is what the shell was given.
|
|
167
|
+
function codexRun(e, main, root) {
|
|
168
|
+
const item = e.payload?.item;
|
|
169
|
+
const at = time(e.timestamp);
|
|
170
|
+
if (e.type !== "event_msg" || e.payload?.type !== "item_completed" || item?.type !== "CommandExecution" || Number.isNaN(at))
|
|
171
|
+
return null;
|
|
172
|
+
const argv = item.command;
|
|
173
|
+
const command = Array.isArray(argv) && /^-\w*c$/.test(String(argv[1])) ? argv[2] : null;
|
|
174
|
+
const shape = typeof command === "string" ? parse(command, main, codexDir(item.cwd), root) : null;
|
|
175
|
+
if (!shape || typeof command !== "string")
|
|
176
|
+
return null;
|
|
177
|
+
// No exit status recorded: nothing says it passed or failed.
|
|
178
|
+
const code = item.exit_code;
|
|
179
|
+
const output = typeof item.aggregated_output === "string" ? item.aggregated_output : "";
|
|
180
|
+
return { command, at, outcome: outcome(output, typeof code === "number" ? code !== 0 : undefined, shape.piped) };
|
|
181
|
+
}
|
|
182
|
+
// The same for a Codex rollout, which has no subagent files.
|
|
183
|
+
export async function codexRuns(rolloutPath, main, root, deadline, since) {
|
|
184
|
+
const runs = [];
|
|
185
|
+
for await (const e of since === undefined ? jsonl(rolloutPath) : jsonlBackward(rolloutPath)) {
|
|
186
|
+
if (Date.now() > deadline)
|
|
187
|
+
return null;
|
|
188
|
+
if (since !== undefined && time(e.timestamp) < since)
|
|
189
|
+
break;
|
|
190
|
+
const r = codexRun(e, main, root);
|
|
191
|
+
if (r)
|
|
192
|
+
runs.push(r);
|
|
193
|
+
}
|
|
194
|
+
return runs.sort((a, b) => a.at - b.at);
|
|
195
|
+
}
|
package/opencode/berliner.js
CHANGED
|
@@ -63,14 +63,23 @@ async function withChildren(client, session, depth = 0) {
|
|
|
63
63
|
return { ...session, cost, tokens };
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Each save's place in the order saves started, and per session the latest one written: a save that finishes after a later
|
|
67
|
+
// one (past its 10-second deadline in serial) never writes, nor reports, its older totals.
|
|
68
|
+
let seq = 0;
|
|
69
|
+
const committed = new Map();
|
|
70
|
+
|
|
66
71
|
// Saves a top-level session with its subagents' totals where `berliner report` run by the agent finds them: its shell can't
|
|
67
|
-
// reach this client. Returns what it saved, or null for a child session, which the top-level session's totals count
|
|
72
|
+
// reach this client. Returns what it saved, or null for a child session, which the top-level session's totals count, and for
|
|
73
|
+
// a save a later one overtook.
|
|
68
74
|
async function save(client, sessionID) {
|
|
75
|
+
const mine = ++seq;
|
|
69
76
|
if (typeof sessionID !== "string" || !SAFE_ID.test(sessionID)) return null;
|
|
70
77
|
const found = await get(client, sessionID);
|
|
71
78
|
if (found?.id !== sessionID || found.parentID) return null;
|
|
72
79
|
// OpenCode moves time.updated only when a prompt arrives, so the session's last activity is now.
|
|
73
80
|
const session = { ...(await withChildren(client, found)), time: { ...found.time, updated: Date.now() } };
|
|
81
|
+
if ((committed.get(sessionID) ?? 0) > mine) return null;
|
|
82
|
+
committed.set(sessionID, mine);
|
|
74
83
|
const dir = join(process.env.XDG_STATE_HOME || join(homedir(), ".local", "state"), "berliner", "opencode");
|
|
75
84
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
76
85
|
const file = join(dir, `${sessionID}.json`);
|
|
@@ -83,7 +92,6 @@ async function save(client, sessionID) {
|
|
|
83
92
|
// One session's saves run one at a time, in the order they came, so an older total never overwrites a newer one. A save that
|
|
84
93
|
// hangs holds the next one up for 10 seconds at most.
|
|
85
94
|
// shortcut: only within this OpenCode process; two `berliner hook` processes can still write out of order, lock there if that shows up.
|
|
86
|
-
// shortcut: a save that resumes after its deadline can write older totals; the next save rewrites them.
|
|
87
95
|
const queues = new Map();
|
|
88
96
|
const serial = (id, fn) => {
|
|
89
97
|
const p = (queues.get(id) ?? Promise.resolve()).then(fn, fn);
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paxnax/berliner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Report coding-agent sessions to Berliner",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"codex",
|
|
8
|
+
"opencode",
|
|
9
|
+
"coding-agents",
|
|
10
|
+
"ai-cost-tracking",
|
|
11
|
+
"cli"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://berliner.app",
|
|
5
14
|
"type": "module",
|
|
6
15
|
"engines": {
|
|
7
16
|
"node": ">=22"
|
package/skill/SKILL.md
CHANGED
|
@@ -28,6 +28,7 @@ Hooks installed by `berliner setup` already report the session's cost, model, re
|
|
|
28
28
|
| `--pr` | Every pull request, one flag each. |
|
|
29
29
|
| `--coverage` | Percent, from the test run's coverage summary. |
|
|
30
30
|
| `--evidence` | Each screenshot or video. |
|
|
31
|
+
| `--team`, `--no-team` | Only when the user asks for a one-off. See [Teams](#teams). |
|
|
31
32
|
|
|
32
33
|
Do not pass `--session-id`, `--cost` or `--model`: the CLI fills them in from the tool's own records, and an explicit value is ignored with a warning. `--repo` and `--plan` are not needed either: the plan comes from the user's `BERLINER_PLAN`, or from their subscriptions in Settings. A report without `--title` keeps the session's existing title. `berliner update` ignores a session ID here.
|
|
33
34
|
|
|
@@ -86,6 +87,36 @@ In a non-interactive session (no user to answer), never ask and never save.
|
|
|
86
87
|
|
|
87
88
|
The repository comes from `git remote get-url origin`; `--repo owner/name` names another one.
|
|
88
89
|
|
|
90
|
+
## Teams
|
|
91
|
+
|
|
92
|
+
A session reports to the user's own plan unless a team is set. The CLI picks the team from the first of these that exists:
|
|
93
|
+
|
|
94
|
+
| Source | Use it for |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `--team <uuid>` or `--no-team` | One report. `--no-team` beats every other source. |
|
|
97
|
+
| `team` in `.berliner.json` at the repository root: `{ "team": "<uuid>" }` | The whole team. The file is meant to be committed. |
|
|
98
|
+
| `BERLINER_TEAM` | One machine or shell. |
|
|
99
|
+
| `berliner team <uuid>` | One machine, saved globally. `berliner team --none` clears it; `berliner team` prints the team in effect and where it came from. |
|
|
100
|
+
|
|
101
|
+
With none of them, the session goes to the user's own plan. Do not set a team yourself: the user or the repository already did.
|
|
102
|
+
|
|
103
|
+
Only the report that creates a session sends the team. A session's team is fixed after that, and the CLI warns when a later setting differs. Ignore the warning.
|
|
104
|
+
|
|
105
|
+
The server can refuse the report that creates a team session with a 422. The message says how to fix it:
|
|
106
|
+
|
|
107
|
+
| Code | Meaning | Fix |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `team_not_found` | No team has that uuid. | Check the uuid in `.berliner.json`, `BERLINER_TEAM` or the global setting. |
|
|
110
|
+
| `team_not_member` | The user is not a member of the team. | The user accepts the team's invite in the web app, or the owner invites them. |
|
|
111
|
+
| `team_not_eligible` | The repository's owner is not an organization the team covers. | The team's owner adds the organization in the web app, or the user reports this repository without the team. |
|
|
112
|
+
| `team_inactive` | The team has no active plan. | The team's owner sets up the plan in the web app. |
|
|
113
|
+
|
|
114
|
+
When a report is refused, tell the user in your next message and quote the message the CLI printed. Inside Claude Code, Codex or OpenCode the hook cannot print it: it saves a note, and the next `berliner report`, `update`, `session` or `sessions` in that repository prints it once. The web app shows it as a banner too.
|
|
115
|
+
|
|
116
|
+
Never add `--no-team` or a different `--team` just to get past a refusal unless the user agrees. That moves the session to another plan.
|
|
117
|
+
|
|
118
|
+
The team's owner sees every session and file reported to the team. Members never see each other's team work.
|
|
119
|
+
|
|
89
120
|
## Keep PR states current
|
|
90
121
|
|
|
91
122
|
A PR in a repository without the Berliner GitHub App keeps the state you last reported. So:
|
|
@@ -119,6 +150,7 @@ Report again with the same `--session-id` whenever something changes.
|
|
|
119
150
|
- `--public`: with `--evidence`, also make those files viewable without sign-in. See below.
|
|
120
151
|
- `--task <id>`, `--task-url <url>`: the ticket. See [Tickets](#tickets).
|
|
121
152
|
- `--project <name>`: only the ticket's tracker project or a project the user named. See [Projects](#projects).
|
|
153
|
+
- `--team <uuid>`, `--no-team`: which plan the session reports to. See [Teams](#teams).
|
|
122
154
|
- `--started-at <ISO>`, `--ended-at <ISO>`.
|
|
123
155
|
|
|
124
156
|
Quote any value that contains spaces.
|