tickmarkr 2.6.1 → 2.6.2

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 (85) hide show
  1. package/README.md +14 -3
  2. package/dist/adapters/catalog-remote.js +89 -47
  3. package/dist/adapters/claude-code.js +9 -6
  4. package/dist/adapters/codex.js +7 -4
  5. package/dist/adapters/prompt.d.ts +1 -0
  6. package/dist/adapters/prompt.js +14 -6
  7. package/dist/adapters/registry.js +3 -3
  8. package/dist/adapters/types.d.ts +12 -4
  9. package/dist/adapters/types.js +6 -0
  10. package/dist/cli/commands/approve.d.ts +5 -1
  11. package/dist/cli/commands/approve.js +66 -23
  12. package/dist/cli/commands/compile.js +13 -3
  13. package/dist/cli/commands/doctor.d.ts +2 -0
  14. package/dist/cli/commands/doctor.js +11 -3
  15. package/dist/cli/commands/fleet.js +45 -7
  16. package/dist/cli/commands/report.js +18 -2
  17. package/dist/cli/commands/resume.js +4 -2
  18. package/dist/cli/commands/status.js +24 -19
  19. package/dist/cli/help.d.ts +2 -0
  20. package/dist/cli/help.js +9 -2
  21. package/dist/config/config.d.ts +20 -0
  22. package/dist/config/config.js +47 -8
  23. package/dist/config/fleet-overlay.d.ts +1 -0
  24. package/dist/config/fleet-overlay.js +56 -0
  25. package/dist/drivers/orca.d.ts +26 -1
  26. package/dist/drivers/orca.js +193 -60
  27. package/dist/eval/canary.d.ts +2 -1
  28. package/dist/eval/canary.js +2 -2
  29. package/dist/eval/dispatch.js +1 -0
  30. package/dist/gates/acceptance.d.ts +2 -1
  31. package/dist/gates/acceptance.js +7 -2
  32. package/dist/gates/baseline.d.ts +12 -1
  33. package/dist/gates/baseline.js +11 -4
  34. package/dist/gates/llm.d.ts +5 -4
  35. package/dist/gates/llm.js +13 -13
  36. package/dist/gates/review.d.ts +8 -0
  37. package/dist/gates/review.js +40 -4
  38. package/dist/gates/run-gates.d.ts +2 -1
  39. package/dist/gates/run-gates.js +27 -13
  40. package/dist/gates/test-manifest.d.ts +3 -1
  41. package/dist/gates/test-manifest.js +9 -2
  42. package/dist/graph/schema.d.ts +2 -0
  43. package/dist/graph/schema.js +2 -0
  44. package/dist/plan/scope.js +2 -2
  45. package/dist/route/preference.d.ts +20 -2
  46. package/dist/route/preference.js +48 -13
  47. package/dist/route/router.js +30 -15
  48. package/dist/run/consult.d.ts +13 -1
  49. package/dist/run/consult.js +14 -5
  50. package/dist/run/daemon.d.ts +37 -2
  51. package/dist/run/daemon.js +579 -141
  52. package/dist/run/git.d.ts +8 -0
  53. package/dist/run/git.js +14 -0
  54. package/dist/run/journal.d.ts +126 -3
  55. package/dist/run/journal.js +410 -37
  56. package/dist/run/merge.d.ts +3 -1
  57. package/dist/run/merge.js +3 -2
  58. package/dist/run/operator-summary.d.ts +3 -0
  59. package/dist/run/operator-summary.js +3 -1
  60. package/dist/run/protocol.d.ts +31 -1
  61. package/dist/run/protocol.js +3 -1
  62. package/dist/run/supervision.d.ts +7 -1
  63. package/dist/run/supervision.js +5 -2
  64. package/dist/tui/cockpit/board.js +3 -3
  65. package/dist/tui/cockpit/decision-actions.d.ts +8 -5
  66. package/dist/tui/cockpit/decision-actions.js +55 -32
  67. package/dist/tui/cockpit/derive.js +13 -2
  68. package/dist/tui/cockpit/live-runtime.d.ts +10 -0
  69. package/dist/tui/cockpit/live-runtime.js +50 -3
  70. package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
  71. package/dist/tui/cockpit/run-cockpit.js +26 -1
  72. package/dist/tui/cockpit/run-view.d.ts +9 -3
  73. package/dist/tui/cockpit/run-view.js +60 -7
  74. package/dist/tui/cockpit/setup-cockpit.d.ts +4 -0
  75. package/dist/tui/cockpit/setup-cockpit.js +6 -3
  76. package/dist/tui/ink/fleet-app.d.ts +15 -3
  77. package/dist/tui/ink/fleet-app.js +91 -22
  78. package/package.json +2 -1
  79. package/schema/config.schema.json +818 -0
  80. package/skills/tickmarkr-loop/SKILL.md +8 -2
  81. package/skills/tickmarkr-overseer/SKILL.md +42 -0
  82. package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +88 -0
  83. package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
  84. package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
  85. package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
