breakaway 1.4.0-main.53 → 1.4.0-main.54
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/package.json +2 -1
- package/scripts/tasks.mjs +18 -1
- package/src/session-report.js +92 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "1.4.0-main.
|
|
3
|
+
"version": "1.4.0-main.54",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"src/prompt.js",
|
|
31
31
|
"src/redact.js",
|
|
32
32
|
"src/repos.js",
|
|
33
|
+
"src/session-report.js",
|
|
33
34
|
"src/specs.js",
|
|
34
35
|
"src/versions.js",
|
|
35
36
|
"prompts/*.md",
|
package/scripts/tasks.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
textFields,
|
|
40
40
|
} from './tasks/structure.js';
|
|
41
41
|
import { looksLikeSecret } from '../src/ping.js';
|
|
42
|
+
import { sessionReport, shortHash, stubText } from '../src/session-report.js';
|
|
42
43
|
import { promptPathOf } from '../src/repos.js';
|
|
43
44
|
import { hookFailure, sessionProxy, routeThroughSessionProxy } from './tasks/proxy.js';
|
|
44
45
|
import { githubFromRemote, inRepo, pickRepo } from './tasks/repo.js';
|
|
@@ -623,6 +624,18 @@ async function startSessionLog(t) {
|
|
|
623
624
|
);
|
|
624
625
|
}
|
|
625
626
|
|
|
627
|
+
/**
|
|
628
|
+
* In a cloud session, what its claim tells the board about its environment, so the routine that started it reads
|
|
629
|
+
* Verified on Connections (BRK-142): whether a name was set, where the token came from, and the hash of this
|
|
630
|
+
* checkout's copy of the stub. Never a value. Null outside a cloud session.
|
|
631
|
+
*/
|
|
632
|
+
async function environmentReport() {
|
|
633
|
+
if (process.env.CLAUDE_CODE_REMOTE !== 'true') return null;
|
|
634
|
+
const path = ['tools/tasks/prompts/stub.md', 'prompts/stub.md'].map((p) => join(REPO, p)).find((p) => existsSync(p));
|
|
635
|
+
const stub = path ? await shortHash(stubText(readFileSync(path, 'utf8'))) : null;
|
|
636
|
+
return sessionReport({ env: process.env, file: fileEnv, named: Boolean(opts.as), stub });
|
|
637
|
+
}
|
|
638
|
+
|
|
626
639
|
/** The session hook couldn't post this checkout's live output: say so where the agent and the owner see it (BRK-86). */
|
|
627
640
|
function warnHookFailure() {
|
|
628
641
|
try {
|
|
@@ -1250,10 +1263,12 @@ const commands = {
|
|
|
1250
1263
|
async claim() {
|
|
1251
1264
|
// The board refuses a task of another repository than the one sent (--all doesn't widen a claim).
|
|
1252
1265
|
const { slug: repo } = await checkoutRepo();
|
|
1266
|
+
const session = await environmentReport();
|
|
1253
1267
|
const { task } = await call('POST', `tasks/${enc(need(args[0], 'task'))}/claim`, {
|
|
1254
1268
|
agent: agent(),
|
|
1255
1269
|
force: Boolean(opts.force),
|
|
1256
1270
|
...(repo ? { repo } : {}),
|
|
1271
|
+
...(session ? { session } : {}),
|
|
1257
1272
|
});
|
|
1258
1273
|
markSession(task);
|
|
1259
1274
|
await startSessionLog(task);
|
|
@@ -1539,6 +1554,8 @@ const commands = {
|
|
|
1539
1554
|
/** Every connection with its state and, for each that isn't working, the fix (IDEA-14). Read only. */
|
|
1540
1555
|
async connections() {
|
|
1541
1556
|
const STATE = { working: 'Working', attention: 'Needs attention', off: 'Not connected' };
|
|
1557
|
+
// A routine's row says whether a session it started has reported back (BRK-142).
|
|
1558
|
+
const READING = { verified: 'Verified', unverified: 'Not verified yet' };
|
|
1542
1559
|
const GROUP = {
|
|
1543
1560
|
repos: 'Repositories',
|
|
1544
1561
|
cloudflare: 'Cloudflare',
|
|
@@ -1568,7 +1585,7 @@ const commands = {
|
|
|
1568
1585
|
out.push('', GROUP[group] ?? group);
|
|
1569
1586
|
}
|
|
1570
1587
|
const when = c.at ? ` (${c.at.slice(0, 16).replace('T', ' ')})` : '';
|
|
1571
|
-
out.push(` ${STATE[c.state].padEnd(16)} ${c.name}: ${c.detail}${when}`);
|
|
1588
|
+
out.push(` ${(READING[c.reading] ?? STATE[c.state] ?? c.state).padEnd(16)} ${c.name}: ${c.detail}${when}`);
|
|
1572
1589
|
if (c.fix) out.push(` ${''.padEnd(16)} Fix: ${c.fix}`);
|
|
1573
1590
|
if (c.fix && c.link) out.push(` ${''.padEnd(16)} ${c.link}`);
|
|
1574
1591
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cloud session's report on its own environment (BRK-142, docs/specs/IDEA-33-onboarding-hardening.md): the CLI
|
|
3
|
+
* sends it with a board-started agent's claim, and the routine that started the session reads Verified on
|
|
4
|
+
* Connections. Only yes/no facts and a hash of a public file are sent, never a value: whether a name was set,
|
|
5
|
+
* where the token came from, and the sha-256 of the checkout's copy of the stub. Pure, so it's tested without a
|
|
6
|
+
* Worker; the CLI builds a report with `sessionReport`, the board checks it with `checkReport`
|
|
7
|
+
* and `judgeStub` when it arrives, and reads it back with `reportProblems`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Where the session's token came from: the cloud environment's API credential, a variable, or a tasks.env file. */
|
|
11
|
+
export const TOKEN_SOURCES = Object.freeze(['credential', 'variable', 'file']);
|
|
12
|
+
|
|
13
|
+
/** The stub's text the way it's hashed on both sides: line endings and trailing space don't count. */
|
|
14
|
+
export const stubText = (text) => String(text).replace(/\r\n/gu, '\n').trim();
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The report a cloud session's CLI sends with its claim, or null outside a cloud session. `env` is the environment,
|
|
18
|
+
* `file` the tasks.env settings, `named` whether --as named the agent, and `stub` the hash of the checkout's
|
|
19
|
+
* stub (null when the checkout has none).
|
|
20
|
+
* @param {{ env: Record<string, string | undefined>, file?: Record<string, string>, named?: boolean, stub?: string | null }} input
|
|
21
|
+
*/
|
|
22
|
+
export function sessionReport({ env, file = {}, named = false, stub = null }) {
|
|
23
|
+
if (env.CLAUDE_CODE_REMOTE !== 'true') return null;
|
|
24
|
+
const token = env.BREAKAWAY_TOKEN ? 'variable' : file.BREAKAWAY_TOKEN ? 'file' : 'credential';
|
|
25
|
+
return { token, agent: Boolean(named || env.BREAKAWAY_AGENT || file.BREAKAWAY_AGENT), stub: stub ?? null };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The report as the board keeps it, or null when it isn't one: only the known facts, so nothing else a client
|
|
30
|
+
* sends is ever stored.
|
|
31
|
+
* @returns {{ token: string, agent: boolean, stub: string | null } | null}
|
|
32
|
+
*/
|
|
33
|
+
export function checkReport(input) {
|
|
34
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
|
|
35
|
+
if (!TOKEN_SOURCES.includes(input.token) || typeof input.agent !== 'boolean') return null;
|
|
36
|
+
const stub = typeof input.stub === 'string' && /^[0-9a-f]{16}$/u.test(input.stub) ? input.stub : null;
|
|
37
|
+
return { token: input.token, agent: input.agent, stub };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The checkout's stub against the board's, both hashes: `same`, `different`, or `missing` (the checkout has none),
|
|
42
|
+
* and null when the board has no copy to compare with, so the stub isn't judged.
|
|
43
|
+
* @returns {'same' | 'different' | 'missing' | null}
|
|
44
|
+
*/
|
|
45
|
+
export function judgeStub(stub, boardStub) {
|
|
46
|
+
if (!boardStub) return null;
|
|
47
|
+
if (!stub) return 'missing';
|
|
48
|
+
return stub === boardStub ? 'same' : 'different';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What's wrong in a kept report, each with its fix: no agent name, the token in a variable or a file instead of
|
|
53
|
+
* the API credential, and a stub that's missing from the checkout or differs from the board's (`stub`, judgeStub's
|
|
54
|
+
* verdict). `slug` names the repository and `host` the board in the fixes.
|
|
55
|
+
* @param {{ token: string, agent: boolean, stub: string | null }} report
|
|
56
|
+
* @param {{ slug: string, host: string }} board
|
|
57
|
+
* @returns {{ what: string, fix: string }[]}
|
|
58
|
+
*/
|
|
59
|
+
export function reportProblems(report, { slug, host }) {
|
|
60
|
+
const out = [];
|
|
61
|
+
if (!report.agent)
|
|
62
|
+
out.push({
|
|
63
|
+
what: 'it had no agent name (BREAKAWAY_AGENT)',
|
|
64
|
+
fix: 'Make the routine’s instructions the stub from the Agents view (Copy stub): it sends each agent to the prompt that sets BREAKAWAY_AGENT to the name the board gave it.',
|
|
65
|
+
});
|
|
66
|
+
if (report.token !== 'credential')
|
|
67
|
+
out.push({
|
|
68
|
+
what:
|
|
69
|
+
report.token === 'variable'
|
|
70
|
+
? 'it read the board’s token from BREAKAWAY_TOKEN, a variable anyone using the environment can see'
|
|
71
|
+
: 'it read the board’s token from a tasks.env file',
|
|
72
|
+
fix: `In the routine’s cloud environment on claude.ai, add the token as an API credential for ${host} and remove BREAKAWAY_TOKEN: the proxy then adds it, and the token never enters the session.`,
|
|
73
|
+
});
|
|
74
|
+
if (report.stub === 'different' || report.stub === 'missing')
|
|
75
|
+
out.push({
|
|
76
|
+
what:
|
|
77
|
+
report.stub === 'different'
|
|
78
|
+
? 'its checkout’s copy of the stub differs from the board’s'
|
|
79
|
+
: 'its checkout has no copy of the stub',
|
|
80
|
+
fix: `Bring ${slug}’s copy of the board’s files up to date (npx breakaway repos init ${slug} --update opens a pull request; in the board’s own repository, update the board to the same release), then paste the stub from the Agents view (Copy stub) as the routine’s instructions.`,
|
|
81
|
+
});
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The first 16 hex digits of the sha-256 of `text`, with Web Crypto (the Worker and Node alike). */
|
|
86
|
+
export async function shortHash(text) {
|
|
87
|
+
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
|
|
88
|
+
return [...new Uint8Array(digest)]
|
|
89
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
90
|
+
.join('')
|
|
91
|
+
.slice(0, 16);
|
|
92
|
+
}
|