face-cli 0.6.1__tar.gz → 0.7.1__tar.gz

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.
Files changed (82) hide show
  1. {face_cli-0.6.1 → face_cli-0.7.1}/CHANGELOG.md +75 -0
  2. face_cli-0.7.1/CLAUDE.md +325 -0
  3. face_cli-0.7.1/PKG-INFO +113 -0
  4. face_cli-0.7.1/README.md +96 -0
  5. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/__init__.py +26 -2
  6. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_commands/learn.py +17 -8
  7. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_commands/overview.py +9 -1
  8. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_commands/whoami.py +2 -2
  9. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/explain/catalog.py +9 -6
  10. {face_cli-0.6.1 → face_cli-0.7.1}/pyproject.toml +2 -2
  11. {face_cli-0.6.1 → face_cli-0.7.1}/tests/test_cli.py +24 -0
  12. {face_cli-0.6.1 → face_cli-0.7.1}/uv.lock +10 -10
  13. face_cli-0.6.1/CLAUDE.md +0 -28
  14. face_cli-0.6.1/PKG-INFO +0 -76
  15. face_cli-0.6.1/README.md +0 -59
  16. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/agent-config/SKILL.md +0 -0
  17. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  18. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/agent-config/scripts/show.sh +0 -0
  19. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/ask-colleague/SKILL.md +0 -0
  20. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  21. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  22. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  23. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
  24. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  25. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  26. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/challenge/SKILL.md +0 -0
  27. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/cicd/SKILL.md +0 -0
  28. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  29. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  30. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  31. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  32. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  33. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/communicate/SKILL.md +0 -0
  34. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  35. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  36. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  37. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  38. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  39. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  40. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/deviate/SKILL.md +0 -0
  41. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  42. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  43. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  44. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  45. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/recall/SKILL.md +0 -0
  46. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/recall/scripts/recall.sh +0 -0
  47. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/remember/SKILL.md +0 -0
  48. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/remember/scripts/remember.sh +0 -0
  49. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/run-tests/SKILL.md +0 -0
  50. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/run-tests/scripts/test.sh +0 -0
  51. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/scope/SKILL.md +0 -0
  52. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/sonarclaude/SKILL.md +0 -0
  53. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  54. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  55. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  56. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/summarize-delivery/SKILL.md +0 -0
  57. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/think/SKILL.md +0 -0
  58. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/think/scripts/think.sh +0 -0
  59. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/version-bump/SKILL.md +0 -0
  60. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills/version-bump/scripts/bump.py +0 -0
  61. {face_cli-0.6.1 → face_cli-0.7.1}/.claude/skills.local.yaml.example +0 -0
  62. {face_cli-0.6.1 → face_cli-0.7.1}/.flake8 +0 -0
  63. {face_cli-0.6.1 → face_cli-0.7.1}/.github/workflows/publish.yml +0 -0
  64. {face_cli-0.6.1 → face_cli-0.7.1}/.github/workflows/tests.yml +0 -0
  65. {face_cli-0.6.1 → face_cli-0.7.1}/.gitignore +0 -0
  66. {face_cli-0.6.1 → face_cli-0.7.1}/.markdownlint-cli2.yaml +0 -0
  67. {face_cli-0.6.1 → face_cli-0.7.1}/AGENTS.colleague.md +0 -0
  68. {face_cli-0.6.1 → face_cli-0.7.1}/LICENSE +0 -0
  69. {face_cli-0.6.1 → face_cli-0.7.1}/culture.yaml +0 -0
  70. {face_cli-0.6.1 → face_cli-0.7.1}/docs/skill-sources.md +0 -0
  71. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/__init__.py +0 -0
  72. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/__main__.py +0 -0
  73. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_commands/__init__.py +0 -0
  74. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_commands/cli.py +0 -0
  75. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_commands/doctor.py +0 -0
  76. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_commands/explain.py +0 -0
  77. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_errors.py +0 -0
  78. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/cli/_output.py +0 -0
  79. {face_cli-0.6.1 → face_cli-0.7.1}/face_cli/explain/__init__.py +0 -0
  80. {face_cli-0.6.1 → face_cli-0.7.1}/sonar-project.properties +0 -0
  81. {face_cli-0.6.1 → face_cli-0.7.1}/tests/__init__.py +0 -0
  82. {face_cli-0.6.1 → face_cli-0.7.1}/tests/test_cli_introspection.py +0 -0
@@ -5,6 +5,81 @@ All notable changes to this project will be documented in this file.
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/). This project
6
6
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.7.1] - 2026-07-24
9
+
10
+ ### Added
11
+
12
+ - **`--json` is accepted before the verb, not only after it** — `face --json
13
+ whoami` used to fail with `unrecognized arguments: --json` while `face whoami
14
+ --json` worked. Flag-before-verb is a common idiom, and an agent-first CLI
15
+ that rejects it is a trap. The root parser now declares `--json`, and
16
+ `main()` re-asserts it after `parse_args` from the existing raw-argv scan:
17
+ each subparser re-declares `--json` with its own `False` default, which
18
+ silently overwrites a leading flag during sub-parsing. Both orders now
19
+ produce identical output, pinned by a parametrized test.
20
+ - **`face --json` with no verb emits the machine-readable command map** rather
21
+ than an error — the JSON analogue of printing help. It reuses `learn`'s
22
+ payload builder (promoted from `_as_json_payload` to `as_json_payload`), so
23
+ there is one command map, not two.
24
+
25
+ ### Changed
26
+
27
+ - **The CLI's own introspection text now describes this agent** — `face --help`,
28
+ `face learn`, `face explain`, and `face overview` still called the repo "a
29
+ clonable template for AgentCulture mesh agents" and gave clone/rename
30
+ instructions, contradicting the docs 0.7.0 rewrote. Because `learn` and
31
+ `explain` are explicitly the machine-readable teaching surface, that mismatch
32
+ misinformed every agent consuming them, not just human readers. All four now
33
+ state the domain (the expressive output side of a face; recognition belongs to
34
+ the sibling) and the honest status (scaffold — no renderer, no gaze primitive,
35
+ no render verbs yet), and `overview` gained a `Status` section carrying the
36
+ same. The leftover clone/rename framing is also gone from the `whoami` and
37
+ `overview` module docstrings. The `face` / `face-cli` naming split is
38
+ untouched: the console script stays `face` and every help / `learn` /
39
+ `explain` string stays `face-cli`, matching argparse's `prog`.
40
+ - **Package metadata no longer advertises capability that was never requested**
41
+ — `pyproject.toml`'s description promised a "gaze **and expression** surface"
42
+ while `README.md` says "gaze surface" and the build brief records that
43
+ expression was *not* asked for. Published metadata is public-facing (it is
44
+ what PyPI shows), so the conflicting claim was narrowed to match the README.
45
+ This removes the claim rather than settling the scope question, which stays
46
+ open in `CLAUDE.md`; the GitHub repo description, set at provisioning, still
47
+ carries the old wording and should follow whichever way the decision lands.
48
+
49
+ - **Public-facing descriptions no longer read as if the renderer exists** —
50
+ `pyproject.toml`'s description (what PyPI renders, and `publish.yml` pushes a
51
+ release on every merge to `main`) and the root `argparse` description both
52
+ described a working browser-rendered face in the present tense, while
53
+ `learn` / `explain` / `overview` said "scaffold, no renderer". Both now carry
54
+ the same qualifier. This also makes 0.7.1's own claim about `face --help`
55
+ true — it stated the honest status was there before the root description
56
+ actually carried it.
57
+
58
+ The first two items were raised by Qodo's review of
59
+ [#2](https://github.com/agentculture/face-cli/pull/2) (review comments
60
+ `3646920415`, `3646920422`), which merged before the fixes landed — hence a
61
+ follow-up release rather than an amendment. The remaining items came from
62
+ Qodo's review of [#3](https://github.com/agentculture/face-cli/pull/3) (review
63
+ comments `3647009663`, `3647009668`).
64
+
65
+ ## [0.7.0] - 2026-07-24
66
+
67
+ ### Added
68
+
69
+ - **`CLAUDE.md` initialized from the seed** (`/init`) — replaces the bootstrap placeholder with a real runtime prompt: what the repo is *today* (a scaffold, no renderer on disk), the deliberate `face` command / `face_cli` import / `face-cli` dist split and why it must not be "fixed", the four CLI contracts the rubric gate enforces (registration via `register(sub)`, the `CliError` + `_json_hint` error path, the strict stdout/stderr split, the tuple-keyed `explain` catalog), the five places a new verb must touch, and the decided-vs-open state of the face work carried over from build brief agentculture/face-cli#1.
70
+ - **Face-recognition acceptance constraint** recorded in `CLAUDE.md` and `README.md` — the rendered face must survive the same OpenCV pipeline sibling `face-recognition-cli` extracts from `reachy-mini-cli` (`reachy/vision/face.py`): YuNet detection above the default `0.6` score threshold, and a stable SFace 128-dim embedding across the gaze range. This settles the brief's stylised-vs-realistic question at "realistic enough that a detector fires" rather than photorealism, and replaces the untestable "it looks 3D" with an assertion CI can make — a face that stops being detectable at extreme yaw has failed the illusion in a way a screenshot diff would miss.
71
+ - **Dependency guidance for when the renderer lands** — the empty runtime-dependency set is a property of the scaffold, not a law: put a server/screenshot stack behind an extra, import it lazily inside functions, and surface absence as a clean `EXIT_ENV_ERROR` `CliError`, mirroring `reachy-mini-cli`'s `[vision]` pattern.
72
+ - **Template conventions restored** into `CLAUDE.md` after the scaffold flattened them — version-bump-every-PR, the `cicd` PR lane, the `ask-colleague` reflex, verbatim-vendored skills, the `../.worktrees.face-cli/<name>/` worktree convention, and in-repo public memory discipline (`/recall` before, `/remember` after).
73
+
74
+ ### Changed
75
+
76
+ - **`README.md` rewritten for this agent** — it described the `culture-agent-template` clone procedure ("Make it your own", the ~100-place rename sweep) rather than face-cli. Now leads with the scaffold status, the two halves of the problem (depth illusion + pointable gaze), the decided browser render target with its three reasons, the face-recognition bar, and the lane boundary against `face-recognition-cli`.
77
+
78
+ ### Fixed
79
+
80
+ - **Wrong console command throughout `README.md`** — the quickstart said `uv run face-cli whoami` / `uv run face-cli learn`, which fail with `Failed to spawn: face-cli`. The installed entry point is `face` (`[project.scripts] face = "face_cli.cli:main"`); only argparse's `prog` is `face-cli`.
81
+ - **Stale identity claim in the seed** — it stated `culture.yaml` declares `backend: claude`, while the checked-in value is `colleague` (resident prompt `AGENTS.colleague.md`, which is what `doctor` and `steward doctor` actually verify).
82
+
8
83
  ## [0.6.1] - 2026-07-20
9
84
 
10
85
  ### Added
@@ -0,0 +1,325 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this project is
6
+
7
+ `face-cli` is the **expressive output side of a face**: a simulated face
8
+ rendered in the browser that reads as three-dimensional on a flat 2D screen and
9
+ can look in any direction — an agent-drivable gaze surface for robots, kiosks,
10
+ and screens.
11
+
12
+ **Status on disk today: scaffold only.** There is no renderer, no browser
13
+ asset, no gaze primitive, and no `serve` verb in this repo yet. What exists is
14
+ the `culture-agent-template` baseline — an agent-first CLI skeleton, a mesh
15
+ identity, the vendored skill kit, and CI. *The face is the work, and it has not
16
+ started.* Keep that distinction visible when you write docs: describe the repo
17
+ as it is, and mark anything else `(planned)` or put it under a roadmap heading.
18
+
19
+ The authoritative statement of the lane is the build brief,
20
+ [agentculture/face-cli#1](https://github.com/agentculture/face-cli/issues/1).
21
+ Read it before designing anything. This file summarizes the parts that are
22
+ already decided and the parts that are still open; the issue is the source of
23
+ truth and the place to renegotiate.
24
+
25
+ ## Naming — deliberately decoupled, do not "fix"
26
+
27
+ | Thing | Value |
28
+ |---|---|
29
+ | Console command | `face` |
30
+ | Import package | `face_cli` |
31
+ | PyPI distribution | `face-cli` |
32
+ | Argparse `prog` | `face-cli` |
33
+
34
+ The command is `face` but the import package is `face_cli` because a bare
35
+ `face` is a real published PyPI distribution (a `glom` dependency) — squatting
36
+ that top-level import would shadow it. This mismatch is intentional; leave it.
37
+
38
+ Practical consequence: the installed entry point is **`uv run face …`**, not
39
+ `uv run face-cli …`. The help text and `explain` catalog say `face-cli`
40
+ (the `prog`), and `explain` accepts both `face` and `face-cli` as the root key.
41
+
42
+ ## Commands
43
+
44
+ ```bash
45
+ uv sync # create/refresh .venv from uv.lock
46
+ uv run pytest -n auto # full suite (xdist, ~22 tests, <1s)
47
+ uv run pytest tests/test_cli.py::test_whoami_json -v # a single test
48
+ uv run pytest -n auto --cov=face_cli --cov-report=term # coverage (fail_under = 60)
49
+ uv run face whoami # identity from culture.yaml
50
+ uv run face learn --json # self-teaching prompt
51
+ uv run teken cli doctor . --strict # the agent-first rubric gate CI runs
52
+ ```
53
+
54
+ Lint — CI runs all five and any one failing fails the `lint` job:
55
+
56
+ ```bash
57
+ uv run black --check face_cli tests # line-length 100
58
+ uv run isort --check-only face_cli tests
59
+ uv run flake8 face_cli tests
60
+ uv run bandit -c pyproject.toml -r face_cli
61
+ markdownlint-cli2 "**/*.md" "#node_modules" "#.local" "#.claude/skills" "#.teken"
62
+ ```
63
+
64
+ Version bump (required on every PR — see Conventions):
65
+
66
+ ```bash
67
+ python3 .claude/skills/version-bump/scripts/bump.py patch|minor|major
68
+ ```
69
+
70
+ ## Architecture — the CLI as it stands
71
+
72
+ The whole CLI is cited (cite-don't-import) from teken's `python-cli` reference,
73
+ which is why **the runtime package has zero third-party dependencies**;
74
+ `teken` is a dev dependency only. Four contracts hold it together, and the
75
+ rubric gate (`teken cli doctor . --strict`, 26 checks) enforces all of them:
76
+
77
+ - **Registration.** `face_cli/cli/__init__.py::_build_parser` imports each
78
+ module under `cli/_commands/` and calls its `register(sub)`. Every command
79
+ module owns its own parser, flags, and `set_defaults(func=…)`. Adding a verb
80
+ never touches the dispatcher.
81
+ - **Errors.** Every failure raises `CliError(code, message, remediation)`
82
+ (`cli/_errors.py`). `_dispatch` catches it, and wraps *any* other exception
83
+ into one, so a Python traceback can never reach stderr. `_CliArgumentParser`
84
+ overrides `argparse`'s `.error()` so parse failures take the same path and
85
+ exit `1` rather than argparse's default `2`. Because parse errors happen
86
+ before `args.json` exists, `main()` pre-scans raw argv for `--json` and
87
+ stashes it in the class-level `_json_hint` — that is why the flag is peeked
88
+ at twice, and it is load-bearing.
89
+ - **Streams.** `cli/_output.py` is the only writer: results to stdout,
90
+ errors and diagnostics to stderr, never mixed, in both text and JSON mode.
91
+ - **Explain.** `face_cli/explain/catalog.py` maps command-path *tuples* to
92
+ verbatim markdown. `tests/test_cli.py::test_every_catalog_path_resolves`
93
+ walks every key, so an entry that exists must resolve; the rubric separately
94
+ requires an entry for the root and a non-zero exit for a bogus path.
95
+
96
+ Two modules encode mesh identity rather than CLI mechanics:
97
+
98
+ - `cli/_commands/whoami.py` walks up from `__file__` (**not** the CWD) to find
99
+ this agent's own `culture.yaml`, and hand-parses the first agent block
100
+ without a YAML dependency — that is what keeps runtime deps empty. A wheel
101
+ install ships no `culture.yaml`, so it falls back to literal defaults.
102
+ - `cli/_commands/doctor.py` mirrors the two invariants `steward doctor`
103
+ verifies — prompt-file-present and backend-consistency — via the
104
+ `_PROMPT_FILE` map (`claude` → `CLAUDE.md`, `colleague` →
105
+ `AGENTS.colleague.md`, `acp` → `AGENTS.md`, `gemini` → `GEMINI.md`), plus a
106
+ skills-present check. It returns the rubric's
107
+ `{healthy, checks: [{id, passed, severity, message, remediation}]}` shape.
108
+
109
+ ### Adding a verb or noun group
110
+
111
+ Five places, and the tests will catch you if you miss one:
112
+
113
+ 1. A module in `face_cli/cli/_commands/` exposing `register(sub)`; add
114
+ `--json` to every parser you create.
115
+ 2. A `register()` call in `_build_parser()`.
116
+ 3. An entry in `face_cli/explain/catalog.py` keyed by the path tuple.
117
+ 4. The command map inside `cli/_commands/learn.py` — both `_TEXT` and
118
+ `_as_json_payload()`. The rubric greps `learn` output for markers.
119
+ 5. Tests in `tests/`.
120
+
121
+ Any noun group that gains action verbs must also expose `overview`
122
+ (`overview_cli_noun_exists`); descriptive verbs must not hard-fail on a bad
123
+ target path (`overview_graceful_on_bad_path` — see the ignored `target`
124
+ positional in `overview.py`).
125
+
126
+ ### When the renderer lands
127
+
128
+ The empty-runtime-deps property is a feature of the scaffold, not a law of the
129
+ project — a browser renderer will need a server and screenshot tooling. Follow
130
+ the sibling pattern instead of adding hard top-level deps: put them behind an
131
+ extra (`[serve]`, `[vision]`), import them **lazily inside functions**, and
132
+ surface a missing dependency as a clean `CliError` with `EXIT_ENV_ERROR` (2)
133
+ pointing at the extra. `reachy-mini-cli`'s `reachy/vision/face.py` is the
134
+ reference implementation of that pattern in this workspace.
135
+
136
+ ## The face work — what is decided
137
+
138
+ **The render target is the browser.** Decided by the operator; hold the line
139
+ when it gets inconvenient. Three reasons, all still true: no display server is
140
+ needed on a headless robot box or server; the screen showing the face need not
141
+ be the machine running the process; and a page can be screenshotted and diffed
142
+ in CI, which a native window effectively cannot. WebGL vs 2D canvas vs SVG is
143
+ an open technique choice — the operator specified the target, not the method.
144
+
145
+ **The rendered face must survive face recognition.** This is an operator
146
+ requirement layered on the brief: our virtual face should be recognizable *as a
147
+ face, and as a consistent identity*, by the same machinery that recognizes real
148
+ people. The concrete pipeline is the sibling `face-recognition-cli`'s, which
149
+ extracts `reachy-mini-cli`'s OpenCV engine (`reachy/vision/face.py`):
150
+
151
+ - **YuNet** (`face_detection_yunet_2023mar.onnx`) for detection — default
152
+ score threshold `0.6`, NMS `0.3`, `320x320` input, largest-face selection.
153
+ - **SFace** (`face_recognition_sface_2021dec.onnx`) for a 128-dim embedding,
154
+ taken after `alignCrop` on YuNet's five landmarks.
155
+
156
+ That has three consequences worth holding onto:
157
+
158
+ 1. It effectively answers the brief's stylised-vs-realistic question. A face
159
+ abstract enough to be two floating eyes will not fire a YuNet detection at
160
+ `0.6`. The render needs the landmark geometry YuNet regresses — two eyes, a
161
+ nose tip, two mouth corners — in plausible proportion, with enough tonal
162
+ contrast to survive `alignCrop`. It does not need photorealism, and chasing
163
+ photorealism buys the uncanny valley; "passes a detector" is a much cheaper
164
+ and much more testable bar than "looks real".
165
+ 2. It replaces "it looks 3D" with an assertion CI can actually make. Render at
166
+ a known gaze angle → YuNet detects exactly one face above threshold →
167
+ SFace's embedding stays cosine-close to an enrolled reference across the
168
+ whole gaze range. That gates *both* halves of the brief at once: a face that
169
+ stops being detectable at ±40° yaw has failed the 3D illusion in a way a
170
+ screenshot diff would not have caught.
171
+ 3. It defines the composition with the sibling without merging the lanes.
172
+ `face-cli` renders; `face-recognition-cli` enrolls and matches; the loop
173
+ closes when the same identity comes back. That integration is explicit and
174
+ coordinated through issues (use the `communicate` skill), not a merge —
175
+ this repo does no recognition, and that one does no rendering. Note the
176
+ sibling is *also* a bare scaffold today, so the near-term testable path is
177
+ OpenCV's YuNet/SFace directly, behind an extra, with the sibling adopted as
178
+ soon as it has a surface.
179
+
180
+ ### Lane boundaries with siblings
181
+
182
+ - `face-recognition-cli` — perception (identifies real people from frames).
183
+ Shares the subject, nothing else.
184
+ - `reachy-mini-cli` — a physical head that actually turns; the on-screen
185
+ counterpart to this. Plausible that a Reachy gets its face from here; agree
186
+ it with that agent rather than assuming.
187
+ - `storybook-cli` — also renders to a browser, but its output is a *document*;
188
+ this one is a *live surface*.
189
+ - `reterminal-cli` — e-paper; its refresh rate is hostile to continuous gaze.
190
+ Not a first target.
191
+
192
+ ## The face work — what is still open
193
+
194
+ Decide these deliberately and **record the decision** (a `docs/` note plus a
195
+ CHANGELOG entry); do not let them get settled by accident in a commit:
196
+
197
+ - **How a one-shot CLI call moves an already-rendered face.** Served page plus
198
+ a control channel (websocket / SSE / polled state file), a state file the
199
+ page watches, or something else.
200
+ - **Whether there is a `serve` verb** that owns the page, with `look` /
201
+ `express` driving it — and what happens when nothing is serving: error, or
202
+ autostart.
203
+ - **The gaze primitive.** Yaw/pitch degrees, a normalized direction vector, a
204
+ screen-space point, or a named target. Whatever it is, an agent must be able
205
+ to compute it with no human in the loop.
206
+ - **Whether expression is in scope at all.** The operator asked for direction.
207
+ Expression is the obvious neighbour and was *not* requested — decide
208
+ explicitly rather than drifting into it. Published metadata no longer
209
+ pre-answers it: `pyproject.toml` used to advertise a "gaze **and expression**
210
+ surface" while nothing of the sort exists, so its description was narrowed to
211
+ match the README. That removes the claim, it does not settle the question —
212
+ the GitHub repo description (set at provisioning) still carries the old
213
+ wording and should follow whichever way the decision lands.
214
+ - **What `--json` returns for a verb whose real output is pixels.** A
215
+ screenshot path is one honest answer; the resolved gaze state is another.
216
+ - **Who runs the render loop** — browser-side animation with the CLI setting
217
+ targets (almost certainly right) or process-side frame pushing.
218
+ - **One face or many** per host.
219
+ - **Pre-rendered assets and their licence**, if any ship. A public repo with
220
+ vendored face art needs that answer before the first release.
221
+
222
+ The path for deciding them is the vendored workflow chain: `/scope` → `/think`
223
+ → `/challenge` → `/spec-to-plan`, then `/assign-to-workforce` if the plan wants
224
+ parallel lanes.
225
+
226
+ ## Identity
227
+
228
+ ```yaml
229
+ agents:
230
+ - suffix: face-cli
231
+ backend: colleague
232
+ model: sakamakismile/Qwen3.6-27B-Text-NVFP4-MTP
233
+ ```
234
+
235
+ `backend: colleague` fixes the resident prompt file to **`AGENTS.colleague.md`**
236
+ — the mesh runtime reads that file, while this `CLAUDE.md` is the Claude Code
237
+ guidance file. Both `steward doctor` and `face doctor` check the pair, and it
238
+ currently passes. (The scaffold seed this file replaced claimed
239
+ `backend: claude`; that was wrong — the checked-in value is `colleague`.) The
240
+ build brief asks that `culture.yaml` be reconciled against the backend actually
241
+ run; if this agent is promoted to `claude`, flipping the `backend` value is the
242
+ whole change — `doctor` already maps `claude` → `CLAUDE.md`, but
243
+ `tests/test_cli.py` asserts `backend: colleague` in two places and would need
244
+ updating with it.
245
+
246
+ ## Conventions and workflow
247
+
248
+ - **Every PR bumps the version** — even docs/config/CI-only PRs. Use the
249
+ `version-bump` skill; the `version-check` CI job blocks merge otherwise, and
250
+ the bump script leaves the CHANGELOG sections blank, so write a real entry.
251
+ - **PRs go through the `cicd` skill** (`devex pr` + SonarCloud gating). Sign
252
+ online posts as `- face-cli (Claude)`; the `cicd` / `communicate` scripts
253
+ resolve that nick from `culture.yaml` automatically, so do not hand-sign in
254
+ bodies those scripts author.
255
+ - **Reach for `ask-colleague` reflexively.** Its value is a *second,
256
+ independent mind* (a different backend/model), not a stronger one. Before
257
+ presenting or opening a PR on a non-trivial committed diff, run `review`; for
258
+ a fresh read of an unfamiliar area, run `explore`. Both are read-only in a
259
+ throwaway worktree, so the reflex is always safe. `write --apply` / `--pr`
260
+ needs the user's go-ahead. Treat its output as an opinion to verify and own.
261
+ - **The vendored `.claude/skills/` are cited verbatim** — do not reformat or
262
+ edit their scripts. Re-sync from guildmaster (or the tracked direct-from-origin
263
+ exceptions) per `docs/skill-sources.md`. Prerequisites on PATH: `devex`
264
+ (>=0.21) and `agtag` (>=0.1); `colleague` is optional.
265
+ - **Deploy**: pushing to `main` publishes to PyPI via Trusted Publishing;
266
+ PRs do a TestPyPI dry-run. Both jobs only fire on changes to `pyproject.toml`
267
+ or `face_cli/**`. A PyPI / TestPyPI Trusted Publisher must be registered for
268
+ `face-cli` before the first real release — `guild create` configured the
269
+ GitHub side only.
270
+
271
+ ### Worktrees
272
+
273
+ Every worktree you create by hand lives in one repo-named directory beside the
274
+ checkout, one subfolder per worktree:
275
+
276
+ ```bash
277
+ git worktree add ../.worktrees.face-cli/<name> -b <branch>
278
+ ```
279
+
280
+ Never a shared `../worktrees/`: this workspace holds ~150 sibling projects, and
281
+ a generic shared folder accumulates orphaned trees from several repos at once
282
+ with nothing indicating ownership — a stale-tree sweep cannot tell a live lane
283
+ from junk. Scope the branch prefix to the work (`gaze/t2`, not `agent/t2`);
284
+ plain `agent/*` collides with leftovers from earlier fan-outs and `git worktree
285
+ add -b` fails on an existing branch. The vendored `assign-to-workforce` skill's
286
+ fan-out example uses *both* the shared path and `agent/<task-id>` branches —
287
+ it is cited verbatim and must not be edited, so override both when following
288
+ it. Remove with `git worktree remove <path>` (which deletes the directory);
289
+ `git worktree prune` only clears metadata for directories already gone. Never
290
+ `rm -rf` a worktree you did not create. Exception: `ask-colleague`'s read-only
291
+ verbs create their own detached worktree under `${TMPDIR:-/tmp}` and reap it on
292
+ an EXIT trap — outside this rule, not a violation of it.
293
+
294
+ ### Memory discipline — recall before, remember after
295
+
296
+ This repo keeps its eidetic memory **in-repo and public**: a plain `/remember`
297
+ resolves to `<repo-root>/.eidetic/memory` — committed, shared with the team and
298
+ mesh peers, with the `claude` and `colleague` backends reading the same
299
+ `face-cli` scope. So memory travels with the repo rather than a private
300
+ home-dir store.
301
+
302
+ - **`/recall` before you start** a non-trivial task — prior decisions, gotchas,
303
+ "have we done this before?" — so you build on what is known.
304
+ - **`/remember` when something worth keeping surfaces**: a non-obvious decision
305
+ and its rationale, a constraint, a fix and *why*, a gotcha that cost time.
306
+ Capture it as it happens.
307
+
308
+ Pass `--visibility private` to keep a record out of the committed store (it
309
+ routes to `$HOME/.eidetic/memory`); `/recall` reads both and merges. Do not
310
+ store what the repo already records — code structure, git history, or anything
311
+ already in this file or `CHANGELOG.md`.
312
+
313
+ ## Layout
314
+
315
+ ```text
316
+ face_cli/ agent-first CLI (cited from teken's python-cli reference)
317
+ cli/ parser, error/output contract, _commands/ (verbs)
318
+ explain/ markdown catalog for `explain`
319
+ tests/ pytest smoke + introspection tests
320
+ .claude/skills/ vendored guildmaster/devague/colleague skill kit (verbatim)
321
+ docs/skill-sources.md skill provenance ledger + re-sync procedure
322
+ culture.yaml mesh identity (suffix + backend)
323
+ AGENTS.colleague.md resident prompt for the colleague backend
324
+ .github/workflows/ tests + lint + version-check; PyPI Trusted Publishing
325
+ ```
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: face-cli
3
+ Version: 0.7.1
4
+ Summary: Scaffold for a browser-rendered face that reads three-dimensional on a flat 2D screen and looks in any direction — an agent-drivable gaze surface for robots, kiosks, and screens. The renderer is not implemented yet; the CLI ships introspection verbs only.
5
+ Project-URL: Homepage, https://github.com/agentculture/face-cli
6
+ Project-URL: Issues, https://github.com/agentculture/face-cli/issues
7
+ Author: AgentCulture
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: Apache Software License
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development
15
+ Requires-Python: >=3.12
16
+ Description-Content-Type: text/markdown
17
+
18
+ # face-cli
19
+
20
+ A simulated face rendered in the browser that appears three-dimensional on a
21
+ flat 2D screen and can look in any direction — an agent-drivable gaze surface
22
+ for robots, kiosks, and screens.
23
+
24
+ > **Status: scaffold.** The renderer does not exist yet. What ships today is
25
+ > the agent-first CLI baseline, the mesh identity, and CI. The face itself is
26
+ > being designed in the open — see [the build brief][brief] and
27
+ > [`CLAUDE.md`](CLAUDE.md).
28
+
29
+ [brief]: https://github.com/agentculture/face-cli/issues/1
30
+
31
+ ## The idea
32
+
33
+ Two halves, and both are the interesting part: the **illusion of depth** on a
34
+ flat surface, and a **gaze that can be pointed anywhere** and reads as pointed
35
+ there. Everything else is in service of those.
36
+
37
+ `face-cli` is the *expressive output* side of a face. Its sibling
38
+ [`face-recognition-cli`](https://github.com/agentculture/face-recognition-cli)
39
+ is the *perceptual input* side — it identifies real people from camera frames.
40
+ They share a subject and nothing else.
41
+
42
+ ## Decided so far
43
+
44
+ - **The render target is the browser** — a local page driven over
45
+ canvas/WebGL/SVG. It needs no display server (so it runs on a headless robot
46
+ box), the screen showing the face need not be the machine running the
47
+ process, and a page can be screenshotted and diffed in CI. The *technique*
48
+ within the browser is still open.
49
+ - **The rendered face must survive face recognition.** Not photorealism — a
50
+ detector-passing bar: OpenCV **YuNet** should detect it above the standard
51
+ `0.6` score threshold, and **SFace**'s 128-dim embedding should stay stable
52
+ across the whole gaze range. That turns "it looks 3D" into an assertion CI
53
+ can actually make, and it is the seam along which this repo composes with
54
+ `face-recognition-cli`.
55
+
56
+ Open questions — the gaze primitive, the control channel between a one-shot CLI
57
+ call and a live page, whether expression is in scope, one face or many — are
58
+ tracked in [`CLAUDE.md`](CLAUDE.md) and [the brief][brief].
59
+
60
+ ## Quickstart
61
+
62
+ ```bash
63
+ uv sync
64
+ uv run pytest -n auto # run the test suite
65
+ uv run face whoami # identity from culture.yaml
66
+ uv run face learn # self-teaching prompt (add --json)
67
+ uv run teken cli doctor . --strict # the agent-first rubric gate CI runs
68
+ ```
69
+
70
+ The console command is **`face`**; the import package is `face_cli` and the
71
+ PyPI distribution is `face-cli`. The mismatch is deliberate — a bare `face` is
72
+ already a published distribution on PyPI, so squatting that import would shadow
73
+ it.
74
+
75
+ ## CLI
76
+
77
+ | Verb | What it does |
78
+ |------|--------------|
79
+ | `whoami` | Report this agent's nick, version, backend, and model from `culture.yaml`. |
80
+ | `learn` | Print a structured self-teaching prompt. |
81
+ | `explain <path>` | Markdown docs for any noun/verb path. |
82
+ | `overview` | Read-only descriptive snapshot of the agent. |
83
+ | `doctor` | Check the agent-identity invariants (prompt-file-present, backend-consistency). |
84
+ | `cli overview` | Describe the CLI surface itself. |
85
+
86
+ Every command supports `--json`. Results go to stdout, errors/diagnostics to
87
+ stderr (never mixed). Exit codes: `0` success, `1` user error, `2` environment
88
+ error, `3+` reserved.
89
+
90
+ No gaze or render verbs exist yet — the list above is the scaffold's
91
+ introspection surface.
92
+
93
+ ## What's in the box
94
+
95
+ - **An agent-first CLI** cited from [teken](https://github.com/agentculture/teken)
96
+ (`afi-cli`) — the runtime package has no third-party dependencies.
97
+ - **A mesh identity** — `culture.yaml` (`suffix` + `backend`) and the matching
98
+ resident prompt file (`AGENTS.colleague.md`, since this agent runs
99
+ `backend: colleague`).
100
+ - **The canonical guildmaster skill kit** under `.claude/skills/`, vendored
101
+ cite-don't-import. See [`docs/skill-sources.md`](docs/skill-sources.md).
102
+ - **A build + deploy baseline** — pytest, lint, the agent-first rubric gate, and
103
+ PyPI Trusted Publishing wired into GitHub Actions.
104
+
105
+ ## Contributing
106
+
107
+ Every PR bumps the version (`version-check` CI blocks merge otherwise) and adds
108
+ a real CHANGELOG entry. See [`CLAUDE.md`](CLAUDE.md) for the full conventions —
109
+ the `cicd` PR lane, worktree layout, and deploy setup.
110
+
111
+ ## License
112
+
113
+ Apache 2.0 — see [`LICENSE`](LICENSE).
@@ -0,0 +1,96 @@
1
+ # face-cli
2
+
3
+ A simulated face rendered in the browser that appears three-dimensional on a
4
+ flat 2D screen and can look in any direction — an agent-drivable gaze surface
5
+ for robots, kiosks, and screens.
6
+
7
+ > **Status: scaffold.** The renderer does not exist yet. What ships today is
8
+ > the agent-first CLI baseline, the mesh identity, and CI. The face itself is
9
+ > being designed in the open — see [the build brief][brief] and
10
+ > [`CLAUDE.md`](CLAUDE.md).
11
+
12
+ [brief]: https://github.com/agentculture/face-cli/issues/1
13
+
14
+ ## The idea
15
+
16
+ Two halves, and both are the interesting part: the **illusion of depth** on a
17
+ flat surface, and a **gaze that can be pointed anywhere** and reads as pointed
18
+ there. Everything else is in service of those.
19
+
20
+ `face-cli` is the *expressive output* side of a face. Its sibling
21
+ [`face-recognition-cli`](https://github.com/agentculture/face-recognition-cli)
22
+ is the *perceptual input* side — it identifies real people from camera frames.
23
+ They share a subject and nothing else.
24
+
25
+ ## Decided so far
26
+
27
+ - **The render target is the browser** — a local page driven over
28
+ canvas/WebGL/SVG. It needs no display server (so it runs on a headless robot
29
+ box), the screen showing the face need not be the machine running the
30
+ process, and a page can be screenshotted and diffed in CI. The *technique*
31
+ within the browser is still open.
32
+ - **The rendered face must survive face recognition.** Not photorealism — a
33
+ detector-passing bar: OpenCV **YuNet** should detect it above the standard
34
+ `0.6` score threshold, and **SFace**'s 128-dim embedding should stay stable
35
+ across the whole gaze range. That turns "it looks 3D" into an assertion CI
36
+ can actually make, and it is the seam along which this repo composes with
37
+ `face-recognition-cli`.
38
+
39
+ Open questions — the gaze primitive, the control channel between a one-shot CLI
40
+ call and a live page, whether expression is in scope, one face or many — are
41
+ tracked in [`CLAUDE.md`](CLAUDE.md) and [the brief][brief].
42
+
43
+ ## Quickstart
44
+
45
+ ```bash
46
+ uv sync
47
+ uv run pytest -n auto # run the test suite
48
+ uv run face whoami # identity from culture.yaml
49
+ uv run face learn # self-teaching prompt (add --json)
50
+ uv run teken cli doctor . --strict # the agent-first rubric gate CI runs
51
+ ```
52
+
53
+ The console command is **`face`**; the import package is `face_cli` and the
54
+ PyPI distribution is `face-cli`. The mismatch is deliberate — a bare `face` is
55
+ already a published distribution on PyPI, so squatting that import would shadow
56
+ it.
57
+
58
+ ## CLI
59
+
60
+ | Verb | What it does |
61
+ |------|--------------|
62
+ | `whoami` | Report this agent's nick, version, backend, and model from `culture.yaml`. |
63
+ | `learn` | Print a structured self-teaching prompt. |
64
+ | `explain <path>` | Markdown docs for any noun/verb path. |
65
+ | `overview` | Read-only descriptive snapshot of the agent. |
66
+ | `doctor` | Check the agent-identity invariants (prompt-file-present, backend-consistency). |
67
+ | `cli overview` | Describe the CLI surface itself. |
68
+
69
+ Every command supports `--json`. Results go to stdout, errors/diagnostics to
70
+ stderr (never mixed). Exit codes: `0` success, `1` user error, `2` environment
71
+ error, `3+` reserved.
72
+
73
+ No gaze or render verbs exist yet — the list above is the scaffold's
74
+ introspection surface.
75
+
76
+ ## What's in the box
77
+
78
+ - **An agent-first CLI** cited from [teken](https://github.com/agentculture/teken)
79
+ (`afi-cli`) — the runtime package has no third-party dependencies.
80
+ - **A mesh identity** — `culture.yaml` (`suffix` + `backend`) and the matching
81
+ resident prompt file (`AGENTS.colleague.md`, since this agent runs
82
+ `backend: colleague`).
83
+ - **The canonical guildmaster skill kit** under `.claude/skills/`, vendored
84
+ cite-don't-import. See [`docs/skill-sources.md`](docs/skill-sources.md).
85
+ - **A build + deploy baseline** — pytest, lint, the agent-first rubric gate, and
86
+ PyPI Trusted Publishing wired into GitHub Actions.
87
+
88
+ ## Contributing
89
+
90
+ Every PR bumps the version (`version-check` CI blocks merge otherwise) and adds
91
+ a real CHANGELOG entry. See [`CLAUDE.md`](CLAUDE.md) for the full conventions —
92
+ the `cicd` PR lane, worktree layout, and deploy setup.
93
+
94
+ ## License
95
+
96
+ Apache 2.0 — see [`LICENSE`](LICENSE).
@@ -26,7 +26,7 @@ import sys
26
26
 
27
27
  from face_cli import __version__
28
28
  from face_cli.cli._errors import EXIT_USER_ERROR, CliError
29
- from face_cli.cli._output import emit_error
29
+ from face_cli.cli._output import emit_error, emit_result
30
30
 
31
31
  _ISSUES_URL = "https://github.com/agentculture/face-cli/issues"
32
32
 
@@ -71,13 +71,23 @@ def _build_parser() -> argparse.ArgumentParser:
71
71
 
72
72
  parser = _CliArgumentParser(
73
73
  prog="face-cli",
74
- description="face-cli — a clonable template for AgentCulture mesh agents.",
74
+ description="face-cli — a browser-rendered face that reads 3D on a 2D screen "
75
+ "and looks in any direction. Scaffold: no renderer or gaze verbs yet; the "
76
+ "verbs below are the introspection surface.",
75
77
  )
76
78
  parser.add_argument(
77
79
  "--version",
78
80
  action="version",
79
81
  version=f"%(prog)s {__version__}",
80
82
  )
83
+ # Accepted before the verb too (`face --json whoami`), not just after it.
84
+ # argparse gives each subparser its own `json` default, so a leading flag is
85
+ # clobbered on the way down; main() re-asserts it from the raw-argv scan.
86
+ parser.add_argument(
87
+ "--json",
88
+ action="store_true",
89
+ help="Emit structured JSON. Accepted before or after the verb.",
90
+ )
81
91
  # parser_class propagates to every subparser so their .error() routes
82
92
  # through _CliArgumentParser too.
83
93
  sub = parser.add_subparsers(dest="command", parser_class=_CliArgumentParser)
@@ -125,7 +135,21 @@ def main(argv: list[str] | None = None) -> int:
125
135
  parser = _build_parser()
126
136
  args = parser.parse_args(argv)
127
137
 
138
+ # Position must not change meaning: each subparser re-declares `--json` with
139
+ # its own `False` default, which overwrites a leading `--json` during
140
+ # sub-parsing. Re-assert from the raw-argv scan so `face --json whoami` and
141
+ # `face whoami --json` behave identically.
142
+ if _CliArgumentParser._json_hint:
143
+ args.json = True
144
+
128
145
  if args.command is None:
146
+ # No verb: the JSON analogue of printing help is the machine-readable
147
+ # command map, which `learn` already owns.
148
+ if getattr(args, "json", False):
149
+ from face_cli.cli._commands.learn import as_json_payload
150
+
151
+ emit_result(as_json_payload(), json_mode=True)
152
+ return 0
129
153
  parser.print_help()
130
154
  return 0
131
155