@lazyingart/agintiflow 0.15.1 → 0.16.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
@@ -179,7 +179,7 @@ aginti capabilities --json
179
179
  aginti doctor --capabilities
180
180
  ```
181
181
 
182
- The report checks the project root, command cwd, shared `.sessions/`, provider-key presence, DeepSeek routes, guarded file and shell tools, Docker status, wrappers, task profiles, TeX, Node/npm, Python, R, conda, and maintenance command policy. It never prints API key or token values.
182
+ The report checks the project root, command cwd, shared `.sessions/`, provider-key presence, DeepSeek routes, guarded file and shell tools, OS/platform hints, Docker status, wrappers, task profiles, TeX, Node/npm, Python, R, conda, and maintenance command policy. It never prints API key or token values.
183
183
 
184
184
  Live DeepSeek verification is opt-in because it spends provider credits:
185
185
 
@@ -337,7 +337,20 @@ npm publish --access public
337
337
 
338
338
  Never publish with `npm publish` from inside an agent run. The runtime command policy blocks npm publish and npm token commands by design.
339
339
 
340
- ## Docker Bootstrap
340
+ ## Platform And Docker Bootstrap
341
+
342
+ AgInTiFlow is designed for Linux, macOS, Windows through WSL2, and best-effort native Windows use. The most portable shell/toolchain path is `docker-workspace`; native Windows host shell commands should be treated as best effort unless you run inside WSL2.
343
+
344
+ Platform notes:
345
+
346
+ | Platform | Recommended setup |
347
+ | --- | --- |
348
+ | Ubuntu/Debian | Node.js 22+, optional `scripts/install-docker-ubuntu.sh`, Docker workspace mode for broad toolchains. |
349
+ | Red Hat/Fedora/Rocky/Alma | Node.js 22+, Docker/Podman-compatible Docker CLI from `dnf`/vendor docs; do not use the Ubuntu Docker script. |
350
+ | macOS | Node.js 22+ from Homebrew/nvm/fnm, Docker Desktop or Colima, optional MacTeX/BasicTeX for host LaTeX. |
351
+ | Windows | Prefer WSL2 with Docker Desktop WSL integration. Native Windows host mode is best effort; Docker/WSL is recommended for Bash, LaTeX, Python/R/Stan, and package-manager workflows. |
352
+
353
+ LaTeX support first checks the active environment for `latexmk` or `pdflatex`. If your host already has MacTeX, BasicTeX, TeX Live, or MiKTeX on `PATH`, host-mode LaTeX tasks can compile directly without downloading TeX again. Docker-mode LaTeX uses the companion image; the setup script preflights an existing image and skips rebuilds when the Docker toolchain already has `latexmk` and `pdflatex`.
341
354
 
342
355
  Ubuntu helper:
343
356
 
@@ -353,14 +366,14 @@ DOCKER_TARGET_USER=lachlan ./scripts/install-docker-ubuntu.sh
353
366
 
354
367
  Open a new login shell, or run `newgrp docker`, before testing non-root Docker access.
355
368
 
356
- Build the companion agent toolchain sandbox:
369
+ Build or verify the companion agent toolchain sandbox:
357
370
 
358
371
  ```bash
359
372
  ./scripts/setup-agent-toolchain-docker.sh
360
373
  npm run smoke:toolchain-docker
