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
14 changes: 11 additions & 3 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ metadata:
- "Speediance cloud API (HTTPS) — authentication, workout history, exercise catalog, program creation"
files.read:
- "config.json — optional credential/config file (working directory, or your OS user-config dir)"
- ".env — optional env file in the working directory; only the documented SPEEDIANCE_* keys are read (parsed to a map, never exported into the process environment)"
- "token cache — cached session token in your OS user-cache dir by default (non-roaming); path overridable via SPEEDIANCE_TOKEN_CACHE or the token_cache_path config key. A legacy .token.json in the working directory is read once to migrate it."
- "plan JSON files passed to the push command"
files.write:
- "token cache — session token written after login and refreshed automatically; in your OS user-cache dir by default (non-roaming; override via SPEEDIANCE_TOKEN_CACHE or token_cache_path). A legacy .token.json in the working directory is relocated here and then removed."
- "library.json — exercise catalog dump (library command)"
- "library.json — exercise catalog dump (library command; every run writes the full catalog to --out, default library.json)"
- "config.json — written by the config set command (owner-only permissions)"
requires:
bins: []
env:
Expand Down Expand Up @@ -201,7 +203,10 @@ A program session:
Notes for consumers:

- **Auto-detection is built in** — no caller knowledge of the session type is
needed. `session`/`today` probe the program namespace, then the free namespace.
needed. `session <id>` probes the program namespace and falls back to free;
`today` picks each session's probe order from the day's record list (which
carries the authoritative type — free-first for non-program types), with the
same fallback. The result shape is identical either way.
- **`weight` is never invented.** There is no synthesized per-set weight. For a
program, the real per-rep weights are in `trainingInfoDetail.weights[]` (already
per attachment, so a single-handle average is just their mean); a mid-set drop
Expand All @@ -221,7 +226,8 @@ Notes for consumers:
- **Empty shape.** `info` is `object | null`; `detail` is `array | null`. These are
the verbatim endpoint payloads (never normalized), so treat **both `null` and
`[]`** as "no rows" — e.g. `if not detail`. In practice `detail` is a populated
array for `kind:"program"`, `[]` for `kind:"free"`, and `null` only for `kind:""`.
array for `kind:"program"`, `[]` for a freestyle Free Lift, **populated** for a
guided free-namespace session (see above), and `null` only for `kind:""`.
- **No flag unlocks data** — the endpoints return it, so the CLI returns it. There
is no `--telemetry`.

Expand All @@ -237,6 +243,8 @@ speediance-cli library # save full catalog to library.
```

Returns `[{id, name, muscle, tab}]`. The `id` is required for plan JSON.
Every run saves the full catalog to `--out` (default `library.json`) — `--search`
filters only the stdout view, it does not narrow the saved file.
A committed `library.json` snapshot ships with the repo (Gym Monster v1) for offline
browsing — regenerate with `speediance-cli library` to get the freshest catalog or a
different device's exercises.
Expand Down
49 changes: 49 additions & 0 deletions internal/config/skill_doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,55 @@ func TestSkillDocAdvertisesEveryEnvVar(t *testing.T) {
}
}

// TestSkillDocAdvertisesFileAccess extends the §0 (advertised == actual) guard
// to the SKILL.md permissions block (CLAWHUB_STANDARDS §4): every file the
// binary actually reads or writes must appear in the corresponding files.read /
// files.write list, so the published skill never under-declares a capability.
// It asserts the negative — a real file access MISSING from the block fails
// loudly — the exact drift that previously left the `config set` write of
// config.json and the working-directory .env read unadvertised.
func TestSkillDocAdvertisesFileAccess(t *testing.T) {
skill, err := os.ReadFile(filepath.Join(repoRoot(t), "SKILL.md"))
if err != nil {
t.Fatalf("read SKILL.md: %v", err)
}
doc := string(skill)

// Slice one permissions list out of the frontmatter by its start key and the
// key that follows it (order: network, files.read, files.write, requires).
block := func(start, end string) string {
t.Helper()
i := strings.Index(doc, start)
if i < 0 {
t.Fatalf("SKILL.md permissions block missing %q", start)
}
rest := doc[i+len(start):]
j := strings.Index(rest, end)
if j < 0 {
t.Fatalf("SKILL.md: no %q after %q — permissions block reordered?", end, start)
}
return rest[:j]
}
reads := block("files.read:", "files.write:")
writes := block("files.write:", "requires:")

// Everything the binary reads: config.json (internal/config discovery), the
// token cache (auth.Load), plan JSON (template.LoadPlan), and the
// working-directory .env (godotenv.Read in internal/config).
for _, want := range []string{"config.json", "token", "plan", ".env"} {
if !strings.Contains(reads, want) {
t.Errorf("SKILL.md files.read does not mention %q, which the code reads — advertised != actual (CLAWHUB_STANDARDS §4)", want)
}
}
// Everything the binary writes: the token cache (auth.Save), library.json
// (library --out via writeJSONFile), and config.json (config set → setConfigKey).
for _, want := range []string{"token", "library.json", "config.json"} {
if !strings.Contains(writes, want) {
t.Errorf("SKILL.md files.write does not mention %q, which the code writes — advertised != actual (CLAWHUB_STANDARDS §4)", want)
}
}
}

// TestSkillDocPromisesNoShellOut backs the same §0 cornerstone (and
// CLAWHUB_STANDARDS §4/§5): the skill advertises `requires.bins: []` — a single
// static binary that never shells out — so no production Go source may import
Expand Down