@lazyingart/agintiflow 0.12.8 → 0.12.9

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
@@ -75,7 +75,7 @@ For code edits, AgInTiFlow routes patch/refactor/database-style tasks to DeepSee
75
75
 
76
76
  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).
77
77
 
78
- AgInTiFlow can also spend cheap DeepSeek calls on parallel scout notes before the main executor starts a complicated task. Scouts run independently for architecture, implementation, review, and research risks, then the main agent uses those notes while still doing the real file/shell/browser work itself. Disable with `--no-parallel-scouts` or set `--scout-count 1..4`.
78
+ AgInTiFlow can also spend cheap DeepSeek calls on parallel scout notes before the main executor starts a complicated task. Scouts run independently for architecture, implementation, review, research, context mapping, tests, git workflow, and integration risks, then a coordinator synthesis is injected for the main agent. The executor still does the real file/shell/browser work itself. Disable with `--no-parallel-scouts` or set `--scout-count 1..8`.
79
79
 
80
80
  For current docs, install errors, package/toolchain setup, and source discovery, the agent has a guarded `web_search` tool. It returns compact search results without browser search-engine loops and respects configured domain allowlists. Disable with `--no-web-search`.
81
81
 
@@ -38,6 +38,22 @@ For complicated tasks, the agent should follow this loop:
38
38
  6. `run_command` for the narrowest relevant check first.
39
39
  7. Broaden checks only after the focused check passes.
40
40
 
41
+ ## Context Budgeting
42
+
43
+ Mature coding agents avoid keeping an entire growing project in the prompt. AgInTiFlow uses a layered context pack:
44
+
45
+ - Stable memory: `AGINTI.md`, `AGENTS.md`, README files, manifests, and package scripts.
46
+ - Project map: `inspect_project` summaries, language counts, source/test directories, and recommended reads.
47
+ - Active evidence: exact search hits, selected files, failing command output, and compact git status/diff.
48
+ - Patch context: only the nearby code needed for `apply_patch`, plus before/after hashes and compact diffs.
49
+ - Scout synthesis: cheap parallel DeepSeek scouts produce bounded advice, then a coordinator summary is injected instead of every long transcript becoming permanent context.
50
+
51
+ This keeps the main executor sober: it knows where it is in the repo, but it still re-reads exact files before editing and validates with commands rather than trusting stale memory.
52
+
53
+ ## Git Discipline
54
+
55
+ When asked to commit, pull, merge, or push, the agent should run `git status --short` and `git diff --stat` first. It should commit only intended changes, use `git fetch` and `git pull --ff-only` when remote state matters, and stop for the user on conflicts, divergence, unrelated dirty files, or any merge/rebase/reset choice. Web and CLI logs fold long command output but keep full command summaries visible.
56
+
41
57
  ## CLI And Web Parity
42
58
 
43
59
  Both CLI and web use the same task profile registry and the same model/tool schemas. Use either:
@@ -74,11 +90,15 @@ DeepSeek calls are cheap enough that complex tasks can use several short advisor
74
90
  - Implementer: predicts patch boundaries and focused checks.
75
91
  - Reviewer: looks for missing tests, risks, and instruction-compliance failures.
76
92
  - Researcher: suggests `web_search` queries when current information may matter.
93
+ - Cartographer: builds a compact context map instead of dumping the whole tree.
94
+ - Tester: finds the narrowest useful checks and setup blockers.
95
+ - Git operator: keeps status/diff/commit/pull/push workflows disciplined.
96
+ - Integrator: looks for cross-stream conflicts and ordering constraints.
77
97
 
78
- Scout output is injected as advisory context only. The main agent still owns execution and must use real tools to inspect, edit, run commands, and finish. CLI flags:
98
+ Scout output is synthesized by a coordinator note and injected as advisory context only. The main agent still owns execution and must use real tools to inspect, edit, run commands, and finish. CLI flags:
79
99
 
80
100
  ```bash
81
- aginti --parallel-scouts --scout-count 4 "fix this complicated repo bug"
101
+ aginti --parallel-scouts --scout-count 8 "fix this complicated repo bug"
82
102
  aginti --no-parallel-scouts "run a cheap short task"
83
103
  ```
84
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.12.8",
3
+ "version": "0.12.9",
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
@@ -757,6 +757,21 @@ function setLogs(text, mode = "active") {
757
757
  logsEl.textContent = text;
758
758
  }
759
759
 
