دستور hermes verify در چند ثانیه به شما می‌گوید پروژه واقعا بالا می‌آید یا نه: دستورهای نصب، build، تست و اجرا را خودش از روی پروژه تشخیص می‌دهد، سورس را در پس‌زمینه بالا می‌آورد و روی پورتی که سرویس اعلام می‌کند درخواست می‌زند. در این پست روی یک اپلیکیشن FastAPI واقعی اجرا شد: تشخیص خودکار دو جا را اشتباه گرفت، و یک اصلاح دستی آن را به Result: OK رساند.

این دستور دقیقا چه چیزی را می‌سنجد

ایجنت‌ها معمولا وقتی می‌خواهند مطمئن شوند کار خراب نشده، تست را اجرا می‌کنند. تست یک چیز را ثابت می‌کند: اینکه توابع درست کار می‌کنند. ولی پروژه‌ای که همه‌ی تست‌هایش سبز است، می‌تواند هنوز اصلا بالا نیاید. ایمپورت‌ها در سطح ماژول اشتباه‌اند، پورت اشغال است، یا هیچ‌کدام از مسیرهای readiness اصلا وجود ندارد و برنامه بالا می‌آید ولی هیچ‌چیز جواب نمی‌دهد.

این دستور دقیقا همان لایه‌ی گم‌شده را پر می‌کند: bootstrap برای نصب وابستگی‌ها، بعد build، بعد test، و در انتها سورس در پس‌زمینه بالا می‌آید، روی آدرسی که خود برنامه اعلام می‌کند درخواست زده می‌شود، و در پایان کل گروه پردازه‌های ساخته‌شده خاموش می‌شود. شما نباید دستی سرور را بالا نگه دارید.

این یک زیر‌دستور از خود هرمز است. همه‌ی عددها و خروجی‌های این نوشته روی نسخه‌ی v0.21.5+2271.gec24378 اجرا شده‌اند.

مرحلهکاری که می‌کندحداکثر زمان انتظار
bootstrapنصب وابستگی‌ها از روی فایل قفل یا فایل نیازمندی پروژه۶۰۰ ثانیه
buildدستور build پروژه، اگر خود پروژه چنین دستوری داشته باشد۶۰۰ ثانیه
testاجرای تست‌ها با دستوری که تشخیص داده شده۶۰۰ ثانیه
startاجرای سورس در پس‌زمینه و صبر برای آماده شدن۶۰ ثانیه

دو عدد پایین جدول از پیش‌فرض‌های همین زیر‌دستور آمده‌اند: سقف زمان هر فاز ۶۰۰ ثانیه و زمان انتظار آماده شدن ۶۰ ثانیه. هر دو با --timeout و --ready-timeout قابل تغییرند. عدد ۶۰ ثانیه برای اپلیکیشن‌هایی که مدل بارگذاری می‌کنند کم است.

پروژه‌ی نمونه را می‌سازیم و اولین اجرا شکست می‌خورد

یک اپلیکیشن FastAPI کوچک می‌سازیم با یک مسیر /health و اول از هرمز می‌پرسیم پروژه را چطور تشخیص می‌دهد:

# ساخت پروژه: دو فایل و یک مسیر /health
$ mkdir -p vdemo/app
$ cd vdemo

# پیش از هر چیز ببینیم هرمز پروژه را چطور تشخیص می‌دهد
$ hermes verify ./vdemo --detect-only

# app/main.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/health")
def health():
    return {"ok": True}

# requirements.txt -- نسخه‌ها را دقیق pin کنید تا اجرا تکرارپذیر بماند
fastapi==0.115.6
uvicorn==0.34.0

حالا همان تشخیص خودکار را با خروجی کامل می‌بینیم:

$ hermes verify ./vdemo --detect-only
{
  "source": "detected",
  "recipe": {
    "name": "FastAPI app",
    "kind": "fastapi",
    "bootstrap": [
      "pip install -r requirements.txt"
    ],
    "build": [],
    "test": [],
    "start": "uvicorn main:app --host 0.0.0.0 --port 8000",
    "port": 8000,
    "readinessPath": "/",
    "evidence": [
      "Detected Python project",
      "Detected FastAPI/Uvicorn dependency"
    ]
  }
}

اینجا اولین نشانه‌ی دقت کار پیدا می‌شود. دو چیزی که تشخیص داده درست‌اند: پروژه پایتون است و وابستگی FastAPI و Uvicorn را دارد. ولی به هر دو چیزی که تعیین‌کننده‌اند اشتباه کرده است.