@@ -44,6 +44,7 @@ export function marginalCostRank(c) {
44
44
  }
45
45
  const toAssignment = (c) => ({
46
46
  adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier,
47
+ ...(c.effort ? { effort: c.effort } : {}),
47
48
  });
48
49
  function resolvePin(pin, channels) {
49
50
  const c = channels.find((c) => c.adapter === pin.via && c.model === pin.model);
@@ -150,6 +151,13 @@ const maybeSlaLint = (lints, task, profile, slaMinutes, c) => {
150
151
  lints.push(`${task.id} (${task.shape}): median ${medianMin}m exceeds sla ${slaMinutes}m — learned perf term references sla ${slaMinutes}m ref`);
151
152
  };
152
153
  export function route(task, cfg, channels, profile, preferCtx, exclude, exploreCtx) {
154
+ // OBS-1143: deny checks below read the probed identity the offered channels carry — the identity
155
+ // discovery admitted them under. A seat no channel vouches for stays unknown (conservative).
156
+ const observed = new Map(channels.flatMap((c) => (c.identity ? [[channelKey(c), c.identity]] : [])));
157
+ const seat = (adapter, model) => {
158
+ const identity = observed.get(channelKey({ adapter, model }));
159
+ return identity ? { adapter, model, identity } : { adapter, model };
160
+ };
153
161
  channels = withoutExcluded(channels, exclude);
154
162
  const lints = [];
155
163
  const advisoryFloor = cfg.routing.floors[task.shape];
@@ -161,7 +169,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
161
169
  const prefer = effectivePrefer(entry);
162
170
  const prefActive = !!(cfg.routing.allow || cfg.routing.deny);
163
171
  const disallowedPin = (via, model, kind) => {
164
- const d = disallowedBy({ adapter: via, model }, cfg.routing);
172
+ const d = disallowedBy(seat(via, model), cfg.routing);
165
173
  if (d) {
166
174
  throw new RoutingError(`${task.id}: ${kind} ${via}:${model} is disallowed by routing.${d.by} (${d.entry}) — remove the ${d.by} entry or re-pin to an allowed channel`);
167
175
  }
@@ -174,8 +182,8 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
174
182
  }
175
183
  if (cfg.tiers[p]) {
176
184
  const expanded = channelsFromConfig(p, cfg);
177
- if (expanded.length && expanded.every((c) => disallowedBy(c, cfg.routing) !== null)) {
178
- const d = disallowedBy(expanded[0], cfg.routing);
185
+ if (expanded.length && expanded.every((c) => disallowedBy(seat(c.adapter, c.model), cfg.routing) !== null)) {
186
+ const d = disallowedBy(seat(expanded[0].adapter, expanded[0].model), cfg.routing);
179
187
  throw new RoutingError(`${task.id}: prefer entry ${p} is disallowed by routing.${d.by} (${d.entry}) — remove the ${d.by} entry or re-pin to an allowed channel`);
180
188
  }
181
189
  return;
@@ -228,21 +236,28 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
228
236
  // single-channel pool) and the open floor/auto path. Exhaustion stays fail-loud (pin precedent).
229
237
  if (entry?.pool) {
230
238
  const pool = entry.pool;
231
- if (prefActive) {
232
- for (const p of pool.channels) {
233
- const i = p.indexOf(":");
234
- const d = disallowedBy({ adapter: p.slice(0, i), model: p.slice(i + 1) }, cfg.routing);
235
- if (d) {
236
- throw new RoutingError(`${task.id}: pool entry ${p} is disallowed by routing.${d.by} (${d.entry}) — remove the ${d.by} entry or drop it from the pool`);
237
- }
238
- }
239
+ // OBS-1144: a disallowed entry is skipped, named in provenance, never fatal for the pool; only an
240
+ // empty admitted remainder refuses. Filtering keeps declaration order, so ordered stays ordered.
241
+ const skipped = [];
242
+ const admitted = pool.channels.filter((p) => {
243
+ if (!prefActive)
244
+ return true;
245
+ const i = p.indexOf(":");
246
+ const d = disallowedBy(seat(p.slice(0, i), p.slice(i + 1)), cfg.routing);
247
+ if (d)
248
+ skipped.push(`${p} (disallowed by routing.${d.by} (${d.entry}))`);
249
+ return d === null;
250
+ });
251
+ const skippedNote = skipped.length ? `; skipped ${skipped.join(", ")}` : "";
252
+ if (!admitted.length) {
253
+ throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool is exhausted — no admitted entry remains: ${skipped.join(", ")} — remove the deny/allow entry or add an admitted channel to the pool`);
239
254
  }
240
255
  // live preserves pool declaration order — ordered mode and any-mode stable-sort ties depend on it
241
- const live = pool.channels
256
+ const live = admitted
242
257
  .map((id) => channels.find((c) => channelKey(c) === id))
243
258
  .filter((c) => c !== undefined && (!taskFloor || TIER_RANK[c.tier] >= TIER_RANK[taskFloor]));
244
259
  if (!live.length) {
245
- throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel${taskFloor ? ` at task floor ${taskFloor}` : ""} — declared: ${pool.channels.join(", ")}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
260
+ throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel${taskFloor ? ` at task floor ${taskFloor}` : ""} — declared: ${pool.channels.join(", ")}${skippedNote}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
246
261
  }
247
262
  // No learned/explore inside pools: any spreads deterministically; ordered keeps declaration order.
248
263
  const chosen = pool.mode === "ordered"
@@ -250,7 +265,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
250
265
  : rankAnyPool(live, task)[0];
251
266
  lintMapPinFloor(chosen.tier, "pool");
252
267
  maybeSlaLint(lints, task, profile, slaMinutes, chosen);
253
- return { assignment: toAssignment(chosen), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pool ${pool.mode} ${channelKey(chosen)} (config routing.map)` };
268
+ return { assignment: toAssignment(chosen), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pool ${pool.mode} ${channelKey(chosen)} (config routing.map${skippedNote})` };
254
269
  }
255
270
  const baseTier = floor ?? "cheap";
256
271
  // A task floor is hard on pool and floor/auto paths; the map-pin branch above stays supreme.
@@ -268,7 +283,7 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
268
283
  let msg = `${task.id}: no channel at tier>=${minTier}; available: ${channels.map(channelKey).join(", ") || "(none)"}`;
269
284
  if (prefActive) {
270
285
  const excluded = Object.keys(cfg.tiers).flatMap((id) => channelsFromConfig(id, cfg))
271
- .filter((c) => disallowedBy(c, cfg.routing) !== null && TIER_RANK[c.tier] >= TIER_RANK[minTier])
286
+ .filter((c) => disallowedBy(seat(c.adapter, c.model), cfg.routing) !== null && TIER_RANK[c.tier] >= TIER_RANK[minTier])
272
287
  .map(channelKey);
273
288
  if (excluded.length)
274
289
  msg += `; ${excluded.length} channel(s) excluded by routing.allow/deny: ${excluded.join(", ")}`;
@@ -1,5 +1,6 @@
1
- import type { WorkerAdapter } from "../adapters/types.js";
1
+ import { type WorkerAdapter } from "../adapters/types.js";
2
2
  import type { TickmarkrConfig } from "../config/config.js";
3
+ import type { Effort } from "../graph/schema.js";
3
4
  import type { ExecutorDriver, Slot } from "../drivers/types.js";
4
5
  import type { GateResult } from "../gates/types.js";
5
6
  import { type VerdictUnparseableCause } from "../gates/verdict-cause.js";
@@ -13,6 +14,16 @@ export interface ConsultVerdict {
13
14
  adapter?: string;
14
15
  model?: string;
15
16
  vendor?: string;
17
+ effort?: Effort;
18
+ invocations?: ConsultInvocation[];
19
+ }
20
+ export interface ConsultInvocation {
21
+ adapter: string;
22
+ model: string;
23
+ vendor: string;
24
+ effort?: Effort;
25
+ outcome: "completed" | "failed";
26
+ reason: string;
16
27
  }
17
28
  export declare function renderRetryGuidance(v: ConsultVerdict): string;
18
29
  export declare function augmentRetryBrief(feedback: string, opts: {
@@ -37,6 +48,7 @@ export declare function buildDossierPrompt(d: Dossier, nonce: string): string;
37
48
  export declare function consult(d: Dossier, cfg: TickmarkrConfig, adapters: WorkerAdapter[], driver: ExecutorDriver, cwd: string, runDir: string, opts?: {
38
49
  keep?: boolean;
39
50
  onSlot?: (slot: Slot) => void;
51
+ onInvocation?: (inv: ConsultInvocation) => void;
40
52
  runId?: string;
41
53
  channels?: Array<{
42
54
  adapter: string;
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { getAdapter } from "../adapters/registry.js";
4
+ import { configuredEffort } from "../adapters/types.js";
4
5
  import { bannerShell, paneDispatchCommand } from "../brand.js";
5
6
  import { dewrapPaneVerdict, extractVerdictJson, gateExitTrailer, gatePaneName, generateVerdictNonce, verdictNonceLine } from "../gates/llm.js";
6
7
  import { classifyVerdictCause } from "../gates/verdict-cause.js";
@@ -145,6 +146,8 @@ export async function consult(d, cfg, adapters, driver, cwd, runDir,
145
146
  // via SlotOpts.owned; without it the legacy gatePaneName shape survives (non-daemon callers/tests).
146
147
  // v1.54 T1: channels — the daemon's doctor-filtered live channel list; prefer-seat liveness is
147
148
  // judged against it only (never rebuilt from config, which would select installed-but-unauthed seats).
149
+ // OBS-1182: onInvocation hears every launched seat — its identity, effort and outcome — failed seats
150
+ // included, so a seat that never produced the verdict still reaches the journal.
148
151
  opts = {}) {
149
152
  const n = ++consultSeq;
150
153
  const nonce = generateVerdictNonce();
@@ -157,11 +160,11 @@ opts = {}) {
157
160
  // One seat = the WHOLE invoke-and-parse unit, both visibility branches (OBS-69 class: a headless-only
158
161
  // failover would leave the production pane path hard-failing on seat one). A null verdict retains
159
162
  // the classifier's cause when extraction failed; parsed content rejections deliberately have none.
160
- const invokeSeat = async (seatAdapter, seatModel, seatIdx) => {
163
+ const invokeSeat = async (seatAdapter, seatModel, seatIdx, effort) => {
161
164
  const adapter = getAdapter(seatAdapter, adapters);
162
165
  let out;
163
166
  if (cfg.visibility.llm === "headless") {
164
- const r = await sh(adapter.headlessCommand(promptFile, seatModel), cwd, cfg.consult.stallMinutes * 60_000);
167
+ const r = await sh(adapter.headlessCommand(promptFile, seatModel, effort), cwd, cfg.consult.stallMinutes * 60_000);
165
168
  out = r.stdout + r.stderr;
166
169
  }
167
170
  else {
@@ -169,7 +172,7 @@ opts = {}) {
169
172
  // but never exits, so the exit trailer after it never runs, the wait runs to the stall cap and
170
173
  // the 300-line read is a TUI frame. The headless command exits, the trailer prints, and the
171
174
  // pane is harvested through dewrap exactly like a judge or review seat.
172
- const command = adapter.headlessCommand(promptFile, seatModel);
175
+ const command = adapter.headlessCommand(promptFile, seatModel, effort);
173
176
  // T8: role-first pane name for fleet visibility (consult · T2); consultSeq stays on the dossier artifact only
174
177
  const slot = await driver.slot(cwd, gatePaneName("consult", d.taskId), {
175
178
  label: `CONSULT ${d.taskId}`,
@@ -242,8 +245,12 @@ opts = {}) {
242
245
  };
243
246
  }
244
247
  for (const [i, seat] of allowedSeats.entries()) {
248
+ // OBS-1182: each seat launches at its own configured effort, never the worker's.
249
+ const effort = configuredEffort(cfg, seat);
250
+ const record = (outcome, reason) => opts.onInvocation?.({ ...seatIdentity(seat), ...(effort ? { effort } : {}), outcome, reason });
245
251
  try {
246
- const parsed = await invokeSeat(seat.adapter, seat.model, i);
252
+ const parsed = await invokeSeat(seat.adapter, seat.model, i, effort);
253
+ record(parsed.verdict ? "completed" : "failed", parsed.verdict ? "verdict" : `verdict unparseable${parsed.cause ? ` (${parsed.cause})` : ""}`);
247
254
  if (parsed.verdict) {
248
255
  const excludeProvider = parsed.verdict.excludeAdapter
249
256
  ? excludedProviderFromDossier(d, parsed.verdict.excludeAdapter)
@@ -255,11 +262,13 @@ opts = {}) {
255
262
  notes: `${parsed.verdict.notes} — excluded provider ${excludeProvider}`,
256
263
  } : {}),
257
264
  ...seatIdentity(seat),
265
+ ...(effort ? { effort } : {}),
258
266
  };
259
267
  }
260
268
  }
261
- catch {
269
+ catch (e) {
262
270
  // failed seat (unknown adapter, dead driver/pane, shell error) — fall to the next entry
271
+ record("failed", `seat error: ${e instanceof Error ? e.message : String(e)}`.slice(0, 200));
263
272
  }
264
273
  }
265
274
  return {
@@ -2,12 +2,14 @@ import { type WorkerAdapter } from "../adapters/types.js";
2
2
  import { type ModeResolution, type RoutingMode, type TickmarkrConfig } from "../config/config.js";
3
3
  import { type DriverChoice } from "../drivers/index.js";
4
4
  import { type ExecutorDriver, type Slot } from "../drivers/types.js";
5
- import { type Baseline } from "../gates/baseline.js";
5
+ import { type Baseline, type BaselineProvenance } from "../gates/baseline.js";
6
6
  import type { GateResult } from "../gates/types.js";
7
7
  import { type RunGraph } from "../graph/schema.js";
8
8
  import { type PreserveProducer } from "./git.js";
9
9
  import { Journal, type JournalEvent } from "./journal.js";
10
10
  export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
11
+ export declare function setContactUnreadableDeadlineMsForTests(ms: number): void;
12
+ export declare function resetContactUnreadableDeadlineMsForTests(): void;
11
13
  /** Optional authoritative transport receipt. Screen text can prove execution, never nonacceptance. */
12
14
  export interface DispatchObservation {
13
15
  slotId: string;
@@ -67,7 +69,34 @@ export interface RunSummary {
67
69
  approvalDisposition?: "complete" | "outstanding";
68
70
  /** the accepted approvals that never reached a dispatch — named, never left to the park buckets */
69
71
  outstandingApprovals?: string[];
72
+ /** OBS-1123: every baseline-recorded red a standing green carried, with the capture that recorded it */
73
+ forgiven?: ForgivenFingerprints[];
70
74
  }
75
+ /** OBS-1123: one forgiveness — the fingerprints a green carried and the capture that recorded them. */
76
+ export interface ForgivenFingerprints {
77
+ /** the task whose gate forgave them; absent for the integration tip */
78
+ taskId?: string;
79
+ gate: string;
80
+ /** absent: a legacy row that named no fingerprints */
81
+ fingerprints?: string[];
82
+ /** absent: a legacy capture or row that recorded no provenance — rendered unknown, never dated */
83
+ baseline?: BaselineProvenance;
84
+ }
85
+ /** Journal rows are untrusted input: a malformed field reads as not recorded, never as a value. */
86
+ export declare const fingerprintsOf: (v: unknown) => string[] | undefined;
87
+ export declare const baselineProvenanceOf: (v: unknown) => BaselineProvenance | undefined;
88
+ /** A forgiven battery green: the structured field, or the battery's own "(forgiven)" wording on a legacy row. */
89
+ export declare const forgivenGateRow: (data: Record<string, unknown>) => boolean;
90
+ /**
91
+ * OBS-1123: the forgiveness standing at this point in the journal — each task gate's LATEST verdict
92
+ * where it is a forgiven green, then the latest verification cycle's forgiven tip rows. A legacy row
93
+ * keeps what it never recorded absent, so the reader says "unknown" instead of borrowing a date.
94
+ */
95
+ export declare function forgivenFingerprints(events: readonly JournalEvent[]): ForgivenFingerprints[];
96
+ /** The capture a forgiveness rests on, or an explicit unknown — never a time read off another clock. */
97
+ export declare function formatBaselineProvenance(p: BaselineProvenance | undefined): string;
98
+ export declare function formatFingerprints(fingerprints: string[] | undefined): string;
99
+ export declare function formatForgiven(f: ForgivenFingerprints): string;
71
100
  export declare function pendingDaemonApprovalActions(events: JournalEvent[]): Map<string, import("./journal.js").PendingApprovalAction>;
72
101
  /**
73
102
  * T14, amended by v2.2 T3: approvals the run accepted and never acted on. `approved` above is still
@@ -131,6 +160,9 @@ export declare const resetApprovalWindowForTests: () => void;
131
160
  /** Test seam — shrink the capacity backoff without minute-long sleeps. */
132
161
  export declare function setCapacityBackoffMsForTests(ms: number): void;
133
162
  export declare function resetCapacityBackoffMsForTests(): void;
163
+ /** Test seam — shrink the bootstrap backoff. */
164
+ export declare function setBootstrapBackoffMsForTests(ms: number): void;
165
+ export declare function resetBootstrapBackoffMsForTests(): void;
134
166
  export declare const EARLY_LAUNCH_LIVENESS_MS = 60000;
135
167
  /** Test seam — lowers the empty-pane liveness window without sleeping 60s per case. */
136
168
  export declare function setEarlyLaunchLivenessMsForTests(ms: number): void;
@@ -154,8 +186,11 @@ export interface StartupFailureEvidence {
154
186
  /** One seat, one startup prefix. Once execution or its input box is seen, later panes are ineligible. */
155
187
  export declare class StartupFailureDetector {
156
188
  private readonly adapter;
189
+ private readonly pattern;
190
+ private readonly wholePrefix;
157
191
  private closed;
158
- constructor(adapter: Pick<WorkerAdapter, "inputBox" | "harnessBannerRows">);
192
+ constructor(adapter: Pick<WorkerAdapter, "inputBox" | "harnessBannerRows">, pattern?: RegExp, // OBS-1169: or known bootstrap text
193
+ wholePrefix?: boolean);
159
194
  sample(output: string, launchedAt: number | undefined): StartupFailureEvidence | undefined;
160
195
  }
161
196
  /** Test seam — shrink the quota-banner silence gate without minute-long sleeps. */