toolroll 0.9.6 → 0.9.8

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 (102) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/dist/actor.d.ts +21 -0
  3. package/dist/actor.js +48 -0
  4. package/dist/assignment-adapters.d.ts +2 -0
  5. package/dist/assignment-adapters.js +2 -0
  6. package/dist/assignment-ui.js +3 -1
  7. package/dist/assignment.d.ts +15 -0
  8. package/dist/assignment.js +80 -16
  9. package/dist/browser/workspace.css +2 -2
  10. package/dist/browser/workspace.js +28 -24
  11. package/dist/browser-workspace.d.ts +117 -2
  12. package/dist/build-review.d.ts +38 -0
  13. package/dist/build-review.js +341 -0
  14. package/dist/builder.d.ts +1 -0
  15. package/dist/builder.js +33 -13
  16. package/dist/chat-actions.js +2 -1
  17. package/dist/chat-delivery.js +2 -0
  18. package/dist/chat-quiet.js +11 -1
  19. package/dist/chat-task-actions.d.ts +13 -0
  20. package/dist/chat-task-actions.js +19 -1
  21. package/dist/check-levels-ui.d.ts +26 -0
  22. package/dist/check-levels-ui.js +84 -0
  23. package/dist/check-levels.d.ts +67 -0
  24. package/dist/check-levels.js +171 -0
  25. package/dist/claim.d.ts +22 -0
  26. package/dist/claim.js +43 -0
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +6 -0
  29. package/dist/demo-chat.js +4 -1
  30. package/dist/dispatch.js +9 -3
  31. package/dist/dispose.d.ts +12 -3
  32. package/dist/dispose.js +19 -5
  33. package/dist/exec.d.ts +5 -0
  34. package/dist/exec.js +30 -4
  35. package/dist/export.js +3 -2
  36. package/dist/first-run.d.ts +34 -0
  37. package/dist/first-run.js +37 -1
  38. package/dist/flow-gallery-ui.d.ts +30 -0
  39. package/dist/flow-gallery-ui.js +85 -0
  40. package/dist/flow-gallery.d.ts +111 -0
  41. package/dist/flow-gallery.js +445 -0
  42. package/dist/flow-share.d.ts +102 -0
  43. package/dist/flow-share.js +614 -0
  44. package/dist/flow-starters.d.ts +2 -0
  45. package/dist/flow-starters.js +1 -1
  46. package/dist/flow-steps.js +7 -0
  47. package/dist/flow-triggers.js +2 -0
  48. package/dist/flows-cli.d.ts +56 -0
  49. package/dist/flows-cli.js +109 -3
  50. package/dist/flows-ui.d.ts +15 -1
  51. package/dist/flows-ui.js +31 -6
  52. package/dist/guides.js +27 -2
  53. package/dist/invoke.js +5 -0
  54. package/dist/lead-context.js +6 -2
  55. package/dist/mate-doors.d.ts +0 -15
  56. package/dist/mate-doors.js +5 -1
  57. package/dist/mate-tools.js +9 -4
  58. package/dist/models-ui.js +1 -1
  59. package/dist/modes.d.ts +3 -1
  60. package/dist/modes.js +3 -1
  61. package/dist/operate.d.ts +4 -1
  62. package/dist/operate.js +711 -98
  63. package/dist/process-custody.js +8 -0
  64. package/dist/project-concurrency.d.ts +62 -0
  65. package/dist/project-concurrency.js +116 -0
  66. package/dist/project-delete-ui.d.ts +7 -0
  67. package/dist/project-delete-ui.js +19 -2
  68. package/dist/project-delete.js +4 -0
  69. package/dist/provider.js +16 -0
  70. package/dist/pull-request-flow.d.ts +9 -0
  71. package/dist/pull-request-flow.js +21 -3
  72. package/dist/result-follow-ups.d.ts +101 -0
  73. package/dist/result-follow-ups.js +333 -0
  74. package/dist/retention-ui.d.ts +3 -1
  75. package/dist/retention-ui.js +4 -4
  76. package/dist/retention.d.ts +13 -6
  77. package/dist/retention.js +50 -12
  78. package/dist/review-switch.d.ts +41 -0
  79. package/dist/review-switch.js +88 -0
  80. package/dist/reviewer.d.ts +24 -2
  81. package/dist/reviewer.js +49 -1
  82. package/dist/scope.js +11 -0
  83. package/dist/serve.d.ts +211 -2
  84. package/dist/serve.js +1038 -145
  85. package/dist/store.d.ts +112 -4
  86. package/dist/store.js +447 -27
  87. package/dist/surface.js +36 -9
  88. package/dist/tailnet.d.ts +11 -0
  89. package/dist/tailnet.js +47 -0
  90. package/dist/task-outcome-cli.js +2 -2
  91. package/dist/task-status.d.ts +13 -3
  92. package/dist/task-status.js +38 -16
  93. package/dist/team-cli.js +3 -0
  94. package/dist/telegram-status.js +2 -2
  95. package/dist/verification-evidence.js +21 -1
  96. package/dist/webhooks.js +5 -0
  97. package/dist/work-index.js +39 -6
  98. package/dist/work-summary.d.ts +0 -4
  99. package/dist/work-summary.js +4 -1
  100. package/dist/workspace-ui.d.ts +6 -1
  101. package/dist/workspace-ui.js +14 -11
  102. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -65,6 +65,7 @@ Usage
65
65
  toolroll session native coding sessions through the running service
66
66
  toolroll connect save a private connection to your central service
