# Artifact commands

> create/get/list/append/status/set flags, per-kind --field validation, --slug and --content-file, journal auto-management, and required ## Outcome on terminal task statuses.

- 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/commands/artifact-create.ts`
- `src/commands/artifact-get.ts`
- `src/commands/artifact-list.ts`
- `src/commands/artifact-append.ts`
- `src/commands/artifact-status.ts`
- `src/commands/artifact-set.ts`
- `src/commands/body-input.ts`
- `test/artifact-store.test.ts`

---

---
title: "Artifact commands"
description: "create/get/list/append/status/set flags, per-kind --field validation, --slug and --content-file, journal auto-management, and required ## Outcome on terminal task statuses."
---

`loopany artifact` is the single namespace for every artifact kind: create, read, list, append body sections, transition status, and patch frontmatter fields. Commands delegate to `ArtifactStore` (markdown files under `$LOOPANY_HOME/artifacts/`) and the kind registry (Zod frontmatter + status machines from `kinds/*.md`). Mutating commands emit JSON on stdout; `artifact get` defaults to raw markdown.

## Command surface

| Command | Positional args | Primary flags | stdout |
|---|---|---|---|
| `artifact create` | — | `--kind`, `--slug`, `--domain`, `--content` \| `--content-file`, per-kind `--<field>` | `{ id, kind, path }` |
| `artifact get` | `<id>` | `--format md` (default) \| `json` | Markdown file or JSON object |
| `artifact list` | — | `--kind`, `--status`, `--domain`, `--contains`, per-kind filters | JSON array of `{ id, kind, path, frontmatter }` |
| `artifact append` | `<id>` | `--section`, `--content` \| `--content-file` | `{ id }` |
| `artifact status` | `<id> <new-status>` | `--reason`, `--addressed-by` | `{ id, status, reason?, addressedBy?, edgesEmitted }` |
| `artifact set` | `<id>` | `--field`, `--value` | `{ id, field, value }` |

Every flag takes exactly one value (no boolean shorthand). Kebab-case flags map to camelCase frontmatter keys (`--check-at` → `checkAt`).

<Note>
`loopany --help` shows `artifact set <id> --<field> <value>`, but the implementation requires `artifact set <id> --field <name> --value <value>`.
</Note>

## `artifact create`

Creates a new markdown artifact, validates frontmatter against the kind’s Zod schema, allocates a globally unique slug id, stamps `createdAt` / `updatedAt`, and (for non-journal kinds) best-effort appends a wiki-link into today’s journal.

<ParamField body="--kind" type="string" required>
Kind name (e.g. `task`, `signal`, `person`). Must exist in the loaded kind registry.
</ParamField>

<ParamField body="--slug" type="string">
Explicit artifact id used in `[[wiki-links]]` and graph edges. Validated (1–60 codepoints; Unicode letters/digits, `-`, `_`; no leading/trailing separators; no `..` path tricks). Must be globally unique across all kinds.
</ParamField>

<ParamField body="--domain" type="string">
Cross-kind scope tag. Not validated against a kind field spec; written as built-in frontmatter.
</ParamField>

<ParamField body="--content" type="string">
Initial body (inline). Mutually exclusive with `--content-file`.
</ParamField>

<ParamField body="--content-file" type="path | -">
File path or `-` for stdin. Preserves real newlines; preferred for multi-line bodies.
</ParamField>

### Per-kind field flags

Any flag that is not reserved is treated as a frontmatter field for the given `--kind`:

- Unknown flags → error listing valid fields for that kind (type, required, enum values, defaults).
- Types: `string`, `enum`, `date`, `number`, `bool`, `string[]` (comma-separated or JSON array for `string[]` on `artifact set`; create uses comma-split).

Reserved flags (never written as frontmatter): `--kind`, `--slug`, `--domain`, `--content`, `--content-file`.

If the kind has a status machine and `--status` is omitted, the store sets `status` to the machine’s `initial` value before validation.

<RequestExample>

```bash
loopany artifact create --kind task \
  --slug q2-cache-spike \
  --title "Investigate cache thrash" \
  --status todo \
  --priority high \
  --domain ads \
  --content-file body.md
```

</RequestExample>

<ResponseExample>

```json
{
  "id": "q2-cache-spike",
  "kind": "task",
  "path": "/Users/you/loopany/artifacts/tasks/q2-cache-spike.md"
}
```

</ResponseExample>

### Slug allocation order

When `--slug` is omitted:

1. Slugify `--title` (Unicode-aware lowercase, punctuation collapsed to `-`; collisions get `-2`, `-3`, …).
2. If title is missing or slugifies to empty → timestamp id `YYYYMMDD-HHMMSS-<3hex>`.

Explicit `--slug` always wins over a title-derived id.

### Journal rejection

`--kind journal` is rejected at the CLI:

```text
Cannot create journal artifacts directly — they are auto-managed by the store.
```

## Body input (`--content` / `--content-file`)

Shared by `artifact create` and `artifact append` via `resolveBody`:

| Source | Behavior |
|---|---|
| `--content <str>` | Inline string; rejects literal `\n` with no real newlines (shell escape footgun) |
| `--content-file <path>` | Reads UTF-8 file |
| `--content-file -` | Reads stdin (`Bun.stdin.text()`) |
| Neither (create only) | Empty body allowed |

<Warning>
Passing both `--content` and `--content-file` fails. `artifact append` requires one of them and rejects empty section bodies.
</Warning>

## Journal auto-management

On every successful `artifact create` for a non-journal kind (when the `journal` kind is registered):

1. Resolve today’s date from `createdAt` (or current date).
2. Ensure `artifacts/journal/<YYYY>/<YYYY-MM-DD>.md` exists (skeleton with `## Activity`).
3. Append `- HH:MM [[<id>]] — <title>` under `## Activity` (or `## Backfilled` when `_backfilled: true`).

Failures are logged to stderr as warnings and **do not** roll back the created artifact. Manual journal creates are blocked; direct file edits outside `## Activity` are preserved.

## `artifact get`

<ParamField body="--format" type="md | json" default="md">
`md`: raw file bytes. `json`: `{ id, kind, path, frontmatter, body, audit }` where `audit` filters `audit.jsonl` for ops `artifact.create`, `artifact.append`, `artifact.status`, `artifact.set`, `refs.add` touching this id.
</ParamField>

## `artifact list`

Starts from the smallest indexed slice (`--kind` → `byKind`, else `--status` → `byStatus`, else `--domain` → `byDomain`, else all), then intersects additional filters.

| Filter | Index path |
|---|---|
| `--kind <K>` + indexed field (e.g. `--status`, `--priority` on `task`) | Per-kind field index (`indexedFields` in kind def) |
| Other frontmatter flags | Linear scan on candidates |
| `--contains <Q>` | Case-insensitive substring in body (reads files) or string frontmatter values; applied last |

`string[]` frontmatter matches if any element equals the query value.

<RequestExample>

```bash
loopany artifact list --kind task --status running --contains "cache"
```

</RequestExample>

## `artifact append`

Appends or extends an H2 section in the artifact body (immutable pattern: no in-place body rewrite except section merge).

<ParamField body="--section" type="string" required>
Section name without `##` (e.g. `Outcome` → `## Outcome` in the file).
</ParamField>

If the section exists, new content is inserted before the next `##` heading; duplicate `## Outcome` headings are not created.

## `artifact status`

Transitions `status` through the kind’s status machine. Illegal transitions throw (e.g. `todo` → `in_review` on `task`).

<ParamField body="--reason" type="string">
Recorded in JSON stdout only. Does **not** append a `## Status` section to the body.
</ParamField>

<ParamField body="--addressed-by" type="artifact id">
Required when transitioning a `signal` to `addressed`. Emits `refs add` edge `<addressed-by> addresses <signal>` (returns `edgesEmitted: 1`). Invalid on other statuses.
</ParamField>

Kinds without a status machine reject all status changes.

### Task terminal workflow and `## Outcome`

The `task` kind definition requires `## Outcome` in the body when `status` is `done` or `failed`. That rule is documented in `kinds/task.md` and enforced by agent workflows (capture, reflect); **the CLI store does not block** `artifact status … done` without an Outcome section.

Recommended terminal sequence:

<Steps>
<Step title="Append Outcome">
```bash
loopany artifact append <task-id> --section Outcome --content "What shipped or failed, with evidence."
```
Or use `--content-file` for longer write-ups.
</Step>
<Step title="Transition status">
```bash
loopany artifact status <task-id> done --reason "one-line summary"
# or
loopany artifact status <task-id> failed --reason "blocked on X"
```
</Step>
<Step title="Verify">
```bash
loopany artifact get <task-id> --format json
```
Confirm `frontmatter.status` and `body` contains `## Outcome`.
</Step>
</Steps>

| Task status | Outcome expectation (kind contract) |
|---|---|
| `done` | `## Outcome` required (workflow) |
| `failed` | `## Outcome` required (workflow) |
| `cancelled` | `--reason` on status; no Outcome requirement in kind def |

Other kinds with documented required sections (`skill-proposal` on accept/reject, `learning` on supersede/archive) follow the same pattern: append first, status second; runtime validates frontmatter and transitions only.

## `artifact set`

Updates one non-status frontmatter field, re-validates with the kind schema, bumps `updatedAt`.

<ParamField body="--field" type="string" required>
Frontmatter key (camelCase as stored, e.g. `title`, `checkAt`, `domain`).
</ParamField>

<ParamField body="--value" type="string" required>
Coerced per field spec (`string[]` also accepts JSON array syntax).
</ParamField>

- `status` → rejected; use `artifact status`.
- Built-ins without kind specs: `domain`, `createdAt`, `updatedAt`, `_backfilled`.

## Errors and validation

| Failure | Typical message / behavior |
|---|---|
| Unknown kind | `Unknown kind: …` |
| Unknown field flag on create | Lists valid `--<field>` lines for that kind |
| Zod frontmatter | `Invalid input:` with per-field paths (not raw Zod JSON) |
| Duplicate slug | `Slug already exists: …` |
| Illegal status transition | `Illegal transition: … → … (allowed: […])` |
| Missing append body | `Missing body: pass --content … or --content-file …` |
| Literal `\n` in `--content` | Actionable shell/newline guidance |

Successful commands also append best-effort rows to `audit.jsonl` (`op` like `artifact.create`); failures may still log with `error`.

## Lifecycle (create → close)

```mermaid
stateDiagram-v2
  [*] --> todo: artifact create --kind task
  todo --> running: artifact status running
  running --> in_review: artifact status in_review
  in_review --> done: append Outcome then status done
  running --> failed: append Outcome then status failed
  todo --> cancelled: artifact status cancelled
```

```text
  artifact create ──► ArtifactStore.create ──► artifacts/<dir>/<id>.md
        │                      │
        │                      └──► journal auto-link (best-effort)
        │
  artifact append ──► appendSection (body H2)
  artifact status ──► setStatus (frontmatter only)
  artifact set    ──► setField (non-status FM)
```

## Related pages

<CardGroup>
<Card title="Kinds and validation" href="/kinds-and-validation">
Markdown kind defs, Zod schemas, status machines, and slug rules behind `--field` flags.
</Card>
<Card title="Artifacts and workspace" href="/artifacts-and-workspace">
On-disk paths, v0.2 slug-as-id layout, and `$LOOPANY_HOME` structure.
</Card>
<Card title="Artifact lifecycle example" href="/artifact-lifecycle-example">
End-to-end signal → task → Outcome → done with graph edges.
</Card>
<Card title="Capture workflow" href="/capture-workflow">
When to append Outcome before closing tasks in agent sessions.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full command tree, JSON conventions, and adjacent graph/search commands.
</Card>
</CardGroup>
