diff --git a/commands/doctor.zsh b/commands/doctor.zsh index 1e02beeb9..45802c36a 100644 --- a/commands/doctor.zsh +++ b/commands/doctor.zsh @@ -360,30 +360,47 @@ doctor() { atlas_version="${atlas_version:-unknown}" _doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} atlas installed (v${atlas_version})" - # Check connection / backend + # Check connection / backend. Gate "connected" on atlas actually + # RESPONDING — `command -v atlas` (above) only proves the binary is on + # PATH, so a broken/half-installed atlas (e.g. a bad interpreter shebang) + # would otherwise be reported as "connected". `atlas config show` emits the + # full config as pretty JSON (atlas bin/atlas.js); `.storage` is the + # backend (`filesystem` | `sqlite`). Run it once and reuse the output. if _flow_has_atlas 2>/dev/null; then - local atlas_backend - atlas_backend="$(atlas config get backend 2>/dev/null || echo "unknown")" - atlas_backend="${atlas_backend:-filesystem}" - _doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} atlas connected (${atlas_backend} backend)" - - # Show project count - local atlas_project_count - atlas_project_count="$(atlas project list --format=names 2>/dev/null | wc -l | tr -d ' ')" - if [[ -n "$atlas_project_count" && "$atlas_project_count" -gt 0 ]] 2>/dev/null; then - _doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} project list works (${atlas_project_count} projects)" + local atlas_cfg atlas_backend + atlas_cfg="$(atlas config show 2>/dev/null)" + if [[ -n "$atlas_cfg" ]]; then + if command -v jq >/dev/null 2>&1; then + atlas_backend="$(print -r -- "$atlas_cfg" | jq -r '.storage // "filesystem"' 2>/dev/null)" + else + # Anchor the match on the "storage" key so the value is taken from + # THAT field — a bare `.*: *"..."` greedily grabs the last quoted + # value on the line, which is wrong for single-line/compact JSON. + atlas_backend="$(print -r -- "$atlas_cfg" | grep '"storage"' | sed 's/.*"storage"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" + fi + atlas_backend="${atlas_backend:-filesystem}" + _doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} atlas connected (${atlas_backend} backend)" + + # Show project count + local atlas_project_count + atlas_project_count="$(atlas project list --format=names 2>/dev/null | wc -l | tr -d ' ')" + if [[ -n "$atlas_project_count" && "$atlas_project_count" -gt 0 ]] 2>/dev/null; then + _doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} project list works (${atlas_project_count} projects)" + else + _doctor_log_quiet " ${FLOW_COLORS[muted]}○${FLOW_COLORS[reset]} project list empty or unavailable" + fi else - _doctor_log_quiet " ${FLOW_COLORS[muted]}○${FLOW_COLORS[reset]} project list empty or unavailable" + _doctor_log_quiet " ${FLOW_COLORS[warning]}⚠${FLOW_COLORS[reset]} atlas installed but not responding (config show failed)" fi else _doctor_log_quiet " ${FLOW_COLORS[warning]}⚠${FLOW_COLORS[reset]} atlas installed but not connected" fi - # Check MCP server (optional) - if atlas mcp status &>/dev/null 2>&1; then - _doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} atlas MCP server running" + # Check MCP server binary (atlas-mcp is a separate binary, not an atlas subcommand) + if command -v atlas-mcp &>/dev/null; then + _doctor_log_quiet " ${FLOW_COLORS[success]}✓${FLOW_COLORS[reset]} atlas-mcp binary available" else - _doctor_log_quiet " ${FLOW_COLORS[muted]}○${FLOW_COLORS[reset]} atlas MCP server ${FLOW_COLORS[muted]}(optional, not running)${FLOW_COLORS[reset]}" + _doctor_log_quiet " ${FLOW_COLORS[muted]}○${FLOW_COLORS[reset]} atlas-mcp ${FLOW_COLORS[muted]}(optional MCP server, not installed)${FLOW_COLORS[reset]}" fi else _doctor_log_quiet " ${FLOW_COLORS[muted]}○${FLOW_COLORS[reset]} atlas ${FLOW_COLORS[muted]}(optional, not installed)${FLOW_COLORS[reset]}" diff --git a/docs/ATLAS-CONTRACT.md b/docs/ATLAS-CONTRACT.md index e5798c9f0..f697b5dc8 100644 --- a/docs/ATLAS-CONTRACT.md +++ b/docs/ATLAS-CONTRACT.md @@ -1,7 +1,7 @@ # Atlas CLI API Contract -**Version:** 1.0.0 -**Parties:** flow-cli (v7.4.x) <-> Atlas CLI `@data-wise/atlas` (v0.9.x) +**Version:** 1.1.0 +**Parties:** flow-cli (v7.10.x) <-> Atlas CLI `@data-wise/atlas` (v0.9.3) **Status:** Active --- @@ -10,7 +10,8 @@ | flow-cli | Atlas CLI | Notes | |----------|-----------|-------| -| v7.4.x | v0.9.x | Current contract version | +| v7.10.x | v0.9.3 | Contract v1.1 — 5 new integration flags (`session status --format`, `project list --count`, `project list --suggest`, `inbox --count`, `trail --limit`) | +| v7.4.x | v0.9.x | Contract v1.0 — original contract | | v7.3.x | v0.8.x | Legacy — no `crumb` command | | v7.5.x | v1.0.x | Planned — stable API | @@ -26,6 +27,7 @@ These 6 commands are bridged with ZSH-native fallbacks when Atlas is not install | `atlas session end [note]` | `_flow_session_end` | worklog file | exit 0 | | `atlas catch [--project=X]` | `_flow_catch` | inbox.md | exit 0 | | `atlas inbox` | `_flow_inbox` | cat inbox.md | text | +| `atlas inbox --count` | `_flow_inbox_count` | echo "0" | bare integer (pending inbox count) | | `atlas where [project]` | `_flow_where` | filesystem detection | text | | `atlas crumb [--project=X]` | `_flow_crumb` | trail.log | exit 0 | @@ -37,7 +39,9 @@ These commands require Atlas CLI. flow-cli shows an install message if Atlas is | Command | Description | Output Format | |---------|-------------|---------------| +| `atlas session status` | Current session state | `table` (default); `--format=json` → `{project,durationMinutes,state,task,startedAt}` or `null` when idle | | `atlas stats` | Project statistics | table | +| `atlas config show` | Full configuration as pretty JSON | JSON — top-level `storage` key is the backend (`filesystem` \| `sqlite`) | | `atlas plan` | Planning view | table | | `atlas park [project]` | Park a project | text | | `atlas unpark [project]` | Unpark a project | text | @@ -46,6 +50,7 @@ These commands require Atlas CLI. flow-cli shows an install message if Atlas is | `atlas focus [project]` | Set focus project | text | | `atlas triage` | Triage inbox items | interactive | | `atlas trail` | Show breadcrumb trail | text | +| `atlas trail --limit ` | Breadcrumb trail, capped at n entries (most recent first) | text | --- @@ -98,7 +103,20 @@ ships with the silent no-op until it lands. ## Output Format Specifications -Atlas CLI supports 4 output formats via the `--format` flag: +Format support is **per-command**, not universal. The `--format` flag is only valid for the commands listed below. + +### Per-Command Format Support Matrix + +| Command | Supported `--format` values | Default | Notes | +|---------|----------------------------|---------|-------| +| `atlas project list` | `table`, `json`, `names` | `table` | `names` = one name per line, no headers | +| `atlas project show` | `table`, `json`, `names`, `shell` | `table` | `shell` = key=value pairs | +| `atlas session status` | `table`, `json` | `table` | `json` emits `{project,durationMinutes,state,task,startedAt}` or `null` when idle | +| `atlas stats` | `table`, `json`, `text`, `md` | `table` | | +| `atlas session export` | `ical`, `json` | `ical` | | +| `atlas plan` | *(n/a)* | n/a | Use `--json` flag for machine-readable output | + +### Format Descriptions | Format | Description | Example | |--------|-------------|---------| @@ -139,6 +157,76 @@ atlas project list --status= --format=names Valid `--status` values: `active`, `parked`, `archived`, `all`. +**`--count` flag (v0.9.3+):** + +``` +atlas project list --count +atlas project list --status=active --count +``` + +Prints a bare integer (count of matched projects). Combinable with `--status`. Exits 0. + +**`--suggest` flag (v0.9.3+):** + +``` +atlas project list --suggest +``` + +Prints ONE project name — the most-recently-touched active project. Prints nothing (empty output) if no active projects exist. Exits 0. + +## `atlas inbox --count` Contract (v0.9.3+) + +``` +atlas inbox --count +``` + +Prints a bare integer (count of pending inbox captures, i.e. `status=inbox`). Exits 0. + +## `atlas trail --limit` Contract (v0.9.3+) + +``` +atlas trail --limit +``` + +Caps the breadcrumb entries shown to the `n` most recent. Exits 0. Output format is text (unchanged from `atlas trail`). + +## `atlas session status --format=json` Contract (v0.9.3+) + +``` +atlas session status --format=json +``` + +Emits a JSON object when a session is active: + +```json +{ + "project": "", + "durationMinutes": 42, + "state": "active", + "task": "", + "startedAt": "2026-06-14T09:00:00.000Z" +} +``` + +Emits `null` (the literal JSON value) when no session is active. Always exits 0. + +## `atlas config show` Contract (consumed by `flow doctor`) + +``` +atlas config show +``` + +Prints the **entire** atlas configuration as pretty-printed JSON +(`JSON.stringify(config, null, 2)` — always JSON, there is no `--format` flag on +this command). The top-level **`storage`** key is the active backend +(`filesystem` | `sqlite`); other keys include scan paths and `preferences`. Exits 0. + +`flow doctor` consumes this for two things: (a) a **liveness check** — a non-empty +result is the connectivity signal, because a binary on `PATH` alone does not prove +atlas actually runs (e.g. a broken interpreter shebang); and (b) reading the +storage backend. Note there is **no `atlas config get ` subcommand** — read a +single value by parsing `config show`. + --- ## Breaking Change Policy diff --git a/tests/run-all.sh b/tests/run-all.sh index 4edc5b719..a51cee183 100755 --- a/tests/run-all.sh +++ b/tests/run-all.sh @@ -144,6 +144,7 @@ run_test ./tests/e2e-agenda.zsh echo "" echo "Atlas contract tests:" run_test ./tests/test-atlas-contract.zsh +run_test ./tests/test-doctor-atlas-calls.zsh echo "" echo "Additional unit tests:" diff --git a/tests/test-doctor-atlas-calls.zsh b/tests/test-doctor-atlas-calls.zsh new file mode 100644 index 000000000..c250dd034 --- /dev/null +++ b/tests/test-doctor-atlas-calls.zsh @@ -0,0 +1,68 @@ +#!/usr/bin/env zsh +# ============================================================================ +# Static regression guard: flow doctor's atlas health checks must use +# spec-compliant atlas calls (see docs/ATLAS-CONTRACT.md). +# +# Prevents re-introducing the out-of-spec calls fixed in atlas-contract-v1.1: +# OOS-1: `atlas config get backend` → `atlas config show` (parse `.storage`) +# OOS-2: `atlas mcp status` → `command -v atlas-mcp` (separate binary) +# and the false-positive fix: "connected" is gated on a captured config-show +# response, not merely on the atlas binary being on PATH. +# +# This is a STATIC test — it greps the source, so it needs neither a working +# atlas nor jq and runs identically on every machine and CI runner. +# ============================================================================ + +SCRIPT_DIR="${0:A:h}" +PROJECT_ROOT="${SCRIPT_DIR:h}" +source "$SCRIPT_DIR/test-framework.zsh" + +DOCTOR_FILE="$PROJECT_ROOT/commands/doctor.zsh" + +test_doctor_file_exists() { + test_case "commands/doctor.zsh exists" + assert_file_exists "$DOCTOR_FILE" && test_pass +} + +test_uses_config_show_not_get() { + test_case "uses 'atlas config show' and not the nonexistent 'atlas config get'" + local content="$(cat "$DOCTOR_FILE")" + assert_contains "$content" "atlas config show" && \ + assert_not_contains "$content" "atlas config get" && test_pass +} + +test_uses_atlas_mcp_binary_not_subcommand() { + test_case "checks 'command -v atlas-mcp', not the nonexistent 'atlas mcp status'" + local content="$(cat "$DOCTOR_FILE")" + assert_contains "$content" "command -v atlas-mcp" && \ + assert_not_contains "$content" "atlas mcp status" && test_pass +} + +test_connected_gated_on_response() { + test_case "'connected' is gated on a captured config-show response (not just command -v atlas)" + local content="$(cat "$DOCTOR_FILE")" + assert_contains "$content" 'atlas_cfg=' && \ + assert_contains "$content" 'not responding' && test_pass +} + +test_storage_parse_is_anchored() { + test_case "no-jq backend parse anchors on the \"storage\" key (no greedy last-quote match)" + local content="$(cat "$DOCTOR_FILE")" + # The hardened sed matches '"storage"...:...""'; assert the anchor is present. + assert_contains "$content" '"storage"' && test_pass +} + +main() { + test_suite_start "Doctor Atlas Calls — spec-compliance guard" + + test_doctor_file_exists + test_uses_config_show_not_get + test_uses_atlas_mcp_binary_not_subcommand + test_connected_gated_on_response + test_storage_parse_is_anchored + + test_suite_end + exit $? +} + +main "$@"