67
67
  toolroll lead named leads on the connected service
68
+ toolroll lead token mint your lead agent's credential: its own work pings nobody
68
69
  toolroll conversation shared and private conversations on that service
69
70
  toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)
70
71
 
@@ -192,6 +193,8 @@ export const TOP_LEVEL_COMMANDS = [
192
193
  ];
193
194
  export const OPERATE_COMMANDS = new Set([
194
195
  "up",
196
+ // Only `lead token` reaches here; the central service's lead verbs answer first.
197
+ "lead",
195
198
  "onboard",
196
199
  "status",
197
200
  "ready",
@@ -210,6 +213,7 @@ export const OPERATE_COMMANDS = new Set([
210
213
  "restore",
211
214
  "export",
212
215
  "policy",
216
+ "review",
213
217
  "assignment",
214
218
  "knowledge",
215
219
  "memory",
@@ -240,6 +244,7 @@ export const OPERATE_COMMANDS = new Set([
240
244
  "bridge",
241
245
  "publish",
242
246
  "reconcile",
247
+ "run",
243
248
  "routine",
244
249
  "flows",
245
250
  "config", "chat", "proposals", "mode", "people", "keys",
@@ -1275,6 +1280,7 @@ async function runDemoCommand(argv, write) {
1275
1280
  write(` sandbox ${sandbox}`);
1276
1281
  write("");
1277
1282
  write("Open Chat and ask for a change, like \"fix the flaky refund test\".");
1283
+ write("For your own project, run `npx toolroll up` in its folder.");
1278
1284
  write("");
1279
1285
  write(keep
1280
1286
  ? "Ctrl-C stops the server; the sandbox is KEPT. It stays a demo forever — every spending command refuses it."
package/dist/demo-chat.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * rendered as plain server HTML with native forms. One small script keeps a
4
4
  * running build's progress live; without it the page still works by reload.
5
5
  */
6
+ import { START_COMMAND } from "./first-run.js";
6
7
  import { headlineOf } from "./task-status.js";
7
8
  const SUGGESTIONS = ["Fix the flaky refund test", "Rewrite the empty Payouts page", "Put the new invoice view behind a flag"];
8
9
  const esc = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -100,7 +101,9 @@ export function demoThreadHtml(exchanges, csrf, resultOf) {
100
101
  if (exchange.state === "ready" || exchange.state === "complete" || exchange.state === "sent-back")
101
102
  parts.push(resultCard(exchange, csrf, resultOf(exchange)));
102
103
  if (exchange.state === "complete") {
103
- parts.push(`<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`);
104
+ parts.push(`<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`, `<section class="card demo-handoff" data-demo-handoff aria-label="Your own project"><h2>Now try it on your own project</h2>` +
105
+ `<p>In your repository's folder, run:</p><pre class="demo-command"><code>${esc(START_COMMAND)}</code></pre>` +
106
+ `<p class="meta">It opens in your browser, already signed in.</p></section>`);
104
107
  }
105
108
  return `<article class="demo-turn" id="demo-${exchange.id}">${parts.join("")}</article>`;
106
109
  }).join("");
package/dist/dispatch.js CHANGED
@@ -12,6 +12,8 @@ import { approvalOf } from "./scope.js";
12
12
  import { plannerSourceProblemOf } from "./planner-source.js";
13
13
  import { authPauseOf, authWaitOf, providerName, signInReason } from "./provider-auth.js";
14
14
  import { REVIEW_TOKENS, resultStatusOf, reviewFactsOf } from "./workspace-ui.js";
15
+ import { replacedWords } from "./task-status.js";
16
+ import { buildReviewOf } from "./review-switch.js";
15
17
  import { BUILT_IN, parseCapabilityKey } from "./store.js";
16
18
  export const DEFAULT_MAX_OPEN_DECISIONS = 5;
17
19
  /**
@@ -186,7 +188,7 @@ export function diagnoseTaskDispatch(store, taskId, now) {
186
188
  const reviewFacts = reviewFactsOf(retry, runner => {
187
189
  const reviewer = store.getRunner(runner)?.runner;
188
190
  return reviewer !== undefined && isAlive(reviewer, now);
189
- });
191
+ }, result !== undefined && buildReviewOf(store, result.id)?.state === "pending");
190
192
  // A no-change conclusion owes no proof beyond its handoff and sealed
191
193
  // diff (the task page's own "attested floor"); with both on record it
192
194
  // is complete, not verification-needed.
@@ -210,8 +212,12 @@ export function diagnoseTaskDispatch(store, taskId, now) {
210
212
  ? `${status.detail} Its independent review succeeded on attempt ${review.attempt} of ${review.cap}.`
211
213
  : status.detail, { action: "open-result", review });
212
214
  }
213
- if (task.state === "cancelled")
214
- return answer("cancelled", "terminal", "Cancelled", "Nothing else will run for this task.");
215
+ if (task.state === "cancelled") {
216
+ // Replaced, never "Cancelled": a successor carries the work on.
217
+ const successor = store.replacementOf(taskId);
218
+ return successor === null ? answer("cancelled", "terminal", "Cancelled", "Nothing else will run for this task.")
219
+ : answer("cancelled", "terminal", replacedWords(successor), `${replacedWords(successor)}. Nothing else will run for this task.`);
220
+ }
215
221
  if (task.state === "failed")
216
222
  return answer("failed", "terminal", "Needs a retry", "The last attempt stopped; review its incident, then retry it.", { action: "retry-task" });
217
223
  if (store.hasLiveClaim(ref.id, now))
package/dist/dispose.d.ts CHANGED
@@ -73,13 +73,22 @@ export type Disposition = {
73
73
  kind: "stopped";
74
74
  stopRun: number;
75
75
  requeued: boolean;
76
+ }
77
+ /** The service stopped or restarted while the attempt ran (no one stopped
78
+ * the task): its work is preserved, the run ends as interrupted, and the
79
+ * task is back in the queue — no strike, no backoff — so the next builder
80
+ * pass resumes it from the kept work. */
81
+ | {
82
+ kind: "handed-back";
83
+ requeued: boolean;
76
84
  } | {
77
85
  kind: "invariant";
78
86
  reason: string;
79
87
  };
80
- /** Compatibility for historical integrations: finished work is handed to the
81
- * lead. No model review is requested and no signed history is rewritten. */
82
- export declare function maybeRequestAutoReview(_store: Store, _repo: string, _runId: number, _committed: boolean, _noChange: boolean, _now: Date): void;
88
+ /** A committed build whose approved check passed gets the project's one
89
+ * automatic review when its review switch is on (build-review.ts). No-change
90
+ * results and failed checks go straight to the person, as before. */
91
+ export declare function maybeRequestAutoReview(store: Store, repo: string, runId: number, committed: boolean, noChange: boolean, now: Date, root?: string): void;
83
92
  export declare function disposeBuildOutcome(context: DisposeContext, result: BuildResult): Disposition;
84
93
  /**
85
94
  * The stale-approval HOLD, shared by the post-build disposition above and
package/dist/dispose.js CHANGED
@@ -14,10 +14,12 @@ import { originalTaskBase, focusedTestCommandSupported } from "./observations.js
14
14
  * inlined before.
15
15
  */
16
16
  import { createHash } from "node:crypto";
17
- import { completeFenced, finalizeFailureFenced, finalizeMalformedFenced, finalizeParkFenced, interruptIfStopped, release, } from "./claim.js";
17
+ import { completeFenced, finalizeFailureFenced, finalizeHandBackFenced, finalizeMalformedFenced, finalizeParkFenced, interruptIfStopped, release, } from "./claim.js";
18
18
  import { bodyHashOf, publicationBody } from "./publish.js";
19
19
  import { modeTermsFromJson } from "./modes.js";
20
- import { readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
20
+ import { evidenceRoot, readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
21
+ import { queueBuildReview } from "./build-review.js";
22
+ import { homedir } from "node:os";
21
23
  import {} from "./proof.js";
22
24
  import { verificationEvidence, failedVerificationEvidence } from "./verification-evidence.js";
23
25
  import { classifyGateFailure, describeGateFailure, gateFailureSummary } from "./gate-failure.js";
@@ -55,9 +57,14 @@ const STANDALONE_BROKE_REASONS = new Set([
55
57
  "timeout",
56
58
  "git",
57
59
  ]);
58
- /** Compatibility for historical integrations: finished work is handed to the
59
- * lead. No model review is requested and no signed history is rewritten. */
60
- export function maybeRequestAutoReview(_store, _repo, _runId, _committed, _noChange, _now) { }
60
+ /** A committed build whose approved check passed gets the project's one
61
+ * automatic review when its review switch is on (build-review.ts). No-change
62
+ * results and failed checks go straight to the person, as before. */
63
+ export function maybeRequestAutoReview(store, repo, runId, committed, noChange, now, root = evidenceRoot(homedir())) {
64
+ if (!committed || noChange)
65
+ return;
66
+ queueBuildReview(store, repo, runId, root, now);
67
+ }
61
68
  export function disposeBuildOutcome(context, result) {
62
69
  // Stop and every terminal side effect compete under the same SQLite
63
70
  // write lock. A separate preliminary transaction leaves a completion gap.
@@ -238,6 +245,7 @@ function disposeBuildOutcomeLocked(context, result) {
238
245
  // proves the credential, clearing any quota stamp.
239
246
  store.resetStrikes(taskRef);
240
247
  store.clearQuota(runner, provider, model ?? "");
248
+ maybeRequestAutoReview(store, repo, runId, result.committed, result.noChange === true, clock(), context.evidenceRoot);
241
249
  return { kind: "built", committed: result.committed, noChange: result.noChange === true };
242
250
  }
243
251
  store.transact(() => {
@@ -338,6 +346,12 @@ function disposeBuildOutcomeLocked(context, result) {
338
346
  });
339
347
  return { kind: "malformed", sealed: sealed.ok };
340
348
  }
349
+ // A stop no operator asked for is the SERVICE stopping (the fence above
350
+ // already settled every task stop): hand the attempt back, never strike it.
351
+ if (result.reason === "stopped" && policy === "tick" && leaseId !== undefined && store.getRun(runId)?.role === "builder") {
352
+ const handed = finalizeHandBackFenced(store, { leaseId, runId, taskId, message: result.message, now: clock() });
353
+ return { kind: "handed-back", requeued: handed.requeued };
354
+ }
341
355
  if (TICK_FAILURE_REASONS.has(result.reason)) {
342
356
  // One fenced transaction decides what the failure means. The
343
357
  // classification trusts only what the machine itself observed.
package/dist/exec.d.ts CHANGED
@@ -113,6 +113,11 @@ export type RunOptions = ProcessTreeObserver & {
113
113
  /** Fires once the child exists, with its pid (the process-group id when
114
114
  * processGroup is set) — the slot ledger records it (v14 finding 26). */
115
115
  onSpawn?: (pid: number) => void;
116
+ /** Fires instead of onSpawn when a spawn that beforeSpawn admitted made no
117
+ * process at all (the OS call threw — ENOENT, EAGAIN — returned no pid, or
118
+ * its OS object could not be made), so its reserved witness can be settled
119
+ * as never started in the same step. Never fires once a child exists. */
120
+ onSpawnFailed?: () => void;
116
121
  /** Fires before any target spawn, so a crash cannot lose its OS object. */
117
122
  onContainer?: (info: {
118
123
  backend: ContainmentBackendId;
package/dist/exec.js CHANGED
@@ -84,9 +84,24 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
84
84
  throw new ContainmentRefusal(effective.refusal);
85
85
  if (bag.beforeSpawn?.() === false)
86
86
  throw new Error("the attempt stopped before this process could spawn");
87
- const made = effective === null ? { container: null } : createContainer(effective, label);
88
- if ("refused" in made)
87
+ // From here a witness may be reserved: every road that makes no process
88
+ // says so through onSpawnFailed before it returns or throws.
89
+ const neverStarted = () => { try {
90
+ bag.onSpawnFailed?.();
91
+ }
92
+ catch { /* the reservation stays unproven */ } };
93
+ let made;
94
+ try {
95
+ made = effective === null ? { container: null } : createContainer(effective, label);
96
+ }
97
+ catch (error) {
98
+ neverStarted();
99
+ throw error;
100
+ }
101
+ if ("refused" in made) {
102
+ neverStarted();
89
103
  throw new ContainmentRefusal(made.refused);
104
+ }
90
105
  const container = made.container;
91
106
  try {
92
107
  if (container !== null)
@@ -94,6 +109,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
94
109
  }
95
110
  catch (error) {
96
111
  container?.release();
112
+ neverStarted();
97
113
  throw error;
98
114
  }
99
115
  let launch;
@@ -102,6 +118,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
102
118
  }
103
119
  catch (error) {
104
120
  container?.release();
121
+ neverStarted();
105
122
  throw error;
106
123
  }
107
124
  let child;
@@ -123,6 +140,7 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
123
140
  }
124
141
  catch { }
125
142
  container?.release();
143
+ neverStarted();
126
144
  throw error;
127
145
  }
128
146
  if (container !== null) {
@@ -143,6 +161,12 @@ function spawnContained(file, args, spawnOptions, label, bag, contain = true) {
143
161
  if (child.pid !== undefined) {
144
162
  bag.onSpawn?.(child.pid);
145
163
  }
164
+ else if (container === null) {
165
+ // No pid: the OS refused the spawn (ENOENT, EACCES) and only an
166
+ // 'error' event follows. A contained spawn settles once its object is
167
+ // proven empty, and its transport's return finishes the witness.
168
+ neverStarted();
169
+ }
146
170
  }
147
171
  catch (error) {
148
172
  throw new SpawnCustodyFailure(child, error);
@@ -201,7 +225,7 @@ function containmentOf(container, empty, attachFailure) {
201
225
  }
202
226
  /** The streaming transports' spawn: contained when the spawn is a provider (process group), plain otherwise. */
203
227
  function spawnStream(file, args, options, stdio, childEnv) {
204
- return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence }, options.processGroup === true);
228
+ return spawnContained(file, args, { cwd: options.cwd, env: childEnv, stdio, detached: options.processGroup === true && process.platform !== "win32" }, options.owner ?? "stream", { beforeSpawn: options.beforeSpawn, onSpawn: options.onSpawn, onSpawnFailed: options.onSpawnFailed, onContainer: options.onContainer, onContainerEmpty: options.onContainerEmpty, onUnknown: options.onUnknown, fence: options.fence }, options.processGroup === true);
205
229
  }
206
230
  /** The refusal-before-spawn shape shared by every containable transport. */
207
231
  function refusedResult(error) {
@@ -506,6 +530,7 @@ export function run(file, args, options = {}) {
506
530
  ...(cwd === undefined ? {} : { cwd }),
507
531
  ...(childEnv === undefined ? {} : { childEnv }),
508
532
  ...(options.onSpawn === undefined ? {} : { onSpawn: options.onSpawn }),
533
+ ...(options.onSpawnFailed === undefined ? {} : { onSpawnFailed: options.onSpawnFailed }),
509
534
  ...(options.owner === undefined ? {} : { owner: options.owner }),
510
535
  ...(options.beforeSpawn === undefined ? {} : { beforeSpawn: options.beforeSpawn }),
511
536
  ...(options.onStdout === undefined ? {} : { onStdout: options.onStdout }),
@@ -553,7 +578,7 @@ function runBufferedGroup(file, args, bag) {
553
578
  let container = null;
554
579
  let attached;
555
580
  try {
556
- const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence });
581
+ const spawned = spawnContained(file, args, { cwd: bag.cwd, env: bag.childEnv, stdio: [bag.stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], detached: process.platform !== "win32" }, bag.owner ?? "buffered", { beforeSpawn: bag.beforeSpawn, onSpawn: bag.onSpawn, onSpawnFailed: bag.onSpawnFailed, onContainer: bag.onContainer, onContainerEmpty: bag.onContainerEmpty, onUnknown: bag.onUnknown, fence: bag.fence });
557
582
  child = spawned.child;
558
583
  container = spawned.container;
559
584
  attached = spawned.attached;
@@ -1578,6 +1603,7 @@ export function startClaudeHeldSession(file, args, options) {
1578
1603
  }, options.owner ?? "held", {
1579
1604
  beforeSpawn: options.beforeSpawn,
1580
1605
  onSpawn: options.onSpawn,
1606
+ onSpawnFailed: options.onSpawnFailed,
1581
1607
  onContainer: options.onContainer,
1582
1608
  onContainerEmpty: options.onContainerEmpty,
1583
1609
  onUnknown: options.onUnknown,
package/dist/export.js CHANGED
@@ -33,7 +33,7 @@ const SECRET_TABLES = new Set(["web_session", "ceremony_nonce", "telegram_pairin
33
33
  const DERIVED_TABLE = /^(sqlite_|memory_search)/;
34
34
  /** Columns that hold a secret (or what stands in for one): never exported. */
35
35
  const SECRET_COLUMNS = new Set([
36
- "api_token.secret_hash", "approver.credential_hash", "coordinator_credential.credential_hash", "runner.credential_hash", "invite.token_hash",
36
+ "api_token.secret_hash", "lead_credential.secret_hash", "approver.credential_hash", "coordinator_credential.credential_hash", "runner.credential_hash", "invite.token_hash",
37
37
  "flow_trigger.hook_hash", "held_session.cookie", "push_subscription.endpoint", "push_subscription.p256dh", "push_subscription.auth",
38
38
  "chat_turn.credential_key", "mate_session.credential_key", "mate_turn.credential_key", "bridge_lease.push_url", "workflow_preview.token",
39
39
  "quota.credential_fp",
@@ -56,7 +56,7 @@ export function categoryOf(table) {
56
56
  [/^teammate/, "teammates"],
57
57
  [/^(flow|routine|workflow_)/, "flows"],
58
58
  [/^(chat_|mate_|team_|telegram_|slack_|discord_|teams_|notification|push_|bridge_lease)/, "chats"],
59
- [/^(approver|api_token|invite|sso_identity|coordinator_)/, "people"],
59
+ [/^(approver|api_token|lead_credential|invite|sso_identity|coordinator_)/, "people"],
60
60
  [/^(run|artifact|claim|worktree$|execution_slot|contest|tournament_terms|fallback_cycle|fallback_transition|held_session|session_turn|attended_authorization|criterion_review|diff_comment|proof_|review_request|repair_chain|incident|publication$|merge_|side_spend|knowledge_snapshot|learning_snapshot|skill_snapshot)/, "runs"],
61
61
  [/^(task|hold$|plan_|scope_|tool_seal|decision|external_|skill_test)/, "tasks"],
62
62
  [/^(project|approval_policy|capability|verify_command|worktree_setup|backend_grant|intake_grant|publication_grant|operating_mode|knowledge_|learning_|memory_|skill_)/, "projects"],
@@ -67,6 +67,7 @@ export function categoryOf(table) {
67
67
  /** Extra shapes beside the evidence scanner's: this installation's own tokens and password hashes, and bearer headers. */
68
68
  const EXTRA_SHAPES = [
69
69
  /\bso_[a-f0-9]{12}_[A-Za-z0-9_-]{43}/g,
70
+ /\blt_[a-f0-9]{12}_[A-Za-z0-9_-]{43}/g,
70
71
  /\bscrypt\$[^\s"',;]+/g,
71
72
  /\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/gi,
72
73
  /\bsk-[A-Za-z0-9_-]{20,}/g,
@@ -1,6 +1,20 @@
1
1
  import type { Runner } from "./backend.js";
2
2
  export declare const SIGN_IN_COMMAND = "claude auth login";
3
3
  export declare const SANDBOX_COMMAND = "npx toolroll demo";
4
+ /** How a person starts on a real project (the demo hands off to this). */
5
+ export declare const START_COMMAND = "npx toolroll up";
6
+ /** How it works, once, above the composer until the first request. */
7
+ export declare const HOW_IT_WORKS = "Ask for a change. The lead writes a short plan; you approve it; an agent builds it on its own branch; it's Ready when your tests pass.";
8
+ type SignInState = "connected" | "signed-out" | "not-installed" | "unverified" | "key-present" | "key-works" | "key-refused" | "missing-key";
9
+ /**
10
+ * The one command that gets an agent signed in on this computer: sign in to the CLI that's installed (Claude Code
11
+ * first, then Codex), or install Claude Code and sign in, written for this operating system's shell. Unknown states
12
+ * (not checked yet) get the install-and-sign-in command, which is right on a fresh machine and harmless otherwise.
13
+ */
14
+ export declare function signInCommandFor(states: {
15
+ claude: SignInState;
16
+ codex: SignInState;
17
+ } | null, platform: NodeJS.Platform): string;
4
18
  export type FirstRunAction = {
5
19
  kind: "link";
6
20
  label: string;
@@ -23,6 +37,7 @@ export type FirstRunFacts = {
23
37
  projects: number;
24
38
  hasTask: boolean;
25
39
  firstResultAt: string | null;
40
+ signInCommand?: string;
26
41
  };
27
42
  /** The three steps, each done or with the one action that does it; null once the first result has arrived. */
28
43
  export declare function firstRunSteps(facts: FirstRunFacts): FirstRunStep[] | null;
@@ -58,3 +73,22 @@ export declare const TODO_GREP_ARGS: readonly string[];
58
73
  export declare function findFirstTasks(repo: string, runner?: Runner): Promise<FirstTaskSuggestion[]>;
59
74
  /** "First result in 7 min": how long after the installation began its first Ready result arrived. */
60
75
  export declare function firstResultWords(since: string, at: string): string | null;
76
+ /** The first task's five steps, as a person reads them. */
77
+ export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "You approve"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
78
+ export type JourneyKey = (typeof JOURNEY_STEPS)[number][0];
79
+ /** `stuck`: the step it stopped at (Failed, Stopped, or waiting on something other than the approval). */
80
+ export type JourneyStep = {
81
+ key: JourneyKey;
82
+ label: string;
83
+ state: "done" | "current" | "stuck" | "todo";
84
+ };
85
+ /**
86
+ * Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
87
+ * (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
88
+ * the build finished with a result.
89
+ */
90
+ export declare function firstTaskJourney(read: {
91
+ stage: "queued" | "planning" | "needs-you" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
92
+ need?: string | undefined;
93
+ }, approved: boolean, built?: boolean): JourneyStep[];
94
+ export {};
package/dist/first-run.js CHANGED
@@ -9,6 +9,23 @@ import { run } from "./exec.js";
9
9
  import { githubIssues } from "./issues.js";
10
10
  export const SIGN_IN_COMMAND = "claude auth login";
11
11
  export const SANDBOX_COMMAND = "npx toolroll demo";
12
+ /** How a person starts on a real project (the demo hands off to this). */
13
+ export const START_COMMAND = "npx toolroll up";
14
+ /** How it works, once, above the composer until the first request. */
15
+ export const HOW_IT_WORKS = "Ask for a change. The lead writes a short plan; you approve it; an agent builds it on its own branch; it's Ready when your tests pass.";
16
+ /**
17
+ * The one command that gets an agent signed in on this computer: sign in to the CLI that's installed (Claude Code
18
+ * first, then Codex), or install Claude Code and sign in, written for this operating system's shell. Unknown states
19
+ * (not checked yet) get the install-and-sign-in command, which is right on a fresh machine and harmless otherwise.
20
+ */
21
+ export function signInCommandFor(states, platform) {
22
+ const installed = (state) => state !== undefined && state !== "not-installed";
23
+ if (installed(states?.claude))
24
+ return SIGN_IN_COMMAND;
25
+ if (installed(states?.codex))
26
+ return "codex login";
27
+ return `npm install -g @anthropic-ai/claude-code${platform === "win32" ? ";" : " &&"} ${SIGN_IN_COMMAND}`;
28
+ }
12
29
  /** The three steps, each done or with the one action that does it; null once the first result has arrived. */
13
30
  export function firstRunSteps(facts) {
14
31
  if (facts.firstResultAt !== null)
@@ -16,7 +33,7 @@ export function firstRunSteps(facts) {
16
33
  const project = facts.projects > 0;
17
34
  return [
18
35
  facts.agentSignedIn === null ? { key: "agent", title: "Agent signed in", done: false, action: null, checking: true }
19
- : { key: "agent", title: "Agent signed in", done: facts.agentSignedIn, action: facts.agentSignedIn ? null : { kind: "command", command: SIGN_IN_COMMAND } },
36
+ : { key: "agent", title: "Agent signed in", done: facts.agentSignedIn, action: facts.agentSignedIn ? null : { kind: "command", command: facts.signInCommand ?? SIGN_IN_COMMAND } },
20
37
  { key: "project", title: "Project added", done: project, action: project ? null : { kind: "link", label: "Add a project", href: "/projects" } },
21
38
  { key: "task", title: "Your first task", done: facts.hasTask, action: facts.hasTask ? null : { kind: "link", label: "New task", href: "/tasks/new" } },
22
39
  ];
@@ -103,3 +120,22 @@ export function firstResultWords(since, at) {
103
120
  }
104
121
  return `First result in ${Math.round(minutes / (24 * 60))} days`;
105
122
  }
123
+ /** The first task's five steps, as a person reads them. */
124
+ export const JOURNEY_STEPS = [["plan", "Plan"], ["approve", "You approve"], ["build", "Build"], ["checks", "Checks"], ["ready", "Ready"]];
125
+ /**
126
+ * Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
127
+ * (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
128
+ * the build finished with a result.
129
+ */
130
+ export function firstTaskJourney(read, approved, built = false) {
131
+ const at = read.stage === "finished" || read.stage === "complete" ? { index: 5, stuck: false }
132
+ : read.stage === "checking" || read.stage === "reviewing" ? { index: 3, stuck: false }
133
+ : read.stage === "building" ? { index: 2, stuck: false }
134
+ : read.stage === "needs-you" && read.need === "approval" ? { index: 1, stuck: false }
135
+ : read.stage === "planning" ? { index: 0, stuck: false }
136
+ : read.stage === "queued" ? { index: approved ? 2 : 0, stuck: false }
137
+ // A build that finished but isn't Ready (its checks failed or didn't run, or it needs a decision) stopped at Checks.
138
+ : { index: built ? 3 : approved ? 2 : 0, stuck: true };
139
+ return JOURNEY_STEPS.map(([key, label], index) => ({ key, label,
140
+ state: index < at.index ? "done" : index === at.index ? (at.stuck ? "stuck" : "current") : "todo" }));
141
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
3
+ * its promise, a small drawing of its zones, what it needs and "Use this". A template's page asks only what it
4
+ * needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
5
+ */
6
+ import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate } from "./flow-gallery.js";
7
+ import type { FlowDefinition } from "./flows.js";
8
+ export declare const GALLERY_CSS: string;
9
+ /** A small drawing of a flow's zones where they sit on its canvas, with its paths; a person's decisions stand out. */
10
+ export declare function zonesDiagram(definition: FlowDefinition, label: string): string;
11
+ /** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
12
+ export declare function galleryHtml(input: {
13
+ repo: string | null;
14
+ canUse: boolean;
15
+ }): string;
16
+ /** A template's page: its questions, then what it will do and never do, then Create. */
17
+ export declare function galleryUseHtml(input: {
18
+ template: GalleryTemplate;
19
+ projects: readonly {
20
+ path: string;
21
+ name: string;
22
+ }[];
23
+ repo: string;
24
+ answers: GalleryAnswers;
25
+ name: string;
26
+ preview: GalleryPreview | null;
27
+ problem: string | null;
28
+ csrf: string;
29
+ diagram: FlowDefinition | null;
30
+ }): string;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
3
+ * its promise, a small drawing of its zones, what it needs and "Use this". A template's page asks only what it
4
+ * needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
5
+ */
6
+ import { BLANK, GALLERY, GALLERY_GROUPS, galleryDiagram, OUTDATED_COMMANDS } from "./flow-gallery.js";
7
+ const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
8
+ export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
9
+ `.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
10
+ `.gallery-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}` +
11
+ `.gallery-card h3{margin:0;font-size:.9375rem}.gallery-card p{margin:0;overflow-wrap:anywhere}.gallery-card .gallery-promise{line-height:1.5;flex:1}` +
12
+ `.gallery-zones{display:block;width:100%;height:56px;border-radius:8px;background:var(--so-raised)}.gallery-zones rect{fill:var(--so-paper);stroke:var(--so-input-line);stroke-width:1.5}` +
13
+ `.gallery-zones rect[data-person="true"]{fill:var(--so-signal-soft,var(--so-raised));stroke:var(--so-ink)}.gallery-zones line{stroke:var(--so-input-line);stroke-width:1.5}` +
14
+ `.gallery-needs{display:flex;flex-wrap:wrap;gap:4px;list-style:none;padding:0;margin:0}.gallery-needs li{font-size:11.5px;font-weight:500;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-neutral-soft,var(--so-raised));color:var(--so-neutral-ink,var(--so-muted))}` +
15
+ // An outline action in a list (the Quiet List Rule), over the workspace's ink default for .button-link.
16
+ `.gallery .gallery-card a.button-link{align-self:flex-start;min-height:36px;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
17
+ `.gallery-blank{margin-top:20px}` +
18
+ `.gallery-use{max-width:720px;min-width:0}.gallery-use .gallery-zones{height:88px;margin:4px 0 12px}.gallery-use form{display:grid;gap:12px;margin:0}.gallery-use label{display:grid;gap:4px;font-weight:500}` +
19
+ `.gallery-use label small{font-weight:400;color:var(--so-muted)}` +
20
+ `.gallery-use input:not([type=hidden]),.gallery-use select{box-sizing:border-box;width:100%;max-width:100%;min-height:32px;padding:0 10px;border:1px solid var(--so-input-line);border-radius:8px;background:var(--so-paper);color:var(--so-ink);font:inherit}` +
21
+ `.gallery-preview{display:grid;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}.gallery-preview h2{margin:0;font-size:.9375rem}` +
22
+ `.gallery-preview ul{margin:0;padding-left:18px;line-height:1.55}.gallery-preview p{margin:0;overflow-wrap:anywhere}.gallery-preview .gallery-never{font-weight:600}` +
23
+ `.gallery-preview details ol{margin:6px 0 0;padding-left:20px;white-space:pre-line;font-size:.875rem;line-height:1.5;color:var(--so-muted)}.gallery-preview details li{margin:0 0 6px;overflow-wrap:anywhere}` +
24
+ `.gallery-preview summary{cursor:pointer;min-height:36px;display:flex;align-items:center;font-weight:500}` +
25
+ `.gallery-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0}.gallery-actions button{min-height:36px;white-space:nowrap}` +
26
+ `.gallery-use .gallery-actions button[value=create]{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}.gallery-heading{margin:36px 0 0;font-size:1.05rem}` +
27
+ `@media(max-width:760px){.gallery-grid{grid-template-columns:minmax(0,1fr)}.gallery .gallery-card a.button-link{min-height:44px;align-self:stretch;justify-content:center}` +
28
+ `.gallery-use input:not([type=hidden]),.gallery-use select{font-size:16px;min-height:44px}.gallery-actions button{flex:1 1 auto;min-height:44px}}`;
29
+ /** A small drawing of a flow's zones where they sit on its canvas, with its paths; a person's decisions stand out. */
30
+ export function zonesDiagram(definition, label) {
31
+ const zones = definition.stages.map(one => one.zone);
32
+ const minX = Math.min(...zones.map(one => one.x)), minY = Math.min(...zones.map(one => one.y));
33
+ const maxX = Math.max(...zones.map(one => one.x + one.w)), maxY = Math.max(...zones.map(one => one.y + one.h));
34
+ const pad = 40;
35
+ const box = `${minX - pad} ${minY - pad} ${maxX - minX + pad * 2} ${maxY - minY + pad * 2}`;
36
+ const centre = (id) => { const at = definition.stages.find(one => one.id === id)?.zone; return at === undefined ? null : { x: at.x + at.w / 2, y: at.y + at.h / 2 }; };
37
+ const lines = definition.stages.flatMap(stage => [stage.next, ...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? [])].map(to => {
38
+ const from = centre(stage.id), end = centre(to);
39
+ return from === null || end === null ? "" : `<line x1="${from.x}" y1="${from.y}" x2="${end.x}" y2="${end.y}" vector-effect="non-scaling-stroke"/>`;
40
+ })).join("");
41
+ const rects = definition.stages.map(one => `<rect x="${one.zone.x}" y="${one.zone.y}" width="${one.zone.w}" height="${one.zone.h}" rx="36" data-person="${one.kind === "approval"}" vector-effect="non-scaling-stroke"/>`).join("");
42
+ return `<svg class="gallery-zones" viewBox="${box}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${e(label)}">${lines}${rects}</svg>`;
43
+ }
44
+ const needsList = (template) => template.needs.length === 0 ? `<ul class="gallery-needs"><li>Nothing to connect</li></ul>`
45
+ : `<ul class="gallery-needs" aria-label="Needs">${template.needs.map(one => `<li>${e(one)}</li>`).join("")}</ul>`;
46
+ /** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
47
+ export function galleryHtml(input) {
48
+ const href = (id) => `/flows/new/${id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}`;
49
+ const card = (template) => {
50
+ const definition = galleryDiagram(template);
51
+ return `<article class="gallery-card" data-template="${e(template.id)}"><h3>${e(template.name)}</h3><p class="gallery-promise">${e(template.promise)}</p>` +
52
+ zonesDiagram(definition, `Zones: ${definition.stages.map(one => one.title).join(", ")}`) + needsList(template) +
53
+ (input.canUse ? `<a class="button-link" href="${e(href(template.id))}" aria-label="Use ${e(template.name)}">Use this</a>` : "") + `</article>`;
54
+ };
55
+ const groups = GALLERY_GROUPS.map(group => `<h2 id="gallery-${group.id}">${e(group.label)}</h2><div class="gallery-grid" data-group="${group.id}">${GALLERY.filter(one => one.group === group.id).map(card).join("")}</div>`).join("");
56
+ return `<section class="gallery">${groups}${input.canUse ? `<p class="gallery-blank meta"><a href="${e(href(BLANK.id))}">Start from a blank flow</a></p>` : `<p class="meta">An approver creates flows.</p>`}</section>`;
57
+ }
58
+ /** A template's page: its questions, then what it will do and never do, then Create. */
59
+ export function galleryUseHtml(input) {
60
+ const { template } = input;
61
+ const field = (ask) => {
62
+ const value = input.answers[ask.key] ?? ask.default;
63
+ const hint = ask.hint === undefined ? "" : ` <small>${e(ask.hint)}</small>`;
64
+ if (ask.key === "outdated")
65
+ return `<label>${e(ask.label)}<select name="outdated">${Object.entries(OUTDATED_COMMANDS).map(([id, one]) => `<option value="${id}"${id === value ? " selected" : ""}>${e(one.label)}</option>`).join("")}</select></label>`;
66
+ return `<label>${e(ask.label)}${hint}<input name="${ask.key}" value="${e(value)}" required maxlength="${ask.key === "command" ? 500 : 100}"${ask.key === "command" ? ' spellcheck="false" autocapitalize="off"' : ""}></label>`;
67
+ };
68
+ const project = input.projects.length > 1
69
+ ? `<label>Project<select name="repo">${input.projects.map(one => `<option value="${e(one.path)}"${one.path === input.repo ? " selected" : ""}>${e(one.name)}</option>`).join("")}</select></label>`
70
+ : `<input type="hidden" name="repo" value="${e(input.repo)}">`;
71
+ const preview = input.preview === null ? "" : `<section class="gallery-preview" aria-labelledby="gallery-will">` +
72
+ `<h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => `<li>${e(one)}</li>`).join("")}</ul>` +
73
+ `<p class="gallery-never">${e(input.preview.built.never)}</p>` +
74
+ (input.preview.startsFrom.length === 0 ? `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => `<p class="meta">Starts from: ${e(one)}</p>`).join("")) +
75
+ `<details><summary>Every step</summary><ol>${input.preview.steps.map(one => `<li>${e(one.replace(/^\d+\.\s/, ""))}</li>`).join("")}</ol></details></section>`;
76
+ const asks = template.asks.length > 0 || input.projects.length > 1;
77
+ return `<section class="gallery-use" data-gallery-template="${e(template.id)}">` +
78
+ (input.problem === null ? "" : `<p class="problem" role="alert">${e(input.problem)}</p>`) +
79
+ `<p>${e(template.promise)}</p>${input.diagram === null ? "" : zonesDiagram(input.diagram, `Zones: ${input.diagram.stages.map(one => one.title).join(", ")}`)}${needsList(template)}` +
80
+ `<form method="post" action="/flows/new/${e(template.id)}" data-gallery-use><input type="hidden" name="csrf" value="${e(input.csrf)}">` +
81
+ `<input type="hidden" name="previewed" value="${e(input.preview?.digest ?? "")}">${project}${template.asks.map(field).join("")}` +
82
+ `<label>Name<input name="name" value="${e(input.name)}" maxlength="80"></label>${preview}` +
83
+ `<p class="gallery-actions">${input.preview === null ? "" : `<button type="submit" name="intent" value="create">Create flow</button>`}${asks || input.preview === null ? `<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button>` : ""}</p>` +
84
+ `</form><p class="meta"><a href="/flows/new">All templates</a></p></section>`;
85
+ }