@foldrun/core 2.3.0 → 2.5.0
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/dist/src/agent-activity.d.ts +5 -0
- package/dist/src/agent-activity.js +31 -0
- package/dist/src/agent-import.js +1 -1
- package/dist/src/agent-tools.js +1 -1
- package/dist/src/agents-md.js +1 -1
- package/dist/src/alerts.d.ts +60 -0
- package/dist/src/alerts.js +161 -0
- package/dist/src/browser-pod.js +4 -0
- package/dist/src/capture.d.ts +23 -0
- package/dist/src/capture.js +80 -0
- package/dist/src/compare.d.ts +57 -0
- package/dist/src/compare.js +116 -0
- package/dist/src/completions.js +1 -0
- package/dist/src/deploy.d.ts +36 -1
- package/dist/src/deploy.js +170 -11
- package/dist/src/egress.d.ts +16 -1
- package/dist/src/evals.d.ts +5 -1
- package/dist/src/evals.js +26 -18
- package/dist/src/frontmatter-fields.js +9 -0
- package/dist/src/frontmatter-schema.js +3 -0
- package/dist/src/history.d.ts +4 -0
- package/dist/src/history.js +15 -0
- package/dist/src/hooks.js +1 -1
- package/dist/src/insights.d.ts +30 -0
- package/dist/src/insights.js +152 -0
- package/dist/src/issues.d.ts +140 -0
- package/dist/src/issues.js +302 -0
- package/dist/src/library.js +1 -1
- package/dist/src/limits.d.ts +8 -0
- package/dist/src/limits.js +36 -5
- package/dist/src/linkable.js +1 -1
- package/dist/src/matter.d.ts +8 -0
- package/dist/src/matter.js +28 -0
- package/dist/src/model-credential.d.ts +14 -0
- package/dist/src/model-credential.js +40 -0
- package/dist/src/model-env.d.ts +14 -2
- package/dist/src/model-env.js +34 -9
- package/dist/src/network.d.ts +12 -0
- package/dist/src/network.js +25 -1
- package/dist/src/notify.d.ts +16 -0
- package/dist/src/notify.js +17 -7
- package/dist/src/okf.js +1 -1
- package/dist/src/online-checks.d.ts +76 -0
- package/dist/src/online-checks.js +432 -0
- package/dist/src/operator.d.ts +277 -0
- package/dist/src/operator.js +176 -2
- package/dist/src/otel-config.d.ts +18 -0
- package/dist/src/otel-config.js +69 -0
- package/dist/src/otel.d.ts +38 -0
- package/dist/src/otel.js +85 -0
- package/dist/src/outcome.d.ts +40 -0
- package/dist/src/outcome.js +77 -0
- package/dist/src/package.js +1 -1
- package/dist/src/platform.d.ts +1 -1
- package/dist/src/prompt-layers.d.ts +30 -0
- package/dist/src/prompt-layers.js +77 -0
- package/dist/src/provider-health.js +1 -1
- package/dist/src/providers.d.ts +5 -0
- package/dist/src/providers.js +2 -2
- package/dist/src/run-container.d.ts +16 -2
- package/dist/src/run-container.js +57 -5
- package/dist/src/runner.d.ts +53 -2
- package/dist/src/runner.js +356 -70
- package/dist/src/runs-filter.d.ts +48 -0
- package/dist/src/runs-filter.js +73 -0
- package/dist/src/scan.d.ts +19 -1
- package/dist/src/scan.js +60 -7
- package/dist/src/starter.js +4 -2
- package/dist/src/step-exec.d.ts +29 -1
- package/dist/src/step-exec.js +106 -6
- package/dist/src/store.d.ts +130 -3
- package/dist/src/store.js +145 -6
- package/dist/src/tokens.d.ts +38 -0
- package/dist/src/tokens.js +57 -0
- package/dist/src/tool-names.js +1 -1
- package/dist/src/tool-programs.js +1 -1
- package/dist/src/trace-tree.d.ts +40 -0
- package/dist/src/trace-tree.js +156 -0
- package/dist/src/translator.d.ts +8 -0
- package/dist/src/translator.js +7 -2
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/workspace-check.d.ts +18 -0
- package/dist/src/workspace-check.js +34 -0
- package/package.json +2 -2
- package/src/agent-activity.ts +32 -0
- package/src/agent-import.ts +1 -1
- package/src/agent-tools.ts +1 -1
- package/src/agents-md.ts +1 -1
- package/src/alerts.ts +190 -0
- package/src/browser-pod.ts +3 -0
- package/src/capture.ts +88 -0
- package/src/compare.ts +146 -0
- package/src/completions.ts +1 -0
- package/src/deploy.ts +184 -10
- package/src/egress.ts +12 -2
- package/src/evals.ts +26 -18
- package/src/frontmatter-fields.ts +9 -0
- package/src/frontmatter-schema.ts +3 -0
- package/src/history.ts +13 -0
- package/src/hooks.ts +1 -1
- package/src/insights.ts +174 -0
- package/src/issues.ts +396 -0
- package/src/library.ts +1 -1
- package/src/limits.ts +38 -5
- package/src/linkable.ts +1 -1
- package/src/matter.ts +33 -0
- package/src/model-credential.ts +45 -0
- package/src/model-env.ts +42 -10
- package/src/network.ts +25 -1
- package/src/notify.ts +22 -7
- package/src/okf.ts +1 -1
- package/src/online-checks.ts +450 -0
- package/src/operator.ts +213 -3
- package/src/otel-config.ts +67 -0
- package/src/otel.ts +100 -0
- package/src/outcome.ts +90 -0
- package/src/package.ts +1 -1
- package/src/platform.ts +1 -1
- package/src/prompt-layers.ts +90 -0
- package/src/provider-health.ts +1 -1
- package/src/providers.ts +7 -2
- package/src/run-container.ts +66 -7
- package/src/runner.ts +363 -46
- package/src/runs-filter.ts +88 -0
- package/src/scan.ts +68 -7
- package/src/starter.ts +4 -2
- package/src/step-exec.ts +147 -9
- package/src/store.ts +254 -8
- package/src/tokens.ts +68 -0
- package/src/tool-names.ts +1 -1
- package/src/tool-programs.ts +1 -1
- package/src/trace-tree.ts +187 -0
- package/src/translator.ts +13 -2
- package/src/trigger-log.ts +1 -0
- package/src/workspace-check.ts +41 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RunSummary } from "./store.ts";
|
|
2
|
+
/** Agent name → the ISO time of its latest step that ran. */
|
|
3
|
+
export declare function agentLastRuns(runs: RunSummary[]): Map<string, string>;
|
|
4
|
+
/** Whole days since `iso`, or null for never. */
|
|
5
|
+
export declare function daysSince(iso: string | null | undefined, now?: number): number | null;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// When each agent last did anything — the question an account with hundreds
|
|
2
|
+
// of agents has to be able to answer before it can retire the ones nobody
|
|
3
|
+
// uses. Read from the run index (listRunSummaries), so it covers what the
|
|
4
|
+
// install keeps: runs older than its retention are gone, and an agent whose
|
|
5
|
+
// last run was pruned reads as never run.
|
|
6
|
+
/** A step that started counts; one that was skipped or never reached does not. */
|
|
7
|
+
const RAN = new Set(["running", "completed", "failed", "awaiting-approval"]);
|
|
8
|
+
/** Agent name → the ISO time of its latest step that ran. */
|
|
9
|
+
export function agentLastRuns(runs) {
|
|
10
|
+
const last = new Map();
|
|
11
|
+
for (const run of runs) {
|
|
12
|
+
for (const step of run.steps ?? []) {
|
|
13
|
+
if (!step.agent || !RAN.has(step.status))
|
|
14
|
+
continue;
|
|
15
|
+
const at = step.startedAt ?? run.startedAt;
|
|
16
|
+
if (!at)
|
|
17
|
+
continue;
|
|
18
|
+
const prev = last.get(step.agent);
|
|
19
|
+
if (!prev || at > prev)
|
|
20
|
+
last.set(step.agent, at);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return last;
|
|
24
|
+
}
|
|
25
|
+
/** Whole days since `iso`, or null for never. */
|
|
26
|
+
export function daysSince(iso, now = Date.now()) {
|
|
27
|
+
if (!iso)
|
|
28
|
+
return null;
|
|
29
|
+
const t = Date.parse(iso);
|
|
30
|
+
return Number.isFinite(t) ? Math.floor((now - t) / 86_400_000) : null;
|
|
31
|
+
}
|
package/dist/src/agent-import.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// were about somebody else's desk.
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import path from "node:path";
|
|
17
|
-
import matter from "
|
|
17
|
+
import matter from "./matter.js";
|
|
18
18
|
import { assertSafeName, listWorkspaceFiles, readWorkspaceFile, writeWorkspaceFile, workspaceDir, workspaceTools, listWorkspaces, } from "./store.js";
|
|
19
19
|
import { libraryDir, libraryTools } from "./library.js";
|
|
20
20
|
import { isRuntimeTool, toolEntryName } from "./tool-names.js";
|
package/dist/src/agent-tools.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import path from "node:path";
|
|
19
|
-
import matter from "
|
|
19
|
+
import matter from "./matter.js";
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
22
22
|
import { resolveModel, resolveEffort } from "./store.js";
|
package/dist/src/agents-md.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// else in the file is untouched. Null removes the key.
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import matter from "
|
|
11
|
+
import matter from "./matter.js";
|
|
12
12
|
const FRONT = /^(---\r?\n)([\s\S]*?)(\r?\n---)(\r?\n|$)/;
|
|
13
13
|
/** The frontmatter of a directory's AGENTS.md (or project.md), or {}. */
|
|
14
14
|
export function readAgentsMdKeys(dir) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Outcome } from "./outcome.ts";
|
|
2
|
+
export interface AlertConfig {
|
|
3
|
+
failureRate?: number;
|
|
4
|
+
minRuns: number;
|
|
5
|
+
creditsDay?: number;
|
|
6
|
+
runMinutes?: number;
|
|
7
|
+
staleDays?: number;
|
|
8
|
+
costSpike?: number;
|
|
9
|
+
runsPerHour?: number;
|
|
10
|
+
/** Told after a run (platform host-watch.ts), not on the watchdog's tick. */
|
|
11
|
+
newHost?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** The config, and what is wrong with it in words (for `foldrun check`). */
|
|
14
|
+
export declare function parseAlerts(raw: unknown): {
|
|
15
|
+
config: AlertConfig | null;
|
|
16
|
+
problems: string[];
|
|
17
|
+
};
|
|
18
|
+
export interface AlertFacts {
|
|
19
|
+
/** Outcomes of the runs started in the last 24 hours. */
|
|
20
|
+
outcomes: Outcome[];
|
|
21
|
+
/** Credits are cents: USD charged in the last 24 hours. */
|
|
22
|
+
chargedUsd: number;
|
|
23
|
+
/** Runs still going. */
|
|
24
|
+
live: {
|
|
25
|
+
id: string;
|
|
26
|
+
flow: string;
|
|
27
|
+
startedAt: string;
|
|
28
|
+
}[];
|
|
29
|
+
/** For stale_days: each agent, when it last ran (null: not in the runs
|
|
30
|
+
* kept), and since when it has existed — an agent added yesterday is not
|
|
31
|
+
* stale for never having run. */
|
|
32
|
+
agents?: {
|
|
33
|
+
name: string;
|
|
34
|
+
lastRunAt: string | null;
|
|
35
|
+
since: string;
|
|
36
|
+
}[];
|
|
37
|
+
/** For cost_spike and runs_per_hour: the runs of the last seven days,
|
|
38
|
+
* test runs left out. costUsd is what its steps cost. */
|
|
39
|
+
runs?: {
|
|
40
|
+
id: string;
|
|
41
|
+
flow: string;
|
|
42
|
+
startedAt: string;
|
|
43
|
+
finishedAt: string | null;
|
|
44
|
+
costUsd: number;
|
|
45
|
+
}[];
|
|
46
|
+
}
|
|
47
|
+
export interface Alert {
|
|
48
|
+
/** What the once-a-day memory is keyed by: the rule, or the rule and run. */
|
|
49
|
+
key: string;
|
|
50
|
+
headline: string;
|
|
51
|
+
detail: string;
|
|
52
|
+
runId?: string;
|
|
53
|
+
flow?: string;
|
|
54
|
+
/** Said once for this key, however long it stays true (a slow run, an
|
|
55
|
+
* idle spell); the watchdog's notice memory is pruned after a week, so
|
|
56
|
+
* a condition that lasts is said again weekly at most. */
|
|
57
|
+
once?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** What crosses its threshold now. */
|
|
60
|
+
export declare function evaluateAlerts(cfg: AlertConfig, facts: AlertFacts, workspace: string, now: number): Alert[];
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// `alerts:` — the thresholds a workspace wants to hear about (FOL-72 step 7):
|
|
2
|
+
//
|
|
3
|
+
// alerts:
|
|
4
|
+
// failure_rate: 30% # of its runs that finished in the last 24 hours
|
|
5
|
+
// min_runs: 5 # …once at least this many have (default 5)
|
|
6
|
+
// credits_day: 2000 # credits charged in the last 24 hours
|
|
7
|
+
// run_minutes: 45 # a run still going after this long
|
|
8
|
+
// stale_days: 30 # an agent that has not run in this many days
|
|
9
|
+
// cost_spike: 3 # a run that cost this many times its flow's 7-day average
|
|
10
|
+
// runs_per_hour: 20 # one flow started this many times in an hour
|
|
11
|
+
// new_host: true # an agent reached a site it never reached before
|
|
12
|
+
//
|
|
13
|
+
// In AGENTS.md (the workspace's, else the account's). The watchdog evaluates
|
|
14
|
+
// them on its tick and tells `notify:` once per rule a day (a slow run once
|
|
15
|
+
// per run), as event `alert`. Pure: `foldrun check` and the watchdog read the
|
|
16
|
+
// same parse.
|
|
17
|
+
const KEYS = ["failure_rate", "min_runs", "credits_day", "run_minutes", "stale_days", "cost_spike", "runs_per_hour", "new_host"];
|
|
18
|
+
const num = (v) => {
|
|
19
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
20
|
+
return v;
|
|
21
|
+
if (typeof v === "string" && v.trim() && Number.isFinite(Number(v.trim())))
|
|
22
|
+
return Number(v.trim());
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
/** The config, and what is wrong with it in words (for `foldrun check`). */
|
|
26
|
+
export function parseAlerts(raw) {
|
|
27
|
+
if (raw === undefined || raw === null)
|
|
28
|
+
return { config: null, problems: [] };
|
|
29
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
30
|
+
return { config: null, problems: ["alerts: is a block — failure_rate, min_runs, credits_day, run_minutes, stale_days, cost_spike, runs_per_hour, new_host"] };
|
|
31
|
+
}
|
|
32
|
+
const o = raw;
|
|
33
|
+
const problems = [];
|
|
34
|
+
for (const k of Object.keys(o))
|
|
35
|
+
if (!KEYS.includes(k))
|
|
36
|
+
problems.push(`alerts: ${k} — not a threshold; the keys are ${KEYS.join(", ")}`);
|
|
37
|
+
const cfg = { minRuns: 5 };
|
|
38
|
+
if (o.failure_rate !== undefined) {
|
|
39
|
+
const s = String(o.failure_rate).trim();
|
|
40
|
+
const pct = /^(\d+(?:\.\d+)?)\s*%$/.exec(s);
|
|
41
|
+
const v = pct ? Number(pct[1]) / 100 : num(o.failure_rate);
|
|
42
|
+
if (v === null || v <= 0 || v > 1)
|
|
43
|
+
problems.push(`alerts: failure_rate ${s} — a share of runs, like 30% or 0.3`);
|
|
44
|
+
else
|
|
45
|
+
cfg.failureRate = v;
|
|
46
|
+
}
|
|
47
|
+
for (const [key, field, min] of [["min_runs", "minRuns", 1], ["credits_day", "creditsDay", 1], ["run_minutes", "runMinutes", 1], ["stale_days", "staleDays", 1], ["cost_spike", "costSpike", 1.5], ["runs_per_hour", "runsPerHour", 2]]) {
|
|
48
|
+
if (o[key] === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
const v = num(o[key]);
|
|
51
|
+
if (v === null || v < min)
|
|
52
|
+
problems.push(`alerts: ${key} ${String(o[key])} — a number, at least ${min}`);
|
|
53
|
+
else
|
|
54
|
+
cfg[field] = v;
|
|
55
|
+
}
|
|
56
|
+
if (o.new_host !== undefined) {
|
|
57
|
+
if (typeof o.new_host !== "boolean")
|
|
58
|
+
problems.push(`alerts: new_host ${String(o.new_host)} — true or false`);
|
|
59
|
+
else if (o.new_host)
|
|
60
|
+
cfg.newHost = true;
|
|
61
|
+
}
|
|
62
|
+
const any = cfg.newHost === true || cfg.failureRate !== undefined || cfg.creditsDay !== undefined || cfg.runMinutes !== undefined || cfg.staleDays !== undefined || cfg.costSpike !== undefined || cfg.runsPerHour !== undefined;
|
|
63
|
+
return { config: any ? cfg : null, problems };
|
|
64
|
+
}
|
|
65
|
+
/** How few earlier runs a flow needs before its average means anything. */
|
|
66
|
+
const SPIKE_MIN_RUNS = 3;
|
|
67
|
+
/** What crosses its threshold now. */
|
|
68
|
+
export function evaluateAlerts(cfg, facts, workspace, now) {
|
|
69
|
+
const out = [];
|
|
70
|
+
if (cfg.failureRate !== undefined) {
|
|
71
|
+
const finished = facts.outcomes.filter((o) => o === "failed" || o === "good" || o === "done" || o === "quiet" || o === "blocked" || o === "bad");
|
|
72
|
+
const failed = finished.filter((o) => o === "failed").length;
|
|
73
|
+
if (finished.length >= cfg.minRuns && failed / finished.length >= cfg.failureRate) {
|
|
74
|
+
out.push({
|
|
75
|
+
key: "failure_rate",
|
|
76
|
+
headline: `⚠ ${workspace}: ${failed} of its last ${finished.length} runs failed`,
|
|
77
|
+
detail: `${Math.round((100 * failed) / finished.length)}% in the last 24 hours, over the ${Math.round(cfg.failureRate * 100)}% in alerts: failure_rate. Its Issues show what broke.`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (cfg.creditsDay !== undefined) {
|
|
82
|
+
const credits = Math.round(facts.chargedUsd * 100);
|
|
83
|
+
if (credits >= cfg.creditsDay) {
|
|
84
|
+
out.push({
|
|
85
|
+
key: "credits_day",
|
|
86
|
+
headline: `⚠ ${workspace} was charged ${credits.toLocaleString("en")} credits in a day`,
|
|
87
|
+
detail: `Over the ${cfg.creditsDay.toLocaleString("en")} in alerts: credits_day, in the last 24 hours. Observe shows which flows and agents.`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (cfg.runMinutes !== undefined) {
|
|
92
|
+
for (const r of facts.live) {
|
|
93
|
+
const mins = (now - Date.parse(r.startedAt)) / 60_000;
|
|
94
|
+
if (mins >= cfg.runMinutes) {
|
|
95
|
+
out.push({
|
|
96
|
+
key: `run_minutes/${r.id}`,
|
|
97
|
+
runId: r.id,
|
|
98
|
+
flow: r.flow,
|
|
99
|
+
headline: `⚠ ${workspace}: ${r.flow} has been running ${Math.round(mins)} minutes`,
|
|
100
|
+
detail: `Run ${r.id} is past the ${cfg.runMinutes} minutes in alerts: run_minutes — still going, maybe stuck. Stop it from its page if it is.`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (cfg.costSpike !== undefined && facts.runs) {
|
|
106
|
+
// A run that finished in the last day, against the runs of its flow that
|
|
107
|
+
// finished before it in the week: once per run.
|
|
108
|
+
const done = facts.runs.filter((r) => r.finishedAt).sort((a, b) => a.finishedAt.localeCompare(b.finishedAt));
|
|
109
|
+
for (const r of done) {
|
|
110
|
+
if (now - Date.parse(r.finishedAt) > 86_400_000)
|
|
111
|
+
continue;
|
|
112
|
+
const before = done.filter((o) => o.flow === r.flow && o.id !== r.id && o.finishedAt < r.finishedAt);
|
|
113
|
+
if (before.length < SPIKE_MIN_RUNS)
|
|
114
|
+
continue;
|
|
115
|
+
const mean = before.reduce((n, o) => n + o.costUsd, 0) / before.length;
|
|
116
|
+
if (mean <= 0 || r.costUsd < cfg.costSpike * mean)
|
|
117
|
+
continue;
|
|
118
|
+
out.push({
|
|
119
|
+
key: `cost_spike/${r.id}`,
|
|
120
|
+
runId: r.id,
|
|
121
|
+
flow: r.flow,
|
|
122
|
+
headline: `${workspace}: a ${r.flow} run cost ${(r.costUsd / mean).toFixed(1)}× its usual`,
|
|
123
|
+
detail: `Run ${r.id} cost $${r.costUsd.toFixed(2)} against an average of $${mean.toFixed(2)} over its last ${before.length} runs this week — past the ${cfg.costSpike}× in alerts: cost_spike. Its page shows which step spent it.`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (cfg.runsPerHour !== undefined && facts.runs) {
|
|
128
|
+
const hour = facts.runs.filter((r) => now - Date.parse(r.startedAt) <= 3_600_000);
|
|
129
|
+
const byFlow = new Map();
|
|
130
|
+
for (const r of hour)
|
|
131
|
+
byFlow.set(r.flow, (byFlow.get(r.flow) ?? 0) + 1);
|
|
132
|
+
for (const [flow, n] of byFlow) {
|
|
133
|
+
if (n < cfg.runsPerHour)
|
|
134
|
+
continue;
|
|
135
|
+
out.push({
|
|
136
|
+
key: `runs_per_hour/${flow}`,
|
|
137
|
+
flow,
|
|
138
|
+
headline: `${workspace}: ${flow} started ${n} times in an hour`,
|
|
139
|
+
detail: `Past the ${cfg.runsPerHour} in alerts: runs_per_hour — a loop, a webhook sender retrying, or a schedule written wrong. Its Triggers page shows what started each; throttle: or debounce: caps it, and Settings → Pause stops it now.`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (cfg.staleDays !== undefined) {
|
|
144
|
+
for (const a of facts.agents ?? []) {
|
|
145
|
+
const from = Date.parse(a.lastRunAt ?? a.since);
|
|
146
|
+
if (!Number.isFinite(from))
|
|
147
|
+
continue;
|
|
148
|
+
const days = Math.floor((now - from) / 86_400_000);
|
|
149
|
+
if (days < cfg.staleDays)
|
|
150
|
+
continue;
|
|
151
|
+
out.push({
|
|
152
|
+
// Keyed by the run it last made, so a run and a new idle spell are news again.
|
|
153
|
+
key: `stale_days/${a.name}/${a.lastRunAt ?? "never"}`,
|
|
154
|
+
once: true,
|
|
155
|
+
headline: `${workspace}: agent ${a.name} has not run in ${days} days`,
|
|
156
|
+
detail: `${a.lastRunAt ? `Its last step started ${a.lastRunAt.slice(0, 10)}` : "No run kept on record has used it"} — past the ${cfg.staleDays} days in alerts: stale_days. If nothing needs it any more, retire it: delete agents/${a.name}/ and redeploy, so it holds no tools or secrets nobody watches.`,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
package/dist/src/browser-pod.js
CHANGED
|
@@ -169,6 +169,10 @@ export function classifyCall(tool, input, ctx = {}) {
|
|
|
169
169
|
return { write: false };
|
|
170
170
|
if (server === "foldrun_ask")
|
|
171
171
|
return write("a question to a person");
|
|
172
|
+
// tools: [peers] — reading the roster changes nothing; a message to a
|
|
173
|
+
// colleague is said, and a re-run would say it twice.
|
|
174
|
+
if (server === "foldrun_peers")
|
|
175
|
+
return name === "peers_list" ? { write: false } : write("a message to a colleague");
|
|
172
176
|
if (server === "foldrun_scripts") {
|
|
173
177
|
if (name !== "web")
|
|
174
178
|
return write(`script ${name}`);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CallIo } from "./step-exec.ts";
|
|
2
|
+
/** How much past the cap a step sends, so masking sees a secret whole
|
|
3
|
+
* before the cut (a value longer than this is a file secret, which is also
|
|
4
|
+
* masked line by line). */
|
|
5
|
+
export declare const CAPTURE_SLACK = 4096;
|
|
6
|
+
/**
|
|
7
|
+
* What the step's scrub replaces: every secret value it holds → its name,
|
|
8
|
+
* longest first. An `@file` secret (a key, a certificate) by its content, not
|
|
9
|
+
* its stored form; a multi-line value also line by line, because a file is
|
|
10
|
+
* read back a line at a time (the Read tool numbers each); and every value
|
|
11
|
+
* also as it appears inside JSON, where `"` `\` and newlines are escaped — a
|
|
12
|
+
* tool's input is JSON. Values under 8 characters are skipped: a two-letter
|
|
13
|
+
* secret would redact half the language, and anything that short is not one.
|
|
14
|
+
*/
|
|
15
|
+
export declare function redactionsFor(secrets: Record<string, string | undefined>): [string, string][];
|
|
16
|
+
/** Masked, then cut to the cap. */
|
|
17
|
+
export declare function maskCall(text: string, redactSecrets: (text: string) => string): {
|
|
18
|
+
text: string;
|
|
19
|
+
cut: boolean;
|
|
20
|
+
};
|
|
21
|
+
export declare function recordCallIo(workspaceRoot: string, runId: string, callId: string, tool: string, io: CallIo,
|
|
22
|
+
/** The step's own scrub: its resolved secret values → `[redacted:NAME]`. */
|
|
23
|
+
redactSecrets: (text: string) => string): boolean;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// A tool call's content, from a step's event to the file beside its run
|
|
2
|
+
// (FOL-54 step 4), masked twice: the step's own secrets by value (the
|
|
3
|
+
// runner's redact, from redactionsFor), then anything shaped like a
|
|
4
|
+
// credential or a private key — a key the agent read from a file or a page
|
|
5
|
+
// is not in the vault, and is still a key. Masked first, cut second, so a
|
|
6
|
+
// secret on the cap's edge is never kept half. Recording never fails the step.
|
|
7
|
+
import { redactText } from "./scan.js";
|
|
8
|
+
import { writeCapturedCall, CAPTURE_CAP } from "./store.js";
|
|
9
|
+
import { isFileValue, fileContent } from "./secrets.js";
|
|
10
|
+
/** How much past the cap a step sends, so masking sees a secret whole
|
|
11
|
+
* before the cut (a value longer than this is a file secret, which is also
|
|
12
|
+
* masked line by line). */
|
|
13
|
+
export const CAPTURE_SLACK = 4096;
|
|
14
|
+
/**
|
|
15
|
+
* What the step's scrub replaces: every secret value it holds → its name,
|
|
16
|
+
* longest first. An `@file` secret (a key, a certificate) by its content, not
|
|
17
|
+
* its stored form; a multi-line value also line by line, because a file is
|
|
18
|
+
* read back a line at a time (the Read tool numbers each); and every value
|
|
19
|
+
* also as it appears inside JSON, where `"` `\` and newlines are escaped — a
|
|
20
|
+
* tool's input is JSON. Values under 8 characters are skipped: a two-letter
|
|
21
|
+
* secret would redact half the language, and anything that short is not one.
|
|
22
|
+
*/
|
|
23
|
+
export function redactionsFor(secrets) {
|
|
24
|
+
const out = new Map();
|
|
25
|
+
const add = (v, name) => {
|
|
26
|
+
if (v.length >= 8 && !out.has(v))
|
|
27
|
+
out.set(v, name);
|
|
28
|
+
};
|
|
29
|
+
for (const [name, raw] of Object.entries(secrets)) {
|
|
30
|
+
if (typeof raw !== "string")
|
|
31
|
+
continue;
|
|
32
|
+
for (const v of isFileValue(raw) ? [raw, fileContent(raw)] : [raw]) {
|
|
33
|
+
add(v, name);
|
|
34
|
+
add(v.trim(), name);
|
|
35
|
+
const escaped = JSON.stringify(v).slice(1, -1);
|
|
36
|
+
if (escaped !== v)
|
|
37
|
+
add(escaped, name);
|
|
38
|
+
if (/\r?\n/.test(v.trim())) {
|
|
39
|
+
for (const line of v.split(/\r?\n/)) {
|
|
40
|
+
const l = line.trim();
|
|
41
|
+
if (l.length >= 16)
|
|
42
|
+
add(l, name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [...out.entries()].sort((a, b) => b[0].length - a[0].length);
|
|
48
|
+
}
|
|
49
|
+
// A private key's whole block — scan.ts names a key header and leaves the
|
|
50
|
+
// body, which is right for a finding and wrong for a stored output.
|
|
51
|
+
// Unterminated too: a key cut short, or printed without its END line, is
|
|
52
|
+
// masked to the end of the text.
|
|
53
|
+
const PRIVATE_KEY = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$(?![\s\S]))/g;
|
|
54
|
+
/** Masked, then cut to the cap. */
|
|
55
|
+
export function maskCall(text, redactSecrets) {
|
|
56
|
+
const masked = redactText(redactSecrets(text)).replace(PRIVATE_KEY, "[redacted:private key]");
|
|
57
|
+
return masked.length > CAPTURE_CAP ? { text: masked.slice(0, CAPTURE_CAP), cut: true } : { text: masked, cut: false };
|
|
58
|
+
}
|
|
59
|
+
export function recordCallIo(workspaceRoot, runId, callId, tool, io,
|
|
60
|
+
/** The step's own scrub: its resolved secret values → `[redacted:NAME]`. */
|
|
61
|
+
redactSecrets) {
|
|
62
|
+
try {
|
|
63
|
+
const input = io.input === undefined ? null : maskCall(io.input, redactSecrets);
|
|
64
|
+
const output = io.output === undefined ? null : maskCall(io.output, redactSecrets);
|
|
65
|
+
writeCapturedCall(workspaceRoot, runId, {
|
|
66
|
+
id: callId,
|
|
67
|
+
tool,
|
|
68
|
+
// Which part was cut, each on its own: in the sandbox (the whole was
|
|
69
|
+
// longer than what was sent) or here, after masking.
|
|
70
|
+
...(input ? { input: input.text, ...(io.inputChars !== undefined ? { inputChars: io.inputChars } : {}), ...(input.cut || (io.inputChars ?? 0) > io.input.length ? { inputCut: true } : {}) } : {}),
|
|
71
|
+
...(output ? { output: output.text, ...(io.outputChars !== undefined ? { outputChars: io.outputChars } : {}), ...(output.cut || (io.outputChars ?? 0) > io.output.length ? { outputCut: true } : {}) } : {}),
|
|
72
|
+
...(io.truncated || input?.cut || output?.cut ? { truncated: true } : {}),
|
|
73
|
+
});
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`[capture] ${tool} (${callId}) for ${runId}:`, err instanceof Error ? err.message : err);
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { RunRecord } from "./store.ts";
|
|
2
|
+
export interface StepSide {
|
|
3
|
+
status: string;
|
|
4
|
+
instruction: string;
|
|
5
|
+
model?: string;
|
|
6
|
+
secs: number | null;
|
|
7
|
+
tokensIn: number;
|
|
8
|
+
tokensOut: number;
|
|
9
|
+
modelCalls: number;
|
|
10
|
+
/** Tool name → how many calls. */
|
|
11
|
+
tools: Record<string, number>;
|
|
12
|
+
toolErrors: number;
|
|
13
|
+
/** The first errors it wrote, each one line. */
|
|
14
|
+
errors: string[];
|
|
15
|
+
/** The first line of what it concluded. */
|
|
16
|
+
said: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface StepDiff {
|
|
19
|
+
/** 1-based, as the trace numbers steps. */
|
|
20
|
+
step: number;
|
|
21
|
+
agent: string;
|
|
22
|
+
base: StepSide | null;
|
|
23
|
+
run: StepSide | null;
|
|
24
|
+
/** What differs, in words, most telling first; [] when nothing does. */
|
|
25
|
+
changes: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface RunComparison {
|
|
28
|
+
base: {
|
|
29
|
+
id: string;
|
|
30
|
+
status: string;
|
|
31
|
+
startedAt: string;
|
|
32
|
+
revision: string | null;
|
|
33
|
+
};
|
|
34
|
+
run: {
|
|
35
|
+
id: string;
|
|
36
|
+
status: string;
|
|
37
|
+
startedAt: string;
|
|
38
|
+
revision: string | null;
|
|
39
|
+
};
|
|
40
|
+
revisionChanged: boolean;
|
|
41
|
+
/** Files changed between the two revisions; null when that cannot be said. */
|
|
42
|
+
changedFiles: string[] | null;
|
|
43
|
+
steps: StepDiff[];
|
|
44
|
+
}
|
|
45
|
+
export declare function compareRuns(base: RunRecord, run: RunRecord, changedFiles?: string[] | null): RunComparison;
|
|
46
|
+
/** The run to compare a run with when none is named: the newest earlier run
|
|
47
|
+
* of the same flow that finished well (completed, not BLOCKED or BAD), or
|
|
48
|
+
* null. `outcomeOf` is the caller's, so this stays free of the verdict reader. */
|
|
49
|
+
export declare function lastGoodBefore<R extends {
|
|
50
|
+
id: string;
|
|
51
|
+
flow: string;
|
|
52
|
+
startedAt: string;
|
|
53
|
+
}>(runs: R[], run: {
|
|
54
|
+
id: string;
|
|
55
|
+
flow: string;
|
|
56
|
+
startedAt: string;
|
|
57
|
+
}, good: (r: R) => boolean): R | null;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Two runs of one flow side by side (FOL-72 step 6): "why did this get
|
|
2
|
+
// worse" read off the records — which files changed between the revisions
|
|
3
|
+
// they ran, and step by step what differs: status, the instruction, the
|
|
4
|
+
// model, time, tokens, model calls, the tools it used and the errors it hit.
|
|
5
|
+
// Pure; the caller hands in the changed files (history.ts changedPathsBetween).
|
|
6
|
+
const firstLine = (s, n = 200) => {
|
|
7
|
+
const l = (s ?? "").split("\n").map((x) => x.trim()).find(Boolean);
|
|
8
|
+
return l ? l.slice(0, n) : null;
|
|
9
|
+
};
|
|
10
|
+
function side(s) {
|
|
11
|
+
const tools = {};
|
|
12
|
+
let toolErrors = 0;
|
|
13
|
+
for (const e of s.events) {
|
|
14
|
+
if (e.type !== "tool")
|
|
15
|
+
continue;
|
|
16
|
+
// A pair is one call: count the call, and its completion's error.
|
|
17
|
+
if (e.ms == null)
|
|
18
|
+
tools[e.text] = (tools[e.text] ?? 0) + 1;
|
|
19
|
+
else if (e.err)
|
|
20
|
+
toolErrors++;
|
|
21
|
+
}
|
|
22
|
+
const start = s.startedAt ? Date.parse(s.startedAt) : NaN;
|
|
23
|
+
const end = s.finishedAt ? Date.parse(s.finishedAt) : NaN;
|
|
24
|
+
return {
|
|
25
|
+
status: s.status,
|
|
26
|
+
instruction: s.instruction,
|
|
27
|
+
...(s.model ? { model: s.model } : {}),
|
|
28
|
+
secs: Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, Math.round((end - start) / 1000)) : null,
|
|
29
|
+
tokensIn: s.tokens?.input ?? 0,
|
|
30
|
+
tokensOut: s.tokens?.output ?? 0,
|
|
31
|
+
modelCalls: s.calls?.length ?? s.turns ?? 0,
|
|
32
|
+
tools,
|
|
33
|
+
toolErrors,
|
|
34
|
+
errors: s.events.filter((e) => e.type === "error").slice(0, 3).map((e) => firstLine(e.text) ?? ""),
|
|
35
|
+
said: firstLine(s.conclusion ?? s.result),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const k = (n) => (n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n));
|
|
39
|
+
/** Much more or much less: by a quarter and by a real amount. */
|
|
40
|
+
const moved = (a, b, floor) => Math.abs(b - a) >= floor && (a === 0 ? b > 0 : Math.abs(b - a) / a >= 0.25);
|
|
41
|
+
function changes(a, b) {
|
|
42
|
+
if (!a && b)
|
|
43
|
+
return ["new step: not in the base run"];
|
|
44
|
+
if (a && !b)
|
|
45
|
+
return ["not in this run"];
|
|
46
|
+
if (!a || !b)
|
|
47
|
+
return [];
|
|
48
|
+
const out = [];
|
|
49
|
+
if (a.status !== b.status)
|
|
50
|
+
out.push(`status: ${a.status} → ${b.status}`);
|
|
51
|
+
const newErrors = b.errors.filter((e) => !a.errors.includes(e));
|
|
52
|
+
if (newErrors.length)
|
|
53
|
+
out.push(`new error: ${newErrors[0]}`);
|
|
54
|
+
if (a.instruction.trim() !== b.instruction.trim())
|
|
55
|
+
out.push("instruction changed");
|
|
56
|
+
if ((a.model ?? "") !== (b.model ?? ""))
|
|
57
|
+
out.push(`model: ${a.model ?? "default"} → ${b.model ?? "default"}`);
|
|
58
|
+
if (b.toolErrors > a.toolErrors)
|
|
59
|
+
out.push(`tool errors: ${a.toolErrors} → ${b.toolErrors}`);
|
|
60
|
+
const names = new Set([...Object.keys(a.tools), ...Object.keys(b.tools)]);
|
|
61
|
+
const toolDiff = [...names]
|
|
62
|
+
.map((n) => ({ n, d: (b.tools[n] ?? 0) - (a.tools[n] ?? 0) }))
|
|
63
|
+
.filter((x) => x.d !== 0)
|
|
64
|
+
.sort((x, y) => Math.abs(y.d) - Math.abs(x.d))
|
|
65
|
+
.slice(0, 4)
|
|
66
|
+
.map((x) => `${x.d > 0 ? "+" : "−"}${x.n} ×${Math.abs(x.d)}`);
|
|
67
|
+
if (toolDiff.length)
|
|
68
|
+
out.push(`tools: ${toolDiff.join(", ")}`);
|
|
69
|
+
if (a.secs !== null && b.secs !== null && moved(a.secs, b.secs, 10))
|
|
70
|
+
out.push(`took ${a.secs}s → ${b.secs}s`);
|
|
71
|
+
if (moved(a.tokensIn, b.tokensIn, 5000))
|
|
72
|
+
out.push(`tokens in ${k(a.tokensIn)} → ${k(b.tokensIn)}`);
|
|
73
|
+
if (moved(a.modelCalls, b.modelCalls, 3))
|
|
74
|
+
out.push(`model calls ${a.modelCalls} → ${b.modelCalls}`);
|
|
75
|
+
if (a.said !== b.said && b.said)
|
|
76
|
+
out.push(`said: ${b.said}`);
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** Steps lined up: the flow's own steps by position where the agents agree,
|
|
80
|
+
* fan-out instances by agent and order. */
|
|
81
|
+
function align(a, b) {
|
|
82
|
+
const pairs = [];
|
|
83
|
+
const used = new Set();
|
|
84
|
+
b.forEach((s, i) => {
|
|
85
|
+
let j = a[i] && a[i].agent === s.agent && !used.has(i) ? i : -1;
|
|
86
|
+
if (j < 0)
|
|
87
|
+
j = a.findIndex((x, m) => !used.has(m) && x.agent === s.agent);
|
|
88
|
+
if (j >= 0)
|
|
89
|
+
used.add(j);
|
|
90
|
+
pairs.push([j >= 0 ? a[j] : null, s]);
|
|
91
|
+
});
|
|
92
|
+
a.forEach((s, m) => !used.has(m) && pairs.push([s, null]));
|
|
93
|
+
return pairs;
|
|
94
|
+
}
|
|
95
|
+
export function compareRuns(base, run, changedFiles = null) {
|
|
96
|
+
const head = (r) => ({ id: r.id, status: r.status, startedAt: r.startedAt, revision: r.revision ?? null });
|
|
97
|
+
return {
|
|
98
|
+
base: head(base),
|
|
99
|
+
run: head(run),
|
|
100
|
+
revisionChanged: (base.revision ?? null) !== (run.revision ?? null),
|
|
101
|
+
changedFiles,
|
|
102
|
+
steps: align(base.steps, run.steps).map(([a, b], i) => {
|
|
103
|
+
const sa = a ? side(a) : null;
|
|
104
|
+
const sb = b ? side(b) : null;
|
|
105
|
+
return { step: b ? run.steps.indexOf(b) + 1 : i + 1, agent: (b ?? a).agent, base: sa, run: sb, changes: changes(sa, sb) };
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** The run to compare a run with when none is named: the newest earlier run
|
|
110
|
+
* of the same flow that finished well (completed, not BLOCKED or BAD), or
|
|
111
|
+
* null. `outcomeOf` is the caller's, so this stays free of the verdict reader. */
|
|
112
|
+
export function lastGoodBefore(runs, run, good) {
|
|
113
|
+
return (runs
|
|
114
|
+
.filter((r) => r.flow === run.flow && r.id !== run.id && r.startedAt < run.startedAt && good(r))
|
|
115
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))[0] ?? null);
|
|
116
|
+
}
|
package/dist/src/completions.js
CHANGED
|
@@ -37,6 +37,7 @@ const TOOL_GROUPS = [
|
|
|
37
37
|
{ label: "code", hint: "Bash — runs anything in the sandbox" },
|
|
38
38
|
{ label: "web", hint: "search, fetch, browse, crawl, map, extract, answer, monitor — providers under web:" },
|
|
39
39
|
{ label: "ask", hint: "ask_person — ask the person running the desk mid-step, and wait for the answer" },
|
|
40
|
+
{ label: "peers", hint: "peers_list + peers_message — the colleagues running beside it in its parallel group, and a message to one of them" },
|
|
40
41
|
{ label: "install", hint: "install apt, pip or npm packages mid-step — every agent has it; disallowedTools: [install] removes it" },
|
|
41
42
|
];
|
|
42
43
|
const SDK_TOOL_NAMES = [
|
package/dist/src/deploy.d.ts
CHANGED
|
@@ -47,7 +47,27 @@ export interface DeployResult extends DeployPlan {
|
|
|
47
47
|
* other things is the normal case, not an error.
|
|
48
48
|
*/
|
|
49
49
|
export declare const SUBAGENT_IMPORT_DIRS: readonly [".agents/agents", ".claude/agents"];
|
|
50
|
-
|
|
50
|
+
/** Bounds for a reader that only looks at the text (the checks, the
|
|
51
|
+
* assistant): on 20 Oct 2026 the daily rescan read one desk's 1.2 GB of
|
|
52
|
+
* state/ into a 2 GB heap and crash-looped the worker. A deploy sets none. */
|
|
53
|
+
export interface ReadTreeOptions {
|
|
54
|
+
/** Data (DATA_PATH) larger than this is left out, unread. */
|
|
55
|
+
maxDataFileBytes?: number;
|
|
56
|
+
/** Data past this many bytes in all, or this many files, is left out. */
|
|
57
|
+
maxDataBytes?: number;
|
|
58
|
+
maxDataFiles?: number;
|
|
59
|
+
/** Everything else past this many bytes in all: readTree throws
|
|
60
|
+
* TreeTooLarge rather than return a part of the definitions, which a
|
|
61
|
+
* check would read as missing agents and flows. */
|
|
62
|
+
maxSourceBytes?: number;
|
|
63
|
+
/** Each data file left out, and its size. */
|
|
64
|
+
onSkip?: (path: string, bytes: number) => void;
|
|
65
|
+
}
|
|
66
|
+
export declare class TreeTooLarge extends Error {
|
|
67
|
+
/** What an HTTP route answers with it. */
|
|
68
|
+
readonly status = 413;
|
|
69
|
+
}
|
|
70
|
+
export declare function readTree(dir: string, opts?: ReadTreeOptions): DeployFile[];
|
|
51
71
|
/**
|
|
52
72
|
* Everything wrong with an incoming workspace, before any of it is live.
|
|
53
73
|
*
|
|
@@ -77,6 +97,21 @@ export declare function unopenedFrontmatter(raw: string): string | null;
|
|
|
77
97
|
* advice (an agent that could leak, an unpinned package) is for `foldrun
|
|
78
98
|
* check` and the Checks page, not for every deploy's output.
|
|
79
99
|
*/
|
|
100
|
+
/**
|
|
101
|
+
* What `deploy_block:` in AGENTS.md refuses (FOL-58). `none`, the default:
|
|
102
|
+
* the scanner's findings are warnings and the deploy goes ahead, as it always
|
|
103
|
+
* did. `secrets`: a key or password written into a file is refused.
|
|
104
|
+
* `security`: every security finding is — secrets, hidden or injected text,
|
|
105
|
+
* the lethal trifecta, pipe-to-shell, an unpinned MCP server, a step that
|
|
106
|
+
* acts outside with no check or approval. A
|
|
107
|
+
* `foldrun-ignore` with a reason still accepts one.
|
|
108
|
+
*/
|
|
109
|
+
export declare const DEPLOY_BLOCK_MODES: readonly ["none", "secrets", "security"];
|
|
110
|
+
export type DeployBlockMode = (typeof DEPLOY_BLOCK_MODES)[number];
|
|
111
|
+
/** The workspace's own AGENTS.md, else the account's, else none. */
|
|
112
|
+
export declare function deployBlockMode(files: DeployFile[], accountKeys: Record<string, unknown>): DeployBlockMode;
|
|
113
|
+
/** The findings `mode` turns into refusals, as deploy issues. */
|
|
114
|
+
export declare function deployBlockers(files: DeployFile[], mode: DeployBlockMode, options?: ScanOptions): DeployIssue[];
|
|
80
115
|
export declare function deployWarnings(files: DeployFile[]): DeployIssue[];
|
|
81
116
|
/**
|
|
82
117
|
* Everything the platform can say about a workspace's files, as findings
|