# Artifacts and workspace

> On-disk layout under $LOOPANY_HOME: artifacts/<dirName>/<slug>.md, config.yaml, references.jsonl, audit.jsonl, optional search.db, and v0.2 slug-as-id storage rules.

- Repository: superdesigndev/loopany
- GitHub: https://github.com/superdesigndev/loopany
- Human docs: https://grok-wiki.com/public/docs/superdesigndev-loopany-97bd9ab97ae8
- Complete Markdown: https://grok-wiki.com/public/docs/superdesigndev-loopany-97bd9ab97ae8/llms-full.txt

## Source Files

- `src/core/artifact-store.ts`
- `src/core/engine.ts`
- `src/commands/init.ts`
- `src/core/config.ts`
- `src/core/audit.ts`
- `skills/loopany-core/kinds/journal.md`
- `INSTALL_FOR_AGENTS.md`

---

---
title: "Artifacts and workspace"
description: "On-disk layout under $LOOPANY_HOME: artifacts/<dirName>/<slug>.md, config.yaml, references.jsonl, audit.jsonl, optional search.db, and v0.2 slug-as-id storage rules."
---

The loopany workspace is a single directory tree (default `~/loopany`, overridable via `LOOPANY_HOME`) where markdown artifacts are the source of truth, append-only JSONL files record graph edges and CLI operations, and an optional SQLite `search.db` is a derived index rebuilt by `loopany reindex`.

## Workspace root

<ParamField body="LOOPANY_HOME" type="path">
Overrides the default workspace location. When unset, the root is `~/loopany` (from `os.homedir()`).
</ParamField>

Bootstrap treats a directory as a workspace when `kinds/` exists. If not, commands throw `WorkspaceNotFoundError` with a pointer to `loopany init`.

| Path | Created by | Role |
|------|------------|------|
| `config.yaml` | `loopany init` (if missing) | `schemaVersion`, `enabled_domains` |
| `kinds/*.md` | `loopany init` copies bundled defs | Kind schemas, status machines, `dirName` / `slugLayout` |
| `artifacts/` | `loopany init` | All artifact markdown files |
| `domains/<name>/kinds/` | Agent / user (not init) | Domain-scoped kind overrides when domain enabled |
| `references.jsonl` | First `refs add` or hand edit | Append-only explicit graph edges |
| `audit.jsonl` | First CLI command after workspace exists | Append-only operational log |
| `search.db` | `loopany reindex` | Derived hybrid search index (safe to delete) |

`loopany init` is idempotent: it creates missing directories and `config.yaml`, copies any bundled kind files from `skills/loopany-core/kinds/` that are not already present, and leaves existing files untouched.

<RequestExample>

```bash
loopany init
```

</RequestExample>

<ResponseExample>

```text
Workspace: /Users/you/loopany
Created: config.yaml, kinds/task.md, ...
Needs onboarding: true   # when artifacts/missions/ has no .md files
```

</ResponseExample>

## Directory layout (v0.2)

:::files
$LOOPANY_HOME/
├── config.yaml
├── kinds/                    # global kind definitions
├── artifacts/
│   ├── tasks/                # flat: artifacts/<dirName>/<slug>.md
│   ├── signals/
│   ├── briefs/
│   ├── learnings/
│   ├── skill-proposals/
│   ├── missions/
│   ├── notes/
│   ├── people/
│   └── journal/
│       └── 2026/             # year layout only
│           └── 2026-06-05.md
├── domains/                  # optional scope packs
│   └── <name>/kinds/
├── references.jsonl
├── audit.jsonl
└── search.db                 # optional; from reindex
:::

<Note>
v0.1 used date buckets such as `artifacts/2026-06/tsk-*.md` and kind-prefixed IDs (`tsk-`, `sig-`). Current binaries expect `schemaVersion: 0.2.0` and flat `artifacts/<dirName>/` paths. See [Schema migration](/schema-migration) if bootstrap reports a version mismatch.
</Note>

### Kind → `dirName` mapping

Each kind definition in `kinds/*.md` (or `domains/<name>/kinds/*.md`) declares storage under `artifacts/`. The registry parses `dirName` and `slugLayout` from kind frontmatter.

| Kind | `dirName` | `slugLayout` | Notes |
|------|-----------|--------------|-------|
| `task` | `tasks` | `flat` (default) | Default `dirName` = `{kind}s` |
| `signal` | `signals` | `flat` | |
| `brief` | `briefs` | `flat` | |
| `learning` | `learnings` | `flat` | |
| `skill-proposal` | `skill-proposals` | `flat` | Hyphenated kind → `{kind}s` |
| `mission` | `missions` | `flat` | Explicit in kind file |
| `note` | `notes` | `flat` | |
| `person` | `people` | `flat` | |
| `journal` | `journal` | `year` | `artifacts/journal/<YYYY>/<YYYY-MM-DD>.md` |

