toolroll 0.9.46 → 0.9.48

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/activity-line.d.ts +26 -0
  3. package/dist/activity-line.js +47 -0
  4. package/dist/browser/workspace.js +25 -25
  5. package/dist/browser-workspace.d.ts +18 -1
  6. package/dist/browser-workspace.js +9 -3
  7. package/dist/builder.js +4 -2
  8. package/dist/cli.d.ts +1 -1
  9. package/dist/cli.js +4 -0
  10. package/dist/contracts/cli.d.ts +87 -0
  11. package/dist/contracts/cli.js +2 -0
  12. package/dist/contracts/health.d.ts +84 -0
  13. package/dist/contracts/health.js +11 -0
  14. package/dist/flow-card-task.d.ts +2 -0
  15. package/dist/flow-card-task.js +57 -0
  16. package/dist/flow-live.d.ts +7 -15
  17. package/dist/flow-live.js +77 -115
  18. package/dist/flows-ui.js +15 -1
  19. package/dist/health-cli.d.ts +3 -0
  20. package/dist/health-cli.js +79 -0
  21. package/dist/live-bus.d.ts +89 -0
  22. package/dist/live-bus.js +321 -0
  23. package/dist/live.d.ts +14 -0
  24. package/dist/live.js +71 -22
  25. package/dist/metrics.d.ts +3 -0
  26. package/dist/metrics.js +53 -1
  27. package/dist/operate.js +43 -25
  28. package/dist/planner.js +4 -3
  29. package/dist/read-executor-worker.d.ts +1 -0
  30. package/dist/read-executor-worker.js +26 -0
  31. package/dist/read-executor.d.ts +49 -0
  32. package/dist/read-executor.js +119 -0
  33. package/dist/request-budget.d.ts +1 -0
  34. package/dist/request-budget.js +6 -0
  35. package/dist/serve.d.ts +7 -0
  36. package/dist/serve.js +118 -42
  37. package/dist/server-telemetry.d.ts +86 -0
  38. package/dist/server-telemetry.js +263 -0
  39. package/dist/setup-guide.d.ts +2 -2
  40. package/dist/sqlite-telemetry.d.ts +9 -0
  41. package/dist/sqlite-telemetry.js +98 -0
  42. package/dist/store.d.ts +22 -0
  43. package/dist/store.js +59 -3
  44. package/dist/surface.js +2 -1
  45. package/dist/task-activity.d.ts +27 -0
  46. package/dist/task-activity.js +53 -0
  47. package/dist/task-live.d.ts +8 -0
  48. package/dist/task-live.js +47 -0
  49. package/dist/work-index.js +12 -1
  50. package/dist/workspace-revision.d.ts +2 -0
  51. package/dist/workspace-revision.js +3 -1
  52. 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${project === null ? '' : `?project=${encodeURIComponent(project)}`}`,
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
- const liveLog = openLiveLog(root, request.runId);
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
- ...(liveLog === null ? {} : { onStreamEvent: (event) => liveLog.observe(event) }),
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/cli.d.ts CHANGED
@@ -40,7 +40,7 @@ export type ParseResult = {
40
40
  error: string;
41
41
  };
42
42
  type Write = (line: string) => void;
43
- export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
43
+ export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll health recent server latency, write waits and live streams\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
44
44
  export declare function parseArgs(argv: readonly string[]): ParseResult;
45
45
  /** The commands that operate the queue rather than report on the world. */
46
46
  /** Every verb the CLI routes OUTSIDE runOperate. Exported (with
package/dist/cli.js CHANGED
@@ -28,6 +28,7 @@ import { COMMAND_GUIDE, SURFACE_NOTES, SURFACE_SCHEMA_VERSION } from "./surface.
28
28
  import { runSessionCommand, SESSION_CLI_ACTIONS } from "./session-cli.js";
29
29
  import { maybeRunTeamCommand, TEAM_CLI_ACTIONS } from "./team-cli.js";
30
30
  import { maybeRunRemoteCommand } from "./remote-exec.js";
31
+ import { runHealthCommand } from "./health-cli.js";
31
32
  import { discover, inspectAll } from "./discover.js";
32
33
  import { readPulls } from "./pulls.js";
33
34
  import { detectGraphs, chooseBackend, setupOptions, LABELS as GRAPH_LABELS, } from "./graph.js";
@@ -67,6 +68,7 @@ Usage
67
68
  toolroll up app + builder for every saved project — the normal start
68
69
  toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)
69
70
  toolroll status running, queued, ready results, release check and plan windows
71
+ toolroll health recent server latency, write waits and live streams
70
72
  toolroll integrations which integrations work, and what to do about the ones that don't (--json)
71
73
  toolroll session native coding sessions through the running service
72
74
  toolroll connect save a private connection to your central service
@@ -383,6 +385,8 @@ export async function main(argv, write = line => console.log(line), mainOptions
383
385
  }
384
386
  async function dispatch(argv, write, mainOptions) {
385
387
  // A saved API token sends commands marked remote to the central server, before anything local opens.
388
+ if (argv[0] === 'health')
389
+ return runHealthCommand(argv.slice(1), write, mainOptions.team ?? mainOptions.remote);
386
390
  const central = await maybeRunRemoteCommand(argv, mainOptions.remote ?? mainOptions.team);
387
391
  if (typeof central === "number")
388
392
  return central;
@@ -304,6 +304,93 @@ export declare const COMMAND_OUTPUTS: {
304
304
  reason: z.ZodString;
305
305
  message: z.ZodOptional<z.ZodString>;
306
306
  }, z.core.$loose>], "ok">;
307
+ readonly health: z.ZodDiscriminatedUnion<[z.ZodObject<{
308
+ envelopeVersion: z.ZodLiteral<1>;
309
+ ok: z.ZodLiteral<true>;
310
+ command: z.ZodLiteral<"health">;
311
+ health: z.ZodObject<{
312
+ version: z.ZodLiteral<1>;
313
+ windowSeconds: z.ZodLiteral<300>;
314
+ uptimeSeconds: z.ZodNumber;
315
+ requests: z.ZodObject<{
316
+ count: z.ZodNumber;
317
+ p50Ms: z.ZodNullable<z.ZodNumber>;
318
+ p99Ms: z.ZodNullable<z.ZodNumber>;
319
+ maxMs: z.ZodNullable<z.ZodNumber>;
320
+ totalMs: z.ZodNumber;
321
+ }, z.core.$loose>;
322
+ routes: z.ZodRecord<z.ZodEnum<{
323
+ assets: "assets";
324
+ auth: "auth";
325
+ chat: "chat";
326
+ "chat-stream": "chat-stream";
327
+ cli: "cli";
328
+ console: "console";
329
+ decisions: "decisions";
330
+ "flow-stream": "flow-stream";
331
+ flows: "flows";
332
+ health: "health";
333
+ mcp: "mcp";
334
+ metrics: "metrics";
335
+ other: "other";
336
+ projects: "projects";
337
+ results: "results";
338
+ sessions: "sessions";
339
+ settings: "settings";
340
+ tasks: "tasks";
341
+ team: "team";
342
+ "team-stream": "team-stream";
343
+ }> & z.core.$partial, z.ZodObject<{
344
+ count: z.ZodNumber;
345
+ p50Ms: z.ZodNullable<z.ZodNumber>;
346
+ p99Ms: z.ZodNullable<z.ZodNumber>;
347
+ maxMs: z.ZodNullable<z.ZodNumber>;
348
+ totalMs: z.ZodNumber;
349
+ }, z.core.$loose>>;
350
+ eventLoop: z.ZodObject<{
351
+ count: z.ZodNumber;
352
+ p50Ms: z.ZodNullable<z.ZodNumber>;
353
+ p99Ms: z.ZodNullable<z.ZodNumber>;
354
+ maxMs: z.ZodNullable<z.ZodNumber>;
355
+ totalMs: z.ZodNumber;
356
+ }, z.core.$loose>;
357
+ writes: z.ZodObject<{
358
+ wait: z.ZodObject<{
359
+ count: z.ZodNumber;
360
+ p50Ms: z.ZodNullable<z.ZodNumber>;
361
+ p99Ms: z.ZodNullable<z.ZodNumber>;
362
+ maxMs: z.ZodNullable<z.ZodNumber>;
363
+ totalMs: z.ZodNumber;
364
+ }, z.core.$loose>;
365
+ hold: z.ZodObject<{
366
+ count: z.ZodNumber;
367
+ p50Ms: z.ZodNullable<z.ZodNumber>;
368
+ p99Ms: z.ZodNullable<z.ZodNumber>;
369
+ maxMs: z.ZodNullable<z.ZodNumber>;
370
+ totalMs: z.ZodNumber;
371
+ }, z.core.$loose>;
372
+ statement: z.ZodObject<{
373
+ count: z.ZodNumber;
374
+ p50Ms: z.ZodNullable<z.ZodNumber>;
375
+ p99Ms: z.ZodNullable<z.ZodNumber>;
376
+ maxMs: z.ZodNullable<z.ZodNumber>;
377
+ totalMs: z.ZodNumber;
378
+ }, z.core.$loose>;
379
+ }, z.core.$loose>;
380
+ streams: z.ZodObject<{
381
+ open: z.ZodNumber;
382
+ queuedBytes: z.ZodNumber;
383
+ maxQueuedBytes: z.ZodNumber;
384
+ }, z.core.$loose>;
385
+ budgetRefusals: z.ZodNumber;
386
+ }, z.core.$loose>;
387
+ }, z.core.$loose>, z.ZodObject<{
388
+ envelopeVersion: z.ZodLiteral<1>;
389
+ ok: z.ZodLiteral<false>;
390
+ command: z.ZodLiteral<"health">;
391
+ reason: z.ZodString;
392
+ message: z.ZodOptional<z.ZodString>;
393
+ }, z.core.$loose>], "ok">;
307
394
  readonly integrations: z.ZodDiscriminatedUnion<[z.ZodObject<{
308
395
  envelopeVersion: z.ZodLiteral<1>;
309
396
  ok: z.ZodLiteral<true>;
@@ -12,6 +12,7 @@ import { z } from "zod";
12
12
  import { ENVELOPE_VERSION, envelopeJson } from "../envelope.js";
13
13
  import { parseContract } from "./contract.js";
14
14
  import { LISTED_TASK_STATES } from "./lead-tools.js";
15
+ import { healthSnapshotSchema } from './health.js';
15
16
  const str = z.string();
16
17
  const int = z.number().int();
17
18
  const list = z.array(z.unknown());
@@ -167,6 +168,7 @@ export const COMMAND_OUTPUTS = {
167
168
  contract: contractOutput,
168
169
  scan: scanOutput,
169
170
  status: statusOutput,
171
+ health: answers('health', { health: healthSnapshotSchema }),
170
172
  integrations: integrationsOutput,
171
173
  ready: z.union([
172
174
  answers("ready", { count: int, tasks: rows({ id: str, title: str, state: str }) }),
@@ -0,0 +1,84 @@
1
+ import { z } from 'zod';
2
+ export declare const latencySummarySchema: z.ZodObject<{
3
+ count: z.ZodNumber;
4
+ p50Ms: z.ZodNullable<z.ZodNumber>;
5
+ p99Ms: z.ZodNullable<z.ZodNumber>;
6
+ maxMs: z.ZodNullable<z.ZodNumber>;
7
+ totalMs: z.ZodNumber;
8
+ }, z.core.$loose>;
9
+ export declare const healthSnapshotSchema: z.ZodObject<{
10
+ version: z.ZodLiteral<1>;
11
+ windowSeconds: z.ZodLiteral<300>;
12
+ uptimeSeconds: z.ZodNumber;
13
+ requests: z.ZodObject<{
14
+ count: z.ZodNumber;
15
+ p50Ms: z.ZodNullable<z.ZodNumber>;
16
+ p99Ms: z.ZodNullable<z.ZodNumber>;
17
+ maxMs: z.ZodNullable<z.ZodNumber>;
18
+ totalMs: z.ZodNumber;
19
+ }, z.core.$loose>;
20
+ routes: z.ZodRecord<z.ZodEnum<{
21
+ assets: "assets";
22
+ auth: "auth";
23
+ chat: "chat";
24
+ "chat-stream": "chat-stream";
25
+ cli: "cli";
26
+ console: "console";
27
+ decisions: "decisions";
28
+ "flow-stream": "flow-stream";
29
+ flows: "flows";
30
+ health: "health";
31
+ mcp: "mcp";
32
+ metrics: "metrics";
33
+ other: "other";
34
+ projects: "projects";
35
+ results: "results";
36
+ sessions: "sessions";
37
+ settings: "settings";
38
+ tasks: "tasks";
39
+ team: "team";
40
+ "team-stream": "team-stream";
41
+ }> & z.core.$partial, z.ZodObject<{
42
+ count: z.ZodNumber;
43
+ p50Ms: z.ZodNullable<z.ZodNumber>;
44
+ p99Ms: z.ZodNullable<z.ZodNumber>;
45
+ maxMs: z.ZodNullable<z.ZodNumber>;
46
+ totalMs: z.ZodNumber;
47
+ }, z.core.$loose>>;
48
+ eventLoop: z.ZodObject<{
49
+ count: z.ZodNumber;
50
+ p50Ms: z.ZodNullable<z.ZodNumber>;
51
+ p99Ms: z.ZodNullable<z.ZodNumber>;
52
+ maxMs: z.ZodNullable<z.ZodNumber>;
53
+ totalMs: z.ZodNumber;
54
+ }, z.core.$loose>;
55
+ writes: z.ZodObject<{
56
+ wait: z.ZodObject<{
57
+ count: z.ZodNumber;
58
+ p50Ms: z.ZodNullable<z.ZodNumber>;
59
+ p99Ms: z.ZodNullable<z.ZodNumber>;
60
+ maxMs: z.ZodNullable<z.ZodNumber>;
61
+ totalMs: z.ZodNumber;
62
+ }, z.core.$loose>;
63
+ hold: z.ZodObject<{
64
+ count: z.ZodNumber;
65
+ p50Ms: z.ZodNullable<z.ZodNumber>;
66
+ p99Ms: z.ZodNullable<z.ZodNumber>;
67
+ maxMs: z.ZodNullable<z.ZodNumber>;
68
+ totalMs: z.ZodNumber;
69
+ }, z.core.$loose>;
70
+ statement: z.ZodObject<{
71
+ count: z.ZodNumber;
72
+ p50Ms: z.ZodNullable<z.ZodNumber>;
73
+ p99Ms: z.ZodNullable<z.ZodNumber>;
74
+ maxMs: z.ZodNullable<z.ZodNumber>;
75
+ totalMs: z.ZodNumber;
76
+ }, z.core.$loose>;
77
+ }, z.core.$loose>;
78
+ streams: z.ZodObject<{
79
+ open: z.ZodNumber;
80
+ queuedBytes: z.ZodNumber;
81
+ maxQueuedBytes: z.ZodNumber;
82
+ }, z.core.$loose>;
83
+ budgetRefusals: z.ZodNumber;
84
+ }, z.core.$loose>;
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ import { ROUTE_FAMILIES } from '../server-telemetry.js';
3
+ const count = z.number().int().nonnegative();
4
+ const milliseconds = z.number().nonnegative();
5
+ export const latencySummarySchema = z.looseObject({ count, p50Ms: milliseconds.nullable(), p99Ms: milliseconds.nullable(), maxMs: milliseconds.nullable(), totalMs: milliseconds });
6
+ export const healthSnapshotSchema = z.looseObject({
7
+ version: z.literal(1), windowSeconds: z.literal(300), uptimeSeconds: z.number().nonnegative(),
8
+ requests: latencySummarySchema, routes: z.partialRecord(z.enum(ROUTE_FAMILIES), latencySummarySchema), eventLoop: latencySummarySchema,
9
+ writes: z.looseObject({ wait: latencySummarySchema, hold: latencySummarySchema, statement: latencySummarySchema }),
10
+ streams: z.looseObject({ open: count, queuedBytes: count, maxQueuedBytes: count }), budgetRefusals: count,
11
+ });
@@ -0,0 +1,2 @@
1
+ import type { Store } from "./store.js";
2
+ export declare function flowCardTaskLine(store: Store, taskId: string, repo: string, now: Date, secrets?: readonly string[]): string;
@@ -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
+ }
@@ -1,24 +1,16 @@
1
- import type { ServerResponse } from "node:http";
1
+ import { type LiveRoomOptions, type LiveRooms, type LiveViewer } from "./live-bus.js";
2
2
  import type { Store } from "./store.js";
3
3
  export type FlowPresence = {
4
4
  name: string;
5
5
  cards: number[];
6
6
  editing: boolean;
7
7
  };
8
- export type FlowViewer = {
9
- name: string;
8
+ export type FlowViewer = LiveViewer & {
10
9
  card: number | null;
11
10
  editing: boolean;
12
- response: ServerResponse;
13
- valid: () => boolean;
14
- };
15
- /** What changes when anything on the canvas changes: one row, one read, kept short and opaque. */
16
- export declare function flowFingerprint(store: Store, flow: number): string | null;
17
- export type FlowRooms = {
18
- /** Keep this page's stream on the flow until it closes; the first event says where the flow stands. */
19
- join: (flow: number, viewer: FlowViewer) => void;
20
- /** Every stream, ended (the server is closing). */
21
- close: () => void;
22
- size: () => number;
23
11
  };
24
- export declare function createFlowRooms(fingerprint: (flow: number) => string | null, tickMs?: number): FlowRooms;
12
+ /** What changes when anything on the canvas changes: metadata only, kept short and opaque. */
13
+ export declare function flowFingerprint(store: Store, flow: number, now?: Date): string | null;
14
+ export type FlowRooms = LiveRooms<number, FlowViewer>;
15
+ /** Rooms keyed by flow. With a bus, a write reaches every open page at once; without one, only the safety net. */
16
+ export declare function createFlowRooms(fingerprint: (flow: number) => string | null, options?: LiveRoomOptions): FlowRooms;