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
@@ -388,9 +388,11 @@ export class TerminalHost {
388
388
  * End the program and everything that is its own: SIGHUP to every member (an interactive shell gives each job its own
389
389
  * process group, so the leader's group is not enough), then SIGKILL, re-reading the process table each time so a
390
390
  * child started during the close is caught too. Membership is the leader's descendants, every process with this
391
- * terminal as its controlling terminal (started before the program ended), and every one noted while it ran, by pid
392
- * and start time. A process that detached itself from the terminal AND its parents before it was noted is not seen —
393
- * the same as any terminal. Resolves when the sweep is done; never waits without bound.
391
+ * terminal as its controlling terminal while the program runs, and every one noted while it ran, by pid and start
392
+ * time (membersOf). Each signal goes only to a process still that pid and start time; a process on the terminal after
393
+ * the program ended that was never noted, or a pid now someone else's, is never signalled and leaves the close
394
+ * uncertain. A process that detached itself from the terminal AND its parents before it was noted is not seen — the
395
+ * same as any terminal. Resolves when the sweep is done; never waits without bound.
394
396
  */
395
397
  close(id, by = "owner") {
396
398
  const session = this.sessions.get(id);
@@ -408,19 +410,30 @@ export class TerminalHost {
408
410
  const pause = sleep;
409
411
  const leaderGone = () => session.pty === null;
410
412
  let readable = true;
413
+ // Processes on its terminal after it ended that were never noted: maybe someone else's, never signalled.
414
+ const ambiguous = new Set();
411
415
  const members = () => {
412
416
  const table = this.processes();
413
417
  if (table === null) {
414
418
  readable = false;
415
419
  return new Map();
416
420
  }
417
- const found = membersOf(table, session);
421
+ const found = membersOf(table, session, ambiguous);
418
422
  for (const [pid, started] of found)
419
423
  session.known.set(pid, started);
420
424
  return found;
421
425
  };
426
+ // Each signal goes only to the process it was meant for: the table is read again right before, and a pid whose
427
+ // start time is no longer the one recorded (gone, or a pid used again) is skipped, and the close is uncertain. The
428
+ // leader's group only while it runs: its pid is our unreaped child until the pty reports its exit, and, where the
429
+ // table reads, still the leader that started. A table that can't be read signals the leader alone (the close is
430
+ // uncertain then anyway).
422
431
  const signal = (found, name) => {
423
- if (!leaderGone()) {
432
+ const table = this.processes();
433
+ if (table === null)
434
+ readable = false;
435
+ const leaderStarted = session.known.get(session.leader);
436
+ if (!leaderGone() && (table === null || (leaderStarted !== undefined && table.get(session.leader)?.started === leaderStarted))) {
424
437
  try {
425
438
  process.kill(-session.leader, name);
426
439
  }
@@ -430,7 +443,16 @@ export class TerminalHost {
430
443
  }
431
444
  catch { /* gone */ }
432
445
  }
433
- for (const pid of found.keys()) {
446
+ if (table === null)
447
+ return;
448
+ for (const [pid, started] of found) {
449
+ const now = table.get(pid);
450
+ if (now === undefined)
451
+ continue;
452
+ if (now.started !== started) {
453
+ ambiguous.add(pid);
454
+ continue;
455
+ }
434
456
  try {
435
457
  process.kill(pid, name);
436
458
  }
@@ -457,13 +479,26 @@ export class TerminalHost {
457
479
  }
458
480
  }
459
481
  for (let round = 0; round < 20; round += 1) {
460
- const found = members();
482
+ let found = members();
461
483
  if (!readable)
462
484
  return;
463
485
  if (found.size === 0 && leaderGone()) {
464
486
  cleanup = "clean";
465
487
  return;
466
488
  }
489
+ // Stopped first, then read again until nothing new appears: a stopped process can't fork, so a child one started
490
+ // just before it stopped is still its descendant here and is killed with it. Killed while running, a parent can
491
+ // fork between the read and the kill, and its child goes to launchd unnoted.
492
+ for (let settle = 0; settle < 5; settle += 1) {
493
+ signal(found, "SIGSTOP");
494
+ const again = members();
495
+ if (!readable)
496
+ return;
497
+ const grew = [...again.keys()].some(pid => !found.has(pid));
498
+ found = again;
499
+ if (!grew)
500
+ break;
501
+ }
467
502
  signal(found, "SIGKILL");
468
503
  await pause(100);
469
504
  }
@@ -486,7 +521,7 @@ export class TerminalHost {
486
521
  }
487
522
  }
488
523
  }
489
- session.cleanup = readable && leaderGone() ? cleanup : "uncertain";
524
+ session.cleanup = readable && leaderGone() && ambiguous.size === 0 ? cleanup : "uncertain";
490
525
  if (session.endedAt === null)
491
526
  session.endedAt = this.clock().toISOString();
492
527
  this.ledger.release(session.id, session.cleanup);
@@ -575,19 +610,21 @@ export function readProcessTable() {
575
610
  }
576
611
  /**
577
612
  * A terminal's processes in this table, by pid and start time: what was noted while it ran (still the same process),
578
- * the leader, everything whose controlling terminal is its terminal and that started before it ended (a later process
579
- * on a reused device is someone else's), and every descendant of those. Never this console itself.
613
+ * the leader while it runs, everything whose controlling terminal is its terminal while it runs, and every descendant
614
+ * of those. Never this console itself. Once the program has ended its terminal's name is no one's proof: the device
615
+ * may already be someone else's, so a process on it that wasn't noted is not a member; it goes in `ambiguous` (when
616
+ * given), and the close that meets one says it is uncertain rather than signal it.
580
617
  */
581
- export function membersOf(table, session) {
618
+ export function membersOf(table, session, ambiguous) {
582
619
  const found = new Map();
583
- const startedMs = (started) => Date.parse(started);
620
+ const live = session.exitedAt === null;
621
+ const onTerminal = (tty) => session.tty !== null && tty === session.tty;
584
622
  for (const [pid, row] of table) {
585
623
  if (pid === process.pid || pid <= 1)
586
624
  continue;
587
625
  const noted = session.known.get(pid);
588
- const ownTty = session.tty !== null && row.tty === session.tty && (session.exitedAt === null || startedMs(row.started) <= session.exitedAt + 1_000);
589
626
  // The leader by pid only while it runs: after it exits its pid may be someone else's (it is noted by start time).
590
- if (noted === row.started || (pid === session.leader && session.exitedAt === null) || ownTty)
627
+ if (noted === row.started || (pid === session.leader && live) || (live && onTerminal(row.tty)))
591
628
  found.set(pid, row.started);
592
629
  }
593
630
  for (let grew = true; grew;) {
@@ -598,6 +635,10 @@ export function membersOf(table, session) {
598
635
  grew = true;
599
636
  }
600
637
  }
638
+ if (ambiguous !== undefined && !live)
639
+ for (const [pid, row] of table)
640
+ if (!found.has(pid) && pid !== process.pid && pid > 1 && onTerminal(row.tty))
641
+ ambiguous.add(pid);
601
642
  return found;
602
643
  }
603
644
  /**
@@ -117,6 +117,14 @@ export type ConnectVisit = {
117
117
  scope?: string | null;
118
118
  /** Where the service revokes a token, when its metadata said: a run's own token is revoked there when the run ends. */
119
119
  revoke?: string | null;
120
+ /** The signer's account generation when the password was typed at Connect: a rotation or revocation since ends the visit. */
121
+ generation?: number;
122
+ /** From a kit: the one grant the password at Connect proved (its subagent, this tool, this project); nothing else. */
123
+ grant?: {
124
+ subagent: number;
125
+ tool: string;
126
+ repo: string;
127
+ } | null;
120
128
  };
121
129
  /** A server's sign-in: the resource metadata its 401 names (or the well-known one), then its authorization server's metadata. */
122
130
  export declare function discoverSignIn(mcpUrl: string, fetcher?: Fetch): Promise<Server | null>;
@@ -154,6 +162,8 @@ export declare function finishConnect(store: Store, visit: ConnectVisit, code: s
154
162
  home?: string | undefined;
155
163
  test?: boolean;
156
164
  omitEnv?: readonly string[];
165
+ /** The signer's standing, checked again after the token answer and before anything changes. */
166
+ stillStanding?: () => boolean;
157
167
  }): Promise<{
158
168
  ok: true;
159
169
  said: string;
@@ -351,6 +351,8 @@ export async function finishConnect(store, visit, code, now, options = {}) {
351
351
  const tokens = tokensFrom(body, now.getTime());
352
352
  if (tokens.access === null)
353
353
  return { ok: false, said: `${service.label} didn't send a sign-in token.` };
354
+ if (options.stillStanding !== undefined && !options.stillStanding())
355
+ return { ok: false, said: "Your access changed during the sign-in, so nothing was connected. Connect again." };
354
356
  if (had === undefined) {
355
357
  const added = addToolTo(store, visit.repo, spec, `${service.label}, connected by signing in`, visit.by, now, { home });
356
358
  if (!added.ok)
@@ -0,0 +1,104 @@
1
+ import { fileIdentity, groupIdentity, journeyIdentity, unitIdentity, versionIdentity, type CheckReport } from "./contracts/release-check.js";
2
+ import type { StandingOrder } from "./contracts/flow.js";
3
+ import { type Store } from "./store.js";
4
+ import type { Runner } from "./worktree.js";
5
+ import type { StandingFiring } from "./flow-schedule.js";
6
+ /** How long a nightly may be missing before its flow says so. */
7
+ export declare const NIGHTLY_MISSED_MS: number;
8
+ export declare const NIGHTLY_STEM = "nightly";
9
+ export type MainCheck = NonNullable<StandingOrder["check"]>;
10
+ /** What every nightly firing files: fixed here, never read from the trigger's settings. */
11
+ export declare const NIGHTLY_GOAL = "Run the full release check on main's head.";
12
+ export declare const NIGHTLY_NOT = "Changing anything: this is a check only.";
13
+ export declare const NIGHTLY_ACCEPTANCE: readonly {
14
+ id: string;
15
+ statement: string;
16
+ how: null;
17
+ evidence: "check"[];
18
+ }[];
19
+ /** The ledger act that records a person's approval of a nightly check, keyed by its trigger. */
20
+ export declare const NIGHTLY_APPROVED = "nightly check approved";
21
+ /** The digest a person's approval of the nightly signs: the trigger, the project, the schedule, the check-only terms
22
+ * and the fixed words each firing files. */
23
+ export declare function nightlyDigestOf(trigger: number, repo: string, schedule: string, check: MainCheck): string;
24
+ /**
25
+ * The approval of a nightly check as the plane recorded it when a person proved themselves (createNightlyCheckFlow):
26
+ * the newest ledger row for this trigger. The trigger's own settings never stand in for it — anyone who can edit them
27
+ * could write one.
28
+ */
29
+ export declare function nightlyApprovalOf(store: Store, trigger: number): {
30
+ digest: string;
31
+ by: string;
32
+ proof: string;
33
+ at: string;
34
+ } | null;
35
+ /** How often the watch loop reads each recorded remote's main again, and how old a read may be when a nightly fires
36
+ * or debt is cleared against it (review round 2: one window, two refreshes wide, for both decisions). */
37
+ export declare const TRUSTED_MAIN_REFRESH_MS: number;
38
+ export declare const TRUSTED_MAIN_FIRE_MS: number;
39
+ export declare const TRUSTED_MAIN_CLEAR_MS: number;
40
+ /**
41
+ * Main's head as Toolroll's own process last read it from the project's RECORDED remote (remote-main.ts: the recorded
42
+ * URL, a private plane-owned Git directory, objects that arrive only by fetch) — never a ref in the project's Git
43
+ * directory, which a build shares and can move (review round 1). Null when there is no read young enough, when the
44
+ * last read failed, or when the read was of another remote than the one recorded now (review round 2).
45
+ */
46
+ export declare function trustedMainOf(store: Store, repo: string, now: Date, maxAgeMs: number): string | null;
47
+ /** The watch loop's read of each project's recorded remote main that the nightly check or main's debt needs, at most
48
+ * every 30 minutes. Each read is a fresh fetch into a private directory. A failed read forgets the last one. A good
49
+ * one hands its verified objects to the project's Git store (hardened git, content-addressed: git checks every object
50
+ * against its hash, and filing verifies the commit again), so the nightly can check a merge the checkout never pulled. */
51
+ export declare function refreshTrustedMains(store: Store, root: string, now: Date, runner?: Runner): Promise<number>;
52
+ /**
53
+ * Set up the nightly full check for a project, approved by a person proved now (their password or a re-check; never a
54
+ * lead or remote token). Its schedule starts paused unless `start`; turning it on is the flow's own switch.
55
+ */
56
+ export declare function createNightlyCheckFlow(store: Store, input: {
57
+ repo: string;
58
+ schedule: string;
59
+ as: string;
60
+ token: string;
61
+ start: boolean;
62
+ }, now: Date): {
63
+ ok: true;
64
+ flow: number;
65
+ trigger: number;
66
+ } | {
67
+ ok: false;
68
+ message: string;
69
+ };
70
+ /** A nightly firing (fileStandingOrder routes here): the approval checked again, main's head read, one check-only task
71
+ * filed and sealed by its standing order. */
72
+ export declare function fileNightlyCheck(store: Store, input: {
73
+ flowId: number;
74
+ repo: string;
75
+ trigger: number;
76
+ schedule: string;
77
+ order: StandingOrder;
78
+ title: string;
79
+ slot: string;
80
+ }, now: Date): StandingFiring;
81
+ /** Whether a task is a nightly check of main (check only: it never deploys, merges or completes). */
82
+ export declare const isNightlyTask: (store: Store, taskId: string) => boolean;
83
+ /** Every item a failed check failed: each failed identity, each failed group or version, and the whole part for a
84
+ * part that couldn't say which items (it stopped, never reported, or crashed). No report: every part. */
85
+ export declare function failedItems(report: CheckReport | null): string[];
86
+ /** The words of one failure notice: "2 unit files, app/pages". */
87
+ export declare function failureSummary(items: readonly string[]): string;
88
+ /**
89
+ * Whether a full run's report shows `item` itself ran and PASSED there (security review): a failed identity has to be
90
+ * reported passed by name — missing, skipped, or a passing part around it never clears it. A part-level item clears
91
+ * when that whole part ran and passed.
92
+ */
93
+ export declare function coversItem(report: CheckReport, item: string): boolean;
94
+ /**
95
+ * The plane's sweep (watch's follow-up checks): settle each finished nightly (failure rows and one attention notice),
96
+ * then let each finished full release check clear the main failures it covers. Returns how many it settled.
97
+ */
98
+ export declare function settleReleaseChecks(store: Store, root: string, now: Date): number;
99
+ /** The nightly's flow health: "the nightly check hasn't run since …" after 26 hours without a firing. Null when it ran
100
+ * recently, or the project has no nightly check. */
101
+ export declare function nightlyHealthWords(store: Store, repo: string, configuredAt: string, now: Date): string | null;
102
+ export { fileIdentity, groupIdentity, journeyIdentity, unitIdentity, versionIdentity };
103
+ /** The items the nightly check of main found failing that are still open: a full check reports each by name. */
104
+ export declare function openMainFailureItems(store: Store, repo: string): string[];
@@ -0,0 +1,362 @@
1
+ /**
2
+ * The nightly full check on main (docs/plans/release-once.md, part 3): a safety net for accidental omissions, not a
3
+ * deadline.
4
+ *
5
+ * - A scheduled flow (flow-schedule.ts) whose standing order names `check: main-full`. A person approves it once, with
6
+ * their password: the approval binds the release check grant (id and digest), the fenced check access and the
7
+ * check-only capability, for future main commits. Each firing files `nightly-<date>-<sha7>` at main's head — never a
8
+ * release branch — sealed with basis `schedule`; that task can't deploy, merge or complete.
9
+ * - A failing nightly writes one `main_check_failure` row per failed item (each failed identity, each group or version,
10
+ * or the whole part for a part-level outcome) and sends one attention notification.
11
+ * - A row clears only when a qualifying full pass covers that exact item: intended mode full and coverage full, on a
12
+ * commit that contains the failed main commit (verified ancestry), the item or its whole part ran and passed there.
13
+ * A composed result never clears anything; a pass that covers one item never clears another.
14
+ * - Open rows refuse targeted planning and block the deploy and merge of a composed release (scope.ts liveReleaseOf).
15
+ * - A missed, stalled or disabled nightly gives no signal but its flow's own health notice after 26 hours.
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import { projectGitDir } from "./check-report.js";
19
+ import { readVerifiedCommit, verifiedFirstParentWalk } from "./commit-extract.js";
20
+ import { fileIdentity, groupIdentity, journeyIdentity, partIdentity, unitIdentity, versionIdentity } from "./contracts/release-check.js";
21
+ import { propose, authenticateApprover, liveReleaseOf, personProof } from "./scope.js";
22
+ import { failedIdentities, sealedRun } from "./release-recheck.js";
23
+ import { verificationEvidence } from "./verification-evidence.js";
24
+ import { BUILT_IN, TRUSTED_MAIN_ACTION, forgetTrustedMain, recordTrustedMain, trustedRemoteIdentity } from "./store.js";
25
+ import { teardownGit } from "./teardown-git.js";
26
+ /** Where the project's Git store keeps the objects of the remote main the plane read (they matter, the ref doesn't). */
27
+ const NIGHTLY_MAIN_REF = "refs/toolroll/remote-main";
28
+ import { RemoteMains } from "./remote-main.js";
29
+ import { run } from "./exec.js";
30
+ /** How long a nightly may be missing before its flow says so. */
31
+ export const NIGHTLY_MISSED_MS = 26 * 60 * 60_000;
32
+ export const NIGHTLY_STEM = "nightly";
33
+ /** What every nightly firing files: fixed here, never read from the trigger's settings. */
34
+ export const NIGHTLY_GOAL = "Run the full release check on main's head.";
35
+ export const NIGHTLY_NOT = "Changing anything: this is a check only.";
36
+ export const NIGHTLY_ACCEPTANCE = Object.freeze([{ id: "c1", statement: "The full release check passes on main", how: null, evidence: ["check"] }]);
37
+ /** The ledger act that records a person's approval of a nightly check, keyed by its trigger. */
38
+ export const NIGHTLY_APPROVED = "nightly check approved";
39
+ /** The digest a person's approval of the nightly signs: the trigger, the project, the schedule, the check-only terms
40
+ * and the fixed words each firing files. */
41
+ export function nightlyDigestOf(trigger, repo, schedule, check) {
42
+ return createHash("sha256").update(`nightly-check:${JSON.stringify({ trigger, repo, schedule, check, goal: NIGHTLY_GOAL, not: NIGHTLY_NOT, acceptance: NIGHTLY_ACCEPTANCE })}`).digest("hex").slice(0, 32);
43
+ }
44
+ /**
45
+ * The approval of a nightly check as the plane recorded it when a person proved themselves (createNightlyCheckFlow):
46
+ * the newest ledger row for this trigger. The trigger's own settings never stand in for it — anyone who can edit them
47
+ * could write one.
48
+ */
49
+ export function nightlyApprovalOf(store, trigger) {
50
+ for (const row of store.handle.prepare("SELECT outcome, detail FROM action_ledger WHERE action = ? ORDER BY id DESC LIMIT 20").all(NIGHTLY_APPROVED)) {
51
+ try {
52
+ const read = JSON.parse(String(row["detail"]));
53
+ if (read["v"] !== 1 || read["trigger"] !== trigger)
54
+ continue;
55
+ if (typeof read["digest"] !== "string" || typeof read["by"] !== "string" || typeof read["proof"] !== "string" || typeof read["at"] !== "string" || !personProof(read["proof"]))
56
+ return null;
57
+ return { digest: read["digest"], by: read["by"], proof: read["proof"], at: read["at"] };
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ }
63
+ return null;
64
+ }
65
+ /** How often the watch loop reads each recorded remote's main again, and how old a read may be when a nightly fires
66
+ * or debt is cleared against it (review round 2: one window, two refreshes wide, for both decisions). */
67
+ export const TRUSTED_MAIN_REFRESH_MS = 30 * 60_000;
68
+ export const TRUSTED_MAIN_FIRE_MS = 60 * 60_000;
69
+ export const TRUSTED_MAIN_CLEAR_MS = TRUSTED_MAIN_FIRE_MS;
70
+ /**
71
+ * Main's head as Toolroll's own process last read it from the project's RECORDED remote (remote-main.ts: the recorded
72
+ * URL, a private plane-owned Git directory, objects that arrive only by fetch) — never a ref in the project's Git
73
+ * directory, which a build shares and can move (review round 1). Null when there is no read young enough, when the
74
+ * last read failed, or when the read was of another remote than the one recorded now (review round 2).
75
+ */
76
+ export function trustedMainOf(store, repo, now, maxAgeMs) {
77
+ const row = store.handle.prepare("SELECT outcome, at, detail FROM action_ledger WHERE repo = ? AND action = ? ORDER BY id DESC LIMIT 1").get(repo, TRUSTED_MAIN_ACTION);
78
+ if (row === undefined || now.getTime() - Date.parse(String(row["at"])) > maxAgeMs)
79
+ return null;
80
+ const sha = String(row["outcome"]);
81
+ if (!/^[0-9a-f]{40}$/.test(sha))
82
+ return null;
83
+ const identity = trustedRemoteIdentity(store, repo);
84
+ try {
85
+ const read = JSON.parse(String(row["detail"]));
86
+ if (read.v !== 2 || read.sha !== sha || identity === null || read.url !== identity.url || read.record !== identity.record)
87
+ return null;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ return sha;
93
+ }
94
+ /** The watch loop's read of each project's recorded remote main that the nightly check or main's debt needs, at most
95
+ * every 30 minutes. Each read is a fresh fetch into a private directory. A failed read forgets the last one. A good
96
+ * one hands its verified objects to the project's Git store (hardened git, content-addressed: git checks every object
97
+ * against its hash, and filing verifies the commit again), so the nightly can check a merge the checkout never pulled. */
98
+ export async function refreshTrustedMains(store, root, now, runner = run) {
99
+ const repos = new Set([
100
+ ...store.handle.prepare("SELECT DISTINCT repo FROM main_check_failure WHERE cleared_at IS NULL").all().map(row => String(row["repo"])),
101
+ ...store.handle.prepare("SELECT DISTINCT repo FROM action_ledger WHERE action = ?").all(NIGHTLY_APPROVED).map(row => String(row["repo"])),
102
+ ]);
103
+ let read = 0;
104
+ const remotes = new RemoteMains(store, runner, root);
105
+ try {
106
+ for (const repo of repos) {
107
+ if (trustedMainOf(store, repo, now, TRUSTED_MAIN_REFRESH_MS) !== null)
108
+ continue;
109
+ const got = await remotes.read(repo);
110
+ if (!got.ok) {
111
+ forgetTrustedMain(store, repo, got.why, now);
112
+ continue;
113
+ }
114
+ await teardownGit(runner)(repo, ["-c", "protocol.file.allow=always", "fetch", "--quiet", "--no-tags", "--no-write-fetch-head", "--", got.remote.gitDir, `+refs/toolroll/remote-main:${NIGHTLY_MAIN_REF}`]);
115
+ if (recordTrustedMain(store, repo, got.remote.main, got.remote.url, now))
116
+ read++;
117
+ }
118
+ }
119
+ finally {
120
+ remotes.close();
121
+ }
122
+ return read;
123
+ }
124
+ /**
125
+ * Set up the nightly full check for a project, approved by a person proved now (their password or a re-check; never a
126
+ * lead or remote token). Its schedule starts paused unless `start`; turning it on is the flow's own switch.
127
+ */
128
+ export function createNightlyCheckFlow(store, input, now) {
129
+ const proved = authenticateApprover(store, input.as, input.token, input.repo);
130
+ if (!proved.ok || !personProof(proved.method))
131
+ return { ok: false, message: "Only an approver of this project, proved as a person, sets up the nightly check." };
132
+ const grant = store.liveVerifyCommand(input.repo);
133
+ if (grant === null)
134
+ return { ok: false, message: "The project has no approved release check to run." };
135
+ if (store.projectCheckAccess(input.repo) !== "sandboxed")
136
+ return { ok: false, message: "The project's checks run outside the sandbox; the nightly check runs only inside it." };
137
+ const check = { kind: "main-full", grant: { id: grant.id, digest: grant.digest }, access: "sandboxed" };
138
+ // The order carries no approval: the person's yes is the plane's own ledger row, written below.
139
+ const order = {
140
+ stem: NIGHTLY_STEM, goal: NIGHTLY_GOAL, outOfScope: NIGHTLY_NOT, touches: [], requirements: [], acceptance: [...NIGHTLY_ACCEPTANCE], budgetPerRunMicrousd: null, costCeilingUsd: null,
141
+ singleFlight: true, filedBy: input.as, routine: null, approval: null, check,
142
+ };
143
+ const definition = { version: 1, start: "build", stages: [
144
+ { id: "build", title: "Check main", kind: "task", zone: { x: 0, y: 0, w: 260, h: 300, color: "blue" }, instructions: order.goal, planning: "skip", approver: null, message: null, close: null, script: null, sort: null, next: "done", onFail: null },
145
+ { id: "done", title: "Done", kind: "done", zone: { x: 420, y: 0, w: 260, h: 300, color: "green" }, instructions: null, planning: null, approver: null, message: null, close: null, script: null, sort: null, next: null, onFail: null },
146
+ ] };
147
+ return store.transact(() => {
148
+ const flow = store.createFlow({ repo: input.repo, name: "Nightly check of main", definitionJson: JSON.stringify(definition), by: input.as }, now);
149
+ const trigger = store.addFlowTrigger({ flow, kind: "schedule", configJson: JSON.stringify({ kind: "schedule", schedule: input.schedule, title: "Nightly check of main", description: null, zone: "build", order }), hookHash: null, cursor: null, nextAt: null, by: input.as }, now);
150
+ if (!input.start)
151
+ store.updateFlowTrigger(trigger, { state: "paused" }, now);
152
+ store.recordAction({ at: now.toISOString(), actor: input.as, repo: input.repo, taskId: null, runId: null, action: NIGHTLY_APPROVED, outcome: input.start ? "on" : "paused", source: "request",
153
+ detail: JSON.stringify({ v: 1, trigger, flow, digest: nightlyDigestOf(trigger, input.repo, input.schedule, check), by: input.as, proof: proved.method, at: now.toISOString(), schedule: input.schedule, grant: grant.id }) });
154
+ return { ok: true, flow, trigger };
155
+ });
156
+ }
157
+ /** A nightly firing (fileStandingOrder routes here): the approval checked again, main's head read, one check-only task
158
+ * filed and sealed by its standing order. */
159
+ export function fileNightlyCheck(store, input, now) {
160
+ const { order, repo } = input;
161
+ const check = order.check;
162
+ // The approval is the plane's own record of the person's yes for this trigger; anything in the settings is ignored.
163
+ const approval = nightlyApprovalOf(store, input.trigger);
164
+ if (approval === null || approval.digest !== nightlyDigestOf(input.trigger, repo, input.schedule, check))
165
+ return { made: "skipped", note: "Skipped: the nightly check isn't approved as it stands. Set it up again." };
166
+ const account = store.accountOf(approval.by);
167
+ if (account === null || account.revokedAt !== null || account.role !== "approver" || !store.accountCanAccess(approval.by, repo))
168
+ return { made: "skipped", note: `Skipped: ${approval.by} can no longer approve in this project.` };
169
+ const grant = store.liveVerifyCommand(repo);
170
+ if (grant === null || grant.id !== check.grant.id || grant.digest !== check.grant.digest)
171
+ return { made: "skipped", note: "Skipped: the release check changed since the nightly check was approved." };
172
+ if (store.projectCheckAccess(repo) !== "sandboxed")
173
+ return { made: "skipped", note: "Skipped: the project's checks run outside the sandbox." };
174
+ const blocker = store.standingOrderBlocker(input.trigger, order.routine, now);
175
+ if (blocker !== null)
176
+ return { made: "skipped", note: `Skipped: the last one (${blocker.taskId}, ${blocker.state}) hasn't finished.` };
177
+ // Main as the plane read it from the recorded remote, recently; never a ref in the shared Git directory.
178
+ const trusted = trustedMainOf(store, repo, now, TRUSTED_MAIN_FIRE_MS);
179
+ if (trusted === null)
180
+ return { made: "skipped", note: "Skipped: main hasn't been read from the project's recorded remote lately." };
181
+ const gitDir = projectGitDir(repo);
182
+ if (gitDir === null || !readVerifiedCommit(gitDir, trusted).ok)
183
+ return { made: "skipped", note: `Skipped: main's ${trusted.slice(0, 7)} isn't in the project's history yet.` };
184
+ const head = { sha: trusted };
185
+ const taskId = `${NIGHTLY_STEM}-${now.toISOString().slice(0, 10).replace(/-/g, "")}-${head.sha.slice(0, 7)}`;
186
+ if (store.getTask(taskId) !== null)
187
+ return { made: "skipped", note: `Skipped: ${head.sha.slice(0, 7)} was already checked today (${taskId}).` };
188
+ store.createTask({ id: taskId, title: `Nightly check of main at ${head.sha.slice(0, 7)}`, filedBy: { name: null, kind: "automation" } }, now);
189
+ const ref = store.refFor(BUILT_IN, taskId, "ours");
190
+ store.placeTask(ref.id, repo, {}, now);
191
+ store.recordAction({ at: now.toISOString(), actor: "toolroll", repo, taskId, runId: null, action: "task checks chosen", outcome: "full", source: "request", detail: "Checks for this task: Full" });
192
+ // Fixed words, never the trigger's: what the person approved is what files.
193
+ propose(store, { taskId, goal: NIGHTLY_GOAL, outOfScope: NIGHTLY_NOT, candidate: head.sha, acceptance: [...NIGHTLY_ACCEPTANCE], now });
194
+ const sealed = store.sealScopeApproval(taskId, approval.by, now, {}, { kind: "schedule", modeDigest: approval.digest }, "automation");
195
+ store.handle.prepare("INSERT INTO nightly_check (repo, sha, task, at) VALUES (?, ?, ?, ?)").run(repo, head.sha, taskId, now.toISOString());
196
+ return sealed ? { made: "filed", taskId, approved: true, note: `Checking main at ${head.sha.slice(0, 7)}.` }
197
+ : { made: "filed", taskId, approved: false, note: "Its approval can't stand on its own here, so this one waits for approval." };
198
+ }
199
+ /** Whether a task is a nightly check of main (check only: it never deploys, merges or completes). */
200
+ export const isNightlyTask = (store, taskId) => store.handle.prepare("SELECT 1 AS hit FROM nightly_check WHERE task = ?").get(taskId) !== undefined;
201
+ /** Every item a failed check failed: each failed identity, each failed group or version, and the whole part for a
202
+ * part that couldn't say which items (it stopped, never reported, or crashed). No report: every part. */
203
+ export function failedItems(report) {
204
+ if (report === null)
205
+ return [partIdentity("all")];
206
+ const items = new Set(failedIdentities(report));
207
+ for (const part of report.parts) {
208
+ if (!part.selected)
209
+ continue;
210
+ const attributed = part.part === "unit" ? report.units !== null && report.units.finished && report.units.runErrors === 0
211
+ : part.part === "upgrade" ? report.upgrades !== null && report.upgrades.finished
212
+ : part.part === "typecheck" || part.part === "build" ? false
213
+ : report.journeys.some(one => (one.kind === "real" ? `${one.script}-real` : one.script) === part.part && one.finished && one.groups.every(group => group.state !== "not-run"));
214
+ if ((!part.ran || part.code !== 0) && (!attributed || part.part === "typecheck" || part.part === "build"))
215
+ items.add(partIdentity(part.part));
216
+ }
217
+ return [...items].sort();
218
+ }
219
+ /** The words of one failure notice: "2 unit files, app/pages". */
220
+ export function failureSummary(items) {
221
+ const files = new Set(items.flatMap(one => /^unit:(.+?)::/.exec(one)?.[1] ?? /^file:(.+)$/.exec(one)?.[1] ?? []));
222
+ const groups = new Set(items.flatMap(one => { const m = /^(?:group|journey):(app|flows):(?:scripted|real):([^:]+?)(?:::|$)/.exec(one); return m === null ? [] : [`${m[1]}/${m[2]}`]; }));
223
+ const versions = items.flatMap(one => /^upgrade:(.+)$/.exec(one)?.[1] ?? []);
224
+ const parts = items.flatMap(one => /^part:(.+)$/.exec(one)?.[1] ?? []);
225
+ return [
226
+ ...(files.size === 0 ? [] : [`${files.size} unit file${files.size === 1 ? "" : "s"}`]), ...groups,
227
+ ...(versions.length === 0 ? [] : [`upgrade ${versions.join(", ")}`]), ...parts.map(one => one === "all" ? "the whole check" : `the ${one} part`),
228
+ ].join(", ") || "the check";
229
+ }
230
+ /**
231
+ * Whether a full run's report shows `item` itself ran and PASSED there (security review): a failed identity has to be
232
+ * reported passed by name — missing, skipped, or a passing part around it never clears it. A part-level item clears
233
+ * when that whole part ran and passed.
234
+ */
235
+ export function coversItem(report, item) {
236
+ const part = (name) => report.parts.find(one => one.part === name);
237
+ const passedPart = (name) => { const one = part(name); return one !== undefined && one.ran && one.code === 0; };
238
+ const unit = /^unit:(.+?)::(.+)$/s.exec(item);
239
+ if (unit !== null)
240
+ return report.units?.identities.some(one => one.file === unit[1] && one.name === unit[2] && one.state === "passed") === true;
241
+ const file = /^file:(.+)$/.exec(item)?.[1];
242
+ if (file !== undefined)
243
+ return report.units?.files.some(one => one.file === file && one.state === "passed" && one.failed === 0) === true;
244
+ const journey = /^journey:(app|flows):(scripted|real):([^:]+?)::(.+)$/s.exec(item);
245
+ if (journey !== null) {
246
+ const found = report.journeys.find(one => one.script === journey[1] && one.kind === journey[2])?.groups.find(one => one.group === journey[3]);
247
+ return found?.journeys.some(one => one.name === journey[4] && one.state === "passed") === true;
248
+ }
249
+ const group = /^group:(app|flows):(scripted|real):(.+)$/.exec(item);
250
+ if (group !== null) {
251
+ const found = report.journeys.find(one => one.script === group[1] && one.kind === group[2])?.groups.find(one => one.group === group[3]);
252
+ return found !== undefined && found.state === "passed" && !found.unattributed;
253
+ }
254
+ const version = /^upgrade:(.+)$/.exec(item)?.[1];
255
+ if (version !== undefined)
256
+ return report.upgrades?.versions.some(one => one.version === version && one.state === "passed") === true;
257
+ const whole = /^part:(.+)$/.exec(item)?.[1];
258
+ if (whole === "all")
259
+ return report.parts.filter(one => one.selected).every(one => one.ran && one.code === 0) && report.exitCode === 0;
260
+ return whole !== undefined && passedPart(whole);
261
+ }
262
+ /**
263
+ * The plane's sweep (watch's follow-up checks): settle each finished nightly (failure rows and one attention notice),
264
+ * then let each finished full release check clear the main failures it covers. Returns how many it settled.
265
+ */
266
+ export function settleReleaseChecks(store, root, now) {
267
+ let settled = 0;
268
+ for (const row of store.handle.prepare("SELECT * FROM nightly_check WHERE outcome IS NULL AND task IS NOT NULL ORDER BY id").all()) {
269
+ const ref = store.lookupRef(String(row["task"]));
270
+ if (ref === null)
271
+ continue;
272
+ const runs = store.runsFor(ref.id).filter(one => one.role === "builder" && one.finishedAt !== null).sort((a, b) => b.id - a.id);
273
+ const run = runs[0];
274
+ if (run === undefined)
275
+ continue;
276
+ const check = store.handle.prepare("SELECT status FROM run_check WHERE run = ?").get(run.id);
277
+ const status = check === undefined ? (run.outcome === "built" ? null : "failed") : String(check["status"]);
278
+ if (status === null || status === "running" || status === "pending")
279
+ continue;
280
+ const repo = String(row["repo"]), sha = String(row["sha"]);
281
+ store.transact(() => {
282
+ store.handle.prepare("UPDATE nightly_check SET run = ?, outcome = ? WHERE id = ?").run(run.id, status, Number(row["id"]));
283
+ if (status !== "passed") {
284
+ const items = failedItems(sealedRun(store, root, run.id)?.report ?? null);
285
+ for (const item of items)
286
+ store.handle.prepare("INSERT OR IGNORE INTO main_check_failure (repo, sha, run, item, opened_at) VALUES (?, ?, ?, ?, ?)").run(repo, sha, run.id, item, now.toISOString());
287
+ store.enqueueNotification({ dedupeKey: `nightly-failed:${Number(row["id"])}`, kind: "nightly-failed", subject: "Nightly check failed on main",
288
+ body: `Nightly full check failed on main at ${sha.slice(0, 7)}: ${failureSummary(items)}.`, pushClass: "attention", source: { taskRef: ref.id } }, now);
289
+ }
290
+ });
291
+ settled++;
292
+ }
293
+ for (const row of store.handle.prepare("SELECT a.run, a.mode, r.task_ref, r.head_revision, r.finished_at FROM release_check_attempt a JOIN run r ON r.id = a.run WHERE a.settled_at IS NULL AND r.finished_at IS NOT NULL ORDER BY a.run").all()) {
294
+ const runId = Number(row["run"]);
295
+ const repo = store.refById(Number(row["task_ref"]))?.repo ?? null;
296
+ store.transact(() => {
297
+ store.handle.prepare("UPDATE release_check_attempt SET settled_at = ? WHERE run = ?").run(now.toISOString(), runId);
298
+ if (repo === null || row["mode"] !== "full" || row["head_revision"] == null)
299
+ return;
300
+ // Only a full pass with coverage full clears anything; a composed result never does.
301
+ const gate = verificationEvidence(store, root, runId);
302
+ if (!gate.ok || gate.bytes === null)
303
+ return;
304
+ // A SUCCESSFUL executed full check only (review round 1): its receipt says it ran and passed, its coverage is full,
305
+ // and its own report exited 0. A failed full check clears nothing, whatever items it reports passed.
306
+ let receipt;
307
+ try {
308
+ receipt = JSON.parse(gate.bytes);
309
+ }
310
+ catch {
311
+ return;
312
+ }
313
+ if (receipt.coverage !== "full" || receipt.result?.ran !== true || receipt.result.exitCode !== 0)
314
+ return;
315
+ const report = sealedRun(store, root, runId)?.report ?? null;
316
+ if (report === null || report.exitCode !== 0)
317
+ return;
318
+ const gitDir = projectGitDir(repo);
319
+ if (gitDir === null)
320
+ return;
321
+ const head = String(row["head_revision"]);
322
+ // The pass has to be on main (its head an ancestor of, or main's head itself) or the exact candidate of a release
323
+ // whose approval stands: a pass on a side branch clears nothing (security review).
324
+ const task = store.refById(Number(row["task_ref"]))?.externalId ?? null;
325
+ const main = trustedMainOf(store, repo, now, TRUSTED_MAIN_CLEAR_MS);
326
+ const onMain = main !== null && (head === main || verifiedFirstParentWalk(gitDir, main, head, 5000).ok);
327
+ const release = task === null ? null : liveReleaseOf(store, task, "dispatch", now);
328
+ const candidate = task === null ? null : store.getScope(task)?.candidate ?? null;
329
+ // The run must have checked that very candidate: its head is the candidate, or the commit a prepared build recorded
330
+ // for it, holding exactly the candidate's verified tree.
331
+ const sameTree = (a, b) => { const x = readVerifiedCommit(gitDir, a), y = readVerifiedCommit(gitDir, b); return x.ok && y.ok && x.value.tree === y.value.tree; };
332
+ const releaseTip = release !== null && release.applies && release.ok && candidate !== null && release.authority.tipSha === candidate
333
+ && (head === candidate || sameTree(head, candidate));
334
+ if (!onMain && !releaseTip)
335
+ return;
336
+ for (const failure of store.handle.prepare("SELECT id, sha, item FROM main_check_failure WHERE repo = ? AND cleared_at IS NULL").all(repo)) {
337
+ const sha = String(failure["sha"]);
338
+ const contains = sha === head || verifiedFirstParentWalk(gitDir, head, sha, 2000).ok;
339
+ if (!contains || !coversItem(report, String(failure["item"])))
340
+ continue;
341
+ store.handle.prepare("UPDATE main_check_failure SET cleared_by_run = ?, cleared_at = ? WHERE id = ? AND cleared_at IS NULL").run(runId, now.toISOString(), Number(failure["id"]));
342
+ store.recordAction({ at: now.toISOString(), actor: "toolroll", repo, taskId: null, runId, action: "main failure cleared", outcome: "cleared", source: "work", detail: `${String(failure["item"])} at ${sha.slice(0, 7)} · by run ${runId}` });
343
+ }
344
+ });
345
+ settled++;
346
+ }
347
+ return settled;
348
+ }
349
+ /** The nightly's flow health: "the nightly check hasn't run since …" after 26 hours without a firing. Null when it ran
350
+ * recently, or the project has no nightly check. */
351
+ export function nightlyHealthWords(store, repo, configuredAt, now) {
352
+ const last = store.handle.prepare("SELECT at FROM nightly_check WHERE repo = ? ORDER BY id DESC LIMIT 1").get(repo);
353
+ const since = last === undefined ? configuredAt : String(last["at"]);
354
+ if (now.getTime() - Date.parse(since) < NIGHTLY_MISSED_MS)
355
+ return null;
356
+ return last === undefined ? `the nightly check hasn't run since it was set up (${since.slice(0, 16).replace("T", " ")})` : `the nightly check hasn't run since ${since.slice(0, 16).replace("T", " ")}`;
357
+ }
358
+ export { fileIdentity, groupIdentity, journeyIdentity, unitIdentity, versionIdentity };
359
+ /** The items the nightly check of main found failing that are still open: a full check reports each by name. */
360
+ export function openMainFailureItems(store, repo) {
361
+ return store.handle.prepare("SELECT DISTINCT item FROM main_check_failure WHERE repo = ? AND cleared_at IS NULL ORDER BY item").all(repo).map(row => String(row["item"]));
362
+ }