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.
- package/CHANGELOG.md +155 -0
- package/dist/agent-fence.d.ts +10 -2
- package/dist/agent-fence.js +47 -9
- package/dist/approval-authority.d.ts +20 -0
- package/dist/approval-authority.js +67 -0
- package/dist/assignment.js +26 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +28 -28
- package/dist/builder.js +48 -6
- package/dist/chat-actions.js +13 -8
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +186 -5
- package/dist/check-access.js +2 -2
- package/dist/check-report.d.ts +109 -0
- package/dist/check-report.js +205 -0
- package/dist/claim.js +1 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +111 -0
- package/dist/commit-extract.js +514 -0
- package/dist/contracts/console-api.d.ts +4 -4
- package/dist/contracts/console-api.js +5 -4
- package/dist/contracts/flow.d.ts +16 -0
- package/dist/contracts/flow.js +3 -0
- package/dist/contracts/release-check.d.ts +335 -0
- package/dist/contracts/release-check.js +159 -0
- package/dist/contracts/release-terms.d.ts +66 -0
- package/dist/contracts/release-terms.js +74 -0
- package/dist/contracts/verification-receipt.d.ts +42 -8
- package/dist/contracts/verification-receipt.js +24 -7
- package/dist/contracts/verification-view.d.ts +1 -0
- package/dist/deploy-gate.d.ts +17 -0
- package/dist/deploy-gate.js +68 -0
- package/dist/dispatch.js +3 -10
- package/dist/dispose.js +4 -1
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +20 -2
- package/dist/evidence.d.ts +1 -0
- package/dist/evidence.js +2 -1
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +7 -3
- package/dist/flow-engine.js +5 -1
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-pull-request.d.ts +5 -2
- package/dist/flow-pull-request.js +14 -5
- package/dist/flow-schedule.js +25 -6
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/flows-ui.js +7 -1
- package/dist/guides.js +10 -1
- package/dist/invoke.d.ts +17 -6
- package/dist/invoke.js +48 -18
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/lead-status.js +5 -2
- package/dist/live-terminal.d.ts +10 -6
- package/dist/live-terminal.js +55 -14
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/nightly-check.d.ts +104 -0
- package/dist/nightly-check.js +362 -0
- package/dist/operate.js +39 -2
- package/dist/person-proof.d.ts +2 -0
- package/dist/person-proof.js +14 -0
- package/dist/plan-auto.js +4 -4
- package/dist/planner-source.d.ts +1 -1
- package/dist/project-delete-ui.js +1 -1
- package/dist/project-tools.js +3 -1
- package/dist/proof.js +2 -0
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/publish.js +156 -144
- package/dist/pull-request-flow.d.ts +2 -0
- package/dist/pull-request-flow.js +24 -2
- package/dist/release-adapters.d.ts +16 -1
- package/dist/release-adapters.js +292 -29
- package/dist/release-authority.d.ts +118 -0
- package/dist/release-authority.js +135 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +236 -0
- package/dist/release-carry.js +664 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +158 -6
- package/dist/release-coverage.js +3 -0
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release-recheck.d.ts +146 -0
- package/dist/release-recheck.js +657 -0
- package/dist/release-view.d.ts +34 -0
- package/dist/release-view.js +27 -0
- package/dist/release.d.ts +139 -3
- package/dist/release.js +641 -250
- package/dist/remote-main.d.ts +2 -0
- package/dist/remote-main.js +1 -1
- package/dist/result-completion.d.ts +6 -0
- package/dist/result-completion.js +15 -1
- package/dist/result-follow-ups.js +29 -1
- package/dist/sandbox-runtime.d.ts +16 -5
- package/dist/sandbox-runtime.js +19 -6
- package/dist/scope.d.ts +122 -5
- package/dist/scope.js +236 -7
- package/dist/server/flows.js +40 -10
- package/dist/server/render-settings.js +1 -1
- package/dist/server/render-tasks.d.ts +8 -0
- package/dist/server/render-tasks.js +50 -9
- package/dist/server/route-table.js +1 -0
- package/dist/server/settings.js +26 -5
- package/dist/server/task-views.js +4 -0
- package/dist/server/tasks.js +12 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +230 -5
- package/dist/store.js +733 -37
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/task-outcome-cli.js +7 -3
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +25 -1
- package/dist/verification-evidence.js +57 -2
- package/dist/work-index.js +3 -6
- package/package.json +1 -1
package/dist/flow-schedule.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
}
|
package/dist/flow-triggers.d.ts
CHANGED
|
@@ -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;
|
package/dist/flow-triggers.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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**:
|
|
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
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
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
|
|
145
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
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
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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}`,
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
133
|
-
export function setUpKitNow(store, kit, repo, by, now, dir
|
|
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
|
}
|
package/dist/lead-status.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
package/dist/live-terminal.d.ts
CHANGED
|
@@ -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
|
|
206
|
-
*
|
|
207
|
-
* the
|
|
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
|
|
235
|
-
*
|
|
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
|