@lazyingart/agintiflow 0.12.1 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,7 +67,9 @@ aginti
67
67
  aginti chat
68
68
  ```
69
69
 
70
- Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, while Enter sends the message. Assistant responses render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, and tables. Esc or Ctrl+C stops the active run cleanly and prints the resume command.
70
+ Inside chat, type normal requests such as `write a small Python CLI app with tests`. The default is Docker workspace mode with approved package installs, so coding, plotting, and LaTeX tasks can set up project-local tools without touching the host. Use `/help` for commands, `/login` or `/auth` to paste a provider key, `/instructions` to inspect `AGINTI.md`, `/latex on` for PDF work, `/docker off` only when you intentionally want host mode, `/sessions` to list project runs, and `/resume latest` or `/resume <session-id>` to continue work. Type `/` then Tab for command completion. `Ctrl+J` inserts a new line in the colored input panel, Enter sends, arrow keys move through wrapped multiline input, and `Ctrl+A`/`Ctrl+E` jump to the current line start/end. Assistant responses render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, and tables. Esc or Ctrl+C stops the active run cleanly and prints the resume command.
71
+
72
+ `aginti init` creates `AGINTI.md` at the project root. This is the editable project-instruction file for both CLI and web runs, similar to `AGENTS.md` or project memory in other agents. Keep durable preferences, commands, and constraints there, but never secrets. You can edit it manually or ask in chat, for example: `update AGINTI.md to remember that this project uses pytest and npm run check`.
71
73
 
72
74
  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
75
 
@@ -11,7 +11,7 @@ Local agent references informed the design:
11
11
  - Claude/Claw-style safety: project-local status, container-first execution, read-only operations by default, and clear failure recovery.
12
12
  - Gemini/Qwen-style extensibility: capability discovery through profiles and tools rather than hardcoding one model behavior.
13
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.
14
+ - Claude/Codex-style context discipline: read project instructions, manifests, entry points, and failing tests before touching broad files.
15
15
 
16
16
  ## Skill vs Tool
17
17
 
@@ -31,7 +31,7 @@ The `inspect_project` function is a tool: it deterministically scans the workspa
31
31
  For complicated tasks, the agent should follow this loop:
32
32
 
33
33
  1. `inspect_project` to map the repository.
34
- 2. `read_file` on `AGENTS.md`, `README.md`, and manifests.
34
+ 2. `read_file` on `AGINTI.md`, `AGENTS.md`, `README.md`, and manifests.
35
35
  3. `search_files` for symbols, tests, errors, routes, or config names.
36
36
  4. `read_file` only on the files needed for the change.
37
37
  5. `apply_patch` in small coherent batches.
@@ -54,6 +54,8 @@ Smart routing sends this profile to DeepSeek v4 pro even when the user prompt is
54
54
 
55
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
56
 
57
+ Profiles are skill bias, not restrictions. `auto` is the broad general agent; `code`, `latex`, `website`, `maintenance`, and other profiles add stronger habits for that task type while still allowing the agent to use files, shell, browser, web search, canvas, and sandbox tools when they help.
58
+
57
59
  Examples:
58
60
 
59
61
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
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",
@@ -29,9 +29,12 @@ async function runCli(args) {
29
29
 
30
30
  try {
31
31
  await runCli(["init"]);
32
+ const agintiMd = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
33
+ assert(agintiMd.includes("Project instructions for AgInTiFlow agents."), "init did not create AGINTI.md");
32
34
  const capabilities = JSON.parse(await runCli(["capabilities", "--json"]));
33
35
  assert(capabilities.project.root === tempRoot, "capabilities did not use cwd as project root");
34
36
  assert(capabilities.project.commandCwd === tempRoot, "capabilities did not default commandCwd to project root");
37
+ assert(capabilities.project.instructionsPresent, "capabilities did not report AGINTI.md");
35
38
  assert(capabilities.project.sharedSessionFolder, "capabilities did not report shared session folder");
36
39
  assert(capabilities.keys?.mock === true, "capabilities did not report mock availability");
37
40
  assert(
@@ -69,13 +72,14 @@ try {
69
72
 
70
73
  const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
71
74
  assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
75
+ assert(doctor.project.instructionsPresent, "doctor --capabilities did not report AGINTI.md");
72
76
 
73
77
  console.log(
74
78
  JSON.stringify(
75
79
  {
76
80
  ok: true,
77
81
  projectRoot: tempRoot,
78
- checks: ["capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy"],
82
+ checks: ["aginti-md-init", "capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy"],
79
83
  },
80
84
  null,
81
85
  2
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
- import { stripMarkdown } from "../src/interactive-cli.js";
7
+ import { buildPromptLayout, stripMarkdown } from "../src/interactive-cli.js";
8
8
 
9
9
  const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
10
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-cli-chat-"));
@@ -70,6 +70,36 @@ try {
70
70
  throw new Error("terminal markdown renderer dropped table content");
71
71
  }
72
72
 
73
+ const promptLayout = buildPromptLayout(`${"x".repeat(180)}\nsecond line`, 95, 80, 24);
74
+ const visibleLengths = promptLayout.renderedRows.map((line) =>
75
+ line.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").length
76
+ );
77
+ if (promptLayout.rows.length < 4 || Math.max(...visibleLengths) > 79) {
78
+ throw new Error("terminal prompt layout did not wrap long multiline input safely");
79
+ }
80
+ if (promptLayout.cursorRow < 0 || promptLayout.cursorColumn < 0) {
81
+ throw new Error("terminal prompt layout returned an invalid cursor location");
82
+ }
83
+ const hugePromptLayout = buildPromptLayout(Array.from({ length: 30 }, (_unused, index) => `line ${index + 1}`).join("\n"), 120, 80, 20);
84
+ if (hugePromptLayout.renderedRows.length > 12 || !hugePromptLayout.renderedRows.some((line) => line.includes("earlier input row"))) {
85
+ throw new Error("terminal prompt layout did not bound redraw size for large prompts");
86
+ }
87
+
88
+ await runCli(["init"], "");
89
+ const instructions = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
90
+ if (!instructions.includes("Project instructions for AgInTiFlow agents.")) {
91
+ throw new Error("init did not create AGINTI.md");
92
+ }
93
+ const instructionsResult = await runChat("/instructions\n/exit\n");
94
+ if (!instructionsResult.stdout.includes("AGINTI.md") || !instructionsResult.stdout.includes("Project instructions")) {
95
+ throw new Error("interactive /instructions did not show AGINTI.md");
96
+ }
97
+ await runChat("remember that this project prefers pytest smoke tests in AGINTI.md\n/exit\n");
98
+ const updatedInstructions = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
99
+ if (!updatedInstructions.includes("pytest smoke tests")) {
100
+ throw new Error("chat did not update AGINTI.md project instructions in mock mode");
101
+ }
102
+
73
103
  const result = await runChat("Create notes/interactive.md with a short CLI chat smoke message\n/exit\n");
74
104
  const written = await fs.readFile(path.join(tempRoot, "notes/interactive.md"), "utf8");
75
105
  if (!written.includes("Created by AgInTiFlow mock mode.")) {
@@ -92,7 +122,7 @@ try {
92
122
  {
93
123
  ok: true,
94
124
  projectRoot: tempRoot,
95
- checks: ["markdown-render", "interactive-chat", "mock-file-write", "run-status", "resume-latest"],
125
+ checks: ["markdown-render", "prompt-layout", "aginti-md", "instructions-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest"],
96
126
  },
97
127
  null,
98
128
  2
@@ -21,6 +21,7 @@ import { generateImage, listAuxiliarySkills } from "./auxiliary-tools.js";
21
21
  import { engineeringGuidanceForTask } from "./engineering-guidance.js";
22
22
  import { searchWeb } from "./web-search.js";
23
23
  import { runParallelScouts, shouldRunParallelScouts } from "./parallel-scouts.js";
24
+ import { readProjectInstructions } from "./project.js";
24
25
 
25
26
  const exec = promisify(execCallback);
26
27
  const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
@@ -41,6 +42,20 @@ function throwIfAborted(config) {
41
42
  }
42
43
  }
43
44
 
45
+ function formatProjectInstructions(instructions) {
46
+ if (!instructions?.exists) {
47
+ return "Project instructions file: AGINTI.md is not present. If the user wants durable project preferences, create or update AGINTI.md in the workspace.";
48
+ }
49
+ const suffix = instructions.truncated ? "\n[AGINTI.md was truncated for context. Read the file if more detail is needed.]" : "";
50
+ return [
51
+ `Project instructions from AGINTI.md (${instructions.path}):`,
52
+ redactSensitiveText(instructions.content).trim() || "(empty)",
53
+ suffix,
54
+ ]
55
+ .filter(Boolean)
56
+ .join("\n");
57
+ }
58
+
44
59
  function isAbortError(error, config = {}) {
45
60
  return Boolean(
46
61
  config.abortSignal?.aborted ||
@@ -233,10 +248,12 @@ export function repairModelMessageHistory(state, config = {}) {
233
248
  };
234
249
  }
235
250
 
236
- function createInitialState(config, sessionId) {
251
+ async function createInitialState(config, sessionId) {
237
252
  const now = new Date().toISOString();
238
253
  const taskProfile = getTaskProfile(config.taskProfile);
239
254
  const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
255
+ const projectInstructions = await readProjectInstructions(config.baseDir || config.commandCwd || process.cwd());
256
+ const projectInstructionContext = formatProjectInstructions(projectInstructions);
240
257
  return {
241
258
  sessionId,
242
259
  createdAt: now,
@@ -249,6 +266,12 @@ function createInitialState(config, sessionId) {
249
266
  stepsCompleted: 0,
250
267
  meta: {
251
268
  lastUrl: "",
269
+ projectInstructions: {
270
+ path: projectInstructions.path,
271
+ exists: projectInstructions.exists,
272
+ truncated: projectInstructions.truncated,
273
+ loadedAt: now,
274
+ },
252
275
  },
253
276
  chat: [
254
277
  {
@@ -270,13 +293,15 @@ function createInitialState(config, sessionId) {
270
293
  "Prefer short, deliberate actions over guessing.",
271
294
  "Never navigate outside the allowed domains when an allowlist exists.",
272
295
  "Avoid destructive actions, purchases, account changes, and sensitive workflows.",
296
+ projectInstructionContext,
297
+ "Treat AGINTI.md as durable project memory and operating instructions for this project. The user can edit it manually or ask you in chat to update it; use workspace file tools for that and never store secrets there.",
273
298
  config.allowShellTool
274
299
  ? config.useDockerSandbox
275
300
  ? `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.`
276
301
  : "A host shell command tool is available under the configured trust policy."
277
302
  : "No shell command tool is available.",
278
303
  config.allowFileTools
279
- ? `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.`
304
+ ? `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.`
280
305
  : "No workspace file tools are available.",
281
306
  config.allowWrapperTools
282
307
  ? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
@@ -321,8 +346,9 @@ function createInitialState(config, sessionId) {
321
346
  : `Shell working directory: ${config.commandCwd}`
322
347
  : "",
323
348
  config.allowFileTools
324
- ? `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.`
349
+ ? `Workspace file tools enabled in: ${config.commandCwd}. Use inspect_project first for large/unfamiliar codebases. 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. Local preview tools available: open_workspace_file and preview_workspace.`
325
350
  : "",
351
+ projectInstructions.exists ? "AGINTI.md project instructions are loaded into system context for this run." : "AGINTI.md is not present unless you create it.",
326
352
  config.allowWrapperTools
327
353
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
328
354
  : "",
@@ -420,11 +446,19 @@ function appendChatEntry(state, role, content) {
420
446
  });
421
447
  }
422
448
 
423
- function applyContinuationPrompt(state, config, observers) {
449
+ async function applyContinuationPrompt(state, config, observers) {
424
450
  if (!config.resume || !config.goal) return;
425
451
 
426
452
  const taskProfile = getTaskProfile(config.taskProfile);
427
453
  const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
454
+ const projectInstructions = await readProjectInstructions(config.baseDir || config.commandCwd || process.cwd());
455
+ state.meta = state.meta || {};
456
+ state.meta.projectInstructions = {
457
+ path: projectInstructions.path,
458
+ exists: projectInstructions.exists,
459
+ truncated: projectInstructions.truncated,
460
+ loadedAt: new Date().toISOString(),
461
+ };
428
462
  ensureChatState(state);
429
463
  state.goal = config.goal;
430
464
  state.provider = config.provider;
@@ -445,13 +479,15 @@ function applyContinuationPrompt(state, config, observers) {
445
479
  : `Shell working directory: ${config.commandCwd}`
446
480
  : "",
447
481
  config.allowFileTools
448
- ? `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.`
482
+ ? `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.`
449
483
  : "",
450
484
  config.allowWrapperTools
451
485
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
452
486
  : "",
453
487
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
454
488
  engineeringGuidance,
489
+ formatProjectInstructions(projectInstructions),
490
+ "AGINTI.md is editable project memory. If the user asks to remember a preference or update instructions, patch AGINTI.md rather than hiding that preference in session-only chat.",
455
491
  ]
456
492
  .filter(Boolean)
457
493
  .join("\n"),
@@ -1091,7 +1127,7 @@ export async function runAgent(config) {
1091
1127
  }
1092
1128
 
1093
1129
  if (!state) {
1094
- state = createInitialState(config, sessionId);
1130
+ state = await createInitialState(config, sessionId);
1095
1131
  await store.appendEvent("session.created", {
1096
1132
  sessionId,
1097
1133
  provider: config.provider,
@@ -1103,7 +1139,7 @@ export async function runAgent(config) {
1103
1139
  await store.saveState(state);
1104
1140
  } else {
1105
1141
  await store.appendEvent("session.resumed", { sessionId });
1106
- applyContinuationPrompt(state, config, observers);
1142
+ await applyContinuationPrompt(state, config, observers);
1107
1143
  await store.saveState(state);
1108
1144
  }
1109
1145
 
@@ -1263,6 +1299,7 @@ export async function runAgent(config) {
1263
1299
  commandCwd: config.commandCwd,
1264
1300
  plan: state.plan || "",
1265
1301
  suggestedStartUrl: config.startUrl || "",
1302
+ projectInstructions: state.meta.projectInstructions || null,
1266
1303
  canvasArtifactsAvailable: true,
1267
1304
  taskProfile: getTaskProfile(config.taskProfile),
1268
1305
  })}`,
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { classifyCommand, evaluateCommandPolicy } from "./command-policy.js";
5
5
  import { getDockerSandboxStatus } from "./docker-sandbox.js";
