fapony 0.2.1 → 0.3.3
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/README.md +95 -72
- package/fapony.ts +12 -5
- package/package.json +5 -4
- package/skill/define-convention/SKILL.md +77 -0
- package/skill/lookup-before-edit/SKILL.md +48 -0
- package/skill/move-to-done/SKILL.md +19 -30
- package/skill/review-pony/SKILL.md +38 -61
- package/src/analyze.ts +1 -1
- package/src/debt/cli.ts +193 -0
- package/src/debt/format.ts +107 -0
- package/src/debt/index.ts +19 -0
- package/src/debt/load.ts +92 -0
- package/src/debt/promotion.ts +152 -0
- package/src/debt/scan.ts +214 -0
- package/src/debt/types.ts +79 -0
- package/src/detect.ts +92 -0
- package/src/gate.ts +3 -3
- package/src/hook.ts +349 -100
- package/src/init-mem.ts +57 -71
- package/src/init.ts +12 -16
- package/src/install/antigravity.ts +112 -0
- package/src/install/claude.ts +16 -123
- package/src/install/codex.ts +58 -19
- package/src/install/detect.ts +17 -7
- package/src/install/opencode.ts +131 -6
- package/src/install.ts +12 -3
- package/src/lint-baseline.ts +2 -2
- package/src/mcp/primitives.ts +1 -1
- package/src/mcp/tools/index.ts +13 -102
- package/src/mcp/tools/mem.ts +71 -0
- package/src/mcp/transport.ts +8 -94
- package/src/mcp/worktree.ts +1 -1
- package/src/mem/commands/read.ts +231 -141
- package/src/mem/index.ts +4 -13
- package/src/memory.ts +17 -8
- package/src/{plan-seed.ts → seed/plan-seed.ts} +14 -32
- package/src/seed/primitives.ts +66 -0
- package/src/{review-seed.ts → seed/review-seed.ts} +7 -54
- package/src/session/helpers.ts +1 -1
- package/src/session/registry.ts +3 -6
- package/src/setup.ts +1 -1
- package/src/stats/data.ts +1 -1
- package/src/telemetry.ts +1 -1
- package/src/util.ts +61 -0
- package/templates/SPEC.md +8 -1
- package/images/logo.png +0 -0
- package/images/logo.webp +0 -0
- package/images/logo@400.webp +0 -0
- package/images/sample.webp +0 -0
- package/images/summary.webp +0 -0
- package/src/debt.ts +0 -811
- package/src/math.ts +0 -13
- package/src/mcp/tools/usage.ts +0 -211
- package/src/mcp/tools/verdict.ts +0 -161
package/src/math.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// src/math.ts — pure numeric helpers shared across modules
|
|
2
|
-
|
|
3
|
-
/** Minutes between two ISO-like timestamps (space separator, UTC assumed). */
|
|
4
|
-
export function minutesBetween(a: string, b: string): number {
|
|
5
|
-
const t0 = new Date(`${a.replace(" ", "T")}Z`).getTime();
|
|
6
|
-
const t1 = new Date(`${b.replace(" ", "T")}Z`).getTime();
|
|
7
|
-
return (t1 - t0) / 60000;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Arithmetic mean of a numeric array. 0 for empty arrays. */
|
|
11
|
-
export function avg(xs: number[]): number {
|
|
12
|
-
return xs.length ? xs.reduce((s, x) => s + x, 0) / xs.length : 0;
|
|
13
|
-
}
|
package/src/mcp/tools/usage.ts
DELETED
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
// src/mcp/tools/usage.ts — fapony_usage tool
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type ImputeSummary,
|
|
5
|
-
imputeResult,
|
|
6
|
-
loadPrices,
|
|
7
|
-
type PriceTable,
|
|
8
|
-
} from "../../price/index.js";
|
|
9
|
-
import {
|
|
10
|
-
CLIENTS,
|
|
11
|
-
mergeBytesByTool,
|
|
12
|
-
type PassiveUsageResult,
|
|
13
|
-
} from "../../session/index.js";
|
|
14
|
-
import { jsonResult, type ToolResult } from "../types.js";
|
|
15
|
-
|
|
16
|
-
/** list-price summary per client — additive, leaves existing lines untouched */
|
|
17
|
-
function imputationOf(
|
|
18
|
-
result: PassiveUsageResult,
|
|
19
|
-
prices: PriceTable | null,
|
|
20
|
-
): (ImputeSummary & { prices_fetched_at: string }) | null {
|
|
21
|
-
if (result.session_count === 0 || !prices) return null;
|
|
22
|
-
return {
|
|
23
|
-
...imputeResult(result, prices),
|
|
24
|
-
prices_fetched_at: prices.fetched_at,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function imputedTextLines(
|
|
29
|
-
label: string,
|
|
30
|
-
result: PassiveUsageResult,
|
|
31
|
-
prices: PriceTable | null,
|
|
32
|
-
): string[] {
|
|
33
|
-
if (result.session_count === 0) return [];
|
|
34
|
-
const imp = imputationOf(result, prices);
|
|
35
|
-
if (!imp)
|
|
36
|
-
return [` ${label}list-price equivalent: — (run \`fapony price-scan\`)`];
|
|
37
|
-
const parts = [
|
|
38
|
-
`~$${imp.total_imputed.toFixed(4)} over ${imp.priced_sessions} priced sessions`,
|
|
39
|
-
];
|
|
40
|
-
if (imp.free_sessions > 0) parts.push(`${imp.free_sessions} free`);
|
|
41
|
-
if (imp.unpriced_sessions > 0)
|
|
42
|
-
parts.push(
|
|
43
|
-
`unpriced: ${imp.unpriced_sessions} sessions / ${imp.unpriced_tokens.toLocaleString()} tokens`,
|
|
44
|
-
);
|
|
45
|
-
return [` ${label}list-price equivalent: ${parts.join(" · ")}`];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** list price per model appended to the model name — only rows where the client records no cost */
|
|
49
|
-
function imputedSuffix(
|
|
50
|
-
provider: string,
|
|
51
|
-
model: string,
|
|
52
|
-
imp: (ImputeSummary & { prices_fetched_at: string }) | null,
|
|
53
|
-
): string {
|
|
54
|
-
if (!imp) return "";
|
|
55
|
-
const m = imp.by_model.find(
|
|
56
|
-
(b) => b.provider === provider && b.model === model,
|
|
57
|
-
);
|
|
58
|
-
if (m && m.status === "priced")
|
|
59
|
-
return ` (~$${m.imputed_cost.toFixed(4)} list-price)`;
|
|
60
|
-
return "";
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function toolPassiveUsage(args: Record<string, unknown>): ToolResult {
|
|
64
|
-
const worktree =
|
|
65
|
-
typeof args.worktree === "string" ? args.worktree : undefined;
|
|
66
|
-
const since = typeof args.since === "number" ? args.since : undefined;
|
|
67
|
-
const until = typeof args.until === "number" ? args.until : undefined;
|
|
68
|
-
const detail = args.detail === true;
|
|
69
|
-
|
|
70
|
-
const primary = CLIENTS.find((c) => c.primary)!;
|
|
71
|
-
const others = CLIENTS.filter((c) => !c.primary);
|
|
72
|
-
|
|
73
|
-
const data: PassiveUsageResult = primary.read(worktree, since, until, detail);
|
|
74
|
-
|
|
75
|
-
// Every non-primary client is always fetched when its own log exists.
|
|
76
|
-
const otherResults = others.map((client) => ({
|
|
77
|
-
client,
|
|
78
|
-
result: client.read(worktree, since, until, detail),
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
|
-
// list prices from the cache only — read once per call, not per section
|
|
82
|
-
const prices = loadPrices();
|
|
83
|
-
|
|
84
|
-
if (args.json === true) {
|
|
85
|
-
const json: Record<string, unknown> = {
|
|
86
|
-
...data,
|
|
87
|
-
imputation: imputationOf(data, prices),
|
|
88
|
-
};
|
|
89
|
-
for (const { client, result } of otherResults) {
|
|
90
|
-
json[client.key] =
|
|
91
|
-
result.session_count > 0
|
|
92
|
-
? { ...result, imputation: imputationOf(result, prices) }
|
|
93
|
-
: null;
|
|
94
|
-
}
|
|
95
|
-
return jsonResult(json);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const lines: string[] = [];
|
|
99
|
-
lines.push("Passive Usage Report");
|
|
100
|
-
lines.push("====================");
|
|
101
|
-
lines.push(`Sessions: ${data.session_count}`);
|
|
102
|
-
lines.push(`Total Input Tokens: ${data.total_tokens_input.toLocaleString()}`);
|
|
103
|
-
lines.push(
|
|
104
|
-
`Total Output Tokens: ${data.total_tokens_output.toLocaleString()}`,
|
|
105
|
-
);
|
|
106
|
-
lines.push(
|
|
107
|
-
`Total Reasoning Tokens: ${data.total_tokens_reasoning.toLocaleString()}`,
|
|
108
|
-
);
|
|
109
|
-
lines.push(`Cache Read: ${data.total_tokens_cache_read.toLocaleString()}`);
|
|
110
|
-
lines.push(`Cache Write: ${data.total_tokens_cache_write.toLocaleString()}`);
|
|
111
|
-
lines.push(`Total Cost: $${data.total_cost.toFixed(4)}`);
|
|
112
|
-
lines.push(...imputedTextLines("", data, prices));
|
|
113
|
-
|
|
114
|
-
if (data.by_model.length > 0) {
|
|
115
|
-
lines.push("");
|
|
116
|
-
lines.push("By Model:");
|
|
117
|
-
lines.push("--------");
|
|
118
|
-
for (const m of data.by_model) {
|
|
119
|
-
const prefix = m.provider ? `${m.provider}/` : "";
|
|
120
|
-
lines.push(
|
|
121
|
-
` ${prefix}${m.model}: ${m.session_count} sessions, ` +
|
|
122
|
-
`${m.tokens_input.toLocaleString()} in / ${m.tokens_output.toLocaleString()} out, ` +
|
|
123
|
-
`$${m.cost.toFixed(4)}`,
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// detail:true is opt-in — default text above is byte-identical to before.
|
|
129
|
-
if (detail && data.detail) {
|
|
130
|
-
lines.push("");
|
|
131
|
-
lines.push("Detail (tool activity — signal, not quality):");
|
|
132
|
-
lines.push(` steps: ${data.detail.steps.toLocaleString()}`);
|
|
133
|
-
const tools = Object.entries(data.detail.tool_breakdown).sort(
|
|
134
|
-
(a, b) => b[1] - a[1],
|
|
135
|
-
);
|
|
136
|
-
if (tools.length > 0) {
|
|
137
|
-
lines.push(" by tool:");
|
|
138
|
-
for (const [tool, count] of tools.slice(0, 20)) {
|
|
139
|
-
lines.push(` ${tool}: ${count.toLocaleString()}`);
|
|
140
|
-
}
|
|
141
|
-
if (tools.length > 20) {
|
|
142
|
-
lines.push(` ... +${tools.length - 20} more (see json:true)`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
// Context bytes by tool — proportion of context window consumed per tool.
|
|
146
|
-
// Bytes are a proxy for tokens; shown as % of total, never as "tokens".
|
|
147
|
-
// Merged across all clients: OpenCode/ZCode (state.output), Claude Code
|
|
148
|
-
// and Codex (tool_result) each populate bytes_by_tool, so reading the
|
|
149
|
-
// top-level (opencode) detail alone would miss the other clients.
|
|
150
|
-
const bytes = mergeBytesByTool(
|
|
151
|
-
data.detail,
|
|
152
|
-
...otherResults.map((r) => r.result.detail),
|
|
153
|
-
);
|
|
154
|
-
if (bytes) {
|
|
155
|
-
const entries = Object.entries(bytes).sort((a, b) => b[1] - a[1]);
|
|
156
|
-
const total = entries.reduce((s, e) => s + e[1], 0);
|
|
157
|
-
if (entries.length > 0 && total > 0) {
|
|
158
|
-
lines.push(" context bytes by tool (% of total):");
|
|
159
|
-
for (const [tool, b] of entries.slice(0, 10)) {
|
|
160
|
-
const pct = ((b / total) * 100).toFixed(1);
|
|
161
|
-
lines.push(` ${tool}: ${pct}%`);
|
|
162
|
-
}
|
|
163
|
-
if (entries.length > 10) {
|
|
164
|
-
lines.push(` ... +${entries.length - 10} more (see json:true)`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
lines.push(` sessions with activity: ${data.detail.by_session.length}`);
|
|
169
|
-
const top = data.detail.by_session.slice(0, 10);
|
|
170
|
-
for (const s of top) {
|
|
171
|
-
const topTool = Object.entries(s.tools).sort((a, b) => b[1] - a[1])[0];
|
|
172
|
-
lines.push(
|
|
173
|
-
` ${s.session_id}: ${s.steps} steps` +
|
|
174
|
-
(topTool ? `, top tool ${topTool[0]}×${topTool[1]}` : ""),
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
if (data.detail.by_session.length > 10) {
|
|
178
|
-
lines.push(
|
|
179
|
-
` ... +${data.detail.by_session.length - 10} more (see json:true)`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// One text section per non-primary client — same shape for every client,
|
|
185
|
-
// new ones need no new formatting code, only a registry.ts entry.
|
|
186
|
-
for (const { client, result } of otherResults) {
|
|
187
|
-
if (result.session_count === 0) continue;
|
|
188
|
-
const imp = imputationOf(result, prices);
|
|
189
|
-
const label = client.reportLabel ?? client.key;
|
|
190
|
-
lines.push("");
|
|
191
|
-
lines.push(`${label} usage:`);
|
|
192
|
-
lines.push(
|
|
193
|
-
` total: ${result.total_tokens_input.toLocaleString()} in / ${result.total_tokens_output.toLocaleString()} out / ${result.total_tokens_reasoning.toLocaleString()} reasoning tokens over ${result.session_count} sessions`,
|
|
194
|
-
);
|
|
195
|
-
lines.push(
|
|
196
|
-
` cache: ${result.total_tokens_cache_read.toLocaleString()} read / ${result.total_tokens_cache_write.toLocaleString()} write`,
|
|
197
|
-
);
|
|
198
|
-
if (result.by_model.length > 0) {
|
|
199
|
-
lines.push(" by model:");
|
|
200
|
-
for (const m of result.by_model) {
|
|
201
|
-
const prefix = m.provider ? `${m.provider}/` : "";
|
|
202
|
-
lines.push(
|
|
203
|
-
` ${prefix}${m.model}: ${m.tokens_input.toLocaleString()} in / ${m.tokens_output.toLocaleString()} out (cache r/w: ${(m.tokens_cache_read ?? 0).toLocaleString()} / ${(m.tokens_cache_write ?? 0).toLocaleString()})${imputedSuffix(m.provider, m.model, imp)}`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
lines.push(...imputedTextLines("", result, prices));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
211
|
-
}
|
package/src/mcp/tools/verdict.ts
DELETED
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
// src/mcp/tools/verdict.ts — verdict_submit tool
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
findOpenRun,
|
|
5
|
-
findOpenRunWithNullPlan,
|
|
6
|
-
getRun,
|
|
7
|
-
newRun,
|
|
8
|
-
openDb,
|
|
9
|
-
patchLastGateEvent,
|
|
10
|
-
} from "../../db/index.js";
|
|
11
|
-
import { gateOnce } from "../../gate.js";
|
|
12
|
-
import { VERDICT_GRADES, type VerdictGrade } from "../../parse.js";
|
|
13
|
-
import {
|
|
14
|
-
errorResult,
|
|
15
|
-
jsonResult,
|
|
16
|
-
REASON_CODES,
|
|
17
|
-
REGIME_CODES,
|
|
18
|
-
type ReasonCode,
|
|
19
|
-
type RegimeCode,
|
|
20
|
-
type ToolResult,
|
|
21
|
-
} from "../types.js";
|
|
22
|
-
import { resolveWorktreeArg } from "../worktree.js";
|
|
23
|
-
|
|
24
|
-
// --- Tool implementation ---
|
|
25
|
-
|
|
26
|
-
export function toolVerdictSubmit(args: Record<string, unknown>): ToolResult {
|
|
27
|
-
const {
|
|
28
|
-
run_id,
|
|
29
|
-
verdict,
|
|
30
|
-
reason_code,
|
|
31
|
-
regime,
|
|
32
|
-
note,
|
|
33
|
-
worktree,
|
|
34
|
-
plan,
|
|
35
|
-
session_id,
|
|
36
|
-
files,
|
|
37
|
-
} = args;
|
|
38
|
-
|
|
39
|
-
if (typeof verdict !== "string" || !VERDICT_GRADES.has(verdict)) {
|
|
40
|
-
return errorResult(
|
|
41
|
-
`verdict must be one of: ${[...VERDICT_GRADES].join(", ")}`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
const grade = verdict as VerdictGrade;
|
|
45
|
-
if (!REASON_CODES.includes(reason_code as ReasonCode)) {
|
|
46
|
-
return errorResult(
|
|
47
|
-
`reason_code must be one of: ${REASON_CODES.join(", ")}`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (reason_code === "other" && (!note || typeof note !== "string")) {
|
|
51
|
-
return errorResult("reason_code 'other' requires a note");
|
|
52
|
-
}
|
|
53
|
-
if (!REGIME_CODES.includes(regime as RegimeCode)) {
|
|
54
|
-
return errorResult(`regime must be one of: ${REGIME_CODES.join(", ")}`);
|
|
55
|
-
}
|
|
56
|
-
const regimeCode = regime as RegimeCode;
|
|
57
|
-
|
|
58
|
-
const db = openDb();
|
|
59
|
-
|
|
60
|
-
// Resolve or create run_id
|
|
61
|
-
let resolvedRunId: number;
|
|
62
|
-
if (typeof run_id === "number" && Number.isInteger(run_id)) {
|
|
63
|
-
const run = getRun(db, run_id);
|
|
64
|
-
if (!run) {
|
|
65
|
-
return jsonResult({ stored: false, error: "run not found" });
|
|
66
|
-
}
|
|
67
|
-
resolvedRunId = run_id;
|
|
68
|
-
} else {
|
|
69
|
-
// Bind to the latest still-open run for the same worktree+plan so a
|
|
70
|
-
// round-2+ verdict lands on the original row (round keeps counting and
|
|
71
|
-
// review.maxRounds can actually trigger). Only when no open run matches
|
|
72
|
-
// is a fresh row created. plan=null always opens a new run (no guess).
|
|
73
|
-
// Free-text plans are normalized (trim+lowercase) so "Fix Login" and
|
|
74
|
-
// "fix login" bind to the same run. When no exact match exists, falls
|
|
75
|
-
// back to any open run with plan=null (the "no PLAN file" flow).
|
|
76
|
-
const resolvedWorktree =
|
|
77
|
-
typeof worktree === "string" && worktree
|
|
78
|
-
? resolveWorktreeArg(worktree)
|
|
79
|
-
: "mcp-external";
|
|
80
|
-
const resolvedPlan =
|
|
81
|
-
typeof plan === "string" && plan ? plan.trim().toLowerCase() : null;
|
|
82
|
-
// BEGIN IMMEDIATE makes the find-then-create below atomic across
|
|
83
|
-
// concurrent MCP processes: without it, two agents closing the same
|
|
84
|
-
// worktree+plan at once can both see "no open run" and each insert a
|
|
85
|
-
// row. The write lock taken here blocks (busy_timeout, not throws) the
|
|
86
|
-
// second caller until the first commits, so it re-reads and finds the
|
|
87
|
-
// row already there instead of duplicating it.
|
|
88
|
-
db.run("BEGIN IMMEDIATE");
|
|
89
|
-
try {
|
|
90
|
-
let open = resolvedPlan
|
|
91
|
-
? findOpenRun(db, resolvedWorktree, resolvedPlan)
|
|
92
|
-
: null;
|
|
93
|
-
// Fallback: if no exact match and plan is non-null, try any open run with
|
|
94
|
-
// plan=null. This lets a verdict with free-text intent bind to a run that
|
|
95
|
-
// was created without a plan (the common "no PLAN file" flow).
|
|
96
|
-
if (!open && resolvedPlan) {
|
|
97
|
-
open = findOpenRunWithNullPlan(db, resolvedWorktree);
|
|
98
|
-
}
|
|
99
|
-
if (open) {
|
|
100
|
-
resolvedRunId = open.id;
|
|
101
|
-
} else {
|
|
102
|
-
// worktree/plan let callers (e.g. move-to-done) attribute the verdict
|
|
103
|
-
// so byReasonCode/bestPassing aggregate correctly instead of collapsing
|
|
104
|
-
// into "mcp-external".
|
|
105
|
-
resolvedRunId = newRun(db, resolvedWorktree, resolvedPlan, null, "mcp");
|
|
106
|
-
}
|
|
107
|
-
db.run("COMMIT");
|
|
108
|
-
} catch (e) {
|
|
109
|
-
db.run("ROLLBACK");
|
|
110
|
-
throw e;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Normalize files: must be a non-empty array of strings.
|
|
115
|
-
const resolvedFiles =
|
|
116
|
-
Array.isArray(files) && files.length > 0
|
|
117
|
-
? files.filter((f): f is string => typeof f === "string" && f.length > 0)
|
|
118
|
-
: undefined;
|
|
119
|
-
|
|
120
|
-
// Route through gateOnce for consistent status/round/memory handling.
|
|
121
|
-
const mcpNote =
|
|
122
|
-
typeof note === "string" && note
|
|
123
|
-
? `[${reason_code}] ${note}`
|
|
124
|
-
: `[${reason_code}]`;
|
|
125
|
-
const result = gateOnce(resolvedRunId, grade, mcpNote, resolvedFiles);
|
|
126
|
-
|
|
127
|
-
if (result.error) {
|
|
128
|
-
return jsonResult({
|
|
129
|
-
stored: false,
|
|
130
|
-
error: result.error,
|
|
131
|
-
run_id: resolvedRunId,
|
|
132
|
-
status: result.status,
|
|
133
|
-
round: result.round,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Patch the gate event with MCP-specific fields (reason_code, source).
|
|
138
|
-
// session_id lets gates.ts resolve model from client session logs when the
|
|
139
|
-
// window has no spawn events (the old execute→review loop that wrote spawns
|
|
140
|
-
// is gone). Optional — agents that can't expose it just omit it.
|
|
141
|
-
const patch: Record<string, unknown> = {
|
|
142
|
-
reason_code,
|
|
143
|
-
regime: regimeCode,
|
|
144
|
-
source: "mcp",
|
|
145
|
-
};
|
|
146
|
-
if (typeof session_id === "string" && session_id) {
|
|
147
|
-
patch.session_id = session_id;
|
|
148
|
-
}
|
|
149
|
-
patchLastGateEvent(db, resolvedRunId, patch);
|
|
150
|
-
|
|
151
|
-
return jsonResult({
|
|
152
|
-
stored: true,
|
|
153
|
-
run_id: resolvedRunId,
|
|
154
|
-
verdict: grade,
|
|
155
|
-
reason_code,
|
|
156
|
-
regime: regimeCode,
|
|
157
|
-
status: result.status,
|
|
158
|
-
round: result.round,
|
|
159
|
-
...(resolvedFiles ? { files: resolvedFiles } : {}),
|
|
160
|
-
});
|
|
161
|
-
}
|