`slugLayout: year` adds a four-digit year subdirectory derived from the first four characters of the artifact id (used by `journal`, whose id is the calendar date).

```mermaid
flowchart TB
  subgraph workspace["$LOOPANY_HOME"]
    config["config.yaml"]
    kinds["kinds/*.md"]
    arts["artifacts/"]
    refs["references.jsonl"]
    audit["audit.jsonl"]
    search["search.db optional"]
  end
  subgraph engine["bootstrap()"]
    KR["KindRegistry.load"]
    AS["ArtifactStore"]
    RG["ReferenceGraph"]
  end
  kinds --> KR
  KR --> AS
  arts --> AS
  refs --> RG
  config --> engine
  AS --> arts
```

## Slug-as-id rules (v0.2)

The filename stem **is** the artifact id. There is no kind prefix in ids or paths. Global uniqueness is enforced at create time by scanning every registered kind directory for `<id>.md`.

### Allocation order

On `artifact create`, the store picks an id in this order:

1. **Caller slug** — `--slug` (validated, must not exist).
2. **Title-derived** — `slugifyTitle(title)` from validated frontmatter; collisions get `-2`, `-3`, … up to 99 attempts.
3. **Timestamp fallback** — `YYYYMMDD-HHMMSS-<3hex>` UTC when title slugify fails (emoji-only titles, empty slugify, or collision exhaustion).

### Slug validation

| Rule | Constraint |
|------|------------|
| Length | 1–60 Unicode codepoints |
| Charset | Letters, marks, digits, `-`, `_` (NFC-normalized) |
| Edges | No leading/trailing `-` or `_`; no `--` or `__` |
| Uniqueness | Global across all kinds (store-enforced) |

Wiki links and CLI commands use this bare slug: `loopany artifact get reddit-claude-design`, `[[reddit-claude-design]]` in body text.

### Resolution and timestamps

- **`get(id)`** — Probes each kind’s storage path until a file exists (~10 kinds, cheap stat calls).
- **`createdAt` / `updatedAt`** — Stamped on every write; `createdAt` set on create if omitted.
- **Built-in frontmatter fields** — `domain`, `createdAt`, `updatedAt`, `_backfilled` (migration marker) bypass per-kind field specs.

## Artifact file format

Each artifact is one markdown file: YAML frontmatter + body. Kind-specific frontmatter is validated with Zod schemas built from the kind’s `## Frontmatter` YAML block.

**Mutations** (enforced by store / CLI):

| Operation | Effect |
|-----------|--------|
| `artifact append` | Appends a `## Section` block to the body |
| `artifact status` | Updates `status` via the kind’s state machine |
| `artifact set` | Updates a non-status frontmatter field |
| In-place body rewrite for “cited” kinds | Avoided — append, status flip, or supersede |

<Warning>
Do not use `artifact set` on `status`; use `artifact status` so transitions are validated against the kind’s status machine.
</Warning>

### Journal (auto-managed spine)

The `journal` kind is not created via `artifact create --kind journal` (rejected at CLI). The store lazily creates `artifacts/journal/<YYYY>/<YYYY-MM-DD>.md` when the first non-journal artifact is created that day, and appends a wiki-link line to `## Activity` (or `## Backfilled` when `_backfilled: true`).

Journal ids equal the date string (`2026-06-05`). The year subdirectory is for human browsing only; resolution still uses the global slug.

## `config.yaml`

<ParamField body="schemaVersion" type="string">
Workspace format version. New workspaces get the binary’s `SCHEMA_VERSION` (`0.2.0`). If absent, assumed `0.1.0` (legacy). Mismatch with the binary throws `SchemaVersionMismatchError` unless `LOOPANY_SKIP_VERSION_CHECK=1` or bootstrap `skipVersionCheck`.
</ParamField>

<ParamField body="enabled_domains" type="string[]">
Domain names whose `domains/<name>/kinds/` packs are merged into the kind registry at boot.
</ParamField>

## `references.jsonl`

Append-only graph log at the workspace root. One JSON object per line:

```json
{"ts":"2026-06-05T12:00:00.000Z","from":"<slug>","to":"<slug>","relation":"<verb>","actor":"cli"}
```

