@oh-my-pi/pi-coding-agent 15.1.2 → 15.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/dist/types/async/job-manager.d.ts +3 -2
- package/dist/types/cli/auth-broker-cli.d.ts +25 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +18 -0
- package/dist/types/cli/grievances-cli.d.ts +12 -0
- package/dist/types/commands/auth-broker.d.ts +54 -0
- package/dist/types/commands/auth-gateway.d.ts +32 -0
- package/dist/types/commands/grievances.d.ts +1 -1
- package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -1
- package/dist/types/commit/agentic/tools/schemas.d.ts +9 -1
- package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -1
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +46 -0
- package/dist/types/discovery/agents.d.ts +12 -1
- package/dist/types/edit/renderer.d.ts +3 -0
- package/dist/types/eval/index.d.ts +0 -2
- package/dist/types/goals/tools/goal-tool.d.ts +10 -2
- package/dist/types/index.d.ts +0 -1
- package/dist/types/internal-urls/index.d.ts +1 -1
- package/dist/types/internal-urls/{pi-protocol.d.ts → omp-protocol.d.ts} +3 -3
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/main.d.ts +11 -2
- package/dist/types/modes/acp/acp-agent.d.ts +2 -1
- package/dist/types/modes/acp/acp-event-mapper.d.ts +13 -1
- package/dist/types/modes/acp/acp-mode.d.ts +3 -1
- package/dist/types/modes/emoji-autocomplete.d.ts +16 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
- package/dist/types/plan-mode/approved-plan.d.ts +10 -4
- package/dist/types/sdk.d.ts +10 -3
- package/dist/types/session/agent-session.d.ts +7 -3
- package/dist/types/session/auth-broker-config.d.ts +13 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/client-bridge.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +41 -7
- package/dist/types/tools/irc.d.ts +8 -2
- package/dist/types/tools/report-tool-issue.d.ts +118 -1
- package/dist/types/tools/resolve.d.ts +8 -2
- package/examples/custom-tools/README.md +3 -12
- package/examples/extensions/README.md +2 -15
- package/examples/extensions/api-demo.ts +1 -7
- package/package.json +7 -7
- package/src/async/job-manager.ts +111 -13
- package/src/autoresearch/tools/init-experiment.ts +11 -33
- package/src/autoresearch/tools/log-experiment.ts +10 -24
- package/src/autoresearch/tools/run-experiment.ts +1 -1
- package/src/autoresearch/tools/update-notes.ts +2 -9
- package/src/cli/auth-broker-cli.ts +746 -0
- package/src/cli/auth-gateway-cli.ts +342 -0
- package/src/cli/grievances-cli.ts +109 -16
- package/src/cli/update-cli.ts +1 -5
- package/src/cli.ts +4 -2
- package/src/commands/auth-broker.ts +96 -0
- package/src/commands/auth-gateway.ts +61 -0
- package/src/commands/grievances.ts +13 -8
- package/src/commands/launch.ts +1 -1
- package/src/commit/agentic/agent.ts +2 -0
- package/src/commit/agentic/tools/analyze-file.ts +2 -2
- package/src/commit/agentic/tools/git-file-diff.ts +2 -2
- package/src/commit/agentic/tools/git-hunk.ts +3 -3
- package/src/commit/agentic/tools/git-overview.ts +2 -2
- package/src/commit/agentic/tools/propose-changelog.ts +1 -3
- package/src/commit/agentic/tools/recent-commits.ts +1 -1
- package/src/commit/agentic/tools/schemas.ts +1 -9
- package/src/config/model-equivalence.ts +279 -174
- package/src/config/model-registry.ts +37 -6
- package/src/config/model-resolver.ts +13 -8
- package/src/config/models-config-schema.ts +8 -0
- package/src/config/settings-schema.ts +52 -0
- package/src/cursor.ts +1 -1
- package/src/debug/log-formatting.ts +1 -1
- package/src/debug/log-viewer.ts +1 -1
- package/src/debug/profiler.ts +4 -0
- package/src/debug/raw-sse-buffer.ts +100 -59
- package/src/debug/raw-sse.ts +1 -1
- package/src/discovery/agents.ts +15 -4
- package/src/edit/modes/apply-patch.ts +1 -5
- package/src/edit/modes/patch.ts +5 -5
- package/src/edit/modes/replace.ts +5 -5
- package/src/edit/renderer.ts +2 -1
- package/src/edit/streaming.ts +1 -1
- package/src/eval/index.ts +0 -2
- package/src/eval/js/shared/runtime.ts +107 -2
- package/src/eval/py/kernel.ts +1 -1
- package/src/exa/researcher.ts +4 -4
- package/src/exa/search.ts +10 -22
- package/src/exa/websets.ts +33 -33
- package/src/extensibility/typebox.ts +44 -17
- package/src/goals/tools/goal-tool.ts +3 -3
- package/src/index.ts +0 -3
- package/src/internal-urls/docs-index.generated.ts +21 -18
- package/src/internal-urls/index.ts +1 -1
- package/src/internal-urls/{pi-protocol.ts → omp-protocol.ts} +10 -10
- package/src/internal-urls/router.ts +3 -3
- package/src/internal-urls/types.ts +1 -1
- package/src/lsp/types.ts +8 -11
- package/src/main.ts +216 -146
- package/src/mcp/tool-bridge.ts +3 -3
- package/src/modes/acp/acp-agent.ts +203 -57
- package/src/modes/acp/acp-client-bridge.ts +2 -1
- package/src/modes/acp/acp-event-mapper.ts +208 -32
- package/src/modes/acp/acp-mode.ts +11 -3
- package/src/modes/components/bash-execution.ts +1 -1
- package/src/modes/components/diff.ts +1 -2
- package/src/modes/components/eval-execution.ts +1 -1
- package/src/modes/components/oauth-selector.ts +38 -2
- package/src/modes/components/tool-execution.ts +1 -2
- package/src/modes/components/tree-selector.ts +26 -7
- package/src/modes/controllers/command-controller.ts +95 -34
- package/src/modes/controllers/input-controller.ts +4 -3
- package/src/modes/data/emojis.json +1 -0
- package/src/modes/emoji-autocomplete.ts +285 -0
- package/src/modes/interactive-mode.ts +92 -19
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/prompt-action-autocomplete.ts +14 -0
- package/src/plan-mode/approved-plan.ts +30 -9
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/ttsr-tool-reminder.md +5 -0
- package/src/prompts/tools/ask.md +4 -3
- package/src/prompts/tools/eval.md +25 -26
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/resolve.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/prompts/tools/web-search.md +1 -1
- package/src/sdk.ts +81 -8
- package/src/session/agent-session.ts +362 -131
- package/src/session/agent-storage.ts +7 -2
- package/src/session/auth-broker-config.ts +102 -0
- package/src/session/auth-storage.ts +7 -1
- package/src/session/client-bridge.ts +3 -0
- package/src/session/streaming-output.ts +1 -1
- package/src/task/types.ts +10 -35
- package/src/tools/bash-interactive.ts +4 -1
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser.ts +12 -20
- package/src/tools/eval.ts +77 -100
- package/src/tools/gh.ts +21 -45
- package/src/tools/hindsight-recall.ts +1 -1
- package/src/tools/hindsight-reflect.ts +2 -2
- package/src/tools/hindsight-retain.ts +3 -7
- package/src/tools/index.ts +8 -1
- package/src/tools/inspect-image.ts +4 -1
- package/src/tools/irc.ts +4 -12
- package/src/tools/job.ts +3 -11
- package/src/tools/report-tool-issue.ts +462 -17
- package/src/tools/resolve.ts +2 -7
- package/src/tools/todo-write.ts +8 -15
- package/src/utils/title-generator.ts +3 -0
- package/src/web/search/index.ts +6 -6
- package/dist/types/eval/parse.d.ts +0 -28
- package/dist/types/eval/sniff.d.ts +0 -11
- package/src/eval/eval.lark +0 -36
- package/src/eval/parse.ts +0 -407
- package/src/eval/sniff.ts +0 -28
|
@@ -1,34 +1,207 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* report_tool_issue — automated QA tool for tracking unexpected tool behavior.
|
|
3
3
|
*
|
|
4
|
-
* Enabled
|
|
4
|
+
* Enabled by default; gated behind PI_AUTO_QA=1 / `dev.autoqa` so a user
|
|
5
|
+
* who flips the setting off short-circuits injection entirely.
|
|
5
6
|
* Always injected into every agent (including subagents) regardless of tool selection.
|
|
6
7
|
* Records grievances to a local SQLite database; never throws.
|
|
8
|
+
*
|
|
9
|
+
* Before the first record lands, the user's consent is checked. If they've
|
|
10
|
+
* never been asked (`dev.autoqa.consent === "unset"`) the process-global
|
|
11
|
+
* consent handler — wired by `InteractiveMode` to a Yes/No popup — is
|
|
12
|
+
* invoked exactly once and the decision is persisted. Subsequent calls
|
|
13
|
+
* (including from subagents) read the cached decision without prompting.
|
|
14
|
+
*
|
|
15
|
+
* When the user grants consent, push is automatically active against the
|
|
16
|
+
* bundled endpoint (`dev.autoqaPush.endpoint`, default `qa.omp.sh`). Each
|
|
17
|
+
* insert schedules a background flush that POSTs pending rows and deletes
|
|
18
|
+
* them on HTTP 2xx. `PI_AUTO_QA_PUSH=1` forces push in non-interactive
|
|
19
|
+
* environments where the consent dialog never fires. Tool execution is
|
|
20
|
+
* never blocked on the network and never throws.
|
|
7
21
|
*/
|
|
8
22
|
import { Database } from "bun:sqlite";
|
|
9
23
|
import path from "node:path";
|
|
10
24
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
11
|
-
import { $flag, getAgentDir, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
25
|
+
import { $env, $flag, getAgentDir, getInstallId, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
12
26
|
import * as z from "zod/v4";
|
|
13
27
|
import type { Settings } from "..";
|
|
14
28
|
import type { ToolSession } from "./index";
|
|
15
29
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
function buildReportToolIssueParams(activeBuiltinNames: readonly string[]) {
|
|
31
|
+
// Enum gives the model a tight schema; the runtime check in `execute` is the
|
|
32
|
+
// source of truth (handles models that ignore the enum and the empty-list
|
|
33
|
+
// fallback used by call sites that don't know the active set yet).
|
|
34
|
+
const toolSchema = activeBuiltinNames.length > 0 ? z.enum(activeBuiltinNames as [string, ...string[]]) : z.string();
|
|
35
|
+
return z.object({
|
|
36
|
+
tool: toolSchema.describe("tool name"),
|
|
37
|
+
report: z
|
|
38
|
+
.string()
|
|
39
|
+
.describe("unexpected behavior; generic, NEVER PII (paths, file contents, identifiers, prompt text)"),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
20
42
|
|
|
21
43
|
export function isAutoQaEnabled(settings?: Settings): boolean {
|
|
22
44
|
return $flag("PI_AUTO_QA") || !!settings?.get("dev.autoqa");
|
|
23
45
|
}
|
|
24
46
|
|
|
47
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// Consent gate
|
|
49
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Resolver for the user's "share grievances?" consent.
|
|
53
|
+
*
|
|
54
|
+
* Return values:
|
|
55
|
+
* - `true` — user agreed; record + ship for this run and persist.
|
|
56
|
+
* - `false` — user declined; suppress for this run and persist.
|
|
57
|
+
* - `null` — user dismissed the dialog (ESC, click-away, …) without
|
|
58
|
+
* picking an option. The decision is NOT cached or persisted,
|
|
59
|
+
* so the next `report_tool_issue` invocation re-prompts.
|
|
60
|
+
*
|
|
61
|
+
* Persistence is the tool's job (so subagent invocations can persist into
|
|
62
|
+
* the disk-backed `Settings` instance the host registered alongside the
|
|
63
|
+
* handler), not the handler's. Implementations live in hosts that have UI
|
|
64
|
+
* affordances — today only `InteractiveMode`. When no handler is
|
|
65
|
+
* registered (CLI subcommands, tests, non-interactive runs) consent
|
|
66
|
+
* defaults to `false` — the explicit "don't collect by default" stance.
|
|
67
|
+
*/
|
|
68
|
+
export type AutoQaConsentHandler = () => Promise<boolean | null>;
|
|
69
|
+
|
|
70
|
+
let consentHandler: AutoQaConsentHandler | null = null;
|
|
71
|
+
/**
|
|
72
|
+
* Persistent settings instance supplied by the consent-handler registrant.
|
|
73
|
+
* Subagents have in-memory `Settings` snapshots that don't write to disk;
|
|
74
|
+
* we persist the decision through this disk-backed reference so a grant
|
|
75
|
+
* survives across runs even when triggered from a subagent tool call.
|
|
76
|
+
*/
|
|
77
|
+
let persistentConsentSettings: Settings | null = null;
|
|
78
|
+
/**
|
|
79
|
+
* Process-global cache of the resolved consent decision. Survives across
|
|
80
|
+
* subagent boundaries (subagents share this module instance), so a grant
|
|
81
|
+
* in the parent applies immediately to children — including children that
|
|
82
|
+
* spawned BEFORE the grant and would otherwise see a stale snapshot of
|
|
83
|
+
* `dev.autoqa.consent` in their isolated `Settings`.
|
|
84
|
+
*
|
|
85
|
+
* `null` = never asked, never cached.
|
|
86
|
+
*/
|
|
87
|
+
let cachedConsent: boolean | null = null;
|
|
88
|
+
/**
|
|
89
|
+
* Single-flight in-flight consent request. While the dialog is open, every
|
|
90
|
+
* concurrent `report_tool_issue` call (main + every subagent) awaits this
|
|
91
|
+
* promise instead of stacking duplicate popups.
|
|
92
|
+
*/
|
|
93
|
+
let consentInFlight: Promise<boolean> | null = null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Register the consent handler and the persistent {@link Settings} instance
|
|
97
|
+
* the decision should be written to. Passing `null` clears the handler
|
|
98
|
+
* (e.g. on `InteractiveMode` teardown). Re-registration is authoritative.
|
|
99
|
+
*/
|
|
100
|
+
export function setAutoQaConsentHandler(
|
|
101
|
+
handler: AutoQaConsentHandler | null,
|
|
102
|
+
persistentSettings: Settings | null = null,
|
|
103
|
+
): void {
|
|
104
|
+
consentHandler = handler;
|
|
105
|
+
persistentConsentSettings = persistentSettings;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Test-only: clear consent cache + handler. Never call from production code. */
|
|
109
|
+
export function __resetAutoQaConsentForTests(): void {
|
|
110
|
+
consentHandler = null;
|
|
111
|
+
persistentConsentSettings = null;
|
|
112
|
+
cachedConsent = null;
|
|
113
|
+
consentInFlight = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readPersistedConsent(settings: Settings | undefined): boolean | null {
|
|
117
|
+
if (!settings) return null;
|
|
118
|
+
const stored = settings.get("dev.autoqa.consent");
|
|
119
|
+
if (stored === "granted") return true;
|
|
120
|
+
if (stored === "denied") return false;
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function persistConsent(localSettings: Settings | undefined, granted: boolean): void {
|
|
125
|
+
const value = granted ? "granted" : "denied";
|
|
126
|
+
// Write on every settings instance we know about. The local one keeps
|
|
127
|
+
// the in-memory snapshot consistent for the current subagent; the
|
|
128
|
+
// persistent one (registered by the host) is what actually lands on disk.
|
|
129
|
+
for (const target of [localSettings, persistentConsentSettings]) {
|
|
130
|
+
if (!target) continue;
|
|
131
|
+
try {
|
|
132
|
+
target.set("dev.autoqa.consent", value);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
logger.debug("autoqa consent persist failed", { error: String(error) });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Resolve the user's consent for `report_tool_issue` grievances.
|
|
141
|
+
*
|
|
142
|
+
* Precedence (highest first):
|
|
143
|
+
* 1. Process-global cache (set on first successful resolution).
|
|
144
|
+
* 2. Persistent setting (`dev.autoqa.consent` on the supplied `Settings`).
|
|
145
|
+
* 3. Persistent setting on the registered host `Settings`.
|
|
146
|
+
* 4. Consent handler popup (single-flight; persists the answer).
|
|
147
|
+
* 5. Default-deny when no handler is registered.
|
|
148
|
+
*
|
|
149
|
+
* Never throws — handler errors degrade to "denied for this call" without
|
|
150
|
+
* caching, so a subsequent invocation can re-prompt instead of being
|
|
151
|
+
* permanently locked into the false branch.
|
|
152
|
+
*/
|
|
153
|
+
export async function resolveAutoQaConsent(settings: Settings | undefined): Promise<boolean> {
|
|
154
|
+
if (cachedConsent !== null) return cachedConsent;
|
|
155
|
+
const persisted = readPersistedConsent(settings) ?? readPersistedConsent(persistentConsentSettings ?? undefined);
|
|
156
|
+
if (persisted !== null) {
|
|
157
|
+
cachedConsent = persisted;
|
|
158
|
+
return persisted;
|
|
159
|
+
}
|
|
160
|
+
if (!consentHandler) return false;
|
|
161
|
+
if (consentInFlight) return consentInFlight;
|
|
162
|
+
const handler = consentHandler;
|
|
163
|
+
consentInFlight = (async () => {
|
|
164
|
+
try {
|
|
165
|
+
const granted = await handler();
|
|
166
|
+
if (granted === null) {
|
|
167
|
+
// User dismissed the dialog (ESC) without picking. Treat as
|
|
168
|
+
// "skip this call" but don't cache or persist — the next
|
|
169
|
+
// invocation gets to re-prompt so a stray ESC isn't a
|
|
170
|
+
// permanent opt-out.
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
cachedConsent = granted;
|
|
174
|
+
persistConsent(settings, granted);
|
|
175
|
+
return granted;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
logger.warn("autoqa consent handler threw", { error: String(error) });
|
|
178
|
+
return false;
|
|
179
|
+
} finally {
|
|
180
|
+
consentInFlight = null;
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
return consentInFlight;
|
|
184
|
+
}
|
|
185
|
+
|
|
25
186
|
export function getAutoQaDbPath(): string {
|
|
26
187
|
return path.join(getAgentDir(), "autoqa.db");
|
|
27
188
|
}
|
|
28
189
|
|
|
29
190
|
let cachedDb: Database | null = null;
|
|
30
191
|
|
|
31
|
-
|
|
192
|
+
/**
|
|
193
|
+
* Open (or return the cached handle for) the auto-QA SQLite database at
|
|
194
|
+
* `~/.omp/agent/autoqa.db`. Idempotently runs schema creation, the
|
|
195
|
+
* `pushed`-column migration, and index setup so every consumer — tool
|
|
196
|
+
* execute path, manual `omp grievances push`, future debug scripts —
|
|
197
|
+
* sees the same prepared schema. Returns `null` only on a hard open
|
|
198
|
+
* failure (filesystem permissions, etc.); a missing file is created.
|
|
199
|
+
*
|
|
200
|
+
* Exported because the `omp grievances` CLI handlers need the migrated
|
|
201
|
+
* handle too — having a second `openDb` in the CLI led to the column
|
|
202
|
+
* never being added on the manual-push path.
|
|
203
|
+
*/
|
|
204
|
+
export function openAutoQaDb(): Database | null {
|
|
32
205
|
if (cachedDb) return cachedDb;
|
|
33
206
|
try {
|
|
34
207
|
const db = new Database(getAutoQaDbPath());
|
|
@@ -41,9 +214,22 @@ function openDb(): Database | null {
|
|
|
41
214
|
model TEXT NOT NULL,
|
|
42
215
|
version TEXT NOT NULL,
|
|
43
216
|
tool TEXT NOT NULL,
|
|
44
|
-
report TEXT NOT NULL
|
|
217
|
+
report TEXT NOT NULL,
|
|
218
|
+
pushed INTEGER NOT NULL DEFAULT 0
|
|
45
219
|
);
|
|
46
220
|
`);
|
|
221
|
+
// Migration: pre-`pushed` databases get the column tacked on. Existing
|
|
222
|
+
// rows default to `0` (unpushed), so legacy grievances from before the
|
|
223
|
+
// consent + push pipeline went live get swept up by the next flush —
|
|
224
|
+
// exactly the behaviour we want for users who just granted consent.
|
|
225
|
+
const cols = db.prepare("PRAGMA table_info(grievances)").all() as Array<{ name: string }>;
|
|
226
|
+
if (!cols.some(c => c.name === "pushed")) {
|
|
227
|
+
db.run("ALTER TABLE grievances ADD COLUMN pushed INTEGER NOT NULL DEFAULT 0");
|
|
228
|
+
}
|
|
229
|
+
// Speed up the per-batch `WHERE pushed = 0` scan that drives the flush
|
|
230
|
+
// loop. Without the index every batch becomes a full table scan once
|
|
231
|
+
// pushed rows dominate the table.
|
|
232
|
+
db.run("CREATE INDEX IF NOT EXISTS grievances_pushed_idx ON grievances(pushed, id)");
|
|
47
233
|
cachedDb = db;
|
|
48
234
|
return db;
|
|
49
235
|
} catch {
|
|
@@ -51,26 +237,285 @@ function openDb(): Database | null {
|
|
|
51
237
|
}
|
|
52
238
|
}
|
|
53
239
|
|
|
54
|
-
|
|
240
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
241
|
+
// Backend push
|
|
242
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
export interface FlushResult {
|
|
245
|
+
pushed: number;
|
|
246
|
+
ok: boolean;
|
|
247
|
+
skipped?: boolean;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Optional per-flush controls. Used by `omp grievances push` to surface
|
|
252
|
+
* progress to a TTY and to skip the user-facing consent gate (manual
|
|
253
|
+
* pushes are the user's explicit intent, not a side effect of a tool call).
|
|
254
|
+
*/
|
|
255
|
+
export interface FlushOptions {
|
|
256
|
+
/**
|
|
257
|
+
* Skip the `dev.autoqa.consent === "granted"` gate in
|
|
258
|
+
* {@link resolvePushConfig}. Endpoint configuration is still required.
|
|
259
|
+
* Reserved for explicit user-driven pushes (CLI `grievances push`,
|
|
260
|
+
* future debug recipes); never set from the tool's auto-flush path.
|
|
261
|
+
*/
|
|
262
|
+
bypassConsent?: boolean;
|
|
263
|
+
/**
|
|
264
|
+
* Fires once at the start of the loop with the snapshot count of
|
|
265
|
+
* unpushed rows. Subsequent inserts won't be reflected (the count is
|
|
266
|
+
* a planning hint for progress reporters, not a live total).
|
|
267
|
+
*/
|
|
268
|
+
onStart?: (totalUnpushed: number) => void;
|
|
269
|
+
/**
|
|
270
|
+
* Fires after every successfully shipped batch with the running pushed
|
|
271
|
+
* count. Reporters compare against the `totalUnpushed` they saw in
|
|
272
|
+
* `onStart` to advance their bar.
|
|
273
|
+
*/
|
|
274
|
+
onProgress?: (pushedSoFar: number) => void;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
interface PushConfig {
|
|
278
|
+
endpoint: string;
|
|
279
|
+
token: string | undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const FLUSH_TIMEOUT_MS = 5_000;
|
|
283
|
+
const FAILURE_COOLDOWN_MS = 30_000;
|
|
284
|
+
/**
|
|
285
|
+
* Per-request batch size. The worker loops until no unpushed rows remain,
|
|
286
|
+
* shipping `FLUSH_BATCH_SIZE` rows per POST. Tunes the trade-off between
|
|
287
|
+
* request count and request size — 50 keeps each payload well under the
|
|
288
|
+
* default `maxBody` limit on the autoqa collector while letting a
|
|
289
|
+
* realistic backlog (a few hundred legacy rows on first flush after the
|
|
290
|
+
* consent grant) drain in single-digit requests.
|
|
291
|
+
*/
|
|
292
|
+
const FLUSH_BATCH_SIZE = 50;
|
|
293
|
+
|
|
294
|
+
let inFlightFlush: Promise<FlushResult> | null = null;
|
|
295
|
+
let lastFailureAt = 0;
|
|
296
|
+
|
|
297
|
+
/** Test-only: clear single-flight + cooldown state. Never call from production code. */
|
|
298
|
+
export function __resetAutoQaFlushStateForTests(): void {
|
|
299
|
+
inFlightFlush = null;
|
|
300
|
+
lastFailureAt = 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function envOverrideString(name: string): string | undefined {
|
|
304
|
+
const value = $env[name];
|
|
305
|
+
if (typeof value !== "string") return undefined;
|
|
306
|
+
const trimmed = value.trim();
|
|
307
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function resolvePushConfig(settings: Settings | undefined, bypassConsent: boolean): PushConfig | null {
|
|
311
|
+
if (!isAutoQaEnabled(settings)) return null;
|
|
312
|
+
|
|
313
|
+
// Consent IS the push opt-in for the auto-flush path. `bypassConsent`
|
|
314
|
+
// covers explicit user-driven pushes (`omp grievances push`) where the
|
|
315
|
+
// user clearly intends to ship regardless of dialog state. The
|
|
316
|
+
// `PI_AUTO_QA_PUSH` env flag stays as a CI/headless override too.
|
|
317
|
+
if (!bypassConsent) {
|
|
318
|
+
const consented = settings?.get("dev.autoqa.consent") === "granted";
|
|
319
|
+
if (!consented && !$flag("PI_AUTO_QA_PUSH")) return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const endpoint = envOverrideString("PI_AUTO_QA_PUSH_URL") ?? settings?.get("dev.autoqaPush.endpoint");
|
|
323
|
+
if (!endpoint || endpoint.trim().length === 0) return null;
|
|
324
|
+
|
|
325
|
+
const token = envOverrideString("PI_AUTO_QA_PUSH_TOKEN") ?? settings?.get("dev.autoqaPush.token");
|
|
326
|
+
return { endpoint: endpoint.trim(), token: token && token.length > 0 ? token : undefined };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface GrievanceRow {
|
|
330
|
+
id: number;
|
|
331
|
+
model: string;
|
|
332
|
+
version: string;
|
|
333
|
+
tool: string;
|
|
334
|
+
report: string;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function performFlush(db: Database, config: PushConfig, options: FlushOptions = {}): Promise<FlushResult> {
|
|
338
|
+
const selectStmt = db.prepare(
|
|
339
|
+
"SELECT id, model, version, tool, report FROM grievances WHERE pushed = 0 ORDER BY id ASC LIMIT ?",
|
|
340
|
+
);
|
|
341
|
+
// Planning snapshot — fires once so progress reporters can size their bar.
|
|
342
|
+
// Mid-flight inserts are NOT folded in (the worker drains them too, but
|
|
343
|
+
// the progress bar treats the initial backlog as the denominator).
|
|
344
|
+
if (options.onStart) {
|
|
345
|
+
const totalRow = db.prepare("SELECT COUNT(*) AS n FROM grievances WHERE pushed = 0").get() as { n: number };
|
|
346
|
+
options.onStart(totalRow.n);
|
|
347
|
+
}
|
|
348
|
+
let totalPushed = 0;
|
|
349
|
+
for (;;) {
|
|
350
|
+
const rows = selectStmt.all(FLUSH_BATCH_SIZE) as GrievanceRow[];
|
|
351
|
+
if (rows.length === 0) return { pushed: totalPushed, ok: true };
|
|
352
|
+
|
|
353
|
+
const body = JSON.stringify({
|
|
354
|
+
agent: { name: "omp", version: VERSION },
|
|
355
|
+
installId: getInstallId(),
|
|
356
|
+
// Coarse host fingerprint for triage — `darwin`/`linux`/`win32` +
|
|
357
|
+
// `arm64`/`x64`. Useful for "is this bug arch-specific?" without
|
|
358
|
+
// leaking the user's machine name (the old payload sent
|
|
359
|
+
// `os.hostname()` verbatim, which trivially deanonymises users).
|
|
360
|
+
platform: process.platform,
|
|
361
|
+
arch: process.arch,
|
|
362
|
+
entries: rows,
|
|
363
|
+
});
|
|
364
|
+
const headers: Record<string, string> = { "content-type": "application/json" };
|
|
365
|
+
if (config.token) headers.authorization = `Bearer ${config.token}`;
|
|
366
|
+
|
|
367
|
+
let response: Response;
|
|
368
|
+
try {
|
|
369
|
+
response = await fetch(config.endpoint, {
|
|
370
|
+
method: "POST",
|
|
371
|
+
headers,
|
|
372
|
+
body,
|
|
373
|
+
signal: AbortSignal.timeout(FLUSH_TIMEOUT_MS),
|
|
374
|
+
});
|
|
375
|
+
} catch (error) {
|
|
376
|
+
lastFailureAt = Date.now();
|
|
377
|
+
logger.warn("autoqa push failed", {
|
|
378
|
+
endpoint: config.endpoint,
|
|
379
|
+
error: String(error),
|
|
380
|
+
batchSize: rows.length,
|
|
381
|
+
pushedSoFar: totalPushed,
|
|
382
|
+
});
|
|
383
|
+
return { pushed: totalPushed, ok: false };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!response.ok) {
|
|
387
|
+
lastFailureAt = Date.now();
|
|
388
|
+
logger.warn("autoqa push failed", {
|
|
389
|
+
endpoint: config.endpoint,
|
|
390
|
+
status: response.status,
|
|
391
|
+
batchSize: rows.length,
|
|
392
|
+
pushedSoFar: totalPushed,
|
|
393
|
+
});
|
|
394
|
+
return { pushed: totalPushed, ok: false };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Mark just this batch — never touch ids the SELECT didn't return so a
|
|
398
|
+
// concurrent insert that landed mid-flight isn't claimed-as-shipped on
|
|
399
|
+
// our behalf. `id IN (?, ?, …)` rather than a range so a non-contiguous
|
|
400
|
+
// batch (after partial fills, retries, etc.) still flips exactly what
|
|
401
|
+
// we sent.
|
|
402
|
+
const ids = rows.map(r => r.id);
|
|
403
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
404
|
+
db.prepare(`UPDATE grievances SET pushed = 1 WHERE id IN (${placeholders})`).run(...ids);
|
|
405
|
+
totalPushed += rows.length;
|
|
406
|
+
options.onProgress?.(totalPushed);
|
|
407
|
+
// Loop continues; the next SELECT picks up the next batch (or returns
|
|
408
|
+
// empty, exiting the loop).
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Flush queued grievances to the configured backend.
|
|
414
|
+
*
|
|
415
|
+
* Single-flight: concurrent callers share the in-flight promise. After a
|
|
416
|
+
* failed push, retries are skipped for {@link FAILURE_COOLDOWN_MS} ms.
|
|
417
|
+
* Never throws — all errors are caught and routed to the logger.
|
|
418
|
+
*/
|
|
419
|
+
export async function flushGrievances(
|
|
420
|
+
db?: Database,
|
|
421
|
+
settings?: Settings,
|
|
422
|
+
options: FlushOptions = {},
|
|
423
|
+
): Promise<FlushResult> {
|
|
424
|
+
const config = resolvePushConfig(settings, options.bypassConsent === true);
|
|
425
|
+
if (!config) return { pushed: 0, ok: false, skipped: true };
|
|
426
|
+
|
|
427
|
+
// `bypassConsent` is the user's explicit "ship NOW" intent — skip the
|
|
428
|
+
// 30s cooldown window so they're not stuck looking at "skipped" after a
|
|
429
|
+
// transient failure. Auto-flush calls still cool off.
|
|
430
|
+
const bypass = options.bypassConsent === true;
|
|
431
|
+
if (!bypass && inFlightFlush) return inFlightFlush;
|
|
432
|
+
|
|
433
|
+
if (!bypass && lastFailureAt > 0 && Date.now() - lastFailureAt < FAILURE_COOLDOWN_MS) {
|
|
434
|
+
return { pushed: 0, ok: false, skipped: true };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const handle = db ?? openAutoQaDb();
|
|
438
|
+
if (!handle) return { pushed: 0, ok: false, skipped: true };
|
|
439
|
+
|
|
440
|
+
const promise = (async () => {
|
|
441
|
+
try {
|
|
442
|
+
return await performFlush(handle, config, options);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
lastFailureAt = Date.now();
|
|
445
|
+
logger.warn("autoqa push failed", { endpoint: config.endpoint, error: String(error) });
|
|
446
|
+
return { pushed: 0, ok: false };
|
|
447
|
+
}
|
|
448
|
+
})();
|
|
449
|
+
|
|
450
|
+
if (!bypass) inFlightFlush = promise;
|
|
451
|
+
try {
|
|
452
|
+
return await promise;
|
|
453
|
+
} finally {
|
|
454
|
+
if (!bypass) inFlightFlush = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function createReportToolIssueTool(session: ToolSession, activeBuiltinNames: readonly string[] = []): AgentTool {
|
|
55
459
|
const getModel = () => session.getActiveModelString?.() ?? "unknown";
|
|
460
|
+
// Snapshotted at construction time. The model's enum is built from the same
|
|
461
|
+
// snapshot; mid-session drift (extensions registering later, etc.) is caught
|
|
462
|
+
// by the silent-drop guard below.
|
|
463
|
+
const allowedToolNames = new Set(activeBuiltinNames);
|
|
56
464
|
|
|
57
465
|
return {
|
|
58
466
|
name: "report_tool_issue",
|
|
59
467
|
label: "Report Tool Issue",
|
|
60
468
|
strict: false,
|
|
61
469
|
description: "Report unexpected tool behavior for automated QA tracking.",
|
|
62
|
-
parameters:
|
|
470
|
+
parameters: buildReportToolIssueParams(activeBuiltinNames),
|
|
63
471
|
intent: "omit",
|
|
64
472
|
async execute(_toolCallId, rawParams) {
|
|
473
|
+
// Save is unconditional: the row lives in the user's own SQLite
|
|
474
|
+
// at ~/.omp/agent/autoqa.db regardless of consent — they always
|
|
475
|
+
// own their local data and can inspect or wipe it via `omp grievances`.
|
|
476
|
+
// Consent only gates whether the row is *shipped* to the shared
|
|
477
|
+
// backend; that decision rides on `dev.autoqa.consent` and is
|
|
478
|
+
// enforced inside `flushGrievances` via `resolvePushConfig`.
|
|
65
479
|
try {
|
|
66
480
|
const params = rawParams as { tool: string; report: string };
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
481
|
+
// Some models emit `proxy_<name>` for tools routed through a
|
|
482
|
+
// passthrough wrapper. Strip the prefix before allowlist check so
|
|
483
|
+
// `proxy_read` lands as a report against `read`, not a silent drop.
|
|
484
|
+
const canonicalTool = params.tool.startsWith("proxy_") ? params.tool.slice("proxy_".length) : params.tool;
|
|
485
|
+
// Silently drop reports targeting tools that aren't shipped built-ins
|
|
486
|
+
// (MCP servers, extensions that overrode a built-in name, typos).
|
|
487
|
+
// Not the model's fault — no error, no DB row, just acknowledge.
|
|
488
|
+
// Empty allowlist means the factory was called without a known active
|
|
489
|
+
// set, so behave as before and record everything.
|
|
490
|
+
if (allowedToolNames.size > 0 && !allowedToolNames.has(canonicalTool)) {
|
|
491
|
+
return { content: [{ type: "text", text: "Noted, thanks!" }] };
|
|
492
|
+
}
|
|
493
|
+
const db = openAutoQaDb();
|
|
494
|
+
if (db) {
|
|
495
|
+
db.prepare("INSERT INTO grievances (model, version, tool, report) VALUES (?, ?, ?, ?)").run(
|
|
496
|
+
getModel(),
|
|
497
|
+
VERSION,
|
|
498
|
+
canonicalTool,
|
|
499
|
+
params.report,
|
|
500
|
+
);
|
|
501
|
+
// Fire-and-forget background pipeline:
|
|
502
|
+
// 1. Trigger the consent popup if it hasn't been answered
|
|
503
|
+
// (single-flight inside `resolveAutoQaConsent`; subagents
|
|
504
|
+
// share the same module-level state).
|
|
505
|
+
// 2. Attempt a flush — `resolvePushConfig` no-ops when consent
|
|
506
|
+
// isn't granted, so a "no" leaves the row local for later
|
|
507
|
+
// `omp grievances push` or a future consent change.
|
|
508
|
+
// Tool execution returns immediately; the model never waits
|
|
509
|
+
// on the dialog.
|
|
510
|
+
void (async () => {
|
|
511
|
+
try {
|
|
512
|
+
await resolveAutoQaConsent(session.settings);
|
|
513
|
+
await flushGrievances(db, session.settings);
|
|
514
|
+
} catch (error) {
|
|
515
|
+
logger.debug("autoqa post-insert pipeline failed", { error: String(error) });
|
|
516
|
+
}
|
|
517
|
+
})();
|
|
518
|
+
}
|
|
74
519
|
} catch (error) {
|
|
75
520
|
logger.error("Failed to record tool issue", { error });
|
|
76
521
|
}
|
package/src/tools/resolve.ts
CHANGED
|
@@ -12,14 +12,9 @@ import { replaceTabs } from "./render-utils";
|
|
|
12
12
|
import { ToolError } from "./tool-errors";
|
|
13
13
|
|
|
14
14
|
const resolveSchema = z.object({
|
|
15
|
-
action: z.
|
|
15
|
+
action: z.enum(["apply", "discard"]),
|
|
16
16
|
reason: z.string().describe("reason for action"),
|
|
17
|
-
extra: z
|
|
18
|
-
.record(z.string(), z.unknown())
|
|
19
|
-
.optional()
|
|
20
|
-
.describe(
|
|
21
|
-
'Free-form metadata interpreted by the resolving tool (e.g. plan-mode approval requires `{ title: "<PLAN_TITLE>" }`).',
|
|
22
|
-
),
|
|
17
|
+
extra: z.record(z.string(), z.unknown()).optional().describe("free-form metadata"),
|
|
23
18
|
});
|
|
24
19
|
|
|
25
20
|
type ResolveParams = z.infer<typeof resolveSchema>;
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -49,31 +49,24 @@ const TodoOp = z
|
|
|
49
49
|
.describe("operation to apply");
|
|
50
50
|
|
|
51
51
|
const InitListEntry = z.object({
|
|
52
|
-
phase: z.string().describe("phase name
|
|
53
|
-
items: z
|
|
54
|
-
.array(z.string().describe("task content (5-10 words)"))
|
|
55
|
-
.min(1)
|
|
56
|
-
.describe("tasks for this phase, in execution order; all start as pending"),
|
|
52
|
+
phase: z.string().describe("phase name"),
|
|
53
|
+
items: z.array(z.string().describe("task content")).min(1).describe("tasks for this phase"),
|
|
57
54
|
});
|
|
58
55
|
|
|
59
56
|
const TodoOpEntry = z.object({
|
|
60
57
|
op: TodoOp,
|
|
61
|
-
list: z.array(InitListEntry).optional().describe("phased task list
|
|
62
|
-
task: z.string().optional().describe("task content
|
|
63
|
-
phase: z.string().optional().describe("phase name
|
|
64
|
-
items: z
|
|
65
|
-
|
|
66
|
-
.min(1)
|
|
67
|
-
.optional()
|
|
68
|
-
.describe("tasks to append to `phase` for op=append"),
|
|
69
|
-
text: z.string().optional().describe("note text for op=note (appended with newline)"),
|
|
58
|
+
list: z.array(InitListEntry).optional().describe("phased task list (init)"),
|
|
59
|
+
task: z.string().optional().describe("task content"),
|
|
60
|
+
phase: z.string().optional().describe("phase name"),
|
|
61
|
+
items: z.array(z.string().describe("task content")).min(1).optional().describe("tasks to append"),
|
|
62
|
+
text: z.string().optional().describe("note text"),
|
|
70
63
|
});
|
|
71
64
|
|
|
72
65
|
const todoWriteSchema = z
|
|
73
66
|
.object({
|
|
74
67
|
ops: z.array(TodoOpEntry).min(1).describe("ordered todo operations"),
|
|
75
68
|
})
|
|
76
|
-
.describe("
|
|
69
|
+
.describe("apply ordered todo operations");
|
|
77
70
|
|
|
78
71
|
type TodoWriteParams = z.infer<typeof todoWriteSchema>;
|
|
79
72
|
type TodoOpEntryValue = TodoWriteParams["ops"][number];
|
|
@@ -166,6 +166,7 @@ export function formatSessionTerminalTitle(sessionName: string | undefined, cwd?
|
|
|
166
166
|
* Set the terminal title using OSC 0 (sets both tab and window title). Unsupported terminals ignore it.
|
|
167
167
|
*/
|
|
168
168
|
export function setTerminalTitle(title: string): void {
|
|
169
|
+
if (!process.stdout.isTTY) return;
|
|
169
170
|
process.stdout.write(`\x1b]0;${sanitizeTerminalTitlePart(title) ?? DEFAULT_TERMINAL_TITLE}\x07`);
|
|
170
171
|
}
|
|
171
172
|
|
|
@@ -177,6 +178,7 @@ export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: s
|
|
|
177
178
|
* Save the current terminal title on terminals that support xterm window ops.
|
|
178
179
|
*/
|
|
179
180
|
export function pushTerminalTitle(): void {
|
|
181
|
+
if (!process.stdout.isTTY) return;
|
|
180
182
|
process.stdout.write("\x1b[22;2t");
|
|
181
183
|
}
|
|
182
184
|
|
|
@@ -184,5 +186,6 @@ export function pushTerminalTitle(): void {
|
|
|
184
186
|
* Restore the previously saved terminal title on terminals that support xterm window ops.
|
|
185
187
|
*/
|
|
186
188
|
export function popTerminalTitle(): void {
|
|
189
|
+
if (!process.stdout.isTTY) return;
|
|
187
190
|
process.stdout.write("\x1b[23;2t");
|
|
188
191
|
}
|
package/src/web/search/index.ts
CHANGED
|
@@ -21,12 +21,12 @@ import { SearchProviderError } from "./types";
|
|
|
21
21
|
|
|
22
22
|
/** Web search tool parameters schema */
|
|
23
23
|
export const webSearchSchema = z.object({
|
|
24
|
-
query: z.string().describe("
|
|
25
|
-
recency: z.enum(["day", "week", "month", "year"]).describe("
|
|
26
|
-
limit: z.number().describe("
|
|
27
|
-
max_tokens: z.number().describe("
|
|
28
|
-
temperature: z.number().describe("
|
|
29
|
-
num_search_results: z.number().describe("
|
|
24
|
+
query: z.string().describe("search query"),
|
|
25
|
+
recency: z.enum(["day", "week", "month", "year"]).describe("recency filter").optional(),
|
|
26
|
+
limit: z.number().describe("max results").optional(),
|
|
27
|
+
max_tokens: z.number().describe("max output tokens").optional(),
|
|
28
|
+
temperature: z.number().describe("sampling temperature").optional(),
|
|
29
|
+
num_search_results: z.number().describe("number of search results").optional(),
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
export type SearchToolParams = z.infer<typeof webSearchSchema>;
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { EvalLanguage } from "./types";
|
|
2
|
-
export type EvalLanguageOrigin = "default" | "header";
|
|
3
|
-
export interface ParsedEvalCell {
|
|
4
|
-
index: number;
|
|
5
|
-
title?: string;
|
|
6
|
-
code: string;
|
|
7
|
-
language: EvalLanguage;
|
|
8
|
-
languageOrigin: EvalLanguageOrigin;
|
|
9
|
-
timeoutMs: number;
|
|
10
|
-
reset: boolean;
|
|
11
|
-
}
|
|
12
|
-
export interface ParsedEvalInput {
|
|
13
|
-
cells: ParsedEvalCell[];
|
|
14
|
-
/**
|
|
15
|
-
* True when the parser encountered `*** Abort` (recovery sentinel emitted
|
|
16
|
-
* by the agent loop's harmony-leak mitigation; see
|
|
17
|
-
* `docs/ERRATA-GPT5-HARMONY.md`). The cell containing the marker, if any,
|
|
18
|
-
* is dropped — its body is incomplete and unsafe to execute.
|
|
19
|
-
*/
|
|
20
|
-
aborted?: boolean;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Warning text appended to the eval tool result when parsing terminated on
|
|
24
|
-
* `*** Abort`. Tells the model that earlier cells (if any) ran normally and
|
|
25
|
-
* that any aborted cell needs to be re-issued.
|
|
26
|
-
*/
|
|
27
|
-
export declare const ABORT_WARNING = "Tool stream truncated mid-call due to detected output corruption. Earlier cells (if any) executed normally; their state persists. Re-issue the aborted cell.";
|
|
28
|
-
export declare function parseEvalInput(input: string): ParsedEvalInput;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { EvalLanguage } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
* Best-effort language sniff for cells with no explicit `language`.
|
|
4
|
-
*
|
|
5
|
-
* Order:
|
|
6
|
-
* 1. Shebang on first line (`#!/usr/bin/env python`, `#!/usr/bin/env node`, etc.)
|
|
7
|
-
* 2. Strong syntactic markers unique to one language. Bias false negatives over
|
|
8
|
-
* false positives — anything ambiguous returns `undefined` and the caller
|
|
9
|
-
* falls back to the default-backend rules.
|
|
10
|
-
*/
|
|
11
|
-
export declare function sniffEvalLanguage(code: string): EvalLanguage | undefined;
|