toolroll 0.9.89 → 0.9.90

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 (136) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +16 -10
  3. package/THIRD_PARTY_NOTICES.md +37 -0
  4. package/dist/agent-board.d.ts +9 -0
  5. package/dist/agent-board.js +20 -0
  6. package/dist/agent-fence.js +20 -2
  7. package/dist/approval-authority.d.ts +7 -0
  8. package/dist/approval-authority.js +13 -2
  9. package/dist/browser/THIRD_PARTY_NOTICES.txt +692 -28
  10. package/dist/browser/terminal.js +42 -42
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +32 -32
  13. package/dist/browser-workspace.d.ts +13 -0
  14. package/dist/builder.js +5 -2
  15. package/dist/chat-actions.d.ts +5 -0
  16. package/dist/chat-actions.js +7 -1
  17. package/dist/chat-delivery.js +2 -0
  18. package/dist/chat-task-actions.d.ts +1 -0
  19. package/dist/check-access.d.ts +3 -0
  20. package/dist/check-access.js +4 -1
  21. package/dist/cli-help.d.ts +1 -1
  22. package/dist/cli-help.js +6 -2
  23. package/dist/cli.d.ts +6 -0
  24. package/dist/cli.js +23 -0
  25. package/dist/coding-ui.js +4 -2
  26. package/dist/connect-cli.js +2 -1
  27. package/dist/console-instance.d.ts +34 -1
  28. package/dist/console-instance.js +124 -15
  29. package/dist/contracts/cli.d.ts +72 -0
  30. package/dist/contracts/cli.js +13 -0
  31. package/dist/contracts/console-api.d.ts +9 -2
  32. package/dist/contracts/console-api.js +9 -2
  33. package/dist/desktop-host.js +8 -3
  34. package/dist/desktop-update.js +2 -1
  35. package/dist/dispatch.d.ts +3 -0
  36. package/dist/dispatch.js +22 -2
  37. package/dist/easy-start-args.d.ts +13 -0
  38. package/dist/easy-start-args.js +24 -0
  39. package/dist/easy-start.d.ts +77 -0
  40. package/dist/easy-start.js +321 -0
  41. package/dist/first-run.d.ts +77 -0
  42. package/dist/first-run.js +45 -0
  43. package/dist/guides.js +1 -1
  44. package/dist/invoke.js +11 -6
  45. package/dist/laptop-background.d.ts +171 -0
  46. package/dist/laptop-background.js +550 -0
  47. package/dist/laptop-service.d.ts +111 -0
  48. package/dist/laptop-service.js +268 -0
  49. package/dist/lead-cli.js +2 -1
  50. package/dist/lead-doors.d.ts +6 -0
  51. package/dist/lead-doors.js +20 -2
  52. package/dist/live-records.d.ts +1 -1
  53. package/dist/live-records.js +3 -12
  54. package/dist/local-owner.d.ts +124 -0
  55. package/dist/local-owner.js +212 -0
  56. package/dist/login-secrets.d.ts +60 -0
  57. package/dist/login-secrets.js +195 -0
  58. package/dist/mcp-oauth.d.ts +4 -0
  59. package/dist/mcp-oauth.js +13 -7
  60. package/dist/model-choices.d.ts +43 -0
  61. package/dist/model-choices.js +80 -0
  62. package/dist/operate.d.ts +46 -1
  63. package/dist/operate.js +461 -40
  64. package/dist/person-proof.js +3 -0
  65. package/dist/project-knowledge.d.ts +7 -7
  66. package/dist/quick-start.d.ts +3 -1
  67. package/dist/quick-start.js +6 -1
  68. package/dist/release-cli.js +2 -1
  69. package/dist/runner-process.d.ts +9 -0
  70. package/dist/runner-process.js +18 -0
  71. package/dist/sample-project.d.ts +31 -0
  72. package/dist/sample-project.js +141 -0
  73. package/dist/scope.d.ts +24 -2
  74. package/dist/scope.js +74 -3
  75. package/dist/serve.js +129 -22
  76. package/dist/server/chat.js +10 -3
  77. package/dist/server/chrome.d.ts +10 -2
  78. package/dist/server/chrome.js +7 -5
  79. package/dist/server/console.css +10 -0
  80. package/dist/server/guards.d.ts +3 -0
  81. package/dist/server/guards.js +22 -2
  82. package/dist/server/http.d.ts +22 -0
  83. package/dist/server/http.js +58 -0
  84. package/dist/server/live.js +4 -14
  85. package/dist/server/mutation-key.d.ts +70 -0
  86. package/dist/server/mutation-key.js +142 -0
  87. package/dist/server/pages.js +10 -7
  88. package/dist/server/people-tokens.js +240 -20
  89. package/dist/server/remote.d.ts +2 -0
  90. package/dist/server/remote.js +19 -4
  91. package/dist/server/render-new-task.js +5 -1
  92. package/dist/server/render-pages.d.ts +2 -1
  93. package/dist/server/render-pages.js +1 -1
  94. package/dist/server/render-people.d.ts +36 -0
  95. package/dist/server/render-people.js +65 -1
  96. package/dist/server/render-projects.js +4 -2
  97. package/dist/server/render-tasks.d.ts +12 -0
  98. package/dist/server/render-tasks.js +12 -4
  99. package/dist/server/request-context.d.ts +6 -0
  100. package/dist/server/route-table.d.ts +16 -0
  101. package/dist/server/route-table.js +121 -0
  102. package/dist/server/runtime.d.ts +6 -4
  103. package/dist/server/session.d.ts +31 -1
  104. package/dist/server/session.js +12 -2
  105. package/dist/server/settings.js +63 -8
  106. package/dist/server/task-views.js +10 -1
  107. package/dist/server/tasks.d.ts +10 -0
  108. package/dist/server/tasks.js +59 -9
  109. package/dist/server/terminals.js +15 -2
  110. package/dist/server/welcome.d.ts +22 -16
  111. package/dist/server/welcome.js +150 -52
  112. package/dist/settings-groups-ui.d.ts +7 -1
  113. package/dist/settings-groups-ui.js +23 -2
  114. package/dist/settings-proofs.js +1 -0
  115. package/dist/setup-guide.d.ts +2 -2
  116. package/dist/store.d.ts +124 -1
  117. package/dist/store.js +245 -12
  118. package/dist/surface.d.ts +10 -0
  119. package/dist/surface.js +40 -1
  120. package/dist/task-text.d.ts +6 -0
  121. package/dist/task-text.js +17 -0
  122. package/dist/telegram-lead.js +2 -0
  123. package/dist/terminal-text.d.ts +11 -0
  124. package/dist/terminal-text.js +26 -0
  125. package/dist/toolroll-update.d.ts +8 -1
  126. package/dist/toolroll-update.js +151 -4
  127. package/dist/tui-contract.d.ts +118 -0
  128. package/dist/tui-contract.js +9 -0
  129. package/dist/tui-source.d.ts +14 -0
  130. package/dist/tui-source.js +182 -0
  131. package/dist/tui.js +129 -0
  132. package/dist/up-processes.d.ts +13 -1
  133. package/dist/up-processes.js +13 -3
  134. package/dist/welcome-facts.d.ts +18 -0
  135. package/dist/welcome-facts.js +79 -0
  136. package/package.json +4 -2
