@lazyingart/agintiflow 0.13.0 → 0.14.1

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,7 @@ 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, `/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. During an active run, Enter sends the draft as an ASAP pipe message (`→`) and Tab queues it for after the run (`↳`); ASAP messages are consumed first, Alt+Up edits the last piped message, and Shift+Left edits the last after-finish queued message. The input panel always shows the current `cwd` footer and a single live status row, so long goals and tool updates are compacted instead of flooding the transcript. Assistant responses start on a fresh line after the `aginti>` header with a colored response gutter and render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, tables, and red/green patch diff lines. Resuming a session prints the full saved chat history with wrapped messages before the prompt. 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. During an active run, Enter sends the draft as an ASAP pipe message (`→`) and Tab queues it for after the run (`↳`); ASAP messages are consumed before after-finish queued prompts, Alt+Up edits the last piped message, and Shift+Left edits the last after-finish queued message. Idle Esc is ignored so it does not disturb the input panel; during a run, Esc waits when `→` messages are pending and otherwise stops the run cleanly. Ctrl+C always stops and prints the resume command. The input panel always shows the current `cwd` footer and a single live status row, so long goals and tool updates are compacted instead of flooding the transcript. Assistant responses start on a fresh line after the `aginti>` header with a colored response gutter and render common Markdown, including headings, inline code, bold text, lists, quotes, code fences, tables, and red/green patch diff lines. Resuming a session prints the full saved chat history with wrapped messages before the prompt.
71
71
 
72
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`.
73
73
 
@@ -83,6 +83,16 @@ For current docs, install errors, package/toolchain setup, and source discovery,
83
83
 
84
84
  For raster image work, AgInTiFlow has an optional `image_generation` skill backed by the `generate_image` tool and a local `GRSAI` key. The skill tells DeepSeek when image generation is appropriate; the tool calls GRS AI Nano Banana, saves manifests/images under `artifacts/images`, and sends the result to the canvas. See [docs/auxiliary-image-generation.md](docs/auxiliary-image-generation.md).
85
85
 
86
+ AgInTiFlow now ships a Markdown skill library in `skills/<id>/SKILL.md`. Skills are prompt playbooks, while tools are deterministic actions such as `apply_patch`, `run_command`, `web_search`, `generate_image`, and `send_to_canvas`. Built-in skills cover code, websites/apps, LaTeX manuscripts, books, Word documents, image generation, GitHub, system maintenance, Android, R/Stan, Python, C/C++, shell, AAPS, and novel writing. See [docs/skills-and-tools.md](docs/skills-and-tools.md).
87
+
88
+ ```bash
89
+ aginti skills
90
+ aginti skills website
91
+ aginti --list-skills latex
92
+ # in chat:
93
+ /skills github commit
94
+ ```
95
+
86
96
  Launch the local web UI from an installed package:
87
97
 
