Add framework prompts to Commands dashboard and clarify setup guidance

- Expose built-in framework prompts alongside project commands/ files
  in the /api/commands endpoint and Commands.jsx UI, grouped by owner.
- Tighten setup.md so approvals aren't re-asked for explicitly requested
  items and agent.md is written without confirmation.
- Document commands/ folders and module portability rules in
  framework-instructions.md; trim duplicated conventions from
  docs/modules.md in favor of that single source.
This commit is contained in:
bernatsampera
2026-08-04 19:20:51 +02:00
parent 3cf2441552
commit bc51b04c31
7 changed files with 97 additions and 46 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ This principle removed two features in sequence. An early version shipped a ~230
There is no database, no manifest system, no type registry. An agent is a directory: markdown for context, YAML for config, an env file for secret values. If you can create a folder and put a markdown file in it, you can extend the agent. The whole thing versions with git, which means agent state gets diffs, history, review, and rollback for free.
The predecessor of this design had typed modules (integration / task / workflow) with manifest files. It was rejected: the classification created decision paralysis ("is this a task or a workflow?") and the manifests had too many fields. A folder is a folder. Progressive complexity instead: start with markdown (knowledge), add a `connection.yaml` when you need secrets (integration), add a `flow.yaml` when work has steps (process).
The predecessor of this design had typed modules (integration / task / workflow) with manifest files. It was rejected: the classification created decision paralysis ("is this a task or a workflow?") and the manifests had too many fields. A folder is a folder. Progressive complexity instead: start with markdown (knowledge), add a `connection.yaml` when you need secrets (integration), add scripts and commands when procedures prove themselves.
## Config is YAML, content is markdown, behavior is scripts
+1 -9
View File
@@ -48,15 +48,7 @@ There is no enforced schema beyond `index.md`. Different modules have different
## How a module grows
Nothing is enforced in code; these are the conventions the framework instructions push to every connected agent. Retrieval in gcontext is `list_dir` and `grep`, no index and no search, so the tree itself is the index. The rules keep it navigable:
- One topic per module. When a second topic appears, it is a second module, not a subfolder.
- A folder's `index.md` is its map: what the folder holds, plus one line per child file or subfolder. A reader (human or agent) should know where to go after reading only the `index.md`.
- Stay flat until several files share a clear sub-topic. Then make one subfolder per sub-topic (`playbooks/`, `logs/`, `scripts/`), and give it its own `index.md` if it holds more than a handful of files. Never create folders for dates or counts; `logs/2026/08/` hides content that one append-only file with a stated format holds better.
- Soft limits, not caps: keep a single listing under a couple dozen entries, and nesting within about three levels below `modules/`. Passing them is a signal to reorganize, not an error.
- Split a file when it stops being readable in one pass, not before. Many small fragments cost more round-trips than one coherent file.
The soft limits deliberately replace harder rules from an earlier design (a fixed maximum of files per level): agents follow numeric caps literally and produce premature subfolders. Judgment plus a self-describing `index.md` scales further.
Nothing is enforced in code. The conventions (one topic per module, index.md as the folder's map, stay flat until sub-topics emerge, split files only when they stop being readable in one pass) are pushed to every connected agent by the framework instructions (`gcontext/prompts/framework-instructions.md` in the package); that file is the single source for them.
## How someone uses a module
+11
View File
@@ -123,6 +123,17 @@ def discover(root: Path) -> list[Path]:
)
_FRAMEWORK_SKIP = {"framework-instructions", "resources", "README"}
def discover_framework_prompts() -> list[Path]:
"""Framework-shipped prompt files (same filter as register_framework_prompts)."""
prompts_dir = Path(__file__).parent / "prompts"
return sorted(
p for p in prompts_dir.glob("*.md") if p.stem not in _FRAMEWORK_SKIP
)
def register_framework_prompts(mcp) -> int:
"""Register the framework's own prompts, shipped in the package.
+26 -23
View File
@@ -93,6 +93,28 @@ async def api_modules(request: Request) -> JSONResponse:
return JSONResponse(result)
def _command_entry(path: Path, owner: str, rel: str, kind: str) -> dict:
entry = {"owner": owner, "name": f"{owner}__{path.stem}", "kind": kind, "path": rel}
try:
text = path.read_text(encoding="utf-8")
if path.suffix == ".md":
meta, _ = commands_mod.parse_command(text)
else:
meta = commands_mod.parse_script_command(text)
entry["description"] = meta.get("description", "")
entry["args"] = [
{
"name": p.get("name", "?"),
"description": p.get("description", ""),
"required": bool(p.get("required", False)),
}
for p in (meta.get("parameters") or [])
]
except (ValueError, KeyError, yaml.YAMLError) as e:
entry["error"] = str(e)
return entry
@mcp.custom_route("/api/commands", methods=["GET"])
async def api_commands(request: Request) -> JSONResponse:
root = _root()
@@ -100,29 +122,10 @@ async def api_commands(request: Request) -> JSONResponse:
for path in commands_mod.discover(root):
rel = str(path.relative_to(root))
owner = path.parent.parent.name
entry = {
"owner": owner,
"name": f"{owner}__{path.stem}",
"kind": path.suffix.lstrip("."),
"path": rel,
}
try:
text = path.read_text(encoding="utf-8")
if path.suffix == ".md":
meta, _ = commands_mod.parse_command(text)
else:
meta = commands_mod.parse_script_command(text)
entry["description"] = meta.get("description", "")
entry["args"] = [
{
"name": p.get("name", "?"),
"description": p.get("description", ""),
"required": bool(p.get("required", False)),
}
for p in (meta.get("parameters") or [])
]
except (ValueError, KeyError, yaml.YAMLError) as e:
entry["error"] = str(e)
result.append(_command_entry(path, owner, rel, path.suffix.lstrip(".")))
for path in commands_mod.discover_framework_prompts():
entry = _command_entry(path, "framework", f"gcontext/prompts/{path.name}", "md")
entry["name"] = path.stem
result.append(entry)
return JSONResponse(result)
@@ -17,9 +17,28 @@ How the folder is organized:
practice. Read the index.md before writing a script against a service, and
update it when you learn something worth keeping.
- modules/<name>/: accumulated knowledge on a topic, entry point index.md.
Modules are portable: another agent can use one by copying the folder. So
keep a module connection-agnostic in its process files (say "the payment
provider", not "Stripe"); the agent finds the concrete service in
connections/ at run time. Company-specific facts learned while working
(playbooks, logs) are fine; hard-wired service names in the steps are not.
- scripts/ folders (inside connections and modules): proven procedures. Run
them by path with run_script instead of rewriting them, and save a script
there once it has proven itself.
- commands/ folders (inside connections and modules): user-invokable entry
points, exposed as MCP prompts (slash commands in Claude Code, named
/mcp__<server>__<owner>__<command>). Two file types:
- <name>.md: a prompt command. Starts with a `---` YAML frontmatter block
holding `description` and optional `parameters` (list of `name`,
`description`, `required`); the body is injected into the conversation
with `$name` placeholders filled from the arguments.
- <name>.py: a script command. Starts with the same frontmatter as a
`# ---` comment block; invoking it tells the agent to run the file via
run_script with the arguments as params.
When the user asks for a reusable command or workflow entry point, this
is where it goes: write the file with write_file under the connection or
module it belongs to. New commands appear after a server restart, which
the user must do; tell them.
- archive/: retired state. Not scanned or listed, still readable by path.
How state grows: one topic per module. A folder's index.md is its map and
+22 -9
View File
@@ -17,6 +17,14 @@ The user's request, possibly empty: "$request"
- The user does not need to know gcontext's concepts. Never ask "do you want
a connection or a module?". They describe goals; YOU decide what each goal
needs and explain your plan in one plain line per item.
- Setup is not a one-shot. Say early that nothing has to be decided now:
the user can start with one or two items and add or change anything later
by running setup again or simply asking the agent.
- Do not re-confirm what the user already said. When the user explicitly
named a service or a topic, that item is approved; asking "should I add
the X connection?" again is noise. The plan confirmation (Step 3) also
counts as write approval for the files that build those items: announce
each write in one line, but do not ask again per file.
- Inspect before you ask. Never ask the user something the state folder can
answer. Start with list_dir(".") and read what you need from there.
- When your runtime has an interactive question tool (AskUserQuestion in
@@ -64,22 +72,27 @@ Translate the description into a plan. The mapping is yours to make:
- Knowledge the agent must hold (how the company works, a product, a process,
a team's rules): a module each. Prefer a few broad modules over many thin
ones; a module can grow files later.
- If agent.md is still the placeholder, writing it from the user's
description is always part of the plan.
- If agent.md is still the placeholder, write it yourself from the user's
description. It is not a plan item and never a question: do not ask what
it should say or confirm its content, just write it at the start of the
build and mention in one line that you did.
Show the plan as a short list, one plain line per item ("stripe: so the agent
can look up payments and refunds"), and confirm it as a choice question:
build all of it, or let the user deselect items (multi-select when the tool
supports it). First-time setups with many items are the normal case; do not
talk the user out of a big plan, but order it so something useful exists
early.
can look up payments and refunds"). Items the user explicitly asked for are
already approved; confirm only the items you inferred yourself, as a choice
question (multi-select when the tool supports it). If every item was
explicitly requested, skip the confirmation and start building. First-time
setups with many items are the normal case; do not talk the user out of a
big plan, but order it so something useful exists early, and remind them
the rest can always be added later.
## Step 4: Build, one item at a time
Work through the confirmed plan. Finish each item before starting the next,
and say briefly where you are ("2 of 5"). Suggested order: agent.md
first, then modules (they only need conversation), then connections (each
needs the user to place secrets).
first (written directly from the description, no questions), then modules
(they only need conversation), then connections (each needs the user to
place secrets).
**Add a connection:**
+17 -4
View File
@@ -57,7 +57,7 @@ export default function Commands() {
<div>
<h1 style={{ ...pageTitle, marginBottom: 5 }}>Commands</h1>
<p style={{ margin: "0 0 20px", color: C.tMuted, fontSize: 13.5, lineHeight: 1.55, maxWidth: 640 }}>
Files under <span style={{ fontFamily: mono }}>commands/</span> folders, served as MCP prompts. New files appear after a server restart.
All MCP prompts: project commands from <span style={{ fontFamily: mono }}>commands/</span> folders and built-in framework prompts, grouped by owner.
</p>
{cmds.length === 0 ? (
<EmptyState>
@@ -69,9 +69,22 @@ export default function Commands() {
) : (
<>
<div style={{ ...sectionLabel, marginBottom: 11 }}>Commands ({cmds.length})</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 13 }}>
{cmds.map((c) => <CommandCard key={c.path} cmd={c} />)}
</div>
{(() => {
const groups = {};
cmds.forEach((c) => { (groups[c.owner] = groups[c.owner] || []).push(c); });
const owners = Object.keys(groups).sort((a, b) => a === "framework" ? 1 : b === "framework" ? -1 : a.localeCompare(b));
return owners.map((owner) => (
<div key={owner} style={{ marginBottom: 22 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 10 }}>
<span style={{ fontFamily: mono, fontSize: 12, fontWeight: 600, color: C.ink, textTransform: "uppercase", letterSpacing: "0.04em" }}>{owner}</span>
<span style={{ fontSize: 11, color: C.t3 }}>({groups[owner].length})</span>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 13 }}>
{groups[owner].map((c) => <CommandCard key={c.path} cmd={c} />)}
</div>
</div>
));
})()}
</>
)}
</div>