@onepointfour-packs/flaredeck-agent 0.1.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/LICENSE.md +14 -0
- package/README.md +102 -0
- package/dist/agent.d.ts +104 -0
- package/dist/agent.js +329 -0
- package/dist/agent.js.map +1 -0
- package/dist/approval.d.ts +72 -0
- package/dist/approval.js +161 -0
- package/dist/approval.js.map +1 -0
- package/dist/approvals.d.ts +2 -0
- package/dist/approvals.js +148 -0
- package/dist/approvals.js.map +1 -0
- package/dist/auth.d.ts +79 -0
- package/dist/auth.js +181 -0
- package/dist/auth.js.map +1 -0
- package/dist/automode.d.ts +33 -0
- package/dist/automode.js +69 -0
- package/dist/automode.js.map +1 -0
- package/dist/branches.d.ts +25 -0
- package/dist/branches.js +81 -0
- package/dist/branches.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +12 -0
- package/dist/cli.js.map +1 -0
- package/dist/codesearch.d.ts +27 -0
- package/dist/codesearch.js +111 -0
- package/dist/codesearch.js.map +1 -0
- package/dist/concurrency.d.ts +14 -0
- package/dist/concurrency.js +52 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config.d.ts +106 -0
- package/dist/config.js +119 -0
- package/dist/config.js.map +1 -0
- package/dist/engine-antigravity.d.ts +23 -0
- package/dist/engine-antigravity.js +357 -0
- package/dist/engine-antigravity.js.map +1 -0
- package/dist/engine-claude.d.ts +18 -0
- package/dist/engine-claude.js +64 -0
- package/dist/engine-claude.js.map +1 -0
- package/dist/engine.d.ts +122 -0
- package/dist/engine.js +36 -0
- package/dist/engine.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.js +10 -0
- package/dist/env.js.map +1 -0
- package/dist/environment.d.ts +39 -0
- package/dist/environment.js +100 -0
- package/dist/environment.js.map +1 -0
- package/dist/flaredeck-check.d.ts +21 -0
- package/dist/flaredeck-check.js +12 -0
- package/dist/flaredeck-check.js.map +1 -0
- package/dist/flaredeck-proxy.d.ts +60 -0
- package/dist/flaredeck-proxy.js +186 -0
- package/dist/flaredeck-proxy.js.map +1 -0
- package/dist/flaredeck.d.ts +132 -0
- package/dist/flaredeck.js +106 -0
- package/dist/flaredeck.js.map +1 -0
- package/dist/gemini.d.ts +21 -0
- package/dist/gemini.js +59 -0
- package/dist/gemini.js.map +1 -0
- package/dist/gitguard.d.ts +2 -0
- package/dist/gitguard.js +90 -0
- package/dist/gitguard.js.map +1 -0
- package/dist/github.d.ts +38 -0
- package/dist/github.js +153 -0
- package/dist/github.js.map +1 -0
- package/dist/githubauth.d.ts +21 -0
- package/dist/githubauth.js +90 -0
- package/dist/githubauth.js.map +1 -0
- package/dist/intake.d.ts +124 -0
- package/dist/intake.js +383 -0
- package/dist/intake.js.map +1 -0
- package/dist/jam.d.ts +45 -0
- package/dist/jam.js +385 -0
- package/dist/jam.js.map +1 -0
- package/dist/jev.d.ts +31 -0
- package/dist/jev.js +75 -0
- package/dist/jev.js.map +1 -0
- package/dist/jevguard.d.ts +28 -0
- package/dist/jevguard.js +213 -0
- package/dist/jevguard.js.map +1 -0
- package/dist/mailbox.d.ts +22 -0
- package/dist/mailbox.js +51 -0
- package/dist/mailbox.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +764 -0
- package/dist/main.js.map +1 -0
- package/dist/markdown.d.ts +5 -0
- package/dist/markdown.js +81 -0
- package/dist/markdown.js.map +1 -0
- package/dist/oauth.d.ts +30 -0
- package/dist/oauth.js +216 -0
- package/dist/oauth.js.map +1 -0
- package/dist/permissions.d.ts +43 -0
- package/dist/permissions.js +57 -0
- package/dist/permissions.js.map +1 -0
- package/dist/preflight.d.ts +71 -0
- package/dist/preflight.js +116 -0
- package/dist/preflight.js.map +1 -0
- package/dist/providers.d.ts +26 -0
- package/dist/providers.js +27 -0
- package/dist/providers.js.map +1 -0
- package/dist/repopicker.d.ts +3 -0
- package/dist/repopicker.js +111 -0
- package/dist/repopicker.js.map +1 -0
- package/dist/runs.d.ts +59 -0
- package/dist/runs.js +211 -0
- package/dist/runs.js.map +1 -0
- package/dist/sandbox.d.ts +13 -0
- package/dist/sandbox.js +110 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/schedules.d.ts +11 -0
- package/dist/schedules.js +85 -0
- package/dist/schedules.js.map +1 -0
- package/dist/secretguard.d.ts +4 -0
- package/dist/secretguard.js +115 -0
- package/dist/secretguard.js.map +1 -0
- package/dist/secrets.d.ts +27 -0
- package/dist/secrets.js +51 -0
- package/dist/secrets.js.map +1 -0
- package/dist/serve.d.ts +17 -0
- package/dist/serve.js +1206 -0
- package/dist/serve.js.map +1 -0
- package/dist/status.d.ts +73 -0
- package/dist/status.js +98 -0
- package/dist/status.js.map +1 -0
- package/dist/stream.d.ts +91 -0
- package/dist/stream.js +103 -0
- package/dist/stream.js.map +1 -0
- package/dist/streamtoken.d.ts +2 -0
- package/dist/streamtoken.js +30 -0
- package/dist/streamtoken.js.map +1 -0
- package/dist/team.d.ts +61 -0
- package/dist/team.js +136 -0
- package/dist/team.js.map +1 -0
- package/dist/ui.d.ts +110 -0
- package/dist/ui.js +348 -0
- package/dist/ui.js.map +1 -0
- package/dist/updater.d.ts +15 -0
- package/dist/updater.js +178 -0
- package/dist/updater.js.map +1 -0
- package/dist/workspace.d.ts +115 -0
- package/dist/workspace.js +247 -0
- package/dist/workspace.js.map +1 -0
- package/dist/worktree.d.ts +42 -0
- package/dist/worktree.js +140 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +52 -0
- package/python/sidecar.py +178 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub, authorised the same way as Claude and FlareDeck: a code you approve on any device.
|
|
3
|
+
*
|
|
4
|
+
* The point is not convenience, it is where the credential lives. Today the agent has `gh` and
|
|
5
|
+
* could read the token out of it; with this, the harness holds it and the agent has only the
|
|
6
|
+
* publishing tools. That turns "the agent should not push to dev" from a rule it is asked to
|
|
7
|
+
* follow into something it has no means to do.
|
|
8
|
+
*
|
|
9
|
+
* Device flow suits an agent box for the same reason `--no-browser` does: the machine may have
|
|
10
|
+
* no browser, and the approval happens wherever you are.
|
|
11
|
+
*/
|
|
12
|
+
/** Public by design — a device-flow client has no secret, which is why it suits an unattended box. */
|
|
13
|
+
export declare const GITHUB_CLIENT_ID = "Ov23liGPUwgxEVJBsYlU";
|
|
14
|
+
export type GitHubTokens = {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
refreshToken?: string;
|
|
17
|
+
/** Epoch ms. Absent expiry means a classic non-expiring token. */
|
|
18
|
+
expiresAt?: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function authorizeGitHub(say: (s: string) => void): Promise<GitHubTokens>;
|
|
21
|
+
export declare function freshGitHubToken(tokens: GitHubTokens, onRefreshed: (t: GitHubTokens) => void): Promise<string>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub, authorised the same way as Claude and FlareDeck: a code you approve on any device.
|
|
3
|
+
*
|
|
4
|
+
* The point is not convenience, it is where the credential lives. Today the agent has `gh` and
|
|
5
|
+
* could read the token out of it; with this, the harness holds it and the agent has only the
|
|
6
|
+
* publishing tools. That turns "the agent should not push to dev" from a rule it is asked to
|
|
7
|
+
* follow into something it has no means to do.
|
|
8
|
+
*
|
|
9
|
+
* Device flow suits an agent box for the same reason `--no-browser` does: the machine may have
|
|
10
|
+
* no browser, and the approval happens wherever you are.
|
|
11
|
+
*/
|
|
12
|
+
/** Public by design — a device-flow client has no secret, which is why it suits an unattended box. */
|
|
13
|
+
export const GITHUB_CLIENT_ID = 'Ov23liGPUwgxEVJBsYlU';
|
|
14
|
+
/** Read/write on the repos you can reach. Broad; a GitHub App would scope it per repo. */
|
|
15
|
+
const SCOPE = 'repo';
|
|
16
|
+
const form = async (url, body) => {
|
|
17
|
+
const res = await fetch(url, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
20
|
+
body: new URLSearchParams(body).toString(),
|
|
21
|
+
});
|
|
22
|
+
return res.json();
|
|
23
|
+
};
|
|
24
|
+
export async function authorizeGitHub(say) {
|
|
25
|
+
const start = await form('https://github.com/login/device/code', {
|
|
26
|
+
client_id: GITHUB_CLIENT_ID,
|
|
27
|
+
scope: SCOPE,
|
|
28
|
+
});
|
|
29
|
+
if (!start.device_code) {
|
|
30
|
+
throw new Error(`GitHub would not start a device login: ${JSON.stringify(start)}. ` +
|
|
31
|
+
`If this says "device flow disabled", the OAuth App needs Enable Device Flow ticked.`);
|
|
32
|
+
}
|
|
33
|
+
say('On any device, open:');
|
|
34
|
+
say('');
|
|
35
|
+
say(start.verification_uri);
|
|
36
|
+
say('');
|
|
37
|
+
say(`and enter this code: ${start.user_code}`);
|
|
38
|
+
say('');
|
|
39
|
+
say(`Waiting for you to approve it (the code is good for ${Math.round(start.expires_in / 60)} minutes)…`);
|
|
40
|
+
const deadline = Date.now() + start.expires_in * 1000;
|
|
41
|
+
// GitHub asks for a minimum gap and will tell us off with slow_down if we ignore it.
|
|
42
|
+
let wait = Math.max(start.interval, 5) * 1000;
|
|
43
|
+
while (Date.now() < deadline) {
|
|
44
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
45
|
+
const res = await form('https://github.com/login/oauth/access_token', {
|
|
46
|
+
client_id: GITHUB_CLIENT_ID,
|
|
47
|
+
device_code: start.device_code,
|
|
48
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
49
|
+
});
|
|
50
|
+
if (res.access_token) {
|
|
51
|
+
return {
|
|
52
|
+
accessToken: res.access_token,
|
|
53
|
+
refreshToken: res.refresh_token,
|
|
54
|
+
...(res.expires_in ? { expiresAt: Date.now() + Number(res.expires_in) * 1000 } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (res.error === 'authorization_pending')
|
|
58
|
+
continue;
|
|
59
|
+
if (res.error === 'slow_down') {
|
|
60
|
+
wait += 5000;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`GitHub login failed: ${res.error_description || res.error}`);
|
|
64
|
+
}
|
|
65
|
+
throw new Error('The GitHub code expired before it was approved.');
|
|
66
|
+
}
|
|
67
|
+
/** Refresh a minute early, so a token never expires in the middle of a push. */
|
|
68
|
+
const EARLY_MS = 60_000;
|
|
69
|
+
export async function freshGitHubToken(tokens, onRefreshed) {
|
|
70
|
+
if (!tokens.expiresAt || Date.now() < tokens.expiresAt - EARLY_MS)
|
|
71
|
+
return tokens.accessToken;
|
|
72
|
+
if (!tokens.refreshToken)
|
|
73
|
+
return tokens.accessToken;
|
|
74
|
+
const res = await form('https://github.com/login/oauth/access_token', {
|
|
75
|
+
client_id: GITHUB_CLIENT_ID,
|
|
76
|
+
grant_type: 'refresh_token',
|
|
77
|
+
refresh_token: tokens.refreshToken,
|
|
78
|
+
});
|
|
79
|
+
if (!res.access_token)
|
|
80
|
+
throw new Error(`Could not refresh GitHub: ${res.error_description || res.error}`);
|
|
81
|
+
const next = {
|
|
82
|
+
accessToken: res.access_token,
|
|
83
|
+
// GitHub re-issues the refresh token; keeping the old one would strand the agent later.
|
|
84
|
+
refreshToken: res.refresh_token ?? tokens.refreshToken,
|
|
85
|
+
...(res.expires_in ? { expiresAt: Date.now() + Number(res.expires_in) * 1000 } : {}),
|
|
86
|
+
};
|
|
87
|
+
onRefreshed(next);
|
|
88
|
+
return next.accessToken;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=githubauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"githubauth.js","sourceRoot":"","sources":["../src/githubauth.ts"],"names":[],"mappings":"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"}
|
package/dist/intake.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Flaredeck } from './flaredeck.js';
|
|
2
|
+
import { type JevVerdict } from './jev.js';
|
|
3
|
+
import { type Providers } from './providers.js';
|
|
4
|
+
import { type CodeSearch } from './codesearch.js';
|
|
5
|
+
/**
|
|
6
|
+
* Intake: what a card is about, worked out BEFORE the working model is woken.
|
|
7
|
+
*
|
|
8
|
+
* A fresh session used to spend ~90k tokens orienting itself — the guide, every comment on the
|
|
9
|
+
* card, the repo — on the most expensive model available, and did it again on every wake. Two
|
|
10
|
+
* cheaper steps now come first:
|
|
11
|
+
*
|
|
12
|
+
* 1. A digest, in plain code: the card and its thread fetched over REST, HTML stripped, the
|
|
13
|
+
* harness's own run-result dumps collapsed to a line, old comments trimmed. Costs nothing.
|
|
14
|
+
* 2. A brief, from a small model: one tool-less Haiku turn turns the digest into a structured
|
|
15
|
+
* requirement — the ask, how to reproduce it, what done looks like, where the evidence is —
|
|
16
|
+
* and a classification of how hard the work is and where it lives.
|
|
17
|
+
*
|
|
18
|
+
* The classification picks the model for the real turn, so a copy change is not done by the
|
|
19
|
+
* model that does production data corrections. The working agent still has every tool and can
|
|
20
|
+
* read the thread itself; the brief is a head start, not a wall.
|
|
21
|
+
*
|
|
22
|
+
* The small model is one provider behind an interface. Gemini Flash-Lite or TypeSafe's Jev can
|
|
23
|
+
* take the same digest and return the same shape; Haiku was first because it runs on the
|
|
24
|
+
* subscription already paid for and needs no key.
|
|
25
|
+
*/
|
|
26
|
+
export type Brief = {
|
|
27
|
+
/** What is actually being asked for, in one or two sentences. */
|
|
28
|
+
ask: string;
|
|
29
|
+
/** How to see the problem, in order. Empty for a feature. */
|
|
30
|
+
repro: string[];
|
|
31
|
+
/** Where the work most likely lives: a layer, module or file area named in the thread. */
|
|
32
|
+
area: string;
|
|
33
|
+
/** What a reviewer would check to call it done. */
|
|
34
|
+
acceptance: string[];
|
|
35
|
+
/** Pointers worth following: attachment names, URLs, error strings, ids. */
|
|
36
|
+
evidence: string[];
|
|
37
|
+
/** Decisions or facts the thread leaves open — what to ask a person about. */
|
|
38
|
+
open_questions: string[];
|
|
39
|
+
/** How much work this is, from what the thread shows. */
|
|
40
|
+
complexity: 'small' | 'medium' | 'large';
|
|
41
|
+
/** Whether doing it right means reading or changing production data. */
|
|
42
|
+
touches_production_data: boolean;
|
|
43
|
+
/** 0–1: how sure the small model is of the classification. */
|
|
44
|
+
confidence: number;
|
|
45
|
+
};
|
|
46
|
+
export type Intake = {
|
|
47
|
+
/** The digest the brief was built from — kept so the saving can be measured. */
|
|
48
|
+
digest: string;
|
|
49
|
+
brief: Brief | null;
|
|
50
|
+
/** Jev's reading of the same digest, when an OpenRouter key is configured. */
|
|
51
|
+
jev: JevVerdict | null;
|
|
52
|
+
/** Who wrote `brief`. */
|
|
53
|
+
briefBy: 'haiku' | 'gemini' | null;
|
|
54
|
+
/** Files in the repo that mention names from the card, when a repo was given. */
|
|
55
|
+
files: CodeSearch | null;
|
|
56
|
+
/** The other writer's brief, when the dry run asked for both. */
|
|
57
|
+
other: {
|
|
58
|
+
by: 'haiku' | 'gemini';
|
|
59
|
+
brief: Brief | null;
|
|
60
|
+
secs: number;
|
|
61
|
+
} | null;
|
|
62
|
+
/** Which classifier chose the model. */
|
|
63
|
+
/** How big the work is, for an engine to map to a model of its own. */
|
|
64
|
+
size: Size | undefined;
|
|
65
|
+
routedBy: 'jev' | 'brief' | 'default';
|
|
66
|
+
/**
|
|
67
|
+
* Set when the card cannot be worked yet: it links evidence the harness cannot read. The turn
|
|
68
|
+
* must not start — a model that cannot see the recording will guess at what is in it — and a
|
|
69
|
+
* person is told what access is missing.
|
|
70
|
+
*/
|
|
71
|
+
blocked?: string;
|
|
72
|
+
/** The model the real turn should run on; undefined leaves the SDK default. */
|
|
73
|
+
model: string | undefined;
|
|
74
|
+
/** One line for the pane. */
|
|
75
|
+
summary: string;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* The card and its thread as compact text. Pure code, so it is always available and always
|
|
79
|
+
* free; the brief step may be skipped, this one never is.
|
|
80
|
+
*/
|
|
81
|
+
export declare function digestCard(deck: Flaredeck, cardIdOrCode: string): Promise<{
|
|
82
|
+
text: string;
|
|
83
|
+
comments: number;
|
|
84
|
+
}>;
|
|
85
|
+
/** One tool-less turn on the small model. Returns null rather than throwing: intake is a head start, not a gate. */
|
|
86
|
+
export declare function writeBrief(digest: string, log: (text: string) => void): Promise<Brief | null>;
|
|
87
|
+
/** The same brief from Gemini: one REST call, JSON enforced by schema, free at this volume. */
|
|
88
|
+
export declare function writeBriefGemini(digest: string, providers: Providers, log: (text: string) => void): Promise<Brief | null>;
|
|
89
|
+
/**
|
|
90
|
+
* Which model does the real work. Production data always gets the best model regardless of
|
|
91
|
+
* size — the cost of a wrong UPDATE is not measured in tokens. Low confidence is treated as
|
|
92
|
+
* medium: the small model not knowing is not a reason to go cheap.
|
|
93
|
+
*/
|
|
94
|
+
export type Size = 'small' | 'medium' | 'large';
|
|
95
|
+
/**
|
|
96
|
+
* How big the work is — the decision itself, separate from any vendor's name for a model that
|
|
97
|
+
* suits it. A Claude model id handed to a Gemini agent is a 404 and a card that fails forever,
|
|
98
|
+
* which is exactly what happened; each engine reads the SIZE and picks its own.
|
|
99
|
+
*/
|
|
100
|
+
export declare function sizeFromBrief(brief: Brief | null): Size | undefined;
|
|
101
|
+
export declare function sizeFromJev(jev: JevVerdict | null): Size | undefined;
|
|
102
|
+
export declare function routeModel(brief: Brief | null): string | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Jev's answer, read the same way, and it is the one that counts: deciding a typed question is
|
|
105
|
+
* what a decision model is for, and it costs a thousandth of what the brief does. The brief's
|
|
106
|
+
* own classification decides only when Jev is not configured or did not answer; when neither
|
|
107
|
+
* is available, the SDK default stands.
|
|
108
|
+
*/
|
|
109
|
+
export declare function routeModelFromJev(jev: JevVerdict | null): string | undefined;
|
|
110
|
+
/** One line each for the pane, so the two classifiers can be compared on real cards. */
|
|
111
|
+
export declare function compareLine(brief: Brief | null, jev: JevVerdict | null, by?: 'haiku' | 'gemini'): string;
|
|
112
|
+
/** What the working agent is handed in place of "read the card". */
|
|
113
|
+
/** Everything intake can hand the agent: the brief when there is one, the candidate files either way. */
|
|
114
|
+
export declare function intakeText(prepared: Intake | null): string;
|
|
115
|
+
export declare function briefText(brief: Brief, digestChars: number, files?: CodeSearch | null): string;
|
|
116
|
+
/**
|
|
117
|
+
* The whole step for one card event. Never throws, never blocks the turn for long: a failure
|
|
118
|
+
* anywhere means the turn runs the old way, on the default model, with the old prompt.
|
|
119
|
+
*/
|
|
120
|
+
export declare function intake(deck: Flaredeck | undefined, cardIdOrCode: string, log: (text: string) => void,
|
|
121
|
+
/** Dry run: write the brief with BOTH available writers so they can be compared. */
|
|
122
|
+
compare?: boolean,
|
|
123
|
+
/** The checkout to search for the card's names in. */
|
|
124
|
+
repo?: string): Promise<Intake | null>;
|
package/dist/intake.js
ADDED
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { subscriptionEnv } from './agent.js';
|
|
3
|
+
import { classifyWithJev } from './jev.js';
|
|
4
|
+
import { geminiBriefText, GEMINI_DEFAULT_MODEL } from './gemini.js';
|
|
5
|
+
import { jamEvidence, jamLinks, loadJam } from './jam.js';
|
|
6
|
+
import { loadProviders } from './providers.js';
|
|
7
|
+
import { codeSearch, codeSearchText } from './codesearch.js';
|
|
8
|
+
/** Cheapest model that can write a structured brief. */
|
|
9
|
+
const INTAKE_MODEL = 'claude-haiku-4-5-20251001';
|
|
10
|
+
/**
|
|
11
|
+
* What the classification buys. Opus 5.5 outscores Fable 5.1 on Anthropic's coding and agentic
|
|
12
|
+
* benchmarks at 40% of the price, so large work stays on Opus; Fable is for a card Opus has
|
|
13
|
+
* already failed, and is chosen by a person, not by the router.
|
|
14
|
+
*/
|
|
15
|
+
const ROUTE = {
|
|
16
|
+
small: 'claude-sonnet-5-5',
|
|
17
|
+
medium: 'claude-opus-5-5',
|
|
18
|
+
large: 'claude-opus-5-5',
|
|
19
|
+
};
|
|
20
|
+
const MAX_COMMENTS = 25;
|
|
21
|
+
const MAX_COMMENT_CHARS = 1500;
|
|
22
|
+
const MAX_DESCRIPTION_CHARS = 6000;
|
|
23
|
+
const MAX_NOTES_CHARS = 4000;
|
|
24
|
+
/**
|
|
25
|
+
* The card and its thread as compact text. Pure code, so it is always available and always
|
|
26
|
+
* free; the brief step may be skipped, this one never is.
|
|
27
|
+
*/
|
|
28
|
+
export async function digestCard(deck, cardIdOrCode) {
|
|
29
|
+
const card = await deck.card(cardIdOrCode);
|
|
30
|
+
const comments = Array.isArray(card.comments) ? card.comments : [];
|
|
31
|
+
const recent = comments.slice(-MAX_COMMENTS);
|
|
32
|
+
const lines = [];
|
|
33
|
+
lines.push(`# ${card.code ?? ''} ${card.name ?? ''}`.trim());
|
|
34
|
+
const meta = [
|
|
35
|
+
card.board_name && `board ${card.board_name}`,
|
|
36
|
+
card.lane?.name && `lane ${card.lane.name}`,
|
|
37
|
+
card.type && `type ${card.type}`,
|
|
38
|
+
card.priority != null && `priority ${card.priority}`,
|
|
39
|
+
card.assigned_to?.name && `assigned to ${card.assigned_to.name}`,
|
|
40
|
+
card.reporter?.name && `reported by ${card.reporter.name}`,
|
|
41
|
+
Array.isArray(card.labels) && card.labels.length && `labels ${card.labels.join(', ')}`,
|
|
42
|
+
].filter(Boolean);
|
|
43
|
+
if (meta.length)
|
|
44
|
+
lines.push(meta.join(' · '));
|
|
45
|
+
if (card.url)
|
|
46
|
+
lines.push(String(card.url));
|
|
47
|
+
if (card.description)
|
|
48
|
+
lines.push('', '## Description', clip(String(card.description), MAX_DESCRIPTION_CHARS));
|
|
49
|
+
// The previous session's handover. Entries are appended oldest first, so a long scratchpad keeps its tail.
|
|
50
|
+
const notes = typeof card.agent_notes === 'string' ? card.agent_notes.trim() : '';
|
|
51
|
+
if (notes) {
|
|
52
|
+
const tail = notes.length > MAX_NOTES_CHARS ? `[… earlier notes on the card]\n${notes.slice(-MAX_NOTES_CHARS)}` : notes;
|
|
53
|
+
lines.push('', '## Agent notes (a previous session\'s handover on this card)', tail);
|
|
54
|
+
}
|
|
55
|
+
const files = comments.map((c) => c.file).filter(Boolean);
|
|
56
|
+
if (files.length)
|
|
57
|
+
lines.push('', '## Attachments', ...files.map((f) => `- ${attachmentName(f)}`));
|
|
58
|
+
if (recent.length) {
|
|
59
|
+
lines.push('', `## Thread (${recent.length} of ${comments.length} comments, oldest first)`);
|
|
60
|
+
for (const c of recent) {
|
|
61
|
+
const who = c.sender_name ?? 'someone';
|
|
62
|
+
const when = c.created_at ? String(c.created_at).slice(0, 16).replace('T', ' ') : '';
|
|
63
|
+
lines.push('', `### ${who}${when ? ` · ${when}` : ''}`, collapse(String(c.message ?? '')));
|
|
64
|
+
if (c.approval) {
|
|
65
|
+
const a = c.approval;
|
|
66
|
+
lines.push(`[approval: ${a.state}${a.outcome ? ` · ${a.outcome}` : ''}${a.ran_at ? ' · ran' : ''}]`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { text: lines.join('\n'), comments: comments.length };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A run result is a header line and then the raw output — hundreds of rows, sometimes. The next
|
|
74
|
+
* session needs to know it ran and what it was, not every row; the card still holds the rows.
|
|
75
|
+
*/
|
|
76
|
+
function collapse(message) {
|
|
77
|
+
const m = message.trim();
|
|
78
|
+
const fence = m.indexOf('```');
|
|
79
|
+
if (/^\*\*Production run/.test(m) && fence > 0) {
|
|
80
|
+
const head = m.slice(0, fence).trim();
|
|
81
|
+
const body = m.slice(fence);
|
|
82
|
+
const rows = (body.match(/\n/g) || []).length;
|
|
83
|
+
return `${head}\n[output: ${rows} lines, on the card]`;
|
|
84
|
+
}
|
|
85
|
+
return clip(m, MAX_COMMENT_CHARS);
|
|
86
|
+
}
|
|
87
|
+
function clip(text, max) {
|
|
88
|
+
return text.length > max ? `${text.slice(0, max)}\n[… ${text.length - max} more characters on the card]` : text;
|
|
89
|
+
}
|
|
90
|
+
function attachmentName(file) {
|
|
91
|
+
const url = typeof file === 'string' ? file : file?.url ?? file?.name ?? '';
|
|
92
|
+
const name = typeof file === 'object' && file?.name ? String(file.name) : decodeURIComponent((String(url).split('?')[0] ?? '').split('/').pop() || '');
|
|
93
|
+
return name.replace(/^\d{13}-/, '') || 'attachment';
|
|
94
|
+
}
|
|
95
|
+
const BRIEF_PROMPT = [
|
|
96
|
+
'You are the intake step for a software engineering agent. Below is a Kanban card and its',
|
|
97
|
+
'comment thread. Produce ONE JSON object, and nothing else — no prose, no code fence — with',
|
|
98
|
+
'exactly these keys:',
|
|
99
|
+
'',
|
|
100
|
+
' ask: string — what is being asked for, one or two sentences, in your own words',
|
|
101
|
+
' repro: string[] — steps to see the problem, in order; [] for a feature or a question',
|
|
102
|
+
' area: string — where the work most likely lives (layer, service, module, file area), from clues in the thread',
|
|
103
|
+
' acceptance: string[] — what a reviewer would check to call it done',
|
|
104
|
+
' evidence: string[] — pointers worth following: attachment names, URLs, error strings, ids, dates',
|
|
105
|
+
' open_questions: string[] — decisions or facts the thread leaves open that a person would have to answer',
|
|
106
|
+
' complexity: "small" | "medium" | "large" — small: a contained change or a lookup, one file or one query;',
|
|
107
|
+
' medium: a real fix across a few files with tests; large: cross-layer, a design choice, a migration, or anything touching production data',
|
|
108
|
+
' touches_production_data: boolean — true if doing it right means reading or changing a production database',
|
|
109
|
+
' confidence: number 0-1 — how sure you are of complexity and area',
|
|
110
|
+
'',
|
|
111
|
+
'If the latest comments show the work is already done or waiting on a person, say so in `ask`.',
|
|
112
|
+
'If there are agent notes, they are a previous session\'s handover: say in `ask` what was already tried or found, and put the places they name in `area` and `evidence`.',
|
|
113
|
+
'Be concrete and short. Quote identifiers exactly.',
|
|
114
|
+
'',
|
|
115
|
+
'----- CARD -----',
|
|
116
|
+
].join('\n');
|
|
117
|
+
/** One tool-less turn on the small model. Returns null rather than throwing: intake is a head start, not a gate. */
|
|
118
|
+
export async function writeBrief(digest, log) {
|
|
119
|
+
return writeBriefHaiku(digest, log);
|
|
120
|
+
}
|
|
121
|
+
/** The same brief from Gemini: one REST call, JSON enforced by schema, free at this volume. */
|
|
122
|
+
export async function writeBriefGemini(digest, providers, log) {
|
|
123
|
+
if (!providers.gemini)
|
|
124
|
+
return null;
|
|
125
|
+
const started = Date.now();
|
|
126
|
+
try {
|
|
127
|
+
const ask = () => geminiBriefText(`${BRIEF_PROMPT}\n${digest}`, providers.gemini.apiKey, providers.gemini.model ?? GEMINI_DEFAULT_MODEL);
|
|
128
|
+
// Overloaded or rate-limited: one more try after a pause, then give the job to Haiku.
|
|
129
|
+
const { text, usage, model } = await ask().catch(async (e) => {
|
|
130
|
+
if ([429, 503].includes(Number(e?.status))) {
|
|
131
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
132
|
+
return ask();
|
|
133
|
+
}
|
|
134
|
+
throw e;
|
|
135
|
+
});
|
|
136
|
+
const brief = parseBrief(text);
|
|
137
|
+
if (!brief) {
|
|
138
|
+
log(`intake: gemini did not return a usable brief (${Math.round((Date.now() - started) / 1000)}s)`);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
log(`intake: gemini ${model} · ${usage.input_tokens} in / ${usage.output_tokens} out · ${Math.round((Date.now() - started) / 1000)}s`);
|
|
142
|
+
return brief;
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
log(`intake: gemini brief failed — ${e instanceof Error ? e.message : String(e)}`);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function writeBriefHaiku(digest, log) {
|
|
150
|
+
const started = Date.now();
|
|
151
|
+
try {
|
|
152
|
+
let text = '';
|
|
153
|
+
const running = query({
|
|
154
|
+
prompt: `${BRIEF_PROMPT}\n${digest}`,
|
|
155
|
+
options: {
|
|
156
|
+
model: INTAKE_MODEL,
|
|
157
|
+
systemPrompt: 'You answer with a single JSON object and nothing else.',
|
|
158
|
+
tools: [],
|
|
159
|
+
allowedTools: [],
|
|
160
|
+
maxTurns: 1,
|
|
161
|
+
permissionMode: 'bypassPermissions',
|
|
162
|
+
allowDangerouslySkipPermissions: true,
|
|
163
|
+
settingSources: [],
|
|
164
|
+
env: subscriptionEnv(),
|
|
165
|
+
managedSettings: { disableClaudeAiConnectors: true },
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
for await (const message of running) {
|
|
169
|
+
if (message?.type === 'assistant') {
|
|
170
|
+
for (const block of message.message?.content ?? [])
|
|
171
|
+
if (block?.type === 'text')
|
|
172
|
+
text += block.text;
|
|
173
|
+
}
|
|
174
|
+
if (message?.type === 'result' && typeof message.result === 'string' && !text)
|
|
175
|
+
text = message.result;
|
|
176
|
+
}
|
|
177
|
+
const brief = parseBrief(text);
|
|
178
|
+
if (!brief) {
|
|
179
|
+
log(`intake: the small model did not return a usable brief (${Math.round((Date.now() - started) / 1000)}s)`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
return brief;
|
|
183
|
+
}
|
|
184
|
+
catch (e) {
|
|
185
|
+
log(`intake: brief failed — ${e instanceof Error ? e.message : String(e)}`);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function parseBrief(text) {
|
|
190
|
+
const start = text.indexOf('{');
|
|
191
|
+
const end = text.lastIndexOf('}');
|
|
192
|
+
if (start < 0 || end <= start)
|
|
193
|
+
return null;
|
|
194
|
+
try {
|
|
195
|
+
const raw = JSON.parse(text.slice(start, end + 1));
|
|
196
|
+
const arr = (v) => (Array.isArray(v) ? v.map(String) : []);
|
|
197
|
+
const complexity = ['small', 'medium', 'large'].includes(raw.complexity) ? raw.complexity : 'medium';
|
|
198
|
+
return {
|
|
199
|
+
ask: String(raw.ask ?? ''),
|
|
200
|
+
repro: arr(raw.repro),
|
|
201
|
+
area: String(raw.area ?? ''),
|
|
202
|
+
acceptance: arr(raw.acceptance),
|
|
203
|
+
evidence: arr(raw.evidence),
|
|
204
|
+
open_questions: arr(raw.open_questions),
|
|
205
|
+
complexity,
|
|
206
|
+
touches_production_data: !!raw.touches_production_data,
|
|
207
|
+
confidence: Math.max(0, Math.min(1, Number(raw.confidence) || 0)),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* How big the work is — the decision itself, separate from any vendor's name for a model that
|
|
216
|
+
* suits it. A Claude model id handed to a Gemini agent is a 404 and a card that fails forever,
|
|
217
|
+
* which is exactly what happened; each engine reads the SIZE and picks its own.
|
|
218
|
+
*/
|
|
219
|
+
export function sizeFromBrief(brief) {
|
|
220
|
+
if (!brief)
|
|
221
|
+
return undefined;
|
|
222
|
+
if (brief.touches_production_data)
|
|
223
|
+
return 'large';
|
|
224
|
+
if (brief.confidence < 0.5)
|
|
225
|
+
return 'medium';
|
|
226
|
+
return brief.complexity;
|
|
227
|
+
}
|
|
228
|
+
export function sizeFromJev(jev) {
|
|
229
|
+
if (!jev)
|
|
230
|
+
return undefined;
|
|
231
|
+
if (jev.production_data >= 0.5)
|
|
232
|
+
return 'large';
|
|
233
|
+
return jev.complexity;
|
|
234
|
+
}
|
|
235
|
+
export function routeModel(brief) {
|
|
236
|
+
const size = sizeFromBrief(brief);
|
|
237
|
+
return size ? ROUTE[size] : undefined;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Jev's answer, read the same way, and it is the one that counts: deciding a typed question is
|
|
241
|
+
* what a decision model is for, and it costs a thousandth of what the brief does. The brief's
|
|
242
|
+
* own classification decides only when Jev is not configured or did not answer; when neither
|
|
243
|
+
* is available, the SDK default stands.
|
|
244
|
+
*/
|
|
245
|
+
export function routeModelFromJev(jev) {
|
|
246
|
+
if (!jev)
|
|
247
|
+
return undefined;
|
|
248
|
+
if (jev.production_data >= 0.5)
|
|
249
|
+
return ROUTE.large;
|
|
250
|
+
// Jev decides whenever it answers. Its confidence is logged beside the brief's reading so a
|
|
251
|
+
// person can see where it was unsure, but a low number is not a reason to hand the choice to
|
|
252
|
+
// a model that costs a thousand times more to ask.
|
|
253
|
+
return ROUTE[jev.complexity];
|
|
254
|
+
}
|
|
255
|
+
/** One line each for the pane, so the two classifiers can be compared on real cards. */
|
|
256
|
+
export function compareLine(brief, jev, by = 'haiku') {
|
|
257
|
+
const b = brief ? `${by}: ${brief.complexity}${brief.touches_production_data ? ' · prod data' : ''} · ${Math.round(brief.confidence * 100)}%` : `${by}: no brief`;
|
|
258
|
+
const j = jev
|
|
259
|
+
? `jev: ${jev.complexity} (${jev.score.toFixed(2)}) · prod ${Math.round(jev.production_data * 100)}% · settled ${Math.round(jev.settled * 100)}% · ${jev.kind} · ${Math.round(jev.confidence * 100)}% · $${jev.cost_usd.toFixed(5)}`
|
|
260
|
+
: 'jev: off';
|
|
261
|
+
return `${b} | ${j}`;
|
|
262
|
+
}
|
|
263
|
+
/** What the working agent is handed in place of "read the card". */
|
|
264
|
+
/** Everything intake can hand the agent: the brief when there is one, the candidate files either way. */
|
|
265
|
+
export function intakeText(prepared) {
|
|
266
|
+
if (!prepared)
|
|
267
|
+
return '';
|
|
268
|
+
if (prepared.brief)
|
|
269
|
+
return briefText(prepared.brief, prepared.digest.length, prepared.files);
|
|
270
|
+
const files = codeSearchText(prepared.files);
|
|
271
|
+
return files ? `\n## Intake (no brief could be written for this card)\n${files}\n` : '';
|
|
272
|
+
}
|
|
273
|
+
export function briefText(brief, digestChars, files = null) {
|
|
274
|
+
const list = (title, items) => (items.length ? [`${title}:`, ...items.map((i) => ` - ${i}`)] : []);
|
|
275
|
+
return [
|
|
276
|
+
'',
|
|
277
|
+
'## Intake brief (prepared from the card and its thread before you started)',
|
|
278
|
+
'',
|
|
279
|
+
`Ask: ${brief.ask}`,
|
|
280
|
+
`Likely area: ${brief.area || 'not clear from the thread'}`,
|
|
281
|
+
`Size: ${brief.complexity}${brief.touches_production_data ? ' · touches production data' : ''} (confidence ${Math.round(brief.confidence * 100)}%)`,
|
|
282
|
+
...list('Reproduce', brief.repro),
|
|
283
|
+
...list('Done means', brief.acceptance),
|
|
284
|
+
...list('Evidence to follow', brief.evidence),
|
|
285
|
+
...list('Open questions', brief.open_questions),
|
|
286
|
+
codeSearchText(files),
|
|
287
|
+
'',
|
|
288
|
+
`This brief is a head start, not the card. Anything the brief does not cover is still yours to`,
|
|
289
|
+
`read with get-card-details — but you`,
|
|
290
|
+
`do not need to re-read a ${Math.round(digestChars / 1000)}k-character thread to find out what the card is about.`,
|
|
291
|
+
].join('\n');
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The whole step for one card event. Never throws, never blocks the turn for long: a failure
|
|
295
|
+
* anywhere means the turn runs the old way, on the default model, with the old prompt.
|
|
296
|
+
*/
|
|
297
|
+
export async function intake(deck, cardIdOrCode, log,
|
|
298
|
+
/** Dry run: write the brief with BOTH available writers so they can be compared. */
|
|
299
|
+
compare = false,
|
|
300
|
+
/** The checkout to search for the card's names in. */
|
|
301
|
+
repo) {
|
|
302
|
+
if (!deck)
|
|
303
|
+
return null;
|
|
304
|
+
const started = Date.now();
|
|
305
|
+
try {
|
|
306
|
+
const digested = await digestCard(deck, cardIdOrCode);
|
|
307
|
+
const comments = digested.comments;
|
|
308
|
+
let text = digested.text;
|
|
309
|
+
// A Jam link on the card is evidence the agent would otherwise have to go and watch. Read it
|
|
310
|
+
// here, as text, so the brief and the classification see it too. Two at most: a card with
|
|
311
|
+
// more is a thread of attempts, and the latest two are the ones that matter.
|
|
312
|
+
const links = jamLinks(text).slice(-2);
|
|
313
|
+
if (links.length) {
|
|
314
|
+
const jam = loadJam();
|
|
315
|
+
const blocked = (why) => ({
|
|
316
|
+
digest: text, brief: null, jev: null, briefBy: null, other: null, files: null, model: undefined, size: undefined, routedBy: 'default',
|
|
317
|
+
summary: `intake: blocked — ${why}`, blocked: why,
|
|
318
|
+
});
|
|
319
|
+
if (!jam) {
|
|
320
|
+
return blocked(`this card links a Jam recording (${links.join(', ')}) and this machine has no Jam login, so the recording cannot be read. ` +
|
|
321
|
+
'On the agent\'s machine run `flaredeck-agent login --jam` in a workspace that can see it, then clear needs-human to retry.');
|
|
322
|
+
}
|
|
323
|
+
const evidence = await Promise.all(links.map((u) => jamEvidence(u, jam, log)));
|
|
324
|
+
const unreadable = evidence.map((e, i) => ({ e, url: links[i] })).filter((x) => !x.e.ok);
|
|
325
|
+
if (unreadable.length) {
|
|
326
|
+
return blocked(`the Jam recording${unreadable.length > 1 ? 's' : ''} on this card cannot be read with the Jam login on the agent's machine: ` +
|
|
327
|
+
unreadable.map((x) => `${x.url} (${x.e.error})`).join('; ') +
|
|
328
|
+
'. Share the recording with that Jam workspace, or sign the machine into the workspace that owns it (`flaredeck-agent login --jam`), then clear needs-human to retry.');
|
|
329
|
+
}
|
|
330
|
+
text += `\n\n## Jam recordings (read by the harness)\n\n${evidence.map((e) => e.text).join('\n\n')}`;
|
|
331
|
+
}
|
|
332
|
+
const providers = loadProviders();
|
|
333
|
+
const briefBy = providers.brief === 'gemini' && providers.gemini ? 'gemini' : 'haiku';
|
|
334
|
+
const otherBy = briefBy === 'gemini' ? 'haiku' : 'gemini';
|
|
335
|
+
const write = (by) => (by === 'gemini' ? writeBriefGemini(text, providers, log) : writeBriefHaiku(text, log));
|
|
336
|
+
const timed = async (by) => { const t = Date.now(); const b = await write(by); return { by, brief: b, secs: Math.round((Date.now() - t) / 1000) }; };
|
|
337
|
+
// All at once: the brief takes tens of seconds, Jev about one, the code search a second or two.
|
|
338
|
+
const [mine, other, jev, files] = await Promise.all([
|
|
339
|
+
timed(briefBy),
|
|
340
|
+
compare && (otherBy === 'haiku' || providers.gemini) ? timed(otherBy) : Promise.resolve(null),
|
|
341
|
+
providers.openrouter
|
|
342
|
+
? classifyWithJev(text, providers.openrouter.apiKey).catch((e) => { log(`intake: jev failed — ${e instanceof Error ? e.message : String(e)}`); return null; })
|
|
343
|
+
: Promise.resolve(null),
|
|
344
|
+
repo ? codeSearch(repo, text).catch(() => null) : Promise.resolve(null),
|
|
345
|
+
]);
|
|
346
|
+
if (files)
|
|
347
|
+
log(`intake: code search — ${files.files.length} candidate files from ${files.terms.length} names in ${files.ms}ms`);
|
|
348
|
+
let brief = mine.brief;
|
|
349
|
+
let briefWrittenBy = briefBy;
|
|
350
|
+
// The chosen writer failed and the comparison did not already run the other one: Haiku is
|
|
351
|
+
// always there, so a live card never goes to the agent without a brief over a 503.
|
|
352
|
+
if (!brief && !other && briefBy === 'gemini') {
|
|
353
|
+
log('intake: gemini gave no brief — falling back to haiku');
|
|
354
|
+
brief = await writeBriefHaiku(text, log);
|
|
355
|
+
briefWrittenBy = 'haiku';
|
|
356
|
+
}
|
|
357
|
+
else if (!brief && other?.brief) {
|
|
358
|
+
brief = other.brief;
|
|
359
|
+
briefWrittenBy = other.by;
|
|
360
|
+
}
|
|
361
|
+
const fromJev = routeModelFromJev(jev);
|
|
362
|
+
const fromBrief = routeModel(brief);
|
|
363
|
+
const model = fromJev ?? fromBrief;
|
|
364
|
+
const size = sizeFromJev(jev) ?? sizeFromBrief(brief);
|
|
365
|
+
const routedBy = fromJev ? 'jev' : fromBrief ? 'brief' : 'default';
|
|
366
|
+
const secs = Math.round((Date.now() - started) / 1000);
|
|
367
|
+
log(compareLine(brief, jev, briefWrittenBy));
|
|
368
|
+
if (other)
|
|
369
|
+
log(compareLine(other.brief, null, other.by).replace(' | jev: off', ` · ${other.secs}s (comparison only)`));
|
|
370
|
+
// The SIZE, not a model name. Routing decides how big the work is; each engine maps that to
|
|
371
|
+
// its own model, so printing one vendor's name told a Gemini agent it was about to run on
|
|
372
|
+
// claude-opus-5-5 — on the line directly after one saying the intake ran on gemini-3.5-flash-lite.
|
|
373
|
+
// It is the line you would read to reason about cost, so it should say the thing that is true
|
|
374
|
+
// whichever engine is about to run.
|
|
375
|
+
const summary = `intake: ${comments} comments → ${Math.round(text.length / 1000)}k chars · ${size ?? 'unsized'}${size ? ` by ${routedBy}` : ''} · ${secs}s`;
|
|
376
|
+
return { digest: text, brief, jev, briefBy: brief ? briefWrittenBy : null, other, files, model, size, routedBy, summary };
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
log(`intake: skipped — ${e instanceof Error ? e.message : String(e)}`);
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
//# sourceMappingURL=intake.js.map
|