@arkhera30/cli 0.8.5 → 0.8.6
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/dist/index.js +102 -3
- package/guides/index.json +1 -1
- package/package.json +1 -1
- package/guides/core-concepts 2.md +0 -111
- package/guides/first-note 2.md +0 -127
- package/guides/first-session 2.md +0 -125
- package/guides/first-workspace 2.md +0 -108
- package/guides/getting-started 2.md +0 -94
- package/guides/index 2.json +0 -2301
package/dist/index.js
CHANGED
|
@@ -2580,6 +2580,92 @@ async function syncSkills(runtime) {
|
|
|
2580
2580
|
}
|
|
2581
2581
|
}
|
|
2582
2582
|
}
|
|
2583
|
+
var GLOBAL_SKILLS = ["horus-anvil", "horus-vault", "horus-forge", "horus-context", "capture", "triage"];
|
|
2584
|
+
async function fetchLatestSkillBody(controlPlaneUrl, id) {
|
|
2585
|
+
const base = controlPlaneUrl.replace(/\/+$/, "");
|
|
2586
|
+
const versionsRes = await fetch(`${base}/api/v1/forge/artifacts/skill/${id}/versions`);
|
|
2587
|
+
if (!versionsRes.ok) return null;
|
|
2588
|
+
const versionsJson = await versionsRes.json();
|
|
2589
|
+
const versions = versionsJson.versions ?? [];
|
|
2590
|
+
if (versions.length === 0) return null;
|
|
2591
|
+
const latest = versions.slice().sort((a, b) => {
|
|
2592
|
+
const pa = a.split(".").map(Number);
|
|
2593
|
+
const pb = b.split(".").map(Number);
|
|
2594
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
2595
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
2596
|
+
if (diff !== 0) return diff;
|
|
2597
|
+
}
|
|
2598
|
+
return 0;
|
|
2599
|
+
}).pop();
|
|
2600
|
+
const artifactRes = await fetch(`${base}/api/v1/forge/artifacts/skill/${id}/${latest}`);
|
|
2601
|
+
if (!artifactRes.ok) return null;
|
|
2602
|
+
const artifact = await artifactRes.json();
|
|
2603
|
+
const b64 = artifact.files?.["SKILL.md"];
|
|
2604
|
+
if (!b64) return null;
|
|
2605
|
+
return Buffer.from(b64, "base64").toString("utf-8");
|
|
2606
|
+
}
|
|
2607
|
+
async function syncSkillsFromRegistry(controlPlaneUrl) {
|
|
2608
|
+
const home = homedir3();
|
|
2609
|
+
const skillsBase = join2(home, ".claude", "skills");
|
|
2610
|
+
let synced = 0;
|
|
2611
|
+
const failed = [];
|
|
2612
|
+
for (const id of GLOBAL_SKILLS) {
|
|
2613
|
+
try {
|
|
2614
|
+
const body = await fetchLatestSkillBody(controlPlaneUrl, id);
|
|
2615
|
+
if (!body) {
|
|
2616
|
+
failed.push(id);
|
|
2617
|
+
continue;
|
|
2618
|
+
}
|
|
2619
|
+
const destDir = join2(skillsBase, id);
|
|
2620
|
+
mkdirSync2(destDir, { recursive: true });
|
|
2621
|
+
writeFileSync3(join2(destDir, "SKILL.md"), body, "utf-8");
|
|
2622
|
+
synced++;
|
|
2623
|
+
} catch {
|
|
2624
|
+
failed.push(id);
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
if (failed.length === 0) {
|
|
2628
|
+
console.log(chalk.green(`\u2714 Synced ${synced} global skills from the control plane`));
|
|
2629
|
+
} else {
|
|
2630
|
+
console.log(chalk.yellow(`\u2714 Synced ${synced} global skills from the control plane`) + chalk.dim(` (failed: ${failed.join(", ")})`));
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
async function syncSkillsForCursorFromRegistry(controlPlaneUrl) {
|
|
2634
|
+
const home = homedir3();
|
|
2635
|
+
const rulesDir = join2(home, ".cursor", "rules");
|
|
2636
|
+
const skillsBase = join2(home, ".cursor", "skills-cursor");
|
|
2637
|
+
mkdirSync2(rulesDir, { recursive: true });
|
|
2638
|
+
let synced = 0;
|
|
2639
|
+
const failed = [];
|
|
2640
|
+
for (const id of GLOBAL_SKILLS) {
|
|
2641
|
+
try {
|
|
2642
|
+
const body = await fetchLatestSkillBody(controlPlaneUrl, id);
|
|
2643
|
+
if (!body) {
|
|
2644
|
+
failed.push(id);
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
const ruleDest = join2(rulesDir, `${id}.mdc`);
|
|
2648
|
+
const frontmatter = `---
|
|
2649
|
+
description: Horus ${id} reference
|
|
2650
|
+
alwaysApply: true
|
|
2651
|
+
---
|
|
2652
|
+
|
|
2653
|
+
`;
|
|
2654
|
+
writeFileSync3(ruleDest, frontmatter + body, "utf-8");
|
|
2655
|
+
const skillDir = join2(skillsBase, id);
|
|
2656
|
+
mkdirSync2(skillDir, { recursive: true });
|
|
2657
|
+
writeFileSync3(join2(skillDir, "SKILL.md"), body, "utf-8");
|
|
2658
|
+
synced++;
|
|
2659
|
+
} catch {
|
|
2660
|
+
failed.push(id);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
if (failed.length === 0) {
|
|
2664
|
+
console.log(chalk.green(`\u2714 Synced ${synced} global skills for Cursor from the control plane`));
|
|
2665
|
+
} else {
|
|
2666
|
+
console.log(chalk.yellow(`\u2714 Synced ${synced} global skills for Cursor from the control plane`) + chalk.dim(` (failed: ${failed.join(", ")})`));
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2583
2669
|
async function syncSkillsForCursor(runtime) {
|
|
2584
2670
|
const home = homedir3();
|
|
2585
2671
|
const rulesDir = join2(home, ".cursor", "rules");
|
|
@@ -2692,8 +2778,14 @@ async function runConnect(config, runtime, targets, host = "localhost") {
|
|
|
2692
2778
|
}
|
|
2693
2779
|
if (targets.includes("claude-code")) {
|
|
2694
2780
|
if (connectedMode) {
|
|
2695
|
-
|
|
2696
|
-
|
|
2781
|
+
const skillsSpinner = ora("Syncing horus-core skills from the control plane...").start();
|
|
2782
|
+
try {
|
|
2783
|
+
await syncSkillsFromRegistry(config.control_plane_url);
|
|
2784
|
+
skillsSpinner.succeed("horus-core skills synced to ~/.claude/skills/");
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
skillsSpinner.warn("Could not sync skills from the control plane");
|
|
2787
|
+
console.log(chalk.dim(error.message));
|
|
2788
|
+
}
|
|
2697
2789
|
} else {
|
|
2698
2790
|
const skillsSpinner = ora("Syncing horus-core skills...").start();
|
|
2699
2791
|
try {
|
|
@@ -2707,7 +2799,14 @@ async function runConnect(config, runtime, targets, host = "localhost") {
|
|
|
2707
2799
|
}
|
|
2708
2800
|
if (targets.includes("cursor")) {
|
|
2709
2801
|
if (connectedMode) {
|
|
2710
|
-
|
|
2802
|
+
const cursorRulesSpinner = ora("Syncing horus-core rules for Cursor from the control plane...").start();
|
|
2803
|
+
try {
|
|
2804
|
+
await syncSkillsForCursorFromRegistry(config.control_plane_url);
|
|
2805
|
+
cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/ and skills to ~/.cursor/skills-cursor/");
|
|
2806
|
+
} catch (error) {
|
|
2807
|
+
cursorRulesSpinner.warn("Could not sync Cursor rules from the control plane");
|
|
2808
|
+
console.log(chalk.dim(error.message));
|
|
2809
|
+
}
|
|
2711
2810
|
} else {
|
|
2712
2811
|
const cursorRulesSpinner = ora("Syncing horus-core rules for Cursor...").start();
|
|
2713
2812
|
try {
|
package/guides/index.json
CHANGED
package/package.json
CHANGED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Horus Core Concepts
|
|
3
|
-
description: The mental model for Anvil, Vault, Forge, and the supporting services Horus depends on.
|
|
4
|
-
slug: core-concepts
|
|
5
|
-
tags: [concepts, architecture, onboarding, anvil, vault, forge]
|
|
6
|
-
schema_version: 1
|
|
7
|
-
keywords: [concepts, architecture, mental-model, anvil, vault, forge, neo4j, typesense, mcp, services, how-it-works, overview]
|
|
8
|
-
related_commands: [horus status, horus config]
|
|
9
|
-
sidebar_position: 2
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Horus Core Concepts
|
|
13
|
-
|
|
14
|
-
Horus is three primary systems (Anvil, Vault, Forge) backed by supporting services (Typesense, Neo4j, a web UI). This guide explains what each one does, why it exists as a separate service, and how they connect.
|
|
15
|
-
|
|
16
|
-
## The three primary systems
|
|
17
|
-
|
|
18
|
-
### Anvil — live state
|
|
19
|
-
|
|
20
|
-
Anvil stores your structured, **changing** data: tasks, stories, projects, journals, notes, conversation state. Every entity is a markdown file with YAML front-matter, indexed by an embedded SQLite database for fast full-text search.
|
|
21
|
-
|
|
22
|
-
Anvil has a **dynamic type system** — type definitions are YAML files, not hardcoded in the server. That means you can add new note types or extend existing ones without changing code. Claude always queries the current type schema before creating a note, so your types stay authoritative.
|
|
23
|
-
|
|
24
|
-
**Use Anvil for:** tasks, projects, meeting notes, daily journals, research notes, anything you'd put in a personal knowledge-management app.
|
|
25
|
-
|
|
26
|
-
**MCP endpoint:** `http://localhost:8100`
|
|
27
|
-
|
|
28
|
-
### Vault — durable knowledge
|
|
29
|
-
|
|
30
|
-
Vault stores your **long-lived**, structured documentation: repo profiles, architecture decisions, how-to guides, procedures, and learnings. Unlike Anvil (which tracks changing state), Vault is the place for knowledge you want to reference across sessions, weeks, and projects.
|
|
31
|
-
|
|
32
|
-
Vault is a FastAPI knowledge service backed by a git repository per vault. You can run **multiple vaults** (e.g. `personal`, `work`, `client-acme`) — each is a separate repo with its own access controls and git history. A separate `vault-router` service fans out read queries to all vaults and routes writes to the correct one by UUID.
|
|
33
|
-
|
|
34
|
-
Search is powered by both Typesense (full-text, typo-tolerant) and Neo4j (graph traversal for "what's related to what"). You get semantic-ish retrieval without a GPU or an external API.
|
|
35
|
-
|
|
36
|
-
**Use Vault for:** how does this codebase work, what conventions does that team follow, what's the decision history for this architecture choice.
|
|
37
|
-
|
|
38
|
-
**MCP endpoint:** `http://localhost:8300` (via the `vault-mcp` adapter)
|
|
39
|
-
|
|
40
|
-
### Forge — execution environment
|
|
41
|
-
|
|
42
|
-
Forge is the **workspace and session manager**. It does three distinct things:
|
|
43
|
-
|
|
44
|
-
1. **Workspaces** — isolated contexts (MCP configs, skills, permissions) for a particular line of work. A workspace doesn't clone any code; it's just the agent environment.
|
|
45
|
-
2. **Sessions** — git worktrees tied to a work item, created on demand via `forge_develop`. A session is where code changes actually happen.
|
|
46
|
-
3. **Repo index** — a scanned index of your local Git repositories, made available to Claude so it can resolve and search across them.
|
|
47
|
-
|
|
48
|
-
Forge also manages **plugins and skills** — reusable packages that install into the registry and add capabilities to your workspaces.
|
|
49
|
-
|
|
50
|
-
**Use Forge for:** starting coded work, managing isolated contexts per project, discovering which repos exist on your machine.
|
|
51
|
-
|
|
52
|
-
**MCP endpoint:** `http://localhost:8200`
|
|
53
|
-
|
|
54
|
-
## Supporting services
|
|
55
|
-
|
|
56
|
-
### Typesense
|
|
57
|
-
|
|
58
|
-
A full-text and vector search engine that runs inside the Horus stack. Both Anvil and Vault use Typesense under the hood for fast, typo-tolerant search. You don't interact with Typesense directly — it's internal plumbing exposed only inside the Docker network.
|
|
59
|
-
|
|
60
|
-
### Neo4j
|
|
61
|
-
|
|
62
|
-
A graph database for relationship-aware queries. Vault uses Neo4j to store and traverse the graph of entities and edges between knowledge pages — useful for questions like "what's related to this decision" or "walk me from this ADR to the procedures that implement it". Runs on ports **7474** (browser / HTTP) and **7687** (Bolt protocol).
|
|
63
|
-
|
|
64
|
-
### Horus UI
|
|
65
|
-
|
|
66
|
-
A React web interface at **`http://localhost:8400`** that lets you browse Anvil notes, Vault pages, and Forge workspaces without going through Claude. It's served by an Express proxy that fans requests out to Anvil, Vault MCP, and Forge over the internal Docker network. Optional — Claude works fine without it — but handy for a visual sanity check.
|
|
67
|
-
|
|
68
|
-
## How they connect
|
|
69
|
-
|
|
70
|
-
Everything runs in a single Docker Compose stack on a bridge network named `horus-net`. Claude (Desktop, Code, or Cursor) connects via the Model Context Protocol (MCP) to three endpoints on your host:
|
|
71
|
-
|
|
72
|
-
| Service | Host endpoint | Who talks to it |
|
|
73
|
-
|---|---|---|
|
|
74
|
-
| Anvil | `http://localhost:8100` | Claude, Horus UI |
|
|
75
|
-
| Vault MCP | `http://localhost:8300` | Claude, Horus UI |
|
|
76
|
-
| Forge | `http://localhost:8200` | Claude, Horus UI |
|
|
77
|
-
|
|
78
|
-
Internally, Vault MCP proxies to the `vault-router`, which fans out to each `vault` instance on ports `8001`, `8002`, etc. Anvil and Vault both query Typesense over the internal network. Forge reads repo metadata from the host filesystem via a read-only bind mount.
|
|
79
|
-
|
|
80
|
-
## Data layout
|
|
81
|
-
|
|
82
|
-
All durable state lives under your data directory (default `~/Horus/data`). On a fresh install you'll see:
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
~/Horus/data/
|
|
86
|
-
notes/ Anvil notes (git repo)
|
|
87
|
-
vaults/
|
|
88
|
-
<name>/ Each vault is a separate git repo
|
|
89
|
-
registry/ Forge plugin registry (git repo)
|
|
90
|
-
workspaces/ Forge workspace directories
|
|
91
|
-
sessions/ Forge session git worktrees
|
|
92
|
-
repos/ Managed clone pool (created by forge_repo_clone)
|
|
93
|
-
config/ Forge-managed config (forge.yaml, repos.json, workspaces.json)
|
|
94
|
-
typesense-data/ Typesense index (internal)
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
User config lives at `~/Horus/config.yaml` and `~/Horus/.env`. The Docker Compose file is installed at `~/Horus/docker-compose.yml` — edit it only if you know what you're doing; `horus setup` regenerates it from the config.
|
|
98
|
-
|
|
99
|
-
## Key principles
|
|
100
|
-
|
|
101
|
-
1. **Everything is local.** All data stays on your machine. Search runs in-process. The only network traffic is to Anthropic (when Claude talks to the API) and to your own Git remotes for sync.
|
|
102
|
-
2. **Data is durable.** Notes, knowledge, and workspaces are files on disk backed by git. Stopping the stack or removing containers never deletes your data.
|
|
103
|
-
3. **Routing is automatic.** You don't pick Anvil vs Vault vs Forge — Claude reads the routing rules from the `horus-*` skills and picks the right system based on intent.
|
|
104
|
-
4. **Types are dynamic.** Anvil's type system is runtime-defined. New types and fields land without a code release.
|
|
105
|
-
|
|
106
|
-
## What's next
|
|
107
|
-
|
|
108
|
-
- **`horus guide getting-started`** if you haven't installed yet
|
|
109
|
-
- **`horus guide first-note`** to create your first Anvil entity
|
|
110
|
-
- **`horus guide first-workspace`** to set up your first isolated working context
|
|
111
|
-
- **`horus guide first-session`** to start your first isolated code session
|
package/guides/first-note 2.md
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Your First Anvil Note
|
|
3
|
-
description: How Anvil's dynamic type system works and how to create your first entity — project, task, or note.
|
|
4
|
-
slug: first-note
|
|
5
|
-
tags: [anvil, notes, onboarding, types]
|
|
6
|
-
schema_version: 1
|
|
7
|
-
keywords: [note, create, anvil, type, project, task, story, journal, list-types, dynamic-types]
|
|
8
|
-
related_commands: [horus status]
|
|
9
|
-
sidebar_position: 5
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Your First Anvil Note
|
|
13
|
-
|
|
14
|
-
Anvil is Horus's live-state layer. It stores tasks, projects, journals, stories, meeting notes, and every other kind of structured entry you might want to track. Every entity is a markdown file with YAML front-matter, indexed by SQLite for fast search.
|
|
15
|
-
|
|
16
|
-
The one thing that surprises most first-time users: **types are not hardcoded**. Anvil has a dynamic type system — you (or a plugin) define types as YAML files, and Anvil discovers them at runtime. That's why the first thing any Anvil-aware tool does is call `anvil_list_types` to ask what's currently available.
|
|
17
|
-
|
|
18
|
-
## Step 1 — See what types exist
|
|
19
|
-
|
|
20
|
-
Ask Claude:
|
|
21
|
-
|
|
22
|
-
> "What note types exist in Anvil?"
|
|
23
|
-
|
|
24
|
-
Claude will call the `anvil_list_types` tool and return a list of types with their fields, required fields, and defaults. On a fresh install you'll see core types like:
|
|
25
|
-
|
|
26
|
-
- **`project`** — top-level container for related work
|
|
27
|
-
- **`story`** — a feature, task, or work item (extends `task`)
|
|
28
|
-
- **`task`** — a tracked to-do with status
|
|
29
|
-
- **`note`** — a generic unstructured entry
|
|
30
|
-
- **`journal`** — append-only chronological record for standups, session logs, decisions
|
|
31
|
-
- **`meeting`** — attendees + agenda + action items
|
|
32
|
-
|
|
33
|
-
Each type has:
|
|
34
|
-
|
|
35
|
-
- **Required fields** (e.g. every `story` needs a `status`)
|
|
36
|
-
- **Optional fields** (`priority`, `due`, `story_points`, etc.)
|
|
37
|
-
- **Enum fields** with allowed values (e.g. `status: open | in-progress | blocked | done`)
|
|
38
|
-
|
|
39
|
-
Don't memorize these — the tool returns them every call.
|
|
40
|
-
|
|
41
|
-
## Step 2 — Create a project
|
|
42
|
-
|
|
43
|
-
> "Create a project called 'My First Horus Project' with status `active`."
|
|
44
|
-
|
|
45
|
-
Claude will pick the right type, populate the required fields, and call `anvil_create_entity`. Under the hood that looks like:
|
|
46
|
-
|
|
47
|
-
```
|
|
48
|
-
anvil_create_entity({
|
|
49
|
-
type: "project",
|
|
50
|
-
title: "My First Horus Project",
|
|
51
|
-
fields: { status: "active" },
|
|
52
|
-
body: "Brief description of what this project is for."
|
|
53
|
-
})
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
The response gives you a UUID (`entityId`) and the file path where Anvil wrote the note on disk — something like `~/Horus/data/notes/my-first-horus-project.md`.
|
|
57
|
-
|
|
58
|
-
## Step 3 — Add a task to the project
|
|
59
|
-
|
|
60
|
-
Projects are just containers — the actual work lives as `task` or `story` entities that reference them. Ask Claude:
|
|
61
|
-
|
|
62
|
-
> "Create a task in My First Horus Project called 'Try out `horus help`' with priority P2-medium."
|
|
63
|
-
|
|
64
|
-
Anvil will create a new task and add the project reference via a typed edge. A lightly-edited version of the tool call:
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
anvil_create_entity({
|
|
68
|
-
type: "task",
|
|
69
|
-
title: "Try out horus help",
|
|
70
|
-
fields: {
|
|
71
|
-
project: "[[My First Horus Project]]",
|
|
72
|
-
priority: "P2-medium",
|
|
73
|
-
status: "open"
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Two things worth noting:
|
|
79
|
-
|
|
80
|
-
- **References are wiki-links.** Anvil uses `[[Title]]` format for cross-entity references, not UUIDs. The ingestion pipeline resolves the link to a UUID under the hood.
|
|
81
|
-
- **Edges are real.** Fields like `project` aren't just strings — they create typed edges in Anvil's graph, visible via `anvil_get_edges` and `anvil_get_related`.
|
|
82
|
-
|
|
83
|
-
## Step 4 — Search for what you just created
|
|
84
|
-
|
|
85
|
-
> "What's pending in My First Horus Project?"
|
|
86
|
-
|
|
87
|
-
Claude calls `anvil_query_view` with a filter on project + status, or `anvil_search` with a free-text query. Both hit Anvil's SQLite index and return the matching entities. You'll see the task you just created.
|
|
88
|
-
|
|
89
|
-
You can also:
|
|
90
|
-
|
|
91
|
-
- **Group results as a board:** `anvil_query_view({ type: "task", project: "[[My First Horus Project]]", format: "board", groupBy: "status" })`
|
|
92
|
-
- **Filter by tags:** `anvil_search({ query: "onboarding", tags: ["experimental"] })`
|
|
93
|
-
- **Get a subtree** starting from a project: `anvil_get_subtree({ rootId: "..." })`
|
|
94
|
-
|
|
95
|
-
## Step 5 — Update without overwriting
|
|
96
|
-
|
|
97
|
-
Anvil uses **PATCH semantics** for updates. When you call `anvil_update_entity`, only the fields you pass are changed — everything else is preserved. That means you can safely update a single field without worrying about clobbering unrelated metadata.
|
|
98
|
-
|
|
99
|
-
> "Mark that task as in-progress."
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
anvil_update_entity({
|
|
103
|
-
noteId: "abc-...",
|
|
104
|
-
fields: { status: "in-progress" }
|
|
105
|
-
})
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
**Journals are a special case:** they're append-only. Updating a journal's body *appends* the new content instead of replacing it, which is useful for daily standups and decision logs.
|
|
109
|
-
|
|
110
|
-
## Common starter flows
|
|
111
|
-
|
|
112
|
-
A few things you'll probably try in your first week:
|
|
113
|
-
|
|
114
|
-
- **Daily journal** — create a `journal` type each morning; Claude appends new entries throughout the day
|
|
115
|
-
- **Project kanban** — one `project`, many `task`s, `anvil_query_view` with `format: "board"` gives you a live kanban
|
|
116
|
-
- **Conversation state** — Claude auto-creates a `conversation-state` entity to track open questions and decisions for a session
|
|
117
|
-
- **SDLC workflow** — `project` → `story` → `task` with typed edges, driven by the `sdlc-*` skills
|
|
118
|
-
|
|
119
|
-
## Where the data lives
|
|
120
|
-
|
|
121
|
-
All Anvil entities are plain markdown files under `<data_dir>/notes/`, tracked in git. You can browse them, edit them in your text editor, commit them, and push them to your remote — Anvil picks up file-system changes via its file watcher. Anvil also periodically pulls the remote and pushes local changes, so your data sync across machines works the same way as git.
|
|
122
|
-
|
|
123
|
-
## What's next
|
|
124
|
-
|
|
125
|
-
- **`horus guide first-workspace`** — set up an isolated agent context so your notes, skills, and MCP configs stay organized
|
|
126
|
-
- **`horus guide first-session`** — tie a code session to a story or task you just created
|
|
127
|
-
- **`horus guide core-concepts`** — how Anvil relates to Vault and Forge in the bigger picture
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Your First Forge Code Session
|
|
3
|
-
description: How to start an isolated code session on a repo for a specific work item, using forge_develop.
|
|
4
|
-
slug: first-session
|
|
5
|
-
tags: [forge, session, onboarding, git, worktree]
|
|
6
|
-
schema_version: 1
|
|
7
|
-
keywords: [session, worktree, forge-develop, code, branch, git, isolate, coding, start-work]
|
|
8
|
-
related_commands: [horus status]
|
|
9
|
-
sidebar_position: 4
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Your First Forge Code Session
|
|
13
|
-
|
|
14
|
-
A **Forge session** is a git worktree on disk, tied to a single work item and a single repository. When you're ready to actually write code for a task — not just plan it, not just take notes — you start a session. The session gives you an isolated, feature-branched workspace that doesn't interfere with whatever else is checked out in your main clone of the repo.
|
|
15
|
-
|
|
16
|
-
Sessions are created by the **`forge_develop`** MCP tool. One call does everything: finds the repo, creates the worktree, checks out a feature branch, wires up the commit scripts, and hands you back a path to work in.
|
|
17
|
-
|
|
18
|
-
## Before you start
|
|
19
|
-
|
|
20
|
-
You need two things:
|
|
21
|
-
|
|
22
|
-
1. **A repo registered with Forge.** Run `forge_repo_list()` to see what Forge has indexed, or `forge_repo_resolve({ name: "..." })` to look up a specific one. Forge scans the `host_repos_path` you set during `horus setup` and auto-adds repos it finds.
|
|
23
|
-
2. **A work item in Anvil.** Sessions are namespaced by work item ID, so you need one before `forge_develop` will do anything. See `first-note` for how to create one.
|
|
24
|
-
|
|
25
|
-
## Create your first session
|
|
26
|
-
|
|
27
|
-
Ask Claude:
|
|
28
|
-
|
|
29
|
-
> "Start a code session for work item `<your-work-item-id>` on repo `<your-repo>`."
|
|
30
|
-
|
|
31
|
-
Or call the MCP tool directly:
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
forge_develop({
|
|
35
|
-
repo: "my-project",
|
|
36
|
-
workItem: "abc12345",
|
|
37
|
-
branch: "feature/abc12345-initial-work"
|
|
38
|
-
})
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Forge returns something like:
|
|
42
|
-
|
|
43
|
-
```json
|
|
44
|
-
{
|
|
45
|
-
"status": "created",
|
|
46
|
-
"sessionId": "sess-...",
|
|
47
|
-
"sessionPath": "/Users/you/Horus/data/sessions/abc12345-my-project",
|
|
48
|
-
"branch": "feature/abc12345-initial-work",
|
|
49
|
-
"baseBranch": "main",
|
|
50
|
-
"repo": "my-project",
|
|
51
|
-
"repoSource": "managed"
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
The **`sessionPath`** is where you do all your work. It's a real git worktree — a separate working directory that shares the underlying git object store with your main clone of the repo. You can `cd` into it, edit files, run tests, and commit like you would in any other clone.
|
|
56
|
-
|
|
57
|
-
## What's in a session directory
|
|
58
|
-
|
|
59
|
-
When you open the session path, you'll see your repo checked out at the feature branch, plus one extra directory:
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
<sessionPath>/
|
|
63
|
-
<your repo contents>
|
|
64
|
-
.forge/
|
|
65
|
-
scripts/
|
|
66
|
-
commit.sh # stage-aware conventional commit wrapper
|
|
67
|
-
push.sh # push to the right remote for this repo's workflow
|
|
68
|
-
create-pr.sh # create a PR against the correct target (handles fork vs owner vs contributor)
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Always commit through `.forge/scripts/commit.sh <type> <scope> <description>`. It handles the conventional-commit formatting and picks up per-repo workflow metadata so you don't have to remember which remote to push to or what the target branch is. Same applies to `push.sh` and `create-pr.sh`.
|
|
72
|
-
|
|
73
|
-
## Resume a session
|
|
74
|
-
|
|
75
|
-
`forge_develop` is **idempotent** for the same work item + repo pair. If the session already exists, Forge resumes it instead of creating a new one:
|
|
76
|
-
|
|
77
|
-
```json
|
|
78
|
-
{
|
|
79
|
-
"status": "resumed",
|
|
80
|
-
"sessionId": "sess-...",
|
|
81
|
-
"sessionPath": "...",
|
|
82
|
-
"branch": "feature/abc12345-initial-work"
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
That means you can re-invoke `forge_develop` any time you restart an agent session or switch machines — it picks up where you left off without polluting your git state.
|
|
87
|
-
|
|
88
|
-
## Workflow confirmation on first use
|
|
89
|
-
|
|
90
|
-
The first time you create a session for a new repo, Forge may return:
|
|
91
|
-
|
|
92
|
-
```json
|
|
93
|
-
{ "status": "needs_workflow_confirmation", "detected": { ... } }
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
This is Forge asking: "I see a `fork` / `owner` / `contributor` workflow here — is that right?" Re-call `forge_develop` with the `workflow` parameter set to confirm. Forge saves the choice so future sessions on the same repo don't need the confirmation again.
|
|
97
|
-
|
|
98
|
-
## Clean up when the work is done
|
|
99
|
-
|
|
100
|
-
Sessions hang around until the linked work item transitions to `done` or `cancelled`. Once that happens, you can bulk-clean:
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
forge_session_cleanup({ auto: true })
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
This removes every session whose work item is complete. Or you can check for stale sessions manually:
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
forge_session_list()
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
## Sessions vs. workspaces
|
|
113
|
-
|
|
114
|
-
This trips up everyone the first time, so here it is one more time:
|
|
115
|
-
|
|
116
|
-
- A **workspace** is an agent *environment* — MCP configs, skills, CLAUDE.md, permissions. It doesn't touch any repo.
|
|
117
|
-
- A **session** is a git *worktree* — an actual checkout of a repo, tied to a work item.
|
|
118
|
-
|
|
119
|
-
You can have one workspace and ten sessions inside it, each working on a different story or repo. Or you can skip workspaces and create sessions directly — they work fine on their own.
|
|
120
|
-
|
|
121
|
-
## What's next
|
|
122
|
-
|
|
123
|
-
- **`horus guide first-note`** — create a work item in Anvil to link your session to
|
|
124
|
-
- **`horus guide first-workspace`** — set up an isolated agent context that wraps multiple sessions
|
|
125
|
-
- **`horus guide core-concepts`** — the mental model behind the workspace / session / repo split
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Your First Forge Workspace
|
|
3
|
-
description: What a Forge workspace is, how it differs from a code session, and how to create your first one.
|
|
4
|
-
slug: first-workspace
|
|
5
|
-
tags: [forge, workspace, onboarding]
|
|
6
|
-
schema_version: 1
|
|
7
|
-
keywords: [workspace, forge, context, isolation, isolated, create, mcp, skills, plugins, environment, setup, initialize]
|
|
8
|
-
related_commands: [horus status]
|
|
9
|
-
sidebar_position: 3
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Your First Forge Workspace
|
|
13
|
-
|
|
14
|
-
A **Forge workspace** is an isolated agent context — a bundle of MCP configs, installed skills, environment variables, and permission settings — that Claude loads when you're working on something specific. Workspaces do **not** clone code. That's what sessions are for (see `first-session`).
|
|
15
|
-
|
|
16
|
-
Think of a workspace as "the agent environment for working on project X", and a session as "the git worktree where I'm actually editing code for story Y inside project X". One workspace, many sessions.
|
|
17
|
-
|
|
18
|
-
## Why workspaces exist
|
|
19
|
-
|
|
20
|
-
Without workspaces, every Claude session shares the same MCP tools, skills, and environment. That's fine at first, but as you start using Horus for real work you'll notice:
|
|
21
|
-
|
|
22
|
-
- Different projects want different skills loaded (one project uses `horus-anvil`, another needs a custom `client-conventions` skill)
|
|
23
|
-
- Sensitive projects shouldn't share environment variables with throwaway experiments
|
|
24
|
-
- You want a clean context when you switch from personal work to client work
|
|
25
|
-
- MCP server configs can drift per-project (e.g. different vault instances)
|
|
26
|
-
|
|
27
|
-
Workspaces solve this by giving each line of work its own agent context.
|
|
28
|
-
|
|
29
|
-
## What's in a workspace
|
|
30
|
-
|
|
31
|
-
When Forge creates a workspace, it writes:
|
|
32
|
-
|
|
33
|
-
- **`.claude/settings.local.json`** — MCP server URLs for Anvil, Vault, and Forge, resolved to host-accessible endpoints (not Docker-internal ones)
|
|
34
|
-
- **`.claude/skills/`** — a subset of skills from the Forge registry, installed for this workspace
|
|
35
|
-
- **`workspace.env`** — environment variables sourced by shells and child processes inside the workspace
|
|
36
|
-
- **`CLAUDE.md`** — a workspace-level instruction file that gets loaded into Claude's context on every session
|
|
37
|
-
- **Permission hooks** — optional pre/post hooks that enforce policies
|
|
38
|
-
|
|
39
|
-
The workspace directory lives under `<data_dir>/workspaces/<workspace-name>/` on your host and is also mounted into the Forge container so the MCP server can read and update it.
|
|
40
|
-
|
|
41
|
-
## Create your first workspace
|
|
42
|
-
|
|
43
|
-
Open Claude and ask it to create a workspace for you:
|
|
44
|
-
|
|
45
|
-
> "Create a workspace called `draft` for general experimentation with the `sdlc-*` skills loaded."
|
|
46
|
-
|
|
47
|
-
Claude will call `forge_workspace_create` under the hood. If you want to do it in a single MCP tool call directly, the arguments look like:
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
forge_workspace_create({
|
|
51
|
-
name: "draft",
|
|
52
|
-
description: "General experimentation workspace",
|
|
53
|
-
skills: ["sdlc-planner", "sdlc-developer", "sdlc-reviewer", "sdlc-story"],
|
|
54
|
-
mcp_servers: ["anvil", "vault", "forge"]
|
|
55
|
-
})
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Forge returns the workspace path (e.g. `~/Horus/data/workspaces/draft/`) and writes all the config files mentioned above.
|
|
59
|
-
|
|
60
|
-
## Use the workspace
|
|
61
|
-
|
|
62
|
-
To actually *work* inside a workspace, open Claude Code or Cursor in the workspace directory:
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
cd ~/Horus/data/workspaces/draft
|
|
66
|
-
claude
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Claude Code picks up `.claude/settings.local.json` automatically and loads the configured MCP servers and skills. The `CLAUDE.md` in the workspace root becomes your project-level instructions. From that point on, every tool call Claude makes is scoped to this workspace's configuration.
|
|
70
|
-
|
|
71
|
-
## Workspaces vs. code sessions
|
|
72
|
-
|
|
73
|
-
Here's the easiest way to keep them straight:
|
|
74
|
-
|
|
75
|
-
| Workspace | Session |
|
|
76
|
-
|---|---|
|
|
77
|
-
| Agent environment (MCP, skills, permissions) | Git worktree on disk |
|
|
78
|
-
| One per project or line of work | One per work item (story / task) |
|
|
79
|
-
| Does NOT clone any repo | Is a git worktree checked out to a feature branch |
|
|
80
|
-
| Persists until you explicitly delete it | Auto-cleans when the linked work item is done |
|
|
81
|
-
| Created via `forge_workspace_create` | Created via `forge_develop` |
|
|
82
|
-
|
|
83
|
-
You typically have one workspace per project and many sessions inside that workspace over time as you pick up different work items.
|
|
84
|
-
|
|
85
|
-
## Listing and inspecting workspaces
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
forge_workspace_list()
|
|
89
|
-
forge_workspace_status({ name: "draft" })
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
The list tool returns every workspace Forge knows about. The status tool tells you its configuration and whether any sessions are linked to it.
|
|
93
|
-
|
|
94
|
-
## Cleaning up
|
|
95
|
-
|
|
96
|
-
When you're done with a workspace:
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
forge_workspace_delete({ name: "draft" })
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
This removes the workspace directory and deregisters it from Forge. If the workspace has active sessions, you'll need to clean those up first (see `first-session`).
|
|
103
|
-
|
|
104
|
-
## What's next
|
|
105
|
-
|
|
106
|
-
- **`horus guide first-session`** — create a git worktree tied to a work item so you can actually edit code
|
|
107
|
-
- **`horus guide first-note`** — create the work item in Anvil that a session links to
|
|
108
|
-
- **`horus guide core-concepts`** — the mental model for why workspace, session, and repo are three separate concepts
|