toolroll 0.9.71 → 0.9.73

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 (131) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/dist/agent-fence.d.ts +10 -2
  3. package/dist/agent-fence.js +47 -9
  4. package/dist/approval-authority.d.ts +20 -0
  5. package/dist/approval-authority.js +67 -0
  6. package/dist/assignment.js +26 -4
  7. package/dist/browser/workspace.css +1 -1
  8. package/dist/browser/workspace.js +28 -28
  9. package/dist/builder.js +48 -6
  10. package/dist/chat-actions.js +13 -8
  11. package/dist/chat-decide.d.ts +47 -1
  12. package/dist/chat-decide.js +186 -5
  13. package/dist/check-access.js +2 -2
  14. package/dist/check-report.d.ts +109 -0
  15. package/dist/check-report.js +205 -0
  16. package/dist/claim.js +1 -1
  17. package/dist/coding-provider.js +4 -2
  18. package/dist/commit-extract.d.ts +111 -0
  19. package/dist/commit-extract.js +514 -0
  20. package/dist/contracts/console-api.d.ts +4 -4
  21. package/dist/contracts/console-api.js +5 -4
  22. package/dist/contracts/flow.d.ts +16 -0
  23. package/dist/contracts/flow.js +3 -0
  24. package/dist/contracts/release-check.d.ts +335 -0
  25. package/dist/contracts/release-check.js +159 -0
  26. package/dist/contracts/release-terms.d.ts +66 -0
  27. package/dist/contracts/release-terms.js +74 -0
  28. package/dist/contracts/verification-receipt.d.ts +42 -8
  29. package/dist/contracts/verification-receipt.js +24 -7
  30. package/dist/contracts/verification-view.d.ts +1 -0
  31. package/dist/deploy-gate.d.ts +17 -0
  32. package/dist/deploy-gate.js +68 -0
  33. package/dist/dispatch.js +3 -10
  34. package/dist/dispose.js +4 -1
  35. package/dist/evidence-pack.d.ts +1 -1
  36. package/dist/evidence-pack.js +20 -2
  37. package/dist/evidence.d.ts +1 -0
  38. package/dist/evidence.js +2 -1
  39. package/dist/exec.d.ts +1 -1
  40. package/dist/exec.js +7 -3
  41. package/dist/flow-engine.js +5 -1
  42. package/dist/flow-gallery-ui.js +4 -2
  43. package/dist/flow-gallery.d.ts +3 -0
  44. package/dist/flow-gallery.js +6 -2
  45. package/dist/flow-pull-request.d.ts +5 -2
  46. package/dist/flow-pull-request.js +14 -5
  47. package/dist/flow-schedule.js +25 -6
  48. package/dist/flow-triggers.d.ts +8 -0
  49. package/dist/flow-triggers.js +15 -0
  50. package/dist/flows-cli.js +13 -3
  51. package/dist/flows-ui.js +7 -1
  52. package/dist/guides.js +10 -1
  53. package/dist/invoke.d.ts +17 -6
  54. package/dist/invoke.js +48 -18
  55. package/dist/kits-ui.js +4 -2
  56. package/dist/kits.d.ts +6 -4
  57. package/dist/kits.js +9 -11
  58. package/dist/lead-status.js +5 -2
  59. package/dist/live-terminal.d.ts +10 -6
  60. package/dist/live-terminal.js +55 -14
  61. package/dist/mcp-connect.d.ts +10 -0
  62. package/dist/mcp-connect.js +2 -0
  63. package/dist/nightly-check.d.ts +104 -0
  64. package/dist/nightly-check.js +362 -0
  65. package/dist/operate.js +39 -2
  66. package/dist/person-proof.d.ts +2 -0
  67. package/dist/person-proof.js +14 -0
  68. package/dist/plan-auto.js +4 -4
  69. package/dist/planner-source.d.ts +1 -1
  70. package/dist/project-delete-ui.js +1 -1
  71. package/dist/project-tools.js +3 -1
  72. package/dist/proof.js +2 -0
  73. package/dist/protected-storage.d.ts +67 -0
  74. package/dist/protected-storage.js +398 -0
  75. package/dist/publish.js +156 -144
  76. package/dist/pull-request-flow.d.ts +2 -0
  77. package/dist/pull-request-flow.js +24 -2
  78. package/dist/release-adapters.d.ts +16 -1
  79. package/dist/release-adapters.js +292 -29
  80. package/dist/release-authority.d.ts +118 -0
  81. package/dist/release-authority.js +135 -0
  82. package/dist/release-card.d.ts +76 -0
  83. package/dist/release-card.js +252 -0
  84. package/dist/release-carry.d.ts +236 -0
  85. package/dist/release-carry.js +664 -0
  86. package/dist/release-cli.d.ts +3 -0
  87. package/dist/release-cli.js +158 -6
  88. package/dist/release-coverage.js +3 -0
  89. package/dist/release-git.d.ts +43 -0
  90. package/dist/release-git.js +106 -0
  91. package/dist/release-recheck.d.ts +146 -0
  92. package/dist/release-recheck.js +657 -0
  93. package/dist/release-view.d.ts +34 -0
  94. package/dist/release-view.js +27 -0
  95. package/dist/release.d.ts +139 -3
  96. package/dist/release.js +641 -250
  97. package/dist/remote-main.d.ts +2 -0
  98. package/dist/remote-main.js +1 -1
  99. package/dist/result-completion.d.ts +6 -0
  100. package/dist/result-completion.js +15 -1
  101. package/dist/result-follow-ups.js +29 -1
  102. package/dist/sandbox-runtime.d.ts +16 -5
  103. package/dist/sandbox-runtime.js +19 -6
  104. package/dist/scope.d.ts +122 -5
  105. package/dist/scope.js +236 -7
  106. package/dist/server/flows.js +40 -10
  107. package/dist/server/render-settings.js +1 -1
  108. package/dist/server/render-tasks.d.ts +8 -0
  109. package/dist/server/render-tasks.js +50 -9
  110. package/dist/server/route-table.js +1 -0
  111. package/dist/server/settings.js +26 -5
  112. package/dist/server/task-views.js +4 -0
  113. package/dist/server/tasks.js +12 -0
  114. package/dist/setup-guide.d.ts +2 -2
  115. package/dist/store.d.ts +230 -5
  116. package/dist/store.js +733 -37
  117. package/dist/subagent-memory.d.ts +1 -0
  118. package/dist/subagent-memory.js +1 -1
  119. package/dist/subagent-tools.d.ts +22 -6
  120. package/dist/subagent-tools.js +44 -8
  121. package/dist/subagent-work.d.ts +2 -0
  122. package/dist/subagent-work.js +5 -2
  123. package/dist/subagents-ui.js +6 -4
  124. package/dist/task-outcome-cli.js +7 -3
  125. package/dist/telegram.d.ts +6 -1
  126. package/dist/telegram.js +58 -6
  127. package/dist/toolroll-update.js +2 -2
  128. package/dist/verification-evidence.d.ts +25 -1
  129. package/dist/verification-evidence.js +57 -2
  130. package/dist/work-index.js +3 -6
  131. package/package.json +1 -1