361
374
  ```
362
375
 
363
- The setup script idempotently builds `agintiflow-sandbox:latest` from `docker/sandbox.Dockerfile` and verifies Node, npm, Python, NumPy, Matplotlib, `latexmk`, and `pdflatex` inside Docker. It also creates the persistent companion folders under `~/.agintiflow/docker/`: `home/` maps to `/aginti-home`, `cache/` maps to `/aginti-cache`, and `env/` maps to `/aginti-env`. Python/conda-style toolchains should live under `/aginti-env` so they survive across agent runs. OS package changes from `apt-get` are container-ephemeral unless you rebuild the image.
376
+ The setup script checks whether `agintiflow-sandbox:latest` already exists and runs a Docker preflight before rebuilding. If Node, npm, Python, NumPy, Matplotlib, `latexmk`, and `pdflatex` are already ready inside the image, it exits without redownloading TeX Live. Set `AGINTIFLOW_FORCE_TOOLCHAIN_REBUILD=true` only when you intentionally want a fresh image. The script also creates persistent companion folders under `~/.agintiflow/docker/`: `home/` maps to `/aginti-home`, `cache/` maps to `/aginti-cache`, and `env/` maps to `/aginti-env`. Python/conda-style toolchains should live under `/aginti-env` so they survive across agent runs. OS package changes from `apt-get` are container-ephemeral unless you rebuild the image.
364
377
 
365
378
  ## Sandbox Modes
366
379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.15.1",
3
+ "version": "0.16.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",
@@ -47,6 +47,7 @@
47
47
  "scripts/smoke-coding-tools.js",
48
48
  "scripts/smoke-capabilities.js",
49
49
  "scripts/smoke-inbox.js",
50
+ "scripts/smoke-platform.js",
50
51
  "scripts/smoke-skills.js",
51
52
  "scripts/smoke-toolchain-docker.js",
52
53
  "scripts/smoke-web-api.js",
@@ -71,9 +72,10 @@
71
72
  "smoke:skills": "node scripts/smoke-skills.js",
72
73
  "smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
73
74
  "smoke:inbox": "node scripts/smoke-inbox.js",
75
+ "smoke:platform": "node scripts/smoke-platform.js",
74
76
  "smoke:web-api": "node scripts/smoke-web-api.js",
75
77
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
76
- "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:auth && npm run smoke:capabilities && npm run smoke:skills && npm run smoke:cli-chat && npm run smoke:inbox",
78
+ "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:auth && npm run smoke:capabilities && npm run smoke:platform && npm run smoke:skills && npm run smoke:cli-chat && npm run smoke:inbox",
77
79
  "pack:dry-run": "npm pack --dry-run",
78
80
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
79
81
  },
package/public/app.js CHANGED
@@ -798,6 +798,7 @@ function renderProjectStatus(info = projectInfo) {
798
798
  projectInfo = info;
799
799
  if (!projectStatusEl || !info) return;
800
800
  projectStatusEl.textContent = [
801
+ info.platform?.label ? `os=${info.platform.label}` : "",
801
802
  `root=${info.root || ""}`,
802
803
  `cwd=${info.commandCwd || ""}`,
803
804
  `sessions=${info.sessionsDir || ""}`,
@@ -7,57 +7,106 @@ IMAGE="${AGINTIFLOW_TOOLCHAIN_IMAGE:-agintiflow-sandbox:latest}"
7
7
  WORKSPACE="${AGINTIFLOW_TOOLCHAIN_WORKSPACE:-${REPO_ROOT}}"
8
8
  DOCKERFILE="${AGINTIFLOW_TOOLCHAIN_DOCKERFILE:-${REPO_ROOT}/docker/sandbox.Dockerfile}"
9
9
  STATE_DIR="${AGINTIFLOW_DOCKER_STATE_DIR:-${HOME:-/tmp}/.agintiflow/docker}"
10
+ FORCE_REBUILD="${AGINTIFLOW_FORCE_TOOLCHAIN_REBUILD:-false}"
11
+
12
+ os_name="$(uname -s 2>/dev/null || printf unknown)"
13
+ case "${os_name}" in
14
+ Darwin) platform_hint="macOS: use Docker Desktop or Colima; host LaTeX can come from MacTeX/BasicTeX." ;;
15
+ Linux)
16
+ if grep -qi microsoft /proc/version 2>/dev/null; then
17
+ platform_hint="WSL: use Docker Desktop WSL integration or Docker Engine inside WSL."
18
+ elif [ -r /etc/os-release ] && grep -Eqi '^(ID|ID_LIKE)=.*(rhel|fedora|centos|rocky|almalinux|redhat)' /etc/os-release; then
19
+ platform_hint="Red Hat/Fedora family: install Docker with dnf/yum or vendor docs; do not use install-docker-ubuntu.sh."
20
+ else
21
+ platform_hint="Linux: Ubuntu/Debian hosts may use scripts/install-docker-ubuntu.sh."
22
+ fi
23
+ ;;
24
+ MINGW*|MSYS*|CYGWIN*) platform_hint="Windows shell: prefer WSL2 for AgInTiFlow Docker/toolchain workflows." ;;
25
+ *) platform_hint="Use a Docker-capable environment with Node.js 22+." ;;
26
+ esac
10
27
 
11
28
  if ! command -v docker >/dev/null 2>&1; then
12
- echo "Docker is not installed or not on PATH. Run scripts/install-docker-ubuntu.sh first." >&2
29
+ echo "Docker is not installed or not on PATH." >&2
30
+ echo "${platform_hint}" >&2
13
31
  exit 1
14
32
  fi
15
33
 
16
34
  mkdir -p "${WORKSPACE}"
17
35
  mkdir -p "${STATE_DIR}/home" "${STATE_DIR}/cache" "${STATE_DIR}/env"
18
36
 
19
- echo "Building AgInTiFlow toolchain sandbox image:"
37
+ host_latex="missing"
38
+ if command -v latexmk >/dev/null 2>&1 && command -v pdflatex >/dev/null 2>&1; then
39
+ host_latex="complete"
40
+ elif command -v latexmk >/dev/null 2>&1 || command -v pdflatex >/dev/null 2>&1; then
41
+ host_latex="partial"
42
+ fi
43
+
44
+ run_preflight() {
45
+ docker run --rm \
46
+ --network none \
47
+ --cap-drop ALL \
48
+ --security-opt no-new-privileges \
49
+ --pids-limit 256 \
50
+ --memory 2g \
51
+ --cpus 2 \
52
+ --tmpfs /tmp:rw,nosuid,nodev,size=512m \
53
+ -e HOME=/aginti-home \
54
+ -e XDG_CACHE_HOME=/aginti-cache \
55
+ -e PIP_CACHE_DIR=/aginti-cache/pip \
56
+ -e MPLCONFIGDIR=/tmp/matplotlib \
57
+ -e NPM_CONFIG_CACHE=/aginti-cache/npm \
58
+ -e CONDA_PKGS_DIRS=/aginti-cache/conda-pkgs \
59
+ -v "${WORKSPACE}:/workspace:rw" \
60
+ -v "${STATE_DIR}/home:/aginti-home:rw" \
61
+ -v "${STATE_DIR}/cache:/aginti-cache:rw" \
62
+ -v "${STATE_DIR}/env:/aginti-env:rw" \
63
+ -w /workspace \
64
+ "${IMAGE}" \
65
+ bash -lc 'set -Eeuo pipefail
66
+ mkdir -p /tmp/matplotlib
67
+ if [ ! -x /aginti-env/python/bin/python ]; then python3 -m venv --system-site-packages /aginti-env/python; fi
68
+ . /aginti-env/python/bin/activate
69
+ node -v
70
+ npm -v
71
+ python3 --version
72
+ python3 - <<PY
73
+ import matplotlib, numpy
74
+ print("matplotlib", matplotlib.__version__)
75
+ print("numpy", numpy.__version__)
76
+ PY
77
+ latexmk -version | head -1
78
+ pdflatex --version | head -1
79
+ '
80
+ }
81
+
82
+ echo "AgInTiFlow toolchain sandbox:"
20
83
  echo " image=${IMAGE}"
21
84
  echo " dockerfile=${DOCKERFILE}"
22
85
  echo " context=${REPO_ROOT}"
23
86
  echo " persistent=${STATE_DIR}"
87
+ echo " platform=${os_name}"
88
+ echo " hostLatex=${host_latex}"
89
+ echo " note=${platform_hint}"
90
+
91
+ if [ "${FORCE_REBUILD}" != "true" ] && docker image inspect "${IMAGE}" >/dev/null 2>&1; then
92
+ echo
93
+ echo "Existing image found. Running preflight before rebuilding:"
94
+ if run_preflight; then
95
+ echo
96
+ echo "Toolchain sandbox is already ready; skipped Docker rebuild and TeX Live redownload."
97
+ exit 0
98
+ fi
99
+ echo
100
+ echo "Existing image failed preflight; rebuilding ${IMAGE}."
101
+ fi
102
+
103
+ echo
104
+ echo "Building AgInTiFlow toolchain sandbox image:"
24
105
  docker build -t "${IMAGE}" -f "${DOCKERFILE}" "${REPO_ROOT}"
25
106
 
26
107
  echo
27
108
  echo "Running toolchain preflight inside Docker:"
28
- docker run --rm \
29
- --network none \
30
- --cap-drop ALL \
31
- --security-opt no-new-privileges \
32
- --pids-limit 256 \
33
- --memory 2g \
34
- --cpus 2 \
35
- --tmpfs /tmp:rw,nosuid,nodev,size=512m \
36
- -e HOME=/aginti-home \
37
- -e XDG_CACHE_HOME=/aginti-cache \
38
- -e PIP_CACHE_DIR=/aginti-cache/pip \
39
- -e NPM_CONFIG_CACHE=/aginti-cache/npm \
40
- -e CONDA_PKGS_DIRS=/aginti-cache/conda-pkgs \
41
- -v "${WORKSPACE}:/workspace:rw" \
42
- -v "${STATE_DIR}/home:/aginti-home:rw" \
43
- -v "${STATE_DIR}/cache:/aginti-cache:rw" \
44
- -v "${STATE_DIR}/env:/aginti-env:rw" \
45
- -w /workspace \
46
- "${IMAGE}" \
47
- bash -lc 'set -Eeuo pipefail
48
- if [ ! -x /aginti-env/python/bin/python ]; then python3 -m venv --system-site-packages /aginti-env/python; fi
49
- . /aginti-env/python/bin/activate
50
- node -v
51
- npm -v
52
- python3 --version
53
- python3 - <<PY
54
- import matplotlib, numpy
55
- print("matplotlib", matplotlib.__version__)
56
- print("numpy", numpy.__version__)
57
- PY
58
- latexmk -version | head -1
59
- pdflatex --version | head -1
60
- '
109
+ run_preflight
61
110
 
62
111
  echo
63
112
  echo "Toolchain sandbox is ready."
@@ -36,6 +36,8 @@ try {
36
36
  assert(capabilities.project.commandCwd === tempRoot, "capabilities did not default commandCwd to project root");
37
37
  assert(capabilities.project.instructionsPresent, "capabilities did not report AGINTI.md");
38
38
  assert(capabilities.project.sharedSessionFolder, "capabilities did not report shared session folder");
39
+ assert(capabilities.platform?.platform, "capabilities did not report platform");
40
+ assert(Array.isArray(capabilities.platform?.setupHints), "capabilities did not report platform setup hints");
39
41
  assert(capabilities.keys?.mock === true, "capabilities did not report mock availability");
40
42
  assert(typeof capabilities.keys?.qwen === "boolean", "capabilities did not report qwen key status");
41
43
  assert(
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import assert from "node:assert/strict";
3
+ import { platformInfo, platformLabel, platformSetupHints, hostShellOption } from "../src/platform.js";
4
+ import { buildCapabilityReport } from "../src/capabilities.js";
5
+ import { resolveRuntimeConfig } from "../src/config.js";
6
+
7
+ const info = platformInfo();
8
+ const label = platformLabel(info);
9
+ const hints = platformSetupHints(info);
10
+
11
+ assert(["linux", "darwin", "win32", "freebsd", "openbsd", "aix", "sunos"].includes(info.platform), "unknown platform shape");
12
+ assert(label.includes(info.arch), "platform label should include architecture");
13
+ assert(hints.length > 0, "platform hints should not be empty");
14
+ assert(hostShellOption(), "host shell option should be defined");
15
+
16
+ if (info.isMac) {
17
+ assert(hints.some((hint) => /Docker Desktop|Colima/i.test(hint)), "macOS hints should mention Docker Desktop or Colima");
18
+ assert(hints.some((hint) => /MacTeX|BasicTeX/i.test(hint)), "macOS hints should mention MacTeX or BasicTeX");
19
+ }
20
+ if (info.isWindows) {
21
+ assert(hints.some((hint) => /WSL/i.test(hint)), "Windows hints should recommend WSL");
22
+ }
23
+ if (info.isWsl) {
24
+ assert(label.includes("WSL"), "WSL platform label should mention WSL");
25
+ }
26
+
27
+ const config = resolveRuntimeConfig({ goal: "platform smoke", provider: "mock" }, { useDockerSandbox: false });
28
+ const report = await buildCapabilityReport(process.cwd(), "0.0.0-smoke", config);
29
+ assert.equal(report.platform.platform, info.platform, "capability report should include platform");
30
+ assert(Array.isArray(report.platform.setupHints), "capability report should include setup hints");
31
+ assert(typeof report.platform.hostLatexAvailable === "boolean", "capability report should include host LaTeX status");
32
+
33
+ console.log(
34
+ JSON.stringify(
35
+ {
36
+ ok: true,
37
+ platform: report.platform.label,
38
+ linuxFamily: report.platform.linuxFamily,
39
+ hostLatexAvailable: report.platform.hostLatexAvailable,
40
+ hints: report.platform.setupHints.length,
41
+ },
42
+ null,
43
+ 2
44
+ )
45
+ );
@@ -23,6 +23,7 @@ import { searchWeb } from "./web-search.js";
23
23
  import { runParallelScouts, shouldRunParallelScouts } from "./parallel-scouts.js";
24
24
  import { readProjectInstructions } from "./project.js";
25
25
  import { formatSkillsForPrompt, selectSkillsForGoal } from "./skill-library.js";
26
+ import { hostShellOption, platformInfo, platformLabel } from "./platform.js";
26
27
 
27
28
  const exec = promisify(execCallback);
28
29
  const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
@@ -257,6 +258,7 @@ async function createInitialState(config, sessionId) {
257
258
  const skillContext = formatSkillsForPrompt(selectedSkills);
258
259
  const projectInstructions = await readProjectInstructions(config.baseDir || config.commandCwd || process.cwd());
259
260
  const projectInstructionContext = formatProjectInstructions(projectInstructions);
261
+ const platform = platformInfo();
260
262
  return {
261
263
  sessionId,
262
264
  createdAt: now,
@@ -302,7 +304,7 @@ async function createInitialState(config, sessionId) {
302
304
  config.allowShellTool
303
305
  ? config.useDockerSandbox
304
306
  ? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Docker workspace mode with approved package installs supports broader setup and network commands. The project is mounted at /workspace and the persistent agent toolchain is mounted at /aginti-env with caches under /aginti-cache.`
