arm101-cli 0.6.0__tar.gz → 0.7.0__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 (115) hide show
  1. arm101_cli-0.7.0/.devague/current +1 -0
  2. arm101_cli-0.7.0/.devague/current_plan +1 -0
  3. arm101_cli-0.7.0/.devague/frames/arm101-s-gated-write-hardware-verbs-set-motor-id-c.json +294 -0
  4. arm101_cli-0.7.0/.devague/plans/arm101-s-gated-write-hardware-verbs-set-motor-id-c.json +250 -0
  5. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/CHANGELOG.md +22 -0
  6. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/PKG-INFO +1 -1
  7. arm101_cli-0.7.0/arm101/cli/_commands/center_motor.py +262 -0
  8. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/learn.py +16 -6
  9. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/overview.py +2 -2
  10. arm101_cli-0.7.0/arm101/cli/_commands/set_motor_id.py +264 -0
  11. arm101_cli-0.7.0/arm101/cli/_consent.py +567 -0
  12. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/explain/catalog.py +41 -16
  13. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/hardware/bus.py +67 -0
  14. arm101_cli-0.7.0/docs/plans/2026-06-27-arm101-s-gated-write-hardware-verbs-set-motor-id-c.md +63 -0
  15. arm101_cli-0.7.0/docs/specs/2026-06-27-arm101-s-gated-write-hardware-verbs-set-motor-id-c.md +64 -0
  16. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/pyproject.toml +1 -1
  17. arm101_cli-0.7.0/tests/conftest.py +23 -0
  18. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_bus.py +80 -3
  19. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_center_motor.py +233 -12
  20. arm101_cli-0.7.0/tests/test_consent.py +835 -0
  21. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_set_motor_id.py +55 -8
  22. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/uv.lock +1 -1
  23. arm101_cli-0.6.0/.devague/current +0 -1
  24. arm101_cli-0.6.0/.devague/current_plan +0 -1
  25. arm101_cli-0.6.0/arm101/cli/_commands/center_motor.py +0 -182
  26. arm101_cli-0.6.0/arm101/cli/_commands/set_motor_id.py +0 -191
  27. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/agent-config/SKILL.md +0 -0
  28. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/agent-config/data/backend-fingerprints.yaml +0 -0
  29. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/agent-config/scripts/show.sh +0 -0
  30. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/ask-colleague/SKILL.md +0 -0
  31. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/ask-colleague/prompts/explore.md +0 -0
  32. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/ask-colleague/prompts/review.md +0 -0
  33. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/ask-colleague/prompts/write.md +0 -0
  34. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/ask-colleague/scripts/ask-colleague.sh +0 -0
  35. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/assign-to-workforce/SKILL.md +0 -0
  36. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/assign-to-workforce/scripts/assign-to-workforce.sh +0 -0
  37. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/cicd/SKILL.md +0 -0
  38. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/cicd/scripts/_resolve-nick.sh +0 -0
  39. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/cicd/scripts/portability-lint.sh +0 -0
  40. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/cicd/scripts/pr-reply.sh +0 -0
  41. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/cicd/scripts/pr-status.sh +0 -0
  42. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/cicd/scripts/workflow.sh +0 -0
  43. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/communicate/SKILL.md +0 -0
  44. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/communicate/scripts/fetch-issues.sh +0 -0
  45. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/communicate/scripts/mesh-message.sh +0 -0
  46. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/communicate/scripts/post-comment.sh +0 -0
  47. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/communicate/scripts/post-issue.sh +0 -0
  48. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/communicate/scripts/templates/skill-new-brief.md +0 -0
  49. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/communicate/scripts/templates/skill-update-brief.md +0 -0
  50. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/doc-test-alignment/SKILL.md +0 -0
  51. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/doc-test-alignment/scripts/check.sh +0 -0
  52. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/pypi-maintainer/SKILL.md +0 -0
  53. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/pypi-maintainer/scripts/switch-source.sh +0 -0
  54. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/recall/SKILL.md +0 -0
  55. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/recall/scripts/recall.sh +0 -0
  56. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/remember/SKILL.md +0 -0
  57. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/remember/scripts/remember.sh +0 -0
  58. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/run-tests/SKILL.md +0 -0
  59. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
  60. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/sonarclaude/SKILL.md +0 -0
  61. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/sonarclaude/scripts/sonar.sh +0 -0
  62. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/spec-to-plan/SKILL.md +0 -0
  63. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/spec-to-plan/scripts/spec-to-plan.sh +0 -0
  64. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/think/SKILL.md +0 -0
  65. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/think/scripts/think.sh +0 -0
  66. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/version-bump/SKILL.md +0 -0
  67. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills/version-bump/scripts/bump.py +0 -0
  68. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.claude/skills.local.yaml.example +0 -0
  69. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.devague/frames/arm101-cli-grows-its-first-hardware-verbs-arm101-f.json +0 -0
  70. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.devague/plans/arm101-cli-grows-its-first-hardware-verbs-arm101-f.json +0 -0
  71. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.flake8 +0 -0
  72. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.github/workflows/publish.yml +0 -0
  73. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.github/workflows/tests.yml +0 -0
  74. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.gitignore +0 -0
  75. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/.markdownlint-cli2.yaml +0 -0
  76. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/AGENTS.colleague.md +0 -0
  77. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/CLAUDE.md +0 -0
  78. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/LICENSE +0 -0
  79. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/README.md +0 -0
  80. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/__init__.py +0 -0
  81. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/__main__.py +0 -0
  82. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/__init__.py +0 -0
  83. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/__init__.py +0 -0
  84. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/calibrate.py +0 -0
  85. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/calibrate_motor.py +0 -0
  86. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/cli.py +0 -0
  87. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/doctor.py +0 -0
  88. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/explain.py +0 -0
  89. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/find_port.py +0 -0
  90. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/setup_motors.py +0 -0
  91. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_commands/whoami.py +0 -0
  92. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_errors.py +0 -0
  93. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/cli/_output.py +0 -0
  94. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/explain/__init__.py +0 -0
  95. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/hardware/__init__.py +0 -0
  96. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/hardware/motor_catalog.py +0 -0
  97. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/hardware/ports.py +0 -0
  98. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/arm101/hardware/profiles.py +0 -0
  99. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/culture.yaml +0 -0
  100. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/docs/hardware-validation.md +0 -0
  101. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/docs/plans/2026-06-26-arm101-cli-grows-its-first-hardware-verbs-arm101-f.md +0 -0
  102. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/docs/skill-sources.md +0 -0
  103. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/docs/specs/2026-06-26-arm101-cli-grows-its-first-hardware-verbs-arm101-f.md +0 -0
  104. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/sonar-project.properties +0 -0
  105. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/__init__.py +0 -0
  106. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_calibrate.py +0 -0
  107. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_calibrate_motor.py +0 -0
  108. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_cli.py +0 -0
  109. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_cli_introspection.py +0 -0
  110. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_find_port.py +0 -0
  111. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_hardware_verbs.py +0 -0
  112. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_motor_catalog.py +0 -0
  113. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_ports.py +0 -0
  114. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_profiles.py +0 -0
  115. {arm101_cli-0.6.0 → arm101_cli-0.7.0}/tests/test_setup_motors.py +0 -0
