toolroll 0.9.46 → 0.9.47
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/CHANGELOG.md +15 -0
- package/dist/activity-line.d.ts +26 -0
- package/dist/activity-line.js +47 -0
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +18 -1
- package/dist/browser-workspace.js +9 -3
- package/dist/builder.js +4 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/flow-card-task.d.ts +2 -0
- package/dist/flow-card-task.js +57 -0
- package/dist/flow-live.d.ts +2 -2
- package/dist/flow-live.js +69 -8
- package/dist/flows-ui.js +15 -1
- package/dist/live.d.ts +14 -0
- package/dist/live.js +71 -22
- package/dist/operate.js +6 -1
- package/dist/planner.js +4 -3
- package/dist/serve.d.ts +5 -0
- package/dist/serve.js +91 -39
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +16 -0
- package/dist/store.js +34 -0
- package/dist/task-activity.d.ts +27 -0
- package/dist/task-activity.js +53 -0
- package/dist/task-live.d.ts +17 -0
- package/dist/task-live.js +144 -0
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* produced by the existing trusted server renderers, never by a model-supplied
|
|
3
3
|
* fragment. These projections do not authenticate, mutate or grant authority. */
|
|
4
4
|
import type { TeamSnapshot } from './team-contract.js';
|
|
5
|
+
import type { RunActivity } from './activity-line.js';
|
|
5
6
|
import type { BrandIconId } from './brand-icons.js';
|
|
6
7
|
import type { AssignmentSnapshot } from './assignment.js';
|
|
7
8
|
export { browserCrewFromIndex, browserWorkActionHref } from './browser-crew.js';
|
|
@@ -26,7 +27,12 @@ export type BrowserNavigationItem = {
|
|
|
26
27
|
href: string;
|
|
27
28
|
active: boolean;
|
|
28
29
|
count?: number;
|
|
30
|
+
/** What the count covers, said aloud and on hover: "3 need you in shop". */
|
|
31
|
+
countLabel?: string;
|
|
29
32
|
};
|
|
33
|
+
/** The Tasks badge's words: the count and the projects it covers
|
|
34
|
+
* ("3 need you in shop", "1 needs you across all your projects"). */
|
|
35
|
+
export declare function needsYouLabelOf(count: number, projectName: string | null, saturated?: boolean): string;
|
|
30
36
|
export type BrowserChatLink = {
|
|
31
37
|
kind: 'project' | 'task';
|
|
32
38
|
title: string;
|
|
@@ -53,6 +59,8 @@ export type BrowserCrewItem = {
|
|
|
53
59
|
detail?: string;
|
|
54
60
|
/** "<name> is on it.": the person's own lead took it on, by the name they gave it. */
|
|
55
61
|
lead?: string;
|
|
62
|
+
/** A running task: what its agent did last, and when. */
|
|
63
|
+
activity?: RunActivity;
|
|
56
64
|
};
|
|
57
65
|
export type BrowserMessage = {
|
|
58
66
|
id: number;
|
|
@@ -419,6 +427,13 @@ export type BrowserTaskView = {
|
|
|
419
427
|
action: BrowserLink | null;
|
|
420
428
|
} | null;
|
|
421
429
|
} | null;
|
|
430
|
+
/** What the agent did last on the live run, and when; the page words it against its own clock (activity-line.ts). */
|
|
431
|
+
activity?: RunActivity | null;
|
|
432
|
+
/** The task's live stream (/t/<id>/live): change nudges and who else has it open. Absent: the page reads on its beat. */
|
|
433
|
+
live?: {
|
|
434
|
+
href: string;
|
|
435
|
+
at: string | null;
|
|
436
|
+
} | null;
|
|
422
437
|
/** Stop, on the Building card: the exact live build's stop form (posts its run id). */
|
|
423
438
|
stop?: {
|
|
424
439
|
action: string;
|
|
@@ -1166,6 +1181,8 @@ export type BrowserHomeAgent = {
|
|
|
1166
1181
|
phase: string;
|
|
1167
1182
|
project: string | null;
|
|
1168
1183
|
since: string;
|
|
1184
|
+
/** What the agent did last, and when. */
|
|
1185
|
+
activity?: RunActivity;
|
|
1169
1186
|
};
|
|
1170
1187
|
export type BrowserHomeCount = {
|
|
1171
1188
|
key: 'working' | 'waiting' | 'ready' | 'done';
|
|
@@ -1277,4 +1294,4 @@ export declare function browserProjectsOf(projects: readonly {
|
|
|
1277
1294
|
name: string;
|
|
1278
1295
|
path: string;
|
|
1279
1296
|
}[]): BrowserProject[];
|
|
1280
|
-
export declare function browserNavigationOf(path: string, project?: string | null, needsYou?: number): BrowserNavigationItem[];
|
|
1297
|
+
export declare function browserNavigationOf(path: string, project?: string | null, needsYou?: number, tasksProject?: string | null, needsYouLabel?: string): BrowserNavigationItem[];
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { browserCrewFromIndex } from './browser-crew.js';
|
|
2
2
|
export { browserCrewFromIndex, browserWorkActionHref } from './browser-crew.js';
|
|
3
3
|
import { workIndexPage } from './work-index.js';
|
|
4
|
+
/** The Tasks badge's words: the count and the projects it covers
|
|
5
|
+
* ("3 need you in shop", "1 needs you across all your projects"). */
|
|
6
|
+
export function needsYouLabelOf(count, projectName, saturated = false) {
|
|
7
|
+
const where = projectName === null ? 'across all your projects' : `in ${projectName}`;
|
|
8
|
+
return `${count}${saturated ? '+' : ''} ${count === 1 && !saturated ? 'needs' : 'need'} you ${where}`;
|
|
9
|
+
}
|
|
4
10
|
/** Safe inside a script[type=application/json] element. JSON escaping alone
|
|
5
11
|
* does not stop the HTML parser from closing that element at </script>. */
|
|
6
12
|
export function serializeBrowserWorkspace(workspace) {
|
|
@@ -31,14 +37,14 @@ export function browserProjectsOf(projects) {
|
|
|
31
37
|
knowledgeHref: `/settings/knowledge?repo=${encodeURIComponent(project.path)}` }];
|
|
32
38
|
});
|
|
33
39
|
}
|
|
34
|
-
export function browserNavigationOf(path, project = null, needsYou = 0) {
|
|
40
|
+
export function browserNavigationOf(path, project = null, needsYou = 0, tasksProject = project, needsYouLabel) {
|
|
35
41
|
const pathname = path.split('?')[0].split('#')[0];
|
|
36
42
|
const knowledge = pathname === '/settings/knowledge' || pathname.startsWith('/settings/knowledge/');
|
|
37
43
|
return [
|
|
38
44
|
{ label: 'Chat', href: '/chat', active: pathname === '/chat' },
|
|
39
|
-
{ label: 'Tasks', href: `/work${
|
|
45
|
+
{ label: 'Tasks', href: `/work${tasksProject === null ? '' : `?project=${encodeURIComponent(tasksProject)}`}`,
|
|
40
46
|
active: pathname === '/work' || pathname === '/tasks' || pathname.startsWith('/t/') || pathname.startsWith('/r/'),
|
|
41
|
-
...(needsYou > 0 ? { count: needsYou } : {}) },
|
|
47
|
+
...(needsYou > 0 ? { count: needsYou, ...(needsYouLabel === undefined ? {} : { countLabel: needsYouLabel }) } : {}) },
|
|
42
48
|
{ label: 'Flows', href: '/flows', active: pathname === '/flows' || pathname.startsWith('/flows/') },
|
|
43
49
|
{ label: 'Projects', href: '/projects', active: pathname === '/projects' },
|
|
44
50
|
{ label: 'Knowledge', href: `/settings/knowledge${project === null ? '' : `?repo=${encodeURIComponent(project)}`}`, active: knowledge },
|
package/dist/builder.js
CHANGED
|
@@ -60,6 +60,7 @@ import { invokeAgent } from "./invoke.js";
|
|
|
60
60
|
import { TELEGRAM_TOKEN_ENVS } from "./names.js";
|
|
61
61
|
import { OPENROUTER_ENV_KEY, auditOf, ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
62
62
|
import { openLiveLog } from "./live.js";
|
|
63
|
+
import { withRunActivity } from "./task-activity.js";
|
|
63
64
|
import { CheckProgressTracker } from "./check-progress.js";
|
|
64
65
|
import { captureParkEvidence, captureTerminalDiff, captureBaseTree, evidenceRoot, handoffName, isImagePath, WORKTREE_EVIDENCE_DIR, storeHandoffArtifact, looksLikeProtocolFile, mailboxName, progressFileName, proofFileName, rubricFileName, proposalFileName, quarantineMailboxes, readMailbox, readVerifiedArtifact, scanForSecrets, redactSecretLines, storeEvidence, validateScreenshotBytes, imageDimensions, SCREENSHOT_BYTE_CAP, boundStreamHeadTail } from "./evidence.js";
|
|
65
66
|
import { PROOF_LIMITS, parseProof, serializeProof, adjudicate, artifactManifestOnly, sameDiffStatFacts } from "./proof.js";
|
|
@@ -1139,7 +1140,8 @@ export async function build(store, request) {
|
|
|
1139
1140
|
// The live window (arc 1): display state beside the run, never evidence.
|
|
1140
1141
|
// Every streaming transport emits events now (peek); a file that cannot
|
|
1141
1142
|
// open is a null, and a null never costs a build.
|
|
1142
|
-
|
|
1143
|
+
// What the agent did last rides the same stream into one row (live tasks).
|
|
1144
|
+
const liveLog = withRunActivity(openLiveLog(root, request.runId), store, request.runId, clock);
|
|
1143
1145
|
// The retry base (steering fix for run 1465's proof): the SAME pinned
|
|
1144
1146
|
// base the machine will use to capture the sealed diff (settleProof
|
|
1145
1147
|
// below, mirroring store.firstBuilderBase's own doc). Null on a first
|
|
@@ -1329,7 +1331,7 @@ export async function build(store, request) {
|
|
|
1329
1331
|
omitEnv: AGENT_ENV_DENYLIST,
|
|
1330
1332
|
...(agent === undefined ? {} : { runner: agent }),
|
|
1331
1333
|
...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
|
|
1332
|
-
|
|
1334
|
+
onStreamEvent: (event) => liveLog.observe(event),
|
|
1333
1335
|
// The receipt (finding 8): the stream proved the prompt reached the
|
|
1334
1336
|
// agent — settle delivery NOW, durably, whatever happens to the run
|
|
1335
1337
|
// later. The runner latches and isolates this callback; a throw
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -94,7 +94,6 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
94
94
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
95
95
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
96
96
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
97
|
-
version?: never;
|
|
98
97
|
active: boolean;
|
|
99
98
|
phase: string;
|
|
100
99
|
detail: string;
|
|
@@ -108,6 +107,7 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
108
107
|
wasRunning?: never;
|
|
109
108
|
backupPath?: never;
|
|
110
109
|
updateId?: never;
|
|
110
|
+
version?: never;
|
|
111
111
|
buildId?: never;
|
|
112
112
|
workDir?: never;
|
|
113
113
|
} | {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** The task behind an agent zone, read through the same status as its task page.
|
|
2
|
+
* Only saved milestone progress belongs on the card; tool calls and logs do not. */
|
|
3
|
+
import { assignmentOf } from "./assignment.js";
|
|
4
|
+
import { scanForSecrets } from "./evidence.js";
|
|
5
|
+
import { scrubIntegrationText } from "./integrations.js";
|
|
6
|
+
import { assignmentStatusFacts, taskStatusOf } from "./task-status.js";
|
|
7
|
+
import { taskWorkSummaryOf } from "./work-summary.js";
|
|
8
|
+
import { buildProgressOf } from "./workspace-ui.js";
|
|
9
|
+
/** Redact before shortening: a cut-off credential must never slip through a scanner.
|
|
10
|
+
* Structured arguments, commands and credential assignments aren't card copy. */
|
|
11
|
+
function cardReason(text, fallback, secrets) {
|
|
12
|
+
if (text === null)
|
|
13
|
+
return fallback;
|
|
14
|
+
const raw = /[{}\[\]`]|\b[\w.-]+\s*=|(?:^|\s)--[\w-]+|\b(?:arguments|tool_input|command|cmd|password|passwd|token|secret|api[_ -]?key|authorization)\s*:|\bcurl\s/i.test(text);
|
|
15
|
+
if (raw || secrets.some(secret => secret !== "" && text.includes(secret)) || scanForSecrets(text).length > 0)
|
|
16
|
+
return fallback;
|
|
17
|
+
const clean = scrubIntegrationText(text, secrets).replace(/[\p{Cf}]/gu, "").replace(/[.\s]+$/, "");
|
|
18
|
+
if (clean === "" || clean.includes("[hidden]"))
|
|
19
|
+
return fallback;
|
|
20
|
+
return clean.length > 100 ? `${clean.slice(0, 99).trimEnd()}…` : clean;
|
|
21
|
+
}
|
|
22
|
+
export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
|
|
23
|
+
const access = { principal: "operator", repos: [repo], dependencyRepos: [repo] };
|
|
24
|
+
const assignment = assignmentOf(store, taskId, now, access);
|
|
25
|
+
if (assignment === null)
|
|
26
|
+
return "Task unavailable";
|
|
27
|
+
const work = taskWorkSummaryOf(store, assignment.activeTaskId, now, access);
|
|
28
|
+
if (work === null)
|
|
29
|
+
return "Task unavailable";
|
|
30
|
+
const live = work.liveRunId === null ? null : store.getRun(work.liveRunId);
|
|
31
|
+
const facts = assignmentStatusFacts(assignment, { work: work.status, planning: live?.role === "planner" });
|
|
32
|
+
// The canvas reads no evidence files. Keep the machine's recorded check visible
|
|
33
|
+
// when the full receipt has no artifact root from which to read its check.
|
|
34
|
+
const check = assignment.receipt === null ? null : store.runCheckFor(assignment.receipt.runId);
|
|
35
|
+
if (facts.checks?.status === "unavailable" && check !== null)
|
|
36
|
+
facts.checks = { ...facts.checks, status: check.status, exitCode: check.exitCode };
|
|
37
|
+
const status = taskStatusOf(facts);
|
|
38
|
+
if (status.headline === "Needs you") {
|
|
39
|
+
const fallback = status.need?.action.label ?? "Open the task";
|
|
40
|
+
// The question itself is useful; routine approvals are clearer as the short action.
|
|
41
|
+
const what = status.need?.key === "answer" || status.need?.key === "other"
|
|
42
|
+
? cardReason(status.sentence, fallback, secrets) : fallback;
|
|
43
|
+
return `Waiting on you: ${what}`;
|
|
44
|
+
}
|
|
45
|
+
if (status.headline === "Building" && live !== null) {
|
|
46
|
+
const ref = store.lookupRef(assignment.activeTaskId);
|
|
47
|
+
const checkpoint = store.latestCheckpointForRun(live.id);
|
|
48
|
+
const progress = buildProgressOf(checkpoint?.taskRef !== ref?.id ? null : checkpoint?.snapshot.milestones.map(one => ({ description: null, state: one.state, note: one.note })));
|
|
49
|
+
if (progress?.stuck != null)
|
|
50
|
+
return `Stuck: ${cardReason(progress.stuck.why, `step ${progress.step} of ${progress.total}`, secrets)}`;
|
|
51
|
+
if (progress !== null)
|
|
52
|
+
return `Building · step ${progress.step} of ${progress.total}`;
|
|
53
|
+
}
|
|
54
|
+
if (status.headline === "Failed")
|
|
55
|
+
return `Stuck: ${cardReason(status.sentence, "Open the task", secrets)}`;
|
|
56
|
+
return status.headline === "Ready for review" ? "Ready" : status.headline;
|
|
57
|
+
}
|
package/dist/flow-live.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export type FlowViewer = {
|
|
|
12
12
|
response: ServerResponse;
|
|
13
13
|
valid: () => boolean;
|
|
14
14
|
};
|
|
15
|
-
/** What changes when anything on the canvas changes:
|
|
16
|
-
export declare function flowFingerprint(store: Store, flow: number): string | null;
|
|
15
|
+
/** What changes when anything on the canvas changes: metadata only, kept short and opaque. */
|
|
16
|
+
export declare function flowFingerprint(store: Store, flow: number, now?: Date): string | null;
|
|
17
17
|
export type FlowRooms = {
|
|
18
18
|
/** Keep this page's stream on the flow until it closes; the first event says where the flow stands. */
|
|
19
19
|
join: (flow: number, viewer: FlowViewer) => void;
|
package/dist/flow-live.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* The live canvas (v88): a flow open in several browsers stays the same in
|
|
3
3
|
* all of them, and each shows who else has it open.
|
|
4
4
|
*
|
|
5
|
-
* - A change nudge. While anyone has a flow open,
|
|
6
|
-
*
|
|
7
|
-
* watchers, step runs and triggers); when it changes, every open page
|
|
5
|
+
* - A change nudge. While anyone has a flow open, a small read each second
|
|
6
|
+
* checks the flow's fingerprint (its revision, cards, moves, comments,
|
|
7
|
+
* watchers, step runs, linked task progress and triggers); when it changes, every open page
|
|
8
8
|
* hears "change" and reads the flow again the usual way. The nudge
|
|
9
9
|
* carries no card data: what a page shows is always what its own read
|
|
10
10
|
* returned, under its own checks.
|
|
@@ -20,17 +20,78 @@ const TICK_MS = 1_000;
|
|
|
20
20
|
const CHECK_EVERY = 15;
|
|
21
21
|
const HEARTBEAT_EVERY = 15;
|
|
22
22
|
const PRESENCE_SETTLE_MS = 150;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// Statements belong to their database, without keeping closed Stores alive.
|
|
24
|
+
const fingerprintStatements = new WeakMap();
|
|
25
|
+
function fingerprintStatement(store) {
|
|
26
|
+
const cached = fingerprintStatements.get(store.handle);
|
|
27
|
+
if (cached !== undefined)
|
|
28
|
+
return cached;
|
|
29
|
+
// Match Store.taskFamilyOf/taskFamilyProjection: built-in tasks, same-repo
|
|
30
|
+
// edges, a revision brief bound to the parent's run, and at most 63 edges.
|
|
31
|
+
// Walk up only from active cards, then down from their proven roots. A
|
|
32
|
+
// missing/invalid/cyclic/over-depth ancestry leaves the linked task alone.
|
|
33
|
+
const statement = store.handle.prepare(`WITH RECURSIVE linked AS MATERIALIZED (
|
|
34
|
+
SELECT DISTINCT tr.id AS ref_id, tr.external_id AS id, tr.repo, tr.revision_of, tr.revision_brief_artifact
|
|
35
|
+
FROM flow_card c JOIN task_ref tr ON tr.external_id = c.task AND tr.backend = 'built-in'
|
|
36
|
+
JOIN task t ON t.id = tr.external_id
|
|
37
|
+
WHERE c.flow = $flow AND c.state = 'active' AND tr.repo IS NOT NULL
|
|
38
|
+
), ancestors(linked_ref, ref_id, id, repo, revision_of, revision_brief_artifact, depth) AS (
|
|
39
|
+
SELECT ref_id, ref_id, id, repo, revision_of, revision_brief_artifact, 0 FROM linked
|
|
40
|
+
UNION ALL
|
|
41
|
+
SELECT child.linked_ref, parent.id, parent.external_id, parent.repo, parent.revision_of, parent.revision_brief_artifact, child.depth + 1
|
|
42
|
+
FROM ancestors child
|
|
43
|
+
JOIN task_ref parent ON parent.backend = 'built-in' AND parent.external_id = child.revision_of AND parent.repo IS child.repo
|
|
44
|
+
JOIN task t ON t.id = parent.external_id
|
|
45
|
+
JOIN artifact brief ON brief.id = child.revision_brief_artifact AND brief.kind = 'revision-brief'
|
|
46
|
+
JOIN run source_run ON source_run.id = brief.run AND source_run.task_ref = parent.id
|
|
47
|
+
WHERE child.depth < 63
|
|
48
|
+
), roots AS MATERIALIZED (
|
|
49
|
+
SELECT DISTINCT ref_id, id, repo FROM ancestors WHERE revision_of IS NULL
|
|
50
|
+
), descendants(ref_id, id, repo, depth) AS (
|
|
51
|
+
SELECT ref_id, id, repo, 0 FROM roots
|
|
52
|
+
UNION ALL
|
|
53
|
+
SELECT child.id, child.external_id, child.repo, parent.depth + 1 FROM descendants parent
|
|
54
|
+
JOIN task_ref child ON child.backend = 'built-in' AND child.revision_of = parent.id AND child.repo IS parent.repo
|
|
55
|
+
JOIN task t ON t.id = child.external_id
|
|
56
|
+
JOIN artifact brief ON brief.id = child.revision_brief_artifact AND brief.kind = 'revision-brief'
|
|
57
|
+
JOIN run source_run ON source_run.id = brief.run AND source_run.task_ref = parent.ref_id
|
|
58
|
+
WHERE parent.depth < 63
|
|
59
|
+
), members AS (
|
|
60
|
+
SELECT ref_id FROM descendants
|
|
61
|
+
UNION
|
|
62
|
+
SELECT ref_id FROM linked WHERE NOT EXISTS (
|
|
63
|
+
SELECT 1 FROM ancestors WHERE linked_ref = linked.ref_id AND revision_of IS NULL
|
|
64
|
+
)
|
|
65
|
+
) SELECT f.state || '|' || f.revision || '|' || f.updated_at || '|' || coalesce(f.owner, '') AS head,
|
|
26
66
|
(SELECT count(*) || ':' || coalesce(max(updated_at), '') FROM flow_card WHERE flow = f.id) AS cards,
|
|
27
67
|
(SELECT coalesce(max(e.id), 0) FROM flow_event e JOIN flow_card c ON c.id = e.card WHERE c.flow = f.id) AS moves,
|
|
28
68
|
(SELECT coalesce(max(m.id), 0) FROM flow_comment m JOIN flow_card c ON c.id = m.card WHERE c.flow = f.id) AS comments,
|
|
29
69
|
(SELECT count(*) || ':' || coalesce(max(w.added_at), '') FROM flow_card_watcher w JOIN flow_card c ON c.id = w.card WHERE c.flow = f.id) AS watchers,
|
|
30
70
|
(SELECT count(*) || ':' || coalesce(sum(r.attempts), 0) || ':' || coalesce(max(coalesce(r.finished_at, r.started_at)), '') || ':' || coalesce(sum(r.state = 'running'), 0)
|
|
31
71
|
FROM flow_step_run r JOIN flow_card c ON c.id = r.card WHERE c.flow = f.id) AS runs,
|
|
32
|
-
(SELECT count(*) || ':' || coalesce(max(updated_at), '') || ':' || coalesce(max(last_at), '') || ':' || coalesce(sum(failures), 0) FROM flow_trigger WHERE flow = f.id) AS triggers
|
|
33
|
-
|
|
72
|
+
(SELECT count(*) || ':' || coalesce(max(updated_at), '') || ':' || coalesce(max(last_at), '') || ':' || coalesce(sum(failures), 0) FROM flow_trigger WHERE flow = f.id) AS triggers,
|
|
73
|
+
(SELECT json_group_array(json_array(tr.id, t.id, t.state, t.updated_at, tr.plan,
|
|
74
|
+
s.digest, s.approved_digest, s.approved_at,
|
|
75
|
+
(SELECT max(p.id) FROM run_checkpoint p WHERE p.task_ref = tr.id),
|
|
76
|
+
(SELECT q.lease_id FROM claim q WHERE q.task_ref = tr.id AND q.released_at IS NULL AND q.expires_at > $now ORDER BY q.lease_generation DESC LIMIT 1),
|
|
77
|
+
(SELECT json_group_array(json_array(r.id, r.outcome, r.finished_at, r.reason,
|
|
78
|
+
(SELECT json_array(k.status, k.exit_code, k.updated_at) FROM run_check k WHERE k.run = r.id)) ORDER BY r.id) FROM run r WHERE r.task_ref = tr.id),
|
|
79
|
+
(SELECT json_group_array(json_array(d.id, d.state, d.question, d.answered_at) ORDER BY d.id) FROM decision d JOIN run r ON r.id = d.run WHERE r.task_ref = tr.id),
|
|
80
|
+
(SELECT json_group_array(json_array(h.id, h.reason) ORDER BY h.id) FROM hold h WHERE h.task_ref = tr.id AND (h.until IS NULL OR h.until > $now)),
|
|
81
|
+
(SELECT max(a.id) FROM action_ledger a WHERE a.task_id = t.id)) ORDER BY tr.id)
|
|
82
|
+
FROM task_ref tr JOIN task t ON t.id = tr.external_id
|
|
83
|
+
LEFT JOIN task_scope s ON s.task_id = t.id
|
|
84
|
+
WHERE tr.id IN (SELECT ref_id FROM members)) AS tasks
|
|
85
|
+
FROM flow f WHERE f.id = $flow`);
|
|
86
|
+
fingerprintStatements.set(store.handle, statement);
|
|
87
|
+
return statement;
|
|
88
|
+
}
|
|
89
|
+
/** What changes when anything on the canvas changes: metadata only, kept short and opaque. */
|
|
90
|
+
export function flowFingerprint(store, flow, now = new Date()) {
|
|
91
|
+
// A revision can progress while its card still links to the original task,
|
|
92
|
+
// including a build in another project. The page read admits the viewer;
|
|
93
|
+
// this stream only carries a hash. No evidence files or logs are read.
|
|
94
|
+
const row = fingerprintStatement(store).get({ $flow: flow, $now: now.toISOString() });
|
|
34
95
|
return row === undefined ? null : createHash("sha256").update(Object.values(row).map(String).join("|")).digest("hex").slice(0, 16);
|
|
35
96
|
}
|
|
36
97
|
export function createFlowRooms(fingerprint, tickMs = TICK_MS) {
|
package/dist/flows-ui.js
CHANGED
|
@@ -14,6 +14,8 @@ import { mailboxReady } from "./mailbox.js";
|
|
|
14
14
|
import { labelOf, nameOf } from "./teammate-admin.js";
|
|
15
15
|
import { callWords, receiptWords } from "./teammate-tools.js";
|
|
16
16
|
import { undoFor } from "./teammate-week.js";
|
|
17
|
+
import { flowCardTaskLine } from "./flow-card-task.js";
|
|
18
|
+
import { readFlowSecrets } from "./flow-secrets.js";
|
|
17
19
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
18
20
|
const projectName = (repo) => repo.split(/[\\/]/).filter(Boolean).pop() ?? repo;
|
|
19
21
|
export const FLOWS_CSS = `.flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:10px 0 0}.flows .flow-import{margin-top:18px;padding-top:14px;border-top:1px solid var(--so-line)}.flows .flow-import h3{font-size:.95rem;margin:0}.flows .flow-terms{list-style:none;padding:0;margin:8px 0 16px;display:grid;gap:10px}.flows .flow-terms li{white-space:pre-line;overflow-wrap:anywhere}.flows .flow-terms pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.8rem;margin:6px 0 0}.flows .flow-import-preview h2{font-size:1rem;margin:20px 0 4px}.flows .flow-untrusted{font-weight:600}.flows button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}.flows fieldset{border:0;padding:0;margin:0;display:grid;gap:10px;min-width:0}.flows legend{font-weight:600;padding:0;margin-bottom:6px}.flows textarea[hidden]{display:none}.flows button.secondary{background:transparent;color:var(--foreground);border:1px solid var(--border)}@media(max-width:600px){.flows input,.flows select{font-size:16px}}@media(max-width:760px){.flows .card{padding:10px 12px;margin:8px 0;position:relative}.flows .flow-row{row-gap:2px}.flows .flow-row h2 a::after{content:"";position:absolute;inset:0;border-radius:inherit}.flows .flow-counts{line-height:1.35}}`;
|
|
@@ -84,9 +86,19 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
84
86
|
const sortZones = new Set(stages.filter(one => one.kind === "sort").map(one => one.id));
|
|
85
87
|
const decisions = sortZones.size === 0 ? new Map() : store.flowSortDecisions(flow.id);
|
|
86
88
|
const mates = new Map(store.teammates([flow.repo]).map(one => [one.id, one]));
|
|
89
|
+
const taskSecrets = new Map();
|
|
90
|
+
const secretsFor = (repo) => {
|
|
91
|
+
if (!taskSecrets.has(repo))
|
|
92
|
+
taskSecrets.set(repo, Object.values(readFlowSecrets(setup.dir, repo)));
|
|
93
|
+
return taskSecrets.get(repo);
|
|
94
|
+
};
|
|
95
|
+
const now = new Date();
|
|
87
96
|
const cards = store.flowCards(flow.id, true).filter(card => card.state === "active" || Date.now() - Date.parse(card.updatedAt) < 7 * 86_400_000).map((card) => {
|
|
88
97
|
const stage = stages.find(one => one.id === card.stage);
|
|
89
98
|
const task = card.task ?? card.primaryTask;
|
|
99
|
+
// Build zones may file into another project; the flow alone does not admit its task's detail.
|
|
100
|
+
const taskRepo = card.task === null ? null : store.lookupRef(card.task)?.repo ?? null;
|
|
101
|
+
const canReadTask = taskRepo !== null && (taskRepo === flow.repo || (setup.repos.includes(taskRepo) && store.accountCanAccess(viewer.name, taskRepo)));
|
|
90
102
|
const discussion = store.flowComments(card.id);
|
|
91
103
|
const watchers = store.flowCardWatchers(card.id);
|
|
92
104
|
const decider = stage?.kind === "approval" ? deciderOf(stage, flow) : null;
|
|
@@ -136,7 +148,9 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
136
148
|
...(content.items === undefined ? {} : { items: content.items.map((item, index) => ({ number: index + 1, title: item.title, lines: whyLines(item.why), source: item.source, url: item.url,
|
|
137
149
|
image: item.shot === null || content.shots === null ? null : { src: `/r/${content.shots.run}/evidence/${item.shot}`, caption: itemCaption(index + 1, item) } })) }) });
|
|
138
150
|
return {
|
|
139
|
-
id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state,
|
|
151
|
+
id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state,
|
|
152
|
+
waiting: card.state === "active" && card.task !== null && (stage?.kind === "task" || stage?.kind === "report")
|
|
153
|
+
? canReadTask ? flowCardTaskLine(store, card.task, taskRepo, now, secretsFor(taskRepo)) : "Task unavailable" : card.waiting,
|
|
140
154
|
task: task === null ? null : { id: task, href: `/t/${encodeURIComponent(task)}` },
|
|
141
155
|
createdBy: card.createdBy, updatedAt: card.updatedAt,
|
|
142
156
|
canDecide,
|
package/dist/live.d.ts
CHANGED
|
@@ -25,6 +25,20 @@ export declare function liveDir(root: string): string;
|
|
|
25
25
|
/** The exact filename, from the numeric id and nothing else — no user
|
|
26
26
|
* input is ever parsed into this path. */
|
|
27
27
|
export declare function liveLogPath(root: string, runId: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* The fixed tool vocabulary. Known harness tools map to OUR kinds and
|
|
30
|
+
* phrases; anything unrecognized gets the generic one. The tool's own
|
|
31
|
+
* name, arguments, and metadata never reach the file: a tool named after
|
|
32
|
+
* a secret stays unspoken.
|
|
33
|
+
*/
|
|
34
|
+
export type LiveActivityKind = "session" | "message" | "edit" | "command" | "read" | "search" | "lookup" | "plan" | "delegate" | "tool";
|
|
35
|
+
export declare const LIVE_ACTIVITY_KINDS: readonly LiveActivityKind[];
|
|
36
|
+
/**
|
|
37
|
+
* What one stream event says the agent just did, as one fixed kind: the
|
|
38
|
+
* last-activity line's only input. Never text, names or arguments; an
|
|
39
|
+
* event that says nothing (results, deltas, subagent chatter) is null.
|
|
40
|
+
*/
|
|
41
|
+
export declare function liveActivityKind(event: Record<string, unknown>): LiveActivityKind | null;
|
|
28
42
|
/**
|
|
29
43
|
* One stream event → zero or more display lines, in the fixed vocabulary.
|
|
30
44
|
* Assistant TEXT is the agent's own words (sanitized below); tool use is a
|
package/dist/live.js
CHANGED
|
@@ -32,28 +32,77 @@ export function liveDir(root) {
|
|
|
32
32
|
export function liveLogPath(root, runId) {
|
|
33
33
|
return join(liveDir(root), `${Math.trunc(runId)}.log`);
|
|
34
34
|
}
|
|
35
|
+
export const LIVE_ACTIVITY_KINDS = ["session", "message", "edit", "command", "read", "search", "lookup", "plan", "delegate", "tool"];
|
|
36
|
+
const TOOL_KINDS = new Map([
|
|
37
|
+
["Edit", "edit"],
|
|
38
|
+
["Write", "edit"],
|
|
39
|
+
["MultiEdit", "edit"],
|
|
40
|
+
["NotebookEdit", "edit"],
|
|
41
|
+
["Bash", "command"],
|
|
42
|
+
["Read", "read"],
|
|
43
|
+
["Glob", "search"],
|
|
44
|
+
["Grep", "search"],
|
|
45
|
+
["LS", "read"],
|
|
46
|
+
["WebFetch", "lookup"],
|
|
47
|
+
["WebSearch", "lookup"],
|
|
48
|
+
["TodoWrite", "plan"],
|
|
49
|
+
["Task", "delegate"],
|
|
50
|
+
]);
|
|
51
|
+
const KIND_HEADLINES = {
|
|
52
|
+
session: "the agent session started",
|
|
53
|
+
message: "",
|
|
54
|
+
edit: "→ editing files",
|
|
55
|
+
command: "→ running a command",
|
|
56
|
+
read: "→ reading the code",
|
|
57
|
+
search: "→ searching the code",
|
|
58
|
+
lookup: "→ looking something up",
|
|
59
|
+
plan: "→ organizing its plan",
|
|
60
|
+
delegate: "→ delegating to a helper",
|
|
61
|
+
tool: "→ using a tool",
|
|
62
|
+
};
|
|
63
|
+
const toolKind = (name) => TOOL_KINDS.get(typeof name === "string" ? name : "") ?? "tool";
|
|
35
64
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* a secret stays unspoken.
|
|
65
|
+
* What one stream event says the agent just did, as one fixed kind: the
|
|
66
|
+
* last-activity line's only input. Never text, names or arguments; an
|
|
67
|
+
* event that says nothing (results, deltas, subagent chatter) is null.
|
|
40
68
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
export function liveActivityKind(event) {
|
|
70
|
+
try {
|
|
71
|
+
const type = String(event["type"] ?? "");
|
|
72
|
+
if ((type === "system" && String(event["subtype"] ?? "") === "init") || type === "thread.started" || type === "init")
|
|
73
|
+
return "session";
|
|
74
|
+
if (type === "item.completed") {
|
|
75
|
+
const item = event["item"];
|
|
76
|
+
const kind = item === undefined || item === null ? "" : String(item["type"] ?? "");
|
|
77
|
+
return kind === "agent_message" ? "message" : kind === "command_execution" ? "command" : kind === "file_change" ? "edit" : kind === "web_search" ? "lookup" : null;
|
|
78
|
+
}
|
|
79
|
+
if (type === "message")
|
|
80
|
+
return String(event["role"] ?? "") === "assistant" && event["delta"] !== true ? "message" : null;
|
|
81
|
+
if (type === "tool_call" || type === "tool_use")
|
|
82
|
+
return toolKind(event["name"] ?? event["tool"]?.["name"]);
|
|
83
|
+
if (type !== "assistant")
|
|
84
|
+
return null;
|
|
85
|
+
const parent = event["parent_tool_use_id"];
|
|
86
|
+
if (parent !== undefined && parent !== null)
|
|
87
|
+
return null;
|
|
88
|
+
const content = event["message"]?.["content"];
|
|
89
|
+
if (!Array.isArray(content))
|
|
90
|
+
return null;
|
|
91
|
+
let last = null;
|
|
92
|
+
for (const block of content) {
|
|
93
|
+
if (block === null || typeof block !== "object")
|
|
94
|
+
continue;
|
|
95
|
+
if (block["type"] === "tool_use")
|
|
96
|
+
last = toolKind(block["name"]);
|
|
97
|
+
else if (block["type"] === "text" && typeof block["text"] === "string" && block["text"].trim() !== "")
|
|
98
|
+
last = "message";
|
|
99
|
+
}
|
|
100
|
+
return last;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
57
106
|
/** Strip ANSI escapes and every control character; tabs become spaces. */
|
|
58
107
|
function stripControls(text) {
|
|
59
108
|
return text
|
|
@@ -108,7 +157,7 @@ export function renderTranscriptLines(event) {
|
|
|
108
157
|
}
|
|
109
158
|
if (type === "tool_call" || type === "tool_use") {
|
|
110
159
|
const name = event["name"] ?? event["tool"]?.["name"];
|
|
111
|
-
return [
|
|
160
|
+
return [KIND_HEADLINES[toolKind(name)]];
|
|
112
161
|
}
|
|
113
162
|
if (type !== "assistant")
|
|
114
163
|
return [];
|
|
@@ -136,7 +185,7 @@ export function renderTranscriptLines(event) {
|
|
|
136
185
|
}
|
|
137
186
|
else if (kind === "tool_use") {
|
|
138
187
|
const name = block["name"];
|
|
139
|
-
lines.push(
|
|
188
|
+
lines.push(KIND_HEADLINES[toolKind(name)]);
|
|
140
189
|
}
|
|
141
190
|
}
|
|
142
191
|
return lines;
|
package/dist/operate.js
CHANGED
|
@@ -90,7 +90,7 @@ import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBus
|
|
|
90
90
|
import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
91
91
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
92
92
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
93
|
-
import { sweepLiveLogs } from "./live.js";
|
|
93
|
+
import { LIVE_FILE_BOUND, LIVE_RETAIN_MS, sweepLiveLogs } from "./live.js";
|
|
94
94
|
import { configPath, addRepos, removeRepos, updateRepos, loadRepos, loadProjectRegistry, updateProjectRegistry } from "./repos.js";
|
|
95
95
|
import { deleteProject, holdingsWords, projectHoldings, projectRunning } from "./project-delete.js";
|
|
96
96
|
import { pushPass } from "./push.js";
|
|
@@ -4671,6 +4671,11 @@ async function reconcileCommand(flags, context) {
|
|
|
4671
4671
|
// day, orphans on mtime, active runs untouchable. Never evidence, never
|
|
4672
4672
|
// load-bearing — a sweep that finds nothing is the common case.
|
|
4673
4673
|
const liveSwept = sweepLiveLogs(store, context.evidenceRoot, clock());
|
|
4674
|
+
// What each agent did last goes by the same rules, from its own row.
|
|
4675
|
+
try {
|
|
4676
|
+
store.sweepRunActivity(clock(), LIVE_RETAIN_MS, LIVE_FILE_BOUND);
|
|
4677
|
+
}
|
|
4678
|
+
catch { /* display state: the next pass sweeps */ }
|
|
4674
4679
|
const worktrees = new WorktreePool(store, {
|
|
4675
4680
|
root: pool,
|
|
4676
4681
|
...(context.gitRunner === undefined ? {} : { runner: context.gitRunner }),
|
package/dist/planner.js
CHANGED
|
@@ -31,6 +31,7 @@ import { TOKEN_ENVS as TELEGRAM_TOKEN_ENVS } from "./telegram.js";
|
|
|
31
31
|
import { evidenceRoot, mailboxName, planFileName, quarantineMailboxes, readMailbox, storeEvidence, writeEvidenceFile, } from "./evidence.js";
|
|
32
32
|
import { MARKER as LEASE_MARKER } from "./worktree.js";
|
|
33
33
|
import { openLiveLog } from "./live.js";
|
|
34
|
+
import { withRunActivity } from "./task-activity.js";
|
|
34
35
|
import { proveTreeUntouched, snapshotIgnored } from "./tree-proof.js";
|
|
35
36
|
import { CLAUDE_LIMITS, ACCEPTANCE_LIMITS } from "./scope.js";
|
|
36
37
|
import { normalizeStructuredJson, storeStructuredAttempt, validationErrorsJson, STRUCTURED_REPAIR_ATTEMPTS, STRUCTURED_REPAIR_MAX_TURNS, STRUCTURED_REPAIR_TIMEOUT_MS, } from "./structured-output.js";
|
|
@@ -590,7 +591,7 @@ export async function plan(store, request) {
|
|
|
590
591
|
}
|
|
591
592
|
// The live window (peek): the same transcript file the builder keeps,
|
|
592
593
|
// so `toolroll peek` and the run page can watch this session too.
|
|
593
|
-
const liveLog = openLiveLog(root, request.runId);
|
|
594
|
+
const liveLog = withRunActivity(openLiveLog(root, request.runId), store, request.runId, clock);
|
|
594
595
|
// The durable run row, not a caller's duplicate options, is the route
|
|
595
596
|
// every correction inherits.
|
|
596
597
|
const provider = logicalRun.provider;
|
|
@@ -730,7 +731,7 @@ export async function plan(store, request) {
|
|
|
730
731
|
...(agent === undefined ? {} : { runner: agent }),
|
|
731
732
|
...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
|
|
732
733
|
clock,
|
|
733
|
-
|
|
734
|
+
onStreamEvent: (event) => liveLog.observe(event),
|
|
734
735
|
});
|
|
735
736
|
const firstProof = await proveAfterInvocation();
|
|
736
737
|
if (firstProof !== null)
|
|
@@ -883,7 +884,7 @@ export async function plan(store, request) {
|
|
|
883
884
|
...(agent === undefined ? {} : { runner: agent }),
|
|
884
885
|
...(request.onProviderSpawn === undefined ? {} : { onSpawn: request.onProviderSpawn }),
|
|
885
886
|
clock,
|
|
886
|
-
|
|
887
|
+
onStreamEvent: (event) => liveLog.observe(event),
|
|
887
888
|
});
|
|
888
889
|
}
|
|
889
890
|
catch (error) {
|
package/dist/serve.d.ts
CHANGED
|
@@ -274,6 +274,11 @@ type Chrome = {
|
|
|
274
274
|
scope?: "all" | "project" | "board-all";
|
|
275
275
|
/** The saturated inbox count — never a sum of unbounded list reads. */
|
|
276
276
|
inboxCount: number;
|
|
277
|
+
/** The project the Tasks count covers: the request's own project view
|
|
278
|
+
* (the open project, or /work's ?project=), null for every admitted project. */
|
|
279
|
+
inboxProject?: string | null;
|
|
280
|
+
/** The Tasks badge's words, naming what it covers: "3 need you in shop". */
|
|
281
|
+
inboxLabel?: string;
|
|
277
282
|
inboxSaturated: boolean;
|
|
278
283
|
settings: boolean;
|
|
279
284
|
/** This database is a demo sandbox: banner every page, spend fenced. */
|