اول start است. نوشته uvicorn main:app، در حالی که فایل ما app/main.py است. ماژول درست app.main است. دلیلش هم روشن است: کد تشخیص، اولین فایلی را که در ریشه‌ی پروژه باشد از بین main.py و app.py پیدا می‌کند و وقتی هیچ‌کدام در ریشه نبود، پیش‌فرضش را می‌گذارد. سورس ما یک پوشه عمیق‌تر است و این تشخیص آن عمق را نمی‌بیند.

دوم readinessPath است که روی / مانده. مسیر / در اپلیکیشن ما وجود ندارد و اصلا سرویسی پشتش نیست. یعنی حتی اگر سرور کاملا سالم بالا بیاید، این نشانی بررسی می‌شود و جواب درست نمی‌دهد. هر دو خطا از جنس یکی هستند: تشخیص ایستا فایل‌ها را نگاه می‌کند و ساختار واقعی پروژه را نمی‌خواند.

حالا واقعا اجرا می‌کنیم. این اجرا حدود ۶۸ ثانیه طول کشید، چون فاز start تمام ۶۰ ثانیه‌ی انتظار را مصرف کرد و بعد نتیجه را FAIL اعلام کرد:

$ hermes verify ./vdemo
Recipe: FastAPI app (fastapi) — source: detected

  bootstrap  PASS        8.1s  pip install -r requirements.txt
  start      FAIL       60.0s  uvicorn main:app --host 0.0.0.0 --port 8000

Readiness: http://127.0.0.1:8000/ -> not ready
 (<urlopen error [Errno 111] Connection refused>)

Result: FAILED

--- output tail: start ---
ERROR:    Error loading ASGI app. Could not import module "main".

این خروجی همان چیزی را می‌گوید که یک تست سبز به شما نمی‌گفت: سرویس اصلا بالا نیامد. توجه کنید که خطای واقعی در انتهای خروجی چاپ شده، نه در عنوان خط. اگر فقط چند خط اول را می‌دیدید، فکر می‌کردید مشکل از پورت است.

دو خط را درست می‌کنیم و دوباره اجرا می‌کنیم

راه‌حل این نیست که هر بار دستی تست را اجرا کنیم. یک بار دستور درست را ذخیره می‌کنیم و از آن به بعد همه‌ی اجراها از روی همان فایل خوانده می‌شوند. پرچم --save دستور تشخیص‌داده‌شده را در فایل .hermes/environment.json داخل خود پروژه می‌نویسد:

$ hermes verify ./vdemo --detect-only --save
{
  "source": "detected",
  "recipe": { ... }
}

# دو خط غلط را دستی درست می‌کنیم و دلیلش را در evidence می‌نویسیم
$ cat ./vdemo/.hermes/environment.json
{
  "version": 1,
  "recipe": {
    "name": "FastAPI app",
    "kind": "fastapi",
    "bootstrap": ["pip install -r requirements.txt"],
    "build": [],
    "test": [],
    "start": "uvicorn app.main:app --host 0.0.0.0 --port 8000",
    "port": 8000,
    "readinessPath": "/health",
    "evidence": [
      "Start module corrected to app.main after the run failed with Could not import module \"main\"",
      "Readiness path corrected to /health, the route the app actually serves"
    ]
  }
}

اجرای دوباره، این بار از روی فایل. عبارت source از detected به manifest تغییر کرده، یعنی دیگر از حدس زدن نمی‌آید. و فاز test اصلا اجرا نشد، چون در فایل خالی است و هر فاز خالی از فهرست مراحل حذف می‌شود:

یک دام واقعی که با خودم دیدم

فاز آماده شدن فقط می‌پرسد سرور جواب می‌دهد یا نه، و حتی خطای 404 را هم «بالا» حساب می‌کند. این رفتار در راهنما نوشته نشده و خودم موقع آزمایش به آن برخوردم. برای اثبات، همان پروژه‌ی سالم را دوباره اجرا کردم و فقط مسیر آماده شدن را به یک مسیر ناموجود بردم:

# بار اول: مسیر درست، کد ۲۰۰
$ hermes verify ./vdemo
Readiness: http://127.0.0.1:8000/health -> ready (HTTP 200)
Result: OK

# بار دوم: فقط readinessPath را به مسیر ناموجود عوض کردم
$ hermes verify ./vdemo
Recipe: FastAPI app (fastapi) — source: manifest

  bootstrap  PASS        0.4s  pip install -r requirements.txt
  start      PASS        1.2s  uvicorn app.main:app --host 0.0.0.0 --port 8000

Readiness: http://127.0.0.1:8000/no-such-route -> ready (HTTP 404)

Result: OK

نتیجه باز هم OK است. رفتار از کد پیاده‌سازی هم پیداست: وقتی سرور خطای HTTP برمی‌گرداند، آن را به عنوان «سرور بالا است» حساب می‌کند، چون هدف فاز آماده شدن اثبات زنده بودن پروسه است نه اثبات درستی مسیر.