@@ -0,0 +1 @@
1
+ arm101-s-gated-write-hardware-verbs-set-motor-id-c
@@ -0,0 +1 @@
1
+ arm101-s-gated-write-hardware-verbs-set-motor-id-c
@@ -0,0 +1,294 @@
1
+ {
2
+ "slug": "arm101-s-gated-write-hardware-verbs-set-motor-id-c",
3
+ "title": "arm101's gated-write hardware verbs (set-motor-id, center-motor) now resolve consent in three driver modes \u2014 human-interactive (type yes at a TTY), agent-interactive (an AI reads a structured write-plan and consents explicitly), and non-interactive (scripted) \u2014 so an AI agent can safely drive an EEPROM write or a commanded motion without faking a TTY, while the unconsented non-TTY default still refuses.",
4
+ "schema_version": 1,
5
+ "status": "exported",
6
+ "created": "2026-06-27T10:35:04Z",
7
+ "updated": "2026-06-27T11:06:51Z",
8
+ "claims": [
9
+ {
10
+ "id": "c1",
11
+ "kind": "announcement",
12
+ "text": "arm101's gated-write hardware verbs (set-motor-id, center-motor) now resolve consent in three driver modes \u2014 human-interactive (type yes at a TTY), agent-interactive (an AI reads a structured write-plan and consents explicitly), and non-interactive (scripted) \u2014 so an AI agent can safely drive an EEPROM write or a commanded motion without faking a TTY, while the unconsented non-TTY default still refuses.",
13
+ "origin": "user",
14
+ "status": "confirmed",
15
+ "honesty_conditions": [
16
+ {
17
+ "id": "h4",
18
+ "text": "Each of the three modes is pinned by a test: TTY->interactive prompt; non-TTY+consent->execute (1-step --apply for set-motor-id, 2-step --apply --plan-hash for center-motor); non-TTY+no-consent->refuse (exit 2).",
19
+ "status": "confirmed"
20
+ }
21
+ ],
22
+ "hard_questions": [],
23
+ "links": []
24
+ },
25
+ {
26
+ "id": "c2",
27
+ "kind": "audience",
28
+ "text": "Three operator types of the SO-101 pre-assembly flow: a human at a bench terminal, an AI agent (Claude Code) driving the arm101 CLI, and scripted/CI callers with no one present.",
29
+ "origin": "llm",
30
+ "status": "confirmed",
31
+ "honesty_conditions": [
32
+ {
33
+ "id": "h7",
34
+ "text": "All three operator types exercise the SAME verbs through one resolver \u2014 a test demonstrates a human-TTY prompt path, an agent --apply path, and a non-TTY no-consent refusal.",
35
+ "status": "confirmed"
36
+ }
37
+ ],
38
+ "hard_questions": [],
39
+ "links": []
40
+ },
41
+ {
42
+ "id": "c3",
43
+ "kind": "after_state",
44
+ "text": "Every gated side-effect verb resolves consent through ONE shared mode-aware helper; an agent can drive set-motor-id/center-motor to completion with explicit consent and machine-readable I/O; a human still types yes at a TTY; a bare non-TTY run with no consent flag still refuses (exit 2).",
45
+ "origin": "llm",
46
+ "status": "confirmed",
47
+ "honesty_conditions": [
48
+ {
49
+ "id": "h8",
50
+ "text": "After the change neither verb has its own duplicated gate \u2014 both import resolve_consent from arm101/cli/_consent.py (asserted by an import/grep test); the human path is behaviorally unchanged.",
51
+ "status": "confirmed"
52
+ }
53
+ ],
54
+ "hard_questions": [],
55
+ "links": []
56
+ },
57
+ {
58
+ "id": "c4",
59
+ "kind": "before_state",
60
+ "text": "set-motor-id/center-motor call _require_tty() and HARD-refuse any non-TTY stdin (exit 2). The only consent channel is a human typing yes at a TTY, so the agent operator can never drive the write \u2014 forcing PTY-faking hacks that defeat the very gate they bypass.",
61
+ "origin": "llm",
62
+ "status": "confirmed",
63
+ "honesty_conditions": [
64
+ {
65
+ "id": "h9",
66
+ "text": "The CURRENT code is accurately described: set_motor_id._require_tty and center_motor._require_tty both raise exit 2 on non-TTY today (the pre-change tests we are replacing assert exactly this).",
67
+ "status": "confirmed"
68
+ }
69
+ ],
70
+ "hard_questions": [],
71
+ "links": []
72
+ },
73
+ {
74
+ "id": "c5",
75
+ "kind": "why_it_matters",
76
+ "text": "Here the operator IS an agent. A guard that blocks all non-TTY input also blocks the legitimate operator. A mode-aware consent model makes the agent a first-class, attributable driver without weakening the human safety gate.",
77
+ "origin": "llm",
78
+ "status": "confirmed",
79
+ "honesty_conditions": [
80
+ {
81
+ "id": "h10",
82
+ "text": "The agent operator genuinely has no TTY \u2014 proven live: invoking 'arm101 set-motor-id 1 < /dev/null' today returns exit 2 at the TTY guard.",
83
+ "status": "confirmed"
84
+ }
85
+ ],
86
+ "hard_questions": [],
87
+ "links": []
88
+ },
89
+ {
90
+ "id": "c6",
91
+ "kind": "boundary",
92
+ "text": "NOT building colleague's full ANSI cockpit / redraw TUI / per-keystroke reader. Scope is the consent + mode-resolution contract for gated hardware verbs plus a dry-run/plan view \u2014 not a general UI framework. Read-only verbs (calibrate-motor, detection) are unchanged.",
93
+ "origin": "llm",
94
+ "status": "confirmed",
95
+ "honesty_conditions": [
96
+ {
97
+ "id": "h11",
98
+ "text": "No ANSI/curses/redraw/per-keystroke code is added \u2014 the diff touches only consent resolution, plan files, audit, and the two verbs (verifiable by reviewing changed files).",
99
+ "status": "confirmed"
100
+ }
101
+ ],
102
+ "hard_questions": [],
103
+ "links": []
104
+ },
105
+ {
106
+ "id": "c7",
107
+ "kind": "success_signal",
108
+ "text": "An agent runs set-motor-id end-to-end on F1 with explicit consent and no PTY hack; a human terminal run is unchanged; a bare non-TTY no-flag run still exits refusing; set-motor-id AND center-motor both route through the one shared consent helper.",
109
+ "origin": "llm",
110
+ "status": "confirmed",
111
+ "honesty_conditions": [
112
+ {
113
+ "id": "h12",
114
+ "text": "On F1 an agent completes 'set-motor-id <id> --apply' with no PTY hack; the existing human-TTY tests still pass; a bare non-TTY no-flag run still exits 2; grep shows BOTH verbs call resolve_consent.",
115
+ "status": "confirmed"
116
+ }
117
+ ],
118
+ "hard_questions": [],
119
+ "links": []
120
+ },
121
+ {
122
+ "id": "c8",
123
+ "kind": "decision",
124
+ "text": "Consent resolves via ONE shared strategy mirroring colleague's _resolve_decide: (a) explicit consent flag present -> consent granted WITHOUT a TTY; (b) else stdin is a TTY -> interactive prompt the human types yes; (c) else refuse with a hint. The bare no-flag + no-TTY path still refuses (exit 2) \u2014 we ADD the explicit-flag channel, we don't remove the default-refuse.",
125
+ "origin": "llm",
126
+ "status": "confirmed",
127
+ "honesty_conditions": [
128
+ {
129
+ "id": "h1",
130
+ "text": "A bare 'set-motor-id 1' with no TTY and no consent flag still exits 2 (default-refuse preserved) \u2014 pinned by a test.",
131
+ "status": "confirmed"
132
+ }
133
+ ],
134
+ "hard_questions": [],
135
+ "links": []
136
+ },
137
+ {
138
+ "id": "c9",
139
+ "kind": "requirement",
140
+ "text": "A --dry-run/plan mode (valid in all three modes) runs detection + read-only snapshot and emits the full write-plan (port, from_id->to_id, baud / target position, action-kind label) as text or JSON, then exits 0 with ZERO side effects. This is the 'agent reads exactly what will happen before consenting' primitive \u2014 the machine-readable mirror.",
141
+ "origin": "llm",
142
+ "status": "rejected",
143
+ "honesty_conditions": [
144
+ {
145
+ "id": "h2",
146
+ "text": "--dry-run calls NO bus write/motion: a FakeBus test asserts write_id_baudrate / write_goal_position is never invoked under --dry-run.",
147
+ "status": "proposed"
148
+ }
149
+ ],
150
+ "hard_questions": [],
151
+ "links": []
152
+ },
153
+ {
154
+ "id": "c10",
155
+ "kind": "requirement",
156
+ "text": "set-motor-id and center-motor both route their gate through ONE shared consent helper (e.g. arm101/cli/_consent.py: resolve_consent(args, plan) -> bool|raise). No verb-specific consent logic; the verbs differ only in the plan object they pass in.",
157
+ "origin": "llm",
158
+ "status": "confirmed",
159
+ "honesty_conditions": [
160
+ {
161
+ "id": "h3",
162
+ "text": "The shared helper carries no per-verb branching: set-motor-id and center-motor pass different plan objects but call the identical resolve_consent().",
163
+ "status": "confirmed"
164
+ }
165
+ ],
166
+ "hard_questions": [],
167
+ "links": []
168
+ },
169
+ {
170
+ "id": "c11",
171
+ "kind": "decision",
172
+ "text": "Headless consent requires the SPECIFIC target named on the command line (e.g. 'set-motor-id 6 --apply' consents to id 6); a bare consent flag with no target is refused. [resolves v2]",
173
+ "origin": "user",
174
+ "status": "confirmed",
175
+ "honesty_conditions": [],
176
+ "hard_questions": [],
177
+ "links": []
178
+ },
179
+ {
180
+ "id": "c12",
181
+ "kind": "decision",
182
+ "text": "Commanded MOTION is gated HARDER than an EEPROM write: motion refuses the pure-CI tier \u2014 it needs a human at a TTY OR a present agent that verifies via the 2-step plan-file read; an EEPROM write is allowed headless with consent. [resolves v1]",
183
+ "origin": "user",
184
+ "status": "confirmed",
185
+ "honesty_conditions": [],
186
+ "hard_questions": [],
187
+ "links": []
188
+ },
189
+ {
190
+ "id": "c13",
191
+ "kind": "decision",
192
+ "text": "The consent/execute flag is --apply (NOT --yes). Default output is MARKDOWN (the agent-readable format); --json is for APPLICATION consumers, not agents. 'Agents verify' via --apply.",
193
+ "origin": "user",
194
+ "status": "confirmed",
195
+ "honesty_conditions": [],
196
+ "hard_questions": [],
197
+ "links": []
198
+ },
199
+ {
200
+ "id": "c14",
201
+ "kind": "decision",
202
+ "text": "Risky actions use a 2-STEP FILE HANDSHAKE, not a single flag: (1) the command writes a plan FILE describing exactly what will happen; (2) the agent must READ that plan file (direct file read) and only then run --apply. The handshake forces the agent to consume the plan before executing.",
203
+ "origin": "user",
204
+ "status": "confirmed",
205
+ "honesty_conditions": [],
206
+ "hard_questions": [
207
+ {
208
+ "id": "q1",
209
+ "text": "risk: Stale-state race: motor state can change between plan generation and --apply. Mitigation: plan embeds a state snapshot + timestamp + hash; --apply re-checks and refuses if stale/mismatched.",
210
+ "resolved": false,
211
+ "blocking": false
212
+ }
213
+ ],
214
+ "links": []
215
+ },
216
+ {
217
+ "id": "c15",
218
+ "kind": "decision",
219
+ "text": "Headless writes require an attributed operator identity (ARM101_OPERATOR env \u2192 culture.yaml nick \u2192 tty:$USER); a non-TTY write is REFUSED without an identity; identity recorded in the result + an append-only audit log. [resolves v3]",
220
+ "origin": "user",
221
+ "status": "confirmed",
222
+ "honesty_conditions": [],
223
+ "hard_questions": [
224
+ {
225
+ "id": "q2",
226
+ "text": "risk: Identity env vars (ARM101_OPERATOR) are trivially spoofable \u2014 treat attribution as informational audit, never as authorization.",
227
+ "resolved": false,
228
+ "blocking": false
229
+ }
230
+ ],
231
+ "links": []
232
+ },
233
+ {
234
+ "id": "c16",
235
+ "kind": "decision",
236
+ "text": "Interaction mode is AUTO-DETECTED from (TTY?, output format, consent state); no explicit --mode flag. [resolves v4]",
237
+ "origin": "user",
238
+ "status": "confirmed",
239
+ "honesty_conditions": [],
240
+ "hard_questions": [],
241
+ "links": []
242
+ },
243
+ {
244
+ "id": "c17",
245
+ "kind": "requirement",
246
+ "text": "The plan file carries a plan-HASH; --apply must reference a matching hash, enforcing 'read the plan before applying' and rejecting a stale/mismatched plan. (mechanism for the 2-step handshake)",
247
+ "origin": "llm",
248
+ "status": "confirmed",
249
+ "honesty_conditions": [
250
+ {
251
+ "id": "h5",
252
+ "text": "An --apply with a mismatched or stale plan-hash is refused (exit 2) and performs NO write \u2014 pinned by a test that mutates motor state between plan and apply.",
253
+ "status": "confirmed"
254
+ }
255
+ ],
256
+ "hard_questions": [],
257
+ "links": []
258
+ },
259
+ {
260
+ "id": "c18",
261
+ "kind": "decision",
262
+ "text": "TIERED consent weight: set-motor-id (reversible EEPROM) uses 1-step agent consent \u2014 'set-motor-id <id> --apply' (specific target + flag, attributed + audited, no plan file). center-motor (physical motion / damage risk) requires the full 2-step plan-file handshake (dry-run writes JSON plan file \u2192 agent must Read it \u2192 '--apply --plan-hash <hash>'). Honors 'motion stricter than EEPROM'.",
263
+ "origin": "user",
264
+ "status": "confirmed",
265
+ "honesty_conditions": [],
266
+ "hard_questions": [],
267
+ "links": []
268
+ },
269
+ {
270
+ "id": "c19",
271
+ "kind": "requirement",
272
+ "text": "Headless callers get a ZERO-side-effect plan FIRST: set-motor-id prints a markdown plan to stdout; center-motor writes a JSON plan FILE (~/.arm101/plans/...) with the plan-hash INSIDE the file only (never stdout) plus a markdown pointer. The plan/dry-run path performs no bus write and no motion.",
273
+ "origin": "user",
274
+ "status": "confirmed",
275
+ "honesty_conditions": [
276
+ {
277
+ "id": "h6",
278
+ "text": "A FakeBus test asserts the plan/dry-run path calls zero bus writes and zero motion (write_id_baudrate / enable_torque / write_goal_position never invoked).",
279
+ "status": "confirmed"
280
+ }
281
+ ],
282
+ "hard_questions": [],
283
+ "links": []
284
+ }
285
+ ],
286
+ "open_vagueness": [
287
+ {
288
+ "id": "v1",
289
+ "text": "Full EEPROM Lock-register unlock/relock inside write_id_baudrate (STS3215 addr 55) is DEFERRED to a follow-on PR. This spec only SURFACES lock_register in the plan snapshot and warns when Lock=1 (a locked motor silently discards the write while the SDK returns result=0,error=0).",
290
+ "kind": "follow_up",
291
+ "claim_id": null
292
+ }
293
+ ]
294
+ }
@@ -0,0 +1,250 @@
1
+ {
2
+ "slug": "arm101-s-gated-write-hardware-verbs-set-motor-id-c",
3
+ "title": "arm101's gated-write hardware verbs (set-motor-id, center-motor) now resolve consent in three driver modes \u2014 human-interactive (type yes at a TTY), agent-interactive (an AI reads a structured write-plan and consents explicitly), and non-interactive (scripted) \u2014 so an AI agent can safely drive an EEPROM write or a commanded motion without faking a TTY, while the unconsented non-TTY default still refuses.",
4
+ "frame_slug": "arm101-s-gated-write-hardware-verbs-set-motor-id-c",
5
+ "schema_version": 1,
6
+ "status": "exported",
7
+ "created": "2026-06-27T11:25:38Z",
8
+ "updated": "2026-06-27T11:31:08Z",
9
+ "targets": [
10
+ {
11
+ "id": "c1",
12
+ "kind": "announcement",
13
+ "text": "arm101's gated-write hardware verbs (set-motor-id, center-motor) now resolve consent in three driver modes \u2014 human-interactive (type yes at a TTY), agent-interactive (an AI reads a structured write-plan and consents explicitly), and non-interactive (scripted) \u2014 so an AI agent can safely drive an EEPROM write or a commanded motion without faking a TTY, while the unconsented non-TTY default still refuses."
14
+ },
15
+ {
16
+ "id": "h4",
17
+ "kind": "honesty",
18
+ "text": "Each of the three modes is pinned by a test: TTY->interactive prompt; non-TTY+consent->execute (1-step --apply for set-motor-id, 2-step --apply --plan-hash for center-motor); non-TTY+no-consent->refuse (exit 2)."
19
+ },
20
+ {
21
+ "id": "c2",
22
+ "kind": "audience",
23
+ "text": "Three operator types of the SO-101 pre-assembly flow: a human at a bench terminal, an AI agent (Claude Code) driving the arm101 CLI, and scripted/CI callers with no one present."
24
+ },
25
+ {
26
+ "id": "h7",
27
+ "kind": "honesty",
28
+ "text": "All three operator types exercise the SAME verbs through one resolver \u2014 a test demonstrates a human-TTY prompt path, an agent --apply path, and a non-TTY no-consent refusal."
29
+ },
30
+ {
31
+ "id": "c3",
32
+ "kind": "after_state",
33
+ "text": "Every gated side-effect verb resolves consent through ONE shared mode-aware helper; an agent can drive set-motor-id/center-motor to completion with explicit consent and machine-readable I/O; a human still types yes at a TTY; a bare non-TTY run with no consent flag still refuses (exit 2)."
34
+ },
35
+ {
36
+ "id": "h8",
37
+ "kind": "honesty",
38
+ "text": "After the change neither verb has its own duplicated gate \u2014 both import resolve_consent from arm101/cli/_consent.py (asserted by an import/grep test); the human path is behaviorally unchanged."
39
+ },
40
+ {
41
+ "id": "c4",
42
+ "kind": "before_state",
43
+ "text": "set-motor-id/center-motor call _require_tty() and HARD-refuse any non-TTY stdin (exit 2). The only consent channel is a human typing yes at a TTY, so the agent operator can never drive the write \u2014 forcing PTY-faking hacks that defeat the very gate they bypass."
44
+ },
45
+ {
46
+ "id": "h9",
47
+ "kind": "honesty",
48
+ "text": "The CURRENT code is accurately described: set_motor_id._require_tty and center_motor._require_tty both raise exit 2 on non-TTY today (the pre-change tests we are replacing assert exactly this)."
49
+ },
50
+ {
51
+ "id": "c5",
52
+ "kind": "why_it_matters",
53
+ "text": "Here the operator IS an agent. A guard that blocks all non-TTY input also blocks the legitimate operator. A mode-aware consent model makes the agent a first-class, attributable driver without weakening the human safety gate."
54
+ },
55
+ {
56
+ "id": "h10",
57
+ "kind": "honesty",
58
+ "text": "The agent operator genuinely has no TTY \u2014 proven live: invoking 'arm101 set-motor-id 1 < /dev/null' today returns exit 2 at the TTY guard."
59
+ },
60
+ {
61
+ "id": "c6",
62
+ "kind": "boundary",
63
+ "text": "NOT building colleague's full ANSI cockpit / redraw TUI / per-keystroke reader. Scope is the consent + mode-resolution contract for gated hardware verbs plus a dry-run/plan view \u2014 not a general UI framework. Read-only verbs (calibrate-motor, detection) are unchanged."
64
+ },
65
+ {
66
+ "id": "h11",
67
+ "kind": "honesty",
68
+ "text": "No ANSI/curses/redraw/per-keystroke code is added \u2014 the diff touches only consent resolution, plan files, audit, and the two verbs (verifiable by reviewing changed files)."
69
+ },
70
+ {
71
+ "id": "c7",
72
+ "kind": "success_signal",
73
+ "text": "An agent runs set-motor-id end-to-end on F1 with explicit consent and no PTY hack; a human terminal run is unchanged; a bare non-TTY no-flag run still exits refusing; set-motor-id AND center-motor both route through the one shared consent helper."
74
+ },
75
+ {
76
+ "id": "h12",
77
+ "kind": "honesty",
78
+ "text": "On F1 an agent completes 'set-motor-id <id> --apply' with no PTY hack; the existing human-TTY tests still pass; a bare non-TTY no-flag run still exits 2; grep shows BOTH verbs call resolve_consent."
79
+ },
80
+ {
81
+ "id": "c10",
82
+ "kind": "requirement",
83
+ "text": "set-motor-id and center-motor both route their gate through ONE shared consent helper (e.g. arm101/cli/_consent.py: resolve_consent(args, plan) -> bool|raise). No verb-specific consent logic; the verbs differ only in the plan object they pass in."
84
+ },
85
+ {
86
+ "id": "h3",
87
+ "kind": "honesty",
88
+ "text": "The shared helper carries no per-verb branching: set-motor-id and center-motor pass different plan objects but call the identical resolve_consent()."
89
+ },
90
+ {
91
+ "id": "c17",
92
+ "kind": "requirement",
93
+ "text": "The plan file carries a plan-HASH; --apply must reference a matching hash, enforcing 'read the plan before applying' and rejecting a stale/mismatched plan. (mechanism for the 2-step handshake)"
94
+ },
95
+ {
96
+ "id": "h5",
97
+ "kind": "honesty",
98
+ "text": "An --apply with a mismatched or stale plan-hash is refused (exit 2) and performs NO write \u2014 pinned by a test that mutates motor state between plan and apply."
99
+ },
100
+ {
101
+ "id": "c19",
102
+ "kind": "requirement",
103
+ "text": "Headless callers get a ZERO-side-effect plan FIRST: set-motor-id prints a markdown plan to stdout; center-motor writes a JSON plan FILE (~/.arm101/plans/...) with the plan-hash INSIDE the file only (never stdout) plus a markdown pointer. The plan/dry-run path performs no bus write and no motion."
104
+ },
105
+ {
106
+ "id": "h6",
107
+ "kind": "honesty",
108
+ "text": "A FakeBus test asserts the plan/dry-run path calls zero bus writes and zero motion (write_id_baudrate / enable_torque / write_goal_position never invoked)."
109
+ }
110
+ ],
111
+ "tasks": [
112
+ {
113
+ "id": "t1",
114
+ "summary": "bus: add FeetechBus.read_lock() + FakeBus lock_register snapshot field",
115
+ "origin": "llm",
116
+ "status": "confirmed",
117
+ "acceptance_criteria": [
118
+ "FeetechBus.read_lock(motor_id) reads STS3215 register 55 (1 byte) and returns 0/1; unit test asserts it reads addr 55 via the fake packet handler.",
119
+ "FakeBus.read_info() includes a constructor-settable 'lock_register' key (default 0) so plan snapshots carry it; test asserts the key is present.",
120
+ "No behavior change to existing bus methods; existing tests/test_bus.py still pass."
121
+ ],
122
+ "deps": [],
123
+ "covers": []
124
+ },
125
+ {
126
+ "id": "t2",
127
+ "summary": "consent core: new arm101/cli/_consent.py (resolve_consent + plan build/write/verify + audit + operator identity)",
128
+ "origin": "llm",
129
+ "status": "confirmed",
130
+ "acceptance_criteria": [
131
+ "resolve_consent(args,*,verb,require_plan_hash) -> 'interactive' under a TTY; 'dry_run' when non-TTY and not args.apply; 'agent' when non-TTY+args.apply and (require_plan_hash False OR plan_hash is valid sha256:<64hex>); raises CliError(EXIT_ENV_ERROR) when non-TTY+apply and require_plan_hash but hash missing; CliError(EXIT_USER_ERROR) when hash malformed. test_consent.py pins every row for both flag values.",
132
+ "build_plan(verb,port,info,action) returns schema_version/verb/created_at/port/operator/action/motor_snapshot(id,model,present_position,torque_enable,lock_register)/plan_hash; plan_hash = sha256 over canonical json of {verb,port,action,motor_snapshot(id,model,present_position,torque_enable)} (excludes operator/created_at/volatile sensors). Test: same state->same hash, changed present_position->different hash.",
133
+ "write_plan_file(plan) writes JSON to $ARM101_PLAN_DIR or ~/.arm101/plans/<verb>-<portbase>-<utc>.json and returns the path; emit_plan_stdout() writes MARKDOWN naming the path but NOT the hash. Test asserts hash is in the file and absent from stdout.",
134
+ "verify_plan_hash(supplied,verb,port,action,info) recomputes from live state; raises CliError(EXIT_ENV_ERROR) on mismatch, returns None on match. Test: mismatch->exit2, match->ok.",
135
+ "resolve_operator() = ARM101_OPERATOR else culture.yaml nick else 'tty:'+getuser(); write_audit(record) appends one JSONL line to $ARM101_AUDIT_LOG or ~/.arm101/audit.log and NEVER raises on write failure. Test asserts pending-before/success-after ordering and that an unwritable log path is swallowed.",
136
+ "Module introduces NO ANSI/curses/redraw code; uses _output.emit_* only."
137
+ ],
138
+ "deps": [
139
+ "t1"
140
+ ],
141
+ "covers": [
142
+ "c1",
143
+ "h4",
144
+ "c2",
145
+ "h7",
146
+ "c3",
147
+ "c5",
148
+ "c10",
149
+ "h3",
150
+ "c17",
151
+ "h5",
152
+ "c19",
153
+ "h6"
154
+ ]
155
+ },
156
+ {
157
+ "id": "t3",
158
+ "summary": "set-motor-id: tiered 1-step --apply via resolve_consent(require_plan_hash=False); drop _require_tty/_confirm",
159
+ "origin": "llm",
160
+ "status": "confirmed",
161
+ "acceptance_criteria": [
162
+ "set_motor_id imports resolve_consent and no longer defines _require_tty or _confirm (grep test asserts absence); register() adds --apply (store_true) + --plan-hash with help noting --apply is non-TTY-only / ignored under a TTY.",
163
+ "Non-TTY + no --apply prints a markdown plan to stdout and performs ZERO eeprom writes (FakeBus.eeprom_writes == []).",
164
+ "Non-TTY + 'set-motor-id 6 --apply' (no hash needed, 1-step EEPROM tier) writes EEPROM exactly once after a 'pending' audit record; result names from_id->to_id, port, operator.",
165
+ "Non-TTY + --apply with new_id absent raises CliError(EXIT_USER_ERROR) and writes nothing (specific-target rule).",
166
+ "TTY path behaviorally unchanged: typed 'yes' writes, 'no'/EOF behave as today; the two old non-TTY-rejection tests are rewritten and result-text assertions updated for markdown."
167
+ ],
168
+ "deps": [
169
+ "t2"
170
+ ],
171
+ "covers": [
172
+ "c4",
173
+ "h9",
174
+ "c7",
175
+ "h12",
176
+ "h10",
177
+ "h8",
178
+ "c10",
179
+ "h3",
180
+ "c19",
181
+ "h6"
182
+ ]
183
+ },
184
+ {
185
+ "id": "t4",
186
+ "summary": "center-motor: 2-step plan-file handshake via resolve_consent(require_plan_hash=True); drop _require_tty/_confirm",
187
+ "origin": "llm",
188
+ "status": "confirmed",
189
+ "acceptance_criteria": [
190
+ "center_motor imports resolve_consent and no longer defines _require_tty/_confirm (grep test); register() adds --apply + --plan-hash.",
191
+ "Non-TTY + no --apply writes a JSON plan FILE (action carries workspace_warning; plan_hash in file) + markdown pointer to stdout; performs ZERO motion (FakeBus enable_torque/write_goal_position never called).",
192
+ "Non-TTY + --apply --plan-hash <matching> runs enable_torque->write_goal_position->relax (relax skipped with --keep-torque) after a 'pending' audit; <mismatched/stale> raises CliError(EXIT_ENV_ERROR) with no motion.",
193
+ "Non-TTY + --apply without --plan-hash raises CliError(EXIT_ENV_ERROR), no motion.",
194
+ "TTY path unchanged: workspace warning + typed 'yes' moves, 'no'/EOF aborts; existing tests pass after migration."
195
+ ],
196
+ "deps": [
197
+ "t2"
198
+ ],
199
+ "covers": [
200
+ "c4",
201
+ "h9",
202
+ "h8",
203
+ "c17",
204
+ "h5",
205
+ "c19",
206
+ "h6",
207
+ "c10"
208
+ ]
209
+ },
210
+ {
211
+ "id": "t5",
212
+ "summary": "docs lockstep: explain/catalog + overview + learn updated for --apply/--plan-hash and the three modes",
213
+ "origin": "llm",
214
+ "status": "confirmed",
215
+ "acceptance_criteria": [
216
+ "explain catalog gains entries for --apply/--plan-hash/dry-run-plan on set-motor-id and center-motor; test_every_catalog_path_resolves passes.",
217
+ "overview._VERBS descriptions and learn text mention agent mode / --apply; existing lockstep tests pass.",
218
+ "Docs/strings only \u2014 no ANSI/TUI code introduced."
219
+ ],
220
+ "deps": [
221
+ "t3",
222
+ "t4"
223
+ ],
224
+ "covers": [
225
+ "c6",
226
+ "h11"
227
+ ]
228
+ }
229
+ ],
230
+ "risks": [
231
+ {
232
+ "id": "r1",
233
+ "text": "Full EEPROM Lock-register unlock/relock inside write_id_baudrate deferred to a follow-on PR; this plan only surfaces lock_register + warns when Lock=1.",
234
+ "kind": "follow_up",
235
+ "task_id": null
236
+ },
237
+ {
238
+ "id": "r2",
239
+ "text": "Stale-plan TTL: hash-over-live-state catches state drift, but an idle motor with constant state means a hash never expires; a created_at TTL check is a soft (warn-only) mitigation.",
240
+ "kind": "unknown_nonblocking",
241
+ "task_id": null
242
+ },
243
+ {
244
+ "id": "r3",
245
+ "text": "Plan file is a TOCTOU surface; mitigated by computing the hash from live MOTOR state at apply time (not from file content), so file tampering can't authorize a different action.",
246
+ "kind": "unknown_nonblocking",
247
+ "task_id": null
248
+ }
249
+ ]
250
+ }
@@ -5,6 +5,28 @@ 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.0] - 2026-06-27
9
+
10
+ ### Added
11
+
12
+ - Three-mode consent for the gated hardware verbs (set-motor-id, center-motor): a new shared arm101/cli/_consent.py core auto-detects the operator from (TTY?, --apply?, --plan-hash?) and resolves one of human-interactive (type `yes` at a TTY), agent-interactive (a non-TTY agent consents with --apply), or non-interactive dry-run (prints a read-only write-plan; zero side effects). An AI agent can now drive an EEPROM write / commanded motion without faking a TTY, while a human still gets the typed-confirmation gate.
13
+ - Tiered consent matched to blast radius: set-motor-id (reversible EEPROM write) is 1-step (`set-motor-id <id> --apply`); center-motor (commanded motion) is a 2-step plan-file handshake — a dry-run writes a JSON plan under ~/.arm101/plans/ whose plan_hash the agent reads and passes back as `--apply --plan-hash <hash>`. The hash is recomputed from live motor state at apply time and refuses on mismatch (stale-state protection); it is written only to the plan file, never to stdout.
14
+ - Attribution + audit for headless writes: an operator identity (ARM101_OPERATOR env -> culture.yaml nick -> tty:$USER) is recorded, and every gated write appends a JSONL `pending` record before and a `success`/`failed` record after to ~/.arm101/audit.log (ARM101_AUDIT_LOG). Audit writes never raise.
15
+ - MotorBus.read_lock() (STS3215 EEPROM Lock register, addr 55) + FakeBus lock_register; the Lock state is surfaced in the center-motor plan snapshot (full unlock->write->relock deferred to a follow-on).
16
+
17
+ ### Changed
18
+
19
+ - set-motor-id / center-motor no longer hard-refuse a non-TTY stdin (reverses the 0.6.0 up-front non-TTY rejection). A non-TTY caller now gets a read-only dry-run plan by default; the destructive write fires only with an explicit --apply (plus --plan-hash for motion). A piped `yes` still cannot drive a write — consent is an explicit flag against a named target, not stdin content.
20
+ - Default output (without --json) is markdown — the agent-readable format; --json is for application consumers. The explain catalog, overview verb list, and learn prompt were updated in lockstep to document the three modes, the tiers, and --apply/--plan-hash.
21
+
22
+ ### Fixed
23
+
24
+ - Test isolation: an autouse `tests/conftest.py` fixture pins `ARM101_AUDIT_LOG` and `ARM101_PLAN_DIR` into each test's tmp dir, so the suite can no longer append test records to the operator's real `~/.arm101/audit.log` (the audit-write tests previously leaked there when they did not set the env var themselves). Found during the F1 live-test.
25
+ - Plan-hash verification now tolerates surrounding whitespace: `verify_plan_hash` strips the supplied `--plan-hash` the same way `resolve_consent` does, so a hash read from the plan file with a trailing newline (or copy-pasted with stray spaces) verifies instead of being falsely refused (Qodo).
26
+ - `center-motor` plans now surface the real EEPROM Lock register on hardware: `FeetechBus.read_info` reads addr 55 (`_INFO_REGISTERS`), so `motor_snapshot.lock_register` reflects the actual lock state instead of defaulting to 0 (previously only `FakeBus` injected it) (Qodo).
27
+ - `set-motor-id` `explain` docs no longer claim a non-interactive stdin without `--apply` exits 2 — it prints a read-only dry-run plan and exits 0 (Qodo).
28
+ - Refactored `cmd_center_motor` and `cmd_set_motor_id` into focused helpers (dry-run/confirm/motion/result/audit) to cut cognitive complexity below the gate, and normalized the `# noqa: BLE001` suppression comments (SonarCloud python:S3776, S3358, S7632).
29
+
8
30
  ## [0.6.0] - 2026-06-27
9
31
 
10
32
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arm101-cli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Agent and CLI for controlling SO-ARM101 robotic arm grippers
5
5
  Project-URL: Homepage, https://github.com/agentculture/arm101-cli
6
6
  Project-URL: Issues, https://github.com/agentculture/arm101-cli/issues