diff --git a/cli.py b/cli.py index 4a32e2998998..a0e4df9f2227 100644 --- a/cli.py +++ b/cli.py @@ -9462,6 +9462,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): self._handle_skills_command(cmd_original) elif canonical == "learn": self._handle_learn_command(cmd_original) + elif canonical == "init": + self._handle_init_command(cmd_original) elif canonical == "memory": self._handle_memory_command(cmd_original) elif canonical == "platforms": diff --git a/gateway/run.py b/gateway/run.py index 1c6a8a839738..b14b4cdda81a 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -11792,6 +11792,34 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew except Exception: return "Could not start /learn — please try again." + if canonical == "init": + # /init: rewrite the turn to a guidance-laden prompt and fall + # through to normal agent processing (same fall-through as /learn + # so role alternation is preserved). The live agent scans the + # project with its own read-only tools and writes/updates + # AGENTS.md via write_file. No engine, works on any backend. + from hermes_cli.init_command import build_init_prompt_for_cwd + + _init_notes = event.get_command_args().strip() + try: + _init_prompt = build_init_prompt_for_cwd(extra=_init_notes) + except Exception: + return "Could not start /init — please try again." + _ack = ( + "Updating AGENTS.md from a project scan…" + if "UPDATE the existing AGENTS.md" in _init_prompt + else "Generating AGENTS.md from a project scan…" + ) + try: + adapter = self._adapter_for_source(source) + if adapter: + _ack_meta = self._thread_metadata_for_source(source) + await adapter.send(str(source.chat_id), _ack, metadata=_ack_meta) + except Exception: + logger.debug("init ack send failed", exc_info=True) + event.text = _init_prompt + # fall through to agent processing + if canonical == "fast": return await self._handle_fast_command(event) diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index d6d6d7e135e6..d515acf4a110 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -1642,6 +1642,32 @@ class CLICommandsMixin: else: # pragma: no cover - defensive (no live input loop) print(" /learn needs an active chat session to run.") + def _handle_init_command(self, cmd: str): + """Handle /init — generate or update AGENTS.md from a project scan. + + Mirrors /learn: build a guidance-laden prompt and inject it onto the + agent's input queue as a normal user turn. The live agent scans the + project with its own read-only tools and writes/updates AGENTS.md via + ``write_file``. No engine, no model-tool footprint, works on any + terminal backend, and preserves prompt-cache invariants (no system + prompt or history mutation). + """ + from hermes_cli.init_command import build_init_prompt_for_cwd + + # Everything after the command word is optional user emphasis. + parts = cmd.strip().split(None, 1) + extra = parts[1].strip() if len(parts) > 1 else "" + + msg = build_init_prompt_for_cwd(extra=extra) + if "UPDATE the existing AGENTS.md" in msg: + print("\n⚡ Updating AGENTS.md from a project scan...") + else: + print("\n⚡ Generating AGENTS.md from a project scan...") + if hasattr(self, "_pending_input"): + self._pending_input.put(msg) + else: # pragma: no cover - defensive (no live input loop) + print(" /init needs an active chat session to run.") + def _handle_memory_command(self, cmd: str): """Handle /memory slash command — pending review + approval-gate toggle.""" from hermes_cli.write_approval_commands import handle_pending_subcommand diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index bad2250da009..87c20087013a 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -196,6 +196,8 @@ COMMAND_REGISTRY: list[CommandDef] = [ "Tools & Skills", cli_only=True, aliases=("generate-pet",), args_hint="[description]"), CommandDef("learn", "Learn a reusable skill from anything you describe (dirs, URLs, this chat, notes)", "Tools & Skills", args_hint=""), + CommandDef("init", "Generate or update AGENTS.md project instructions from a repo scan", + "Tools & Skills", args_hint="[notes]"), CommandDef("cron", "Manage scheduled tasks", "Tools & Skills", cli_only=True, args_hint="[subcommand]", subcommands=("list", "add", "create", "edit", "pause", "resume", "run", "remove")), @@ -1172,7 +1174,10 @@ _SLACK_PRIORITY_ALIASES = ("btw", "bg") # displacing existing native Slack slash commands at the 50-command cap. # - debug: the log/report upload surface; reached via /hermes debug on Slack. # - egress: Docker-only proxy status; reachable as /hermes egress on Slack. -_SLACK_VIA_HERMES_ONLY = frozenset({"topup", "moa", "debug", "egress"}) +# - init: repo-scan AGENTS.md bootstrap — a cwd-centric dev command that is +# rare from Slack; reachable as /hermes init. Without this entry, adding +# /init clamps /version off the native list and breaks Telegram parity. +_SLACK_VIA_HERMES_ONLY = frozenset({"topup", "moa", "debug", "egress", "init"}) def _sanitize_slack_name(raw: str) -> str: diff --git a/hermes_cli/init_command.py b/hermes_cli/init_command.py new file mode 100644 index 000000000000..576b84fbbc44 --- /dev/null +++ b/hermes_cli/init_command.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""``/init`` — build the prompt that generates or updates a project AGENTS.md. + +Port of Codex ``/init`` (Claude Code has the same for CLAUDE.md). Hermes +already *loads* AGENTS.md / CLAUDE.md / .cursorrules as project context, but +had no command to bootstrap one. ``/init`` hands the live agent ONE +guidance-laden prompt instructing it to: + + 1. Inspect the project with its own read-only tools (``read_file`` / + ``search_files`` on manifests, CI configs, lockfiles, existing docs) to + learn the layout, toolchain, and the exact build/test/lint commands. + 2. Write a CONCISE ``AGENTS.md`` (target under 100 lines) with the sections + an agent actually needs — overview, setup, commands, conventions, + pitfalls — not an essay. + 3. If an AGENTS.md already exists, UPDATE it: preserve the user's existing + content and merge in what's missing, never blow it away. + +There is no engine and no model-tool footprint: the agent does the work with +its existing toolset, so this works identically on local, Docker, and remote +terminal backends. Every surface (CLI ``/init``, gateway ``/init``, TUI +``/init``) calls :func:`build_init_prompt` and feeds the result to the agent +as a normal user turn — the same prompt-injection pattern as ``/learn`` and +``/blueprint``, which preserves prompt-cache invariants (no system-prompt or +history mutation). +""" + +from __future__ import annotations + +# The quality bar, embedded in every prompt so the generated file reads like a +# maintainer wrote it — concrete and command-exact, not generic advice. +_QUALITY_BAR = """\ +Quality bar for the file you write (this is what separates a useful AGENTS.md +from noise): +- CONCISE: target under 100 lines. Agents load this file every session — every + line costs context. No essays, no marketing prose, no filler. +- Commands must be EXACT invocations you verified from the repo (package.json + scripts, Makefile targets, pyproject/tox/CI config, existing docs). Write + `npm run test:unit` or `scripts/run_tests.sh tests/foo`, never "run the + tests". NEVER invent a command you didn't see evidence for. +- No generic advice. "Write tests for new code" and "follow best practices" + are banned — if a line would be true of any repo, cut it. +- Conventions must be OBSERVED, not assumed: naming patterns, module layout, + error-handling style, commit-message format — only what the code actually + shows. +- Include pitfalls that would genuinely trip up a newcomer or an agent + (required env vars, generated files not to hand-edit, slow test suites, + ports already in use), if you found any. Skip the section if you found none. +- Markdown structure: a short title + one-paragraph overview, then focused + sections (e.g. "Dev environment", "Build & test", "Conventions", + "Pitfalls"). Flat and scannable — no deep nesting.""" + + +def build_init_prompt( + cwd: str, + existing_file: str | None = None, + extra: str = "", +) -> str: + """Build the agent prompt for a ``/init`` request. + + Args: + cwd: the project directory the agent should scan and write + ``AGENTS.md`` into (usually the session working directory). + existing_file: the current content of ``AGENTS.md`` if one already + exists, else ``None``. When present the prompt switches to + update-and-merge discipline instead of fresh generation. + extra: free-text the user gave after ``/init`` — emphasis or notes to + honor while authoring (e.g. "focus on the test setup"). + + Returns: + A complete instruction the agent runs as a normal turn. + """ + extra = (extra or "").strip() + + parts: list[str] = [ + "[/init] The user wants you to " + + ( + "UPDATE the existing AGENTS.md project-instructions file" + if existing_file is not None + else "generate an AGENTS.md project-instructions file" + ) + + f" for the project at: {cwd}\n", + "AGENTS.md is the instruction file coding agents (Hermes included) " + "load as project context every session. It should teach an agent how " + "to work in THIS repo: what the project is, how to set up, the exact " + "build/test/lint commands, the conventions the code actually follows, " + "and the pitfalls that waste time.\n", + "Do this:\n" + "1. Inspect the project with your read-only tools (`read_file`, " + "`search_files`) — start with manifests and toolchain files " + "(package.json, pyproject.toml, Cargo.toml, go.mod, Makefile, " + "CI workflow configs, lockfiles), then the directory layout, existing " + "README/docs, and test/lint configuration. Learn the real commands, " + "don't guess them.\n" + "2. Write the file to " + f"{cwd.rstrip('/')}/AGENTS.md with `write_file`" + + ( + " — but this is an UPDATE, so follow the merge discipline below." + if existing_file is not None + else "." + ) + + "\n" + "3. Confirm to the user the exact path you wrote and summarize in one " + "or two lines what the file covers.\n", + ] + + if existing_file is not None: + parts.append( + "MERGE DISCIPLINE — an AGENTS.md already exists (its current " + "content is below). Do NOT overwrite or regenerate it from " + "scratch. Preserve the user's existing content — their wording, " + "their sections, their rules — and merge in only what is missing " + "or verifiably stale (e.g. a command that no longer exists in the " + "repo). When existing content conflicts with what you observed, " + "prefer minimal surgical edits over rewrites, and keep the " + "user's intent. The result must still meet the quality bar.\n\n" + "CURRENT AGENTS.md CONTENT:\n" + "<< str: + """Convenience wrapper used by the dispatch surfaces. + + Resolves ``cwd`` (defaults to the process working directory), reads an + existing ``AGENTS.md`` there if present, and returns the full prompt. + """ + import os + + resolved = os.path.abspath(cwd or os.getcwd()) + existing: str | None = None + agents_path = os.path.join(resolved, "AGENTS.md") + try: + if os.path.isfile(agents_path): + with open(agents_path, encoding="utf-8", errors="replace") as fh: + existing = fh.read() + except OSError: + existing = None + return build_init_prompt(resolved, existing_file=existing, extra=extra) diff --git a/tests/hermes_cli/test_init_command.py b/tests/hermes_cli/test_init_command.py new file mode 100644 index 000000000000..2123f3ce4dd5 --- /dev/null +++ b/tests/hermes_cli/test_init_command.py @@ -0,0 +1,117 @@ +"""Tests for /init — generate or update AGENTS.md from a project scan. + +Covers the shared prompt builder (hermes_cli.init_command.build_init_prompt) +and the slash-command registry wiring. /init has no engine and no model tool: +it builds a guidance-laden prompt that the live agent runs as a normal turn +(the /learn pattern), so these are the load-bearing behavior contracts. +""" + +from hermes_cli.init_command import ( + _QUALITY_BAR, + build_init_prompt, + build_init_prompt_for_cwd, +) + + +class TestBuildInitPrompt: + def test_includes_the_cwd(self): + prompt = build_init_prompt("/home/alice/projects/acme") + assert "/home/alice/projects/acme" in prompt + + def test_mentions_agents_md(self): + prompt = build_init_prompt("/tmp/proj") + assert "AGENTS.md" in prompt + + def test_fresh_generation_when_no_existing_file(self): + prompt = build_init_prompt("/tmp/proj", existing_file=None) + assert "generate an AGENTS.md" in prompt + # No merge discipline block for a fresh file. + assert "MERGE DISCIPLINE" not in prompt + + def test_merge_not_overwrite_when_existing_file_passed(self): + existing = "# My Project\n\nAlways run `make lint` before committing.\n" + prompt = build_init_prompt("/tmp/proj", existing_file=existing) + low = prompt.lower() + # Update mode, with explicit merge-not-overwrite discipline. + assert "UPDATE the existing AGENTS.md" in prompt + assert "merge" in low + assert "do not overwrite" in low or "not overwrite" in low + assert "preserve" in low + # And it carries the current content so the agent can merge. + assert existing.strip() in prompt + + def test_empty_existing_file_still_triggers_update_mode(self): + # An existing-but-empty AGENTS.md is still an update, not a fresh gen + # (None means "no file"; "" means "empty file"). + prompt = build_init_prompt("/tmp/proj", existing_file="") + assert "UPDATE the existing AGENTS.md" in prompt + + def test_includes_extra_notes_verbatim(self): + notes = "focus on the test setup, and mention the flaky e2e suite" + prompt = build_init_prompt("/tmp/proj", extra=notes) + assert notes in prompt + + def test_no_notes_section_when_extra_empty(self): + assert "USER NOTES" not in build_init_prompt("/tmp/proj", extra=" ") + assert "USER NOTES" in build_init_prompt("/tmp/proj", extra="hi") + + def test_always_includes_the_quality_bar(self): + for existing in (None, "# old content"): + prompt = build_init_prompt("/tmp/proj", existing_file=existing) + assert _QUALITY_BAR in prompt + + def test_quality_bar_demands_exact_commands_and_conciseness(self): + low = _QUALITY_BAR.lower() + assert "100 lines" in low + assert "never invent" in low + assert "no generic advice" in low + + def test_references_read_only_scan_tools(self): + prompt = build_init_prompt("/tmp/proj") + for tool in ("read_file", "search_files", "write_file"): + assert tool in prompt + + +class TestBuildInitPromptForCwd: + def test_defaults_to_process_cwd(self, tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + prompt = build_init_prompt_for_cwd() + assert str(tmp_path) in prompt + assert "generate an AGENTS.md" in prompt + + def test_reads_existing_agents_md(self, tmp_path): + (tmp_path / "AGENTS.md").write_text( + "# Existing\n\nRun `tox -e py311`.\n", encoding="utf-8" + ) + prompt = build_init_prompt_for_cwd(cwd=str(tmp_path)) + assert "UPDATE the existing AGENTS.md" in prompt + assert "Run `tox -e py311`." in prompt + + def test_passes_extra_through(self, tmp_path): + prompt = build_init_prompt_for_cwd(cwd=str(tmp_path), extra="keep it short") + assert "keep it short" in prompt + + +class TestInitRegistryWiring: + def test_init_is_registered_and_resolves(self): + from hermes_cli.commands import resolve_command + + cmd = resolve_command("init") + assert cmd is not None + assert cmd.name == "init" + + def test_init_is_in_tools_and_skills_category(self): + from hermes_cli.commands import resolve_command + + assert resolve_command("init").category == "Tools & Skills" + + def test_init_works_on_the_gateway(self): + # /init is a both-surfaces command like /learn, not CLI-only. + from hermes_cli.commands import GATEWAY_KNOWN_COMMANDS + + assert "init" in GATEWAY_KNOWN_COMMANDS + + def test_init_is_not_cli_only(self): + from hermes_cli.commands import resolve_command + + assert not resolve_command("init").cli_only diff --git a/tui_gateway/server.py b/tui_gateway/server.py index f90350d2ec82..622097bbd1a1 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -14987,6 +14987,7 @@ _PENDING_INPUT_COMMANDS: frozenset[str] = frozenset( "moa", "undo", "learn", + "init", "compress", "compact", } @@ -15338,6 +15339,14 @@ def _(rid, params: dict) -> dict: from agent.learn_prompt import build_learn_prompt return _ok(rid, {"type": "send", "message": build_learn_prompt(arg)}) + if name == "init": + # Generate-or-update AGENTS.md: build the guidance-laden prompt and + # submit it as a normal agent turn (same pattern as /learn). The live + # agent scans the project with its own read-only tools and writes or + # merge-updates AGENTS.md via write_file. Works on any backend. + from hermes_cli.init_command import build_init_prompt_for_cwd + + return _ok(rid, {"type": "send", "message": build_init_prompt_for_cwd(extra=arg)}) if name == "moa": # /moa is one-shot sugar only: run a single prompt through the default # MoA preset, then restore the prior model. To *switch* to a MoA preset diff --git a/website/docs/reference/slash-commands.md b/website/docs/reference/slash-commands.md index ce78b22e386a..38622d4e5078 100644 --- a/website/docs/reference/slash-commands.md +++ b/website/docs/reference/slash-commands.md @@ -95,6 +95,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in | `/memory [pending\|approve\|reject\|approval]` | Review pending memory writes staged by the write-approval gate (`memory.write_approval`) and toggle the gate. See [Controlling memory writes](/user-guide/features/memory#controlling-memory-writes-write_approval). | | `/bundles` | List configured skill bundles — `/` slash aliases that preload several skills at once. Configure under `bundles:` in `~/.hermes/config.yaml`. See [Skill Bundles](/user-guide/features/skills#skill-bundles). | | `/learn ` | Distill a reusable skill from anything you describe — a directory, a URL, the workflow you just walked the agent through, or pasted notes. Open-ended: the agent gathers the sources with its own tools and authors a `SKILL.md` following the house authoring standards. Works in the CLI, the messaging gateway, the TUI, and the dashboard Skills page. | +| `/init [notes]` | Generate or update `AGENTS.md` project instructions from a repo scan (port of Codex `/init`). The agent inspects manifests, layout, and toolchain configs with its read-only tools, then writes a concise `AGENTS.md` — or, if one exists, merge-updates it preserving your content. Optional notes steer the emphasis. Works in the CLI, the messaging gateway, and the TUI. | | `/cron` | Manage scheduled tasks (list, add/create, edit, pause, resume, run, remove) | | `/suggestions [accept\|dismiss N\|catalog\|clear]` (alias: `/suggest`) | Review suggested automations. Use `/suggestions` to list pending suggestions, `/suggestions accept ` to create the proposed automation, `/suggestions dismiss ` to reject one, `/suggestions catalog` to add curated starter automations, and `/suggestions clear` to clear resolved suggestion records. Accepted jobs preserve the current surface as the delivery origin. | | `/blueprint [name] [slot=value ...]` (alias: `/bp`) | Set up an automation from a blueprint template. Bare `/blueprint` lists the catalog; `/blueprint ` starts a guided slot-filling flow on the next agent turn; `/blueprint slot=value ...` creates the job directly. | @@ -259,7 +260,7 @@ The messaging gateway supports the following built-in commands inside Telegram, - `/skills` is **CLI-only for search/browse/install**; its write-approval review subcommands (`pending`, `approve`, `reject`, `diff`, `approval`) also work on messaging platforms when `skills.write_approval` is on. `/memory` works on **both** surfaces. - `/verbose` is **CLI-only by default**, but can be enabled for messaging platforms by setting `display.tool_progress_command: true` in `config.yaml`. When enabled, it cycles the `display.tool_progress` mode and saves to config. - `/sethome`, `/update`, `/restart`, `/approve`, `/deny`, `/topic`, `/platform`, and `/commands` are **messaging-only** commands. -- `/status`, `/egress`, `/version`, `/background`, `/queue`, `/steer`, `/voice`, `/reload-mcp`, `/reload-skills`, `/rollback`, `/debug`, `/fast`, `/footer`, `/curator`, `/kanban`, `/credits`, `/suggestions`, `/blueprint`, `/learn`, `/sessions`, and `/yolo` work in **both** the CLI and the messaging gateway. +- `/status`, `/egress`, `/version`, `/background`, `/queue`, `/steer`, `/voice`, `/reload-mcp`, `/reload-skills`, `/rollback`, `/debug`, `/fast`, `/footer`, `/curator`, `/kanban`, `/credits`, `/suggestions`, `/blueprint`, `/learn`, `/init`, `/sessions`, and `/yolo` work in **both** the CLI and the messaging gateway. - `/voice join`, `/voice channel`, and `/voice leave` are only meaningful on Discord. - In the TUI, `/sessions` shows live sessions in the current TUI process. Use `/resume [name]` or `hermes --tui --resume ` for saved or closed transcripts.