@@ -235,6 +235,16 @@ export declare const COMMAND_OUTPUTS: {
235
235
  error: z.ZodNullable<z.ZodString>;
236
236
  retry: z.ZodString;
237
237
  }, z.core.$loose>>>;
238
+ laptop: z.ZodOptional<z.ZodObject<{
239
+ running: z.ZodBoolean;
240
+ url: z.ZodNullable<z.ZodString>;
241
+ pid: z.ZodNullable<z.ZodNumber>;
242
+ background: z.ZodEnum<{
243
+ off: "off";
244
+ on: "on";
245
+ "other-service": "other-service";
246
+ }>;
247
+ }, z.core.$loose>>;
238
248
  }, z.core.$loose>, z.ZodObject<{
239
249
  envelopeVersion: z.ZodLiteral<1>;
240
250
  ok: z.ZodLiteral<false>;
@@ -838,6 +848,68 @@ export declare const COMMAND_OUTPUTS: {
838
848
  reason: z.ZodString;
839
849
  message: z.ZodOptional<z.ZodString>;
840
850
  }, z.core.$loose>], "ok">;
851
+ readonly welcome: z.ZodDiscriminatedUnion<[z.ZodObject<{
852
+ envelopeVersion: z.ZodLiteral<1>;
853
+ ok: z.ZodLiteral<true>;
854
+ command: z.ZodLiteral<"welcome">;
855
+ agent: z.ZodObject<{
856
+ done: z.ZodBoolean;
857
+ checking: z.ZodBoolean;
858
+ agents: z.ZodArray<z.ZodObject<{
859
+ id: z.ZodString;
860
+ name: z.ZodString;
861
+ state: z.ZodString;
862
+ command: z.ZodNullable<z.ZodString>;
863
+ inUse: z.ZodBoolean;
864
+ canUse: z.ZodBoolean;
865
+ }, z.core.$loose>>;
866
+ inUse: z.ZodNullable<z.ZodString>;
867
+ buildsWith: z.ZodNullable<z.ZodString>;
868
+ }, z.core.$loose>;
869
+ project: z.ZodObject<{
870
+ done: z.ZodBoolean;
871
+ projects: z.ZodArray<z.ZodObject<{
872
+ name: z.ZodString;
873
+ path: z.ZodString;
874
+ }, z.core.$loose>>;
875
+ sample: z.ZodNullable<z.ZodString>;
876
+ notTakingWork: z.ZodBoolean;
877
+ }, z.core.$loose>;
878
+ task: z.ZodObject<{
879
+ done: z.ZodBoolean;
880
+ open: z.ZodBoolean;
881
+ starters: z.ZodArray<z.ZodObject<{
882
+ label: z.ZodString;
883
+ draft: z.ZodString;
884
+ href: z.ZodString;
885
+ }, z.core.$loose>>;
886
+ }, z.core.$loose>;
887
+ next: z.ZodNullable<z.ZodEnum<{
888
+ agent: "agent";
889
+ project: "project";
890
+ task: "task";
891
+ }>>;
892
+ left: z.ZodNumber;
893
+ }, z.core.$loose>, z.ZodObject<{
894
+ envelopeVersion: z.ZodLiteral<1>;
895
+ ok: z.ZodLiteral<false>;
896
+ command: z.ZodLiteral<"welcome">;
897
+ reason: z.ZodString;
898
+ message: z.ZodOptional<z.ZodString>;
899
+ }, z.core.$loose>], "ok">;
900
+ readonly "password set": z.ZodDiscriminatedUnion<[z.ZodObject<{
901
+ envelopeVersion: z.ZodLiteral<1>;
902
+ ok: z.ZodLiteral<true>;
903
+ command: z.ZodLiteral<"password set">;
904
+ account: z.ZodString;
905
+ generation: z.ZodNumber;
906
+ }, z.core.$loose>, z.ZodObject<{
907
+ envelopeVersion: z.ZodLiteral<1>;
908
+ ok: z.ZodLiteral<false>;
909
+ command: z.ZodLiteral<"password set">;
910
+ reason: z.ZodString;
911
+ message: z.ZodOptional<z.ZodString>;
912
+ }, z.core.$loose>], "ok">;
841
913
  };
