Merge pull request #203 from multica-ai/forrestchang/finance-benchmark

fix: agent stability, tooling, and sub-agent orchestration
This commit is contained in:
Jiayuan Zhang 2026-02-15 20:50:44 +08:00 committed by GitHub
commit 74c0ca0ddc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 294 additions and 66 deletions

View file

@ -7,7 +7,7 @@
*/
import { join } from "node:path";
import { Agent } from "@multica/core";
import { Agent, Hub, listSubagentRuns } from "@multica/core";
import type { AgentOptions } from "@multica/core";
import type { ToolsConfig } from "@multica/core";
import { DATA_DIR } from "@multica/utils";
@ -192,35 +192,90 @@ export async function runCommand(args: string[]): Promise<void> {
const enableRunLog = opts.runLog || !!process.env.MULTICA_RUN_LOG;
const agent = new Agent({
profileId: opts.profile,
provider: opts.provider,
model: opts.model,
apiKey: opts.apiKey,
baseUrl: opts.baseUrl,
systemPrompt: opts.system,
thinkingLevel: opts.thinking as any,
reasoningMode: opts.reasoning as AgentOptions["reasoningMode"],
cwd: opts.cwd,
sessionId: opts.session,
debug: opts.debug,
enableRunLog,
tools: toolsConfig,
});
// Initialize Hub to enable full agent capabilities (sub-agents, channels, cron).
// Matches Desktop environment where Hub is always active.
// Gateway connection failures are non-blocking (auto-reconnect with backoff).
const gatewayUrl = process.env.GATEWAY_URL || "http://localhost:3000";
const hub = new Hub(gatewayUrl);
const sessionDir = join(DATA_DIR, "sessions", agent.sessionId);
try {
const agent = new Agent({
profileId: opts.profile,
provider: opts.provider,
model: opts.model,
apiKey: opts.apiKey,
baseUrl: opts.baseUrl,
systemPrompt: opts.system,
thinkingLevel: opts.thinking as any,
reasoningMode: opts.reasoning as AgentOptions["reasoningMode"],
cwd: opts.cwd,
sessionId: opts.session,
debug: opts.debug,
enableRunLog,
tools: toolsConfig,
});
// If it's a newly created session, notify user of sessionId
if (!opts.session) {
console.error(`[session: ${agent.sessionId}]`);
}
if (enableRunLog) {
console.error(`[session-dir: ${sessionDir}]`);
}
const sessionDir = join(DATA_DIR, "sessions", agent.sessionId);
const result = await agent.run(finalPrompt);
if (result.error) {
console.error(`Error: ${result.error}`);
process.exitCode = 1;
// If it's a newly created session, notify user of sessionId
if (!opts.session) {
console.error(`[session: ${agent.sessionId}]`);
}
if (enableRunLog) {
console.error(`[session-dir: ${sessionDir}]`);
}
const result = await agent.run(finalPrompt);
if (result.error) {
console.error(`Error: ${result.error}`);
process.exitCode = 1;
}
// Wait for sub-agents to complete and parent to process their results.
// Without this, CLI exits before sub-agent announcements are delivered.
await waitForSubagents(agent);
} finally {
hub.shutdown();
}
}
/**
* Wait for any running sub-agents to complete, then output their findings.
*
* In CLI mode, the parent Agent is not registered with the Hub, so the normal
* announce flow (Hub writeInternal) can't deliver results. Instead, we poll
* the registry and print findings directly once all sub-agents finish.
*
* Max wait: 30 minutes (matches default sub-agent timeout).
*/
async function waitForSubagents(agent: Agent): Promise<void> {
const MAX_WAIT_MS = 30 * 60 * 1000;
const POLL_INTERVAL_MS = 2000;
const start = Date.now();
const allRuns = listSubagentRuns(agent.sessionId);
if (allRuns.length === 0) return;
// Phase 1: Wait for all sub-agent runs to finish
while (Date.now() - start < MAX_WAIT_MS) {
const runs = listSubagentRuns(agent.sessionId);
const running = runs.filter((r) => !r.endedAt);
if (running.length === 0) break;
console.error(dim(`[waiting for ${running.length} sub-agent(s)...]`));
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
}
// Phase 2: Output sub-agent findings directly (bypasses Hub announce flow)
const completedRuns = listSubagentRuns(agent.sessionId).filter((r) => r.endedAt);
if (completedRuns.length === 0) return;
console.error(dim(`[${completedRuns.length} sub-agent(s) completed]`));
for (const run of completedRuns) {
const displayName = run.label || run.task.slice(0, 60);
const status = run.outcome?.status ?? "unknown";
const findings = run.findings || "(no output)";
console.log(`\n--- Sub-agent: ${displayName} [${status}] ---`);
console.log(findings);
}
}

View file

@ -36,6 +36,7 @@ export type CredentialsConfig = {
};
const DEFAULT_CREDENTIALS_PATH = join(DATA_DIR, "credentials.json5");
const FALLBACK_CREDENTIALS_PATH = join(homedir(), ".super-multica", "credentials.json5");
function expandHome(value: string): string {
if (value === "~") return homedir();
@ -53,9 +54,34 @@ function isTestEnv(): boolean {
);
}
/**
* Resolve the credentials file path.
*
* Lookup order:
* 1. SMC_CREDENTIALS_PATH env var (explicit override)
* 2. {DATA_DIR}/credentials.json5 (current data dir, respects SMC_DATA_DIR)
* 3. ~/.super-multica/credentials.json5 (default location fallback
* allows E2E tests and other custom SMC_DATA_DIR setups to
* share the production credentials)
*/
export function getCredentialsPath(): string {
const raw = process.env.SMC_CREDENTIALS_PATH ?? DEFAULT_CREDENTIALS_PATH;
return expandHome(raw);
// Explicit env override — use as-is
if (process.env.SMC_CREDENTIALS_PATH) {
return expandHome(process.env.SMC_CREDENTIALS_PATH);
}
// Primary: current DATA_DIR
if (existsSync(DEFAULT_CREDENTIALS_PATH)) {
return DEFAULT_CREDENTIALS_PATH;
}
// Fallback: default ~/.super-multica location when using a custom data dir
if (DEFAULT_CREDENTIALS_PATH !== FALLBACK_CREDENTIALS_PATH && existsSync(FALLBACK_CREDENTIALS_PATH)) {
return FALLBACK_CREDENTIALS_PATH;
}
// Return primary path even if it doesn't exist (for error messages / creation)
return DEFAULT_CREDENTIALS_PATH;
}
export class CredentialManager {

View file

@ -25,7 +25,10 @@
* - `tool_start` Tool execution begins.
* Fields: tool (name), args (first 500 chars of JSON)
* - `tool_end` Tool execution completes.
* Fields: tool (name), duration_ms, is_error
* Fields: tool (name), duration_ms, is_error, result_chars, result_summary?, error_type?
* result_chars: total character count of result content (survives session compaction)
* result_summary: short tool-specific summary (e.g. "10 results", "12.5KB", "finance/get_price_snapshot")
* error_type: error category when tool returned an error (e.g. "fetch_failed", "ssrf_blocked")
*
* ### Context Management Preflight (before LLM call)
* - `preflight_compact_start` Preflight compaction triggered.

View file

@ -85,6 +85,51 @@ export function isRotatableError(reason: AuthProfileFailureReason): boolean {
return reason === "auth" || reason === "rate_limit" || reason === "billing" || reason === "timeout";
}
// ── Run-log result extraction helpers ──────────────────────────────────────
// Lightweight extractors for tool_end metadata. These mirror the patterns in
// cli/output.ts but are kept separate to avoid CLI-specific dependencies.
function extractRunLogResultText(result: unknown): string | undefined {
if (!result || typeof result !== "object") return undefined;
const msg = result as { content?: Array<{ type: string; text?: string }> };
if (Array.isArray(msg.content)) {
for (const c of msg.content) {
if (c.type === "text" && c.text) return c.text;
}
}
return undefined;
}
function extractRunLogResultDetails(result: unknown): Record<string, unknown> | null {
const text = extractRunLogResultText(result);
if (text) {
try { return JSON.parse(text) as Record<string, unknown>; } catch { /* non-JSON result */ }
}
const withDetails = result as { details?: unknown };
if (withDetails?.details && typeof withDetails.details === "object") {
return withDetails.details as Record<string, unknown>;
}
return null;
}
function formatRunLogToolSummary(tool: string, details: Record<string, unknown> | null): string | undefined {
if (!details) return undefined;
if (details.error) return `error: ${details.code || details.message || details.error}`;
switch (tool) {
case "web_search": return `${details.count ?? 0} results`;
case "web_fetch": {
const parts: string[] = [];
if (typeof details.length === "number") parts.push(`${(details.length as number / 1024).toFixed(1)}KB`);
if (details.cached) parts.push("cached");
return parts.join(", ") || undefined;
}
case "data": return `${details.domain}/${details.action}`;
case "glob": return `${details.count ?? 0} files`;
case "exec": return details.exitCode !== undefined ? `exit ${details.exitCode}` : undefined;
default: return undefined;
}
}
export class Agent {
private readonly agent: PiAgentCore;
private output;
@ -348,9 +393,12 @@ export class Agent {
const profileToolsConfig = this.profile?.getToolsConfig();
const mergedToolsConfig = mergeToolsConfig(profileToolsConfig, options.tools);
const profileDir = this.profile?.getProfileDir();
// Use this.sessionId (which may be auto-generated) instead of options.sessionId
// (which may be undefined). Without this, sessions_list and sessions_spawn
// can't find sub-agent runs because they have no session context.
this.toolsOptions = mergedToolsConfig
? { ...options, cwd: effectiveCwd, tools: mergedToolsConfig, profileDir, provider: this.resolvedProvider }
: { ...options, cwd: effectiveCwd, profileDir, provider: this.resolvedProvider };
? { ...options, sessionId: this.sessionId, cwd: effectiveCwd, tools: mergedToolsConfig, profileDir, provider: this.resolvedProvider }
: { ...options, sessionId: this.sessionId, cwd: effectiveCwd, profileDir, provider: this.resolvedProvider };
const tools = resolveTools(this.toolsOptions);
if (this.debug) {
@ -746,11 +794,24 @@ export class Agent {
const startTime = this.toolStartTimes.get(toolName);
const duration_ms = startTime ? Date.now() - startTime : undefined;
this.toolStartTimes.delete(toolName);
this.runLog.log("tool_end", {
// Extract result metadata for run-log persistence (survives session compaction)
const result = (event as any).result;
const resultText = extractRunLogResultText(result);
const resultChars = resultText?.length ?? 0;
const details = extractRunLogResultDetails(result);
const toolEndData: Record<string, unknown> = {
tool: toolName,
duration_ms,
is_error: (event as any).isError ?? false,
});
result_chars: resultChars,
result_summary: formatRunLogToolSummary(toolName, details),
};
if (details?.error) {
toolEndData.error_type = details.code ? String(details.code) : String(details.error);
}
this.runLog.log("tool_end", toolEndData);
}
}

View file

@ -5,6 +5,7 @@
* Compatible with OpenClaw/AgentSkills specification
*/
import { dirname } from "path";
import type { Skill, SkillManagerOptions, SkillsConfig, SkillCommandSpec, SkillInvocationResult } from "./types.js";
import { loadAllSkills, getProfileSkillsDir, initializeManagedSkills, getManagedSkillsDir } from "./loader.js";
import {
@ -332,6 +333,11 @@ export class SkillManager {
parts.push(`## ${emoji} ${name} (${id})`);
parts.push(`${desc}\n`);
// Include skill directory path so the agent can resolve relative paths
// (e.g., scripts/recalc.py → /absolute/path/to/skill/scripts/recalc.py)
const skillDir = dirname(skill.filePath);
parts.push(`**Skill directory**: \`${skillDir}\`\n`);
// Include full instructions
if (skill.instructions) {
parts.push(skill.instructions);

View file

@ -47,6 +47,8 @@ describe("sessions_list tool", () => {
startedAt: now - 60000,
endedAt: now - 30000,
outcome: { status: "ok" },
findings: "All tests passed successfully.",
findingsCaptured: true,
}),
);
seedSubagentRunForTests(
@ -56,6 +58,8 @@ describe("sessions_list tool", () => {
startedAt: now - 60000,
endedAt: now,
outcome: { status: "error", error: "timeout" },
findings: "Lint check timed out.",
findingsCaptured: true,
}),
);
@ -71,6 +75,13 @@ describe("sessions_list tool", () => {
expect((text as { text: string }).text).toContain("Code Review");
expect((text as { text: string }).text).toContain("Test Analysis");
expect((text as { text: string }).text).toContain("Lint Check");
// Verify full runId is shown for completed runs
expect((text as { text: string }).text).toContain("id:run-aaa");
expect((text as { text: string }).text).toContain("id:run-bbb");
expect((text as { text: string }).text).toContain("id:run-ccc");
// Verify findings are shown for completed runs
expect((text as { text: string }).text).toContain("All tests passed successfully.");
expect((text as { text: string }).text).toContain("Lint check timed out.");
expect(result.details!.runs).toHaveLength(3);
expect(result.details!.runs[0]!.status).toBe("running");
@ -141,6 +152,44 @@ describe("sessions_list tool", () => {
expect(result.details).toEqual({ runs: [] });
});
it("shows findings for grouped completed runs", async () => {
const now = Date.now();
const groupId = "group-001";
seedSubagentRunForTests(
makeRecord({
runId: "run-g1",
label: "Bull Case Research",
startedAt: now - 60000,
endedAt: now - 10000,
outcome: { status: "ok" },
findings: "AI infrastructure capex growing 40% YoY.",
findingsCaptured: true,
groupId,
}),
);
seedSubagentRunForTests(
makeRecord({
runId: "run-g2",
label: "Bear Case Research",
startedAt: now - 60000,
endedAt: now - 5000,
outcome: { status: "ok" },
findings: "Valuation risk: forward P/E above historical average.",
findingsCaptured: true,
groupId,
}),
);
const tool = createSessionsListTool({ sessionId: "parent-001" });
const result = await tool.execute("call-1", {});
const text = (result.content[0] as { text: string }).text;
expect(text).toContain("id:run-g1");
expect(text).toContain("id:run-g2");
expect(text).toContain("AI infrastructure capex growing 40% YoY.");
expect(text).toContain("Valuation risk: forward P/E above historical average.");
});
it("shows findings status for running task", async () => {
const now = Date.now();
seedSubagentRunForTests(

View file

@ -212,10 +212,13 @@ export function createSessionsListTool(
const status = resolveStatus(r);
if (status === "running") {
const elapsed = r.startedAt ? formatElapsed(now - r.startedAt) : "just spawned";
statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed})`);
statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed}) id:${r.runId}`);
} else {
const elapsed = r.startedAt && r.endedAt ? formatElapsed(r.endedAt - r.startedAt) : "";
statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed})`);
const findings = r.findingsCaptured
? (r.findings ? r.findings.slice(0, 4000) + (r.findings.length > 4000 ? "…" : "") : "(no output)")
: "(findings not yet captured)";
statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed}) id:${r.runId}\n Findings: ${findings}`);
}
}
}
@ -227,13 +230,13 @@ export function createSessionsListTool(
const status = resolveStatus(r);
if (status === "running") {
const elapsed = r.startedAt ? formatElapsed(now - r.startedAt) : "just spawned";
statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed})`);
statusLines.push(` ${idx}. [RUNNING] "${displayName}" (${elapsed}) id:${r.runId}`);
} else {
const elapsed = r.startedAt && r.endedAt ? formatElapsed(r.endedAt - r.startedAt) : "";
const findings = r.findingsCaptured
? (r.findings ? r.findings.slice(0, 200) + (r.findings.length > 200 ? "…" : "") : "(no output)")
? (r.findings ? r.findings.slice(0, 4000) + (r.findings.length > 4000 ? "…" : "") : "(no output)")
: "(findings not yet captured)";
statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed})\n Findings: ${findings}`);
statusLines.push(` ${idx}. [${status.toUpperCase()}] "${displayName}" (${elapsed}) id:${r.runId}\n Findings: ${findings}`);
}
}

View file

@ -1,7 +1,11 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, beforeEach } from "vitest";
import { createSessionsSpawnTool } from "./sessions-spawn.js";
import { getSubagentGroup, resetSubagentRegistryForTests } from "../subagent/registry.js";
describe("sessions_spawn tool", () => {
beforeEach(() => {
resetSubagentRegistryForTests();
});
it("has correct name and description", () => {
const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "test-session" });
expect(tool.name).toBe("sessions_spawn");
@ -24,6 +28,23 @@ describe("sessions_spawn tool", () => {
expect(firstContent.text).toContain("not allowed");
});
it("auto-creates group when custom groupId is provided", async () => {
const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "parent-session" });
// Should not error — the group is auto-created
await tool.execute(
"call-group",
{ task: "research topic", label: "Research", groupId: "my-custom-group" } as any,
new AbortController().signal,
);
// Verify group was created in the registry
const group = getSubagentGroup("my-custom-group");
expect(group).toBeDefined();
expect(group!.groupId).toBe("my-custom-group");
expect(group!.label).toBe("Group: Research");
});
it("fails gracefully when Hub is not initialized", async () => {
const tool = createSessionsSpawnTool({ isSubagent: false, sessionId: "parent-session" });

View file

@ -126,19 +126,20 @@ export function createSessionsSpawnTool(
const runId = uuidv7();
const childSessionId = uuidv7();
// Validate groupId if provided
// Auto-create group when groupId is provided but doesn't exist yet,
// or when `next` is provided without a groupId.
if (groupId) {
const existingGroup = getSubagentGroup(groupId);
if (!existingGroup) {
return {
content: [{ type: "text", text: `Error: group not found: ${groupId}. Use the groupId returned by a previous sessions_spawn call.` }],
details: { status: "error", error: `group not found: ${groupId}` },
};
// LLM provided a custom groupId — auto-create the group
createSubagentGroup({
groupId,
requesterSessionId,
label: label ? `Group: ${label}` : undefined,
next,
});
}
}
// Auto-create group when `next` is provided without an existing groupId
if (!groupId && next) {
} else if (next) {
groupId = uuidv7();
createSubagentGroup({
groupId,

View file

@ -343,12 +343,14 @@ export function createWebFetchTool(): AgentTool<typeof WebFetchSchema, unknown>
} catch (error) {
if (error instanceof SsrfBlockedError) {
return jsonResult({
error: "ssrf_blocked",
error: true,
code: "ssrf_blocked",
message: error.message,
});
}
return jsonResult({
error: "fetch_failed",
error: true,
code: "fetch_failed",
message: error instanceof Error ? error.message : String(error),
});
}

View file

@ -135,7 +135,8 @@ export function createWebSearchTool(): AgentTool<typeof WebSearchSchema, unknown
return jsonResult(result);
} catch (error) {
return jsonResult({
error: "search_failed",
error: true,
code: "search_failed",
message: error instanceof Error ? error.message : String(error),
});
}

20
pnpm-lock.yaml generated
View file

@ -15944,9 +15944,9 @@ snapshots:
'@typescript-eslint/eslint-plugin': 8.55.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-expo: 1.0.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2(jiti@2.6.1))
globals: 16.5.0
@ -15961,8 +15961,8 @@ snapshots:
'@next/eslint-plugin-next': 16.1.6
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
@ -15984,7 +15984,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@nolyfill/is-core-module': 1.0.39
debug: 4.4.3
@ -15995,18 +15995,18 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
'@typescript-eslint/parser': 8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@ -16019,7 +16019,7 @@ snapshots:
- supports-color
- typescript
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
@ -16030,7 +16030,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3

View file

@ -151,7 +151,7 @@ This applies to ALL calculations - totals, percentages, ratios, differences, etc
4. **Save**: Write to file
5. **Recalculate formulas (MANDATORY IF USING FORMULAS)**: Use the scripts/recalc.py script
```bash
python scripts/recalc.py output.xlsx
python3 scripts/recalc.py output.xlsx
```
6. **Verify and fix any errors**:
- The script returns JSON with error details
@ -224,12 +224,12 @@ wb.save('modified.xlsx')
Excel files created or modified by openpyxl contain formulas as strings but not calculated values. Use the provided `scripts/recalc.py` script to recalculate formulas:
```bash
python scripts/recalc.py <excel_file> [timeout_seconds]
python3 scripts/recalc.py <excel_file> [timeout_seconds]
```
Example:
```bash
python scripts/recalc.py output.xlsx 30
python3 scripts/recalc.py output.xlsx 30
```
The script: