Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/). This project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.1] - 2026-07-08

### Changed

- Re-initialized CLAUDE.md from the bootstrap seed into a full runtime prompt (via /init): the harmonics non-TTS-audio mission and intent -> sound design-spine table from issue #1, the agent-first CLI architecture (register(sub) command pattern, stdout/stderr + CliError + --json output contract, the tuple-keyed explain catalog, doctor rubric invariants), the uv build/test/lint commands, and the identity/version-bump/cite-dont-import conventions. Documented the command/dist name split (command `harmonics`, dist/identity `harmonics-cli`).
- Made the CLI command name consistent so `harmonics <verb>` is what the docs actually show. The console script stays `harmonics` (matching the `culture`/`teken`/`devex` short-name convention); the runnable-command examples that previously read `harmonics-cli <verb>` — in the README, `learn` output, the `explain` catalog (headings + usage), argparse `prog`, `doctor`'s status header, `cli overview`'s subject, and the module docstrings — now read `harmonics <verb>`. Bare product/identity labels (root `explain`/`overview` page titles, the culture nick, `learn`'s JSON `tool` field, the PyPI dist name) stay `harmonics-cli`. Added `harmonics` as a root `explain` topic alias so `harmonics explain harmonics` resolves alongside `harmonics explain harmonics-cli`.

### Fixed

- Re-synced `uv.lock`, which was stale at `0.3.4` on main, up to the `0.4.1` release, and recorded resolved dev-dependency entries (execnet, flake8).

## [0.4.0] - 2026-06-23

### Added
Expand Down
193 changes: 174 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,183 @@
# CLAUDE.md — seed / bootstrap placeholder
# CLAUDE.md

> **This is a self-initializing seed, not a finished runtime prompt.**
> Run `/init` (or describe the agent's domain to your AI assistant) to
> re-initialize this file into a full runtime prompt, using the description
> below and the scaffolded repo as context.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Agent
## What this repo is

This repository hosts the **harmonics-cli** agent.
**harmonics-cli** is an agent + CLI that gives agents and robots **non-TTS
audio**: it expresses meaning through pleasant sonic signatures — chimes,
flutes, pulses, tonal motifs — mapped to *intent, confidence, urgency, state,
and identity*. It is the inverse of text-to-speech: turn text/sentences into
**notes** and **audio** (text-to-notes / text-to-audio), not words.

## Description
> **Status: scaffold, not yet the harmonics agent.** The code today is the
> AgentCulture *culture-agent-template* — a generic agent-first CLI (`whoami`,
> `learn`, `explain`, `overview`, `doctor`, `cli`). **None of the audio domain
> exists yet.** The self-description strings (in `_build_parser`, `learn`, the
> explain catalog, `overview`) still literally say "a clonable template for
> AgentCulture mesh agents." Your job when building the domain is to (a) add the
> harmonics verbs and synthesis, and (b) replace those placeholder
> self-descriptions with the real mission. The build brief is
> [issue #1](https://github.com/agentculture/harmonics-cli/issues/1) — treat it
> as the source of truth for the domain.

An agent + CLI giving agents and robots non-TTS (non-speech) audio: express meaning through pleasant sonic signatures — chimes, flutes, pulses, and tonal motifs mapped to intent, confidence, urgency, state, and identity. Turns text/sentences into notes and audio (text-to-notes / text-to-audio).
## The design spine — intent → sound mapping

## Re-init instruction
This table is the heart of the project. Everything the domain builds resolves to
it. Finalize and document it before/while shipping the first audio verb.

This file is a seed. To expand it into your full runtime prompt:
| Axis | Example values | Sonic mapping |
|------|---|---|
| **intent** | ack, question, success, failure, thinking, handoff | timbre / motif family (chime vs. flute vs. pulse) |
| **confidence** | low → high | consonance, pitch stability, resolved vs. suspended cadence |
| **urgency** | calm → urgent | tempo, attack sharpness, repetition |
| **state** | idle, working, blocked, done | sustained pad vs. discrete events |
| **identity** | which agent | signature motif / key / instrument per agent |

1. Open this repo in Claude Code (or your preferred AI assistant).
2. Run `/init` — the assistant will read the repo, incorporate the description
above, and replace this seed with a complete `CLAUDE.md`.
3. Commit the result.
Two text-to-audio pathways (both target verbs, neither built yet):

Until you run `/init`, `harmonics-cli` satisfies the `steward doctor`
`prompt-file-present` and `backend-consistency` invariants (a `CLAUDE.md`
exists and `culture.yaml` declares `backend: claude`) but the prompt is not
yet tailored to this agent's domain.
- **Explicit axes** — `harmonics play --intent success --confidence high --urgency low`
- **Inferred from a sentence** — `harmonics say "done, tests all green"` parses
the sentence into axes, then renders.

Output targets: WAV/OGG files, live playback (portaudio / sounddevice /
simpleaudio), and a MIDI / note-sequence representation for robot/synth
consumption. Planned discovery verbs: `motifs` / `signatures` (list the palette
and per-agent identities), and `explain` for the sonic mappings.

**Domain build rules (from issue #1):**

- Keep synthesis **unit-testable offline** — assert on note sequences, not on a
speaker. The pure text→notes core must run with no audio device and no
third-party import in the test path.
- Audio-producing verbs default to **dry-run** (emit the note sequence);
producing sound or a file requires an explicit flag (`--play` / `--out` /
`--apply`). Read/describe verbs never need it.
- Keep the palette **pleasant and non-fatiguing** — it plays repeatedly next to
a human.

## Commands

The dev toolchain is **uv**. Python **3.12+**.

```bash
uv sync # install (incl. dev group)
uv run pytest -n auto # full test suite (xdist parallel)
uv run pytest tests/test_cli.py::test_whoami_json # a single test
uv run pytest -k doctor # tests matching a name
uv run harmonics whoami # run the CLI (see note below)
uv run teken cli doctor . --strict # the agent-first rubric gate CI enforces
```

Lint (all four gate merges in CI — run them before opening a PR):

```bash
uv run black --check harmonics tests
uv run isort --check-only harmonics tests
uv run flake8 harmonics tests
uv run bandit -c pyproject.toml -r harmonics
markdownlint-cli2 "**/*.md" "#node_modules" "#.local" "#.claude/skills" "#.teken"
```

**Name split — the command is `harmonics`; the dist/identity is `harmonics-cli`.**
The PyPI distribution, the culture nick, and product titles are `harmonics-cli`
(`uv tool install harmonics-cli`); the console script you actually run is
`harmonics` (`uv run harmonics whoami`, later `harmonics play …`), matching the
workspace short-name convention (`culture`, `teken`, `devex`). Convention across
code and docs: runnable examples and argparse's `prog` use `harmonics`; bare
product/identity labels (the root `explain`/`overview` page titles, the nick,
`learn`'s JSON `tool` field) stay `harmonics-cli`. The root `explain` topic
resolves under **both** names — `harmonics explain harmonics` (canonical) and
`harmonics explain harmonics-cli` (dist-name alias).

## Architecture

A single argparse tree with a strict machine-first output contract. Read
`harmonics/cli/__init__.py` first — it is the spine.

### Command registration

Every verb/noun lives in its own module under `harmonics/cli/_commands/` and
exposes a `register(sub)` function that adds its subparser and sets
`func=<handler>` via `set_defaults`. `_build_parser()` in
`harmonics/cli/__init__.py` calls each module's `register()`. **To add a verb:**
create the module, implement `cmd_x(args) -> int | None`, add a `register()`,
and wire one `_x_cmd.register(sub)` line into `_build_parser`. Nouns with
sub-verbs nest their own `add_subparsers` (see `_commands/cli.py`) and **must**
pass `parser_class=type(p)` so nested parse errors route through the structured
error contract instead of argparse's default `stderr` + exit 2.

### Output & error contract (stable — agents parse this)

- **Results → stdout, diagnostics/errors → stderr. Never mixed.** All output
goes through `harmonics/cli/_output.py` (`emit_result`, `emit_error`,
`emit_diagnostic`). Don't `print()`.
- **Every verb supports `--json`.** Text and JSON go to the *same* stream; JSON
mode just serializes the payload.
- **Failures raise `CliError`** (`harmonics/cli/_errors.py`) carrying
`{code, message, remediation}`. `main()`/`_dispatch` catch it, format it, and
return the code. Any *other* exception is wrapped so **no Python traceback
ever leaks**. Text-mode errors render as `error: <msg>` + `hint: <remediation>`
— the `hint:` prefix is required by the rubric.
- **Exit codes:** `0` success, `1` user-input error, `2` environment/setup
error, `3+` reserved. Centralized in `_errors.py`.
- **Argparse errors** honor `--json` via a pre-parse trick: `main()` scans raw
argv for `--json` and sets `_CliArgumentParser._json_hint` *before*
`parse_args`, because at parse-error time `args.json` doesn't exist yet.

### The `explain` catalog

`harmonics/explain/catalog.py` holds verbatim markdown keyed by command-path
**tuples** (`("whoami",)`, `("cli", "overview")`; `()` and `("harmonics-cli",)`
both map to root). `resolve()` looks up the tuple or raises `CliError`. The test
`test_every_catalog_path_resolves` walks `known_paths()`, so **every catalog
entry must resolve** — and every new verb should get a catalog entry.

### `doctor` and the agent-first rubric

`doctor` mirrors the invariants `steward doctor` verifies for a mesh agent and
emits the rubric-shaped `{healthy, checks: [{id, passed, severity, message,
remediation}]}`:

- **prompt-file-present / backend-consistency** — the `backend` in `culture.yaml`
must have its matching prompt file on disk. Mapping: `claude → CLAUDE.md`,
`colleague → AGENTS.colleague.md`, `acp → AGENTS.md`, `gemini → GEMINI.md`.
- **skills-present** — the vendored `.claude/skills/` kit exists.

CI runs `teken cli doctor . --strict` (the "afi rubric gate"). It enforces, among
others: `learn` output must be ≥200 chars and mention purpose, the command map,
exit codes, `--json`, and `explain`; any noun exposing action-verbs must also
expose `overview` (why `cli overview` exists); descriptive verbs (`overview`)
must not hard-fail on a stray path argument. Keep these contracts intact.

## Identity & conventions

- **Backend is `colleague`, not `claude`.** `culture.yaml` declares
`backend: colleague` (model `Qwen3.6-27B-...`), so the **resident/runtime**
prompt the mesh daemon loads is `AGENTS.colleague.md` — *this* `CLAUDE.md` is
read by Claude Code only, and does **not** satisfy `doctor`'s
backend-consistency check (`AGENTS.colleague.md` does). (The seed's claim that
it declares `backend: claude` was stale; see CHANGELOG 0.3.4.)
- **`whoami` reads identity from `culture.yaml`** by walking *up from
`__file__`* (not CWD) so it reports this agent's own identity; a wheel install
with no `culture.yaml` falls back to literal defaults. Parsed without a YAML
dependency on purpose.
- **Runtime dependencies stay empty.** `pyproject.toml` `dependencies = []` — the
CLI core is cited from `teken` (`afi-cli`), not installed. Keep the pure
text→notes core dependency-free and offline-testable; isolate optional audio
I/O (playback, encoders) so importing the package never requires a sound stack.
- **`.claude/skills/` is vendored cite-don't-import** from *guildmaster* (with a
few tracked divergences). Do **not** hand-edit skill script bodies to "fix"
them here; re-sync per `docs/skill-sources.md`. That file is the provenance
ledger and re-sync procedure.
- **Version-bump every PR.** CI's `version-check` job fails a PR whose
`pyproject.toml` version equals `main`'s — *even for docs/config/CI-only
changes*. Use `.claude/skills/version-bump/` (or `/version-bump patch|minor|
major`); it also prepends a Keep-a-Changelog entry to `CHANGELOG.md`.
- **PR lane:** the `cicd` skill (layered on `devex pr`) creates PRs, handles
review feedback, and gates on the SonarCloud quality gate + unresolved
threads. SonarCloud project key is `agentculture_harmonics-cli`; `sonar.sources
= harmonics`, coverage floor is 60% (`fail_under`).
- **Publishing** is PyPI Trusted Publishing (OIDC): push to `main` touching
`pyproject.toml` or `harmonics/**` publishes to PyPI; PRs publish a
`.devN` build to TestPyPI. No tokens in the repo.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ An agent + CLI giving agents and robots non-TTS (non-speech) audio: express mean
```bash
uv sync
uv run pytest -n auto # run the test suite
uv run harmonics-cli whoami # identity from culture.yaml
uv run harmonics-cli learn # self-teaching prompt (add --json)
uv run harmonics whoami # identity from culture.yaml
uv run harmonics learn # self-teaching prompt (add --json)
uv run teken cli doctor . --strict # the agent-first rubric gate CI runs
```

## CLI

Installed from PyPI as `harmonics-cli` (`uv tool install harmonics-cli`); the
command you run is `harmonics`.

| Verb | What it does |
|------|--------------|
| `whoami` | Report this agent's nick, version, backend, and model from `culture.yaml`. |
Expand Down
2 changes: 1 addition & 1 deletion harmonics/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def _build_parser() -> argparse.ArgumentParser:
from harmonics.cli._commands import whoami as _whoami_cmd

parser = _CliArgumentParser(
prog="harmonics-cli",
prog="harmonics",
description="harmonics-cli — a clonable template for AgentCulture mesh agents.",
)
parser.add_argument(
Expand Down
8 changes: 4 additions & 4 deletions harmonics/cli/_commands/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""``harmonics-cli cli`` — noun grouping CLI-surface introspection.
"""``harmonics cli`` — noun grouping CLI-surface introspection.

Exists to satisfy the agent-first rubric's ``overview_cli_noun_exists`` check:
any noun with action-verbs must also expose ``overview``. There are no
Expand All @@ -15,22 +15,22 @@

def cmd_cli_overview(args: argparse.Namespace) -> int:
emit_overview(
"harmonics-cli cli",
"harmonics cli",
cli_sections(),
json_mode=bool(getattr(args, "json", False)),
)
return 0


def _no_verb(args: argparse.Namespace) -> int:
# `harmonics-cli cli` with no sub-verb prints the noun's overview.
# `harmonics cli` with no sub-verb prints the noun's overview.
return cmd_cli_overview(args)


def register(sub: argparse._SubParsersAction) -> None:
p = sub.add_parser(
"cli",
help="CLI-surface introspection (see 'harmonics-cli cli overview').",
help="CLI-surface introspection (see 'harmonics cli overview').",
)
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
p.set_defaults(func=_no_verb, json=False)
Expand Down
4 changes: 2 additions & 2 deletions harmonics/cli/_commands/doctor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""``harmonics-cli doctor`` — check the agent-identity invariants.
"""``harmonics doctor`` — check the agent-identity invariants.

Mirrors the two invariants ``steward doctor`` verifies for a mesh agent:

Expand Down Expand Up @@ -105,7 +105,7 @@ def cmd_doctor(args: argparse.Namespace) -> int:
emit_result(report, json_mode=True)
else:
status = "healthy" if report["healthy"] else "unhealthy"
lines = [f"harmonics-cli doctor: {status}", ""]
lines = [f"harmonics doctor: {status}", ""]
for check in report["checks"]:
mark = "ok" if check["passed"] else "FAIL"
lines.append(f"[{mark}] {check['id']}: {check['message']}")
Expand Down
4 changes: 2 additions & 2 deletions harmonics/cli/_commands/explain.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""``harmonics-cli explain <path>...`` — global markdown catalog lookup (stable-contract).
"""``harmonics explain <path>...`` — global markdown catalog lookup (stable-contract).

``explain`` is global (not nested under a noun). It takes zero or more path
tokens and resolves them via the catalog in :mod:`harmonics.explain`.
Expand Down Expand Up @@ -32,7 +32,7 @@ def register(sub: argparse._SubParsersAction) -> None:
p.add_argument(
"path",
nargs="*",
help="Command path tokens; empty = root (same as 'harmonics-cli').",
help="Command path tokens; empty = root (same as 'harmonics').",
)
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
p.set_defaults(func=cmd_explain)
19 changes: 10 additions & 9 deletions harmonics/cli/_commands/learn.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""``harmonics-cli learn`` — the learnability affordance.
"""``harmonics learn`` — the learnability affordance.

Prints a structured self-teaching prompt. Must satisfy the agent-first rubric:
>=200 chars and mention purpose, command map, exit codes, --json, and explain.
Expand All @@ -13,6 +13,7 @@

_TEXT = """\
harmonics-cli — a clonable template for AgentCulture mesh agents.
Installed from PyPI as harmonics-cli; the command you run is `harmonics`.

Purpose
-------
Expand All @@ -23,12 +24,12 @@

Commands
--------
harmonics-cli whoami Identity from culture.yaml.
harmonics-cli learn This self-teaching prompt.
harmonics-cli explain <path>... Markdown docs for any noun/verb path.
harmonics-cli overview Descriptive snapshot of the agent.
harmonics-cli doctor Check the agent-identity invariants.
harmonics-cli cli overview Describe the CLI surface itself.
harmonics whoami Identity from culture.yaml.
harmonics learn This self-teaching prompt.
harmonics explain <path>... Markdown docs for any noun/verb path.
harmonics overview Descriptive snapshot of the agent.
harmonics doctor Check the agent-identity invariants.
harmonics cli overview Describe the CLI surface itself.

Machine-readable output
-----------------------
Expand All @@ -44,7 +45,7 @@

More detail
-----------
harmonics-cli explain harmonics-cli
harmonics explain harmonics
"""


Expand All @@ -67,7 +68,7 @@ def _as_json_payload() -> dict[str, object]:
"2": "environment/setup error",
},
"json_support": True,
"explain_pointer": "harmonics-cli explain <path>",
"explain_pointer": "harmonics explain <path>",
}


Expand Down
2 changes: 1 addition & 1 deletion harmonics/cli/_commands/overview.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""``harmonics-cli overview`` — read-only descriptive snapshot of the agent.
"""``harmonics overview`` — read-only descriptive snapshot of the agent.

Describes the agent to an agent reader: identity (from culture.yaml), the verb
surface, and the sibling-pattern artifacts this template carries. The shared
Expand Down
2 changes: 1 addition & 1 deletion harmonics/cli/_commands/whoami.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""``harmonics-cli whoami`` — the smallest identity probe.
"""``harmonics whoami`` — the smallest identity probe.

Reports the agent's identity as declared in ``culture.yaml``: its nick
(``suffix``), the backend it runs on, and the served model (if any) — plus the
Expand Down
2 changes: 1 addition & 1 deletion harmonics/cli/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from dataclasses import dataclass

# Exit-code policy. Documented in ``harmonics-cli learn`` output.
# Exit-code policy. Documented in ``harmonics learn`` output.
# 0 = success
# 1 = user-input error (bad flag, missing required arg, unknown path)
# 2 = environment / setup error (tool not installed, file unreadable)
Expand Down
2 changes: 1 addition & 1 deletion harmonics/explain/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def resolve(path: tuple[str, ...]) -> str:
raise CliError(
code=EXIT_USER_ERROR,
message=f"no explain entry for: {display}",
remediation="list entries with: harmonics-cli explain harmonics-cli",
remediation="list entries with: harmonics explain harmonics",
)


Expand Down
Loading
Loading