88
98
  ```bash
@@ -9,11 +9,11 @@ AgInTiFlow keeps CLI and web runs equivalent by using the project folder as the
9
9
 
10
10
  When a run is active, the web chat and `aginti queue <session-id> "..."` append messages to the inbox instead of trying to mutate the running process directly. The web API exposes `GET /api/sessions/:id/inbox`, `POST /api/sessions/:id/inbox`, `PATCH /api/sessions/:id/inbox/:itemId`, and `DELETE /api/sessions/:id/inbox/:itemId` so browser users can inspect, edit, or remove pending pipe messages before the runner consumes them. The runner drains the inbox at safe boundaries: before each model step and after tool execution. This mirrors the event-queue style used by mature agent UIs while keeping the backend decoupled from any specific frontend.
11
11
 
12
- The interactive CLI keeps the input panel visible while a run is working. Enter sends the current draft as an ASAP pipe message and displays it as `→`; the runner drains those messages before normal inbox items. Tab stores the draft as an after-finish queue item and displays it as `↳`; those prompts run only after the current run completes. Alt+Up moves the last pending `→` message back into the editor, and Shift+Left moves the last pending `↳` message back into the editor. The current command cwd is rendered below the input panel in both idle and running states.
12
+ The interactive CLI keeps the input panel visible while a run is working. Enter sends the current draft as an ASAP pipe message and displays it as `→`; the runner drains those messages before normal inbox items and before after-finish queued prompts. Tab stores the draft as an after-finish queue item and displays it as `↳`; those prompts run only after the current run completes. Alt+Up moves the last pending `→` message back into the editor, and Shift+Left moves the last pending `↳` message back into the editor. Idle Esc is ignored so it does not redraw the prompt into the transcript. During a run, Esc waits when `→` pipe messages are still pending and stops the run only when no ASAP pipe message is pending; Ctrl+C always stops. The current command cwd is rendered below the input panel in both idle and running states.
13
13
 
14
14
  The web UI uses a related but browser-appropriate pattern. Enter sends and Shift+Enter adds a newline. `Pipe to run` writes an ASAP inbox item shared with CLI. `Queue after finish` keeps a browser-local next prompt and starts it after the current web-owned run finishes. Both lanes render in a pending panel with Edit and Remove buttons instead of terminal-only keybindings.
15
15
 
16
- Runs can be stopped without corrupting session state. The CLI listens for Esc or Ctrl+C during an active run, and the web UI exposes a Stop button plus Esc. Both paths send an abort signal, persist `session.stopped`, and leave the session resumable through `aginti resume <session-id>`.
16
+ Runs can be stopped without corrupting session state. The CLI listens for Ctrl+C during an active run, and Esc stops only when no ASAP pipe message is waiting to be applied. The web UI exposes a Stop button plus Esc. Stop paths send an abort signal, persist `session.stopped`, and leave the session resumable through `aginti resume <session-id>`.
17
17
 
18
18
  Default execution is Docker workspace mode with package installs approved inside the sandbox. The project is mounted at `/workspace`; persistent agent toolchain folders are mounted at `/aginti-home`, `/aginti-cache`, and `/aginti-env` from `~/.agintiflow/docker/`. Python, conda, and other language-level environments should be installed under `/aginti-env` so they survive across runs. Apt/apk package changes are ephemeral unless the Docker image is rebuilt.
19
19
 
@@ -0,0 +1,58 @@
1
+ # Skills And Tools
2
+
3
+ AgInTiFlow separates **skills** from **tools** so the agent can stay general while still improving on specialized work.
4
+
5
+ ## Definitions
6
+
7
+ **Skill**: Markdown guidance stored at `skills/<id>/SKILL.md`. A skill describes when to use a workflow, what to inspect first, which outputs matter, and which tools are usually useful. Skills are prompt context, not executable code.
8
+
9
+ **Tool**: A deterministic callable capability exposed to the model, such as `inspect_project`, `read_file`, `apply_patch`, `run_command`, `web_search`, `generate_image`, `preview_workspace`, or `send_to_canvas`.
10
+
11
+ **Profile**: A broad runtime mode such as `auto`, `code`, `latex`, or `maintenance`. Profiles tune routing, max steps, and general behavior. Skills can combine across profiles.
12
+
13
+ ## Built-In Skills
14
+
15
+ The package ships built-in skills for code engineering, website/app building, LaTeX manuscripts, books, Microsoft Word documents, image generation, GitHub maintenance, system maintenance, Android, R/Stan, Python, C/C++, shell scripting, AAPS, and novel writing.
16
+
17
+ List them from a project:
18
+
19
+ ```bash
20
+ aginti skills
21
+ aginti skills website
22
+ aginti --list-skills latex
23
+ ```
24
+
25
+ Inside interactive chat:
26
+
27
+ ```text
28
+ /skills
29
+ /skills github commit
30
+ ```
31
+
32
+ ## Selection Flow
33
+
34
+ For every run, AgInTiFlow scores the user goal and active profile against skill frontmatter:
35
+
36
+ ```yaml
37
+ ---
38
+ id: latex-manuscript
39
+ label: LaTeX Manuscript
40
+ description: Write, compile, and package LaTeX papers, reports, figures, bibliographies, and PDFs.
41
+ triggers:
42
+ - latex
43
+ - tex
44
+ - manuscript
45
+ tools:
46
+ - write_file
47
+ - apply_patch
48
+ - run_command
49
+ ---
50
+ ```
51
+
52
+ Selected skills are injected into the plan and execution prompts. The LLM still decides what to do; skills only provide domain playbooks and guardrails.
53
+
54
+ ## Adding A Skill
55
+
56
+ Create `skills/<id>/SKILL.md` with valid YAML frontmatter and a short Markdown body. Keep descriptions strings, not YAML arrays, because loaders expect `id`, `label`, and `description` as scalar strings.
57
+
58
+ Good skills are small, actionable, and tool-aware. They should say what to inspect, what to create or verify, and what to avoid. They should not hard-code one exact task.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.13.0",
3
+ "version": "0.14.1",
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",
@@ -46,10 +46,12 @@
46
46
  "scripts/smoke-coding-tools.js",
47
47
  "scripts/smoke-capabilities.js",
48
48
  "scripts/smoke-inbox.js",
49
+ "scripts/smoke-skills.js",
49
50
  "scripts/smoke-toolchain-docker.js",
50
51
  "scripts/smoke-web-api.js",
51
52
  "src/",
52
53
  "run.js",
54
+ "skills/",
53
55
  "web.js"
54
56
  ],
55
57
  "bin": {
@@ -64,11 +66,12 @@
64
66
  "smoke:coding-tools": "node scripts/smoke-coding-tools.js",
65
67
  "smoke:auxiliary-tools": "node scripts/smoke-auxiliary-tools.js",
66
68
  "smoke:cli-chat": "node scripts/smoke-cli-chat.js",
69
+ "smoke:skills": "node scripts/smoke-skills.js",
67
70
  "smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
68
71
  "smoke:inbox": "node scripts/smoke-inbox.js",
69
72
  "smoke:web-api": "node scripts/smoke-web-api.js",
70
73
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
71
- "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:capabilities && npm run smoke:cli-chat && npm run smoke:inbox",
74
+ "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:capabilities && npm run smoke:skills && npm run smoke:cli-chat && npm run smoke:inbox",
72
75
  "pack:dry-run": "npm pack --dry-run",
73
76
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
74
77
  },
@@ -81,6 +81,14 @@ try {
81
81
  capabilities.trustedDockerPolicy.some((check) => check.command.startsWith("chmod") && check.allowed),
82
82
  "trusted Docker policy did not allow chmod"
83
83
  );
84
+ assert(
85
+ capabilities.tools?.skills?.some((skill) => skill.id === "website-app"),
86
+ "capabilities did not report built-in website skill"
87
+ );
88
+ assert(
89
+ capabilities.tools?.skills?.some((skill) => skill.id === "latex-manuscript"),
90
+ "capabilities did not report built-in LaTeX skill"
91
+ );
84
92
 
85
93
  const doctor = JSON.parse(await runCli(["doctor", "--capabilities", "--json"]));
86
94
  assert(doctor.project.root === tempRoot, "doctor --capabilities used the wrong project root");
@@ -91,7 +99,7 @@ try {
91
99
  {
92
100
  ok: true,
93
101
  projectRoot: tempRoot,
94
- checks: ["aginti-md-init", "capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy", "git-policy"],
102
+ checks: ["aginti-md-init", "capabilities-cli", "doctor-capabilities", "maintenance-policy", "trusted-docker-policy", "git-policy", "skills-capability"],
95
103
  },
96
104
  null,
97
105
  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 { buildPromptLayout, stripMarkdown } from "../src/interactive-cli.js";
7
+ import { buildPromptLayout, classifyEscapeAction, 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-"));
@@ -112,6 +112,15 @@ try {
112
112
  if (!queuedText.includes("run running · tool: apply_patch") || !queuedText.includes("→ apply this") || !queuedText.includes("↳ run this") || !queuedText.includes("cwd /tmp/aginti-project")) {
113
113
  throw new Error("terminal prompt layout did not render live input queue and cwd footer");
114
114
  }
115
+ if (classifyEscapeAction({ active: false }) !== "noop") {
116
+ throw new Error("idle Esc should not redraw or clear the prompt");
117
+ }
118
+ if (classifyEscapeAction({ active: true, pendingAsap: [{ content: "apply now" }] }) !== "wait-for-asap") {
119
+ throw new Error("active Esc should wait when ASAP pipe messages are pending");
120
+ }
121
+ if (classifyEscapeAction({ active: true, pendingAsap: [] }) !== "abort") {
122
+ throw new Error("active Esc should abort when no ASAP pipe messages are pending");
123
+ }
115
124
 
116
125
  await runCli(["init"], "");
117
126
  const instructions = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
@@ -122,6 +131,10 @@ try {
122
131
  if (!instructionsResult.stdout.includes("AGINTI.md") || !instructionsResult.stdout.includes("Project instructions")) {
123
132
  throw new Error("interactive /instructions did not show AGINTI.md");
124
133
  }
134
+ const skillsResult = await runChat("/skills website\n/exit\n");
135
+ if (!skillsResult.stdout.includes("website-app") || !skillsResult.stdout.includes("Website And App Builder")) {
136
+ throw new Error("interactive /skills did not show matching built-in skills");
137
+ }
125
138
  await runChat("remember that this project prefers pytest smoke tests in AGINTI.md\n/exit\n");
126
139
  const updatedInstructions = await fs.readFile(path.join(tempRoot, "AGINTI.md"), "utf8");
127
140
  if (!updatedInstructions.includes("pytest smoke tests")) {
@@ -159,7 +172,7 @@ try {
159
172
  {
160
173
  ok: true,
161
174
  projectRoot: tempRoot,
162
- checks: ["markdown-render", "markdown-table-no-duplicate", "patch-diff-render", "prompt-layout", "live-input-status-layout", "agent-response-gutter", "aginti-md", "instructions-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest", "resume-history-full"],
175
+ checks: ["markdown-render", "markdown-table-no-duplicate", "patch-diff-render", "prompt-layout", "escape-policy", "live-input-status-layout", "agent-response-gutter", "aginti-md", "instructions-command", "skills-command", "instructions-chat-edit", "interactive-chat", "mock-file-write", "run-status", "resume-latest", "resume-history-full"],
163
176
  },
164
177
  null,
165
178
  2
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { execFile } from "node:child_process";
3
+ import path from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { fileURLToPath } from "node:url";
6
+ import { formatSkillsForPrompt, listSkills, selectSkillsForGoal } from "../src/skill-library.js";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
+
11
+ function assert(condition, message) {
12
+ if (!condition) throw new Error(message);
13
+ }
14
+
15
+ function selectedIds(goal, taskProfile = "auto") {
16
+ return selectSkillsForGoal(goal, { taskProfile, limit: 8 }).map((skill) => skill.id);
17
+ }
18
+
19
+ const skills = listSkills({ includeBody: true });
20
+ const ids = new Set(skills.map((skill) => skill.id));
21
+ assert(skills.length >= 14, "expected built-in skills to load");
22
+ for (const required of [
23
+ "aaps",
24
+ "code",
25
+ "github-maintenance",
26
+ "image-generation",
27
+ "latex-manuscript",
28
+ "system-maintenance",
29
+ "website-app",
30
+ "word-documents",
31
+ ]) {
32
+ assert(ids.has(required), `missing required skill ${required}`);
33
+ }
34
+
35
+ assert(selectedIds("write a beautiful React website and preview it").includes("website-app"), "website prompt did not select website-app");
36
+ assert(selectedIds("write a LaTeX paper and compile a PDF").includes("latex-manuscript"), "latex prompt did not select latex-manuscript");
37
+ assert(selectedIds("edit a Microsoft Word docx and preserve the original").includes("word-documents"), "docx prompt did not select word-documents");
38
+ assert(selectedIds("generate a logo image with grsai nanobanana").includes("image-generation"), "image prompt did not select image-generation");
39
+ assert(selectedIds("git status commit push with gh").includes("github-maintenance"), "git prompt did not select github-maintenance");
40
+ assert(selectedIds("create an .aaps example for @lazyingart/aaps").includes("aaps"), "AAPS prompt did not select aaps");
41
+ assert(selectedIds("debug a C++ CMake build").includes("c-cpp"), "C++ prompt did not select c-cpp");
42
+ assert(selectedIds("set up Stan and CmdStanR reproducibly").includes("r-stan"), "Stan prompt did not select r-stan");
43
+
44
+ const prompt = formatSkillsForPrompt(selectSkillsForGoal("write latex manuscript with figures", { taskProfile: "latex", limit: 3 }));
45
+ assert(prompt.includes("A skill is Markdown guidance"), "skill prompt does not explain skill semantics");
46
+ assert(prompt.includes("latex-manuscript"), "skill prompt omitted selected skill");
47
+ assert(prompt.length < 5400, "skill prompt is too large for normal runs");
48
+
49
+ const cli = await execFileAsync(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), "skills", "website"], {
50
+ cwd: repoRoot,
51
+ timeout: 10000,
52
+ maxBuffer: 512 * 1024,
53
+ env: {
54
+ ...process.env,
55
+ AGINTIFLOW_RUNTIME_DIR: "",
56
+ },
57
+ });
58
+ assert(cli.stdout.includes("website-app"), "aginti skills website did not print website-app");
59
+
60
+ console.log(
61
+ JSON.stringify(
62
+ {
63
+ ok: true,
64
+ skills: skills.length,
65
+ checks: ["load-built-ins", "select-by-goal", "prompt-format", "cli-skills"],
66
+ },
67
+ null,
68
+ 2
69
+ )
70
+ );
@@ -81,6 +81,9 @@ try {
81
81
  if (!Array.isArray(config.taskProfiles) || !config.taskProfiles.some((profile) => profile.id === "latex")) {
82
82
  throw new Error("task profiles are not advertised by /api/config");
83
83
  }
84
+ if (!Array.isArray(config.skills) || !config.skills.some((skill) => skill.id === "website-app")) {
85
+ throw new Error("built-in skills are not advertised by /api/config");
86
+ }
84
87
 
85
88
  const keyStatus = await fetchJson("/api/keys/status");
86
89
  if (typeof keyStatus.keyStatus?.deepseek !== "boolean") throw new Error("key status endpoint is invalid");
@@ -0,0 +1,21 @@
1
+ ---
2
+ id: aaps
3
+ label: AAPS Workflows
4
+ description: Work with ../AAPS, .aaps files, @lazyingart/aaps, automation plans, and related project conventions.
5
+ triggers:
6
+ - aaps
7
+ - .aaps
8
+ - @lazyingart/aaps
9
+ - automation plan
10
+ tools:
11
+ - inspect_project
12
+ - read_file
13
+ - write_file
14
+ - run_command
15
+ - web_search
16
+ ---
17
+ # AAPS Workflows
18
+
19
+ Inspect nearby `.aaps` files, package docs, and project notes before generating workflows. Keep assumptions explicit and outputs project-local.
20
+
21
+ Do not publish, upload, or expose credentials unless explicitly requested and safe.
@@ -0,0 +1,23 @@
1
+ ---
2
+ id: android
3
+ label: Android Development
4
+ description: Build and debug Android, Gradle, Kotlin, Java, emulator, and mobile app projects.
5
+ triggers:
6
+ - android
7
+ - gradle
8
+ - kotlin
9
+ - java
10
+ - apk
11
+ - emulator
12
+ tools:
13
+ - inspect_project
14
+ - search_files
15
+ - apply_patch
16
+ - run_command
17
+ - web_search
18
+ ---
19
+ # Android Development
20
+
21
+ Inspect Gradle files, manifests, package names, modules, and existing build scripts before editing. Prefer narrow Gradle tasks and source-set-aware patches.
22
+
23
+ If SDK/emulator tooling is missing, produce a setup report or project-local script rather than guessing.
@@ -0,0 +1,23 @@
1
+ ---
2
+ id: book-writing
3
+ label: Book And Long-Form Writing
4
+ description: Plan, draft, revise, and structure books, chapters, scripts, tutorials, and long documents.
5
+ triggers:
6
+ - book
7
+ - chapter
8
+ - script
9
+ - tutorial
10
+ - long form
11
+ - outline
12
+ - manuscript
13
+ tools:
14
+ - write_file
15
+ - read_file
16
+ - web_search
17
+ - send_to_canvas
18
+ ---
19
+ # Book And Long-Form Writing
20
+
21
+ Start with structure: audience, promise, outline, chapter flow, voice, and revision plan. Save durable drafts as Markdown unless the user requests another format.
22
+
23
+ Use web search for current facts and keep source notes when claims matter. For long work, produce incremental chapters and revision notes instead of one huge brittle output.
@@ -0,0 +1,23 @@
1
+ ---
2
+ id: c-cpp
3
+ label: C And C++ Development
4
+ description: Work with C, C++, CMake, Make, native builds, tests, and low-level debugging.
5
+ triggers:
6
+ - c++
7
+ - cpp
8
+ - c language
9
+ - cmake
10
+ - makefile
11
+ - gcc
12
+ - clang
13
+ tools:
14
+ - inspect_project
15
+ - search_files
16
+ - apply_patch
17
+ - run_command
18
+ ---
19
+ # C And C++ Development
20
+
21
+ Inspect build files before source edits. Keep patches narrow, respect formatting style, and run the smallest compile/test target available.
22
+
23
+ If native dependencies are missing, prefer Docker/project-local setup notes and do not silently change host toolchains.
@@ -0,0 +1,25 @@
1
+ ---
2
+ id: code
3
+ label: Code Engineering
4
+ description: Build, edit, debug, test, and maintain software projects across languages.
5
+ triggers:
6
+ - code
7
+ - app
8
+ - bug
9
+ - test
10
+ - refactor
11
+ - package
12
+ - project
13
+ tools:
14
+ - inspect_project
15
+ - search_files
16
+ - read_file
17
+ - apply_patch
18
+ - run_command
19
+ - web_search
20
+ ---
21
+ # Code Engineering
22
+
23
+ Orient before editing: inspect the project map, read instructions/manifests, search exact symbols or errors, then patch the smallest coherent set of files.
24
+
25
+ Prefer deterministic `apply_patch` for source edits. Run focused checks first, repair failures, then broaden checks only when useful. Summarize changed files, commands run, and residual risk.
@@ -0,0 +1,23 @@
1
+ ---
2
+ id: github-maintenance
3
+ label: GitHub And Release Maintenance
4
+ description: Use git and gh safely for status, commits, pull requests, releases, and repository maintenance.
5
+ triggers:
6
+ - git
7
+ - github
8
+ - gh
9
+ - commit
10
+ - push
11
+ - pull request
12
+ - release
13
+ tools:
14
+ - run_command
15
+ - read_file
16
+ - apply_patch
17
+ - web_search
18
+ ---
19
+ # GitHub And Release Maintenance
20
+
21
+ Always inspect `git status --short` and relevant diffs before committing or pushing. Keep commits scoped and stop on conflicts, divergence, or unrelated dirty work.
22
+
23
+ Use `gh` for PR/release/status workflows when authenticated. Fold long command output but preserve key errors, URLs, branch names, and commit hashes.
@@ -0,0 +1,23 @@
1
+ ---
2
+ id: image-generation
3
+ label: Image Generation
4
+ description: Generate or edit raster images, posters, covers, logos, illustrations, and visual assets.
5
+ triggers:
6
+ - image
7
+ - logo
8
+ - poster
9
+ - cover
10
+ - illustration
11
+ - nanobanana
12
+ - grsai
13
+ - gpt image
14
+ tools:
15
+ - generate_image
16
+ - write_file
17
+ - send_to_canvas
18
+ ---
19
+ # Image Generation
20
+
21
+ Use image generation when the user asks for a raster visual asset or prompt. Write a concise prompt with subject, style, composition, color, lighting, and output constraints.
22
+
23
+ Prefer `generate_image` when a GRS AI key is available. Save selected outputs under artifacts and send the chosen image to canvas.
@@ -0,0 +1,25 @@
1
+ ---
2
+ id: latex-manuscript
3
+ label: LaTeX Manuscript
4
+ description: Write, compile, and package LaTeX papers, reports, figures, bibliographies, and PDFs.
5
+ triggers:
6
+ - latex
7
+ - tex
8
+ - manuscript
9
+ - paper
10
+ - pdf
11
+ - figure
12
+ - overleaf
13
+ tools:
14
+ - write_file
15
+ - apply_patch
16
+ - run_command
17
+ - open_workspace_file
18
+ - send_to_canvas
19
+ - web_search
20
+ ---
21
+ # LaTeX Manuscript
22
+
23
+ Use a project subfolder with `main.tex`, figures, bibliography, and generated PDFs. Compile from the correct directory with `latexmk` or `pdflatex` when available.
24
+
25
+ If TeX is missing, produce an honest setup note or Docker-local setup plan instead of faking success. Send the final PDF or source to canvas when useful.
@@ -0,0 +1,21 @@
1
+ ---
2
+ id: novel-writing
3
+ label: Novel Writing
4
+ description: Plan, draft, revise, and manage novels, character arcs, scenes, chapters, and long fiction.
5
+ triggers:
6
+ - novel
7
+ - fiction
8
+ - character
9
+ - scene
10
+ - plot
11
+ - chapter
12
+ tools:
13
+ - write_file
14
+ - read_file
15
+ - send_to_canvas
16
+ ---
17
+ # Novel Writing
18
+
19
+ Track premise, characters, setting, conflict, tone, and continuity. Save outlines, chapter drafts, and revision notes as durable files.
20
+
21
+ For long works, write in scenes or chapters and maintain a compact continuity bible instead of relying on chat history alone.
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: python
3
+ label: Python Development
4
+ description: Build Python packages, CLIs, scripts, tests, notebooks, plotting, and data workflows.
5
+ triggers:
6
+ - python
7
+ - pytest
8
+ - pyproject
9
+ - pip
10
+ - uv
11
+ - matplotlib
12
+ - pandas
13
+ tools:
14
+ - inspect_project
15
+ - search_files
16
+ - apply_patch
17
+ - run_command
18
+ - send_to_canvas
19
+ ---
20
+ # Python Development
21
+
22
+ Inspect `pyproject.toml`, requirements, package layout, and tests. Prefer stdlib tests when dependency installs are unnecessary; otherwise use project-local venv/conda/uv or Docker.
23
+
24
+ For plots and artifacts, save files with clear names and send useful outputs to canvas.
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: r-stan
3
+ label: R And Stan
4
+ description: Work with R, Stan, CmdStan, CmdStanR, PyStan, statistics, and reproducible analysis projects.
5
+ triggers:
6
+ - r
7
+ - stan
8
+ - cmdstan
9
+ - cmdstanr
10
+ - pystan
11
+ - statistics
12
+ - bayesian
13
+ tools:
14
+ - inspect_project
15
+ - write_file
16
+ - run_command
17
+ - web_search
18
+ - send_to_canvas
19
+ ---
20
+ # R And Stan
21
+
22
+ Keep analysis reproducible: scripts, data paths, package notes, seed handling, and output folders. Prefer Docker or project-local toolchains for installation.
23
+
24
+ For Stan, validate model syntax and compile/run only when toolchains exist; otherwise write honest setup scripts and checks.
@@ -0,0 +1,21 @@
1
+ ---
2
+ id: shell
3
+ label: Shell Scripting
4
+ description: Write, audit, and run shell scripts for automation, setup, diagnostics, and maintenance.
5
+ triggers:
6
+ - shell
7
+ - bash
8
+ - script
9
+ - terminal
10
+ - command
11
+ - cli
12
+ tools:
13
+ - write_file
14
+ - run_command
15
+ - web_search
16
+ ---
17
+ # Shell Scripting
18
+
19
+ Prefer idempotent scripts with `set -euo pipefail` when appropriate, clear variables, dry-run or validation modes, and `bash -n` checks.
20
+
21
+ Separate diagnosis from mutation. Keep dangerous host operations explicit and reversible.
@@ -0,0 +1,24 @@
1
+ ---
2
+ id: system-maintenance
3
+ label: System Maintenance
4
+ description: Diagnose and repair local system, shell, package, Docker, conda, Python, R, and toolchain problems.
5
+ triggers:
6
+ - system
7
+ - install
8
+ - package
9
+ - docker
10
+ - conda
11
+ - environment
12
+ - fix computer
13
+ - error
14
+ tools:
15
+ - run_command
16
+ - write_file
17
+ - web_search
18
+ - send_to_canvas
19
+ ---
20
+ # System Maintenance
21
+
22
+ Diagnose first with read-only commands. Prefer Docker/project-local scripts and `/aginti-env` for broad toolchain setup. Host-level sudo or destructive operations require explicit user approval outside normal automation.
23
+
24
+ For complicated setup, write idempotent scripts, run syntax checks, and report exact next safe command.
@@ -0,0 +1,26 @@
1
+ ---
2
+ id: website-app
3
+ label: Website And App Builder
4
+ description: Build polished websites, web apps, landing pages, dashboards, and local previews.
5
+ triggers:
6
+ - website
7
+ - web app
8
+ - landing page
9
+ - frontend
10
+ - dashboard
11
+ - react
12
+ - html
13
+ tools:
14
+ - inspect_project
15
+ - write_file
16
+ - apply_patch
17
+ - run_command
18
+ - preview_workspace
19
+ - open_workspace_file
20
+ - send_to_canvas
21
+ ---
22
+ # Website And App Builder
23
+
24
+ Create real files, not only advice. For new UI, choose a clear visual direction, responsive layout, purposeful typography, and meaningful content. For existing apps, preserve conventions.
25
+
26
+ Use package scripts when available. Preview with `preview_workspace` or `open_workspace_file`; avoid transient Docker localhost loops.
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: word-documents
3
+ label: Microsoft Word Documents
4
+ description: Edit, convert, inspect, or generate Word-style documents using available local tools.
5
+ triggers:
6
+ - word
7
+ - docx
8
+ - microsoft word
9
+ - office
10
+ - pandoc
11
+ - libreoffice
12
+ tools:
13
+ - read_file
14
+ - write_file
15
+ - run_command
16
+ - send_to_canvas
17
+ ---
18
+ # Microsoft Word Documents
19
+
20
+ Prefer safe conversions through available tools such as `pandoc`, `libreoffice`, or Python libraries when installed. Preserve originals; write converted or edited outputs to a new file unless overwrite is explicit.
21
+
22
+ If binary `.docx` content cannot be inspected directly, explain the needed converter and create a project-local script or setup note.
@@ -22,6 +22,7 @@ import { engineeringGuidanceForTask } from "./engineering-guidance.js";
22
22
  import { searchWeb } from "./web-search.js";
23
23
  import { runParallelScouts, shouldRunParallelScouts } from "./parallel-scouts.js";
24
24
  import { readProjectInstructions } from "./project.js";
25
+ import { formatSkillsForPrompt, selectSkillsForGoal } from "./skill-library.js";
25
26
 
26
27
  const exec = promisify(execCallback);
27
28
  const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
@@ -252,6 +253,8 @@ async function createInitialState(config, sessionId) {
252
253
  const now = new Date().toISOString();
253
254
  const taskProfile = getTaskProfile(config.taskProfile);
254
255
  const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
256
+ const selectedSkills = selectSkillsForGoal(config.goal, { taskProfile: config.taskProfile, limit: 6 });
257
+ const skillContext = formatSkillsForPrompt(selectedSkills);
255
258
  const projectInstructions = await readProjectInstructions(config.baseDir || config.commandCwd || process.cwd());
256
259
  const projectInstructionContext = formatProjectInstructions(projectInstructions);
257
260
  return {
@@ -272,6 +275,7 @@ async function createInitialState(config, sessionId) {
272
275
  truncated: projectInstructions.truncated,
273
276
  loadedAt: now,
274
277
  },
278
+ selectedSkills: selectedSkills.map((skill) => skill.id),
275
279
  },
276
280
  chat: [
277
281
  {
@@ -318,6 +322,7 @@ async function createInitialState(config, sessionId) {
318
322
  ? `Parallel DeepSeek scouts may run before complex execution. Scout count: ${config.parallelScoutCount}.`
319
323
  : "Parallel scouts are disabled.",
320
324
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
325
+ skillContext,
321
326
  engineeringGuidance,
322
327
  "A frontend canvas/artifacts tunnel exists. Use send_to_canvas when important markdown, diffs, screenshots, images, or workspace files should be highlighted in the UI. It is optional and ordinary final text can still go directly to finish.",
323
328
  "For visual-output requests such as draw, plot, graph, chart, diagram, figure, image, or visualization, proactively publish a canvas artifact even when the user does not mention canvas. If workspace file tools are enabled, prefer creating a small SVG or markdown artifact and call send_to_canvas with selected=true.",
@@ -360,6 +365,7 @@ async function createInitialState(config, sessionId) {
360
365
  config.allowWebSearch ? "Web search tool: enabled." : "Web search tool: disabled.",
361
366
  config.allowParallelScouts ? `Parallel scouts: enabled count=${config.parallelScoutCount}.` : "Parallel scouts: disabled.",
362
367
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
368
+ skillContext,
363
369
  engineeringGuidance,
364
370
  "Canvas/artifacts tunnel: available through send_to_canvas for optional frontend rendering.",
365
371
  "Visual-output requests should produce a canvas artifact without requiring the user to ask for canvas explicitly.",
@@ -451,6 +457,8 @@ async function applyContinuationPrompt(state, config, observers) {
451
457
 
452
458
  const taskProfile = getTaskProfile(config.taskProfile);
453
459
  const engineeringGuidance = engineeringGuidanceForTask(config.goal, config.taskProfile);
460
+ const selectedSkills = selectSkillsForGoal(config.goal, { taskProfile: config.taskProfile, limit: 6 });
461
+ const skillContext = formatSkillsForPrompt(selectedSkills);
454
462
  const projectInstructions = await readProjectInstructions(config.baseDir || config.commandCwd || process.cwd());
455
463
  state.meta = state.meta || {};
456
464
  state.meta.projectInstructions = {
@@ -459,6 +467,7 @@ async function applyContinuationPrompt(state, config, observers) {
459
467
  truncated: projectInstructions.truncated,
460
468
  loadedAt: new Date().toISOString(),
461
469
  };
470
+ state.meta.selectedSkills = selectedSkills.map((skill) => skill.id);
462
471
  ensureChatState(state);
463
472
  state.goal = config.goal;
464
473
  state.provider = config.provider;
@@ -485,6 +494,7 @@ async function applyContinuationPrompt(state, config, observers) {
485
494
  ? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
486
495
  : "",
487
496
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
497
+ skillContext,
488
498
  engineeringGuidance,
489
499
  formatProjectInstructions(projectInstructions),
490
500
  "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.",
@@ -9,6 +9,7 @@ import { listTaskProfiles } from "./task-profiles.js";
9
9
  import { listAgentWrappers } from "./tool-wrappers.js";
10
10
  import { listAuxiliarySkills } from "./auxiliary-tools.js";
11
11
  import { readCodebaseMap } from "./codebase-map.js";
12
+ import { listSkills } from "./skill-library.js";
12
13
 
13
14
  const execFileAsync = promisify(execFile);
14
15
 
@@ -218,6 +219,13 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
218
219
  label: profile.label,
219
220
  tools: profile.tools,
220
221
  })),
222
+ skills: listSkills().map((skill) => ({
223
+ id: skill.id,
224
+ label: skill.label,
225
+ description: skill.description,
226
+ triggers: skill.triggers,
227
+ tools: skill.tools,
228
+ })),
221
229
  auxiliarySkills: listAuxiliarySkills().map((skill) => ({
222
230
  id: skill.id,
223
231
  label: skill.label,
package/src/cli.js CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import { listTaskProfiles } from "./task-profiles.js";
18
18
  import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
19
19
  import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
20
+ import { listSkills, selectSkillsForGoal } from "./skill-library.js";
20
21
  import fs from "node:fs/promises";
21
22
  import path from "node:path";
22
23
  import { fileURLToPath } from "node:url";
@@ -64,6 +65,7 @@ export function parseArgs(argv) {
64
65
  port: "",
65
66
  host: "",
66
67
  listProfiles: false,
68
+ listSkills: false,
67
69
  latex: false,
68
70
  image: false,
69
71
  };
@@ -242,6 +244,10 @@ export function parseArgs(argv) {
242
244
  result.listProfiles = true;
243
245
  continue;
244
246
  }
247
+ if (arg === "--list-skills") {
248
+ result.listSkills = true;
249
+ continue;
250
+ }
245
251
  if (arg === "--sandbox-status") {
246
252
  result.sandboxStatus = true;
247
253
  continue;
@@ -261,7 +267,7 @@ export function parseArgs(argv) {
261
267
 
262
268
  function printUsage() {
263
269
  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 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--sandbox-status|--sandbox-preflight] "your task"'
270
+ 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti skills [query] 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..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--list-skills] [--sandbox-status|--sandbox-preflight] "your task"'
265
271
  );
266
272
  }
267
273
 
@@ -320,6 +326,17 @@ function printProfiles() {
320
326
  }
321
327
  }
322
328
 
329
+ function printSkills(query = "") {
330
+ const skills = query
331
+ ? selectSkillsForGoal(query, { taskProfile: "auto", limit: 40, includeBody: false })
332
+ : listSkills({ includeBody: false });
333
+ for (const skill of skills) {
334
+ const triggers = skill.triggers?.length ? ` triggers=${skill.triggers.join(",")}` : "";
335
+ const tools = skill.tools?.length ? ` tools=${skill.tools.join(",")}` : "";
336
+ console.log(`${skill.id}: ${skill.label} - ${skill.description}${triggers}${tools}`);
337
+ }
338
+ }
339
+
323
340
  function printInitResult(result) {
324
341
  console.log(`AgInTiFlow project initialized: ${result.projectRoot}`);
325
342
  console.log(`instructions=${result.instructionsPath}`);
@@ -534,6 +551,11 @@ export async function main(argv = process.argv.slice(2)) {
534
551
  return;
535
552
  }
536
553
 
554
+ if (argv[0] === "skills" || argv[0] === "skill") {
555
+ printSkills(argv.slice(1).join(" ").trim());
556
+ return;
557
+ }
558
+
537
559
  if (argv[0] === "queue") {
538
560
  await handleQueueCommand(argv.slice(1));
539
561
  return;
@@ -592,6 +614,11 @@ export async function main(argv = process.argv.slice(2)) {
592
614
  return;
593
615
  }
594
616
 
617
+ if (args.listSkills) {
618
+ printSkills(args.goal);
619
+ return;
620
+ }
621
+
595
622
  if (args.sandboxStatus || args.sandboxPreflight) {
596
623
  const config = loadConfig({ ...args, goal: args.goal || "sandbox preflight" }, { packageDir });
597
624
  const result = args.sandboxPreflight
@@ -9,6 +9,7 @@ import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles
9
9
  import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
10
10
  import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
11
11
  import { SessionStore } from "./session-store.js";
12
+ import { listSkills, selectSkillsForGoal } from "./skill-library.js";
12
13
 
13
14
  const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
14
15
  const ansi = {
@@ -44,6 +45,8 @@ const SLASH_COMMANDS = [
44
45
  "/new",
45
46
  "/resume",
46
47
  "/sessions",
48
+ "/skills",
49
+ "/skill",
47
50
  "/profile",
48
51
  "/web-search",
49
52
  "/scouts",
@@ -480,6 +483,7 @@ function printHelp() {
480
483
  " /new Start a fresh session on the next message.",
481
484
  " /resume <session-id> Continue a saved session.",
482
485
  " /sessions List recent sessions in this project.",
486
+ " /skills [query] List Markdown skills selected for a topic.",
483
487
  " /profile <name> Set task profile, e.g. code, website, latex, maintenance.",
484
488
  " /web-search on|off Enable or disable the web_search tool.",
485
489
  " /scouts on|off|<1-10> Enable parallel DeepSeek scouts and set scout count.",
@@ -497,7 +501,7 @@ function printHelp() {
497
501
  "Type / then Tab to autocomplete commands.",
498
502
  "While a run is active, Enter pipes a message into the current run (→), Tab queues it after finish (↳).",
499
503
  "Alt+Up edits the last piped message; Shift+Left edits the last queued message.",
500
- "Esc or Ctrl+C stops gracefully and prints a resume command.",
504
+ "Esc is ignored while idle. During a run, Esc waits for pending → pipe messages or stops if none; Ctrl+C always stops.",
501
505
  ].join("\n")
502
506
  );
503
507
  }
@@ -789,6 +793,11 @@ function createAbortError(message = "Aborted with Ctrl+C") {
789
793
  return error;
790
794
  }
791
795
 
796
+ export function classifyEscapeAction({ active = false, pendingAsap = [] } = {}) {
797
+ if (!active) return "noop";
798
+ return Array.isArray(pendingAsap) && pendingAsap.length > 0 ? "wait-for-asap" : "abort";
799
+ }
800
+
792
801
  function readTtyPrompt(options = {}) {
793
802
  return new Promise((resolve, reject) => {
794
803
  emitKeypressEvents(input);
@@ -962,10 +971,7 @@ function readTtyPrompt(options = {}) {
962
971
  return;
963
972
  }
964
973
  if (key.name === "escape") {
965
- buffer = "";
966
- cursor = 0;
967
- preferredColumn = null;
968
- redraw();
974
+ if (classifyEscapeAction({ active: false }) === "noop") return;
969
975
  return;
970
976
  }
971
977
  if (key.ctrl || key.meta) return;
@@ -1188,6 +1194,13 @@ class LiveRunInput {
1188
1194
  return;
1189
1195
  }
1190
1196
  if (key.name === "escape") {
1197
+ const action = classifyEscapeAction({ active: true, pendingAsap: this.pendingAsap });
1198
+ if (action === "wait-for-asap") {
1199
+ const count = this.pendingAsap.length;
1200
+ this.setStatus(`running · waiting to apply ${count} asap pipe message${count === 1 ? "" : "s"}`);
1201
+ this.redraw();
1202
+ return;
1203
+ }
1191
1204
  this.controller.abort(new Error("Interrupted by escape."));
1192
1205
  return;
1193
1206
  }
@@ -1589,6 +1602,30 @@ async function handleCommand(line, state, packageDir) {
1589
1602
  }
1590
1603
  return true;
1591
1604
  }
1605
+ if (command === "skills" || command === "skill") {
1606
+ const skills = value
1607
+ ? selectSkillsForGoal(value, { taskProfile: state.taskProfile, limit: 12, includeBody: false })
1608
+ : listSkills({ includeBody: false });
1609
+ if (skills.length === 0) {
1610
+ printAgentMessage("No matching skills found.");
1611
+ return true;
1612
+ }
1613
+ printAgentMessage(
1614
+ skills
1615
+ .map((skill) =>
1616
+ [
1617
+ `${skill.id}: ${skill.label}`,
1618
+ ` ${skill.description}`,
1619
+ skill.triggers?.length ? ` triggers: ${skill.triggers.join(", ")}` : "",
1620
+ skill.tools?.length ? ` tools: ${skill.tools.join(", ")}` : "",
1621
+ ]
1622
+ .filter(Boolean)
1623
+ .join("\n")
1624
+ )
1625
+ .join("\n\n")
1626
+ );
1627
+ return true;
1628
+ }
1592
1629
  if (command === "profile") {
1593
1630
  state.taskProfile = normalizeTaskProfile(value || "auto");
1594
1631
  state.maxSteps = Math.max(state.maxSteps, defaultMaxStepsForProfile(state.taskProfile));
@@ -3,6 +3,7 @@ import { normalizeWrapperName, wrapperStatusText } from "./tool-wrappers.js";
3
3
  import { getTaskProfile } from "./task-profiles.js";
4
4
  import { listAuxiliarySkills } from "./auxiliary-tools.js";
5
5
  import { engineeringGuidanceForTask } from "./engineering-guidance.js";
6
+ import { formatSkillsForPrompt, selectSkillsForGoal } from "./skill-library.js";
6
7
 
7
8
  export function createClient(config) {
8
9
  if (config.provider === "mock") {
@@ -193,6 +194,8 @@ function mockChatResponse(content, toolCalls = []) {
193
194
  export async function createPlan(client, config, state) {
194
195
  const taskProfile = getTaskProfile(config.taskProfile);
195
196
  const engineeringGuidance = engineeringGuidanceForTask(state.goal, config.taskProfile);
197
+ const selectedSkills = selectSkillsForGoal(state.goal, { taskProfile: config.taskProfile, limit: 5 });
198
+ const skillContext = formatSkillsForPrompt(selectedSkills);
196
199
  const projectInstructions = state.meta?.projectInstructions;
197
200
  if (client.mock) {
198
201
  return [
@@ -242,6 +245,7 @@ export async function createPlan(client, config, state) {
242
245
  ? `Parallel scout notes may be injected before execution for complex tasks. Scout count: ${config.parallelScoutCount}.`
243
246
  : "Parallel scouts are disabled.",
244
247
  `Task profile: ${taskProfile.label}. ${taskProfile.prompt}`,
248
+ skillContext,
245
249
  engineeringGuidance,
246
250
  "A canvas/artifacts tunnel is available through send_to_canvas. Use it when an output should be highlighted visually, such as screenshots, image files, important markdown, diffs, or generated artifact paths. It is optional for ordinary text answers.",
247
251
  "Work like a practical coding agent: inspect when useful, edit with file tools, run safe checks when they add confidence, and keep outputs inside the workspace.",
@@ -0,0 +1,153 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const DEFAULT_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
7
+ const DEFAULT_PROMPT_CHARS = 5200;
8
+
9
+ function parseScalar(value = "") {
10
+ const trimmed = String(value || "").trim();
11
+ if (
12
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
13
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
14
+ ) {
15
+ return trimmed.slice(1, -1);
16
+ }
17
+ return trimmed;
18
+ }
19
+
20
+ function parseFrontmatter(text, filePath) {
21
+ const match = String(text || "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
22
+ if (!match) throw new Error(`${filePath}: missing YAML frontmatter`);
23
+ const meta = {};
24
+ const lines = match[1].split(/\r?\n/);
25
+ for (let index = 0; index < lines.length; index += 1) {
26
+ const line = lines[index];
27
+ if (!line.trim() || line.trim().startsWith("#")) continue;
28
+ const scalar = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
29
+ if (!scalar) throw new Error(`${filePath}: invalid YAML line: ${line}`);
30
+ const key = scalar[1];
31
+ const value = scalar[2];
32
+ if (value.trim()) {
33
+ meta[key] = parseScalar(value);
34
+ continue;
35
+ }
36
+ const items = [];
37
+ while (index + 1 < lines.length && /^\s+-\s+/.test(lines[index + 1])) {
38
+ index += 1;
39
+ items.push(parseScalar(lines[index].replace(/^\s+-\s+/, "")));
40
+ }
41
+ meta[key] = items;
42
+ }
43
+ return {
44
+ meta,
45
+ body: String(text || "").slice(match[0].length).trim(),
46
+ };
47
+ }
48
+
49
+ function normalizeList(value) {
50
+ if (Array.isArray(value)) return value.map((item) => String(item || "").trim()).filter(Boolean);
51
+ if (!value) return [];
52
+ return String(value)
53
+ .split(",")
54
+ .map((item) => item.trim())
55
+ .filter(Boolean);
56
+ }
57
+
58
+ function loadSkillFile(filePath) {
59
+ const text = fs.readFileSync(filePath, "utf8");
60
+ const { meta, body } = parseFrontmatter(text, filePath);
61
+ for (const field of ["id", "label", "description"]) {
62
+ if (typeof meta[field] !== "string" || !meta[field].trim()) {
63
+ throw new Error(`${filePath}: ${field} must be a non-empty string`);
64
+ }
65
+ }
66
+ return {
67
+ id: meta.id.trim(),
68
+ label: meta.label.trim(),
69
+ description: meta.description.trim(),
70
+ triggers: normalizeList(meta.triggers),
71
+ tools: normalizeList(meta.tools),
72
+ body,
73
+ path: filePath,
74
+ };
75
+ }
76
+
77
+ export function listSkills({ includeBody = false, skillsDir = DEFAULT_SKILLS_DIR } = {}) {
78
+ let dirEntries = [];
79
+ try {
80
+ dirEntries = fs.readdirSync(skillsDir, { withFileTypes: true });
81
+ } catch {
82
+ return [];
83
+ }
84
+ const skills = [];
85
+ for (const entry of dirEntries) {
86
+ if (!entry.isDirectory()) continue;
87
+ const skillPath = path.join(skillsDir, entry.name, "SKILL.md");
88
+ try {
89
+ const skill = loadSkillFile(skillPath);
90
+ if (!includeBody) delete skill.body;
91
+ skills.push(skill);
92
+ } catch {
93
+ // Invalid local skill files are skipped so one bad skill does not break the agent.
94
+ }
95
+ }
96
+ return skills.sort((a, b) => a.id.localeCompare(b.id));
97
+ }
98
+
99
+ function scoreSkill(skill, text, taskProfile) {
100
+ let score = 0;
101
+ if (skill.id === taskProfile) score += 10;
102
+ if (skill.triggers.includes(taskProfile)) score += 6;
103
+ for (const trigger of skill.triggers) {
104
+ const needle = trigger.toLowerCase();
105
+ if (needle && text.includes(needle)) score += Math.max(2, Math.min(6, Math.ceil(needle.length / 6)));
106
+ }
107
+ for (const token of skill.description.toLowerCase().split(/[^a-z0-9+#.-]+/).filter((item) => item.length > 3)) {
108
+ if (text.includes(token)) score += 0.25;
109
+ }
110
+ return score;
111
+ }
112
+
113
+ export function selectSkillsForGoal(goal = "", { taskProfile = "auto", limit = 6, includeBody = true } = {}) {
114
+ const text = `${goal} ${taskProfile}`.toLowerCase();
115
+ const skills = listSkills({ includeBody });
116
+ const scored = skills
117
+ .map((skill) => ({ skill, score: scoreSkill(skill, text, taskProfile) }))
118
+ .filter((item) => item.score > 0)
119
+ .sort((a, b) => b.score - a.score || a.skill.id.localeCompare(b.skill.id))
120
+ .map((item) => item.skill);
121
+ return scored.slice(0, Math.max(1, limit));
122
+ }
123
+
124
+ function compactBody(body = "", limit = 620) {
125
+ const text = String(body || "")
126
+ .replace(/^# .+$/gm, "")
127
+ .replace(/\n{3,}/g, "\n\n")
128
+ .trim();
129
+ if (text.length <= limit) return text;
130
+ return `${text.slice(0, limit - 20).trim()}\n...`;
131
+ }
132
+
133
+ export function formatSkillsForPrompt(skills = [], { maxChars = DEFAULT_PROMPT_CHARS } = {}) {
134
+ if (!Array.isArray(skills) || skills.length === 0) return "";
135
+ const chunks = [
136
+ "Selected AgInTiFlow skills. A skill is Markdown guidance for when and how to use tools; it is not itself a tool. Follow relevant skill guidance without becoming constrained by it.",
137
+ ];
138
+ for (const skill of skills) {
139
+ chunks.push(
140
+ [
141
+ `## ${skill.id}: ${skill.label}`,
142
+ `Description: ${skill.description}`,
143
+ skill.tools?.length ? `Preferred tools: ${skill.tools.join(", ")}` : "",
144
+ skill.body ? compactBody(skill.body) : "",
145
+ ]
146
+ .filter(Boolean)
147
+ .join("\n")
148
+ );
149
+ }
150
+ const output = chunks.join("\n\n");
151
+ if (output.length <= maxChars) return output;
152
+ return `${output.slice(0, Math.max(maxChars - 80, 1)).trim()}\n... [skills truncated]`;
153
+ }
package/web.js CHANGED
@@ -14,6 +14,7 @@ import { summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./src/workspace-t
14
14
  import { listTaskProfiles, normalizeTaskProfile } from "./src/task-profiles.js";
15
15
  import { loadProjectEnv, projectPaths, providerKeyStatus, setProviderKey } from "./src/project.js";
16
16
  import { buildCapabilityReport } from "./src/capabilities.js";
17
+ import { listSkills } from "./src/skill-library.js";
17
18
  import {
18
19
  buildArtifacts,
19
20
  countUnreadArtifacts,
@@ -574,6 +575,7 @@ app.get("/api/config", async (_req, res) => {
574
575
  maxSteps: 24,
575
576
  },
576
577
  taskProfiles: listTaskProfiles(),
578
+ skills: listSkills(),
577
579
  routing: {
578
580
  modes: ["smart", "fast", "complex", "manual"],
579
581
  presets: getModelPresets(),