@lazyingart/agintiflow 0.9.0 → 0.11.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/README.md CHANGED
@@ -71,6 +71,8 @@ Inside chat, type normal requests such as `write a small Python CLI app with tes
71
71
 
72
72
  For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSeek v4 pro by default and exposes `apply_patch` as a deterministic workspace tool. It supports exact replacements, Codex-style patch envelopes, and unified diffs, with preflight checks, path guardrails, hashes, and compact per-file diffs. See [docs/patch-tools.md](docs/patch-tools.md).
73
73
 
74
+ For larger repositories, use `--profile large-codebase` or choose **Large codebase engineering** in the web UI. The web default stays **Auto**, and Auto now escalates codebase/system/debugging prompts to the same engineering loop when needed. Complex work routes to DeepSeek v4 pro, starts with `inspect_project`, then uses search/read/patch/check loops inspired by Codex, Copilot SDK, Claude Code, Gemini CLI, Qwen, and Claw Code. See [docs/large-codebase-engineering.md](docs/large-codebase-engineering.md).
75
+
74
76
  For raster image work, AgInTiFlow has an optional `image_generation` skill backed by the `generate_image` tool and a local `GRSAI` key. The skill tells DeepSeek when image generation is appropriate; the tool calls GRS AI Nano Banana, saves manifests/images under `artifacts/images`, and sends the result to the canvas. See [docs/auxiliary-image-generation.md](docs/auxiliary-image-generation.md).
75
77
 
76
78
  Launch the local web UI from an installed package:
@@ -0,0 +1,75 @@
1
+ # Large Codebase Engineering
2
+
3
+ AgInTiFlow now treats large or complicated coding tasks as a different operating mode from short file edits.
4
+
5
+ ## What Was Borrowed
6
+
7
+ Local agent references informed the design:
8
+
9
+ - Codex-style editing: read/search first, apply deterministic patches, record diffs and hashes, then run checks.
10
+ - Copilot-style SDK surfaces: structured tools, session persistence, plan/history/workspace APIs, and explicit permission hooks.
11
+ - Claude/Claw-style safety: project-local status, container-first execution, read-only operations by default, and clear failure recovery.
12
+ - Gemini/Qwen-style extensibility: capability discovery through profiles and tools rather than hardcoding one model behavior.
13
+ - Claw-style `doctor` discipline: check health and environment before treating system symptoms as code bugs.
14
+ - Claude/Codex-style context discipline: read instructions, manifests, entry points, and failing tests before touching broad files.
15
+
16
+ ## Skill vs Tool
17
+
18
+ The `large-codebase` profile is a skill: it changes the model’s engineering behavior. It tells DeepSeek v4 pro to orient first, plan minimally, patch incrementally, and verify.
19
+
20
+ The `inspect_project` function is a tool: it deterministically scans the workspace and returns:
21
+
22
+ - top-level files and directories
23
+ - manifest files such as `package.json`, `pyproject.toml`, `Cargo.toml`, and `go.mod`
24
+ - package scripts
25
+ - likely source and test directories
26
+ - language and extension counts
27
+ - recommended files to read next
28
+
29
+ ## Recommended Loop
30
+
31
+ For complicated tasks, the agent should follow this loop:
32
+
33
+ 1. `inspect_project` to map the repository.
34
+ 2. `read_file` on `AGENTS.md`, `README.md`, and manifests.
35
+ 3. `search_files` for symbols, tests, errors, routes, or config names.
36
+ 4. `read_file` only on the files needed for the change.
37
+ 5. `apply_patch` in small coherent batches.
38
+ 6. `run_command` for the narrowest relevant check first.
39
+ 7. Broaden checks only after the focused check passes.
40
+
41
+ ## CLI And Web Parity
42
+
43
+ Both CLI and web use the same task profile registry and the same model/tool schemas. Use either:
44
+
45
+ ```bash
46
+ aginti --profile large-codebase "fix the failing tests"
47
+ ```
48
+
49
+ or choose **Large codebase engineering** in the web task-profile dropdown.
50
+
51
+ Smart routing sends this profile to DeepSeek v4 pro even when the user prompt is short.
52
+
53
+ ## Auto Profile Behavior
54
+
55
+ The web app still defaults to **Auto**. Auto does not mean weak. When the prompt mentions a large repo, system bug, failing tests, setup, install, migration, or a known language stack, AgInTiFlow adds engineering guidance and raises the step budget automatically.
56
+
57
+ Examples:
58
+
59
+ ```bash
60
+ aginti "debug this Python project system bug and fix failing tests"
61
+ aginti "fix the Rust workspace build"
62
+ aginti "repair the Docker setup and run the Node tests"
63
+ ```
64
+
65
+ These route to DeepSeek v4 pro when the complexity score is high enough.
66
+
67
+ ## Cross-Language Playbook
68
+
69
+ AgInTiFlow gives DeepSeek stack-specific reminders without hardcoding a solution:
70
+
71
+ - JS/TS: inspect package scripts and lockfiles, then run focused `node`, `tsc`, or test commands.
72
+ - Python: inspect `pyproject.toml` or requirements, prefer project-local venv/conda/Docker, then run focused pytest/module checks.
73
+ - Rust/Go/JVM/C/C++: inspect native manifests, format only touched files when possible, and start with narrow build/test targets.
74
+ - R/Stan/LaTeX: keep toolchains project-local or Docker-backed, compile from the right directory, and publish useful artifacts to canvas.
75
+ - System tasks: diagnose first, capture versions/logs, write reversible scripts, use Docker for installs, and avoid silent host-level changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
package/public/app.js CHANGED
@@ -775,6 +775,25 @@ function renderTaskProfiles(selected = "auto") {
775
775
  taskProfileField.value = profiles.some((profile) => profile.id === selected) ? selected : "auto";
776
776
  }
777
777
 
778
+ const COMPLEX_ENGINEERING_HINT = /\b(large|complex|complicated|monorepo|codebase|repository|repo-wide|multi[- ]file|cross[- ]file|architecture|refactor|migration|regression|root cause|failing tests?|fix build|system bug|debug|performance|security)\b/i;
779
+
780
+ function recommendedMaxStepsForProfile(profile = "auto", goal = "") {
781
+ if (profile === "large-codebase") return 36;
782
+ if (profile === "latex") return 30;
783
+ if (COMPLEX_ENGINEERING_HINT.test(goal || "")) return 36;
784
+ if (/\b(latex|tex|pdflatex|latexmk|pdf|website|app|docker|system|install|setup|debug)\b/i.test(goal || "")) return 30;
785
+ return 24;
786
+ }
787
+
788
+ function ensureRecommendedMaxStepsForCurrentTask() {
789
+ const maxStepsField = document.querySelector("#maxSteps");
790
+ const goalField = document.querySelector("#goal");
791
+ const recommended = recommendedMaxStepsForProfile(taskProfileField?.value || "auto", goalField?.value || "");
792
+ if (maxStepsField && Number(maxStepsField.value || 0) < recommended) {
793
+ maxStepsField.value = String(recommended);
794
+ }
795
+ }
796
+
778
797
  function renderWrapperStatus(wrappers = lastWrappers) {
779
798
  lastWrappers = wrappers || [];
780
799
  if (lastWrappers.length === 0) {
@@ -1959,7 +1978,10 @@ sandboxModeField.addEventListener("change", updatePackageWarning);
1959
1978
  packageInstallPolicyField.addEventListener("change", updatePackageWarning);
1960
1979
  allowWrapperToolsField.addEventListener("change", () => renderWrapperStatus());
1961
1980
  preferredWrapperField.addEventListener("change", () => renderWrapperStatus());
1962
- taskProfileField?.addEventListener("change", schedulePreferenceSave);
1981
+ taskProfileField?.addEventListener("change", () => {
1982
+ ensureRecommendedMaxStepsForCurrentTask();
1983
+ schedulePreferenceSave();
1984
+ });
1963
1985
 
1964
1986
  saveApiKeyButton?.addEventListener("click", async () => {
1965
1987
  const provider = setupProviderField.value || "deepseek";
@@ -2035,6 +2057,7 @@ form.addEventListener("submit", async (event) => {
2035
2057
  setLogs(t("goalRequired"), "empty");
2036
2058
  return;
2037
2059
  }
2060
+ ensureRecommendedMaxStepsForCurrentTask();
2038
2061
 
2039
2062
  const payload = {
2040
2063
  ...formPayload(),
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { repairModelMessageHistory, runAgent } from "../src/agent-runner.js";
7
7
  import { resolveRuntimeConfig } from "../src/config.js";
8
+ import { engineeringGuidanceForTask, recommendedMaxStepsForTask } from "../src/engineering-guidance.js";
8
9
  import { selectModelRoute } from "../src/model-routing.js";
9
10
  import { SessionStore } from "../src/session-store.js";
10
11
  import { executeWorkspaceTool } from "../src/workspace-tools.js";
@@ -86,6 +87,71 @@ try {
86
87
  goal: "patch this large codebase and migrate the database tests",
87
88
  });
88
89
  assert(/pro/i.test(patchRoute.model), "patch/refactor task did not route to DeepSeek pro");
90
+ const largeProfileRoute = selectModelRoute({
91
+ routingMode: "smart",
92
+ provider: "deepseek",
93
+ goal: "fix this bug",
94
+ taskProfile: "large-codebase",
95
+ });
96
+ assert(/pro/i.test(largeProfileRoute.model), "large-codebase profile did not route to DeepSeek pro");
97
+ const autoSystemRoute = selectModelRoute({
98
+ routingMode: "smart",
99
+ provider: "deepseek",
100
+ goal: "debug this Python project system bug and fix failing tests",
101
+ taskProfile: "auto",
102
+ });
103
+ assert(/pro/i.test(autoSystemRoute.model), "auto system/code problem did not route to DeepSeek pro");
104
+ assert(
105
+ recommendedMaxStepsForTask({
106
+ goal: "debug this Python project system bug and fix failing tests",
107
+ taskProfile: "auto",
108
+ complexityScore: autoSystemRoute.complexityScore,
109
+ }) >= 36,
110
+ "auto system/code problem did not get engineering step budget"
111
+ );
112
+ const guidance = engineeringGuidanceForTask("debug this Python project system bug and fix failing tests", "auto");
113
+ assert(guidance.includes("Python:"), "engineering guidance did not include Python stack advice");
114
+ assert(guidance.includes("System/shell:"), "engineering guidance did not include system stack advice");
115
+
116
+ await fs.mkdir(path.join(workspace, "src"), { recursive: true });
117
+ await fs.mkdir(path.join(workspace, "test"), { recursive: true });
118
+ await fs.writeFile(
119
+ path.join(workspace, "package.json"),
120
+ JSON.stringify(
121
+ {
122
+ name: "agintiflow-inspect-smoke",
123
+ scripts: {
124
+ test: "node --test test/index.test.js",
125
+ check: "node --check src/index.js",
126
+ },
127
+ },
128
+ null,
129
+ 2
130
+ ),
131
+ "utf8"
132
+ );
133
+ await fs.writeFile(path.join(workspace, "src/index.js"), "export function answer() { return 42; }\n", "utf8");
134
+ await fs.writeFile(path.join(workspace, "test/index.test.js"), "import test from 'node:test';\n", "utf8");
135
+ const inspected = await executeWorkspaceTool(
136
+ "inspect_project",
137
+ { path: ".", maxDepth: 4, limit: 200 },
138
+ {
139
+ commandCwd: workspace,
140
+ allowFileTools: true,
141
+ }
142
+ );
143
+ assert(inspected.ok, "inspect_project failed");
144
+ assert(inspected.manifestFiles.some((item) => item.path === "package.json"), "inspect_project did not find package.json");
145
+ assert(inspected.packageScripts.some((item) => item.name === "test"), "inspect_project did not extract package scripts");
146
+ assert(inspected.sourceDirs.some((item) => item.path === "src"), "inspect_project did not identify src directory");
147
+ assert(inspected.testFiles.some((item) => item.path === "test/index.test.js"), "inspect_project did not identify test file");
148
+ assert(inspected.recommendedReads.includes("package.json"), "inspect_project did not recommend package.json");
149
+
150
+ const inspectRun = await runMock("Inspect this large codebase and recommend next reads.", "coding-inspect");
151
+ assert(
152
+ inspectRun.events.some((event) => event.type === "tool.completed" && event.data?.toolName === "inspect_project"),
153
+ "mock large-codebase run did not use inspect_project"
154
+ );
89
155
 
90
156
  const writeRun = await runMock("Create notes/hello.md with a short coding smoke message.", "coding-write");
91
157
  const written = await fs.readFile(path.join(workspace, "notes/hello.md"), "utf8");
@@ -216,6 +282,11 @@ try {
216
282
  checks: [
217
283
  "deepseek_history_repair",
218
284
  "deepseek_pro_patch_route",
285
+ "large_profile_pro_route",
286
+ "auto_system_pro_route",
287
+ "auto_engineering_guidance",
288
+ "inspect_project",
289
+ "mock_inspect_project",
219
290
  "write_file",
220
291
  "duplicate_write_failed",
221
292
  "resume_session_write",
@@ -18,6 +18,7 @@ import { executeWorkspaceTool, resolveWorkspacePath, summarizeWorkspaceTools, WO
18
18
  import { normalizeCanvasPayload } from "./artifact-tunnel.js";
19
19
  import { getTaskProfile } from "./task-profiles.js";
20
20
  import { generateImage, listAuxiliarySkills } from "./auxiliary-tools.js";
21
+ import { engineeringGuidanceForTask } from "./engineering-guidance.js";
21
22
 
22
23
  const exec = promisify(execCallback);
23
24
  const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
@@ -233,6 +234,7 @@ export function repairModelMessageHistory(state, config = {}) {
233
234
  function createInitialState(config, sessionId) {
234
235
  const now = new Date().toISOString();
235
236
  const taskProfile = getTaskProfile(config.taskProfile);
237
+ const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
236
238
  return {
237
239
  sessionId,
238
240
  createdAt: now,
@@ -272,7 +274,7 @@ function createInitialState(config, sessionId) {
272
274
  : "A host shell command tool is available under the configured trust policy."
273
275
  : "No shell command tool is available.",
274
276
  config.allowFileTools
275
- ? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. apply_patch supports exact single-file replacements plus Codex-style/unified multi-file patches; prefer it for source edits after reading/searching the relevant context. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked. For generated local websites/pages, use open_workspace_file or preview_workspace instead of starting a localhost server inside Docker.`
277
+ ? `Workspace file tools are available in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. For large or unfamiliar repositories, call inspect_project first, then search/read exact files before editing. apply_patch supports exact single-file replacements plus Codex-style/unified multi-file patches; prefer it for source edits after reading/searching the relevant context. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked. For generated local websites/pages, use open_workspace_file or preview_workspace instead of starting a localhost server inside Docker.`
276
278
  : "No workspace file tools are available.",