760
+ function outputLineCount(value = "") {
761
+ const text = String(value || "");
762
+ return text ? text.split(/\r?\n/).length : 0;
763
+ }
764
+
765
+ function outputPreviewText(value = "", maxLines = 18) {
766
+ const lines = String(value || "").split(/\r?\n/);
767
+ const shown = lines.slice(0, maxLines);
768
+ return {
769
+ text: shown.join("\n"),
770
+ hidden: Math.max(lines.length - shown.length, 0),
771
+ total: value ? lines.length : 0,
772
+ };
773
+ }
774
+
760
775
  function updateStopRunButton() {
761
776
  if (!stopRunButton) return;
762
777
  stopRunButton.hidden = currentRunStatus !== "running";
@@ -1027,7 +1042,7 @@ function formPayload() {
1027
1042
  allowAuxiliaryTools: allowAuxiliaryToolsField?.checked ?? true,
1028
1043
  allowWebSearch: allowWebSearchField?.checked ?? true,
1029
1044
  allowParallelScouts: allowParallelScoutsField?.checked ?? true,
1030
- parallelScoutCount: Number(parallelScoutCountField?.value) || 3,
1045
+ parallelScoutCount: Math.min(Math.max(Number(parallelScoutCountField?.value) || 3, 1), 8),
1031
1046
  allowWrapperTools: allowWrapperToolsField.checked,
1032
1047
  preferredWrapper: preferredWrapperField.value,
1033
1048
  taskProfile: taskProfileField?.value || "auto",
@@ -1038,22 +1053,60 @@ function formPayload() {
1038
1053
  };
1039
1054
  }
1040
1055
 
1056
+ function renderCommandOutputLog(entry) {
1057
+ const data = entry.data || {};
1058
+ const command = data.command || "";
1059
+ const stdout = data.stdout || "";
1060
+ const stderr = data.stderr || "";
1061
+ const stdoutPreview = outputPreviewText(stdout);
1062
+ const stderrPreview = outputPreviewText(stderr);
1063
+ const totalLines = outputLineCount(stdout) + outputLineCount(stderr);
1064
+ const large = totalLines > 18 || stdout.length + stderr.length > 2600;
1065
+ const policy = data.commandPolicy?.category ? ` · ${data.commandPolicy.category}` : "";
1066
+ const status = data.blocked ? "blocked" : data.error ? "error" : "ok";
1067
+ const details = [
1068
+ stdout ? `<div class="log-stream-title">stdout</div><pre>${escapeHtml(stdoutPreview.text)}</pre>` : "",
1069
+ stdoutPreview.hidden > 0 ? `<div class="log-fold-note">... ${stdoutPreview.hidden} more stdout line(s) folded</div>` : "",
1070
+ stderr ? `<div class="log-stream-title">stderr</div><pre>${escapeHtml(stderrPreview.text)}</pre>` : "",
1071
+ stderrPreview.hidden > 0 ? `<div class="log-fold-note">... ${stderrPreview.hidden} more stderr line(s) folded</div>` : "",
1072
+ data.error ? `<div class="log-fold-note">${escapeHtml(data.error)}</div>` : "",
1073
+ ]
1074
+ .filter(Boolean)
1075
+ .join("");
1076
+
1077
+ return `
1078
+ <details class="log-command" ${large ? "" : "open"}>
1079
+ <summary>
1080
+ <span>${escapeHtml(`[${entry.at}] command ${status}${policy}`)}</span>
1081
+ <code>${escapeHtml(command)}</code>
1082
+ <small>stdout=${outputLineCount(stdout)} stderr=${outputLineCount(stderr)}${large ? " · folded" : ""}</small>
1083
+ </summary>
1084
+ ${details || `<div class="log-fold-note">No command output.</div>`}
1085
+ </details>
1086
+ `;
1087
+ }
1088
+
1041
1089
  function renderLogs(run) {
1042
- const lines = [];
1043
- lines.push(`status=${run.status} session=${run.sessionId} provider=${run.provider} model=${run.model}`);
1044
- if (run.result) lines.push(`result=${run.result}`);
1045
- if (run.error) lines.push(`error=${run.error}`);
1046
- lines.push("");
1090
+ logsEl.dataset.mode = "active";
1091
+ const parts = [
1092
+ `<div class="log-line">${escapeHtml(`status=${run.status} session=${run.sessionId} provider=${run.provider} model=${run.model}`)}</div>`,
1093
+ run.result ? `<div class="log-line">${escapeHtml(`result=${run.result}`)}</div>` : "",
1094
+ run.error ? `<div class="log-line error">${escapeHtml(`error=${run.error}`)}</div>` : "",
1095
+ ];
1047
1096
 
1048
1097
  for (const entry of run.logs || []) {
1049
- lines.push(`[${entry.at}] ${entry.kind}: ${entry.message}`);
1098
+ if (entry.message === "command.output") {
1099
+ parts.push(renderCommandOutputLog(entry));
1100
+ continue;
1101
+ }
1102
+ parts.push(`<div class="log-line">${escapeHtml(`[${entry.at}] ${entry.kind}: ${entry.message}`)}</div>`);
1050
1103
  if (entry.data && Object.keys(entry.data).length > 0) {
1051
- lines.push(JSON.stringify(entry.data, null, 2));
1104
+ parts.push(`<pre class="log-json">${escapeHtml(JSON.stringify(entry.data, null, 2))}</pre>`);
1052
1105
  }
1053
- lines.push("");
1054
1106
  }
1055
1107
 
1056
- setLogs(lines.join("\n"));
1108
+ logsEl.innerHTML = parts.filter(Boolean).join("");
1109
+ logsEl.scrollTop = logsEl.scrollHeight;
1057
1110
  }
1058
1111
 
1059
1112
  function escapeHtml(value) {
package/public/index.html CHANGED
@@ -205,7 +205,7 @@
205
205
 
206
206
  <label>
207
207
  <span data-i18n="parallelScoutCountLabel">Scout count</span>
208
- <input id="parallelScoutCount" name="parallelScoutCount" type="number" min="1" max="4" value="3" />
208
+ <input id="parallelScoutCount" name="parallelScoutCount" type="number" min="1" max="8" value="3" />
209
209
  </label>
210
210
 
211
211
  <div class="grid wrapper-controls">
@@ -338,7 +338,7 @@
338
338
  <div id="run-meta" class="subtle"></div>
339
339
  </div>
340
340
  </div>
341
- <pre id="logs" class="logs" data-i18n="noRunStarted">No run started.</pre>
341
+ <div id="logs" class="logs" data-i18n="noRunStarted">No run started.</div>
342
342
  </section>
343
343
  </section>
344
344
  </main>
package/public/styles.css CHANGED
@@ -530,6 +530,64 @@ button.danger {
530
530
  overflow-wrap: anywhere;
531
531
  }
532
532
 
533
+ .log-line {
534
+ margin: 0 0 10px;
535
+ }
536
+
537
+ .log-line.error {
538
+ color: #fecaca;
539
+ }
540
+
541
+ .log-json,
542
+ .log-command pre {
543
+ margin: 8px 0 12px;
544
+ padding: 10px;
545
+ border-radius: 10px;
546
+ background: rgba(15, 23, 42, 0.78);
547
+ color: #e5e7eb;
548
+ border: 1px solid rgba(148, 163, 184, 0.18);
549
+ white-space: pre-wrap;
550
+ overflow: auto;
551
+ }
552
+
553
+ .log-command {
554
+ margin: 0 0 10px;
555
+ border: 1px solid rgba(148, 163, 184, 0.18);
556
+ border-radius: 12px;
557
+ background: rgba(255, 255, 255, 0.04);
558
+ }
559
+
560
+ .log-command summary {
561
+ display: grid;
562
+ grid-template-columns: minmax(0, 1fr);
563
+ gap: 4px;
564
+ cursor: pointer;
565
+ padding: 10px 12px;
566
+ color: #dbeafe;
567
+ white-space: normal;
568
+ }
569
+
570
+ .log-command summary code {
571
+ color: #fef3c7;
572
+ overflow-wrap: anywhere;
573
+ }
574
+
575
+ .log-command summary small,
576
+ .log-stream-title,
577
+ .log-fold-note {
578
+ color: #94a3b8;
579
+ font-size: 0.78rem;
580
+ }
581
+
582
+ .log-command[open] {
583
+ background: rgba(255, 255, 255, 0.06);
584
+ }
585
+
586
+ .log-stream-title,
587
+ .log-fold-note {
588
+ padding: 0 12px 8px;
589
+ }
590
+
533
591
  .chat-panel {
534
592
  display: grid;
535
593
  gap: 12px;
@@ -53,6 +53,18 @@ try {
53
53
  capabilities.checks.some((check) => check.name === "bash-syntax-policy" && check.ok),
54
54
  "bash -n maintenance script policy is not allowed"
55
55
  );
56
+ assert(
57
+ capabilities.checks.some((check) => check.name === "git-status-policy" && check.ok),
58
+ "git status policy is not allowed"
59
+ );
60
+ assert(
61
+ capabilities.checks.some((check) => check.name === "git-commit-policy" && check.ok),
62
+ "git commit policy is not allowed"
63
+ );
64
+ assert(
65
+ capabilities.checks.some((check) => check.name === "git-pull-ff-only-policy" && check.ok),
66
+ "unsafe git pull policy was not blocked"
67
+ );
56
68
  assert(
57
69
  capabilities.maintenancePolicy.some((check) => check.command.startsWith("sudo") && !check.allowed),
58
70
  "sudo maintenance command was not blocked"
@@ -79,7 +91,7 @@ try {
79
91
  {
80
92
  ok: true,
81
93
  projectRoot: tempRoot,
82
- checks: ["aginti-md-init", "capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy"],
94
+ checks: ["aginti-md-init", "capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy", "git-policy"],
83
95
  },
84
96
  null,
85
97
  2
@@ -1175,6 +1175,7 @@ export async function runAgent(config) {
1175
1175
  model: scouts.model,
1176
1176
  requested: scouts.requested,
1177
1177
  completed: scouts.completed,
1178
+ synthesis: scouts.synthesis || "",
1178
1179
  };
1179
1180
  state.messages.push({
1180
1181
  role: "user",
@@ -1184,6 +1185,7 @@ export async function runAgent(config) {
1184
1185
  model: scouts.model,
1185
1186
  requested: scouts.requested,
1186
1187
  completed: scouts.completed,
1188
+ synthesis: scouts.synthesis || "",
1187
1189
  scouts: scouts.scouts.map((scout) => ({
1188
1190
  name: scout.name,
1189
1191
  model: scout.model,
@@ -129,6 +129,9 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
129
129
  const nodeTestPolicy = evaluateCommandPolicy("node --test round9-node-app/test/app.test.js", config);
130
130
  const bashSyntaxPolicy = evaluateCommandPolicy("bash -n maintenance/setup-conda.sh", config);
131
131
  const texPolicy = evaluateCommandPolicy("pdflatex -interaction=nonstopmode -halt-on-error docs/note.tex", config);
132
+ const gitStatusPolicy = evaluateCommandPolicy("git status --short", config);
133
+ const gitCommitPolicy = evaluateCommandPolicy('git commit -m "test commit"', config);
134
+ const gitPullPolicy = evaluateCommandPolicy("git pull", config);
132
135
 
133
136
  const checks = [
134
137
  capability("node", node.available, node),
@@ -156,6 +159,9 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
156
159
  capability("node-test-policy", Boolean(nodeTestPolicy.allowed), nodeTestPolicy),
157
160
  capability("bash-syntax-policy", Boolean(bashSyntaxPolicy.allowed), bashSyntaxPolicy),
158
161
  capability("tex-policy", Boolean(texPolicy.allowed), texPolicy),
162
+ capability("git-status-policy", Boolean(gitStatusPolicy.allowed), gitStatusPolicy),
163
+ capability("git-commit-policy", Boolean(gitCommitPolicy.allowed), gitCommitPolicy),
164
+ capability("git-pull-ff-only-policy", !gitPullPolicy.allowed, gitPullPolicy),
159
165
  ];
160
166
 
161
167
  return {
package/src/cli.js CHANGED
@@ -261,7 +261,7 @@ export function parseArgs(argv) {
261
261
 
262
262
  function printUsage() {
263
263
  console.log(
264
- 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti login deepseek|openai|grsai OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 3] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
264
+ 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti login deepseek|openai|grsai OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..8] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
265
265
  );
266
266
  }
267
267
 
@@ -46,6 +46,19 @@ const NETWORK_FETCH_PATTERNS = [
46
46
  /^wget\s+(?:-[A-Za-z0-9]*\s+)*(?:-O\s+[-\w./]+\s+)?https?:\/\/\S+$/,
47
47
  ];
48
48
 
49
+ const GIT_WORKFLOW_PATTERNS = [
50
+ /^git\s+add(?:\s+[-\w./*]+)+$/,
51
+ /^git\s+commit\s+(?:-a\s+)?-m\s+(['"])[^'"\n]{1,220}\1$/,
52
+ /^git\s+fetch(?:\s+[-\w./:=]+)*$/,
53
+ /^git\s+pull\s+--ff-only(?:\s+[-\w./:=]+)*$/,
54
+ /^git\s+push(?:\s+[-\w./:=]+)*$/,
55
+ ];
56
+
57
+ const UNSAFE_GIT_PATTERNS = [
58
+ /^git\s+pull\b(?!\s+--ff-only(?:\s|$))/,
59
+ /^git\s+(merge|rebase|reset|checkout|switch|clean)\b/,
60
+ ];
61
+
49
62
  const TOOLCHAIN_PATTERNS = [
50
63
  /^python(?:3)?\s+[-\w./]+\.py(?:\s+[-\w./:=]+)*$/,
51
64
  /^latexmk\s+(?=[-\w./=\s]*-pdf\b)(?:(?:-cd|-pdf|-interaction=nonstopmode|-halt-on-error|-output-directory=[-\w./]+)\s+)+[-\w./]+\.tex$/,
@@ -90,10 +103,6 @@ const BLOCKED_WRITE_TOKENS = [
90
103
  " touch",
91
104
  " tee",
92
105
  "-delete",
93
- "git add",
94
- "git commit",
95
- "git push",
96
- "git pull",
97
106
  "git checkout",
98
107
  "git switch",
99
108
  "git reset",
@@ -153,6 +162,14 @@ function classifySimpleCommand(normalized) {
153
162
  if (SENSITIVE_COMMAND_PATTERNS.some((pattern) => pattern.test(normalized))) {
154
163
  return { category: "blocked", reason: "Command is blocked because it references secrets or credential files." };
155
164
  }
165
+ if (matchAny(UNSAFE_GIT_PATTERNS, normalized)) {
166
+ return {
167
+ category: "destructive",
168
+ needsApproval: true,
169
+ reason:
170
+ "Git merge/rebase/reset/checkout/switch/clean, and non-ff-only pulls, can rewrite or conflict with local work. Inspect status/diff first and ask the user when the repository is divergent or conflicted.",
171
+ };
172
+ }
156
173
 
157
174
  if (matchAny(SAFE_WORKSPACE_WRITE_PATTERNS, normalized)) {
158
175
  const target = normalized.replace(/^mkdir\s+-p\s+/, "");
@@ -170,6 +187,16 @@ function classifySimpleCommand(normalized) {
170
187
  reason: `Command changes workspace file mode: ${normalized}`,
171
188
  };
172
189
  }
190
+ if (matchAny(GIT_WORKFLOW_PATTERNS, normalized)) {
191
+ const remote = /^git\s+(fetch|pull|push)\b/.test(normalized);
192
+ const writesWorkspace = /^git\s+(add|commit|pull)\b/.test(normalized);
193
+ return {
194
+ category: remote ? "git-remote" : "git-workflow",
195
+ needsNetwork: remote,
196
+ writesWorkspace,
197
+ reason: "Git workflow command. Agent should run git status/diff first and stop on conflicts or divergence.",
198
+ };
199
+ }
173
200
 
174
201
  const lowered = ` ${normalized.toLowerCase()} `;
175
202
  if (BLOCKED_WRITE_TOKENS.some((part) => lowered.includes(part))) {
@@ -341,7 +368,7 @@ export function evaluateCommandPolicy(command, config) {
341
368
  };
342
369
  }
343
370
 
344
- if (classification.category === "network-fetch" && config.useDockerSandbox && !trustedDockerShell) {
371
+ if (classification.needsNetwork && config.useDockerSandbox && !trustedDockerShell) {
345
372
  return {
346
373
  allowed: false,
347
374
  ...classification,
package/src/config.js CHANGED
@@ -17,6 +17,10 @@ function parseNumber(value, fallback) {
17
17
  return Number.isFinite(parsed) ? parsed : fallback;
18
18
  }
19
19
 
20
+ function clampNumber(value, min, max) {
21
+ return Math.min(Math.max(value, min), max);
22
+ }
23
+
20
24
  function parseList(value) {
21
25
  if (!value) return [];
22
26
  return String(value)
@@ -95,7 +99,11 @@ export function resolveRuntimeConfig(args, overrides = {}) {
95
99
  overrides.allowParallelScouts ?? args.allowParallelScouts ?? process.env.AGINTI_PARALLEL_SCOUTS,
96
100
  true
97
101
  ),
98
- parallelScoutCount: parseNumber(overrides.parallelScoutCount ?? args.parallelScoutCount ?? process.env.AGINTI_SCOUT_COUNT, 3),
102
+ parallelScoutCount: clampNumber(
103
+ parseNumber(overrides.parallelScoutCount ?? args.parallelScoutCount ?? process.env.AGINTI_SCOUT_COUNT, 3),
104
+ 1,
105
+ 8
106
+ ),
99
107
  preferredWrapper: normalizeWrapperName(
100
108
  overrides.preferredWrapper ?? args.preferredWrapper ?? process.env.PREFERRED_WRAPPER ?? process.env.AGENT_WRAPPER
101
109
  ),
@@ -43,6 +43,12 @@ const LANGUAGE_HINTS = [
43
43
  text:
44
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
45
  },
46
+ {
47
+ id: "git",
48
+ pattern: /\b(git|commit|push|pull|merge|rebase|branch|remote|status|diff)\b/i,
49
+ text:
50
+ "Git: always run git status --short and git diff --stat before commit/push. Commit only requested changes, use a clear message, run git fetch before push when remote state matters, prefer git pull --ff-only, and stop to ask if there are conflicts, unrelated dirty files, divergent branches, or ambiguous merge choices.",
51
+ },
46
52
  {
47
53
  id: "r-stats",
48
54
  pattern: /\b(rstats|r language|cmdstanr|stan|renv|tidyverse|shiny)\b/i,
@@ -77,6 +83,7 @@ export function engineeringGuidanceForTask(goal = "", taskProfile = "auto") {
77
83
  "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
84
  "Keep CLI and web behavior equivalent: use the same workspace, sessions, profiles, file tools, shell policy, Docker mounts, and canvas artifacts.",
79
85
  "For large repositories, preserve context by reading fewer but more relevant files; prefer deterministic tools and diffs over long model memory.",
86
+ "Build a compact context pack before major edits: project instructions, manifests/scripts, git status/diff, relevant symbols/search hits, target files, and the narrowest checks. Do not paste whole trees or huge files into model context.",
80
87
  "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
88
  ];
82
89
 
@@ -150,6 +150,21 @@ function compactLine(value = "", limit = 96) {
150
150
  return text.length <= limit ? text : `${text.slice(0, Math.max(limit - 1, 1))}…`;
151
151
  }
152
152
 
153
+ function wrapTextLine(value = "", width = 72) {
154
+ const text = stripAnsi(String(value || ""));
155
+ if (text.length <= width) return [text];
156
+ const chunks = [];
157
+ let remaining = text;
158
+ while (remaining.length > width) {
159
+ let splitAt = remaining.lastIndexOf(" ", width);
160
+ if (splitAt < Math.floor(width * 0.45)) splitAt = width;
161
+ chunks.push(remaining.slice(0, splitAt).trimEnd());
162
+ remaining = remaining.slice(splitAt).trimStart();
163
+ }
164
+ if (remaining) chunks.push(remaining);
165
+ return chunks;
166
+ }
167
+
153
168
  export function stripMarkdown(text) {
154
169
  const lines = String(text || "").split(/\r?\n/);
155
170
  let inFence = false;
@@ -320,6 +335,22 @@ function printAgentMessage(text) {
320
335
  for (const line of lines) outputLine(`${responsePrefix()}${line}`);
321
336
  }
322
337
 
338
+ function printPreviewBlock(role, text, { time = "", bg = ansi.systemBg, maxLines = 5 } = {}) {
339
+ const header = [label(role, bg).trimEnd(), time ? color(time, ansi.dim) : ""].filter(Boolean).join(" ");
340
+ outputLine(header);
341
+ const width = Math.max(terminalWidth() - 8, 38);
342
+ const rendered = stripMarkdown(text)
343
+ .split(/\r?\n/)
344
+ .map((line) => line.trimEnd())
345
+ .filter((line, index, all) => line.trim() || (index > 0 && index < all.length - 1));
346
+ const wrapped = rendered.flatMap((line) => wrapTextLine(line || " ", width)).slice(0, maxLines);
347
+ const truncated = rendered.flatMap((line) => wrapTextLine(line || " ", width)).length > maxLines;
348
+ for (const [index, line] of (wrapped.length ? wrapped : ["(empty)"]).entries()) {
349
+ const suffix = truncated && index === wrapped.length - 1 ? " …" : "";
350
+ outputLine(`${color(" | ", bg)} ${line}${suffix}`);
351
+ }
352
+ }
353
+
323
354
  function printSystemLine(text) {
324
355
  if (!String(text || "").trim()) {
325
356
  outputLine("");
@@ -328,6 +359,50 @@ function printSystemLine(text) {
328
359
  outputLine(`${label("state", ansi.systemBg)} ${color(text, ansi.dim)}`);
329
360
  }
330
361
 
362
+ function outputStats(value = "") {
363
+ const text = String(value || "");
364
+ return {
365
+ bytes: Buffer.byteLength(text, "utf8"),
366
+ lines: text ? text.split(/\r?\n/).length : 0,
367
+ };
368
+ }
369
+
370
+ function outputPreview(value = "", maxLines = 12) {
371
+ const lines = String(value || "")
372
+ .split(/\r?\n/)
373
+ .filter((line, index, all) => line.trim() || index < all.length - 1);
374
+ const shown = lines.slice(0, maxLines);
375
+ const hidden = Math.max(lines.length - shown.length, 0);
376
+ return { shown, hidden, total: lines.length };
377
+ }
378
+
379
+ function printCommandOutputLog(data = {}) {
380
+ const command = String(data.command || "").trim();
381
+ const stdout = String(data.stdout || "");
382
+ const stderr = String(data.stderr || "");
383
+ const isGit = /^git\b/.test(command);
384
+ const failed = Boolean(data.error || stderr || data.blocked);
385
+ if (!isGit && !failed) return;
386
+
387
+ const stdoutStats = outputStats(stdout);
388
+ const stderrStats = outputStats(stderr);
389
+ const policy = data.commandPolicy?.category ? ` ${data.commandPolicy.category}` : "";
390
+ outputLine(
391
+ `${label("shell", ansi.systemBg)} ${compactLine(command || "(command)", 86)}${policy} stdout=${stdoutStats.lines} stderr=${stderrStats.lines}`
392
+ );
393
+
394
+ for (const [name, value] of [
395
+ ["stdout", stdout],
396
+ ["stderr", stderr],
397
+ ]) {
398
+ if (!value) continue;
399
+ const preview = outputPreview(value, 12);
400
+ outputLine(`${color(" | ", ansi.systemBg)} ${name}`);
401
+ for (const line of preview.shown) outputLine(`${color(" | ", ansi.systemBg)} ${line}`);
402
+ if (preview.hidden > 0) outputLine(`${color(" | ", ansi.systemBg)} ... ${preview.hidden} more line(s) folded`);
403
+ }
404
+ }
405
+
331
406
  function printHeading(text) {
332
407
  outputLine(color(stripMarkdown(text), ansi.bold, ansi.cyan));
333
408
  }
@@ -393,7 +468,7 @@ function printHelp() {
393
468
  " /sessions List recent sessions in this project.",
394
469
  " /profile <name> Set task profile, e.g. code, website, latex, maintenance.",
395
470
  " /web-search on|off Enable or disable the web_search tool.",
396
- " /scouts on|off|<1-4> Enable parallel DeepSeek scouts and set scout count.",
471
+ " /scouts on|off|<1-8> Enable parallel DeepSeek scouts and set scout count.",
397
472
  " /routing <mode> Set routing: smart, fast, complex, manual.",
398
473
  " /provider <name> Set provider: deepseek, openai, mock.",
399
474
  " /model <name> Set an explicit model, or /model auto.",
@@ -1232,8 +1307,8 @@ function printHistoryEntry(entry) {
1232
1307
  const role = entry.role === "assistant" ? "aginti" : entry.role === "user" ? "user" : String(entry.role || "note");
1233
1308
  const bg = role === "aginti" ? ansi.agentBg : role === "user" ? ansi.userBg : ansi.systemBg;
1234
1309
  const time = entry.at ? new Date(entry.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
1235
- const suffix = time ? ` ${color(time, ansi.dim)}` : "";
1236
- outputLine(`${label(role, bg)} ${color("|", bg)} ${compactHistoryText(entry.content)}${suffix}`);
1310
+ const maxLines = role === "aginti" ? 4 : 3;
1311
+ printPreviewBlock(role, compactHistoryText(entry.content, 520), { time, bg, maxLines });
1237
1312
  }
1238
1313
 
1239
1314
  async function printResumeHistory(state, { limit = 8 } = {}) {
@@ -1521,7 +1596,7 @@ async function handleCommand(line, state, packageDir) {
1521
1596
  } else {
1522
1597
  state.allowParallelScouts = true;
1523
1598
  const count = Number(value);
1524
- if (Number.isFinite(count) && count > 0) state.parallelScoutCount = Math.min(Math.max(count, 1), 4);
1599
+ if (Number.isFinite(count) && count > 0) state.parallelScoutCount = Math.min(Math.max(count, 1), 8);
1525
1600
  }
1526
1601
  printSystemLine(`parallelScouts=${state.allowParallelScouts ? "on" : "off"} count=${state.parallelScoutCount}`);
1527
1602
  return true;
@@ -1673,6 +1748,9 @@ async function runPrompt(prompt, state, packageDir) {
1673
1748
  printSystemLine(text);
1674
1749
  }
1675
1750
  },
1751
+ onLog: (message, data = {}) => {
1752
+ if (message === "command.output") printCommandOutputLog(data);
1753
+ },
1676
1754
  onEvent: (type, data = {}) => {
1677
1755
  if (type === "plan.created") {
1678
1756
  printStatusEvent(state, "planned");
@@ -24,6 +24,26 @@ const SCOUTS = [
24
24
  prompt:
25
25
  "If current external information may matter, suggest exact web_search queries and source types. Otherwise say no web search needed.",
26
26
  },
27
+ {
28
+ name: "cartographer",
29
+ prompt:
30
+ "Design a compact context map: key instructions, manifests, entry points, tests, symbols, and files the executor should read. Avoid dumping a full tree.",
31
+ },
32
+ {
33
+ name: "tester",
34
+ prompt:
35
+ "Identify the narrowest checks to run first, then broader checks. Include likely setup blockers and how to validate without wasting steps.",
36
+ },
37
+ {
38
+ name: "git-operator",
39
+ prompt:
40
+ "If git is relevant, outline the safe git sequence: status/diff first, commit boundaries, fetch/pull --ff-only, push, and stop conditions.",
41
+ },
42
+ {
43
+ name: "integrator",
44
+ prompt:
45
+ "Look across workstreams for conflicts, shared files, ordering constraints, and what each scout might be missing. Keep it execution-focused.",
46
+ },
27
47
  ];
28
48
 
29
49
  function shouldUseComplexScouts(config, state) {
@@ -80,6 +100,30 @@ function scoutMessages(config, state, scout) {
80
100
  ];
81
101
  }
82
102
 
103
+ async function synthesizeScouts(client, config, model, scouts) {
104
+ const usable = scouts.filter((scout) => scout.content);
105
+ if (usable.length < 2) return "";
106
+ const response = await client.chat.completions.create(
107
+ {
108
+ model,
109
+ temperature: 0,
110
+ messages: [
111
+ {
112
+ role: "system",
113
+ content:
114
+ "You synthesize parallel coding-agent scout notes. Produce a compact execution brief under 220 words. Resolve conflicts, identify shared context, and list stop conditions. Do not claim work is done.",
115
+ },
116
+ {
117
+ role: "user",
118
+ content: usable.map((scout) => `## ${scout.name}\n${scout.content}`).join("\n\n"),
119
+ },
120
+ ],
121
+ },
122
+ config.abortSignal ? { signal: config.abortSignal } : undefined
123
+ );
124
+ return redactSensitiveText(response.choices[0]?.message?.content || "").trim();
125
+ }
126
+
83
127
  export async function runParallelScouts(client, config, state) {
84
128
  const count = Math.min(Math.max(Number(config.parallelScoutCount) || 3, 1), SCOUTS.length);
85
129
  const selected = SCOUTS.slice(0, count);
@@ -111,12 +155,21 @@ export async function runParallelScouts(client, config, state) {
111
155
  };
112
156
  });
113
157
  const completed = scouts.filter((scout) => scout.content).length;
158
+ const synthesis =
159
+ completed > 1
160
+ ? await synthesizeScouts(client, config, model, scouts).catch((error) =>
161
+ `Synthesis failed: ${redactSensitiveText(error instanceof Error ? error.message : String(error))}`
162
+ )
163
+ : "";
114
164
  const summary = [
115
165
  "Parallel scout notes. Treat these as advisory, not completed work.",
166
+ synthesis ? `\n## coordinator\n${synthesis}` : "",
116
167
  ...scouts.map((scout) =>
117
168
  scout.content ? `\n## ${scout.name}\n${scout.content}` : `\n## ${scout.name}\nScout failed: ${scout.error || "unknown error"}`
118
169
  ),
119
- ].join("\n");
170
+ ]
171
+ .filter(Boolean)
172
+ .join("\n");
120
173
 
121
174
  return {
122
175
  ok: completed > 0,
@@ -124,6 +177,7 @@ export async function runParallelScouts(client, config, state) {
124
177
  requested: selected.length,
125
178
  completed,
126
179
  scouts,
180
+ synthesis,
127
181
  summary,
128
182
  };
129
183
  }
@@ -509,6 +509,10 @@ async function inspectProject(config, args) {
509
509
  manifestFiles.some((item) => item.path.endsWith("Cargo.toml")) ? "cargo" : "",
510
510
  manifestFiles.some((item) => item.path.endsWith("go.mod")) ? "go" : "",
511
511
  ].filter(Boolean);
512
+ const gitPresent = await fs
513
+ .stat(path.join(target.absolutePath, ".git"))
514
+ .then(() => true)
515
+ .catch(() => false);
512
516
 
513
517
  const summary = {
514
518
  ok: true,
@@ -532,13 +536,21 @@ async function inspectProject(config, args) {
532
536
  languageCounts: sortCounts(languageCounts),
533
537
  packageManagers,
534
538
  packageScripts,
539
+ git: {
540
+ present: gitPresent,
541
+ recommendedCommands: gitPresent ? ["git status --short", "git diff --stat"] : [],
542
+ workflow: gitPresent
543
+ ? "Before commit/push: inspect git status and diff, avoid unrelated files, prefer git pull --ff-only, and stop on conflicts or divergence."
544
+ : "",
545
+ },
535
546
  recommendedReads: [],
536
547
  engineeringHints: [
537
548
  "Read AGINTI/AGENTS/README/manifests before editing.",
538
549
  "Use search_files to locate symbols and tests, then read exact files.",
539
550
  "Use apply_patch for source edits and run the smallest relevant check first.",
540
551
  "If a change spans modules, patch in small batches and verify after each batch.",
541
- ],
552
+ gitPresent ? "For git tasks, run status/diff first; commit only intended changes and stop on conflicts/divergence." : "",
553
+ ].filter(Boolean),
542
554
  };
543
555
  summary.recommendedReads = recommendedReads(summary);
544
556
  if (includeFiles) summary.files = files.slice(0, limit);