پس نتیجه‌ی OK را به تنهایی مدرک سلامت مسیر ندانید. همیشه کد وضعیت را در همان خط آماده شدن نگاه کنید. اگر عددی غیر از ۲۰۰ دیدید، یعنی مسیر را اشتباه گرفته‌اید و باید به فایل برگردید.

این را کجا استفاده کنیم

بهترین جای استفاده، قبل از تحویل کار است. وقتی ایجنت چند فایل را عوض کرده و شما می‌خواهید مطمئن شویم که پروژه هنوز بالا می‌آید، همین یک دستور جواب را قطعی می‌کند. در خط لوله‌ی ساخت هم جای خودش را دارد، چون خروجی --json یک نتیجه‌ی ساخت‌یافته می‌دارد که می‌شود روی آن شرط گذاشت.

$ hermes verify ./vdemo --json
{
  "recipe": "FastAPI app",
  "ok": true,
  "phases": [
    {
      "phase": "bootstrap",
      "command": "pip install -r requirements.txt",
      "exitCode": 0,
      "duration": 0.448,
      "ok": true,
      "timedOut": false
    }
  ],
  "readiness": {
    "url": "http://127.0.0.1:8000/health",
    "ready": true,
    "statusCode": 200,
    "duration": 1.293
  },
  "source": "manifest"
}

برای خط لوله، پرچم --phase هم مفید است: می‌توانید فقط فاز test را اجرا کنید و دوباره سرور را بالا نیاورید. برای پروژه‌هایی که اپلیکیشن‌شان اصلا سورس دائمی ندارد، همان --skip-start کار را می‌کند و فقط مرحله‌های اجرایی را می‌سنجد.

دو نکته‌ی عملی برای انتهای کار. اول، فایل .hermes/environment.json را در گیت نگه دارید. روشن نیست که باید در .gitignore برود یا نه، اما یک چیز قطعی است: هر بار که این فایل وجود داشته باشد، تشخیص خودکار را کنار می‌زند و مقدار شما را می‌خواند. پس اگر تیم در این مورد توافق نکرده، همان توافق را در کامیت کنار فایل بنویسید.

دوم، اگر ایجنت خودتان دارد با این پروژه کار می‌کند، این فایل یک سند از وضعیت واقعی محیط است و ارزش نگه داشتن دارد. هر تلاشی که در پست لایه‌ی مجوز ایجنت و اعداد واقعی یک بازبین خودکار درباره‌ی سنجیدن ادعاها با عدد گرفته بود، همین مشکل را از سمت دیگری حل می‌کرد: آنجا ادعای ایجنت را می‌سنجیدیم، اینجا ادعای خود پروژه را.

جمع‌بندی

سه چیز را با عدد ثابت کردیم. اول اینکه تشخیص خودکار روی یک اپلیکیشن واقعی FastAPI، هم ماژول start را اشتباه گرفت و هم مسیر آماده شدن را، و اجرا با خطای ۶۸ ثانیه‌ای FAIL شد. دوم اینکه بعد از اصلاح دو خط در فایل، اجرا به Result: OK با کد ۲۰۰ رسید و کل زمان به زیر دو ثانیه افتاد. سوم اینکه یک مسیر ناموجود هم OK می‌دهد، پس کد وضعیت را باید خواند.

این دستور جای تست را نمی‌گیرد و قرار نیست بگیرد. تست می‌گوید منطق درست است؛ این دستور می‌گوید پروژه اصلا بالا می‌آید و روی پورتش جواب می‌دهد. این دو سوال جدا هستند و پروژه‌های واقعی هر دو را می‌خواهند.

منابع

  1. راهنمای خط فرمان هرمز — اجرای برنامه و پاک‌سازی worktree.
  2. مرجع زیر‌دستورهای خط فرمان هرمز — فهرست کامل زیر‌دستورها.
  3. کد runner.py — اجرای مراحل و حلقه‌ی آماده شدن.
  4. کد recipes.py — تشخیص ایستای دستور و ترتیب اولویت فریم‌ورک‌ها.
  5. کد environment.py — خواندن و نوشتن فایل environment.json.
  6. مخزن اصلی هرمز ایجنت در گیت‌هاب — صفحه‌ی اصلی مخزن.
  7. راهنمای worktree‌های گیت — اجرای موازی ایجنت‌ها.
  8. راهنمای نقاط بازگشت و بازگردانی فایل‌ها — لایه‌ی ایمنی کنار تغییر فایل.
  9. راهنمای انتقال تنظیمات از کلاد کد و کدکس
  10. مرجع متغیرهای محیطی هرمز