277
279
  config.allowWrapperTools
278
280
  ? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
@@ -283,10 +285,11 @@ function createInitialState(config, sessionId) {
283
285
  .join(", ")}. Use generate_image for real raster image/photo/illustration/cover/poster/logo requests when appropriate; if the key is missing, ask the user to run /auxilliary grsai or aginti login grsai.`
284
286
  : "Auxiliary skills are disabled for this run.",
285
287
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
288
+ engineeringGuidance,
286
289
  "A frontend canvas/artifacts tunnel exists. Use send_to_canvas when important markdown, diffs, screenshots, images, or workspace files should be highlighted in the UI. It is optional and ordinary final text can still go directly to finish.",
287
290
  "For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
288
- "Work like a practical coding agent: inspect when useful, patch code with apply_patch, run safe checks when they add confidence, iterate on failures, and keep outputs inside the workspace.",
289
- "For large projects, decompose into useful files and milestones, implement a coherent minimal version first, then iterate with checks rather than only describing what you would do.",
291
+ "Work like a practical coding agent: orient with inspect_project/search/read, patch code with apply_patch, run safe checks when they add confidence, iterate on failures, and keep outputs inside the workspace.",
292
+ "For large projects, decompose into useful files and milestones, identify entry points/tests/contracts first, implement a coherent minimal version, then iterate with checks rather than only describing what you would do.",
290
293
  "For website/app/code/LaTeX/Python/C/shell tasks, create or edit real workspace files, run available build/compile/test commands, and surface artifacts through the canvas when useful.",
291
294
  "For research or web-search tasks, use browser tools or safe shell network tools when the current policy allows; cite or save useful sources in workspace notes when the task needs traceability.",
292
295
  "Use the canvas tunnel for outputs the user would likely want to inspect visually, such as figures, PDFs, screenshots, images, important markdown, or generated files.",
@@ -310,7 +313,7 @@ function createInitialState(config, sessionId) {
310
313
  : `Shell working directory: ${config.commandCwd}`
311
314
  : "",
312
315
  config.allowFileTools
313
- ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. Local preview tools available: open_workspace_file and preview_workspace.`
316
+ ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large/unfamiliar codebases. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. Local preview tools available: open_workspace_file and preview_workspace.`
314
317
  : "",
315
318
  config.allowWrapperTools
316
319
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
@@ -321,9 +324,10 @@ function createInitialState(config, sessionId) {
321
324
  .join(" ")}`