842
914
  /** The schema for `command`'s answer: its own when it names its fields, else the envelope under its own name. */
843
915
  export declare function commandOutputSchema(command: string): z.ZodType;
@@ -90,6 +90,9 @@ const statusOutput = answers("status", {
90
90
  integrations: z.looseObject({ broken: rows({ key: str, name: str, fix: nullable(str) }) }).optional(),
91
91
  updateWaiting: z.looseObject({}).optional(),
92
92
  unsentReplies: rows({ since: str, error: nullable(str), retry: str }).optional(),
93
+ // This computer's console (easy-start D): proven live or not, its address and console process, and whether a
94
+ // background service runs it — ours, another on this database or the desktop app's, or none. Not for a person limited to some projects.
95
+ laptop: z.looseObject({ running: z.boolean(), url: nullable(str), pid: nullable(int), background: z.enum(["on", "off", "other-service"]) }).optional(),
93
96
  });
94
97
  /** One integration (`integrations.ts` Integration): never a secret. */
95
98
  const integrationRow = z.looseObject({
@@ -203,6 +206,16 @@ export const COMMAND_OUTPUTS = {
203
206
  reap: answers("reap", { count: int, released: list }),
204
207
  audit: auditOutput,
205
208
  "skills list": answers("skills list", { guides: rows({ name: str, title: str, oneLiner: str }) }),
209
+ // The welcome's three steps (first-run.ts welcomeModel), the same model the console's /welcome shows.
210
+ welcome: answers("welcome", {
211
+ agent: z.looseObject({ done: z.boolean(), checking: z.boolean(), agents: rows({ id: str, name: str, state: str, command: nullable(str), inUse: z.boolean(), canUse: z.boolean() }), inUse: nullable(str), buildsWith: nullable(str) }),
212
+ project: z.looseObject({ done: z.boolean(), projects: rows({ name: str, path: str }), sample: nullable(str), notTakingWork: z.boolean() }),
213
+ task: z.looseObject({ done: z.boolean(), open: z.boolean(), starters: rows({ label: str, draft: str, href: str }) }),
214
+ next: nullable(z.enum(["agent", "project", "task"])),
215
+ left: int,
216
+ }),
217
+ // The first password set: whose, and the account generation every older session was bound to before it.
218
+ "password set": answers("password set", { account: str, generation: int }),
206
219
  };
207
220
  const declared = new Map(Object.entries(COMMAND_OUTPUTS));
208
221
  /** The schema for `command`'s answer: its own when it names its fields, else the envelope under its own name. */
@@ -61,7 +61,9 @@ export declare const CONSOLE_FORMS: {
61
61
  readonly signup: FormContract<"code" | "name" | "password">;
62
62
  readonly login: FormContract<"name" | "return" | "token">;
63
63
  readonly join: FormContract<"name" | "password">;
64
- readonly mutationGuard: FormContract<"confirm" | "csrf" | "digest" | "nonce" | "token">;
64
+ readonly rekey: FormContract<"return" | "token">;
65
+ readonly passwordSet: FormContract<"again" | "csrf" | "next" | "password" | "return" | "toolroll-key">;
66
+ readonly mutationGuard: FormContract<"confirm" | "csrf" | "digest" | "nonce" | "token" | "toolroll-key">;
65
67
  readonly ledgerTarget: FormContract<"repo">;
66
68
  readonly projectCeiling: FormContract<"repo">;
67
69
  readonly demo: FormContract<"message" | "note">;
@@ -72,6 +74,7 @@ export declare const CONSOLE_FORMS: {
72
74
  readonly code: FormContract<"acceptance" | "answers" | "base" | "candidate" | "decision" | "goal" | "model" | "password" | "prompt" | "repo" | "requestId" | "title" | "visual" | `question:${string}`>;
73
75
  readonly sweep: FormContract<"confirm" | "snapshot">;
74
76
  readonly passwordless: FormContract<"action" | "password">;
77
+ readonly localModeOff: FormContract<"password">;
75
78
  readonly releaseApproval: FormContract<"action" | "password" | "repo">;
76
79
  readonly term: FormContract<"password" | "program" | "repo">;
77
80
  readonly skillsRevise: FormContract<"feedback" | "nonce" | "repo" | "run">;
@@ -156,12 +159,16 @@ export declare const CONSOLE_FORMS: {
156
159
  readonly leadAbout: FormContract<"about">;
157
160
  readonly leadPromiseCancel: FormContract<"promise">;
158
161
  readonly leadCheck: FormContract<"return">;
162
+ readonly leadOn: FormContract<"agent" | "return">;
163
+ readonly welcomeSample: FormContract<never>;
159
164
  readonly chatConfig: FormContract<"daily-turns" | "forget-key" | "key" | "model" | "off" | "provider" | "return" | "token" | "weekly-usd">;
160
165
  readonly leadMint: FormContract<"ceiling-usd" | "follow" | "return" | "token">;
161
166
  readonly leadFollow: FormContract<"enabled" | "return">;
162
167
  readonly leadEnd: FormContract<"return">;
163
168
  readonly leadStop: FormContract<"return" | "turn">;
164
169
  readonly leadQueuedRemove: FormContract<"return">;
170
+ readonly agentSeen: FormContract<"run">;
171
+ readonly agentWatch: FormContract<"op" | "run" | "view">;
165
172
  readonly leadProposal: FormContract<"confirm" | "nonce" | "return" | "token">;
166
173
  readonly coordinatorProposal: FormContract<"confirm" | "return">;
167
174
  readonly chat: FormContract<"message" | "mode" | "project" | "request" | "request-session" | "result" | "task" | "token">;
@@ -200,7 +207,7 @@ export declare const CONSOLE_FORMS: {
200
207
  };
201
208
  export type ConsoleFormName = keyof typeof CONSOLE_FORMS;
202
209
  /** The console POSTs whose handlers read no field (sign-out not even the shared guard): their contract is the empty form. */
203
- export declare const BODILESS_POSTS: readonly ["POST /settings/models/<unknown action>", "POST /settings/lead/on", "POST /r/<run>/draft-repair", "POST /ledger/checkpoint", "POST /settings/backups/now", "POST /mode/revoke", "POST /settings/telegram/retry", "POST /providers/<provider>/resume", "POST /logout", "POST /t/<task>/unhold", "POST /t/<task>/stop-carrying", "POST /t/<task>/plan", "POST /t/<task>/retry-review", "POST /settings/updates/check", "POST /settings/storage/check", "POST /projects/github", "POST /code/<session>/ship/preview"];
210
+ export declare const BODILESS_POSTS: readonly ["POST /settings/models/<unknown action>", "POST /r/<run>/draft-repair", "POST /ledger/checkpoint", "POST /settings/backups/now", "POST /mode/revoke", "POST /settings/telegram/retry", "POST /providers/<provider>/resume", "POST /logout", "POST /t/<task>/unhold", "POST /t/<task>/stop-carrying", "POST /t/<task>/plan", "POST /t/<task>/retry-review", "POST /settings/updates/check", "POST /settings/storage/check", "POST /projects/github", "POST /code/<session>/ship/preview"];
204
211
  /** The field names a console form's handler may read. */
205
212
  export type FormFieldOf<N extends ConsoleFormName> = (typeof CONSOLE_FORMS)[N] extends FormContract<infer K> ? K : never;
206
213
  /** `GET /ledger?format=json`: the chain's state, a page of entries (newest first) and the cursor for the next. */
@@ -69,8 +69,10 @@ export const CONSOLE_FORMS = {
69
69
  signup: formContract("POST /signup", ["code", "name", "password"]),
70
70
  login: formContract("POST /login", ["name", "token", "return"]),
71
71
  join: formContract("POST /join/<invite>", ["name", "password"]),
72
+ rekey: formContract("POST /login/rekey", ["token", "return"]),
73
+ passwordSet: formContract("POST /password/set", ["csrf", "password", "again", "next", "return", "toolroll-key"]),
72
74
  // Shared by every signed-in POST.
73
- mutationGuard: formContract("POST (every signed-in form)", ["csrf", "token", "digest", "nonce", "confirm"]),
75
+ mutationGuard: formContract("POST (every signed-in form)", ["csrf", "token", "digest", "nonce", "confirm", "toolroll-key"]),
74
76
  ledgerTarget: formContract("POST (the action ledger's project)", ["repo"]),
75
77
  projectCeiling: formContract("POST (project ceiling)", ["repo"]),
76
78
  demo: formContract("POST /chat/demo/ask, /chat/demo/<id>/approve|change|revise|complete", ["message", "note"]),
@@ -81,6 +83,7 @@ export const CONSOLE_FORMS = {
81
83
  code: formContract("POST /code, /code/start, /code/<session>/<act>, /code/<anything else>", ["repo", "password", "title", "model", "prompt", "requestId", "base", "candidate", "goal", "acceptance", "visual", "decision", "answers"], { prefixes: ["question:"] }),
82
84
  sweep: formContract("POST /sweeps/<sweep>/cancel-rest", ["confirm", "snapshot"]),
83
85
  passwordless: formContract("POST /settings/passwordless", ["action", "password"]),
86
+ localModeOff: formContract("POST /settings/local-mode/off", ["password"]),
84
87
  releaseApproval: formContract("POST /settings/release-approval", ["repo", "action", "password"]),
85
88
  term: formContract("POST /term/start, /term/<terminal>/close, /term/<terminal>/remove", ["repo", "program", "password"]),
86
89
  skillsRevise: formContract("POST /settings/skills/revise", ["run", "repo", "feedback", "nonce"]),
@@ -169,12 +172,17 @@ export const CONSOLE_FORMS = {
169
172
  leadAbout: formContract("POST /settings/lead/about", ["about"]),
170
173
  leadPromiseCancel: formContract("POST /settings/lead/promise/cancel", ["promise"]),
171
174
  leadCheck: formContract("POST /lead/check", ["return"]),
175
+ leadOn: formContract("POST /settings/lead/on", ["agent", "return"]),
176
+ welcomeSample: formContract("POST /welcome/sample", []),
172
177
  chatConfig: formContract("POST /chat/config", ["return", "token", "off", "forget-key", "provider", "model", "weekly-usd", "daily-turns", "key"]),
173
178
  leadMint: formContract("POST /chat/mate/mint", ["return", "ceiling-usd", "token", "follow"]),
174
179
  leadFollow: formContract("POST /chat/mate/follow", ["enabled", "return"]),
175
180
  leadEnd: formContract("POST /chat/mate/end", ["return"]),
176
181
  leadStop: formContract("POST /chat/mate/stop", ["return", "turn"]),
177
182
  leadQueuedRemove: formContract("POST /chat/queued/<message>/remove", ["return"]),
183
+ // Easy-start C1 (N1): what a page load used to write, now a keyed POST from the shell.
184
+ agentSeen: formContract("POST /agents/seen", ["run"]),
185
+ agentWatch: formContract("POST /agents/watch", ["run", "view", "op"]),
178
186
  leadProposal: formContract("POST /chat/proposal/<id>/confirm|dismiss", ["return", "nonce", "confirm", "token"]),
179
187
  coordinatorProposal: formContract("POST /proposals/<id>/confirm|dismiss", ["return", "confirm"]),
180
188
  chat: formContract("POST /chat", ["task", "project", "result", "mode", "request", "request-session", "message", "token"]),
@@ -218,7 +226,6 @@ export const CONSOLE_FORMS = {
218
226
  /** The console POSTs whose handlers read no field (sign-out not even the shared guard): their contract is the empty form. */
219
227
  export const BODILESS_POSTS = [
220
228
  "POST /settings/models/<unknown action>",
221
- "POST /settings/lead/on",
222
229
  "POST /r/<run>/draft-repair",
223
230
  "POST /ledger/checkpoint",
224
231
  "POST /settings/backups/now",
@@ -9,6 +9,7 @@ import { openStore, openStoreNoMigrate, databasePath, readSchemaVersion, SCHEMA_
9
9
  import { createRequire } from "node:module";
10
10
  import { authenticateApprover, hashPassword } from "./scope.js";
11
11
  import { runOperate, writeLoginFileDurably } from "./operate.js";
12
+ import { ensureSecretsDir, migrateLegacyLogin, savedLoginFile } from "./login-secrets.js";
12
13
  import { daemonStatus, installLaunchdService, planDesktopService, stopLaunchdService } from "./daemon.js";
13
14
  import { run as execRun } from "./exec.js";
14
15
  import { containmentStatus, effectiveContainment, parseContainmentPolicy, probeContainmentCapability } from "./containment.js";
@@ -172,7 +173,7 @@ export function pairDesktopLogin(store, file, login) {
172
173
  writeLoginFileDurably(file, login.name, login.password);
173
174
  }
174
175
  else if (!(first && remembered.name === login.name && remembered.password === login.password) && !authenticateApprover(store, remembered.name, remembered.password).ok) {
175
- throw new Error("The existing up-login.txt is stale. Restore its valid login before starting the desktop service.");
176
+ throw new Error("The saved login (up-login.txt) is stale. Restore its valid login before starting the desktop service.");
176
177
  }
177
178
  if (first)
178
179
  store.bootstrapApproverIfNone(login.name, hashPassword(login.password), new Date());
@@ -371,7 +372,8 @@ export async function desktopMain(argv, supervise = execRun) {
371
372
  }
372
373
  // Inspection and launch must never migrate another running controller.
373
374
  const store = openConfiguredDesktopStore(stateDir);
374
- const legacyFile = join(dirname(config.databaseFile), "up-login.txt");
375
+ // The saved login: in the reserved folder every agent fence denies (login-secrets.ts), else where older versions kept it.
376
+ const legacyFile = savedLoginFile(dirname(config.databaseFile));
375
377
  if (argv[0] !== "serve-controller") {
376
378
  try {
377
379
  if (argv[0] === "inspect") {
@@ -387,7 +389,10 @@ export async function desktopMain(argv, supervise = execRun) {
387
389
  }
388
390
  else if (argv[0] === "pair") {
389
391
  const login = await readInput();
390
- pairDesktopLogin(store, legacyFile, login);
392
+ // A login saved here goes into the reserved folder, made and proven first; an older one moves in.
393
+ migrateLegacyLogin(dirname(config.databaseFile));
394
+ ensureSecretsDir(dirname(config.databaseFile));
395
+ pairDesktopLogin(store, savedLoginFile(dirname(config.databaseFile)), login);
391
396
  console.log(JSON.stringify({ ok: true }));
392
397
  }
393
398
  else if (argv[0] === "add-repo" || argv[0] === "add-repos") {
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from "node:crypto";
3
3
  import { closeSync, chmodSync, copyFileSync, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import { savedLoginFile } from "./login-secrets.js";
6
7
  import { createRequire } from "node:module";
7
8
  import { bundleHash, readDesktopBundle, verifyDesktopUpdateBundles } from "./desktop-bundle.js";
8
9
  import { readDesktopConfig, desktopServiceCommand, desktopServiceDefinition, verifyDesktopLiveness } from "./desktop-host.js";
@@ -535,7 +536,7 @@ async function verifiedBackup(j) {
535
536
  const backupHash = await verifiedDatabaseBackup(j.databaseFile, backupPath, j.id, async () => {
536
537
  await verifiedCodingBackup(j);
537
538
  }, () => {
538
- for (const [source, name] of [[join(j.stateDir, "desktop.json"), "desktop.json"], [join(dirname(j.databaseFile), "repos.json"), "repos.json"], [join(dirname(j.databaseFile), "up-login.txt"), "up-login.txt"]]) {
539
+ for (const [source, name] of [[join(j.stateDir, "desktop.json"), "desktop.json"], [join(dirname(j.databaseFile), "repos.json"), "repos.json"], [savedLoginFile(dirname(j.databaseFile)), "up-login.txt"]]) {
539
540
  if (source && name && existsSync(source)) {
540
541
  if (!lstatSync(source).isFile() || lstatSync(source).isSymbolicLink())
541
542
  throw Error("A saved configuration file is linked or not a regular file. Nothing was replaced.");
@@ -118,3 +118,6 @@ export declare function diagnosisIsDispatchable(diagnosis: DispatchDiagnosis | n
118
118
  /** Attach the same lifecycle answer to the bounded fleet snapshot used by
119
119
  * both direct chat and the long-running lead. */
120
120
  export declare function withDispatchDiagnoses(store: Store, snapshot: ChatSnapshot, now: Date): ChatSnapshot;
121
+ /** When a backed-off task tries again, as a person reads it: "in under a minute", "in 3 minutes", or a later day's
122
+ * time in UTC (the console's own clock words, when-html.ts shortWhen). */
123
+ export declare function retryWhenWords(until: string, now: Date): string;
package/dist/dispatch.js CHANGED
@@ -17,6 +17,7 @@ import { replacedWords } from "./task-status.js";
17
17
  import { failedAttemptSentence, latestFinishedAttempt } from "./needs-you.js";
18
18
  import { buildReviewOf } from "./review-switch.js";
19
19
  import { BUILT_IN, refArchived, parseCapabilityKey } from "./store.js";
20
+ import { shortWhen } from "./when-html.js";
20
21
  export const DEFAULT_MAX_OPEN_DECISIONS = 5;
21
22
  /**
22
23
  * Task state, active holds, and unfinished dependencies in the exact order
@@ -260,10 +261,11 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
260
261
  if (local.ownerKind === "backoff") {
261
262
  // Run 2085: an attempt that stopped before its handoff says so plainly.
262
263
  const last = store.runsFor(ref.id).find(one => one.role === "builder" && one.finishedAt !== null);
263
- const when = local.until === null ? "" : ` The next attempt is eligible at ${local.until}.`;
264
+ // Said once, with a time a person reads (nextAt keeps the exact stamp for machines).
265
+ const when = local.until === null ? "" : ` It tries again ${retryWhenWords(local.until, now)}.`;
264
266
  if (last?.reason === "no-handoff")
265
267
  return answer("retry-scheduled", "retrying", "Retry scheduled", `The agent stopped before handing off; its work was kept and it is being resumed.${when}`, { nextAt: local.until });
266
- return answer("retry-scheduled", "retrying", "Retry scheduled", local.until === null ? "The failed attempt is backing off before retrying." : `The next attempt is eligible at ${local.until}.`, { nextAt: local.until });
268
+ return answer("retry-scheduled", "retrying", "Retry scheduled", local.until === null ? "The failed attempt is backing off before retrying." : `It tries again ${retryWhenWords(local.until, now)}.`, { nextAt: local.until });
267
269
  }
268
270
  if (local.ownerKind === "decision")
269
271
  return answer("waiting-decision", "waiting", "Waiting on your answer", "An agent parked a question; answering it resumes the task.", { action: "answer-decision" });
@@ -358,3 +360,21 @@ export function withDispatchDiagnoses(store, snapshot, now) {
358
360
  tasks: snapshot.tasks.map(one => ({ ...one, dispatch: diagnoseTaskDispatch(store, one.id, now) })),
359
361
  };
360
362
  }
363
+ /** When a backed-off task tries again, as a person reads it: "in under a minute", "in 3 minutes", or a later day's
364
+ * time in UTC (the console's own clock words, when-html.ts shortWhen). */
365
+ export function retryWhenWords(until, now) {
366
+ const ms = Date.parse(until) - now.getTime();
367
+ if (Number.isNaN(ms))
368
+ return "soon";
369
+ if (ms <= 60_000)
370
+ return "in under a minute";
371
+ const minutes = Math.ceil(ms / 60_000);
372
+ if (minutes < 60)
373
+ return `in ${minutes} minutes`;
374
+ const at = shortWhen(until, now, "UTC", true);
375
+ if (/^\d/.test(at))
376
+ return `at ${at} UTC`;
377
+ if (at.startsWith("Tomorrow "))
378
+ return `tomorrow at ${at.slice("Tomorrow ".length)} UTC`;
379
+ return `on ${at} UTC`;
380
+ }
@@ -0,0 +1,13 @@
1
+ /** Which invocations are an easy start (easy-start.ts), kept apart so the CLI's entry decides without loading it. */
2
+ export type EasyStartArgs = {
3
+ web: boolean;
4
+ projectRoot: string | null;
5
+ };
6
+ /** The flags bare `toolroll` takes at a terminal (--web, --project-root <dir>), or null: not an easy start. */
7
+ export declare function easyStartArgs(argv: readonly string[]): EasyStartArgs | null;
8
+ /** Whether this invocation is an easy start: only those flags, a person at a terminal, no coding agent. */
9
+ export declare function easyStartApplies(argv: readonly string[], facts: {
10
+ stdin: boolean;
11
+ stdout: boolean;
12
+ agent: boolean;
13
+ }): boolean;
@@ -0,0 +1,24 @@
1
+ /** Which invocations are an easy start (easy-start.ts), kept apart so the CLI's entry decides without loading it. */
2
+ /** The flags bare `toolroll` takes at a terminal (--web, --project-root <dir>), or null: not an easy start. */
3
+ export function easyStartArgs(argv) {
4
+ let web = false, projectRoot = null;
5
+ for (let index = 0; index < argv.length; index += 1) {
6
+ const one = argv[index];
7
+ if (one === "--web" && !web)
8
+ web = true;
9
+ else if (one === "--project-root" && projectRoot === null) {
10
+ const value = argv[index + 1];
11
+ if (value === undefined || value === "" || value.startsWith("-"))
12
+ return null;
13
+ projectRoot = value;
14
+ index += 1;
15
+ }
16
+ else
17
+ return null;
18
+ }
19
+ return { web, projectRoot };
20
+ }
21
+ /** Whether this invocation is an easy start: only those flags, a person at a terminal, no coding agent. */
22
+ export function easyStartApplies(argv, facts) {
23
+ return facts.stdin && facts.stdout && !facts.agent && easyStartArgs(argv) !== null;
24
+ }
@@ -0,0 +1,77 @@
1
+ import { type AttachProof } from "./console-instance.js";
2
+ import type { TuiExit, TuiSession } from "./tui-contract.js";
3
+ export { easyStartApplies, easyStartArgs, type EasyStartArgs } from "./easy-start-args.js";
4
+ import type { EasyStartArgs } from "./easy-start-args.js";
5
+ /** The `up` this process started and owns. */
6
+ export type OwnedUp = {
7
+ pid: number;
8
+ exited: Promise<number | null>;
9
+ running(): boolean;
10
+ stop(): void;
11
+ tail(): readonly string[];
12
+ };
13
+ export type EasyStartSeams = {
14
+ databaseFile: string;
15
+ cwd: string;
16
+ write: (line: string) => void;
17
+ /** Prove this database's console live (read only). */
18
+ prove: () => Promise<AttachProof>;
19
+ /** Local mode as saved now: on, off, or no database yet (a new install). */
20
+ localMode: () => "on" | "off" | "absent";
21
+ serviceInstalled: () => boolean;
22
+ /** Start the installed background service through its supervisor and prove its console. */
23
+ startService: () => Promise<AttachProof>;
24
+ /** `up` (with these arguments) as an ordinary child of this process. */
25
+ spawnUp: (args: readonly string[]) => OwnedUp;
26
+ /** `up` (with these arguments) in this terminal, until it stops: today's foreground start. */
27
+ runUpForeground: (args: readonly string[]) => Promise<number>;
28
+ openBrowser: (url: string) => void | Promise<void>;
29
+ /** A local owner's one-time sign-in address from the live console (the socket `link` verb). */
30
+ requestLink: () => Promise<{
31
+ ok: true;
32
+ url: string;
33
+ } | {
34
+ ok: false;
35
+ message: string;
36
+ }>;
37
+ /** The terminal UI, in this process, as the local owner, until the person quits. */
38
+ openTui: (session: TuiSession) => Promise<TuiExit>;
39
+ /** `background on`, handing over from the `up` this process owns (when it owns one). */
40
+ backgroundOn: (foreground: {
41
+ controllerPid: number;
42
+ restart: () => Promise<void>;
43
+ } | null) => Promise<{
44
+ ok: boolean;
45
+ message: string;
46
+ }>;
47
+ backgroundAsked: () => boolean;
48
+ recordBackgroundAnswer: (yes: boolean) => void;
49
+ /** This folder's repository, whether it is added, and adding it (only when attaching to a running console). */
50
+ findRepo: (cwd: string) => Promise<string | null>;
51
+ enrolled: (repo: string) => Promise<boolean>;
52
+ admit: (repo: string) => Promise<number>;
53
+ sleep?: (ms: number) => Promise<void>;
54
+ startTimeoutMs?: number;
55
+ };
56
+ /**
57
+ * Bare `toolroll` ("start") or `toolroll open` ("open") at a person's terminal. Resolves with the exit code once the
58
+ * screen (or the foreground `up`, or the browser hand-off) is done.
59
+ */
60
+ export declare function runEasyStart(mode: "start" | "open", args: EasyStartArgs, seams: EasyStartSeams): Promise<number>;
61
+ /** `up` as an ordinary child of this process: its output kept (last lines, for a failed start), never on the screen. */
62
+ export declare function spawnOwnedUp(args: readonly string[], options: {
63
+ cwd: string;
64
+ databaseFile: string;
65
+ entry?: string;
66
+ }): OwnedUp;
67
+ /** The real seams, for this installation's database. */
68
+ export declare function defaultEasyStartSeams(options: {
69
+ databaseFile: string;
70
+ write: (line: string) => void;
71
+ build: string;
72
+ cwd?: string;
73
+ operate?: import("./operate.js").OperateOptions;
74
+ admit: (repo: string) => Promise<number>;
75
+ }): EasyStartSeams;
76
+ /** Local mode as saved, read only: "absent" with no database yet; an older database has none ("off"). */
77
+ export declare function localModeRead(file: string): "on" | "off" | "absent";