305
- : "A host shell command tool is available under the configured trust policy."
307
+ : `A host shell command tool is available under the configured trust policy on ${platformLabel(platform)}. On native Windows, prefer PowerShell/cmd-compatible commands or switch to WSL/Docker for bash-like toolchains.`
306
308
  : "No shell command tool is available.",
307
309
  config.allowFileTools
308
310
  ? `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 AGINTI.md/AGENTS.md/README/manifests as relevant 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.`
@@ -329,6 +331,7 @@ async function createInitialState(config, sessionId) {
329
331
  "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.",
330
332
  "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.",
331
333
  "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.",
334
+ "For LaTeX/PDF tasks, check existing latexmk/pdflatex first and compile with the available host or Docker TeX toolchain before installing packages or rebuilding the sandbox.",
332
335
  "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.",
333
336
  "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.",
334
337
  "For environment or system-maintenance work, use the configured sandbox and package policy; Docker workspace mode is the preferred place for installs and toolchain setup.",
@@ -476,6 +479,7 @@ async function applyContinuationPrompt(state, config, observers) {
476
479
  state.plan = "";
477
480
  state.stepsCompleted = 0;
478
481
  state.updatedAt = new Date().toISOString();
482
+ const platform = platformInfo();
479
483
  state.messages.push({
480
484
  role: "user",
481
485
  content: [
@@ -485,7 +489,7 @@ async function applyContinuationPrompt(state, config, observers) {
485
489
  config.allowShellTool
486
490
  ? config.useDockerSandbox
487
491
  ? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
488
- : `Shell working directory: ${config.commandCwd}`
492
+ : `Shell working directory: ${config.commandCwd}. Host platform: ${platformLabel(platform)}. Use OS-compatible commands; prefer WSL/Docker for bash-heavy workflows on Windows.`
489
493
  : "",
490
494
  config.allowFileTools
491
495
  ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large or unfamiliar codebases, then search/read exact files before editing. Read AGINTI.md/AGENTS.md/README/manifests when relevant. 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.`
@@ -658,7 +662,7 @@ async function runShellCommand(command, config, policy = evaluateCommandPolicy(c
658
662
  cwd: config.commandCwd,
659
663
  timeout: 30000,
660
664
  maxBuffer: 200 * 1024,
661
- shell: "/bin/bash",
665
+ shell: hostShellOption(),
662
666
  env: safeExecutionEnv(),
663
667
  signal: config.abortSignal,
664
668
  });
@@ -681,6 +685,7 @@ async function runShellCommand(command, config, policy = evaluateCommandPolicy(c
681
685
  }
682
686
 
683
687
  async function captureSyntheticSnapshot(store, step, config) {
688
+ const platform = platformInfo();
684
689
  const snapshot = {
685
690
  title: "No browser page open",
686
691
  url: "",
@@ -690,7 +695,7 @@ async function captureSyntheticSnapshot(store, step, config) {
690
695
  config.allowShellTool
691
696
  ? config.useDockerSandbox
692
697
  ? `Shell tool available in Docker with mounted workspace /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
693
- : `Shell tool available in: ${config.commandCwd}`
698
+ : `Shell tool available in: ${config.commandCwd} on ${platformLabel(platform)}. Use OS-compatible commands; prefer WSL/Docker for bash-heavy workflows on Windows.`
694
699
  : "Shell tool disabled.",
695
700
  config.allowFileTools
696
701
  ? `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.`
@@ -700,7 +705,7 @@ async function captureSyntheticSnapshot(store, step, config) {
700
705
  : "Agent wrappers disabled.",
701
706
  "Canvas/artifacts tunnel available through send_to_canvas.",
702
707
  "For draw/plot/graph/chart/diagram/figure requests, publish a canvas artifact proactively.",
703
- "For LaTeX/PDF requests, publish the source and compiled PDF artifacts when available. Keep subfolder outputs beside their source and use pdflatex-compatible figure formats.",
708
+ "For LaTeX/PDF requests, check latexmk/pdflatex first, publish the source and compiled PDF artifacts when available, and avoid reinstalling TeX when an existing toolchain works.",
704
709
  "Use open_url only if the task actually needs the web.",
705
710
  "For generated local HTML/SVG/PDF/site output, use open_workspace_file or preview_workspace instead of shelling a transient local server.",
706
711
  ]
@@ -10,6 +10,7 @@ import { listAgentWrappers } from "./tool-wrappers.js";
10
10
  import { listAuxiliarySkills } from "./auxiliary-tools.js";
11
11
  import { readCodebaseMap } from "./codebase-map.js";
12
12
  import { listSkills } from "./skill-library.js";
13
+ import { platformInfo, platformLabel, platformSetupHints } from "./platform.js";
13
14
 
14
15
  const execFileAsync = promisify(execFile);
15
16
 
@@ -112,19 +113,28 @@ function trustedDockerPolicyChecks(config) {
112
113
  export async function buildCapabilityReport(projectRoot, packageVersion, config) {
113
114
  const paths = projectPaths(projectRoot);
114
115
  const keyStatus = providerKeyStatus(projectRoot);
115
- const [node, npm, python, conda, r, pdflatex, latexmk, dockerStatus, sessions, instructions, codebaseMap] = await Promise.all([
116
+ const platform = platformInfo();
117
+ const commandChecks = [
116
118
  commandAvailable("node", ["--version"]),
117
119
  commandAvailable("npm", ["--version"]),
118
- commandAvailable("python3", ["--version"]),
120
+ commandAvailable(platform.isWindows ? "python" : "python3", ["--version"]),
119
121
  commandAvailable("conda", ["--version"]),
120
122
  commandAvailable("R", ["--version"]),
121
123
  commandAvailable("pdflatex", ["--version"]),
122
124
  commandAvailable("latexmk", ["--version"]),
125
+ ];
126
+ if (platform.isMac) commandChecks.push(commandAvailable("brew", ["--version"]));
127
+ if (platform.isWindows) commandChecks.push(commandAvailable("wsl", ["--status"]));
128
+ const [commands, dockerStatus, sessions, instructions, codebaseMap] = await Promise.all([
129
+ Promise.all(commandChecks),
123
130
  getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
124
131
  listProjectSessions(projectRoot, 12),
125
132
  readProjectInstructions(projectRoot, { maxBytes: 1 }),
126
133
  readCodebaseMap(projectRoot),
127
134
  ]);
135
+ const [node, npm, python, conda, r, pdflatex, latexmk] = commands;
136
+ const homebrew = platform.isMac ? commands[7] : undefined;
137
+ const wsl = platform.isWindows ? commands[7] : undefined;
128
138
 
129
139
  const npmPrefixPolicy = evaluateCommandPolicy("npm --prefix round9-node-app test", config);
130
140
  const cdNpmTestPolicy = evaluateCommandPolicy("cd round9-node-app && npm test", config);
@@ -144,6 +154,8 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
144
154
  capability("R", r.available, r.available ? r : { ...r, setup: "Optional. Generate a project-local R setup plan; do not install globally from the agent." }),
145
155
  capability("pdflatex", pdflatex.available, pdflatex.available ? pdflatex : { ...pdflatex, setup: "LaTeX tasks should create .tex source and an honest setup report when TeX is unavailable." }),
146
156
  capability("latexmk", latexmk.available, latexmk.available ? latexmk : { ...latexmk, setup: "latexmk is optional if pdflatex is available." }),
157
+ ...(platform.isMac ? [capability("homebrew", homebrew?.available, homebrew?.available ? homebrew : { ...homebrew, setup: "Optional on macOS for host-mode tool installs." })] : []),
158
+ ...(platform.isWindows ? [capability("wsl", wsl?.available, wsl?.available ? wsl : { ...wsl, setup: "Recommended for Windows. Install WSL2 for the most compatible shell and Docker workflow." })] : []),
147
159
  capability("docker", Boolean(dockerStatus?.dockerAvailable), dockerStatus || {}),
148
160
  capability("deepseek-key", keyStatus.deepseek, { envVars: keyStatus.envVars.deepseek }),
149
161
  capability("openai-key", keyStatus.openai, { envVars: keyStatus.envVars.openai }),
@@ -191,6 +203,13 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
191
203
  summary: codebaseMap.ok ? codebaseMap.map.inspection?.summary || "" : "",
192
204
  },
193
205
  },
206
+ platform: {
207
+ ...platform,
208
+ label: platformLabel(platform),
209
+ setupHints: platformSetupHints(platform),
210
+ hostLatexAvailable: Boolean(pdflatex.available || latexmk.available),
211
+ hostLatexComplete: Boolean(pdflatex.available && latexmk.available),
212
+ },
194
213
  routing: {
195
214
  active: {
196
215
  provider: config.provider,
@@ -256,6 +275,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
256
275
 
257
276
  export function printCapabilityReport(report) {
258
277
  console.log(`AgInTiFlow capabilities ${report.package.version}`);
278
+ console.log(`platform=${report.platform.label} family=${report.platform.linuxFamily || report.platform.platform}`);
259
279
  console.log(`project=${report.project.root}`);
260
280
  console.log(`cwd=${report.project.commandCwd}`);
261
281
  console.log(`instructions=${report.project.instructionsPath} present=${report.project.instructionsPresent}`);
@@ -286,4 +306,8 @@ export function printCapabilityReport(report) {
286
306
  console.log(`- ${item.capability}: ${item.setup}`);
287
307
  }
288
308
  }
309
+ if (report.platform?.setupHints?.length) {
310
+ console.log("platform setup hints:");
311
+ for (const hint of report.platform.setupHints) console.log(`- ${hint}`);
312
+ }
289
313
  }
package/src/cli.js CHANGED
@@ -349,6 +349,7 @@ function printInitResult(result) {
349
349
  function printDoctorReport(report) {
350
350
  console.log(`AgInTiFlow ${report.package.version} (npm latest: ${report.package.npmLatest})`);
351
351
  console.log(`node=${report.node.version} ok=${report.node.ok}`);
352
+ console.log(`platform=${report.platform.label} family=${report.platform.linuxFamily || report.platform.platform}`);
352
353
  console.log(`project=${report.project.root}`);
353
354
  console.log(`instructions=${report.project.instructionsPath} present=${report.project.instructionsPresent}`);
354
355
  console.log(`sessions=${report.project.sessionsDir}`);
@@ -369,6 +370,10 @@ function printDoctorReport(report) {
369
370
  `wrappers=${report.wrappers.map((wrapper) => `${wrapper.name}:${wrapper.available ? "ok" : "missing"}`).join(" ")}`
370
371
  );
371
372
  console.log(`sessions=${report.sessions.length}`);
373
+ if (report.platform.setupHints.length > 0) {
374
+ console.log("platform setup hints:");
375
+ for (const hint of report.platform.setupHints) console.log(`- ${hint}`);
376
+ }
372
377
  }
373
378
 
374
379
  async function readStdin() {
@@ -84,7 +84,12 @@ const PACKAGE_INSTALL_PATTERNS = [
84
84
  const SYSTEM_PACKAGE_INSTALL_PATTERNS = [
85
85
  /^(?:sudo\s+)?apt(?:-get)?\s+update$/,
86
86
  /^(?:sudo\s+)?apt(?:-get)?\s+install(?:\s+-y)?(?:\s+[-@\w.+:=]+)+$/,
87
+ /^(?:sudo\s+)?(?:dnf|yum)\s+(?:makecache|check-update)(?:\s+[-\w]+)*$/,
88
+ /^(?:sudo\s+)?(?:dnf|yum)\s+install(?:\s+-y)?(?:\s+[-@\w.+:=]+)+$/,
87
89
  /^apk\s+add(?:\s+--no-cache)?(?:\s+[-@\w.+:=]+)+$/,
90
+ /^brew\s+install(?:\s+[-@\w.+:=/]+)+$/,
91
+ /^winget\s+install(?:\s+[-@\w.+:=/]+)+$/,
92
+ /^choco\s+install(?:\s+[-@\w.+:=/]+)+$/,
88
93
  ];
89
94
 
90
95
  const ENV_SETUP_PATTERNS = [
@@ -214,6 +214,7 @@ function dockerCommand(command, policy) {
214
214
  `export HOME=${DOCKER_HOME}`,
215
215
  `export XDG_CACHE_HOME=${DOCKER_CACHE}`,
216
216
  `export PIP_CACHE_DIR=${DOCKER_CACHE}/pip`,
217
+ `export MPLCONFIGDIR=/tmp/matplotlib`,
217
218
  `export UV_CACHE_DIR=${DOCKER_CACHE}/uv`,
218
219
  `export NPM_CONFIG_CACHE=${DOCKER_CACHE}/npm`,
219
220
  `export CONDA_PKGS_DIRS=${DOCKER_CACHE}/conda-pkgs`,
@@ -225,6 +226,7 @@ function dockerCommand(command, policy) {
225
226
 
226
227
  if (!policy.requiresDockerRoot) {
227
228
  envLines.push(
229
+ `mkdir -p /tmp/matplotlib >/dev/null 2>&1 || true`,
228
230
  `if command -v python3 >/dev/null 2>&1 && [ ! -x ${DOCKER_ENV}/python/bin/python ]; then python3 -m venv --system-site-packages ${DOCKER_ENV}/python >/dev/null 2>&1 || true; fi`,
229
231
  `if [ -f ${DOCKER_ENV}/python/bin/activate ]; then . ${DOCKER_ENV}/python/bin/activate; fi`
230
232
  );
@@ -4,6 +4,7 @@ import { getTaskProfile } from "./task-profiles.js";
4
4
  import { listAuxiliarySkills } from "./auxiliary-tools.js";
5
5
  import { engineeringGuidanceForTask } from "./engineering-guidance.js";
6
6
  import { formatSkillsForPrompt, selectSkillsForGoal } from "./skill-library.js";
7
+ import { platformInfo, platformLabel } from "./platform.js";
7
8
 
8
9
  export function createClient(config) {
9
10
  if (config.provider === "mock") {
@@ -197,6 +198,7 @@ export async function createPlan(client, config, state) {
197
198
  const selectedSkills = selectSkillsForGoal(state.goal, { taskProfile: config.taskProfile, limit: 5 });
198
199
  const skillContext = formatSkillsForPrompt(selectedSkills);
199
200
  const projectInstructions = state.meta?.projectInstructions;
201
+ const platform = platformInfo();
200
202
  if (client.mock) {
201
203
  return [
202
204
  "1. Inspect the request and prefer the local shell when available.",
@@ -222,7 +224,7 @@ export async function createPlan(client, config, state) {
222
224
  state.startUrl ? `Suggested start URL: ${state.startUrl}` : "",
223
225
  config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
224
226
  config.allowShellTool
225
- ? `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.`
227
+ ? `Shell tool is enabled in ${config.commandCwd}. Host platform: ${platformLabel(platform)}. 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. On native Windows host mode, prefer PowerShell/cmd-compatible commands or WSL/Docker for bash-like toolchains.`
226
228
  : "",
227
229
  config.allowFileTools
228
230
  ? `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 AGINTI.md/AGENTS.md/README/manifests and 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.`
@@ -250,6 +252,7 @@ export async function createPlan(client, config, state) {
250
252
  "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.",
251
253
  "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.",
252
254
  "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.",
255
+ "For LaTeX/PDF work, first check whether latexmk or pdflatex already exists in the active host/Docker environment; compile with the existing toolchain before installing packages or rebuilding Docker.",
253
256
  "For web search or current information tasks, plan to use browser tools or safe shell network tools when allowed, then preserve useful source notes if the output depends on them.",
254
257
  "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.",
255
258
  "For environment or system-maintenance work, prefer project-local dry-run plans/scripts unless the configured policy explicitly allows stronger actions.",
@@ -0,0 +1,103 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+
4
+ function parseOsRelease() {
5
+ if (process.platform !== "linux") return {};
6
+ try {
7
+ const content = fs.readFileSync("/etc/os-release", "utf8");
8
+ const data = {};
9
+ for (const line of content.split(/\r?\n/)) {
10
+ const match = line.match(/^([A-Z0-9_]+)=(.*)$/);
11
+ if (!match) continue;
12
+ data[match[1].toLowerCase()] = match[2].replace(/^"|"$/g, "");
13
+ }
14
+ return data;
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+
20
+ export function platformInfo() {
21
+ const osRelease = parseOsRelease();
22
+ const release = os.release();
23
+ const isWsl = Boolean(process.env.WSL_DISTRO_NAME) || /microsoft|wsl/i.test(release);
24
+ const linuxId = osRelease.id || "";
25
+ const linuxLike = osRelease.id_like || "";
26
+ const linuxFamily = /ubuntu|debian/i.test(`${linuxId} ${linuxLike}`)
27
+ ? "debian"
28
+ : /rhel|fedora|centos|rocky|almalinux|redhat/i.test(`${linuxId} ${linuxLike}`)
29
+ ? "redhat"
30
+ : process.platform === "linux"
31
+ ? "linux"
32
+ : "";
33
+
34
+ return {
35
+ platform: process.platform,
36
+ arch: process.arch,
37
+ release,
38
+ type: os.type(),
39
+ distro: osRelease.pretty_name || osRelease.name || "",
40
+ linuxId,
41
+ linuxFamily,
42
+ isMac: process.platform === "darwin",
43
+ isLinux: process.platform === "linux",
44
+ isWindows: process.platform === "win32",
45
+ isWsl,
46
+ };
47
+ }
48
+
49
+ export function platformLabel(info = platformInfo()) {
50
+ if (info.isWsl) return `WSL ${info.distro || info.linuxId || "Linux"} (${info.arch})`;
51
+ if (info.isMac) return `macOS (${info.arch})`;
52
+ if (info.isWindows) return `Windows (${info.arch})`;
53
+ if (info.isLinux) return `${info.distro || "Linux"} (${info.arch})`;
54
+ return `${info.platform} (${info.arch})`;
55
+ }
56
+
57
+ export function platformSetupHints(info = platformInfo()) {
58
+ if (info.isWsl) {
59
+ return [
60
+ "Use the Linux/WSL shell as the primary AgInTiFlow environment; keep projects under the WSL filesystem for best file and Docker performance.",
61
+ "Use Docker Desktop with WSL integration or Docker Engine inside WSL for docker-workspace mode.",
62
+ "For LaTeX, reuse WSL latexmk/pdflatex when installed, or use the companion Docker sandbox image.",
63
+ ];
64
+ }
65
+ if (info.isMac) {
66
+ return [
67
+ "Use Node.js 22+ from Homebrew, nvm, fnm, or the official installer.",
68
+ "Use Docker Desktop or Colima for docker-workspace mode; the Ubuntu Docker installer is intentionally not used on macOS.",
69
+ "For LaTeX, reuse MacTeX/BasicTeX when latexmk and pdflatex are on PATH; otherwise use the companion Docker sandbox image.",
70
+ "Use Homebrew for optional host tools such as ripgrep, git, python, and latexmk when you choose host mode.",
71
+ ];
72
+ }
73
+ if (info.isWindows) {
74
+ return [
75
+ "Best supported Windows path: run AgInTiFlow inside WSL2 with Node.js 22+ and Docker Desktop WSL integration.",
76
+ "Native Windows host shell is best effort; prefer docker-workspace or WSL for bash-like coding, LaTeX, and package-manager tasks.",
77
+ "For LaTeX, use Docker/WSL, or install MiKTeX/TeX Live and ensure pdflatex/latexmk are on PATH.",
78
+ ];
79
+ }
80
+ if (info.linuxFamily === "debian") {
81
+ return [
82
+ "Use Node.js 22+ from nvm/fnm, NodeSource, or distro packages.",
83
+ "Docker can be installed with scripts/install-docker-ubuntu.sh on Ubuntu/Debian-like hosts.",
84
+ "For LaTeX, reuse host latexmk/pdflatex when available; otherwise use the companion Docker sandbox image.",
85
+ ];
86
+ }
87
+ if (info.linuxFamily === "redhat") {
88
+ return [
89
+ "Use Node.js 22+ from nvm/fnm or the Red Hat/Fedora package stream.",
90
+ "Install Docker/Podman-compatible Docker CLI with dnf/yum or vendor docs; do not use the Ubuntu Docker installer.",
91
+ "For LaTeX, reuse host latexmk/pdflatex from texlive packages when available; otherwise use the companion Docker sandbox image.",
92
+ ];
93
+ }
94
+ return [
95
+ "Use Node.js 22+ and prefer docker-workspace for portable shell/toolchain work.",
96
+ "For LaTeX, reuse host latexmk/pdflatex when available; otherwise use the companion Docker sandbox image.",
97
+ ];
98
+ }
99
+
100
+ export function hostShellOption() {
101
+ if (process.platform === "win32") return process.env.ComSpec || true;
102
+ return process.env.SHELL || "/bin/bash";
103
+ }
package/src/project.js CHANGED
@@ -5,6 +5,7 @@ import { execFile } from "node:child_process";
5
5
  import { promisify } from "node:util";
6
6
  import { listAgentWrappers } from "./tool-wrappers.js";
7
7
  import { getDockerSandboxStatus } from "./docker-sandbox.js";
8
+ import { platformInfo, platformLabel, platformSetupHints } from "./platform.js";
8
9
 
9
10
  const execFileAsync = promisify(execFile);
10
11
  const LOCAL_ENV_KEYS = new Set([
@@ -390,6 +391,7 @@ export async function npmLatestVersion(packageName = "@lazyingart/agintiflow") {
390
391
  export async function doctorReport(projectRoot, packageVersion, config) {
391
392
  const paths = projectPaths(projectRoot);
392
393
  const keyStatus = providerKeyStatus(projectRoot);
394
+ const platform = platformInfo();
393
395
  const [sessions, dockerStatus, latestVersion, instructions] = await Promise.all([
394
396
  listProjectSessions(projectRoot, 8),
395
397
  getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
@@ -408,6 +410,11 @@ export async function doctorReport(projectRoot, packageVersion, config) {
408
410
  version: process.version,
409
411
  ok: Number(process.versions.node.split(".")[0]) >= 22,
410
412
  },
413
+ platform: {
414
+ ...platform,
415
+ label: platformLabel(platform),
416
+ setupHints: platformSetupHints(platform),
417
+ },
411
418
  project: {
412
419
  root: paths.root,
413
420
  instructionsPath: paths.agintiInstructionsPath,
@@ -80,7 +80,7 @@ export const TASK_PROFILES = {
80
80
  id: "latex",
81
81
  label: "LaTeX",
82
82
  prompt:
83
- "Bias toward LaTeX/PDF production while still using writing, plotting, code, and web research when needed. Locate or create source and figures in a subfolder, compile when a TeX toolchain is available, run enough passes for references, and send the PDF through the canvas tunnel. In Docker, use /workspace for outputs and /aginti-env for persistent tools when setup is needed.",
83
+ "Bias toward LaTeX/PDF production while still using writing, plotting, code, and web research when needed. Locate or create source and figures in a subfolder, check existing latexmk/pdflatex before installing or rebuilding toolchains, compile when a TeX toolchain is available, run enough passes for references, and send the PDF through the canvas tunnel. In Docker, use /workspace for outputs and /aginti-env for persistent tools only when setup is actually needed.",
84
84
  tools: ["files", "shell", "canvas", "sandbox"],
85
85
  },
86
86
  maintenance: {
@@ -16,7 +16,11 @@ const BASE_ADVISORY_PROMPT = [
16
16
 
17
17
  function commandExists(command) {
18
18
  try {
19
- execFileSync("bash", ["-lc", `command -v ${command}`], { stdio: "ignore" });
19
+ if (process.platform === "win32") {
20
+ execFileSync("where", [command], { stdio: "ignore" });
21
+ } else {
22
+ execFileSync("sh", ["-lc", `command -v ${command}`], { stdio: "ignore" });
23
+ }
20
24
  return true;
21
25
  } catch {
22
26
  return false;
package/web.js CHANGED
@@ -15,6 +15,7 @@ import { listTaskProfiles, normalizeTaskProfile } from "./src/task-profiles.js";
15
15
  import { loadProjectEnv, projectPaths, providerKeyStatus, setProviderKey } from "./src/project.js";
16
16
  import { buildCapabilityReport } from "./src/capabilities.js";
17
17
  import { listSkills } from "./src/skill-library.js";
18
+ import { platformInfo, platformLabel, platformSetupHints } from "./src/platform.js";
18
19
  import {
19
20
  buildArtifacts,
20
21
  countUnreadArtifacts,
@@ -559,6 +560,7 @@ app.get("/api/config", async (_req, res) => {
559
560
  const config = buildRunConfig({ ...preferences, goal: "" });
560
561
  const paths = projectPaths(baseDir);
561
562
  const keyStatus = publicKeyStatus(baseDir);
563
+ const platform = platformInfo();
562
564
  res.json({
563
565
  project: {
564
566
  root: paths.root,
@@ -567,6 +569,11 @@ app.get("/api/config", async (_req, res) => {
567
569
  sessionDbPath: paths.sessionDbPath,
568
570
  sharedSessionFolder: path.resolve(config.sessionsDir) === path.resolve(sessionsDir),
569
571
  localEnvPresent: keyStatus.localEnv,
572
+ platform: {
573
+ ...platform,
574
+ label: platformLabel(platform),
575
+ setupHints: platformSetupHints(platform),
576
+ },
570
577
  },
571
578
  defaults: {
572
579
  openai: publicProviderDefault("openai"),