`relation` is an open registry (conventions in skills, not a closed enum). Reverse edges are built in memory at load time, not stored.

**Implicit edges** — At index build, `mentions` in frontmatter and `[[id]]` wiki links in the body synthesize `mentions` edges in memory only. They do not append rows to `references.jsonl`; updating the artifact updates the implicit graph on the next boot.

## `audit.jsonl`

Every CLI invocation appends one row after dispatch:

```json
{"ts":"...","op":"artifact.create","actor":"cli","duration_ms":42}
```

Optional `error` field when the command failed. Audit write failures are silent so they never break user-facing operations.

## `search.db` (optional)

| Property | Detail |
|----------|--------|
| Location | `$LOOPANY_HOME/search.db` |
| Engine | Bun SQLite |
| Source of truth | Markdown artifacts only |
| Population | `loopany reindex` (optional `--force`) |
| Missing DB | `loopany search` returns empty results and stderr hint to run reindex |

The index chunks artifact bodies/titles, uses FTS5 + optional embeddings fused by reciprocal rank fusion. Deleting `search.db` does not lose artifacts; rebuild with reindex.

## Engine composition

`bootstrap()` wires the workspace for command handlers:

| Module | Path / object | Responsibility |
|--------|---------------|----------------|
| `Config` | `config.yaml` | Schema version, enabled domains |
| `KindRegistry` | `kinds/` + domain packs | Parse kind defs, Zod validators, status machines |
| `ArtifactStore` | `artifacts/` | Create, read, list, append, set field/status |
| `ReferenceGraph` | `references.jsonl` | Append/load explicit edges |
| `ArtifactIndex` | In-memory | Built per command via `engine.index()` — filters, refs, implicit mentions |

Storage (flat artifact pool) and organization (`domains/` curated views) are separate: one artifact can be tagged with `domain` frontmatter and referenced from multiple domain scopes without duplicating files.

## v0.1 → v0.2 migration summary

When `config.yaml#schemaVersion` is older than the binary’s `SCHEMA_VERSION`, bootstrap refuses normal operations and points at the migration skill and `loopany migrate`.

| v0.1 | v0.2 |
|------|------|
| `tsk-20260427-103045` ids | `20260427-103045` slug ids |
| `artifacts/{YYYY-MM}/...` buckets | `artifacts/<dirName>/<slug>.md` |
| `check_at`, snake_case FM | `checkAt`, camelCase FM |
| No journal | Auto `journal/<YYYY>/<date>.md` + backfill |
| Kind knobs: `idPrefix`, `storage`, `bodyMode` | Dropped; optional `slugLayout` only |

Migration scripts rewrite files, emit `.migration-id-map.json`, rebuild `references.jsonl`, and set `schemaVersion: 0.2.0`.

<Steps>
<Step title="Detect mismatch">
Run any command; `SchemaVersionMismatchError` names workspace vs binary versions and the migrate subcommand.
</Step>
<Step title="Snapshot">
Git-commit or copy `$LOOPANY_HOME` before applying migration scripts (see migration skill pre-flight).
</Step>
<Step title="Migrate and verify">
`loopany migrate v0.1.0-to-v0.2.0` (or run scripted steps), then `loopany doctor` for schema version, artifact validation, and dangling refs.
</Step>
</Steps>

## Verification

```bash
loopany doctor
loopany kind list          # dirName + slugLayout per kind
loopany artifact list --kind task
```

Doctor checks: workspace presence, schema version, kind parse health, artifact frontmatter validity, reference endpoint integrity, onboarding (`self` person + active mission), and warnings for mission/domain coverage.

<Check>
Exit code `0` from `loopany doctor` means the on-disk workspace matches what the binary expects for v0.2 storage and graph integrity.
</Check>

## Related pages

<CardGroup>
<Card title="Workspace setup" href="/workspace-setup">
Run `loopany init`, verify bundled kinds, onboarding, and doctor.
</Card>
<Card title="Kinds and validation" href="/kinds-and-validation">
Kind markdown defs, Zod frontmatter, status machines, immutable write rules.
</Card>
<Card title="Reference graph" href="/reference-graph">
`references.jsonl`, implicit mentions, `refs` / `trace` queries.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`LOOPANY_HOME`, `LOOPANY_SKIP_VERSION_CHECK`, schema version guard.
</Card>
<Card title="Schema migration" href="/schema-migration">
v0.1.0→v0.2.0 scripts, id map, journal backfill.
</Card>
<Card title="Artifact commands" href="/artifact-commands">
`create` / `get` / `list`, `--slug`, append and status flows.
</Card>
</CardGroup>