@@ -14,6 +14,7 @@ import { legOf, routeDigestOf, routeFromJson, routeProblems } from "./phase-rout
14
14
  import { canonicalAcceptance, digestOf, parseAcceptanceCriteria, profileDigestOf, profileFromJson } from "./scope.js";
15
15
  import { hasDisguisedText, hasForbiddenControls } from "./decision.js";
16
16
  import { BUILT_IN, parseCapabilityKey } from "./store.js";
17
+ import { fileNightlyCheck } from "./nightly-check.js";
17
18
  export const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
18
19
  export function validTimezone(value) {
19
20
  if (value.trim() !== value)
@@ -195,10 +196,22 @@ export class StandingOrderRefused extends Error {
195
196
  */
196
197
  export function fileStandingOrder(store, input, now) {
197
198
  const { order, repo } = input;
199
+ // The nightly full check on main files a check-only task of its own (nightly-check.ts).
200
+ if (order.check !== undefined)
201
+ return fileNightlyCheck(store, input, now);
198
202
  const blocker = store.standingOrderBlocker(input.trigger, order.routine, now);
199
203
  if (blocker !== null)
200
204
  return { made: "skipped", note: `Skipped: the last one (${blocker.taskId}, ${blocker.state}) hasn't finished.` };
201
- const approval = standingApproval(order, repo, input.schedule);
205
+ const standing = standingApproval(order, repo, input.schedule);
206
+ // The carried approval stands only while its approver still can approve here (approval policy §1.6): a demoted
207
+ // person, one off the project, or an approval that names nobody files an ordinary proposal instead.
208
+ const approverStands = standing.live && standing.by !== null && (() => {
209
+ const account = store.accountOf(standing.by);
210
+ return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(standing.by, repo);
211
+ })();
212
+ const approval = standing.live && !approverStands
213
+ ? { live: false, problem: standing.by === null ? "it names no approver" : "its approver can no longer approve in this project", acceptance: standing.acceptance }
214
+ : standing;
202
215
  if (approval.acceptance === null)
203
216
  return { made: "skipped", note: "Skipped: its success checks can't be read. Set the flow up again." };
204
217
  if (order.costCeilingUsd !== null) {
@@ -238,11 +251,17 @@ export function fileStandingOrder(store, input, now) {
238
251
  store.setRequirements(ref.id, order.requirements);
239
252
  const draft = { goal: order.goal, outOfScope: order.outOfScope, touches: [...order.touches], acceptance: approval.acceptance, ...(order.budgetPerRunMicrousd == null ? {} : { budgetMicrousd: order.budgetPerRunMicrousd }) };
240
253
  const digest = digestOf(draft, approval.profile, approval.route);
241
- store.saveScope({ taskId, ...draft, proposedAt: now.toISOString(), digest, budgetMicrousd: order.budgetPerRunMicrousd ?? null, approvedAt: order.approval.at, approvedBy: approval.by, approvedDigest: digest }, {}, { profile: approval.profile, route: approval.route });
242
- const sealed = store.sealScopeApproval(taskId, approval.by ?? "schedule", now, {}, undefined, "automation");
243
- const sealedRoute = sealed ? store.sealedRouteOf(taskId) : null;
244
- if (!sealed || sealedRoute === null || !sealedRoute.ok || routeDigestOf(sealedRoute.route) !== routeDigestOf(approval.route)) {
245
- throw new StandingOrderRefused(!sealed ? "the approval couldn't be sealed" : sealedRoute !== null && !sealedRoute.ok ? sealedRoute.detail : "the sealed agents aren't the approved ones");
254
+ // Filed unapproved; only the seal below approves it (no approval fields are written beside it).
255
+ store.saveScope({ taskId, ...draft, proposedAt: now.toISOString(), digest, budgetMicrousd: order.budgetPerRunMicrousd ?? null, approvedAt: null, approvedBy: null, approvedDigest: null }, {}, { profile: approval.profile, route: approval.route });
256
+ // Its own authority (approval policy §1.6): the standing order's carried approval, never a person's ceremony. A seal
257
+ // the project's rules refuse to an automatic approval (checks beyond the sandbox, protected work, policy) leaves the
258
+ // task filed and waiting for a person, as any unapproved firing does — never a dropped firing.
259
+ const sealed = store.sealScopeApproval(taskId, approval.by, now, {}, { kind: "schedule", modeDigest: order.approval.digest }, "automation");
260
+ if (!sealed)
261
+ return { made: "filed", taskId, approved: false, note: "Its approval can't stand on its own here (its checks reach beyond the sandbox, or the project's rules need a person), so this one waits for approval." };
262
+ const sealedRoute = store.sealedRouteOf(taskId);
263
+ if (sealedRoute === null || !sealedRoute.ok || routeDigestOf(sealedRoute.route) !== routeDigestOf(approval.route)) {
264
+ throw new StandingOrderRefused(sealedRoute !== null && !sealedRoute.ok ? sealedRoute.detail : "the sealed agents aren't the approved ones");
246
265
  }
247
266
  return { made: "filed", taskId, approved: true, note: null };
248
267
  }
@@ -57,6 +57,11 @@ export declare const senderOf: (config: Extract<TriggerConfig, {
57
57
  /** Whether this trigger takes deliveries at a secret address rather than being checked. */
58
58
  /** Whether a raw trigger the caller is about to add would take deliveries (an address): read leniently, before validation. */
59
59
  export declare function rawTakesDeliveries(raw: unknown): boolean;
60
+ /** Whether a trigger brings in items from anyone (chat-management.md §0a.3): GitHub issues or pull requests from anyone,
61
+ * or mail from any sender. A widening: it takes a person's password, on every road that adds one. */
62
+ export declare function admitsAnyone(config: TriggerConfig): boolean;
63
+ /** The same, read leniently from a raw trigger before validation (so the form can ask for the password first). */
64
+ export declare function rawAdmitsAnyone(raw: unknown): boolean;
60
65
  export declare function takesDeliveries(config: TriggerConfig): boolean;
61
66
  /** A webhook's signing secret (made here, or pasted from its sender). In the keychain (or an owner-only file where there's none), never a column. */
62
67
  export declare function saveHookSecret(dir: string, trigger: number, secret: string, credential?: string): void;
@@ -104,6 +109,9 @@ export type TriggerMade = {
104
109
  export declare function addFlowTriggerTo(store: Store, flow: FlowRow, raw: unknown, actor: string, now: Date, dir: string | null, options?: {
105
110
  shown?: boolean;
106
111
  ingress?: "proved";
112
+ /** The person proved themselves for this act (a password, or a session under Passwordless): a trigger that admits
113
+ * anyone is added only then. */
114
+ widening?: "proved";
107
115
  }): TriggerMade;
108
116
  /** A new secret address for a webhook trigger (the old one stops working at once). */
109
117
  export declare function renewFlowHook(store: Store, trigger: FlowTriggerRow, now: Date, dir: string): TriggerMade;
@@ -368,6 +368,18 @@ export function rawTakesDeliveries(raw) {
368
368
  const one = raw;
369
369
  return one["kind"] === "webhook" || ((one["kind"] === "github" || one["kind"] === "linear") && one["delivery"] === "webhook");
370
370
  }
371
+ /** Whether a trigger brings in items from anyone (chat-management.md §0a.3): GitHub issues or pull requests from anyone,
372
+ * or mail from any sender. A widening: it takes a person's password, on every road that adds one. */
373
+ export function admitsAnyone(config) {
374
+ return (config.kind === "github" && config.from === "anyone") || (config.kind === "email" && config.sender === null);
375
+ }
376
+ /** The same, read leniently from a raw trigger before validation (so the form can ask for the password first). */
377
+ export function rawAdmitsAnyone(raw) {
378
+ if (raw === null || typeof raw !== "object")
379
+ return false;
380
+ const one = raw;
381
+ return (one["kind"] === "github" && one["from"] === "anyone") || (one["kind"] === "email" && (typeof one["sender"] !== "string" || one["sender"].trim() === ""));
382
+ }
371
383
  export function takesDeliveries(config) {
372
384
  return config.kind === "webhook" || ((config.kind === "github" || config.kind === "linear") && config.delivery === "webhook");
373
385
  }
@@ -495,6 +507,9 @@ export function addFlowTriggerTo(store, flow, raw, actor, now, dir, options = {}
495
507
  return { ok: false, message: "Webhook addresses are set up on the console." };
496
508
  if (takesDeliveries(config) && config.kind !== "webhook" && options.ingress !== "proved")
497
509
  return { ok: false, message: "Webhook delivery is set up on the flow's trigger panel, with your password." };
510
+ // Items from anyone come in only on a person's proof (chat-management.md §0a.3): never from a card, an import or a template alone.
511
+ if (admitsAnyone(config) && options.ingress !== "proved" && options.widening !== "proved")
512
+ return { ok: false, message: "A trigger that brings in items from anyone takes your password. Add it on the flow's Triggers panel." };
498
513
  // v126: a webhook made where no one is shown its address (the gallery, an import) is made with none: its first
499
514
  // set-up or New address makes one and shows it. A webhook made on its panel is shown its address now.
500
515
  const token = takesDeliveries(config) && options.ingress === "proved" && (options.shown !== false || config.kind !== "webhook") ? randomBytes(24).toString("base64url") : null;
package/dist/flows-cli.js CHANGED
@@ -21,7 +21,8 @@ import { deciderOf, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowDigest, flowFromSteps,
21
21
  import { prepareUnattendedCandidates, activeUnattendedGrant, parseUnattendedAsk, revokeUnattended, signUnattended, unattendedViewOf, unattendedWords, untilWords, UNATTENDED_MAX_RUNS } from './flow-unattended.js';
22
22
  import { MODE_MAX_DAYS } from './modes.js';
23
23
  import { verifyApproverByPassword } from './principal.js';
24
- import { proveCredential } from './scope.js';
24
+ import { authenticateApprover, proveCredential } from './scope.js';
25
+ import { personProof } from './person-proof.js';
25
26
  import { projectName } from './project.js';
26
27
  const EXIT = { ok: 0, failed: 1, usage: 2, refused: 3 };
27
28
  const json = { name: 'json', takesValue: false, meaning: 'answer with one machine envelope on stdout' };
@@ -157,6 +158,12 @@ export async function runFlowsCommand(positional, flags, context) {
157
158
  store.recordAction({ at: context.clock().toISOString(), actor: who, repo: project, taskId: null, runId: null, action: command, outcome, source: 'request', detail: `command line${detail === null ? '' : ` · ${detail}`}` });
158
159
  };
159
160
  const refuse = (project, reason, message, code = EXIT.refused) => { record(project, 'refused', reason); return fail(reason, message, code); };
161
+ // A trigger from anyone widens what comes in (chat-management.md §0a.3): only the person's own proof counts, never a
162
+ // machine credential standing in for them (a remote run's API token, a lead's token), though both pass the check above.
163
+ const widening = (project) => {
164
+ const proved = authenticateApprover(store, acting.name, acting.token, project);
165
+ return proved.ok && personProof(proved.method) ? { widening: 'proved' } : {};
166
+ };
160
167
  const settle = (project) => { try {
161
168
  advanceFlows(store, project, context.clock(), { evidenceRoot: context.evidenceRoot });
162
169
  }
@@ -243,11 +250,13 @@ export async function runFlowsCommand(positional, flags, context) {
243
250
  if (!flags.has('yes'))
244
251
  return preview(project, `Create the ${flowName} flow in ${projectName(project)}`, terms, { definition });
245
252
  let id;
253
+ const proof = template?.trigger === undefined ? {} : widening(project);
246
254
  try {
247
255
  id = store.transact(() => {
248
256
  const made = store.createFlow({ repo: project, name: flowName, definitionJson: JSON.stringify(definition), by: who }, now);
249
257
  if (template?.trigger !== undefined) {
250
- const trigger = addFlowTriggerTo(store, store.getFlow(made), template.trigger, who, now, context.configDir);
258
+ // A trigger from anyone only on the person's own proof (widening above); a machine credential's is refused.
259
+ const trigger = addFlowTriggerTo(store, store.getFlow(made), template.trigger, who, now, context.configDir, proof);
251
260
  if (!trigger.ok)
252
261
  throw new Error(trigger.message);
253
262
  }
@@ -508,7 +517,8 @@ export async function runFlowsCommand(positional, flags, context) {
508
517
  const ingress = rawTakesDeliveries(input.value);
509
518
  if (ingress && !proveCredential(store, acting.name, acting.token).ok)
510
519
  return refuse(flow.repo, 'unauthenticated', 'Making a webhook address takes your password, typed: pass --token.');
511
- const made = addFlowTriggerTo(store, flow, input.value, who, now, context.configDir, ingress ? { ingress: 'proved' } : {});
520
+ // A trigger from anyone only on the person's own proof (widening above): a machine credential's is refused.
521
+ const made = addFlowTriggerTo(store, flow, input.value, who, now, context.configDir, ingress ? { ingress: 'proved' } : widening(flow.repo));
512
522
  if (!made.ok)
513
523
  return refuse(flow.repo, 'invalid-trigger', made.message);
514
524
  await prepareUnattendedCandidates(store, flow.id);
package/dist/flows-ui.js CHANGED
@@ -8,6 +8,7 @@ import { SENDER_WORDS } from "./webhook-signing.js";
8
8
  import { READER_CAPTURED } from "./webhook-readers.js";
9
9
  import { hostOf, linkWords, provedWords, snapshotOf } from "./task-source.js";
10
10
  import { deciderOf, FLOW_COLORS, FLOW_KIND_WORDS, FLOW_STAGE_KINDS } from "./flows.js";
11
+ import { nightlyHealthWords } from "./nightly-check.js";
11
12
  import { flowInsights, troubleWords } from "./flow-insights.js";
12
13
  import { parseSortDecision, sortChip } from "./flow-sort.js";
13
14
  import { flowSecretNames, readEmailSettings, sendingReady } from "./flow-actions.js";
@@ -68,7 +69,7 @@ export function flowsListHtml(store, flows, projects, canCreate, problem) {
68
69
  const definition = flowDefinitionOf(flow);
69
70
  const waiting = cards.filter(card => ["approval", "choose"].includes(definition?.stages.find(one => one.id === card.stage)?.kind ?? "")).length;
70
71
  const buttons = store.flowTriggers(flow.id).filter(one => one.state === "active").flatMap(one => { const config = triggerConfigOf(one); return config?.kind === "button" ? [{ id: one.id, label: config.label }] : []; });
71
- const trouble = troubleWords(flowInsights(store, flow, new Date(), 7));
72
+ const trouble = troubleWords(flowInsights(store, flow, new Date(), 7)) ?? nightlyHealthOf(store, flow);
72
73
  return html `<article class="card"><div class="flow-row"><h2><a href="/flows/${flow.id}">${flow.name}</a></h2><span class="flow-counts">${projectName(flow.repo)} · ${cards.length} card${cards.length === 1 ? "" : "s"} in progress${waiting > 0 ? ` · ${waiting} waiting for a decision` : ""}${trouble === null ? "" : ` · ${trouble}`}</span></div>${buttons.length > 0 && html `<p class="flow-buttons">${joinHtml(buttons.map(one => html `<a class="button-link" href="/flows/${flow.id}?start=${one.id}">${one.label}</a>`), " ")}</p>`}</article>`;
73
74
  });
74
75
  const projectField = projects.length === 1 ? html `<input type="hidden" name="repo" value="${projects[0]}">` : html `<label>Project<select name="repo">${projects.map(repo => html `<option value="${repo}">${projectName(repo)}</option>`)}</select></label>`;
@@ -275,3 +276,8 @@ export function flowFallbackHtml(view) {
275
276
  return html `<li><strong>${stage.title}</strong> · ${FLOW_KIND_WORDS[stage.kind].label}${cards.length > 0 && html `<ul>${cards.map(card => html `<li>${card.title}${card.waiting !== null && ` — ${card.waiting}`}</li>`)}</ul>`}</li>`;
276
277
  })}</ol></section>`;
277
278
  }
279
+ /** The nightly check's own health on its flow (release-once.md, part 3): nothing since the last firing for 26 hours. */
280
+ function nightlyHealthOf(store, flow) {
281
+ const nightly = store.flowTriggers(flow.id).find(one => one.state === "active" && triggerConfigOf(one)?.kind === "schedule" && triggerConfigOf(one).order?.check !== undefined);
282
+ return nightly === undefined ? null : nightlyHealthWords(store, flow.repo, nightly.updatedAt, new Date());
283
+ }
package/dist/guides.js CHANGED
@@ -445,7 +445,13 @@ with a time limit.
445
445
 
446
446
  ## What it does, in order
447
447
 
448
- 1. **gate**: files task \`release-<version>-<sha7>\` pinned to the
448
+ 1. **gate**: before filing, looks at the pull request's checks on the
449
+ branch's exact commit: a required check that failed stops here, before
450
+ anything is filed or sent for approval. Checks not started or still
451
+ running are named and waited for (GitHub not answering is waited out
452
+ too); if they don't finish within the gate's limit (60 min, \`--limit
453
+ gate=<min>\`), it stops here as well. A task already filed for the commit is kept and
454
+ not looked at again. Then it files task \`release-<version>-<sha7>\` pinned to the
449
455
  branch's exact commit with \`--checks full\`, regardless of the project's default.
450
456
  2. **approval**: waits for the owner to approve that exact scope
451
457
  (\`toolroll task approve <id> --digest <d>\`, or the console). A
@@ -499,6 +505,9 @@ instead of making them twice.
499
505
  checks and obtain a passing result; Quick and Off cannot authorize a release.
500
506
  - \`check-failed\`: fix the branch and release the new commit with \`--new\`.
501
507
  - \`ci-failed\`: fix the failed GitHub check, then rerun. Nothing was merged.
508
+ A stop at the gate comes before the task is filed, so nothing was filed
509
+ either: a fix pushed as a new commit is released with \`--new\`; after
510
+ rerunning the failed job on GitHub, run the same command again.
502
511
  - \`publish-failed\` / \`homebrew-failed\`: fix the failed workflow before
503
512
  resuming. A failed tag check cannot finish publication or update Homebrew.
504
513
  - \`base-moved\`: main gained a commit the branch doesn't have, so the
package/dist/invoke.d.ts CHANGED
@@ -136,13 +136,24 @@ export declare function fencedCheckoutCommands(store: Store, repo: string, check
136
136
  * started unless all of it is.
137
137
  */
138
138
  /**
139
- * Whether a check with sign-ins also reaches the keychain (macOS securityd: any item every program may read, not only
140
- * the sign-ins). Only while the project's sign-ins level stands as a person set it: widening to sign-ins takes an
141
- * instance operator's password typed now (operate.ts project check-access, Settings → Project → Check access), and the
142
- * one who set it must still be an approver for the project and an instance operator now. Anything else: no keychain.
139
+ * macOS: the sign-ins a check with sign-ins needs that live in the keychain, not in files: Claude Code's
140
+ * ("Claude Code-credentials", its current access token as CLAUDE_CODE_OAUTH_TOKEN) and gh's ("gh:github.com", as
141
+ * GH_TOKEN). Read here, by Toolroll, outside the sandbox, and handed to the check in its environment, so the check never
142
+ * reaches securityd (sandbox-runtime.ts SEATBELT_NO_KEYCHAIN) and nothing else in the keychain. The check can't renew
143
+ * Claude's token: one that has expired fails its sign-in. A value that isn't a plain token is left out.
143
144
  */
144
- export declare function signInsKeychain(store: Store, repo: string | null): boolean;
145
- export declare function signInsLaunch(store: Store, own: string, temp: string, checkout: string, policy: NetworkProfile, gitPaths: readonly string[], platform?: NodeJS.Platform): Promise<{
145
+ export declare const KEYCHAIN_SIGN_INS: readonly {
146
+ service: string;
147
+ env: string;
148
+ token(secret: string): string | null;
149
+ }[];
150
+ /** One keychain item's secret by its service, read with the system's `security` (null: none, or it can't be read). A
151
+ * keychain file may be named (tests: a throwaway one); never the item's value in an argument. */
152
+ export declare function readKeychainSecret(service: string, keychain?: string): string | null;
153
+ /** The keychain's sign-ins as a check's environment (macOS only; elsewhere, for anything missing, and where Toolroll
154
+ * keeps nothing in the keychain (TOOLROLL_SECRET_STORE=files: the test suite), nothing). */
155
+ export declare function keychainSignIns(platform?: NodeJS.Platform, read?: (service: string) => string | null): Record<string, string>;
156
+ export declare function signInsLaunch(store: Store, own: string, temp: string, checkout: string, policy: NetworkProfile, gitPaths: readonly string[], platform?: NodeJS.Platform, keychain?: () => Record<string, string>): Promise<{
146
157
  grants: SignInGrants;
147
158
  network: NetworkFence;
148
159
  env: Record<string, string>;
package/dist/invoke.js CHANGED
@@ -24,6 +24,7 @@ import { classifyTerminal } from "./exhaustion.js";
24
24
  import { liftAuthPause } from "./provider-auth.js";
25
25
  import { attestProvider } from "./attest.js";
26
26
  import { run as runCommand, runOwnerTag, streamsInput } from "./exec.js";
27
+ import { spawnSync } from "node:child_process";
27
28
  import { currentContainment } from "./containment.js";
28
29
  import { witnessedRunner } from "./process-custody.js";
29
30
  import { underStopWatch } from "./task-control.js";
@@ -1017,9 +1018,8 @@ export function fencedCommands(store, runId, writes, inner, available = () => ma
1017
1018
  ...options,
1018
1019
  fence: fenced.fence,
1019
1020
  // Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
1020
- // With sign-ins, the keychain too (Claude Code keeps its macOS sign-in there), only while a person still set it.
1021
- fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network,
1022
- ...(signInsKeychain(store, checkout === null ? null : store.getWorktree(checkout)?.repo ?? null) ? { keychain: true } : {}) },
1021
+ // Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
1022
+ fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network },
1023
1023
  confinement: "required",
1024
1024
  env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(fenced.cache === undefined || fenced.grants.writes === undefined ? {} : buildCacheEnv(fenced.cache)), ...(signIns?.env ?? {}) },
1025
1025
  ...(signIns === null ? {} : { omitEnv: [...(options?.omitEnv ?? []), ...SIGN_INS_OMITTED_ENV] }),
@@ -1088,9 +1088,8 @@ export function fencedCheckoutCommands(store, repo, checkout, inner, available =
1088
1088
  ...options,
1089
1089
  fence: launch.fence,
1090
1090
  // Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
1091
- // With sign-ins, the keychain too (Claude Code keeps its macOS sign-in there), only while a person still set it.
1092
- fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network,
1093
- ...(signInsKeychain(store, repo) ? { keychain: true } : {}) },
1091
+ // Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
1092
+ fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network },
1094
1093
  confinement: launch.confinement,
1095
1094
  env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(allowed === null ? {} : buildCacheEnv(cache)), ...(signIns?.env ?? {}) },
1096
1095
  ...(signIns === null ? {} : { omitEnv: [...(options?.omitEnv ?? []), ...SIGN_INS_OMITTED_ENV] }),
@@ -1178,19 +1177,50 @@ const SIGN_INS_OMITTED_ENV = ["CLAUDE_CONFIG_DIR"];
1178
1177
  * started unless all of it is.
1179
1178
  */
1180
1179
  /**
1181
- * Whether a check with sign-ins also reaches the keychain (macOS securityd: any item every program may read, not only
1182
- * the sign-ins). Only while the project's sign-ins level stands as a person set it: widening to sign-ins takes an
1183
- * instance operator's password typed now (operate.ts project check-access, Settings → Project → Check access), and the
1184
- * one who set it must still be an approver for the project and an instance operator now. Anything else: no keychain.
1180
+ * macOS: the sign-ins a check with sign-ins needs that live in the keychain, not in files: Claude Code's
1181
+ * ("Claude Code-credentials", its current access token as CLAUDE_CODE_OAUTH_TOKEN) and gh's ("gh:github.com", as
1182
+ * GH_TOKEN). Read here, by Toolroll, outside the sandbox, and handed to the check in its environment, so the check never
1183
+ * reaches securityd (sandbox-runtime.ts SEATBELT_NO_KEYCHAIN) and nothing else in the keychain. The check can't renew
1184
+ * Claude's token: one that has expired fails its sign-in. A value that isn't a plain token is left out.
1185
1185
  */
1186
- export function signInsKeychain(store, repo) {
1187
- if (repo === null || store.projectCheckAccess(repo) !== "sign-ins")
1188
- return false;
1189
- const change = store.projectCheckAccessChange(repo);
1190
- const account = change === null ? null : store.accountOf(change.by);
1191
- return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(change.by, repo) && store.isInstanceOperator(change.by);
1186
+ export const KEYCHAIN_SIGN_INS = [
1187
+ { service: "Claude Code-credentials", env: "CLAUDE_CODE_OAUTH_TOKEN", token: secret => {
1188
+ try {
1189
+ const token = JSON.parse(secret).claudeAiOauth?.accessToken;
1190
+ return typeof token === "string" ? token : null;
1191
+ }
1192
+ catch {
1193
+ return null;
1194
+ }
1195
+ } },
1196
+ // go-keyring (gh's) may keep it base64-encoded behind a prefix.
1197
+ { service: "gh:github.com", env: "GH_TOKEN", token: secret => secret.startsWith("go-keyring-base64:") ? Buffer.from(secret.slice("go-keyring-base64:".length), "base64").toString("utf8") : secret },
1198
+ ];
1199
+ const PLAIN_TOKEN = /^[A-Za-z0-9._~+/=-]{8,4096}$/;
1200
+ /** One keychain item's secret by its service, read with the system's `security` (null: none, or it can't be read). A
1201
+ * keychain file may be named (tests: a throwaway one); never the item's value in an argument. */
1202
+ export function readKeychainSecret(service, keychain) {
1203
+ const read = spawnSync("/usr/bin/security", ["find-generic-password", "-s", service, "-w", ...keychain === undefined ? [] : [keychain]], { encoding: "utf8", timeout: 10_000, env: { PATH: "/usr/bin:/bin", HOME: homedir() }, stdio: ["ignore", "pipe", "ignore"] });
1204
+ return read.status === 0 && typeof read.stdout === "string" ? read.stdout.replace(/\n$/, "") : null;
1205
+ }
1206
+ /** The keychain's sign-ins as a check's environment (macOS only; elsewhere, for anything missing, and where Toolroll
1207
+ * keeps nothing in the keychain (TOOLROLL_SECRET_STORE=files: the test suite), nothing). */
1208
+ export function keychainSignIns(platform = process.platform, read) {
1209
+ if (platform !== "darwin")
1210
+ return {};
1211
+ if (read === undefined && process.env["TOOLROLL_SECRET_STORE"] === "files")
1212
+ return {};
1213
+ read ??= service => readKeychainSecret(service);
1214
+ const env = {};
1215
+ for (const one of KEYCHAIN_SIGN_INS) {
1216
+ const secret = read(one.service);
1217
+ const token = secret === null ? null : one.token(secret);
1218
+ if (token !== null && PLAIN_TOKEN.test(token))
1219
+ env[one.env] = token;
1220
+ }
1221
+ return env;
1192
1222
  }
1193
- export async function signInsLaunch(store, own, temp, checkout, policy, gitPaths, platform = process.platform) {
1223
+ export async function signInsLaunch(store, own, temp, checkout, policy, gitPaths, platform = process.platform, keychain = () => keychainSignIns(platform)) {
1194
1224
  if (!signInsAvailable(platform))
1195
1225
  return { problem: "Sign-ins can't be shared with checks here, so nothing ran." };
1196
1226
  const home = homedir();
@@ -1198,7 +1228,7 @@ export async function signInsLaunch(store, own, temp, checkout, policy, gitPaths
1198
1228
  let egress = null;
1199
1229
  const close = async () => { await egress?.close(); };
1200
1230
  try {
1201
- const env = { CODEX_HOME: join(home, ".codex"), GH_CONFIG_DIR: join(home, ".config", "gh"), XDG_CONFIG_HOME: join(home, ".config") };
1231
+ const env = { CODEX_HOME: join(home, ".codex"), GH_CONFIG_DIR: join(home, ".config", "gh"), XDG_CONFIG_HOME: join(home, ".config"), ...keychain() };
1202
1232
  if (policy.mode === "open")
1203
1233
  return { grants, network: { mode: "open" }, env, close };
1204
1234
  // Exactly the project's network: the services the sign-ins are for are reached only if the operator allowed their
package/dist/kits-ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { kitChecklist, kitInstalled, KITS } from "./kits.js";
2
2
  import { SUBAGENT_TEMPLATES } from "./subagents.js";
3
- import { html, postForm } from "./html.js";
3
+ import { html, postForm, reauth } from "./html.js";
4
4
  const CHECK = html `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
5
5
  const RING = html `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>`;
6
6
  export const KITS_CSS = `.kits{max-width:960px;min-width:0}.kits-lede{color:var(--so-muted);margin:.25rem 0 1.5rem;max-width:40rem}.kit-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}` +
@@ -35,7 +35,9 @@ export function kitPageHtml(store, kit, repo, dir, canSetUp, notice) {
35
35
  const rows = steps.map(step => {
36
36
  const go = step.done ? (step.href === null ? "" : html `<a class="kit-step-go" href="${step.href}">Open</a>`)
37
37
  : step.action === "sample" || step.action === "github"
38
- ? (canSetUp ? postForm(`/kits/${kit.id}/${step.action}`, html `<button${step.action === "sample" ? "" : html ` class="secondary"`}>${step.action === "sample" ? "Try it" : "Bring them in"}</button>`, { hidden: { repo } }) : "")
38
+ ? (canSetUp ? postForm(`/kits/${kit.id}/${step.action}`, step.action === "sample" ? html `<button>Try it</button>`
39
+ // Issues from anyone come in: it takes your password (an ordinary step-up; a session does under Passwordless).
40
+ : html `${reauth(html `<input type="password" name="password" autocomplete="current-password" aria-label="Your password" placeholder="Your password">`)}<button class="secondary">Bring them in</button>`, { hidden: { repo } }) : "")
39
41
  : step.href === null ? "" : html `<a class="kit-step-go" href="${step.href}">${step.action === "connect" ? "Connect" : "Set up"}</a>`;
40
42
  return html `<li data-step="${step.id}" data-done="${String(step.done)}">${step.done ? CHECK : RING}<span class="kit-step-said">${step.said}</span>${go}</li>`;
41
43
  });
package/dist/kits.d.ts CHANGED
@@ -53,9 +53,11 @@ export declare function kitInstalled(store: Store, kit: Kit, repo: string): {
53
53
  * flow, and its button triggers; then its subagent may use whichever of the
54
54
  * kit's tools the project already has (reading free, writing asking first).
55
55
  */
56
- export declare function setUpKit(store: Store, kit: Kit, repo: string, by: string, now: Date, dir: string | null, io?: ToolIo): Promise<Done>;
57
- /** The same, all at once (a chat confirmation can't wait on a tool): its subagent is given the kit's connected tools by the names their last test found, and each is listed properly on its first turn. */
58
- export declare function setUpKitNow(store: Store, kit: Kit, repo: string, by: string, now: Date, dir: string | null, grantByName?: boolean): Done;
56
+ export declare function setUpKit(store: Store, kit: Kit, repo: string, by: string, now: Date, dir: string | null, io?: ToolIo, options?: {
57
+ widening?: "proved";
58
+ }): Promise<Done>;
59
+ /** The same, all at once and without its tools (a chat confirmation is never a grant's proof): its subagent, its flow and its buttons. */
60
+ export declare function setUpKitNow(store: Store, kit: Kit, repo: string, by: string, now: Date, dir: string | null): Done;
59
61
  /** One line of a kit's checklist: done, or the next step with where to take it. */
60
62
  export type KitStep = {
61
63
  id: string;
@@ -76,7 +78,7 @@ export declare function addKitSample(store: Store, kit: Kit, repo: string, by: s
76
78
  said: string;
77
79
  };
78
80
  /** Bring new issues from the project's GitHub repository into the kit's flow (issues from anyone, polled). */
79
- export declare function addKitGithubTrigger(store: Store, kit: Kit, repo: string, by: string, now: Date, dir: string | null): {
81
+ export declare function addKitGithubTrigger(store: Store, kit: Kit, repo: string, by: string, now: Date, dir: string | null, proved?: "proved" | null): {
80
82
  ok: true;
81
83
  said: string;
82
84
  } | {
package/dist/kits.js CHANGED
@@ -17,7 +17,7 @@ import { readEmailSettings } from "./email-settings.js";
17
17
  import { mailboxReady } from "./mailbox.js";
18
18
  import { projectToolsOf } from "./project-tools.js";
19
19
  import { createSubagentFrom, nameOf } from "./subagent-admin.js";
20
- import { grantListed, grantTool } from "./subagent-tools.js";
20
+ import { grantTool } from "./subagent-tools.js";
21
21
  import { SUBAGENT_TEMPLATES } from "./subagents.js";
22
22
  export const KITS = [
23
23
  {
@@ -115,22 +115,23 @@ export function kitInstalled(store, kit, repo) {
115
115
  * flow, and its button triggers; then its subagent may use whichever of the
116
116
  * kit's tools the project already has (reading free, writing asking first).
117
117
  */
118
- export async function setUpKit(store, kit, repo, by, now, dir, io = {}) {
118
+ export async function setUpKit(store, kit, repo, by, now, dir, io = {}, options = {}) {
119
119
  const made = setUpKitNow(store, kit, repo, by, now, dir);
120
- if (!made.ok || made.said.endsWith("already set up here."))
120
+ // Its tools only with the person's proof (chat-management.md §0a.3); without it, the checklist says to let it use each.
121
+ if (!made.ok || made.said.endsWith("already set up here.") || options.widening !== "proved")
121
122
  return made;
122
123
  const mate = store.getSubagent(made.subagent);
123
124
  const present = new Set(projectToolsOf(store, repo).map(one => one.name));
124
125
  for (const tool of kit.tools.filter(one => present.has(one.tool))) {
125
126
  try {
126
- await grantTool(store, mate, tool.tool, by, now, io);
127
+ await grantTool(store, mate, tool.tool, by, now, io, options);
127
128
  }
128
129
  catch { /* it can be given the tool on its page */ }
129
130
  }
130
131
  return made;
131
132
  }
132
- /** The same, all at once (a chat confirmation can't wait on a tool): its subagent is given the kit's connected tools by the names their last test found, and each is listed properly on its first turn. */
133
- export function setUpKitNow(store, kit, repo, by, now, dir, grantByName = false) {
133
+ /** The same, all at once and without its tools (a chat confirmation is never a grant's proof): its subagent, its flow and its buttons. */
134
+ export function setUpKitNow(store, kit, repo, by, now, dir) {
134
135
  const had = kitInstalled(store, kit, repo);
135
136
  if (had !== null)
136
137
  return { ok: true, said: `${kit.name} is already set up here.`, flow: had.flow.id, subagent: had.mate.id };
@@ -155,9 +156,6 @@ export function setUpKitNow(store, kit, repo, by, now, dir, grantByName = false)
155
156
  const flow = store.getFlow(flowId);
156
157
  for (const button of kit.buttons)
157
158
  addFlowTriggerTo(store, flow, { kind: "button", label: button.label, questions: button.questions, zone: definition.start }, by, now, dir);
158
- if (grantByName)
159
- for (const tool of projectToolsOf(store, repo).filter(one => kit.tools.some(each => each.tool === one.name)))
160
- grantListed(store, mate, tool, null, by, now);
161
159
  return { ok: true, said: `${kit.name} is ready: ${nameOf(mate)} works its ${kit.flowName} flow.`, flow: flowId, subagent: mate.id };
162
160
  }
163
161
  /** What's set up and what's left, read fresh each time. */
@@ -205,11 +203,11 @@ export function addKitSample(store, kit, repo, by, now) {
205
203
  return added.ok ? { ok: true, href: flowCardHref(set.flow.id, added.card) } : { ok: false, said: added.message };
206
204
  }
207
205
  /** Bring new issues from the project's GitHub repository into the kit's flow (issues from anyone, polled). */
208
- export function addKitGithubTrigger(store, kit, repo, by, now, dir) {
206
+ export function addKitGithubTrigger(store, kit, repo, by, now, dir, proved = null) {
209
207
  const set = kitInstalled(store, kit, repo);
210
208
  const github = githubRepoOf(repo);
211
209
  if (set === null || github === null)
212
210
  return { ok: false, said: "This project isn't on GitHub." };
213
- const made = addFlowTriggerTo(store, set.flow, { kind: "github", repo: github, watch: "issues", from: "anyone", delivery: "poll", zone: flowDefinitionOf(set.flow)?.start }, by, now, dir);
211
+ const made = addFlowTriggerTo(store, set.flow, { kind: "github", repo: github, watch: "issues", from: "anyone", delivery: "poll", zone: flowDefinitionOf(set.flow)?.start }, by, now, dir, proved === null ? {} : { widening: proved });
214
212
  return made.ok ? { ok: true, said: `New issues on ${github} now come into ${kit.flowName}.` } : { ok: false, said: made.message };
215
213
  }
@@ -122,13 +122,16 @@ export function taskWaitSnapshot(store, taskId, now, watchedRun) {
122
122
  const scope = store.handle.prepare(`SELECT digest, approved_digest, profile_state,
123
123
  (approved_at IS NOT NULL AND approved_by IS NOT NULL AND approved_digest = digest) AS approved
124
124
  FROM task_scope WHERE task_id = ?`).get(taskId);
125
- if (task["plan"] === "drafted" && Number(scope?.["approved"]) !== 1)
125
+ // Approved only as dispatch reads it (approval policy): a seal whose authority doesn't stand — an old seal a lead token
126
+ // made, an ended mode — waits for a person, never for a worker.
127
+ const approvedNow = Number(scope?.["approved"]) === 1 && store.modeApprovalLive(taskRef, now);
128
+ if (task["plan"] === "drafted" && !approvedNow)
126
129
  return answer("Needs a person", "Approve plan", true, 1, "needs-person");
127
130
  if (Number(task["strikes"] ?? 0) >= 3)
128
131
  return answer("Needs a person", "Review repeated failures", true, 1, "needs-person");
129
132
  if (scope === undefined)
130
133
  return answer("Needs a person", "Add scope", true, 1, "needs-person");
131
- if (Number(scope["approved"]) !== 1 || scope["profile_state"] !== "resolved")
134
+ if (!approvedNow || scope["profile_state"] !== "resolved")
132
135
  return answer("Needs a person", "Approve task", true, 1, "needs-person");
133
136
  if (state === "running" && attempt === undefined) {
134
137
  const liveClaim = store.handle.prepare(`SELECT 1 AS hit FROM claim INDEXED BY claim_by_task
@@ -202,9 +202,11 @@ export declare class TerminalHost {
202
202
  * End the program and everything that is its own: SIGHUP to every member (an interactive shell gives each job its own
203
203
  * process group, so the leader's group is not enough), then SIGKILL, re-reading the process table each time so a
204
204
  * child started during the close is caught too. Membership is the leader's descendants, every process with this
205
- * terminal as its controlling terminal (started before the program ended), and every one noted while it ran, by pid
206
- * and start time. A process that detached itself from the terminal AND its parents before it was noted is not seen —
207
- * the same as any terminal. Resolves when the sweep is done; never waits without bound.
205
+ * terminal as its controlling terminal while the program runs, and every one noted while it ran, by pid and start
206
+ * time (membersOf). Each signal goes only to a process still that pid and start time; a process on the terminal after
207
+ * the program ended that was never noted, or a pid now someone else's, is never signalled and leaves the close
208
+ * uncertain. A process that detached itself from the terminal AND its parents before it was noted is not seen — the
209
+ * same as any terminal. Resolves when the sweep is done; never waits without bound.
208
210
  */
209
211
  close(id: string, by?: "owner" | "console"): Promise<void>;
210
212
  private sweep;
@@ -231,15 +233,17 @@ export type ProcessTable = Map<number, {
231
233
  export declare function readProcessTable(): ProcessTable | null;
232
234
  /**
233
235
  * A terminal's processes in this table, by pid and start time: what was noted while it ran (still the same process),
234
- * the leader, everything whose controlling terminal is its terminal and that started before it ended (a later process
235
- * on a reused device is someone else's), and every descendant of those. Never this console itself.
236
+ * the leader while it runs, everything whose controlling terminal is its terminal while it runs, and every descendant
237
+ * of those. Never this console itself. Once the program has ended its terminal's name is no one's proof: the device
238
+ * may already be someone else's, so a process on it that wasn't noted is not a member; it goes in `ambiguous` (when
239
+ * given), and the close that meets one says it is uncertain rather than signal it.
236
240
  */
237
241
  export declare function membersOf(table: ProcessTable, session: {
238
242
  leader: number;
239
243
  tty: string | null;
240
244
  known: ReadonlyMap<number, string>;
241
245
  exitedAt: number | null;
242
- }): Map<number, string>;
246
+ }, ambiguous?: Set<number>): Map<number, string>;
243
247
  /**
244
248
  * Each listed process's working directory (Linux: /proc; macOS: lsof), and which ones couldn't be read (a setgid
245
249
  * helper such as ssh-agent is unreadable even to its own user). Null when the lookup itself failed: a timeout, a signal