Skip to content
Draft
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
49 changes: 33 additions & 16 deletions commands/doctor.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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]}"
Expand Down
96 changes: 92 additions & 4 deletions docs/ATLAS-CONTRACT.md
Original file line number Diff line number Diff line change
@@ -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

---
Expand All @@ -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 |

Expand All @@ -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 <text> [--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 <text> [--project=X]` | `_flow_crumb` | trail.log | exit 0 |

Expand All @@ -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 |
Expand All @@ -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 <n>` | Breadcrumb trail, capped at n entries (most recent first) | text |

---

Expand Down Expand Up @@ -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 |
|--------|-------------|---------|
Expand Down Expand Up @@ -139,6 +157,76 @@ atlas project list --status=<filter> --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 <n>
```

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": "<project-name>",
"durationMinutes": 42,
"state": "active",
"task": "<task text or null>",
"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 <key>` subcommand** — read a
single value by parsing `config show`.

---

## Breaking Change Policy
Expand Down
1 change: 1 addition & 0 deletions tests/run-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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:"
Expand Down
68 changes: 68 additions & 0 deletions tests/test-doctor-atlas-calls.zsh
Original file line number Diff line number Diff line change
@@ -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"...:..."<value>"'; 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 "$@"