toolroll 0.9.5 → 0.9.7
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 +49 -0
- package/dist/actor.d.ts +21 -0
- package/dist/actor.js +48 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-presentation.d.ts +13 -2
- package/dist/assignment-presentation.js +22 -9
- package/dist/assignment-ui.d.ts +10 -15
- package/dist/assignment-ui.js +19 -18
- package/dist/assignment.d.ts +9 -0
- package/dist/assignment.js +62 -13
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +29 -24
- package/dist/browser-workspace.d.ts +120 -12
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +33 -13
- package/dist/chat-acceptance.js +2 -2
- package/dist/chat-actions.js +2 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-quiet.js +30 -19
- package/dist/chat-task-actions.d.ts +13 -0
- package/dist/chat-task-actions.js +19 -1
- package/dist/check-levels-ui.d.ts +21 -0
- package/dist/check-levels-ui.js +70 -0
- package/dist/check-levels.d.ts +67 -0
- package/dist/check-levels.js +171 -0
- package/dist/claim.d.ts +22 -0
- package/dist/claim.js +43 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/demo-chat.d.ts +0 -5
- package/dist/demo-chat.js +14 -3
- package/dist/dispatch.js +7 -2
- package/dist/dispose.d.ts +8 -0
- package/dist/dispose.js +7 -1
- package/dist/export.js +3 -2
- package/dist/first-run.d.ts +34 -0
- package/dist/first-run.js +37 -1
- package/dist/flow-gallery-ui.d.ts +30 -0
- package/dist/flow-gallery-ui.js +85 -0
- package/dist/flow-gallery.d.ts +111 -0
- package/dist/flow-gallery.js +445 -0
- package/dist/flow-share.d.ts +102 -0
- package/dist/flow-share.js +614 -0
- package/dist/flow-starters.d.ts +2 -0
- package/dist/flow-starters.js +1 -1
- package/dist/flow-steps.js +7 -0
- package/dist/flow-triggers.js +2 -0
- package/dist/flows-cli.d.ts +56 -0
- package/dist/flows-cli.js +109 -3
- package/dist/flows-ui.d.ts +15 -1
- package/dist/flows-ui.js +31 -6
- package/dist/guides.js +27 -2
- package/dist/lead-context.js +6 -2
- package/dist/lead-status.d.ts +7 -0
- package/dist/lead-status.js +11 -2
- package/dist/mate-doors.d.ts +0 -15
- package/dist/mate-doors.js +5 -1
- package/dist/mate-tools.js +9 -4
- package/dist/models-ui.js +1 -1
- package/dist/operate.d.ts +4 -1
- package/dist/operate.js +601 -85
- package/dist/project-concurrency.d.ts +62 -0
- package/dist/project-concurrency.js +116 -0
- package/dist/project-delete-ui.d.ts +7 -0
- package/dist/project-delete-ui.js +19 -2
- package/dist/project-delete.js +4 -0
- package/dist/pull-request-flow.d.ts +9 -0
- package/dist/pull-request-flow.js +21 -3
- package/dist/result-follow-ups.d.ts +101 -0
- package/dist/result-follow-ups.js +333 -0
- package/dist/scope.js +11 -0
- package/dist/serve.d.ts +211 -2
- package/dist/serve.js +1070 -204
- package/dist/store.d.ts +76 -3
- package/dist/store.js +311 -22
- package/dist/surface.js +31 -8
- package/dist/tailnet.d.ts +11 -0
- package/dist/tailnet.js +47 -0
- package/dist/task-outcome-cli.js +2 -2
- package/dist/task-status.d.ts +186 -0
- package/dist/task-status.js +335 -0
- package/dist/team-cli.js +3 -0
- package/dist/telegram-progress.js +62 -100
- package/dist/telegram-status.js +21 -8
- package/dist/verification-evidence.js +21 -1
- package/dist/webhooks.js +5 -0
- package/dist/work-index.js +60 -11
- package/dist/work-summary.d.ts +0 -4
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +10 -0
- package/dist/workspace-ui.js +33 -2
- package/package.json +1 -1
|
@@ -8,7 +8,8 @@ import type { Store } from './store.js';
|
|
|
8
8
|
import type { WorkSummaryAccess } from './work-summary.js';
|
|
9
9
|
import type { StatusTone } from './workspace-ui.js';
|
|
10
10
|
import type { AssignmentCard } from './assignment-ui.js';
|
|
11
|
-
import type {
|
|
11
|
+
import type { TaskStatus } from './task-status.js';
|
|
12
|
+
import type { FirstRunStep, FirstTaskSuggestion, JourneyStep } from './first-run.js';
|
|
12
13
|
export type BrowserProject = {
|
|
13
14
|
name: string;
|
|
14
15
|
path: string;
|
|
@@ -205,6 +206,12 @@ export type BrowserSettingsView = {
|
|
|
205
206
|
chat?: {
|
|
206
207
|
mode: 'quiet' | 'all';
|
|
207
208
|
digestAt: string | null;
|
|
209
|
+
/** Each project this person can see; a muted one sends no pings but stays in Tasks and the digest. */
|
|
210
|
+
projects?: {
|
|
211
|
+
repo: string;
|
|
212
|
+
name: string;
|
|
213
|
+
muted: boolean;
|
|
214
|
+
}[];
|
|
208
215
|
} | null;
|
|
209
216
|
digest: {
|
|
210
217
|
every: string;
|
|
@@ -284,12 +291,39 @@ export type BrowserTaskSection = {
|
|
|
284
291
|
open: boolean;
|
|
285
292
|
count: number | null;
|
|
286
293
|
};
|
|
294
|
+
/** `at`: a moment, shown in the reader's own time ("16:39", "Yesterday 16:39", "Sep 28"). */
|
|
287
295
|
export type BrowserTaskFact = {
|
|
288
296
|
label: string;
|
|
289
297
|
parts: (string | BrowserLink | {
|
|
290
298
|
seal: string;
|
|
299
|
+
} | {
|
|
300
|
+
at: string;
|
|
291
301
|
})[];
|
|
292
302
|
};
|
|
303
|
+
/** One entry in a task's thread, in time order beside the task's own
|
|
304
|
+
* conversation: the plan, the agent's progress notes and results, its
|
|
305
|
+
* questions, and the person's replies. `html` is the server's own card (same
|
|
306
|
+
* forms and ids) when the entry carries one. */
|
|
307
|
+
export type BrowserTaskThreadItem = {
|
|
308
|
+
key: string;
|
|
309
|
+
at: string;
|
|
310
|
+
kind: 'filed' | 'plan' | 'progress' | 'result' | 'question' | 'reply';
|
|
311
|
+
who: 'agent' | 'person';
|
|
312
|
+
author: string;
|
|
313
|
+
title: string;
|
|
314
|
+
text: string | null;
|
|
315
|
+
link: BrowserLink | null;
|
|
316
|
+
html: string;
|
|
317
|
+
more: {
|
|
318
|
+
summary: string;
|
|
319
|
+
html: string;
|
|
320
|
+
} | null;
|
|
321
|
+
};
|
|
322
|
+
/** The task's metadata, grouped for the Details panel (desk) or sheet (phone). */
|
|
323
|
+
export type BrowserTaskDetailGroup = {
|
|
324
|
+
title: 'Work' | 'Links' | 'Review' | 'About';
|
|
325
|
+
facts: BrowserTaskFact[];
|
|
326
|
+
};
|
|
293
327
|
export type BrowserTaskView = {
|
|
294
328
|
kind: 'task';
|
|
295
329
|
id: string;
|
|
@@ -327,6 +361,13 @@ export type BrowserTaskView = {
|
|
|
327
361
|
href: string;
|
|
328
362
|
starter: string;
|
|
329
363
|
} | null;
|
|
364
|
+
/** Until the first Ready result: Plan → You approve → Build → Checks → Ready, filled in as it moves. */
|
|
365
|
+
journey?: JourneyStep[] | null;
|
|
366
|
+
/** The thread's server-side entries; the conversation's messages join them in time order. */
|
|
367
|
+
thread?: BrowserTaskThreadItem[];
|
|
368
|
+
details?: BrowserTaskDetailGroup[];
|
|
369
|
+
/** Where to message the agent when this page carries no conversation (null: messaging isn't available here). */
|
|
370
|
+
chatHref?: string | null;
|
|
330
371
|
};
|
|
331
372
|
/** One project on the Projects page; opening it is a POST to /projects/open. */
|
|
332
373
|
export type BrowserProjectRow = {
|
|
@@ -342,6 +383,11 @@ export type BrowserProjectRow = {
|
|
|
342
383
|
href: string;
|
|
343
384
|
on: boolean;
|
|
344
385
|
};
|
|
386
|
+
/** Settings → Projects → Checks for this project, and its level in words. */
|
|
387
|
+
checks?: {
|
|
388
|
+
href: string;
|
|
389
|
+
level: string;
|
|
390
|
+
};
|
|
345
391
|
/** What waits, runs, queues or finished today; null when not scanned. */
|
|
346
392
|
peek: {
|
|
347
393
|
label: string;
|
|
@@ -365,12 +411,6 @@ export type BrowserProjectsView = {
|
|
|
365
411
|
};
|
|
366
412
|
};
|
|
367
413
|
export type BrowserResultTab = 'summary' | 'changes' | 'checks';
|
|
368
|
-
export type BrowserResultChip = {
|
|
369
|
-
tone: 'success' | 'danger' | 'warning' | 'info' | 'neutral';
|
|
370
|
-
label: string;
|
|
371
|
-
icon: 'check' | 'x' | null;
|
|
372
|
-
title: string | null;
|
|
373
|
-
};
|
|
374
414
|
/** The shared result panel in parts. Each tab's content, the feedback
|
|
375
415
|
* section and the learning card are the server's own HTML; the page script
|
|
376
416
|
* binds to the same data attributes and ids (tabs, drafts, line notes). */
|
|
@@ -378,10 +418,8 @@ export type BrowserResultPanel = {
|
|
|
378
418
|
attributes: Record<string, string>;
|
|
379
419
|
heading: string;
|
|
380
420
|
outcome: string;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
by: string | null;
|
|
384
|
-
} | null;
|
|
421
|
+
/** The shared task status (task-status.ts): headline, sentence and detail rows. */
|
|
422
|
+
status: TaskStatus | null;
|
|
385
423
|
reviewHistory: string | null;
|
|
386
424
|
attention: string[];
|
|
387
425
|
/** Requirements only a person can confirm, in plain words, and the one
|
|
@@ -759,6 +797,8 @@ export type BrowserFlowView = {
|
|
|
759
797
|
usedHere: string[];
|
|
760
798
|
language: "shell" | "python" | "node";
|
|
761
799
|
file: string | null;
|
|
800
|
+
/** It came with an imported flow and waits for a person's approval before it runs. */
|
|
801
|
+
held: boolean;
|
|
762
802
|
}[];
|
|
763
803
|
start: string;
|
|
764
804
|
stages: BrowserFlowStage[];
|
|
@@ -818,12 +858,80 @@ export type BrowserWorkspace = {
|
|
|
818
858
|
update?: BrowserUpdateNotice;
|
|
819
859
|
/** Chat's first run, until the first Ready result: the three steps and first tasks to try. */
|
|
820
860
|
firstRun?: BrowserFirstRun;
|
|
861
|
+
/** Chat, after the first Ready result until put away: use it from the phone. */
|
|
862
|
+
phone?: BrowserPhoneCard;
|
|
863
|
+
/** The Chat landing's live view: who is working now, four counts, and Catch up in tabs. */
|
|
864
|
+
home?: BrowserHome | null;
|
|
821
865
|
};
|
|
822
|
-
/**
|
|
866
|
+
/** One agent at work now: its task and the machine's own phase, in words. */
|
|
867
|
+
export type BrowserHomeAgent = {
|
|
868
|
+
runId: number;
|
|
869
|
+
taskId: string;
|
|
870
|
+
title: string;
|
|
871
|
+
href: string;
|
|
872
|
+
agent: string;
|
|
873
|
+
phase: string;
|
|
874
|
+
project: string | null;
|
|
875
|
+
since: string;
|
|
876
|
+
};
|
|
877
|
+
export type BrowserHomeCount = {
|
|
878
|
+
key: 'working' | 'waiting' | 'ready' | 'done';
|
|
879
|
+
label: string;
|
|
880
|
+
value: number;
|
|
881
|
+
href: string;
|
|
882
|
+
};
|
|
883
|
+
export type BrowserCatchUpTab = 'needs-you' | 'ready' | 'running' | 'all';
|
|
884
|
+
export type BrowserCatchUpItem = {
|
|
885
|
+
id: string;
|
|
886
|
+
title: string;
|
|
887
|
+
href: string;
|
|
888
|
+
project: string | null;
|
|
889
|
+
tab: Exclude<BrowserCatchUpTab, 'all'> | 'finished';
|
|
890
|
+
label: string;
|
|
891
|
+
tone: StatusTone;
|
|
892
|
+
detail: string;
|
|
893
|
+
at: string;
|
|
894
|
+
};
|
|
895
|
+
/** Plan-window use stands in for spend: subscriptions don't bill per run. */
|
|
896
|
+
export type BrowserHome = {
|
|
897
|
+
agents: BrowserHomeAgent[];
|
|
898
|
+
counts: BrowserHomeCount[];
|
|
899
|
+
planUse: {
|
|
900
|
+
name: string;
|
|
901
|
+
window: string;
|
|
902
|
+
percent: number;
|
|
903
|
+
detail: string;
|
|
904
|
+
tone: 'neutral' | 'warning' | 'danger';
|
|
905
|
+
}[];
|
|
906
|
+
catchUp: BrowserCatchUpItem[];
|
|
907
|
+
allHref: string;
|
|
908
|
+
};
|
|
909
|
+
/** `sandbox`: the demo command, offered beside the sign-in command while no agent is signed in. `intro`: how it works,
|
|
910
|
+
* once, above the composer with the suggestions. `lead`: the one line saying what runs the lead, and where to change it.
|
|
911
|
+
* `recheck`: while no agent is signed in, where the page asks again on its own. */
|
|
823
912
|
export type BrowserFirstRun = {
|
|
824
913
|
steps: FirstRunStep[];
|
|
825
914
|
suggestions: FirstTaskSuggestion[];
|
|
826
915
|
sandbox: string | null;
|
|
916
|
+
intro?: string;
|
|
917
|
+
lead?: {
|
|
918
|
+
words: string;
|
|
919
|
+
href: string;
|
|
920
|
+
} | null;
|
|
921
|
+
recheck?: string | null;
|
|
922
|
+
};
|
|
923
|
+
/** After the first Ready result: the phone, through a chat app or the console over Tailscale. `tailnet.restart`: the
|
|
924
|
+
* command that makes the console listen beyond this computer, when it doesn't yet. */
|
|
925
|
+
export type BrowserPhoneCard = {
|
|
926
|
+
chatApps: {
|
|
927
|
+
label: string;
|
|
928
|
+
href: string;
|
|
929
|
+
}[];
|
|
930
|
+
tailnet: {
|
|
931
|
+
address: string;
|
|
932
|
+
restart: string | null;
|
|
933
|
+
} | null;
|
|
934
|
+
dismissHref: string;
|
|
827
935
|
};
|
|
828
936
|
/** The console's update notice: neutral, never the accent — an update does not need a person. */
|
|
829
937
|
export type BrowserUpdateNotice = {
|
package/dist/builder.d.ts
CHANGED
|
@@ -368,6 +368,7 @@ export declare const NO_HANDOFF_WORDS = "The agent stopped before handing off; i
|
|
|
368
368
|
export declare const WIP_COMMIT_WORDS = "Work in progress: the agent stopped before handing off. Kept so the next attempt continues from it.";
|
|
369
369
|
/** The short turn a stopped agent's own session is resumed with. */
|
|
370
370
|
export declare function handoffResumePrompt(done: string): string;
|
|
371
|
+
export declare const SERVICE_STOP_WORDS = "Toolroll stopped while this task was building.";
|
|
371
372
|
/**
|
|
372
373
|
* What a non-zero agent exit means, in words a person can act on: the
|
|
373
374
|
* harness's own result line names the ending (a turn ceiling, an error
|
package/dist/builder.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OBSERVATION_MAILBOX, observationBrief, parseObservationCases, collectObservations } from "./observations.js";
|
|
2
|
+
import { checkCommandFor, effectiveCheckLevel, recordRunCheckLevel } from "./check-levels.js";
|
|
2
3
|
import { skillsContext } from "./project-skills.js";
|
|
3
4
|
import { failedVerificationEvidence, sealVerificationReceipt, verificationEvidence, reuseObservationVerification } from "./verification-evidence.js";
|
|
4
5
|
import { learningContext } from "./project-learning.js";
|
|
@@ -1469,11 +1470,7 @@ export async function settleProviderOutcome(captured, result) {
|
|
|
1469
1470
|
// own words.
|
|
1470
1471
|
if (stopRequestedFor(store, request.runId, request.shouldStop)) {
|
|
1471
1472
|
quarantineMailboxes(worktree, root, request.runId);
|
|
1472
|
-
return {
|
|
1473
|
-
ok: false,
|
|
1474
|
-
reason: "stopped",
|
|
1475
|
-
message: stopWords(store, request.runId, worktree, `the operator stopped this watch while the agent ran — the work is preserved uncommitted in ${worktree}`),
|
|
1476
|
-
};
|
|
1473
|
+
return { ok: false, reason: "stopped", message: await stoppedWords(captured) };
|
|
1477
1474
|
}
|
|
1478
1475
|
if (result.timedOut) {
|
|
1479
1476
|
// A mailbox cut down mid-write is quarantined, never ingested: whatever
|
|
@@ -1782,11 +1779,7 @@ export async function settleProviderOutcome(captured, result) {
|
|
|
1782
1779
|
// (audit IV-1): an operator's stop beats an agent's finish. The work
|
|
1783
1780
|
// stays in the worktree, uncommitted, preserved for the successor.
|
|
1784
1781
|
if (stopRequestedFor(store, request.runId, request.shouldStop)) {
|
|
1785
|
-
return {
|
|
1786
|
-
ok: false,
|
|
1787
|
-
reason: "stopped",
|
|
1788
|
-
message: stopWords(store, request.runId, worktree, `the operator stopped this watch while the agent ran — the work is preserved uncommitted in ${worktree}`),
|
|
1789
|
-
};
|
|
1782
|
+
return { ok: false, reason: "stopped", message: await stoppedWords(captured) };
|
|
1790
1783
|
}
|
|
1791
1784
|
store.setRunPhase(request.runId, "committing");
|
|
1792
1785
|
// Kept work that is already complete is already committed: the attempt's
|
|
@@ -2145,7 +2138,14 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2145
2138
|
// re-proved before every authorized spawn, and tracked post-commit changes
|
|
2146
2139
|
// stop recovery rather than being silently certified.
|
|
2147
2140
|
const repo = store.getWorktree(worktree)?.repo ?? null;
|
|
2148
|
-
|
|
2141
|
+
// The check level (check-levels.ts): the task's own choice, else the
|
|
2142
|
+
// project's. Quick runs its own approved command (or the full one when it
|
|
2143
|
+
// has none); Off runs nothing. The level that ran is recorded either way.
|
|
2144
|
+
const checkChoice = effectiveCheckLevel(store, repo, request.taskId);
|
|
2145
|
+
const chosenCheck = repo === null ? null : checkCommandFor(store, repo, checkChoice.level);
|
|
2146
|
+
const configured = chosenCheck?.command ?? null;
|
|
2147
|
+
if (repo !== null && chosenCheck !== null)
|
|
2148
|
+
recordRunCheckLevel(store, { id: runId, taskId: request.taskId, repo }, chosenCheck.level, checkChoice.from, now());
|
|
2149
2149
|
let verifyCommand;
|
|
2150
2150
|
const checkLog = [];
|
|
2151
2151
|
const checkOutcomes = [];
|
|
@@ -2238,7 +2238,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2238
2238
|
const liveRecoverySetup = () => {
|
|
2239
2239
|
if (repo === null || recoveryDigest === null)
|
|
2240
2240
|
return null;
|
|
2241
|
-
const liveVerify = store.liveVerifyCommand(repo);
|
|
2241
|
+
const liveVerify = store.liveVerifyCommand(configured.repo);
|
|
2242
2242
|
const liveSetup = store.liveWorktreeSetup(repo);
|
|
2243
2243
|
return liveVerify !== null &&
|
|
2244
2244
|
liveVerify.digest === configured.digest &&
|
|
@@ -2374,7 +2374,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2374
2374
|
: checkSuites.some(one => one.status === "failed") ? "failed"
|
|
2375
2375
|
: "not-run";
|
|
2376
2376
|
const suites = checkSuites.length > 0 ? checkSuites : verifyCommand.configured === false ? [] : [{
|
|
2377
|
-
name: reused === null ? "Project check" : "Project check (reused)",
|
|
2377
|
+
name: reused === null ? chosenCheck?.level === "quick" ? "Quick check" : "Project check" : "Project check (reused)",
|
|
2378
2378
|
status: checkStatus,
|
|
2379
2379
|
exitCode: "ran" in verifyCommand && verifyCommand.ran ? verifyCommand.exitCode : null,
|
|
2380
2380
|
}];
|
|
@@ -2589,6 +2589,26 @@ async function resumeUnhandedWork(captured, sessionId) {
|
|
|
2589
2589
|
}
|
|
2590
2590
|
return null;
|
|
2591
2591
|
}
|
|
2592
|
+
/**
|
|
2593
|
+
* The words for an attempt a stop ended after its agent ran. A task stop
|
|
2594
|
+
* names who asked. A SERVICE stop (the builder stopping or restarting) also
|
|
2595
|
+
* saves the work as a patch in the run's evidence before saying so: the
|
|
2596
|
+
* changes stay uncommitted in the checkout, and the requeued task resumes
|
|
2597
|
+
* from them on the builder's next pass.
|
|
2598
|
+
*/
|
|
2599
|
+
async function stoppedWords(captured) {
|
|
2600
|
+
const { store, request, git, worktree, root } = captured;
|
|
2601
|
+
if (store.applicableStopFor(request.runId) !== null) {
|
|
2602
|
+
return stopWords(store, request.runId, worktree, `stopped while the agent ran — the work is preserved uncommitted in ${worktree}`);
|
|
2603
|
+
}
|
|
2604
|
+
const changes = await unhandedChanges(git, worktree);
|
|
2605
|
+
const saved = changes === null || changes.length === 0 ? null
|
|
2606
|
+
: await saveWorkPatch(git, worktree, join(root, String(request.runId), "service-stop-work.patch"), `# work in ${worktree} when Toolroll stopped\n# kept ${captured.clock().toISOString()}\n`);
|
|
2607
|
+
const kept = changes !== null && changes.length === 0 ? "It had not changed any files yet"
|
|
2608
|
+
: saved !== null && saved.ok ? `Its work is kept in ${worktree} and saved as ${saved.file}` : `Its work is kept uncommitted in ${worktree}`;
|
|
2609
|
+
return `${SERVICE_STOP_WORDS} ${kept}; the task is back in the queue and resumes from it when the builder runs again.`;
|
|
2610
|
+
}
|
|
2611
|
+
export const SERVICE_STOP_WORDS = "Toolroll stopped while this task was building.";
|
|
2592
2612
|
/**
|
|
2593
2613
|
* The attempt ended without a handoff, even after any resumed turn. When it
|
|
2594
2614
|
* left changes, they are never discarded: first saved as a patch in this
|
package/dist/chat-acceptance.js
CHANGED
|
@@ -6,7 +6,7 @@ export function acceptanceStatus(proof, accepted) {
|
|
|
6
6
|
if (accepted)
|
|
7
7
|
return manualReviewOnly(proof) ? "Accepted by a person" : "Accepted with an exception";
|
|
8
8
|
if (manualReviewOnly(proof))
|
|
9
|
-
return "
|
|
9
|
+
return "You check the remaining requirements";
|
|
10
10
|
return proof === null ? "No checks recorded" : ({ verified: "Checks passed", attested: "Checks reported by the agent", short: "Some required material is missing", refuted: "Saved result conflicts with the approved scope" }[proof.verdict] ?? "Some required material is missing");
|
|
11
11
|
}
|
|
12
12
|
export function readAcceptanceEvidence(store, who, root, task, runId, offset = 0) {
|
|
@@ -75,7 +75,7 @@ export function readAcceptanceEvidence(store, who, root, task, runId, offset = 0
|
|
|
75
75
|
accepted: acceptance !== null, acceptance: acceptance === null ? null : { at: acceptance.acceptedAt, note: brief(acceptance.note) },
|
|
76
76
|
reasons: (recorded?.reasons ?? []).map(one => brief(one)), gate,
|
|
77
77
|
criteriaTotal: matrix.length, nextCriterionOffset: offset + page.length < matrix.length ? offset + page.length : null,
|
|
78
|
-
criteria: page.map(row => ({ id: row.id, requirement: brief(row.statement), state: row.state === "manual-review" ? "
|
|
78
|
+
criteria: page.map(row => ({ id: row.id, requirement: brief(row.statement), state: row.state === "manual-review" ? "You check" : row.state,
|
|
79
79
|
details: row.detail.map(one => brief(one)), requiredEvidence: row.requiredEvidence,
|
|
80
80
|
reviewer: row.review == null ? null : { judgement: row.review.judgement, note: brief(row.review.note) },
|
|
81
81
|
evidence: row.answered.map(one => ({ kind: one.kind, reference: brief(one.ref) })),
|
package/dist/chat-actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared, exact-state actions. Models may prepare; only the existing human
|
|
2
2
|
* confirmation door executes. Protected actions use a one-use review receipt. */
|
|
3
|
+
import { withActor } from "./actor.js";
|
|
3
4
|
import { publicChatText } from "./chat-display.js";
|
|
4
5
|
import { gateWords } from "./approval-policy.js";
|
|
5
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -1102,7 +1103,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1102
1103
|
throw Error(completed.message);
|
|
1103
1104
|
}
|
|
1104
1105
|
else if (payload.operation === "task_cancel") {
|
|
1105
|
-
const result = store.cancelTask(task, now);
|
|
1106
|
+
const result = withActor({ account: who.name, lead: false }, () => store.cancelTask(task, now));
|
|
1106
1107
|
if (!result.ok)
|
|
1107
1108
|
throw Error(`Task was not cancelled: ${result.reason}.`);
|
|
1108
1109
|
}
|
package/dist/chat-delivery.js
CHANGED
|
@@ -627,6 +627,8 @@ export async function planChatNotifications(options) {
|
|
|
627
627
|
const personal = notification.recipient !== null;
|
|
628
628
|
if (notification.createdAt >= binding.created &&
|
|
629
629
|
notification.resolvedAt === null &&
|
|
630
|
+
// Pings follow responsibility: the lead's work, this person's own act and a muted project stay in the console.
|
|
631
|
+
store.pingAllowed(notification, binding.approver) &&
|
|
630
632
|
// A flow decision for "anyone who approves" reaches every approver who can see the project.
|
|
631
633
|
// A notification addressed to this person reaches them whatever project
|
|
632
634
|
// their channel follows (a sign-in pause is the installation's, v108).
|
package/dist/chat-quiet.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { assignmentOf } from "./assignment.js";
|
|
8
8
|
import { chatControlHref } from "./chat-controls.js";
|
|
9
9
|
import { telegramProgressCard } from "./telegram-progress.js";
|
|
10
|
+
import { assignmentStatusFacts, headlineEmoji, replacedWords, taskStatusOf } from "./task-status.js";
|
|
10
11
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
11
12
|
import { isLifecycleNotification } from "./store.js";
|
|
12
13
|
/** What always makes a new message, even in quiet mode: a question or approval waiting, a failure or other
|
|
@@ -37,15 +38,16 @@ function cardRun(store, taskRef) {
|
|
|
37
38
|
return store.runsFor(taskRef).find(run => run.role === "builder" && run.contestant === null) ?? null;
|
|
38
39
|
}
|
|
39
40
|
/** Before any attempt starts, the card says where the task stands in the words of its last lifecycle fact. */
|
|
41
|
+
const before = (headline) => ({ icon: headlineEmoji(headline), status: headline });
|
|
40
42
|
const BEFORE_RUN = {
|
|
41
|
-
"task-filed":
|
|
42
|
-
"scope-approved":
|
|
43
|
-
"approval-withdrawn":
|
|
44
|
-
"task-held":
|
|
45
|
-
"task-released":
|
|
46
|
-
"task-queued":
|
|
47
|
-
"task-requeued":
|
|
48
|
-
"task-cancelled":
|
|
43
|
+
"task-filed": before("Queued"),
|
|
44
|
+
"scope-approved": before("Queued"),
|
|
45
|
+
"approval-withdrawn": before("Needs you"),
|
|
46
|
+
"task-held": before("Stopped"),
|
|
47
|
+
"task-released": before("Queued"),
|
|
48
|
+
"task-queued": before("Queued"),
|
|
49
|
+
"task-requeued": before("Queued"),
|
|
50
|
+
"task-cancelled": before("Stopped"),
|
|
49
51
|
};
|
|
50
52
|
function taskLine(store, taskRef, now, root) {
|
|
51
53
|
const ref = store.refById(taskRef);
|
|
@@ -53,6 +55,16 @@ function taskLine(store, taskRef, now, root) {
|
|
|
53
55
|
return null;
|
|
54
56
|
const task = store.getTask(ref.externalId);
|
|
55
57
|
const title = phoneText(task?.title ?? ref.externalId, 88);
|
|
58
|
+
// Replaced, never "Cancelled": the card names its successor and links to it.
|
|
59
|
+
const successor = task?.state === "cancelled" ? store.replacementOf(ref.externalId) : null;
|
|
60
|
+
if (successor !== null) {
|
|
61
|
+
const heading = `${headlineEmoji("Stopped")} ${replacedWords(successor)}`;
|
|
62
|
+
const text = [title, heading, "", `${projectLabel(ref.repo)} · ${ref.externalId}`].join("\n");
|
|
63
|
+
return { title, icon: headlineEmoji("Stopped"), status: replacedWords(successor), project: ref.repo, view: {
|
|
64
|
+
text, entities: [{ type: "bold", offset: 0, length: title.length }, { type: "bold", offset: title.length + 1, length: heading.length }],
|
|
65
|
+
link: { label: `Open ${phoneText(successor, 40)}`, path: chatControlHref("task", successor) },
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
56
68
|
const run = cardRun(store, taskRef);
|
|
57
69
|
if (run !== null) {
|
|
58
70
|
const view = telegramProgressCard(store, run, ref.externalId, ref.repo, now, root);
|
|
@@ -85,10 +97,8 @@ export function quietCardView(store, taskRefs, now, root) {
|
|
|
85
97
|
export function readyPingView(store, run, taskId, project, now, root) {
|
|
86
98
|
const card = telegramProgressCard(store, run, taskId, project, now, root);
|
|
87
99
|
const [title = "", shown = ""] = card.entities.map(one => card.text.slice(one.offset, one.offset + one.length));
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
const heading = saved ? "✅ Ready" : shown;
|
|
91
|
-
const next = card.next !== "" ? card.next : saved ? "Next: open the result, then mark it complete or request changes." : "";
|
|
100
|
+
const heading = shown;
|
|
101
|
+
const next = card.next;
|
|
92
102
|
const first = `${heading} · ${title}`;
|
|
93
103
|
return { text: next === "" ? first : `${first}\n${next}`, entities: [{ type: "bold", offset: 0, length: heading.length }], link: card.link };
|
|
94
104
|
}
|
|
@@ -120,18 +130,19 @@ export function eveningDigestText(store, account, now, root) {
|
|
|
120
130
|
failed.set(row.taskId, title(row.taskId));
|
|
121
131
|
}
|
|
122
132
|
for (const taskId of new Set(facts.map(row => row.taskId).filter((one) => one !== null))) {
|
|
123
|
-
let
|
|
133
|
+
let headline = null;
|
|
124
134
|
try {
|
|
125
135
|
const project = facts.find(row => row.taskId === taskId).project;
|
|
126
|
-
|
|
136
|
+
const assignment = assignmentOf(store, taskId, now, { principal: "operator", repos: [project] }, root);
|
|
137
|
+
headline = assignment === null ? null : taskStatusOf(assignmentStatusFacts(assignment)).headline;
|
|
127
138
|
}
|
|
128
139
|
catch {
|
|
129
|
-
|
|
140
|
+
headline = null;
|
|
130
141
|
}
|
|
131
|
-
if (
|
|
132
|
-
waits.set(taskId, `${title(taskId)} ·
|
|
133
|
-
else if (
|
|
134
|
-
|
|
142
|
+
if (headline === "Ready for review" || headline === "Needs you")
|
|
143
|
+
waits.set(taskId, `${title(taskId)} · ${headline}`);
|
|
144
|
+
else if (headline === "Failed")
|
|
145
|
+
failed.set(taskId, title(taskId));
|
|
135
146
|
}
|
|
136
147
|
if (finished.size + failed.size + waits.size === 0)
|
|
137
148
|
return null;
|
|
@@ -26,6 +26,18 @@ export declare const CHAT_TASK_ACTIONS: {
|
|
|
26
26
|
readonly label: "Remove dependency";
|
|
27
27
|
readonly detail: "Stops waiting for the selected task. Other requirements still apply.";
|
|
28
28
|
};
|
|
29
|
+
readonly run_checks: {
|
|
30
|
+
readonly label: "Run checks";
|
|
31
|
+
readonly detail: "Runs the project's quick check on this result's exact commit. A pass upgrades its status; a failure stays visible.";
|
|
32
|
+
};
|
|
33
|
+
readonly run_full_checks: {
|
|
34
|
+
readonly label: "Run full checks";
|
|
35
|
+
readonly detail: "Runs the project's full check on this result's exact commit. A pass upgrades its status; a failure stays visible.";
|
|
36
|
+
};
|
|
37
|
+
readonly add_tests: {
|
|
38
|
+
readonly label: "Add tests";
|
|
39
|
+
readonly detail: "Files a small task to write tests for this change. Nothing runs until it is approved.";
|
|
40
|
+
};
|
|
29
41
|
};
|
|
30
42
|
export declare function isChatTaskAction(value: unknown): value is keyof typeof CHAT_TASK_ACTIONS;
|
|
31
43
|
/** Bind cards to a specific execution and its current state, not the family's moving pointer. */
|
|
@@ -34,6 +46,7 @@ export declare function chatTaskStamp(store: Store, who: VerifiedApprover, task:
|
|
|
34
46
|
export declare function chatTaskRun(store: Store, task: string, operation: "stop" | "resume", now: Date): number | null;
|
|
35
47
|
export declare function applyChatTaskAction(store: Store, who: VerifiedApprover, payload: Record<string, unknown>, now: Date, web: boolean, controls?: Pick<StopRequest, "held" | "deferSignal"> & {
|
|
36
48
|
via?: StopRequest["via"];
|
|
49
|
+
evidenceRoot?: string;
|
|
37
50
|
}): {
|
|
38
51
|
ok: true;
|
|
39
52
|
taskId: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { verifiedAuthor } from "./store.js";
|
|
2
2
|
import { requestTaskStop, taskControlOf } from "./task-control.js";
|
|
3
3
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
4
|
+
import { fileAddTestsTask, requestFollowUpChecks } from "./result-follow-ups.js";
|
|
4
5
|
export const CHAT_TASK_ACTIONS = {
|
|
5
6
|
// Build #1604 feedback: one short consequence that still says saved work
|
|
6
7
|
// remains, other tasks continue, and the pause waits for acknowledgement.
|
|
@@ -10,6 +11,10 @@ export const CHAT_TASK_ACTIONS = {
|
|
|
10
11
|
plan: { label: "Plan first", detail: "Requests a plan. Building still requires approval." },
|
|
11
12
|
wait_for: { label: "Add dependency", detail: "Waits for the selected task to finish." },
|
|
12
13
|
stop_waiting: { label: "Remove dependency", detail: "Stops waiting for the selected task. Other requirements still apply." },
|
|
14
|
+
// Follow-ups on a result (result-follow-ups.ts): the same actions as the result page.
|
|
15
|
+
run_checks: { label: "Run checks", detail: "Runs the project's quick check on this result's exact commit. A pass upgrades its status; a failure stays visible." },
|
|
16
|
+
run_full_checks: { label: "Run full checks", detail: "Runs the project's full check on this result's exact commit. A pass upgrades its status; a failure stays visible." },
|
|
17
|
+
add_tests: { label: "Add tests", detail: "Files a small task to write tests for this change. Nothing runs until it is approved." },
|
|
13
18
|
};
|
|
14
19
|
export function isChatTaskAction(value) {
|
|
15
20
|
return typeof value === "string" && Object.hasOwn(CHAT_TASK_ACTIONS, value);
|
|
@@ -52,7 +57,7 @@ export function applyChatTaskAction(store, who, payload, now, web, controls = {}
|
|
|
52
57
|
// Resume remains the existing nonce + password ceremony. A proposal only opens it.
|
|
53
58
|
if (operation === "resume")
|
|
54
59
|
return { ok: true, taskId: task, said: `Resume review requested for ${task}. Complete the password confirmation on the task to resume.` };
|
|
55
|
-
const { via, ...rest } = controls;
|
|
60
|
+
const { via, evidenceRoot: _root, ...rest } = controls;
|
|
56
61
|
const result = requestTaskStop(store, { taskId: task, runId: run, by: verifiedAuthor(who.name), via: via ?? (web ? "web" : "cli"), ...rest }, now);
|
|
57
62
|
return result.ok ? { ok: true, taskId: task, said: `Stop requested for ${task}, run #${run}.` }
|
|
58
63
|
: { ok: false, message: result.detail };
|
|
@@ -61,6 +66,19 @@ export function applyChatTaskAction(store, who, payload, now, web, controls = {}
|
|
|
61
66
|
const result = store.requeueTask(task, who.name, now);
|
|
62
67
|
return result.ok ? { ok: true, taskId: task, said: "Task queued again." } : { ok: false, message: result.reason === "claimed" ? "This task is already running." : result.reason === "not-stalled" ? "This task does not need another attempt." : "That task is no longer available." };
|
|
63
68
|
}
|
|
69
|
+
if (operation === "run_checks" || operation === "run_full_checks" || operation === "add_tests") {
|
|
70
|
+
const result = store.runsFor(ref.id).find(one => one.role === "builder" && one.finishedAt !== null && one.headRevision !== null) ?? null;
|
|
71
|
+
if (result === null)
|
|
72
|
+
return { ok: false, message: "This task has no finished result yet." };
|
|
73
|
+
if (operation === "add_tests") {
|
|
74
|
+
const filed = fileAddTestsTask(store, controls.evidenceRoot ?? null, { runId: result.id, actor: who.name, filedVia: web ? "console" : "cli", admittedRepos: who.repos }, now);
|
|
75
|
+
return filed.ok ? { ok: true, taskId: filed.id, said: filed.existing ? `Tests are already filed as ${filed.id}.` : `Filed ${filed.id} to add tests. Review and approve it to start.` }
|
|
76
|
+
: { ok: false, message: filed.message };
|
|
77
|
+
}
|
|
78
|
+
const asked = requestFollowUpChecks(store, { runId: result.id, level: operation === "run_full_checks" ? "full" : "quick", actor: who.name }, now);
|
|
79
|
+
return asked.ok ? { ok: true, taskId: task, said: `${asked.level === "full" ? "Full" : "Quick"} checks ${asked.existing ? "are already" : "are"} queued on this result's commit.` }
|
|
80
|
+
: { ok: false, message: asked.message };
|
|
81
|
+
}
|
|
64
82
|
if (operation === "plan") {
|
|
65
83
|
const result = store.requestPlan(ref.id, now);
|
|
66
84
|
if (!result.ok)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Settings → Projects → Checks: how much checks after each build, in plain
|
|
2
|
+
* words. One choice (Quick, Full, Off) and the quick command beside the full
|
|
3
|
+
* one. Every change takes the password and lands in the ledger; approving a
|
|
4
|
+
* command restates exactly what will run, unattended, before the yes. */
|
|
5
|
+
import { type CheckLevel } from "./check-levels.js";
|
|
6
|
+
import type { VerifyCommand } from "./store.js";
|
|
7
|
+
export type CheckSettingsView = {
|
|
8
|
+
repo: string;
|
|
9
|
+
name: string;
|
|
10
|
+
csrf: string;
|
|
11
|
+
canChange: boolean;
|
|
12
|
+
level: CheckLevel;
|
|
13
|
+
full: VerifyCommand | null;
|
|
14
|
+
quick: VerifyCommand | null;
|
|
15
|
+
/** A starting point for the quick command, from the project's scripts. */
|
|
16
|
+
suggestion: string | null;
|
|
17
|
+
said: string | null;
|
|
18
|
+
problem: string | null;
|
|
19
|
+
};
|
|
20
|
+
export declare function checkSettingsHtml(view: CheckSettingsView): string;
|
|
21
|
+
export declare const CHECK_SETTINGS_CSS = "\n .check-settings { display: grid; gap: .6rem; max-width: 40rem; }\n .check-settings p, .check-settings h2 { margin: 0; }\n .check-settings h2 { font-size: 1rem; }\n .check-settings fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: .25rem; }\n .check-settings legend { font-weight: 600; margin-bottom: .25rem; }\n .check-settings label { display: grid; gap: .25rem; }\n .check-settings .check-choice { display: flex; align-items: flex-start; gap: .6rem; min-height: 44px; padding: .35rem 0; cursor: pointer; }\n .check-settings .check-choice > span { display: grid; gap: .1rem; }\n .check-settings .check-choice input { margin-top: .2rem; }\n .check-settings input[type=password], .check-settings input[type=number], .check-settings textarea { min-height: 44px; box-sizing: border-box; width: 100%; }\n .check-settings button { min-height: 44px; justify-self: start; }\n .check-settings .check-command { overflow-wrap: anywhere; }\n .check-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n @media (max-width: 480px) { .check-settings button { justify-self: stretch; } }\n .follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }\n .follow-ups .follow-up-act { margin: 0; }\n .follow-ups button { min-height: 44px; }\n @media (max-width: 480px) { .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: grid; } .follow-ups .follow-up-act button { width: 100%; } }\n";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Settings → Projects → Checks: how much checks after each build, in plain
|
|
2
|
+
* words. One choice (Quick, Full, Off) and the quick command beside the full
|
|
3
|
+
* one. Every change takes the password and lands in the ledger; approving a
|
|
4
|
+
* command restates exactly what will run, unattended, before the yes. */
|
|
5
|
+
import { CHECK_LEVELS, CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS } from "./check-levels.js";
|
|
6
|
+
const escape = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7
|
+
export function checkSettingsHtml(view) {
|
|
8
|
+
const hidden = (act) => `<input type="hidden" name="csrf" value="${escape(view.csrf)}"><input type="hidden" name="repo" value="${escape(view.repo)}"><input type="hidden" name="act" value="${act}">`;
|
|
9
|
+
const password = `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`;
|
|
10
|
+
const notes = [
|
|
11
|
+
view.said === null ? "" : `<p class="notice" role="status">${escape(view.said)}</p>`,
|
|
12
|
+
view.problem === null ? "" : `<p class="problem" role="alert">${escape(view.problem)}</p>`,
|
|
13
|
+
].join("");
|
|
14
|
+
const head = `<p class="meta">${escape(view.name)} · <span class="mono">${escape(view.repo)}</span></p>${notes}`;
|
|
15
|
+
const command = (one) => `<p class="mono check-command">${escape(one.command)}</p><p class="meta">Up to ${Math.round(one.timeoutMs / 1000)}s · approved by ${escape(one.approvedBy)}</p>`;
|
|
16
|
+
// Quick with no quick command runs the full check; say so where the choice is made.
|
|
17
|
+
const fallsBack = view.level === "quick" && view.quick === null;
|
|
18
|
+
const current = `<p><strong>${CHECK_LEVEL_WORDS[view.level]}</strong> · ${escape(CHECK_LEVEL_HINTS[view.level])}</p>` +
|
|
19
|
+
(fallsBack ? `<p class="meta">No quick command yet, so builds run the full check.</p>` : "") +
|
|
20
|
+
(view.level !== "full" && view.full !== null ? `<p class="meta">The full check still runs when a pull request opens, and Merge waits for it.</p>` : "");
|
|
21
|
+
if (!view.canChange) {
|
|
22
|
+
return head + `<section class="card check-settings">${current}</section>` +
|
|
23
|
+
`<section class="card check-settings"><h2>Quick check</h2>${view.quick === null ? `<p class="meta">None yet.</p>` : command(view.quick)}</section>` +
|
|
24
|
+
`<section class="card check-settings"><h2>Full check</h2>${view.full === null ? `<p class="meta">None yet.</p>` : command(view.full)}</section>`;
|
|
25
|
+
}
|
|
26
|
+
const choices = CHECK_LEVELS.map(level => `<label class="check-choice"><input type="radio" name="level" value="${level}"${level === view.level ? " checked" : ""}>` +
|
|
27
|
+
`<span><strong>${CHECK_LEVEL_WORDS[level]}</strong><span class="meta">${escape(CHECK_LEVEL_HINTS[level])}</span></span></label>`).join("");
|
|
28
|
+
const levelForm = `<form method="post" action="/settings/checks" class="card check-settings">${hidden("level")}` +
|
|
29
|
+
`<fieldset><legend>After each build</legend>${choices}</fieldset>` +
|
|
30
|
+
(fallsBack ? `<p class="meta">No quick command yet, so Quick runs the full check.</p>` : "") +
|
|
31
|
+
`<p class="meta">A task can choose its own checks when it's filed. With Quick or Off, the full check runs when a pull request opens, and Merge waits for it unless you merge anyway.</p>` +
|
|
32
|
+
`${password}<button type="submit">Save</button></form>`;
|
|
33
|
+
const quickTerms = `<ul class="check-terms"><li>Runs unattended right after every Quick build, in that build's checkout.</li><li>Plain environment: no credentials.</li><li>A failure shows on the result; nothing is blocked.</li></ul>`;
|
|
34
|
+
const quickForm = (value, submit) => `<form method="post" action="/settings/checks" class="check-settings">${hidden("quick")}` +
|
|
35
|
+
`<label>Command<textarea name="command" rows="2" maxlength="2000" spellcheck="false" class="mono" required>${escape(value)}</textarea></label>` +
|
|
36
|
+
`<label>Time limit (seconds)<input type="number" name="timeout" min="1" max="3600" value="${view.quick === null ? 180 : Math.round(view.quick.timeoutMs / 1000)}" required></label>` +
|
|
37
|
+
`${quickTerms}${password}<button type="submit">${submit}</button></form>`;
|
|
38
|
+
const quick = view.quick === null
|
|
39
|
+
? `<section class="card check-settings"><h2>Quick check</h2>` +
|
|
40
|
+
(view.suggestion === null ? `<p class="meta">Typecheck and the tests near the change, ideally under 2 minutes.</p>` : `<p class="meta">Suggested from the project's scripts. Edit it before approving.</p>`) +
|
|
41
|
+
quickForm(view.suggestion ?? "", "Approve quick check") + `</section>`
|
|
42
|
+
: `<section class="card check-settings"><h2>Quick check</h2>${command(view.quick)}` +
|
|
43
|
+
`<details class="settings-more"><summary>Change quick check</summary>${quickForm(view.quick.command, "Approve change")}</details>` +
|
|
44
|
+
`<details class="settings-more"><summary>Remove quick check</summary><form method="post" action="/settings/checks" class="check-settings">${hidden("quick-clear")}` +
|
|
45
|
+
`<p class="meta">Quick builds run the full check instead.</p>${password}<button type="submit" class="danger">Remove</button></form></details></section>`;
|
|
46
|
+
const full = `<details class="settings-more"><summary>Full check</summary><section class="card check-settings">` +
|
|
47
|
+
(view.full === null ? `<p class="meta">None yet. Approve one with <span class="mono">toolroll verify set --repo … --command "…"</span>.</p>` : command(view.full)) + `</section></details>`;
|
|
48
|
+
return head + levelForm + quick + full;
|
|
49
|
+
}
|
|
50
|
+
export const CHECK_SETTINGS_CSS = `
|
|
51
|
+
.check-settings { display: grid; gap: .6rem; max-width: 40rem; }
|
|
52
|
+
.check-settings p, .check-settings h2 { margin: 0; }
|
|
53
|
+
.check-settings h2 { font-size: 1rem; }
|
|
54
|
+
.check-settings fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: .25rem; }
|
|
55
|
+
.check-settings legend { font-weight: 600; margin-bottom: .25rem; }
|
|
56
|
+
.check-settings label { display: grid; gap: .25rem; }
|
|
57
|
+
.check-settings .check-choice { display: flex; align-items: flex-start; gap: .6rem; min-height: 44px; padding: .35rem 0; cursor: pointer; }
|
|
58
|
+
.check-settings .check-choice > span { display: grid; gap: .1rem; }
|
|
59
|
+
.check-settings .check-choice input { margin-top: .2rem; }
|
|
60
|
+
.check-settings input[type=password], .check-settings input[type=number], .check-settings textarea { min-height: 44px; box-sizing: border-box; width: 100%; }
|
|
61
|
+
.check-settings button { min-height: 44px; justify-self: start; }
|
|
62
|
+
.check-settings .check-command { overflow-wrap: anywhere; }
|
|
63
|
+
.check-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
64
|
+
@media (max-width: 480px) { .check-settings button { justify-self: stretch; } }
|
|
65
|
+
.follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
66
|
+
.follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }
|
|
67
|
+
.follow-ups .follow-up-act { margin: 0; }
|
|
68
|
+
.follow-ups button { min-height: 44px; }
|
|
69
|
+
@media (max-width: 480px) { .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: grid; } .follow-ups .follow-up-act button { width: 100%; } }
|
|
70
|
+
`;
|