6
6
  import { getModelPresets } from "./model-routing.js";
7
- import { listProjectSessions, projectPaths, providerKeyStatus } from "./project.js";
7
+ import { listProjectSessions, projectPaths, providerKeyStatus, readProjectInstructions } from "./project.js";
8
8
  import { listTaskProfiles } from "./task-profiles.js";
9
9
  import { listAgentWrappers } from "./tool-wrappers.js";
10
10
  import { listAuxiliarySkills } from "./auxiliary-tools.js";
@@ -110,7 +110,7 @@ function trustedDockerPolicyChecks(config) {
110
110
  export async function buildCapabilityReport(projectRoot, packageVersion, config) {
111
111
  const paths = projectPaths(projectRoot);
112
112
  const keyStatus = providerKeyStatus(projectRoot);
113
- const [node, npm, python, conda, r, pdflatex, latexmk, dockerStatus, sessions] = await Promise.all([
113
+ const [node, npm, python, conda, r, pdflatex, latexmk, dockerStatus, sessions, instructions] = await Promise.all([
114
114
  commandAvailable("node", ["--version"]),
115
115
  commandAvailable("npm", ["--version"]),
116
116
  commandAvailable("python3", ["--version"]),
@@ -120,6 +120,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
120
120
  commandAvailable("latexmk", ["--version"]),
121
121
  getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
122
122
  listProjectSessions(projectRoot, 12),
123
+ readProjectInstructions(projectRoot, { maxBytes: 1 }),
123
124
  ]);
124
125
 
125
126
  const npmPrefixPolicy = evaluateCommandPolicy("npm --prefix round9-node-app test", config);
@@ -167,6 +168,8 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
167
168
  project: {
168
169
  root: paths.root,
169
170
  commandCwd: path.resolve(config.commandCwd),
171
+ instructionsPath: paths.agintiInstructionsPath,
172
+ instructionsPresent: instructions.exists,
170
173
  sessionsDir: paths.sessionsDir,
171
174
  sessionDbPath: paths.sessionDbPath,
172
175
  sharedSessionFolder: path.resolve(config.sessionsDir) === path.resolve(paths.sessionsDir),
@@ -230,6 +233,7 @@ export function printCapabilityReport(report) {
230
233
  console.log(`AgInTiFlow capabilities ${report.package.version}`);
231
234
  console.log(`project=${report.project.root}`);
232
235
  console.log(`cwd=${report.project.commandCwd}`);
236
+ console.log(`instructions=${report.project.instructionsPath} present=${report.project.instructionsPresent}`);
233
237
  console.log(`sessions=${report.project.sessionsDir}`);
234
238
  console.log(`sessionDb=${report.project.sessionDbPath}`);
235
239
  console.log(`sharedSessions=${report.project.sharedSessionFolder}`);
package/src/cli.js CHANGED
@@ -322,6 +322,7 @@ function printProfiles() {
322
322
 
323
323
  function printInitResult(result) {
324
324
  console.log(`AgInTiFlow project initialized: ${result.projectRoot}`);
325
+ console.log(`instructions=${result.instructionsPath}`);
325
326
  console.log(`control=${result.controlDir}`);
326
327
  console.log(`sessions=${result.sessionsDir}`);
327
328
  console.log(`created=${result.created.length} updated=${result.updated.length} skipped=${result.skipped.length}`);
@@ -331,6 +332,7 @@ function printDoctorReport(report) {
331
332
  console.log(`AgInTiFlow ${report.package.version} (npm latest: ${report.package.npmLatest})`);
332
333
  console.log(`node=${report.node.version} ok=${report.node.ok}`);
333
334
  console.log(`project=${report.project.root}`);
335
+ console.log(`instructions=${report.project.instructionsPath} present=${report.project.instructionsPresent}`);
334
336
  console.log(`sessions=${report.project.sessionsDir}`);
335
337
  console.log(`sessionDb=${report.project.sessionDbPath}`);
336
338
  console.log(
@@ -3,7 +3,7 @@ import { emitKeypressEvents } from "node:readline";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
4
  import { runAgent } from "./agent-runner.js";
5
5
  import { loadConfig } from "./config.js";
6
- import { initProject, listProjectSessions, providerKeyStatus, setProviderKey } from "./project.js";
6
+ import { initProject, listProjectSessions, providerKeyStatus, readProjectInstructions, setProviderKey } from "./project.js";
7
7
  import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
8
8
  import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
9
9
  import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
@@ -34,6 +34,8 @@ const SLASH_COMMANDS = [
34
34
  "/status",
35
35
  "/login",
36
36
  "/auth",
37
+ "/instructions",
38
+ "/memory",
37
39
  "/auxilliary",
38
40
  "/auxiliary",
39
41
  "/new",
@@ -53,6 +55,7 @@ const SLASH_COMMANDS = [
53
55
  "/web",
54
56
  "/exit",
55
57
  ];
58
+ const promptHistory = [];
56
59
 
57
60
  function color(value, ...codes) {
58
61
  if (!useColor || codes.length === 0) return String(value);
@@ -71,6 +74,18 @@ function terminalWidth() {
71
74
  return Math.max(Number(output.columns) || 80, 40);
72
75
  }
73
76
 
77
+ function terminalHeight() {
78
+ return Math.max(Number(output.rows) || 24, 10);
79
+ }
80
+
81
+ function editorWidth(width = terminalWidth()) {
82
+ return Math.max(Number(width) - 1, 39);
83
+ }
84
+
85
+ function promptViewportRows(height = terminalHeight()) {
86
+ return Math.max(Math.min(Math.floor(Number(height) * 0.42), 10), 4);
87
+ }
88
+
74
89
  function visualLength(value) {
75
90
  return stripAnsi(value).length;
76
91
  }
@@ -80,10 +95,11 @@ function padVisible(value, width) {
80
95
  return `${value}${" ".repeat(padding)}`;
81
96
  }
82
97
 
83
- function panelLine(content = "", bgCode = ansi.systemBg) {
84
- const width = terminalWidth();
85
- if (!useColor) return padVisible(content, width);
86
- const padded = padVisible(content, width).replaceAll(ansi.reset, `${ansi.reset}${bgCode}`);
98
+ function panelLine(content = "", bgCode = ansi.systemBg, width = editorWidth()) {
99
+ const raw = String(content || "");
100
+ const safeContent = visualLength(raw) > width ? stripAnsi(raw).slice(0, width) : raw;
101
+ if (!useColor) return padVisible(safeContent, width);
102
+ const padded = padVisible(safeContent, width).replaceAll(ansi.reset, `${ansi.reset}${bgCode}`);
87
103
  return `${bgCode}${padded}${ansi.reset}`;
88
104
  }
89
105
 
@@ -113,6 +129,10 @@ function commandSuggestions(line = "") {
113
129
  return SLASH_COMMANDS.filter((command) => command.startsWith(trimmed)).slice(0, 8);
114
130
  }
115
131
 
132
+ function clamp(value, min, max) {
133
+ return Math.min(Math.max(value, min), max);
134
+ }
135
+
116
136
  export function stripMarkdown(text) {
117
137
  const lines = String(text || "").split(/\r?\n/);
118
138
  let inFence = false;
@@ -319,6 +339,8 @@ function printHelp() {
319
339
  " /status Show active route, workspace, sandbox, and session.",
320
340
  " /login [deepseek|openai|grsai] Paste and save a project-local API key.",
321
341
  " /auth [deepseek|openai|grsai] Alias for /login.",
342
+ " /instructions Show AGINTI.md project instructions status.",
343
+ " /memory Alias for /instructions.",
322
344
  " /auxilliary [status|grsai|on|off|image]",
323
345
  " Manage optional auxiliary skills, including GRS AI image generation.",
324
346
  " /new Start a fresh session on the next message.",
@@ -344,29 +366,236 @@ function printHelp() {
344
366
  );
345
367
  }
346
368
 
347
- function renderPromptBuffer(buffer, previousLineCount = 0) {
348
- for (let index = 0; index < previousLineCount; index += 1) {
349
- output.write(`\r${ansi.clearLine}`);
350
- if (index < previousLineCount - 1) output.write("\x1b[1A");
369
+ function logicalLinesWithOffsets(buffer = "") {
370
+ const lines = String(buffer).split("\n");
371
+ let offset = 0;
372
+ return lines.map((line, index) => {
373
+ const start = offset;
374
+ const end = start + line.length;
375
+ offset = end + 1;
376
+ return {
377
+ text: line,
378
+ start,
379
+ end,
380
+ hasNewline: index < lines.length - 1,
381
+ };
382
+ });
383
+ }
384
+
385
+ function promptVisibleWindow(rows, cursorRow, height = terminalHeight()) {
386
+ const maxRows = promptViewportRows(height);
387
+ if (rows.length <= maxRows) {
388
+ return { start: 0, end: rows.length, topHidden: 0, bottomHidden: 0 };
351
389
  }
390
+ const half = Math.floor(maxRows / 2);
391
+ const start = clamp(cursorRow - half, 0, rows.length - maxRows);
392
+ const end = start + maxRows;
393
+ return {
394
+ start,
395
+ end,
396
+ topHidden: start,
397
+ bottomHidden: rows.length - end,
398
+ };
399
+ }
352
400
 
353
- const lines = String(buffer || "").split("\n");
354
- const suggestions = commandSuggestions(lines[0] || "");
355
- const rendered = [];
401
+ export function buildPromptLayout(buffer = "", cursor = 0, width = terminalWidth(), height = terminalHeight()) {
402
+ const safeBuffer = String(buffer || "");
403
+ const safeCursor = clamp(Number(cursor) || 0, 0, safeBuffer.length);
404
+ const lineWidth = editorWidth(width);
356
405
  const firstPrefix = " user ";
357
406
  const nextPrefix = " ... ";
358
- const emptyHint = color("type a request, /help, Enter to send, Ctrl+J for newline", ansi.faint);
359
- const cursor = color("▌", ansi.bold);
360
- rendered.push(panelLine(`${firstPrefix}${lines[0] || emptyHint}${lines.length === 1 ? cursor : ""}`, ansi.userBg));
361
- for (const [index, line] of lines.slice(1).entries()) {
362
- const isLast = index === lines.length - 2;
363
- rendered.push(panelLine(`${nextPrefix}${line}${isLast ? cursor : ""}`, ansi.userBg));
407
+ const firstInnerWidth = Math.max(lineWidth - firstPrefix.length, 8);
408
+ const nextInnerWidth = Math.max(lineWidth - nextPrefix.length, 8);
409
+ const rows = [];
410
+
411
+ for (const [lineIndex, line] of logicalLinesWithOffsets(safeBuffer).entries()) {
412
+ let localOffset = 0;
413
+ const text = line.text;
414
+ if (!text) {
415
+ rows.push({
416
+ prefix: lineIndex === 0 ? firstPrefix : nextPrefix,
417
+ text: "",
418
+ start: line.start,
419
+ end: line.start,
420
+ innerWidth: lineIndex === 0 ? firstInnerWidth : nextInnerWidth,
421
+ lineStart: line.start,
422
+ lineEnd: line.end,
423
+ hasNewline: line.hasNewline,
424
+ });
425
+ continue;
426
+ }
427
+
428
+ while (localOffset < text.length) {
429
+ const prefix = lineIndex === 0 && localOffset === 0 ? firstPrefix : nextPrefix;
430
+ const innerWidth = prefix === firstPrefix ? firstInnerWidth : nextInnerWidth;
431
+ const chunk = text.slice(localOffset, localOffset + innerWidth);
432
+ rows.push({
433
+ prefix,
434
+ text: chunk,
435
+ start: line.start + localOffset,
436
+ end: line.start + localOffset + chunk.length,
437
+ innerWidth,
438
+ lineStart: line.start,
439
+ lineEnd: line.end,
440
+ hasNewline: line.hasNewline,
441
+ });
442
+ localOffset += chunk.length;
443
+ }
444
+ }
445
+
446
+ if (rows.length === 0) {
447
+ rows.push({
448
+ prefix: firstPrefix,
449
+ text: "",
450
+ start: 0,
451
+ end: 0,
452
+ innerWidth: firstInnerWidth,
453
+ lineStart: 0,
454
+ lineEnd: 0,
455
+ hasNewline: false,
456
+ });
457
+ }
458
+
459
+ const last = rows[rows.length - 1];
460
+ if (safeCursor === safeBuffer.length && last.end === safeCursor && last.text.length >= last.innerWidth) {
461
+ rows.push({
462
+ prefix: nextPrefix,
463
+ text: "",
464
+ start: safeCursor,
465
+ end: safeCursor,
466
+ innerWidth: nextInnerWidth,
467
+ lineStart: safeCursor,
468
+ lineEnd: safeCursor,
469
+ hasNewline: false,
470
+ });
471
+ }
472
+
473
+ let cursorRow = rows.length - 1;
474
+ let cursorColumn = rows[cursorRow].prefix.length;
475
+ for (let index = 0; index < rows.length; index += 1) {
476
+ const row = rows[index];
477
+ const next = rows[index + 1];
478
+ if (safeCursor < row.end) {
479
+ cursorRow = index;
480
+ cursorColumn = row.prefix.length + safeCursor - row.start;
481
+ break;
482
+ }
483
+ if (safeCursor === row.end) {
484
+ if (next && next.start === safeCursor && row.text.length >= row.innerWidth) continue;
485
+ cursorRow = index;
486
+ cursorColumn = row.prefix.length + safeCursor - row.start;
487
+ break;
488
+ }
489
+ }
490
+
491
+ const suggestions = commandSuggestions(safeBuffer.split("\n")[0] || "");
492
+ const emptyHint = "type a request, /help, Enter to send, Ctrl+J for newline";
493
+ const visible = promptVisibleWindow(rows, cursorRow, height);
494
+ const renderedRows = [];
495
+ let renderedCursorRow = cursorRow - visible.start;
496
+
497
+ if (visible.topHidden > 0) {
498
+ renderedRows.push(panelLine(` ... ${visible.topHidden} earlier input row${visible.topHidden === 1 ? "" : "s"}`, ansi.systemBg, lineWidth));
499
+ renderedCursorRow += 1;
500
+ }
501
+
502
+ for (const row of rows.slice(visible.start, visible.end)) {
503
+ const content = safeBuffer ? `${row.prefix}${row.text}` : `${row.prefix}${emptyHint}`;
504
+ renderedRows.push(panelLine(content, ansi.userBg, lineWidth));
364
505
  }
506
+
507
+ if (visible.bottomHidden > 0) {
508
+ renderedRows.push(panelLine(` ... ${visible.bottomHidden} later input row${visible.bottomHidden === 1 ? "" : "s"}`, ansi.systemBg, lineWidth));
509
+ }
510
+
365
511
  if (suggestions.length > 0) {
366
- rendered.push(panelLine(` hint ${color(suggestions.join(" "), ansi.dim)}`, ansi.systemBg));
512
+ renderedRows.push(panelLine(` hint ${suggestions.join(" ")}`, ansi.systemBg, lineWidth));
513
+ }
514
+
515
+ return {
516
+ rows,
517
+ renderedRows,
518
+ cursorRow: renderedCursorRow,
519
+ absoluteCursorRow: cursorRow,
520
+ cursorColumn: clamp(cursorColumn, 0, editorWidth(width) - 1),
521
+ };
522
+ }
523
+
524
+ function cursorLocation(layout, cursor) {
525
+ for (let index = 0; index < layout.rows.length; index += 1) {
526
+ const row = layout.rows[index];
527
+ const next = layout.rows[index + 1];
528
+ if (cursor < row.end) return { rowIndex: index, column: cursor - row.start };
529
+ if (cursor === row.end) {
530
+ if (next && next.start === cursor && row.text.length >= row.innerWidth) continue;
531
+ return { rowIndex: index, column: cursor - row.start };
532
+ }
533
+ }
534
+ const rowIndex = Math.max(layout.rows.length - 1, 0);
535
+ const row = layout.rows[rowIndex];
536
+ return { rowIndex, column: Math.max(row.end - row.start, 0) };
537
+ }
538
+
539
+ function clearRenderedPrompt(previous) {
540
+ if (!previous.lineCount) return;
541
+ const below = previous.lineCount - 1 - previous.cursorRow;
542
+ if (below > 0) output.write(`\x1b[${below}B`);
543
+ output.write(`\r${ansi.clearLine}`);
544
+ for (let index = 1; index < previous.lineCount; index += 1) {
545
+ output.write(`\x1b[1A\r${ansi.clearLine}`);
367
546
  }
368
- output.write(rendered.join("\n"));
369
- return rendered.length;
547
+ }
548
+
549
+ function renderPromptBuffer(buffer, cursor, previous = { lineCount: 0, cursorRow: 0 }) {
550
+ output.write(ansi.cursorHide);
551
+ clearRenderedPrompt(previous);
552
+ const layout = buildPromptLayout(buffer, cursor);
553
+ output.write(layout.renderedRows.join("\n"));
554
+ const below = layout.renderedRows.length - 1 - layout.cursorRow;
555
+ if (below > 0) output.write(`\x1b[${below}A`);
556
+ output.write(`\r\x1b[${layout.cursorColumn + 1}G`);
557
+ output.write(ansi.cursorShow);
558
+ return {
559
+ lineCount: layout.renderedRows.length,
560
+ cursorRow: layout.cursorRow,
561
+ };
562
+ }
563
+
564
+ function moveToPromptBottom(rendered) {
565
+ const below = Math.max((rendered?.lineCount || 1) - 1 - (rendered?.cursorRow || 0), 0);
566
+ if (below > 0) output.write(`\x1b[${below}B`);
567
+ output.write("\r");
568
+ }
569
+
570
+ function lineBounds(buffer, cursor) {
571
+ const safeCursor = clamp(cursor, 0, buffer.length);
572
+ const start = buffer.lastIndexOf("\n", safeCursor - 1) + 1;
573
+ const nextNewline = buffer.indexOf("\n", safeCursor);
574
+ const end = nextNewline === -1 ? buffer.length : nextNewline;
575
+ return { start, end };
576
+ }
577
+
578
+ function insertAt(buffer, cursor, text) {
579
+ return {
580
+ buffer: `${buffer.slice(0, cursor)}${text}${buffer.slice(cursor)}`,
581
+ cursor: cursor + text.length,
582
+ };
583
+ }
584
+
585
+ function removeBefore(buffer, cursor) {
586
+ if (cursor <= 0) return { buffer, cursor };
587
+ return {
588
+ buffer: `${buffer.slice(0, cursor - 1)}${buffer.slice(cursor)}`,
589
+ cursor: cursor - 1,
590
+ };
591
+ }
592
+
593
+ function removeAt(buffer, cursor) {
594
+ if (cursor >= buffer.length) return { buffer, cursor };
595
+ return {
596
+ buffer: `${buffer.slice(0, cursor)}${buffer.slice(cursor + 1)}`,
597
+ cursor,
598
+ };
370
599
  }
371
600
 
372
601
  function createAbortError(message = "Aborted with Ctrl+C") {
@@ -381,34 +610,97 @@ function readTtyPrompt() {
381
610
  emitKeypressEvents(input);
382
611
  const wasRaw = Boolean(input.isRaw);
383
612
  let buffer = "";
384
- let renderedLines = 0;
613
+ let cursor = 0;
614
+ let rendered = { lineCount: 0, cursorRow: 0 };
615
+ let preferredColumn = null;
616
+ let historyIndex = promptHistory.length;
617
+ let draft = "";
618
+ let redrawHandle = null;
385
619
 
386
620
  const cleanup = () => {
621
+ if (redrawHandle) {
622
+ clearImmediate(redrawHandle);
623
+ redrawHandle = null;
624
+ }
387
625
  input.off("keypress", handler);
388
626
  if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
389
627
  input.pause();
390
628
  output.write(ansi.cursorShow);
391
629
  };
392
630
 
631
+ const renderNow = () => {
632
+ if (redrawHandle) {
633
+ clearImmediate(redrawHandle);
634
+ redrawHandle = null;
635
+ }
636
+ rendered = renderPromptBuffer(buffer, cursor, rendered);
637
+ };
638
+
393
639
  const redraw = () => {
394
- renderedLines = renderPromptBuffer(buffer, renderedLines);
640
+ if (redrawHandle) return;
641
+ redrawHandle = setImmediate(() => {
642
+ redrawHandle = null;
643
+ rendered = renderPromptBuffer(buffer, cursor, rendered);
644
+ });
395
645
  };
396
646
 
397
647
  const submit = () => {
648
+ renderNow();
649
+ moveToPromptBottom(rendered);
398
650
  cleanup();
399
651
  output.write("\n");
652
+ const saved = buffer.trim();
653
+ if (saved && promptHistory[promptHistory.length - 1] !== buffer) promptHistory.push(buffer);
400
654
  resolve(buffer);
401
655
  };
402
656
 
657
+ const setBuffer = (nextBuffer, nextCursor = nextBuffer.length) => {
658
+ buffer = nextBuffer;
659
+ cursor = clamp(nextCursor, 0, buffer.length);
660
+ preferredColumn = null;
661
+ redraw();
662
+ };
663
+
664
+ const moveVertical = (direction) => {
665
+ const layout = buildPromptLayout(buffer, cursor);
666
+ const location = cursorLocation(layout, cursor);
667
+ const targetRowIndex = location.rowIndex + direction;
668
+ if (targetRowIndex < 0) {
669
+ if (promptHistory.length === 0) return;
670
+ if (historyIndex === promptHistory.length) draft = buffer;
671
+ historyIndex = Math.max(historyIndex - 1, 0);
672
+ setBuffer(promptHistory[historyIndex], promptHistory[historyIndex].length);
673
+ return;
674
+ }
675
+ if (targetRowIndex >= layout.rows.length) {
676
+ if (historyIndex < promptHistory.length - 1) {
677
+ historyIndex += 1;
678
+ setBuffer(promptHistory[historyIndex], promptHistory[historyIndex].length);
679
+ } else if (historyIndex < promptHistory.length) {
680
+ historyIndex = promptHistory.length;
681
+ setBuffer(draft, draft.length);
682
+ }
683
+ return;
684
+ }
685
+ const currentColumn = preferredColumn ?? location.column;
686
+ const targetRow = layout.rows[targetRowIndex];
687
+ cursor = targetRow.start + Math.min(currentColumn, targetRow.end - targetRow.start);
688
+ preferredColumn = currentColumn;
689
+ redraw();
690
+ };
691
+
403
692
  const handler = (str = "", key = {}) => {
404
693
  if (key.ctrl && key.name === "c") {
694
+ renderNow();
695
+ moveToPromptBottom(rendered);
405
696
  cleanup();
406
697
  output.write("\n");
407
698
  reject(createAbortError());
408
699
  return;
409
700
  }
410
701
  if ((key.ctrl && key.name === "j") || key.sequence === "\n") {
411
- buffer += "\n";
702
+ ({ buffer, cursor } = insertAt(buffer, cursor, "\n"));
703
+ preferredColumn = null;
412
704
  redraw();
413
705
  return;
414
706
  }
@@ -417,7 +709,61 @@ function readTtyPrompt() {
417
709
  return;
418
710
  }
419
711
  if (key.name === "backspace") {
420
- buffer = buffer.slice(0, -1);
712
+ ({ buffer, cursor } = removeBefore(buffer, cursor));
713
+ preferredColumn = null;
714
+ redraw();
715
+ return;
716
+ }
717
+ if (key.name === "delete") {
718
+ ({ buffer, cursor } = removeAt(buffer, cursor));
719
+ preferredColumn = null;
720
+ redraw();
721
+ return;
722
+ }
723
+ if (key.name === "left") {
724
+ cursor = Math.max(cursor - 1, 0);
725
+ preferredColumn = null;
726
+ redraw();
727
+ return;
728
+ }
729
+ if (key.name === "right") {
730
+ cursor = Math.min(cursor + 1, buffer.length);
731
+ preferredColumn = null;
732
+ redraw();
733
+ return;
734
+ }
735
+ if (key.name === "up") {
736
+ moveVertical(-1);
737
+ return;
738
+ }
739
+ if (key.name === "down") {
740
+ moveVertical(1);
741
+ return;
742
+ }
743
+ if ((key.ctrl && key.name === "a") || key.name === "home") {
744
+ cursor = lineBounds(buffer, cursor).start;
745
+ preferredColumn = null;
746
+ redraw();
747
+ return;
748
+ }
749
+ if ((key.ctrl && key.name === "e") || key.name === "end") {
750
+ cursor = lineBounds(buffer, cursor).end;
751
+ preferredColumn = null;
752
+ redraw();
753
+ return;
754
+ }
755
+ if (key.ctrl && key.name === "u") {
756
+ const bounds = lineBounds(buffer, cursor);
757
+ buffer = `${buffer.slice(0, bounds.start)}${buffer.slice(cursor)}`;
758
+ cursor = bounds.start;
759
+ preferredColumn = null;
760
+ redraw();
761
+ return;
762
+ }
763
+ if (key.ctrl && key.name === "k") {
764
+ const bounds = lineBounds(buffer, cursor);
765
+ buffer = `${buffer.slice(0, cursor)}${buffer.slice(bounds.end)}`;
766
+ preferredColumn = null;
421
767
  redraw();
422
768
  return;
423
769
  }
@@ -425,27 +771,32 @@ function readTtyPrompt() {
425
771
  const suggestions = commandSuggestions(buffer.split("\n")[0] || "");
426
772
  if (suggestions.length === 1) {
427
773
  buffer = suggestions[0];
774
+ cursor = buffer.length;
428
775
  }
776
+ preferredColumn = null;
429
777
  redraw();
430
778
  return;
431
779
  }
432
780
  if (key.name === "escape") {
433
781
  buffer = "";
782
+ cursor = 0;
783
+ preferredColumn = null;
434
784
  redraw();
435
785
  return;
436
786
  }
437
787
  if (key.ctrl || key.meta) return;
438
788
  if (str && !key.sequence?.startsWith("\x1b")) {
439
- buffer += str;
789
+ const text = str.replace(/\r/g, "");
790
+ ({ buffer, cursor } = insertAt(buffer, cursor, text));
791
+ preferredColumn = null;
440
792
  redraw();
441
793
  }
442
794
  };
443
795
 
444
796
  input.resume();
445
797
  input.setRawMode(true);
446
- output.write(ansi.cursorHide);
447
798
  input.on("keypress", handler);
448
- redraw();
799
+ renderNow();
449
800
  });
450
801
  }
451
802
 
@@ -638,6 +989,28 @@ async function handleCommand(line, state, packageDir) {
638
989
  await promptAndSaveProviderKey(value || "deepseek", state);
639
990
  return true;
640
991
  }
992
+ if (command === "instructions" || command === "memory") {
993
+ if (value === "init") {
994
+ const result = await initProject(process.cwd());
995
+ printAgentMessage(`AGINTI.md ready at ${result.instructionsPath}`);
996
+ return true;
997
+ }
998
+ const instructions = await readProjectInstructions(process.cwd(), { maxBytes: 4000 });
999
+ if (!instructions.exists) {
1000
+ printAgentMessage("No AGINTI.md found. Run `/init` or `/instructions init` to create editable project instructions.");
1001
+ return true;
1002
+ }
1003
+ printAgentMessage(
1004
+ [
1005
+ `AGINTI.md: ${instructions.path}${instructions.truncated ? " (preview truncated)" : ""}`,
1006
+ "",
1007
+ instructions.content.trim() || "(empty)",
1008
+ "",
1009
+ "To edit it, ask normally: update AGINTI.md to remember that tests use pytest.",
1010
+ ].join("\n")
1011
+ );
1012
+ return true;
1013
+ }
641
1014
  if (command === "auxilliary" || command === "auxiliary") {
642
1015
  const action = value || "status";
643
1016
  if (action === "status") {
@@ -787,7 +1160,7 @@ async function handleCommand(line, state, packageDir) {
787
1160
  }
788
1161
  if (command === "init") {
789
1162
  const result = await initProject(process.cwd());
790
- printAgentMessage(`initialized project=${result.projectRoot}`);
1163
+ printAgentMessage(`initialized project=${result.projectRoot}\nAGINTI.md=${result.instructionsPath}`);
791
1164
  return true;
792
1165
  }
793
1166
  if (command === "web") {
@@ -67,6 +67,7 @@ function mockCommandForGoal(goal = "") {
67
67
 
68
68
  function mockPathForGoal(goal = "") {
69
69
  const text = String(goal);
70
+ if (/\bAGINTI\.md\b|project instructions|remember (?:that|this)|durable preference/i.test(text)) return "AGINTI.md";
70
71
  const explicit = text.match(/(?:file|path):\s*`?([A-Za-z0-9_./-]+)`?/i)?.[1];
71
72
  if (explicit) return explicit;
72
73
  const createPath = text.match(
@@ -82,6 +83,13 @@ function mockPathForGoal(goal = "") {
82
83
  function mockWorkspaceToolForGoal(goal = "") {
83
84
  const text = String(goal).toLowerCase();
84
85
  const targetPath = mockPathForGoal(goal);
86
+ if (targetPath === "AGINTI.md" && /update|remember|instruction|preference|aginti\.md/.test(text)) {
87
+ return mockToolCall("write_file", {
88
+ path: targetPath,
89
+ mode: "overwrite",
90
+ content: `# AGINTI.md\n\nProject instructions for AgInTiFlow agents.\n\n## Notes\n\n- ${String(goal).slice(0, 180)}\n`,
91
+ });
92
+ }
85
93
  if (/inspect|map|overview|architecture|large codebase|large repo|repository|repo\b|codebase/.test(text)) {
86
94
  return mockToolCall("inspect_project", {
87
95
  path: ".",
@@ -185,6 +193,7 @@ function mockChatResponse(content, toolCalls = []) {
185
193
  export async function createPlan(client, config, state) {
186
194
  const taskProfile = getTaskProfile(config.taskProfile);
187
195
  const engineeringGuidance = engineeringGuidanceForTask(state.goal, config.taskProfile);
196
+ const projectInstructions = state.meta?.projectInstructions;
188
197
  if (client.mock) {
189
198
  return [
190
199
  "1. Inspect the request and prefer the local shell when available.",
@@ -213,8 +222,11 @@ export async function createPlan(client, config, state) {
213
222
  ? `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.`
214
223
  : "",
215
224
  config.allowFileTools
216
- ? `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.`
225
+ ? `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.`
217
226
  : "",
227
+ projectInstructions?.exists
228
+ ? `Project instructions: AGINTI.md is loaded from ${projectInstructions.path}${projectInstructions.truncated ? " (truncated)" : ""}. Follow it and update it with file tools when the user asks to remember or change project instructions.`
229
+ : "Project instructions: AGINTI.md is not present unless created by /init or file tools.",
218
230
  config.allowWrapperTools
219
231
  ? `Agent wrappers are enabled. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Status: ${wrapperStatusText()}.`
220
232
  : "",
@@ -67,6 +67,9 @@ function scoutMessages(config, state, scout) {
67
67
  `Goal: ${config.goal || state.goal || ""}`,
68
68
  `Task profile: ${profile.label}. ${profile.prompt}`,
69
69
  guidance,
70
+ state.meta?.projectInstructions?.exists
71
+ ? `Project instructions: AGINTI.md loaded from ${state.meta.projectInstructions.path}${state.meta.projectInstructions.truncated ? " (truncated)" : ""}.`
72
+ : "Project instructions: AGINTI.md not present.",
70
73
  `Workspace: ${config.commandCwd}`,
71
74
  `Sandbox: ${config.sandboxMode}; package policy: ${config.packageInstallPolicy}`,
72
75
  `Current plan:\n${state.plan || "(none yet)"}`,
package/src/project.js CHANGED
@@ -27,6 +27,7 @@ export function projectPaths(projectRoot = process.cwd()) {
27
27
  const root = resolveProjectRoot(projectRoot);
28
28
  return {
29
29
  root,
30
+ agintiInstructionsPath: path.join(root, "AGINTI.md"),
30
31
  controlDir: path.join(root, ".aginti"),
31
32
  envPath: path.join(root, ".aginti", ".env"),
32
33
  rootEnvPath: path.join(root, ".env"),
@@ -40,6 +41,60 @@ export function projectPaths(projectRoot = process.cwd()) {
40
41
  };
41
42
  }
42
43
 
44
+ export function defaultAgintiInstructions() {
45
+ return [
46
+ "# AGINTI.md",
47
+ "",
48
+ "Project instructions for AgInTiFlow agents.",
49
+ "",
50
+ "Edit this file directly or ask AgInTiFlow to update it during chat. Keep durable project preferences here; keep secrets in `.aginti/.env` instead.",
51
+ "",
52
+ "## Project Goals",
53
+ "",
54
+ "- Describe what this project is for.",
55
+ "- Note the main user-facing workflows the agent should preserve.",
56
+ "",
57
+ "## Agent Preferences",
58
+ "",
59
+ "- Prefer small, inspectable changes over broad rewrites.",
60
+ "- Read relevant files before editing.",
61
+ "- Run focused checks when tools and dependencies are available.",
62
+ "- Keep generated artifacts in project folders with clear names.",
63
+ "",
64
+ "## Useful Commands",
65
+ "",
66
+ "- Add build, test, lint, preview, or compile commands here.",
67
+ "",
68
+ "## Notes",
69
+ "",
70
+ "- Add project-specific terminology, style preferences, and known constraints here.",
71
+ "",
72
+ ].join("\n");
73
+ }
74
+
75
+ export async function readProjectInstructions(projectRoot = process.cwd(), { maxBytes = 24_000 } = {}) {
76
+ const paths = projectPaths(projectRoot);
77
+ try {
78
+ const stat = await fsp.stat(paths.agintiInstructionsPath);
79
+ if (!stat.isFile()) return { exists: false, path: paths.agintiInstructionsPath, content: "", truncated: false };
80
+ const handle = await fsp.open(paths.agintiInstructionsPath, "r");
81
+ try {
82
+ const buffer = Buffer.alloc(Math.min(stat.size, maxBytes));
83
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
84
+ return {
85
+ exists: true,
86
+ path: paths.agintiInstructionsPath,
87
+ content: buffer.subarray(0, bytesRead).toString("utf8"),
88
+ truncated: stat.size > maxBytes,
89
+ };
90
+ } finally {
91
+ await handle.close();
92
+ }
93
+ } catch {
94
+ return { exists: false, path: paths.agintiInstructionsPath, content: "", truncated: false };
95
+ }
96
+ }
97
+
43
98
  export function parseEnvText(text = "") {
44
99
  const values = {};
45
100
  for (const rawLine of String(text).split(/\r?\n/)) {
@@ -126,6 +181,10 @@ export async function initProject(projectRoot = process.cwd()) {
126
181
  await ensureDir(paths.controlDir);
127
182
  await ensureDir(paths.notesDir);
128
183
  await ensureDir(paths.sessionsDir);
184
+ await ensureFile(
185
+ paths.agintiInstructionsPath,
186
+ defaultAgintiInstructions()
187
+ );
129
188
  await ensureFile(
130
189
  paths.controlReadmePath,
131
190
  [
@@ -133,6 +192,7 @@ export async function initProject(projectRoot = process.cwd()) {
133
192
  "",
134
193
  "This folder stores project-local AgInTiFlow configuration.",
135
194
  "",
195
+ "- `../AGINTI.md` stores editable project instructions for CLI and web agents.",
136
196
  "- `.env` is ignored and can hold local provider keys.",
137
197
  "- `.env.example` documents accepted variable names.",
138
198
  "- `.sessions/` at the project root stores CLI and web run history.",
@@ -175,6 +235,7 @@ export async function initProject(projectRoot = process.cwd()) {
175
235
  return {
176
236
  ok: true,
177
237
  projectRoot: paths.root,
238
+ instructionsPath: paths.agintiInstructionsPath,
178
239
  controlDir: paths.controlDir,
179
240
  sessionsDir: paths.sessionsDir,
180
241
  created,
@@ -318,10 +379,11 @@ export async function npmLatestVersion(packageName = "@lazyingart/agintiflow") {
318
379
  export async function doctorReport(projectRoot, packageVersion, config) {
319
380
  const paths = projectPaths(projectRoot);
320
381
  const keyStatus = providerKeyStatus(projectRoot);
321
- const [sessions, dockerStatus, latestVersion] = await Promise.all([
382
+ const [sessions, dockerStatus, latestVersion, instructions] = await Promise.all([
322
383
  listProjectSessions(projectRoot, 8),
323
384
  getDockerSandboxStatus(config).catch((error) => ({ ok: false, error: error.message })),
324
385
  npmLatestVersion(),
386
+ readProjectInstructions(projectRoot, { maxBytes: 1 }),
325
387
  ]);
326
388
 
327
389
  return {
@@ -337,6 +399,8 @@ export async function doctorReport(projectRoot, packageVersion, config) {
337
399
  },
338
400
  project: {
339
401
  root: paths.root,
402
+ instructionsPath: paths.agintiInstructionsPath,
403
+ instructionsPresent: instructions.exists,
340
404
  controlDir: paths.controlDir,
341
405
  sessionsDir: paths.sessionsDir,
342
406
  sessionDbPath: paths.sessionDbPath,
@@ -3,63 +3,63 @@ export const TASK_PROFILES = {
3
3
  id: "auto",
4
4
  label: "Auto",
5
5
  prompt:
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.",
6
+ "Act as the general-purpose AgInTiFlow agent. Infer the task type from the request, then choose the right mix of browser, shell, files, web search, canvas, and sandbox tools. For short tasks, use the smallest safe tool sequence that completes the work. For codebase, writing, website, LaTeX, system, debugging, migration, or multi-language tasks, automatically borrow the relevant specialized profile habits without becoming narrowly constrained.",
7
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 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.",
13
+ "Bias toward coding-agent behavior across languages, but remain a general assistant when the task needs docs, shell, web, or design work. Inspect project instructions/manifests/conventions, edit workspace files with patches, run useful focused checks, iterate on failures, and report changed files plus residual risks.",
14
14
  tools: ["inspect_project", "files", "shell", "sandbox"],
15
15
  },
16
16
  "large-codebase": {
17
17
  id: "large-codebase",
18
18
  label: "Large codebase engineering",
19
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.",
20
+ "Bias toward senior large-repo engineering while still answering ordinary side questions. Inspect_project first unless context is already known, read AGINTI/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
21
  tools: ["inspect_project", "search_files", "read_file", "apply_patch", "shell", "sandbox", "canvas"],
22
22
  },
23
23
  writing: {
24
24
  id: "writing",
25
25
  label: "Book/script writing",
26
26
  prompt:
27
- "Create structured drafts with outlines, sections, and revision notes. Use files for long-form output and canvas for important drafts.",
27
+ "Bias toward long-form writing quality without refusing adjacent research, code, or formatting tasks. Create structured drafts with outlines, sections, revision notes, and saved files for durable output. Use web search when current sources matter and canvas for important drafts.",
28
28
  tools: ["files", "canvas"],
29
29
  },
30
30
  design: {
31
31
  id: "design",
32
32
  label: "Design docs",
33
33
  prompt:
34
- "Produce concise design documents with goals, constraints, options, tradeoffs, implementation steps, and verification criteria.",
34
+ "Bias toward clear product/engineering design while remaining able to implement or test when asked. Produce concise design documents with goals, constraints, options, tradeoffs, implementation steps, verification criteria, and decision records.",
35
35
  tools: ["files", "canvas"],
36
36
  },
37
37
  python: {
38
38
  id: "python",
39
39
  label: "Python",
40
40
  prompt:
41
- "For Python tasks, create small scripts or notebooks as files, prefer virtual environments or Docker for package setup, and run smoke checks when shell is enabled.",
41
+ "Bias toward Python best practices without ignoring non-Python project context. Inspect pyproject/requirements, create scripts/packages/tests as files, prefer project-local venv/conda/uv or Docker for setup, and run focused smoke checks when shell is enabled.",
42
42
  tools: ["files", "shell", "sandbox"],
43
43
  },
44
44
  shell: {
45
45
  id: "shell",
46
46
  label: "Shell",
47
47
  prompt:
48
- "For shell tasks, use allowlisted commands, explain blocked commands, avoid destructive operations, and keep outputs concise.",
48
+ "Bias toward terminal/system diagnosis and scripting while still using files, web, or docs when useful. Gather evidence first, write reusable scripts when appropriate, follow the configured trust/sandbox policy, and keep command outputs concise.",
49
49
  tools: ["shell", "sandbox"],
50
50
  },
51
51
  node: {
52
52
  id: "node",
53
53
  label: "Node",
54
54
  prompt:
55
- "For Node.js tasks, use the local project structure, add tests when useful, and run safe npm/node checks when available.",
55
+ "Bias toward Node/JavaScript/TypeScript workflows without excluding frontend, backend, docs, or deployment work. Inspect package.json/lockfiles, respect the package manager, add tests when useful, and run safe npm/node checks when available.",
56
56
  tools: ["files", "shell", "sandbox"],
57
57
  },
58
58
  website: {
59
59
  id: "website",
60
60
  label: "Website testing",
61
61
  prompt:
62
- "For website/app tasks, create or inspect real site files, preview with workspace preview tools, add local checks when useful, and use the configured sandbox/package policy for dependencies.",
62
+ "Bias toward real website/app delivery while staying general enough for copy, assets, backend, and tests. Create or inspect real site files, use vivid but tidy UI when designing from scratch, preview with workspace preview tools, add local checks when useful, and use the configured sandbox/package policy for dependencies.",
63
63
  tools: ["files", "shell", "canvas", "sandbox"],
64
64
  },
65
65
  image: {
@@ -73,21 +73,21 @@ export const TASK_PROFILES = {
73
73
  id: "aaps",
74
74
  label: "AAPS",
75
75
  prompt:
76
- "For AAPS tasks, recognize .aaps folders and @lazyingart/aaps workflows, keep work project-local, and avoid secrets or publishing.",
76
+ "Bias toward AAPS workflows while still handling normal project work. Recognize .aaps folders and @lazyingart/aaps conventions, keep work project-local, document assumptions, and avoid secrets or publishing unless explicitly requested.",
77
77
  tools: ["files", "shell", "sandbox"],
78
78
  },
79
79
  latex: {
80
80
  id: "latex",
81
81
  label: "LaTeX",
82
82
  prompt:
83
- "For LaTeX/PDF tasks, 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 project outputs and the persistent Python/conda/tool cache under /aginti-env 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, 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.",
84
84
  tools: ["files", "shell", "canvas", "sandbox"],
85
85
  },
86
86
  maintenance: {
87
87
  id: "maintenance",
88
88
  label: "System maintenance",
89
89
  prompt:
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.",
90
+ "Bias toward practical system repair while staying useful for code/docs around the fix. 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
91
  tools: ["shell", "sandbox", "files", "inspect_project"],
92
92
  },
93
93
  };
@@ -53,6 +53,7 @@ const IMPORTANT_MANIFESTS = new Set([
53
53
  "compose.yml",
54
54
  "README.md",
55
55
  "AGENTS.md",
56
+ "AGINTI.md",
56
57
  ]);
57
58
  const SOURCE_DIR_NAMES = new Set(["src", "app", "lib", "packages", "apps", "bin", "scripts", "server", "client", "public", "docs"]);
58
59
  const TEST_DIR_NAMES = new Set(["test", "tests", "__tests__", "spec", "specs", "e2e"]);
@@ -279,7 +280,7 @@ async function readJsonFileSafe(absolutePath) {
279
280
 
280
281
  function recommendedReads(summary) {
281
282
  const reads = [];
282
- for (const name of ["AGENTS.md", "README.md", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"]) {
283
+ for (const name of ["AGINTI.md", "AGENTS.md", "README.md", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"]) {
283
284
  const match = summary.manifestFiles.find((item) => item.path === name || item.path.endsWith(`/${name}`));
284
285
  if (match) reads.push(match.path);
285
286
  }
@@ -533,7 +534,7 @@ async function inspectProject(config, args) {
533
534
  packageScripts,
534
535
  recommendedReads: [],
535
536
  engineeringHints: [
536
- "Read AGENTS/README/manifests before editing.",
537
+ "Read AGINTI/AGENTS/README/manifests before editing.",
537
538
  "Use search_files to locate symbols and tests, then read exact files.",
538
539
  "Use apply_patch for source edits and run the smallest relevant check first.",
539
540
  "If a change spans modules, patch in small batches and verify after each batch.",