322
325
  : "",
323
326
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
327
+ engineeringGuidance,
324
328
  "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
325
329
  "Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
326
- "Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request.",
330
+ "Use file, shell, browser, canvas, and wrapper tools when they are useful; choose the workflow from the user's request. For complicated engineering tasks, keep a tight loop: inspect, choose minimal files, patch, run focused checks, repair, then summarize.",
327
331
  "Do not stop at a plan when tools can accomplish the request. Continue through implementation, checks, artifact selection, and finish.",
328
332
  "Use the configured sandbox and package policy for environment or system-maintenance work.",
329
333
  ]
@@ -410,6 +414,7 @@ function applyContinuationPrompt(state, config, observers) {
410
414
  if (!config.resume || !config.goal) return;
411
415
 
412
416
  const taskProfile = getTaskProfile(config.taskProfile);
417
+ const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
413
418
  ensureChatState(state);
414
419
  state.goal = config.goal;
415
420
  state.provider = config.provider;
@@ -430,12 +435,13 @@ function applyContinuationPrompt(state, config, observers) {
430
435
  : `Shell working directory: ${config.commandCwd}`
431
436
  : "",
432
437
  config.allowFileTools
433
- ? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths. For generated local files/sites, use open_workspace_file or preview_workspace.`
438
+ ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it accepts exact replacements or Codex-style/unified multi-file patches. For generated local files/sites, use open_workspace_file or preview_workspace.`
434
439
  : "",
435
440
  config.allowWrapperTools
436
441
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
437
442
  : "",
438
443
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
444
+ engineeringGuidance,
439
445
  ]
440
446
  .filter(Boolean)
441
447
  .join("\n"),
@@ -631,7 +637,7 @@ async function captureSyntheticSnapshot(store, step, config) {
631
637
  : `Shell tool available in: ${config.commandCwd}`
632
638
  : "Shell tool disabled.",
633
639
  config.allowFileTools
634
- ? `Workspace file tools available in: ${config.commandCwd}. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches.`
640
+ ? `Workspace file tools available in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Use workspace-relative paths. Use apply_patch for code edits; it supports exact single-file replacement and multi-file Codex-style/unified patches.`
635
641
  : "Workspace file tools disabled.",
636
642
  config.allowWrapperTools
637
643
  ? `Agent wrappers available: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
@@ -815,6 +821,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
815
821
  await new Promise((resolve) => setTimeout(resolve, Number.isFinite(args.ms) ? Number(args.ms) : 1000));
816
822
  }
817
823
  break;
824
+ case "inspect_project":
818
825
  case "list_files":
819
826
  case "read_file":
820
827
  case "search_files":
package/src/cli.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  showProjectSession,
16
16
  } from "./project.js";
17
17
  import { listTaskProfiles } from "./task-profiles.js";
18
+ import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
18
19
  import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
19
20
  import fs from "node:fs/promises";
20
21
  import path from "node:path";
@@ -256,7 +257,12 @@ function agentDefaults(args) {
256
257
  sandboxMode: args.sandboxMode || "docker-workspace",
257
258
  packageInstallPolicy: args.packageInstallPolicy || "allow",
258
259
  useDockerSandbox: args.useDockerSandbox ?? true,
259
- maxSteps: args.maxSteps || (args.latex || args.taskProfile === "latex" ? 30 : 24),
260
+ maxSteps:
261
+ args.maxSteps ||
262
+ recommendedMaxStepsForTask({
263
+ goal: args.goal || "",
264
+ taskProfile: args.taskProfile || (args.latex ? "latex" : "auto"),
265
+ }),
260
266
  };
261
267
 
262
268
  if (defaults.sandboxMode === "host") {
package/src/config.js CHANGED
@@ -5,6 +5,7 @@ import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-p
5
5
  import { normalizeWrapperName } from "./tool-wrappers.js";
6
6
  import { loadProjectEnv, resolveProjectRoot } from "./project.js";
7
7
  import { normalizeTaskProfile } from "./task-profiles.js";
8
+ import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
8
9
 
9
10
  function parseBoolean(value, fallback) {
10
11
  if (value === undefined) return fallback;
@@ -33,14 +34,21 @@ export function resolveRuntimeConfig(args, overrides = {}) {
33
34
  process.env.AGENT_PROVIDER ||
34
35
  (process.env.DEEPSEEK_API_KEY ? "deepseek" : process.env.OPENAI_API_KEY ? "openai" : "deepseek");
35
36
  const routingMode = normalizeRoutingMode(overrides.routingMode || args.routingMode || process.env.AGENT_ROUTING_MODE || "smart");
37
+ const taskProfile = normalizeTaskProfile(overrides.taskProfile || args.taskProfile || process.env.AGINTI_TASK_PROFILE || "auto");
36
38
  const route = selectModelRoute({
37
39
  routingMode,
38
40
  provider: requestedProvider,
39
41
  model: overrides.model || args.model || process.env.LLM_MODEL || "",
40
42
  goal: args.goal || "",
43
+ taskProfile,
41
44
  });
42
45
 
43
46
  const defaults = getProviderDefaults(route.provider);
47
+ const defaultMaxSteps = recommendedMaxStepsForTask({
48
+ goal: args.goal || "",
49
+ taskProfile,
50
+ complexityScore: route.complexityScore,
51
+ });
44
52
  const packageDir = path.resolve(overrides.packageDir || process.env.AGINTIFLOW_PACKAGE_DIR || baseDir);
45
53
  const dockerRequested = parseBoolean(overrides.useDockerSandbox ?? args.useDockerSandbox ?? process.env.USE_DOCKER_SANDBOX, true);
46
54
  const requestedSandboxMode =
@@ -56,7 +64,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
56
64
  resume: args.resume || "",
57
65
  sessionId: overrides.sessionId || args.sessionId || process.env.SESSION_ID || `web-agent-${crypto.randomUUID()}`,
58
66
  routingMode,
59
- taskProfile: normalizeTaskProfile(overrides.taskProfile || args.taskProfile || process.env.AGINTI_TASK_PROFILE || "auto"),
67
+ taskProfile,
60
68
  routeReason: route.reason,
61
69
  routeComplexityScore: route.complexityScore,
62
70
  requestedProvider,
@@ -65,7 +73,7 @@ export function resolveRuntimeConfig(args, overrides = {}) {
65
73
  apiKey: overrides.apiKey || defaults.apiKey,
66
74
  baseURL: overrides.baseURL || defaults.baseURL,
67
75
  model: route.model || defaults.model,
68
- maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, 24),
76
+ maxSteps: parseNumber(overrides.maxSteps ?? args.maxSteps ?? process.env.MAX_STEPS, defaultMaxSteps),
69
77
  headless: parseBoolean(overrides.headless ?? args.headless ?? process.env.HEADLESS, false),
70
78
  allowedDomains: Array.isArray(overrides.allowedDomains)
71
79
  ? overrides.allowedDomains
@@ -0,0 +1,102 @@
1
+ import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
2
+
3
+ const LANGUAGE_HINTS = [
4
+ {
5
+ id: "javascript-typescript",
6
+ pattern: /\b(node|npm|pnpm|yarn|bun|javascript|typescript|react|vue|svelte|next\.?js|vite|express)\b/i,
7
+ text:
8
+ "JS/TS: inspect package.json and lockfiles, identify package manager, use npm/pnpm/yarn scripts before inventing commands, prefer targeted node --check/tsc/test runs before broad builds.",
9
+ },
10
+ {
11
+ id: "python",
12
+ pattern: /\b(python|pytest|pip|uv|poetry|conda|venv|jupyter|fastapi|django|flask|pandas|numpy)\b/i,
13
+ text:
14
+ "Python: inspect pyproject/requirements, prefer project-local venv/uv/conda or Docker, run python -m pytest or focused module checks, avoid global package installs on host.",
15
+ },
16
+ {
17
+ id: "rust",
18
+ pattern: /\b(rust|cargo|crate|clippy|rustfmt|tokio|actix)\b/i,
19
+ text:
20
+ "Rust: inspect Cargo.toml/workspace crates, run cargo fmt/check/test on the narrowest crate first, preserve Cargo.lock discipline, avoid broad workspace runs until focused checks pass.",
21
+ },
22
+ {
23
+ id: "go",
24
+ pattern: /\b(golang|go test|go mod|goroutine|gin|grpc)\b/i,
25
+ text:
26
+ "Go: inspect go.mod, use go test ./pkg-or-target first, run gofmt on touched files, avoid changing module paths unless required.",
27
+ },
28
+ {
29
+ id: "java-jvm",
30
+ pattern: /\b(java|kotlin|gradle|maven|spring|junit|jvm)\b/i,
31
+ text:
32
+ "JVM: inspect pom.xml/build.gradle/settings.gradle, use focused Maven/Gradle test targets, keep generated build outputs out of source patches.",
33
+ },
34
+ {
35
+ id: "c-cpp",
36
+ pattern: /\b(c\+\+|cpp|cmake|makefile|gcc|clang|native|segfault|asan|valgrind)\b/i,
37
+ text:
38
+ "C/C++: inspect CMake/Make/build scripts, prefer out-of-tree builds, run compile-only or narrow tests first, use sanitizers only when available and safe.",
39
+ },
40
+ {
41
+ id: "shell-system",
42
+ pattern: /\b(shell|bash|zsh|system|systemd|docker|linux|ubuntu|debian|apt|yum|dnf|brew|service|permission denied|port|network)\b/i,
43
+ text:
44
+ "System/shell: diagnose first with read-only commands, capture versions/logs, make reversible scripts, use Docker for installs/toolchains, and only use host-level changes when policy explicitly allows them.",
45
+ },
46
+ {
47
+ id: "r-stats",
48
+ pattern: /\b(rstats|r language|cmdstanr|stan|renv|tidyverse|shiny)\b/i,
49
+ text:
50
+ "R/Stan: inspect renv/DESCRIPTION and project notes, prefer project-local libraries or Docker, validate scripts with non-interactive Rscript commands when available.",
51
+ },
52
+ {
53
+ id: "latex",
54
+ pattern: /\b(latex|tex|pdflatex|latexmk|bibtex|biber|pdf)\b/i,
55
+ text:
56
+ "LaTeX: keep source/figures together, compile from the document directory, run enough passes for refs/bibliography, publish PDF/source artifacts to canvas.",
57
+ },
58
+ ];
59
+
60
+ const COMPLEX_ENGINEERING_PATTERN =
61
+ /\b(large|complex|complicated|monorepo|codebase|repository|repo-wide|multi[- ]file|cross[- ]file|architecture|refactor|migration|regression|root cause|failing tests?|fix build|system bug|debug|performance|security)\b/i;
62
+
63
+ export function engineeringGuidanceForTask(goal = "", taskProfile = "auto") {
64
+ const normalizedProfile = normalizeTaskProfile(taskProfile);
65
+ const text = String(goal || "");
66
+ const matched = LANGUAGE_HINTS.filter((hint) => hint.pattern.test(text));
67
+ const wantsComplex =
68
+ normalizedProfile === "large-codebase" ||
69
+ normalizedProfile === "maintenance" ||
70
+ COMPLEX_ENGINEERING_PATTERN.test(text) ||
71
+ text.length > 500;
72
+
73
+ if (!wantsComplex && matched.length === 0) return "";
74
+
75
+ const lines = [
76
+ "Engineering operating mode:",
77
+ "Use the proven coding-agent loop: inspect_project, read instructions/manifests, search exact symbols/errors, patch small coherent batches, run focused checks, repair failures, then summarize changed files and residual risks.",
78
+ "Keep CLI and web behavior equivalent: use the same workspace, sessions, profiles, file tools, shell policy, Docker mounts, and canvas artifacts.",
79
+ "For large repositories, preserve context by reading fewer but more relevant files; prefer deterministic tools and diffs over long model memory.",
80
+ "For system repair, act like a doctor: gather evidence first, avoid silent destructive host changes, prefer Docker or project-local scripts for installs, and make every stronger action explicit in logs.",
81
+ ];
82
+
83
+ if (matched.length > 0) {
84
+ lines.push("Stack-specific checks:");
85
+ for (const hint of matched.slice(0, 5)) lines.push(`- ${hint.text}`);
86
+ }
87
+
88
+ return lines.join("\n");
89
+ }
90
+
91
+ export function recommendedMaxStepsForTask({ goal = "", taskProfile = "auto", complexityScore = 0 } = {}) {
92
+ const normalizedProfile = normalizeTaskProfile(taskProfile);
93
+ const profileDefault = defaultMaxStepsForProfile(normalizedProfile);
94
+ const text = String(goal || "");
95
+ if (normalizedProfile === "large-codebase" || complexityScore >= 3 || COMPLEX_ENGINEERING_PATTERN.test(text)) {
96
+ return Math.max(profileDefault, 36);
97
+ }
98
+ if (/\b(latex|tex|pdflatex|latexmk|pdf|website|app|docker|system|install|setup|debug)\b/i.test(text)) {
99
+ return Math.max(profileDefault, 30);
100
+ }
101
+ return profileDefault;
102
+ }
@@ -5,7 +5,8 @@ import { runAgent } from "./agent-runner.js";
5
5
  import { loadConfig } from "./config.js";
6
6
  import { initProject, listProjectSessions, providerKeyStatus, setProviderKey } from "./project.js";
7
7
  import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
8
- import { normalizeTaskProfile } from "./task-profiles.js";
8
+ import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
9
+ import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
9
10
  import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
10
11
 
11
12
  const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
@@ -439,7 +440,10 @@ function createState(args = {}) {
439
440
  preferredWrapper: args.preferredWrapper || "codex",
440
441
  taskProfile: normalizeTaskProfile(args.taskProfile || "auto"),
441
442
  headless: args.headless ?? false,
442
- maxSteps: Number.isFinite(args.maxSteps) && args.maxSteps > 0 ? args.maxSteps : args.latex ? 30 : 24,
443
+ maxSteps:
444
+ Number.isFinite(args.maxSteps) && args.maxSteps > 0
445
+ ? args.maxSteps
446
+ : defaultMaxStepsForProfile(args.taskProfile || (args.latex ? "latex" : "auto")),
443
447
  sessionId: args.resume || "",
444
448
  };
445
449
  }
@@ -589,6 +593,7 @@ async function handleCommand(line, state, packageDir) {
589
593
  }
590
594
  if (command === "profile") {
591
595
  state.taskProfile = normalizeTaskProfile(value || "auto");
596
+ state.maxSteps = Math.max(state.maxSteps, defaultMaxStepsForProfile(state.taskProfile));
592
597
  printSystemLine(`profile=${state.taskProfile}`);
593
598
  return true;
594
599
  }
@@ -669,6 +674,13 @@ async function handleCommand(line, state, packageDir) {
669
674
 
670
675
  async function runPrompt(prompt, state, packageDir) {
671
676
  const controller = new AbortController();
677
+ const runMaxSteps = Math.max(
678
+ state.maxSteps,
679
+ recommendedMaxStepsForTask({
680
+ goal: prompt,
681
+ taskProfile: state.taskProfile,
682
+ })
683
+ );
672
684
  const config = loadConfig(
673
685
  {
674
686
  provider: state.provider,
@@ -684,7 +696,7 @@ async function runPrompt(prompt, state, packageDir) {
684
696
  allowDestructive: state.allowDestructive,
685
697
  preferredWrapper: state.preferredWrapper,
686
698
  taskProfile: state.taskProfile,
687
- maxSteps: state.maxSteps,
699
+ maxSteps: runMaxSteps,
688
700
  headless: state.headless,
689
701
  resume: state.sessionId,
690
702
  goal: prompt,
@@ -2,6 +2,7 @@ import OpenAI from "openai";
2
2
  import { normalizeWrapperName, wrapperStatusText } from "./tool-wrappers.js";
3
3
  import { getTaskProfile } from "./task-profiles.js";
4
4
  import { listAuxiliarySkills } from "./auxiliary-tools.js";
5
+ import { engineeringGuidanceForTask } from "./engineering-guidance.js";
5
6
 
6
7
  export function createClient(config) {
7
8
  if (config.provider === "mock") {
@@ -81,6 +82,13 @@ function mockPathForGoal(goal = "") {
81
82
  function mockWorkspaceToolForGoal(goal = "") {
82
83
  const text = String(goal).toLowerCase();
83
84
  const targetPath = mockPathForGoal(goal);
85
+ if (/inspect|map|overview|architecture|large codebase|large repo|repository|repo\b|codebase/.test(text)) {
86
+ return mockToolCall("inspect_project", {
87
+ path: ".",
88
+ maxDepth: 6,
89
+ limit: 400,
90
+ });
91
+ }
84
92
  if (/patch|replace|edit/.test(text)) {
85
93
  if (/multi|codex|unified|several|multiple/i.test(text)) {
86
94
  return mockToolCall("apply_patch", {
@@ -166,6 +174,7 @@ function mockChatResponse(content, toolCalls = []) {
166
174
 
167
175
  export async function createPlan(client, config, state) {
168
176
  const taskProfile = getTaskProfile(config.taskProfile);
177
+ const engineeringGuidance = engineeringGuidanceForTask(state.goal, config.taskProfile);
169
178
  if (client.mock) {
170
179
  return [
171
180
  "1. Inspect the request and prefer the local shell when available.",
@@ -194,7 +203,7 @@ export async function createPlan(client, config, state) {
194
203
  ? `Shell tool is enabled in ${config.commandCwd}. In Docker, this path is mounted as /workspace with persistent /aginti-env and /aginti-cache mounts. Use relative paths or /workspace paths, not absolute host temp paths. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}. For npm/pip/conda/venv setup, explain the need and wait for approval unless policy is allow.`
195
204
  : "",
196
205
  config.allowFileTools
197
- ? `Workspace file tools are enabled in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. apply_patch supports exact single-file replacements and Codex-style/unified multi-file patches; prefer it for edits after reading relevant context. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
206
+ ? `Workspace file tools are enabled in ${config.commandCwd}: inspect_project, list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, preview_workspace. For large or unfamiliar repos, plan to call inspect_project first, then search/read exact files. apply_patch supports exact single-file replacements and Codex-style/unified multi-file patches; prefer it for edits after reading relevant context. Keep all paths workspace-relative, for example plot_fx.svg or docs/report.tex, and avoid secrets. For generated local HTML/SVG/PDF/static sites, plan to use open_workspace_file or preview_workspace rather than starting a localhost server inside Docker.`
198
207
  : "",
199
208
  config.allowWrapperTools
200
209
  ? `Agent wrappers are enabled. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Status: ${wrapperStatusText()}.`
@@ -205,6 +214,7 @@ export async function createPlan(client, config, state) {
205
214
  .join(", ")}. For raster image generation requests, plan to use generate_image when a GRSAI key is available; otherwise ask the user to run /auxilliary grsai or aginti login grsai.`
206
215
  : "Auxiliary skills are disabled for this run.",
207
216
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
217
+ engineeringGuidance,
208
218
  "A canvas/artifacts tunnel is available through send_to_canvas. Use it when an output should be highlighted visually, such as screenshots, image files, important markdown, diffs, or generated artifact paths. It is optional for ordinary text answers.",
209
219
  "Work like a practical coding agent: inspect when useful, edit with file tools, run safe checks when they add confidence, and keep outputs inside the workspace.",
210
220
  "For large apps, websites, LaTeX documents, Python/C/shell projects, or system tasks, plan a coherent minimal implementation, then use tools to create files, run checks, and publish artifacts.",
@@ -414,6 +424,24 @@ export async function requestNextStep(client, config, messages) {
414
424
  tools.splice(
415
425
  -1,
416
426
  0,
427
+ {
428
+ type: "function",
429
+ function: {
430
+ name: "inspect_project",
431
+ description:
432
+ "Build a compact, deterministic map of the workspace for large-codebase work. Returns top-level entries, manifests, source/test directories, package scripts, language counts, and recommended files to read next. Use this before editing an unfamiliar or multi-file repository.",
433
+ parameters: {
434
+ type: "object",
435
+ properties: {
436
+ path: { type: "string", description: "Workspace-relative directory to inspect. Defaults to ." },
437
+ maxDepth: { type: "integer", description: "Recursive depth, 1 to 10. Defaults to 6." },
438
+ limit: { type: "integer", description: "Maximum filesystem entries to inspect." },
439
+ includeFiles: { type: "boolean", description: "Include a compact file list when needed. Defaults to false." },
440
+ },
441
+ additionalProperties: false,
442
+ },
443
+ },
444
+ },
417
445
  {
418
446
  type: "function",
419
447
  function: {
@@ -604,6 +632,11 @@ export async function requestNextStep(client, config, messages) {
604
632
  toolPayload.stderr,
605
633
  toolPayload.error,
606
634
  toolPayload.reason,
635
+ toolPayload.summary ? `Summary: ${toolPayload.summary}` : "",
636
+ toolPayload.counts ? `Counts: ${JSON.stringify(toolPayload.counts)}` : "",
637
+ Array.isArray(toolPayload.recommendedReads) && toolPayload.recommendedReads.length
638
+ ? `Recommended reads: ${toolPayload.recommendedReads.join(", ")}`
639
+ : "",
607
640
  toolPayload.path ? `Path: ${toolPayload.path}` : "",
608
641
  Array.isArray(toolPayload.changes)
609
642
  ? toolPayload.changes
@@ -9,6 +9,16 @@ const COMPLEXITY_KEYWORDS = [
9
9
  "apply_patch",
10
10
  "edit",
11
11
  "large codebase",
12
+ "codebase",
13
+ "monorepo",
14
+ "repository",
15
+ "cross-file",
16
+ "multi file",
17
+ "large repo",
18
+ "engineering",
19
+ "entry point",
20
+ "regression",
21
+ "root cause",
12
22
  "design",
13
23
  "review",
14
24
  "migrate",
@@ -19,9 +29,35 @@ const COMPLEXITY_KEYWORDS = [
19
29
  "docker",
20
30
  "ci",
21
31
  "github",
32
+ "system",
33
+ "systemd",
34
+ "permission denied",
35
+ "install",
36
+ "setup",
37
+ "toolchain",
38
+ "conda",
39
+ "venv",
40
+ "kubernetes",
41
+ "nginx",
42
+ "postgres",
43
+ "redis",
44
+ "segfault",
45
+ "typescript",
46
+ "python",
47
+ "rust",
48
+ "cargo",
49
+ "golang",
50
+ "cmake",
51
+ "gradle",
52
+ "maven",
22
53
  ];
23
54
 
24
55
  const COMPLEX_ROUTE_HINTS = [
56
+ /\b(large|big|complex|complicated)\s+(repo|repository|codebase|project|task)\b/i,
57
+ /\b(multi[- ]file|cross[- ]file|repo[- ]wide|workspace[- ]wide)\b/i,
58
+ /\b(root cause|regression|failing tests?|fix the build|make it pass)\b/i,
59
+ /\b(system bug|system problem|permission denied|service failed|daemon|systemd|toolchain|install|setup)\b/i,
60
+ /\b(conda|venv|python|node|typescript|rust|cargo|golang|java|gradle|maven|cmake|c\+\+)\b.*\b(project|app|tests?|build|compile|fix)\b/i,
25
61
  /\blatex\b/i,
26
62
  /\btexlive\b/i,
27
63
  /\bpdflatex\b/i,
@@ -102,9 +138,10 @@ export function getModelPresets() {
102
138
  };
103
139
  }
104
140
 
105
- export function scoreTaskComplexity(goal = "") {
141
+ export function scoreTaskComplexity(goal = "", taskProfile = "auto") {
106
142
  const text = String(goal).toLowerCase();
107
143
  let score = text.length > 600 ? 2 : text.length > 240 ? 1 : 0;
144
+ if (["large-codebase", "engineering", "codebase"].includes(String(taskProfile || "").toLowerCase())) score += 3;
108
145
  for (const keyword of COMPLEXITY_KEYWORDS) {
109
146
  if (text.includes(keyword)) score += 1;
110
147
  }
@@ -118,7 +155,7 @@ export function normalizeRoutingMode(value) {
118
155
  return ROUTING_MODES.includes(value) ? value : "smart";
119
156
  }
120
157
 
121
- export function selectModelRoute({ routingMode = "smart", provider = "deepseek", model = "", goal = "" } = {}) {
158
+ export function selectModelRoute({ routingMode = "smart", provider = "deepseek", model = "", goal = "", taskProfile = "auto" } = {}) {
122
159
  const mode = normalizeRoutingMode(routingMode);
123
160
  const presets = getModelPresets();
124
161
 
@@ -129,7 +166,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
129
166
  provider: defaults.provider,
130
167
  model: model || defaults.model,
131
168
  reason: "Local mock route selected for smoke tests and offline UI/API checks.",
132
- complexityScore: scoreTaskComplexity(goal),
169
+ complexityScore: scoreTaskComplexity(goal, taskProfile),
133
170
  };
134
171
  }
135
172
 
@@ -140,7 +177,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
140
177
  provider: defaults.provider,
141
178
  model: model || defaults.model,
142
179
  reason: "Manual provider/model selection.",
143
- complexityScore: scoreTaskComplexity(goal),
180
+ complexityScore: scoreTaskComplexity(goal, taskProfile),
144
181
  };
145
182
  }
146
183
 
@@ -150,7 +187,7 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
150
187
  provider: presets.complex.provider,
151
188
  model: presets.complex.model,
152
189
  reason: "Complex route selected explicitly.",
153
- complexityScore: scoreTaskComplexity(goal),
190
+ complexityScore: scoreTaskComplexity(goal, taskProfile),
154
191
  };
155
192
  }
156
193
 
@@ -160,11 +197,11 @@ export function selectModelRoute({ routingMode = "smart", provider = "deepseek",
160
197
  provider: presets.fast.provider,
161
198
  model: presets.fast.model,
162
199
  reason: "Fast route selected explicitly.",
163
- complexityScore: scoreTaskComplexity(goal),
200
+ complexityScore: scoreTaskComplexity(goal, taskProfile),
164
201
  };
165
202
  }
166
203
 
167
- const complexityScore = scoreTaskComplexity(goal);
204
+ const complexityScore = scoreTaskComplexity(goal, taskProfile);
168
205
  const selected = complexityScore >= 3 ? presets.complex : presets.fast;
169
206
  return {
170
207
  routingMode: mode,
@@ -3,15 +3,22 @@ export const TASK_PROFILES = {
3
3
  id: "auto",
4
4
  label: "Auto",
5
5
  prompt:
6
- "Infer the task type from the user request. Prefer the smallest safe tool sequence that actually completes the work, preserve workspace files, run useful checks, and summarize what changed.",
7
- tools: ["browser", "shell", "files", "canvas"],
6
+ "Infer the task type from the user request. For short tasks, use the smallest safe tool sequence that completes the work. For codebase, system, debugging, migration, or multi-language tasks, switch into the engineering loop: inspect, read/search exact context, patch incrementally, run focused checks, repair failures, and summarize changed files plus residual risks.",
7
+ tools: ["browser", "shell", "files", "canvas", "inspect_project"],
8
8
  },
9
9
  code: {
10
10
  id: "code",
11
11
  label: "Code writing",
12
12
  prompt:
13
- "Act like a coding agent: understand the request, edit workspace files, run useful safe checks, iterate on failures, and report changed files and residual risks.",
14
- tools: ["files", "shell", "sandbox"],
13
+ "Act like a coding agent across languages: inspect project manifests and conventions, edit workspace files with patches, run useful focused checks, iterate on failures, and report changed files and residual risks.",
14
+ tools: ["inspect_project", "files", "shell", "sandbox"],
15
+ },
16
+ "large-codebase": {
17
+ id: "large-codebase",
18
+ label: "Large codebase engineering",
19
+ prompt:
20
+ "For large or complicated engineering work, behave like a senior coding agent: inspect_project first unless the repo is already known, read AGENTS/README/manifests, locate entry points and tests, make a small explicit change plan, patch in coherent batches, run the narrowest relevant checks first, escalate to broader checks when stable, and summarize files changed, checks, tradeoffs, and remaining risks.",
21
+ tools: ["inspect_project", "search_files", "read_file", "apply_patch", "shell", "sandbox", "canvas"],
15
22
  },
16
23
  writing: {
17
24
  id: "writing",
@@ -80,20 +87,37 @@ export const TASK_PROFILES = {
80
87
  id: "maintenance",
81
88
  label: "System maintenance",
82
89
  prompt:
83
- "For system maintenance, diagnose first, use Docker for broad installs when available, and follow the configured trust/package policy for host-level changes.",
84
- tools: ["shell", "sandbox", "files"],
90
+ "For system maintenance and system bugs, diagnose first with read-only evidence, use Docker for broad installs/toolchains when available, generate reversible project-local scripts, follow the configured trust/package policy for host-level changes, and stop with clear next actions if stronger permission is needed.",
91
+ tools: ["shell", "sandbox", "files", "inspect_project"],
85
92
  },
86
93
  };
87
94
 
95
+ const PROFILE_ALIASES = {
96
+ large: "large-codebase",
97
+ codebase: "large-codebase",
98
+ repo: "large-codebase",
99
+ repository: "large-codebase",
100
+ engineering: "large-codebase",
101
+ engineer: "large-codebase",
102
+ };
103
+
88
104
  export function listTaskProfiles() {
89
105
  return Object.values(TASK_PROFILES);
90
106
  }
91
107
 
92
108
  export function normalizeTaskProfile(value = "auto") {
93
109
  const key = String(value || "auto").trim().toLowerCase();
110
+ if (PROFILE_ALIASES[key]) return PROFILE_ALIASES[key];
94
111
  return TASK_PROFILES[key] ? key : "auto";
95
112
  }
96
113
 
97
114
  export function getTaskProfile(value = "auto") {
98
115
  return TASK_PROFILES[normalizeTaskProfile(value)];
99
116
  }
117
+
118
+ export function defaultMaxStepsForProfile(value = "auto") {
119
+ const profile = normalizeTaskProfile(value);
120
+ if (profile === "large-codebase") return 36;
121
+ if (profile === "latex") return 30;
122
+ return 24;
123
+ }
package/src/web-db.js CHANGED
@@ -3,7 +3,7 @@ import path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { getModelPresets } from "./model-routing.js";
5
5
 
6
- const PREFERENCES_SCHEMA_VERSION = 4;
6
+ const PREFERENCES_SCHEMA_VERSION = 5;
7
7
 
8
8
  function defaultPreferences(baseDir) {
9
9
  const presets = getModelPresets();
@@ -95,6 +95,9 @@ export class WebDatabase {
95
95
  if (!Number.isFinite(Number(parsed.maxSteps)) || Number(parsed.maxSteps) < 24) {
96
96
  preferences.maxSteps = 24;
97
97
  }
98
+ if ((parsed.preferencesSchemaVersion || 1) < 5) {
99
+ preferences.taskProfile = "auto";
100
+ }
98
101
  this.savePreferences(preferences);
99
102
  }
100
103
  return preferences;
@@ -3,7 +3,7 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { redactSensitiveText } from "./redaction.js";
5
5
 
6
- export const WORKSPACE_TOOL_NAMES = ["list_files", "read_file", "search_files", "write_file", "apply_patch"];
6
+ export const WORKSPACE_TOOL_NAMES = ["inspect_project", "list_files", "read_file", "search_files", "write_file", "apply_patch"];
7
7
  export const WORKSPACE_WRITE_TOOL_NAMES = ["write_file", "apply_patch"];
8
8
 
9
9
  const MAX_READ_BYTES = 220_000;
@@ -11,8 +11,51 @@ const MAX_WRITE_BYTES = 220_000;
11
11
  const MAX_PATCH_BYTES = 260_000;
12
12
  const MAX_LIST_ENTRIES = 360;
13
13
  const MAX_SEARCH_RESULTS = 80;
14
+ const MAX_INSPECT_ENTRIES = 1400;
14
15
  const DEFAULT_MAX_DEPTH = 4;
15
- const SKIP_DIRS = new Set([".git", "node_modules", ".sessions"]);
16
+ const SKIP_DIRS = new Set([
17
+ ".git",
18
+ ".sessions",
19
+ ".aginti",
20
+ "node_modules",
21
+ "dist",
22
+ "build",
23
+ "coverage",
24
+ ".next",
25
+ ".nuxt",
26
+ ".cache",
27
+ ".venv",
28
+ "venv",
29
+ "__pycache__",
30
+ ]);
31
+ const IMPORTANT_MANIFESTS = new Set([
32
+ "package.json",
33
+ "package-lock.json",
34
+ "pnpm-lock.yaml",
35
+ "yarn.lock",
36
+ "tsconfig.json",
37
+ "jsconfig.json",
38
+ "vite.config.js",
39
+ "vite.config.ts",
40
+ "next.config.js",
41
+ "pyproject.toml",
42
+ "requirements.txt",
43
+ "requirements-dev.txt",
44
+ "setup.py",
45
+ "Pipfile",
46
+ "Cargo.toml",
47
+ "go.mod",
48
+ "pom.xml",
49
+ "build.gradle",
50
+ "Makefile",
51
+ "Dockerfile",
52
+ "docker-compose.yml",
53
+ "compose.yml",
54
+ "README.md",
55
+ "AGENTS.md",
56
+ ]);
57
+ const SOURCE_DIR_NAMES = new Set(["src", "app", "lib", "packages", "apps", "bin", "scripts", "server", "client", "public", "docs"]);
58
+ const TEST_DIR_NAMES = new Set(["test", "tests", "__tests__", "spec", "specs", "e2e"]);
16
59
  const SENSITIVE_EXTENSIONS = new Set([".key", ".pem", ".p12", ".pfx", ".crt", ".csr"]);
17
60
  const SENSITIVE_BASENAMES = new Set([
18
61
  ".env",
@@ -183,10 +226,68 @@ export function summarizeWorkspaceTools(config) {
183
226
  maxPatchBytes: MAX_PATCH_BYTES,
184
227
  maxListEntries: MAX_LIST_ENTRIES,
185
228
  maxSearchResults: MAX_SEARCH_RESULTS,
229
+ maxInspectEntries: MAX_INSPECT_ENTRIES,
186
230
  },
187
231
  };
188
232
  }
189
233
 
234
+ function languageForExtension(ext) {
235
+ const normalized = String(ext || "").toLowerCase();
236
+ if ([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"].includes(normalized)) return "javascript";
237
+ if ([".py", ".pyw"].includes(normalized)) return "python";
238
+ if ([".rs"].includes(normalized)) return "rust";
239
+ if ([".go"].includes(normalized)) return "go";
240
+ if ([".java", ".kt", ".scala"].includes(normalized)) return "jvm";
241
+ if ([".c", ".cc", ".cpp", ".h", ".hpp"].includes(normalized)) return "c-cpp";
242
+ if ([".sh", ".bash", ".zsh"].includes(normalized)) return "shell";
243
+ if ([".md", ".mdx", ".rst"].includes(normalized)) return "docs";
244
+ if ([".json", ".yaml", ".yml", ".toml", ".ini"].includes(normalized)) return "config";
245
+ if ([".html", ".css", ".scss", ".svg"].includes(normalized)) return "web";
246
+ return normalized ? normalized.slice(1) : "no-extension";
247
+ }
248
+
249
+ function isLikelyTest(relativePath) {
250
+ const normalized = normalizeRelative(relativePath).toLowerCase();
251
+ const base = path.basename(normalized);
252
+ return (
253
+ normalized.split("/").some((segment) => TEST_DIR_NAMES.has(segment)) ||
254
+ /\.(test|spec)\.[cm]?[jt]sx?$/.test(base) ||
255
+ /^test_.*\.py$/.test(base) ||
256
+ /_test\.go$/.test(base) ||
257
+ /test.*\.rs$/.test(base)
258
+ );
259
+ }
260
+
261
+ function sortCounts(map, limit = 16) {
262
+ return [...map.entries()]
263
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
264
+ .slice(0, limit)
265
+ .map(([name, count]) => ({ name, count }));
266
+ }
267
+
268
+ async function readJsonFileSafe(absolutePath) {
269
+ try {
270
+ const stat = await fs.stat(absolutePath);
271
+ if (!stat.isFile() || stat.size > 120_000) return null;
272
+ const buffer = await fs.readFile(absolutePath);
273
+ if (buffer.includes(0)) return null;
274
+ return JSON.parse(buffer.toString("utf8"));
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+
280
+ function recommendedReads(summary) {
281
+ const reads = [];
282
+ for (const name of ["AGENTS.md", "README.md", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"]) {
283
+ const match = summary.manifestFiles.find((item) => item.path === name || item.path.endsWith(`/${name}`));
284
+ if (match) reads.push(match.path);
285
+ }
286
+ for (const dir of summary.sourceDirs.slice(0, 4)) reads.push(dir.path);
287
+ for (const file of summary.testFiles.slice(0, 4)) reads.push(file.path);
288
+ return [...new Set(reads)].slice(0, 12);
289
+ }
290
+
190
291
  async function fileInfo(absolutePath, root) {
191
292
  const stat = await fs.stat(absolutePath);
192
293
  return {
@@ -316,6 +417,133 @@ async function searchFiles(config, args) {
316
417
  };
317
418
  }
318
419
 
420
+ async function inspectProject(config, args) {
421
+ const target = resolveWorkspacePath(config, args.path || ".");
422
+ const maxDepth = Math.min(Math.max(Number(args.maxDepth) || 6, 1), 10);
423
+ const limit = Math.min(Math.max(Number(args.limit) || MAX_INSPECT_ENTRIES, 80), MAX_INSPECT_ENTRIES);
424
+ const includeFiles = Boolean(args.includeFiles);
425
+ const files = [];
426
+ const sourceDirs = [];
427
+ const testFiles = [];
428
+ const manifestFiles = [];
429
+ const extensionCounts = new Map();
430
+ const languageCounts = new Map();
431
+ const topLevel = [];
432
+ let fileCount = 0;
433
+ let dirCount = 0;
434
+ let totalBytes = 0;
435
+ let truncated = false;
436
+
437
+ async function visit(currentPath, depth = 0) {
438
+ if (fileCount + dirCount >= limit) {
439
+ truncated = true;
440
+ return;
441
+ }
442
+
443
+ const stat = await fs.stat(currentPath).catch(() => null);
444
+ if (!stat) return;
445
+ const relativePath = normalizeRelative(path.relative(target.root, currentPath));
446
+ const segments = pathSegments(relativePath);
447
+ const base = path.basename(currentPath);
448
+
449
+ if (relativePath !== ".") {
450
+ const policy = pathPolicy("read_file", relativePath);
451
+ if (!policy.allowed) return;
452
+ if (segments.some((segment) => SKIP_DIRS.has(segment))) return;
453
+ }
454
+
455
+ if (stat.isDirectory()) {
456
+ dirCount += 1;
457
+ if (depth === 1) topLevel.push({ path: relativePath, type: "directory" });
458
+ if (SOURCE_DIR_NAMES.has(base) || TEST_DIR_NAMES.has(base)) {
459
+ sourceDirs.push({ path: relativePath, kind: TEST_DIR_NAMES.has(base) ? "tests" : "source" });
460
+ }
461
+ if (depth >= maxDepth) return;
462
+ const children = await fs.readdir(currentPath, { withFileTypes: true }).catch(() => []);
463
+ children.sort((a, b) => a.name.localeCompare(b.name));
464
+ for (const child of children) {
465
+ if (SKIP_DIRS.has(child.name)) continue;
466
+ await visit(path.join(currentPath, child.name), depth + 1);
467
+ if (truncated) break;
468
+ }
469
+ return;
470
+ }
471
+
472
+ if (!stat.isFile()) return;
473
+ fileCount += 1;
474
+ totalBytes += stat.size;
475
+ const ext = path.extname(base).toLowerCase() || "(none)";
476
+ extensionCounts.set(ext, (extensionCounts.get(ext) || 0) + 1);
477
+ const language = languageForExtension(ext);
478
+ languageCounts.set(language, (languageCounts.get(language) || 0) + 1);
479
+
480
+ const item = {
481
+ path: relativePath,
482
+ size: stat.size,
483
+ modifiedAt: stat.mtime.toISOString(),
484
+ };
485
+ if (depth === 1) topLevel.push({ path: relativePath, type: "file", size: stat.size });
486
+ if (IMPORTANT_MANIFESTS.has(base) || base.endsWith(".config.js") || base.endsWith(".config.ts")) {
487
+ manifestFiles.push(item);
488
+ }
489
+ if (isLikelyTest(relativePath)) testFiles.push(item);
490
+ if (includeFiles) files.push(item);
491
+ }
492
+
493
+ await visit(target.absolutePath, 0);
494
+
495
+ const packageJsonPath = path.join(target.absolutePath, "package.json");
496
+ const packageJson = await readJsonFileSafe(packageJsonPath);
497
+ const packageScripts =
498
+ packageJson && packageJson.scripts && typeof packageJson.scripts === "object"
499
+ ? Object.entries(packageJson.scripts)
500
+ .slice(0, 30)
501
+ .map(([name, command]) => ({ name, command: redactSensitiveText(String(command)).slice(0, 220) }))
502
+ : [];
503
+ const packageManagers = [
504
+ manifestFiles.some((item) => item.path.endsWith("pnpm-lock.yaml")) ? "pnpm" : "",
505
+ manifestFiles.some((item) => item.path.endsWith("yarn.lock")) ? "yarn" : "",
506
+ manifestFiles.some((item) => item.path.endsWith("package-lock.json")) ? "npm" : "",
507
+ manifestFiles.some((item) => item.path.endsWith("pyproject.toml")) ? "python/pyproject" : "",
508
+ manifestFiles.some((item) => item.path.endsWith("Cargo.toml")) ? "cargo" : "",
509
+ manifestFiles.some((item) => item.path.endsWith("go.mod")) ? "go" : "",
510
+ ].filter(Boolean);
511
+
512
+ const summary = {
513
+ ok: true,
514
+ toolName: "inspect_project",
515
+ path: target.relativePath,
516
+ root: target.root,
517
+ summary: `${fileCount} files, ${dirCount} directories, ${sortCounts(languageCounts, 5)
518
+ .map((item) => `${item.name}:${item.count}`)
519
+ .join(" ")}`,
520
+ counts: {
521
+ files: fileCount,
522
+ directories: dirCount,
523
+ totalBytes,
524
+ },
525
+ truncated,
526
+ topLevel: topLevel.slice(0, 80),
527
+ manifestFiles: manifestFiles.slice(0, 80),
528
+ sourceDirs: sourceDirs.slice(0, 60),
529
+ testFiles: testFiles.slice(0, 80),
530
+ extensionCounts: sortCounts(extensionCounts),
531
+ languageCounts: sortCounts(languageCounts),
532
+ packageManagers,
533
+ packageScripts,
534
+ recommendedReads: [],
535
+ engineeringHints: [
536
+ "Read AGENTS/README/manifests before editing.",
537
+ "Use search_files to locate symbols and tests, then read exact files.",
538
+ "Use apply_patch for source edits and run the smallest relevant check first.",
539
+ "If a change spans modules, patch in small batches and verify after each batch.",
540
+ ],
541
+ };
542
+ summary.recommendedReads = recommendedReads(summary);
543
+ if (includeFiles) summary.files = files.slice(0, limit);
544
+ return summary;
545
+ }
546
+
319
547
  function compactDiff(relativePath, beforeText, afterText) {
320
548
  const beforeLines = String(beforeText || "").split("\n");
321
549
  const afterLines = String(afterText || "").split("\n");
@@ -723,6 +951,8 @@ export async function executeWorkspaceTool(toolName, args, config) {
723
951
  }
724
952
 
725
953
  switch (toolName) {
954
+ case "inspect_project":
955
+ return inspectProject(config, args);
726
956
  case "list_files":
727
957
  return listFiles(config, args);
728
958
  case "read_file":