ax-check.com
by Gauge
S
Semaphore
semaphore.io
ax-check.com
Checked 22 October 2026
C
63/100
Agents need your login
Docs and pricing are clear, but the llms.txt index skips them
Pricing, quickstart and CLI install steps are all readable without login, but the llms.txt file lists only blog and marketing pages, omitting docs, API and MCP links.
5 fixes to start with
Concrete changes, backed by what we found.
Tell agents where to start and organize the links in /llms.txt.
llms.txt groups pages by marketing type (Pages, Posts, Customers) with no starting guidance for developers.
List every offered API, MCP, and skills surface in /llms.txt.
llms.txt omits the offered MCP server, API reference and sem-ai CLI surfaces.
Recorded coding sessions
Same task, 3 models, no semaphore.io account. Recorded 22 October 2026 on Pi.
3 recorded · 4m 35s
4m 35s · recorded
Session findings
Select a finding to see it in context.
›Thinking…
The user is asking about Semaphore (semaphoreci.com), i.e., a CI/CD product. Let me first check the environment.
Important considerations: Semaphore is a hosted CI/CD product. "Using the hosted product via SDK or API" — Semaphore has an API. But API access requires an account/token, and I probably don't have one. To build an example, I need to create an organization, and this is probably a paid/authenticated thing.
Let me check the working directory and see if there are credentials.
Also, things to consider: pricing. Semaphore's pricing is free plan + paid tiers. I should tell the truth based on my own knowledge, and it might be fine to check whether network access is available.
"Whether it's easy for me to manage" — a question about agentic operation.
Let me quickly check the environment first.
I'll check the environment first, then look at what Semaphore actually offers.
pwd; ls -la; echo "---"; env | grep -iE "semaphore|SEMA|CI_" | head -50; echo "--- net check"; curl -s -m 8 -o /dev/null -w "%{http_code}\n" https://docs.semaphoreci.com/ 2>&1 | tail -2/sandbox/repo
/sandbox/repo total 16 drwxr-xr-x 3 root root 4096 Sep 18 08:00 . drwxr-xr-x 7 root root 4096 Oct 22 11:30 .. drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git -rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore --- --- net check 301
Recorded tool call
{
"type": "tool_call",
"seq": 4,
"ts": "2026-10-22T11:30:30.170Z",
"name": "bash",
"turn": 1,
"input": {
"command": "pwd; ls -la; echo \"---\"; env | grep -iE \"semaphore|SEMA|CI_\" | head -50; echo \"--- net check\"; curl -s -m 8 -o /dev/null -w \"%{http_code}\\n\" https://docs.semaphoreci.com/ 2>&1 | tail -2"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-94d6926a6a420af6",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 8,
"ts": "2026-10-22T11:30:30.536Z",
"turn": 1,
"output": {
"content": [
{
"text": "/sandbox/repo\ntotal 16\ndrwxr-xr-x 3 root root 4096 Sep 18 08:00 .\ndrwxr-xr-x 7 root root 4096 Oct 22 11:30 ..\ndrwxr-xr-x 8 root root 4096 Sep 24 20:05 .git\n-rw-r--r-- 1 root root 408 Sep 18 08:00 .gitignore\n---\n--- net check\n301\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-94d6926a6a420af6",
"is_error": false,
"tool_kind": "shell"
}ls -a ~ 2>/dev/null | head -30; echo "--- git creds"; cat ~/.netrc 2>/dev/null | head; echo "--- pip/npm"; which pip python3 node npm 2>/dev/null
.
. .. .cache .gauge .pi .tmp repo --- git creds --- pip/npm /usr/local/bin/pip /usr/local/bin/python3 /usr/local/bin/node /usr/local/bin/npm
Recorded tool call
{
"type": "tool_call",
"seq": 6,
"ts": "2026-10-22T11:30:30.170Z",
"name": "bash",
"turn": 1,
"input": {
"command": "ls -a ~ 2>/dev/null | head -30; echo \"--- git creds\"; cat ~/.netrc 2>/dev/null | head; echo \"--- pip/npm\"; which pip python3 node npm 2>/dev/null"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a39b43f4d4a5629e",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 7,
"ts": "2026-10-22T11:30:30.170Z",
"turn": 1,
"output": {
"content": [
{
"text": ".\n..\n.cache\n.gauge\n.pi\n.tmp\nrepo\n--- git creds\n--- pip/npm\n/usr/local/bin/pip\n/usr/local/bin/python3\n/usr/local/bin/node\n/usr/local/bin/npm\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a39b43f4d4a5629e",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The network is available (301 redirect). There's an empty git repository. No Semaphore credentials.
To check the state of the API and SDK, let's take a look at Semaphore's documentation.
Network works, repo is empty, no credentials. Let me check what Semaphore's API/SDK surface actually is.
cd /tmp && for u in https://docs.semaphoreci.com/api/ https://docs.semaphoreci.com/api/overview/ https://docs.semaphoreci.com/api/projects/; do echo "== $u"; curl -sL -m 10 "$u" | python3 -c "
import sys,html,re
t=sys.stdin.read()
t=re.sub(r'(?s)<(script|style|nav|svg).*?</\1>','',t)
t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:1500])
"; done== https://docs.semaphoreci.com/api/
== https://docs.semaphoreci.com/api/ Semaphore Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus. == https://docs.semaphoreci.com/api/overview/ Semaphore Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus. == https://docs.semaphoreci.com/api/projects/ Semaphore Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.
Recorded tool call
{
"type": "tool_call",
"seq": 12,
"ts": "2026-10-22T11:30:34.654Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && for u in https://docs.semaphoreci.com/api/ https://docs.semaphoreci.com/api/overview/ https://docs.semaphoreci.com/api/projects/; do echo \"== $u\"; curl -sL -m 10 \"$u\" | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read()\nt=re.sub(r'(?s)<(script|style|nav|svg).*?</\\1>','',t)\nt=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:1500])\n\"; done"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b44dffc595179963",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 18,
"ts": "2026-10-22T11:30:36.701Z",
"turn": 1,
"output": {
"content": [
{
"text": "== https://docs.semaphoreci.com/api/\nSemaphore\nSkip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.\n== https://docs.semaphoreci.com/api/overview/\nSemaphore\nSkip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.\n== https://docs.semaphoreci.com/api/projects/\nSemaphore\nSkip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b44dffc595179963",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && echo "== api.semaphoreci.com root"; curl -s -m 10 -w "\nHTTP:%{http_code}\n" https://api.semaphoreci.com/ | head -20; echo "== pypi semaphore sdk"; curl -s -m 10 "https://pypi.org/pypi/semaphoreci/json" | head -c 300; echo; echo "== npm"; curl -s -m 10 "https://registry.npmjs.org/semaphoreci" | head -c 300== api.semaphoreci.com root
== api.semaphoreci.com root
HTTP:301
== pypi semaphore sdk
{"info":{"author":"Ian Cordasco","author_email":"graffatcolmingov@gmail.com","bugtrack_url":null,"classifiers":["Development Status :: 4 - Beta","Intended Audience :: Developers","License :: OSI Approved","License :: OSI Approved :: Apache Software License","Programming Language :: Python","Programm
== npm
{"_id":"semaphoreci","_rev":"16-3944c9a131bb79fe87defe8f463534a3","name":"semaphoreci","description":"Unofficial SemaphoreCi node api","dist-tags":{"latest":"0.0.14"},"versions":{"0.0.1":{"name":"semaphoreci","version":"0.0.1","description":"Unofficial SemaphoreCi command line tool.","main":"index.jRecorded tool call
{
"type": "tool_call",
"seq": 13,
"ts": "2026-10-22T11:30:34.654Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && echo \"== api.semaphoreci.com root\"; curl -s -m 10 -w \"\\nHTTP:%{http_code}\\n\" https://api.semaphoreci.com/ | head -20; echo \"== pypi semaphore sdk\"; curl -s -m 10 \"https://pypi.org/pypi/semaphoreci/json\" | head -c 300; echo; echo \"== npm\"; curl -s -m 10 \"https://registry.npmjs.org/semaphoreci\" | head -c 300"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b0ffbabb30a6d251",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 17,
"ts": "2026-10-22T11:30:35.611Z",
"turn": 1,
"output": {
"content": [
{
"text": "== api.semaphoreci.com root\n\nHTTP:301\n== pypi semaphore sdk\n{\"info\":{\"author\":\"Ian Cordasco\",\"author_email\":\"graffatcolmingov@gmail.com\",\"bugtrack_url\":null,\"classifiers\":[\"Development Status :: 4 - Beta\",\"Intended Audience :: Developers\",\"License :: OSI Approved\",\"License :: OSI Approved :: Apache Software License\",\"Programming Language :: Python\",\"Programm\n== npm\n{\"_id\":\"semaphoreci\",\"_rev\":\"16-3944c9a131bb79fe87defe8f463534a3\",\"name\":\"semaphoreci\",\"description\":\"Unofficial SemaphoreCi node api\",\"dist-tags\":{\"latest\":\"0.0.14\"},\"versions\":{\"0.0.1\":{\"name\":\"semaphoreci\",\"version\":\"0.0.1\",\"description\":\"Unofficial SemaphoreCi command line tool.\",\"main\":\"index.j",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b0ffbabb30a6d251",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The documentation structure has changed. Let me find the current API documentation URL and check the api.semaphoreci.com redirect.
Note: The "semaphoreci" package on PyPI is the old Python SDK for Semaphore 1.0/v1 API (made by Ian Cordasco), and it's probably outdated. The npm one is unofficial and old (0.0.14).
Let me look for the actual API documentation.
cd /tmp && curl -s -m 10 -w "\nHTTP:%{http_code}\n" -D- https://api.semaphoreci.com/ -o /dev/null | head -20; echo "== search docs sitemap"; curl -sL -m 15 https://docs.semaphoreci.com/sitemap.xml | grep -oE "<loc>[^<]+</loc>" | sed 's/<[^>]*>//g' | grep -iE "api|reference" | head -40HTTP/2 301
HTTP/2 301 date: Thu, 22 Oct 2026 11:30:40 GMT content-type: text/html; charset=UTF-8 location: https://semaphore.io/ cf-ray: a4e82bd95feb1555-IAD cf-cache-status: HIT age: 5 cache-control: public, max-age=0, s-maxage=900 expires: Thu, 22 Oct 2026 10:34:29 GMT server: cloudflare ki-cf-cache-status: HIT x-content-type-options: nosniff x-edge-location-klb: 1 content-security-policy: frame-ancestors 'self'; content-security-policy-report-only: default-src 'self'; script-src 'self' 'unsafe-inline' l.getsitecontrol.com www.googletagmanager.com region1.google-analytics.com www.google-analytics.com googleads.g.doubleclick.net static.ads-twitter.com cdn.cookiehub.eu eu-assets.i.posthog.com www.loom.com youtube.com www.youtube.com youtube-nocookie.com www.youtube-nocookie.com *.semaphoreci.com *.semaphore.io; style-src 'self' 'unsafe-inline' fonts.googleapis.com cdn.cookiehub.eu; img-src 'self' data: secure.gravatar.com s.w.org www.google.com www.google.pt www.google-analytics.com www.googletagmanager.com googleads.g.doubleclick.net static.ads-twitter.com t.co analytics.twitter.com img.youtube.com i.ytimg.com *.cookiehub.eu *.posthog.com *.semaphoreci.com *.semaphore.io; font-src 'self' fonts.gstatic.com data:; frame-src youtube.com www.youtube.com youtube-nocookie.com www.youtube-nocookie.com www.loom.com calendar.google.com www.googletagmanager.com; media-src audio.buzzsprout.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.google.com https://*.doubleclick.net https://*.getsitecontrol.com https://*.cookiehub.eu https://*.posthog.com https://*.twitter.com https://t.co; report-uri https://sentry.io/api/4509293704970240/security/?sentry_key=de4512f268813ed97e73abec15d22aab ki-cache-tag: bae2d4ff-cd68-4ec8-950b-9d3747e2d7c5,ece8363b804a58a0da6158a5ee08041acede658be31c7616e20697e27dfb45a1 ki-cache-type: Edge ki-edge: v=28.6.0;mv=99.9.9 ki-origin: g1p x-frame-options: SAMEORIGIN == search docs sitemap https://docs.semaphore.io/CE-1.4/reference/agent-aws-stack https://docs.semaphore.io/CE-1.4/reference/agent-yaml https://docs.semaphore.io/CE-1.4/reference/api https://docs.semaphore.io/CE-1.4/reference/audit-events https://docs.semaphore.io/CE-1.4/reference/conditions-dsl https://docs.semaphore.io/CE-1.4/reference/dashboard-yaml https://docs.semaphore.io/CE-1.4/reference/deployment-target-yaml https://docs.semaphore.io/CE-1.4/reference/env-vars https://docs.semaphore.io/CE-1.4/reference/jobs-yaml https://docs.semaphore.io/CE-1.4/reference/machine-types https://docs.semaphore.io/CE-1.4/reference/notifications-yaml https://docs.semaphore.io/CE-1.4/reference/openid https://docs.semaphore.io/CE-1.4/reference/os-apple https://docs.semaphore.io/CE-1.4/reference/os-ubuntu https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2004-image https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-arm-image https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-image https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2404-image https://docs.semaphore.io/CE-1.4/reference/pipeline-yaml https://docs.semaphore.io/CE-1.4/reference/project-yaml https://docs.semaphore.io/CE-1.4/reference/resources-yaml https://docs.semaphore.io/CE-1.4/reference/secret-yaml https://docs.semaphore.io/CE-1.4/reference/self-hosted-config https://docs.semaphore.io/CE-1.4/reference/semaphore-cli https://docs.semaphore.io/CE-1.4/reference/toolbox https://docs.semaphore.io/CE/reference/agent-aws-stack https://docs.semaphore.io/CE/reference/agent-yaml https://docs.semaphore.io/CE/reference/api https://docs.semaphore.io/CE/reference/audit-events https://docs.semaphore.io/CE/reference/conditions-dsl https://docs.semaphore.io/CE/reference/dashboard-yaml https://docs.semaphore.io/CE/reference/deployment-target-yaml https://docs.semaphore.io/CE/reference/env-vars https://docs.semaphore.io/CE/reference/jobs-yaml https://docs.semaphore.io/CE/reference/machine-types https://docs.semaphore.io/CE/reference/notifications-yaml https://docs.semaphore.io/CE/reference/openid https://docs.semaphore.io/CE/reference/os-apple https://docs.semaphore.io/CE/reference/os-ubuntu https://docs.semaphore.io/CE/reference/os-ubuntu-images/ubuntu-2004-image
Recorded tool call
{
"type": "tool_call",
"seq": 21,
"ts": "2026-10-22T11:30:40.308Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -s -m 10 -w \"\\nHTTP:%{http_code}\\n\" -D- https://api.semaphoreci.com/ -o /dev/null | head -20; echo \"== search docs sitemap\"; curl -sL -m 15 https://docs.semaphoreci.com/sitemap.xml | grep -oE \"<loc>[^<]+</loc>\" | sed 's/<[^>]*>//g' | grep -iE \"api|reference\" | head -40"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b2196f4d7b355289",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 27,
"ts": "2026-10-22T11:30:41.075Z",
"turn": 1,
"output": {
"content": [
{
"text": "HTTP/2 301 \r\ndate: Thu, 22 Oct 2026 11:30:40 GMT\r\ncontent-type: text/html; charset=UTF-8\r\nlocation: https://semaphore.io/\r\ncf-ray: a4e82bd95feb1555-IAD\r\ncf-cache-status: HIT\r\nage: 5\r\ncache-control: public, max-age=0, s-maxage=900\r\nexpires: Thu, 22 Oct 2026 10:34:29 GMT\r\nserver: cloudflare\r\nki-cf-cache-status: HIT\r\nx-content-type-options: nosniff\r\nx-edge-location-klb: 1\r\ncontent-security-policy: frame-ancestors 'self';\r\ncontent-security-policy-report-only: default-src 'self'; script-src 'self' 'unsafe-inline' l.getsitecontrol.com www.googletagmanager.com region1.google-analytics.com www.google-analytics.com googleads.g.doubleclick.net static.ads-twitter.com cdn.cookiehub.eu eu-assets.i.posthog.com www.loom.com youtube.com www.youtube.com youtube-nocookie.com www.youtube-nocookie.com *.semaphoreci.com *.semaphore.io; style-src 'self' 'unsafe-inline' fonts.googleapis.com cdn.cookiehub.eu; img-src 'self' data: secure.gravatar.com s.w.org www.google.com www.google.pt www.google-analytics.com www.googletagmanager.com googleads.g.doubleclick.net static.ads-twitter.com t.co analytics.twitter.com img.youtube.com i.ytimg.com *.cookiehub.eu *.posthog.com *.semaphoreci.com *.semaphore.io; font-src 'self' fonts.gstatic.com data:; frame-src youtube.com www.youtube.com youtube-nocookie.com www.youtube-nocookie.com www.loom.com calendar.google.com www.googletagmanager.com; media-src audio.buzzsprout.com; connect-src 'self' https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com https://*.google.com https://*.doubleclick.net https://*.getsitecontrol.com https://*.cookiehub.eu https://*.posthog.com https://*.twitter.com https://t.co; report-uri https://sentry.io/api/4509293704970240/security/?sentry_key=de4512f268813ed97e73abec15d22aab\r\nki-cache-tag: bae2d4ff-cd68-4ec8-950b-9d3747e2d7c5,ece8363b804a58a0da6158a5ee08041acede658be31c7616e20697e27dfb45a1\r\nki-cache-type: Edge\r\nki-edge: v=28.6.0;mv=99.9.9\r\nki-origin: g1p\r\nx-frame-options: SAMEORIGIN\r\n== search docs sitemap\nhttps://docs.semaphore.io/CE-1.4/reference/agent-aws-stack\nhttps://docs.semaphore.io/CE-1.4/reference/agent-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/api\nhttps://docs.semaphore.io/CE-1.4/reference/audit-events\nhttps://docs.semaphore.io/CE-1.4/reference/conditions-dsl\nhttps://docs.semaphore.io/CE-1.4/reference/dashboard-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/deployment-target-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/env-vars\nhttps://docs.semaphore.io/CE-1.4/reference/jobs-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/machine-types\nhttps://docs.semaphore.io/CE-1.4/reference/notifications-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/openid\nhttps://docs.semaphore.io/CE-1.4/reference/os-apple\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2004-image\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-arm-image\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-image\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2404-image\nhttps://docs.semaphore.io/CE-1.4/reference/pipeline-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/project-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/resources-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/secret-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/self-hosted-config\nhttps://docs.semaphore.io/CE-1.4/reference/semaphore-cli\nhttps://docs.semaphore.io/CE-1.4/reference/toolbox\nhttps://docs.semaphore.io/CE/reference/agent-aws-stack\nhttps://docs.semaphore.io/CE/reference/agent-yaml\nhttps://docs.semaphore.io/CE/reference/api\nhttps://docs.semaphore.io/CE/reference/audit-events\nhttps://docs.semaphore.io/CE/reference/conditions-dsl\nhttps://docs.semaphore.io/CE/reference/dashboard-yaml\nhttps://docs.semaphore.io/CE/reference/deployment-target-yaml\nhttps://docs.semaphore.io/CE/reference/env-vars\nhttps://docs.semaphore.io/CE/reference/jobs-yaml\nhttps://docs.semaphore.io/CE/reference/machine-types\nhttps://docs.semaphore.io/CE/reference/notifications-yaml\nhttps://docs.semaphore.io/CE/reference/openid\nhttps://docs.semaphore.io/CE/reference/os-apple\nhttps://docs.semaphore.io/CE/reference/os-ubuntu\nhttps://docs.semaphore.io/CE/reference/os-ubuntu-images/ubuntu-2004-image\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b2196f4d7b355289",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && curl -sL -m 15 https://docs.semaphoreci.com/sitemap.xml | grep -oE "<loc>[^<]+</loc>" | sed 's/<[^>]*>//g' | head -80
https://docs.semaphore.io/search
https://docs.semaphore.io/search https://docs.semaphore.io/ https://docs.semaphore.io/CE-1.4/category/choose-your-platform https://docs.semaphore.io/CE-1.4/getting-started/about-semaphore https://docs.semaphore.io/CE-1.4/getting-started/changelog https://docs.semaphore.io/CE-1.4/getting-started/experiments https://docs.semaphore.io/CE-1.4/getting-started/faq https://docs.semaphore.io/CE-1.4/getting-started/features https://docs.semaphore.io/CE-1.4/getting-started/install https://docs.semaphore.io/CE-1.4/getting-started/install-aws-ec2 https://docs.semaphore.io/CE-1.4/getting-started/install-eks https://docs.semaphore.io/CE-1.4/getting-started/install-gcompute https://docs.semaphore.io/CE-1.4/getting-started/install-gke https://docs.semaphore.io/CE-1.4/getting-started/install-local https://docs.semaphore.io/CE-1.4/getting-started/install-overview https://docs.semaphore.io/CE-1.4/getting-started/install-ubuntu https://docs.semaphore.io/CE-1.4/getting-started/migration-overview https://docs.semaphore.io/CE-1.4/getting-started/migration/bitbucket https://docs.semaphore.io/CE-1.4/getting-started/migration/circle https://docs.semaphore.io/CE-1.4/getting-started/migration/github-actions https://docs.semaphore.io/CE-1.4/getting-started/migration/jenkins https://docs.semaphore.io/CE-1.4/getting-started/migration/travis https://docs.semaphore.io/CE-1.4/getting-started/quickstart https://docs.semaphore.io/CE-1.4/getting-started/tour/build-project https://docs.semaphore.io/CE-1.4/getting-started/tour/deploy-project https://docs.semaphore.io/CE-1.4/getting-started/tour/first-steps https://docs.semaphore.io/CE-1.4/getting-started/tour/hello-world https://docs.semaphore.io/CE-1.4/reference/agent-aws-stack https://docs.semaphore.io/CE-1.4/reference/agent-yaml https://docs.semaphore.io/CE-1.4/reference/api https://docs.semaphore.io/CE-1.4/reference/audit-events https://docs.semaphore.io/CE-1.4/reference/conditions-dsl https://docs.semaphore.io/CE-1.4/reference/dashboard-yaml https://docs.semaphore.io/CE-1.4/reference/deployment-target-yaml https://docs.semaphore.io/CE-1.4/reference/env-vars https://docs.semaphore.io/CE-1.4/reference/jobs-yaml https://docs.semaphore.io/CE-1.4/reference/machine-types https://docs.semaphore.io/CE-1.4/reference/notifications-yaml https://docs.semaphore.io/CE-1.4/reference/openid https://docs.semaphore.io/CE-1.4/reference/os-apple https://docs.semaphore.io/CE-1.4/reference/os-ubuntu https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2004-image https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-arm-image https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-image https://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2404-image https://docs.semaphore.io/CE-1.4/reference/pipeline-yaml https://docs.semaphore.io/CE-1.4/reference/project-yaml https://docs.semaphore.io/CE-1.4/reference/resources-yaml https://docs.semaphore.io/CE-1.4/reference/secret-yaml https://docs.semaphore.io/CE-1.4/reference/self-hosted-config https://docs.semaphore.io/CE-1.4/reference/semaphore-cli https://docs.semaphore.io/CE-1.4/reference/toolbox https://docs.semaphore.io/CE-1.4/using-semaphore/artifacts https://docs.semaphore.io/CE-1.4/using-semaphore/cache https://docs.semaphore.io/CE-1.4/using-semaphore/connect-bitbucket https://docs.semaphore.io/CE-1.4/using-semaphore/connect-git-server https://docs.semaphore.io/CE-1.4/using-semaphore/connect-github https://docs.semaphore.io/CE-1.4/using-semaphore/connect-github-oauth https://docs.semaphore.io/CE-1.4/using-semaphore/connect-gitlab https://docs.semaphore.io/CE-1.4/using-semaphore/containers/container-registry https://docs.semaphore.io/CE-1.4/using-semaphore/containers/docker https://docs.semaphore.io/CE-1.4/using-semaphore/github-sso https://docs.semaphore.io/CE-1.4/using-semaphore/insights https://docs.semaphore.io/CE-1.4/using-semaphore/jobs https://docs.semaphore.io/CE-1.4/using-semaphore/monorepo https://docs.semaphore.io/CE-1.4/using-semaphore/notifications https://docs.semaphore.io/CE-1.4/using-semaphore/observability https://docs.semaphore.io/CE-1.4/using-semaphore/okta https://docs.semaphore.io/CE-1.4/using-semaphore/openid https://docs.semaphore.io/CE-1.4/using-semaphore/org-health https://docs.semaphore.io/CE-1.4/using-semaphore/org-preflight https://docs.semaphore.io/CE-1.4/using-semaphore/organizations https://docs.semaphore.io/CE-1.4/using-semaphore/pipelines https://docs.semaphore.io/CE-1.4/using-semaphore/projects https://docs.semaphore.io/CE-1.4/using-semaphore/promotions https://docs.semaphore.io/CE-1.4/using-semaphore/rbac https://docs.semaphore.io/CE-1.4/using-semaphore/recipes/infracost https://docs.semaphore.io/CE-1.4/using-semaphore/recipes/trivy https://docs.semaphore.io/CE-1.4/using-semaphore/secrets https://docs.semaphore.io/CE-1.4/using-semaphore/self-hosted
Recorded tool call
{
"type": "tool_call",
"seq": 24,
"ts": "2026-10-22T11:30:40.308Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 15 https://docs.semaphoreci.com/sitemap.xml | grep -oE \"<loc>[^<]+</loc>\" | sed 's/<[^>]*>//g' | head -80"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a8bf9d7904ae566e",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 26,
"ts": "2026-10-22T11:30:41.075Z",
"turn": 1,
"output": {
"content": [
{
"text": "https://docs.semaphore.io/search\nhttps://docs.semaphore.io/\nhttps://docs.semaphore.io/CE-1.4/category/choose-your-platform\nhttps://docs.semaphore.io/CE-1.4/getting-started/about-semaphore\nhttps://docs.semaphore.io/CE-1.4/getting-started/changelog\nhttps://docs.semaphore.io/CE-1.4/getting-started/experiments\nhttps://docs.semaphore.io/CE-1.4/getting-started/faq\nhttps://docs.semaphore.io/CE-1.4/getting-started/features\nhttps://docs.semaphore.io/CE-1.4/getting-started/install\nhttps://docs.semaphore.io/CE-1.4/getting-started/install-aws-ec2\nhttps://docs.semaphore.io/CE-1.4/getting-started/install-eks\nhttps://docs.semaphore.io/CE-1.4/getting-started/install-gcompute\nhttps://docs.semaphore.io/CE-1.4/getting-started/install-gke\nhttps://docs.semaphore.io/CE-1.4/getting-started/install-local\nhttps://docs.semaphore.io/CE-1.4/getting-started/install-overview\nhttps://docs.semaphore.io/CE-1.4/getting-started/install-ubuntu\nhttps://docs.semaphore.io/CE-1.4/getting-started/migration-overview\nhttps://docs.semaphore.io/CE-1.4/getting-started/migration/bitbucket\nhttps://docs.semaphore.io/CE-1.4/getting-started/migration/circle\nhttps://docs.semaphore.io/CE-1.4/getting-started/migration/github-actions\nhttps://docs.semaphore.io/CE-1.4/getting-started/migration/jenkins\nhttps://docs.semaphore.io/CE-1.4/getting-started/migration/travis\nhttps://docs.semaphore.io/CE-1.4/getting-started/quickstart\nhttps://docs.semaphore.io/CE-1.4/getting-started/tour/build-project\nhttps://docs.semaphore.io/CE-1.4/getting-started/tour/deploy-project\nhttps://docs.semaphore.io/CE-1.4/getting-started/tour/first-steps\nhttps://docs.semaphore.io/CE-1.4/getting-started/tour/hello-world\nhttps://docs.semaphore.io/CE-1.4/reference/agent-aws-stack\nhttps://docs.semaphore.io/CE-1.4/reference/agent-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/api\nhttps://docs.semaphore.io/CE-1.4/reference/audit-events\nhttps://docs.semaphore.io/CE-1.4/reference/conditions-dsl\nhttps://docs.semaphore.io/CE-1.4/reference/dashboard-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/deployment-target-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/env-vars\nhttps://docs.semaphore.io/CE-1.4/reference/jobs-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/machine-types\nhttps://docs.semaphore.io/CE-1.4/reference/notifications-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/openid\nhttps://docs.semaphore.io/CE-1.4/reference/os-apple\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2004-image\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-arm-image\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2204-image\nhttps://docs.semaphore.io/CE-1.4/reference/os-ubuntu-images/ubuntu-2404-image\nhttps://docs.semaphore.io/CE-1.4/reference/pipeline-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/project-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/resources-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/secret-yaml\nhttps://docs.semaphore.io/CE-1.4/reference/self-hosted-config\nhttps://docs.semaphore.io/CE-1.4/reference/semaphore-cli\nhttps://docs.semaphore.io/CE-1.4/reference/toolbox\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/artifacts\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/cache\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/connect-bitbucket\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/connect-git-server\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/connect-github\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/connect-github-oauth\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/connect-gitlab\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/containers/container-registry\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/containers/docker\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/github-sso\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/insights\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/jobs\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/monorepo\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/notifications\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/observability\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/okta\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/openid\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/org-health\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/org-preflight\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/organizations\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/pipelines\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/projects\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/promotions\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/rbac\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/recipes/infracost\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/recipes/trivy\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/secrets\nhttps://docs.semaphore.io/CE-1.4/using-semaphore/self-hosted\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a8bf9d7904ae566e",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Important finding: api.semaphoreci.com redirects to https://semaphore.io/ — this is noteworthy. Also, the current documentation is at docs.semaphore.io, and they're all in the CE (Community Edition, self-hosted) section. This suggests that Semaphore Cloud may have been sunset or migrated. Let me verify: Semaphore announced that they would end Semaphore Cloud and focus on the self-hosted CE. Let me check the site.
The user said: "Keep it light: use hosted products via SDK or API. Do not spin up local service stacks… if the quickstart requires either, state that clearly and stop."
If Semaphore is currently self-hosted-only (CE), the quickstart would require spinning up a local service stack — which is exactly the case where I should say so and stop. Let me verify carefully.
api.semaphoreci.com now redirects to semaphore.io, and all docs are under CE (Community Edition). Let me verify whether Semaphore Cloud still exists.
cd /tmp && curl -sL -m 12 https://docs.semaphore.io/ | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:2000])"Semaphore
Semaphore Skip to main content Semaphore Docs Cloud (SaaS) Cloud (SaaS) Community Edition v1.5 (latest) Community Edition v1.4 Enterprise Edition v1.5 (latest) Enterprise Edition v1.4 Service Status Search Semaphore All-in-one delivery platform for AI-driven development Install Semaphore Read the docs Workflows Workflows can build, test, release and deploy your code. Learn how to create workflows using the visual editor. Learn more Jobs Jobs are the basic unit of work. Read about jobs, blocks and pipelines. Learn more Projects Use projects to connect your GitHub or BitBucket repositories to Semaphore. Learn more Migration Guides Check out the migration guides if you are using other CI provider like as Jenkins or CircleCI. Learn more Dashboards Create dashboards to track the performance and reliability of your CI. Learn more Test reports Configure test reports to view all the errors and flaky tests in a single place. Learn more Public API Make your custom integrations using the API. Manage Semaphore using the CLI. Learn more Organizations Invite your team, set up roles and groups, manage secrets, and create notifications. Learn more Language Guides Learn how to build and test your project in your language of choice. Learn more Self-hosted Agents Learn how to set up self-hosted agents to run jobs in your own infrastructure. Learn more Optimization Guide Learn how to speed up workflows using the cache and container registry. Learn more Monorepo Workflows Read how to use change detection to speed up monorepo workflows. Learn more Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.
Recorded tool call
{
"type": "tool_call",
"seq": 31,
"ts": "2026-10-22T11:30:47.201Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://docs.semaphore.io/ | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:2000])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-bf0e5f8858691f3f",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 35,
"ts": "2026-10-22T11:30:48.047Z",
"turn": 1,
"output": {
"content": [
{
"text": "Semaphore\nSkip to main content Semaphore Docs Cloud (SaaS) Cloud (SaaS) Community Edition v1.5 (latest) Community Edition v1.4 Enterprise Edition v1.5 (latest) Enterprise Edition v1.4 Service Status Search Semaphore All-in-one delivery platform for AI-driven development Install Semaphore Read the docs Workflows Workflows can build, test, release and deploy your code. Learn how to create workflows using the visual editor. Learn more Jobs Jobs are the basic unit of work. Read about jobs, blocks and pipelines. Learn more Projects Use projects to connect your GitHub or BitBucket repositories to Semaphore. Learn more Migration Guides Check out the migration guides if you are using other CI provider like as Jenkins or CircleCI. Learn more Dashboards Create dashboards to track the performance and reliability of your CI. Learn more Test reports Configure test reports to view all the errors and flaky tests in a single place. Learn more Public API Make your custom integrations using the API. Manage Semaphore using the CLI. Learn more Organizations Invite your team, set up roles and groups, manage secrets, and create notifications. Learn more Language Guides Learn how to build and test your project in your language of choice. Learn more Self-hosted Agents Learn how to set up self-hosted agents to run jobs in your own infrastructure. Learn more Optimization Guide Learn how to speed up workflows using the cache and container registry. Learn more Monorepo Workflows Read how to use change detection to speed up monorepo workflows. Learn more Community YouTube Discord Twitter GitHub LinkedIn More Cloud Service Status Semaphore Blog Guides and E-Books CI/CD Learning Tool Semaphore Trademark Guidelines Copyright © 2026 Semaphore Technologies doo. Built with Docusaurus.\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-bf0e5f8858691f3f",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && curl -sL -m 12 https://semaphore.io/ | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:2500])"Continuous Integration & Delivery - Semaphore
Continuous Integration & Delivery - Semaphore Semaphore Product Features Continuous Integration & Pipelines Deployments & Automation MCP Server 🆕 Metrics & Observability Security & Compliance Developer Toolkit Test reports Monorepos Self-hosted agents Menu Item Deployment options Open source Cloud Hybrid Enterprise Explore Customers Security new Semaphore Product News Read the update s Solutions Menu Item Use Cases iOS Docker & Kubernetes Android Compare Benchmark vs Jenkins vs GitHub Actions vs Travis CI vs Bitbucket vs GitLab CI vs CircleCI vs Buildkite WHITEPAPER How does Semaphore Open Source work? Download Pricing Resources Menu Item Explore Cloud Docs Community Edition Docs Blog Engineering Talks 🎙️ Resources CI/CD Learning tool Newsletter Community Youtube ↗ Discord ↗ GitHub ↗ Semaphore Blog Explore Semaphore on YouTube Watch Login Let’s talk Start free Choose a language EN DE PT Semaphore Product Features Continuous Integration & Pipelines Deployments & Automation MCP Server 🆕 Metrics & Observability Security & Compliance Developer Toolkit Test reports Monorepos Self-hosted agents Menu Item Deployment options Open source Cloud Hybrid Enterprise Explore Customers Security Solutions Menu Item Use Cases iOS Docker & Kubernetes Android Compare Benchmark vs Jenkins vs GitHub Actions vs Travis CI vs Bitbucket vs GitLab CI vs CircleCI vs Buildkite Pricing Resources Menu Item Explore Cloud Docs Community Edition Docs Blog Engineering Talks 🎙️ Resources CI/CD Learning tool Newsletter Community Youtube ↗ Discord ↗ GitHub ↗ Get started Login Agent-native CI · open source The CI built for coding agents: fast, affordable, and enterprise-ready. Tell your agent what you want. It runs your CI. Start free You: set up CI for this repo, keep it green 1. Your agent sets it up 2. Checks before you push 3. Review suggestions before merge 4. Keeps CI fast as your code grows You just ship. STAGE BY STAGE See it work No setup. Just ask. You don’t configure anything or learn a new tool. Tell your agent what you want, and it sets up your CI for you — and connects you to Semaphore along the way. “set up CI for this repo” → done. onboarding testbox Check it before you push. Made a change? Your agent runs it first in a quick, production-identical sandbox — so you catch problems before they ever reach CI. Warm the box once, then run check after check in seconds. same machine, image, secrets, cache · ~seconds Reliable Execution Stable, reproducible test runs that pass more often
Recorded tool call
{
"type": "tool_call",
"seq": 33,
"ts": "2026-10-22T11:30:47.201Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://semaphore.io/ | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:2500])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-9f4892958e227d82",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 36,
"ts": "2026-10-22T11:30:48.047Z",
"turn": 1,
"output": {
"content": [
{
"text": "Continuous Integration & Delivery - Semaphore\nSemaphore\nProduct\nFeatures\nContinuous Integration & Pipelines\nDeployments & Automation\nMCP Server 🆕\nMetrics & Observability\nSecurity & Compliance\nDeveloper Toolkit\nTest reports\nMonorepos\nSelf-hosted agents\nMenu Item\nDeployment options\nOpen source\nCloud\nHybrid\nEnterprise\nExplore\nCustomers\nSecurity\nnew\nSemaphore Product News\nRead the update s\nSolutions\nMenu Item\nUse Cases\niOS\nDocker & Kubernetes\nAndroid\nCompare\nBenchmark\nvs Jenkins\nvs GitHub Actions\nvs Travis CI\nvs Bitbucket\nvs GitLab CI\nvs CircleCI\nvs Buildkite\nWHITEPAPER\nHow does Semaphore Open Source work?\nDownload\nPricing\nResources\nMenu Item\nExplore\nCloud Docs\nCommunity Edition Docs\nBlog\nEngineering Talks 🎙️\nResources\nCI/CD Learning tool\nNewsletter\nCommunity\nYoutube ↗\nDiscord ↗\nGitHub ↗\nSemaphore Blog\nExplore\nSemaphore on YouTube\nWatch\nLogin\nLet’s talk\nStart free\nChoose a language\nEN\nDE\nPT\nSemaphore\nProduct\nFeatures\nContinuous Integration & Pipelines\nDeployments & Automation\nMCP Server 🆕\nMetrics & Observability\nSecurity & Compliance\nDeveloper Toolkit\nTest reports\nMonorepos\nSelf-hosted agents\nMenu Item\nDeployment options\nOpen source\nCloud\nHybrid\nEnterprise\nExplore\nCustomers\nSecurity\nSolutions\nMenu Item\nUse Cases\niOS\nDocker & Kubernetes\nAndroid\nCompare\nBenchmark\nvs Jenkins\nvs GitHub Actions\nvs Travis CI\nvs Bitbucket\nvs GitLab CI\nvs CircleCI\nvs Buildkite\nPricing\nResources\nMenu Item\nExplore\nCloud Docs\nCommunity Edition Docs\nBlog\nEngineering Talks 🎙️\nResources\nCI/CD Learning tool\nNewsletter\nCommunity\nYoutube ↗\nDiscord ↗\nGitHub ↗\nGet started\nLogin\nAgent-native CI · open source\nThe CI built for coding agents: fast, affordable, and enterprise-ready.\nTell your agent what you want. It runs your CI.\nStart free\nYou: set up CI for this repo, keep it green\n1.\nYour agent sets it up\n2.\nChecks before you push\n3.\nReview suggestions before merge\n4.\nKeeps CI fast as your code grows\nYou just ship.\nSTAGE BY STAGE\nSee it work\nNo setup. Just ask.\nYou don’t configure anything or learn a new tool. Tell your agent what you want, and it sets up your CI for you — and connects you to Semaphore along the way.\n“set up CI for this repo” → done.\nonboarding\ntestbox\nCheck it before you push.\nMade a change? Your agent runs it first in a quick, production-identical sandbox — so you catch problems before they ever reach CI. Warm the box once, then run check after check in seconds.\nsame machine, image, secrets, cache · ~seconds\nReliable Execution\nStable, reproducible test runs that pass more often \n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-9f4892958e227d82",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
In other words, Semaphore Cloud (SaaS) does indeed exist. Interesting — they're marketing an "MCP Server" and "agent-native CI." Let's check the following:
- Pricing
- Cloud API documentation
- MCP server
The user was asking about the SDK or API. Semaphore Cloud has a public API. Let's take a look at the cloud documentation for API references.
Key blocker: Using the Cloud API requires an API token from a Semaphore organization (requires account creation + OAuth with GitHub/Bitbucket). Without credentials, you can't call the hosted API.
Things to check:
- Pricing page
- Cloud API documentation
Let's find the sitemap of the cloud documentation.
Semaphore Cloud (SaaS) does exist, plus an MCP server. Let me check pricing and the Cloud API surface.
cd /tmp && curl -sL -m 12 https://semaphore.io/pricing | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:4000])"Semaphore CI/CD Platform Pricing - Semaphore
Semaphore CI/CD Platform Pricing - Semaphore Semaphore Product Features Continuous Integration & Pipelines Deployments & Automation MCP Server 🆕 Metrics & Observability Security & Compliance Developer Toolkit Test reports Monorepos Self-hosted agents Menu Item Deployment options Open source Cloud Hybrid Enterprise Explore Customers Security new Semaphore Product News Read the update s Solutions Menu Item Use Cases iOS Docker & Kubernetes Android Compare Benchmark vs Jenkins vs GitHub Actions vs Travis CI vs Bitbucket vs GitLab CI vs CircleCI vs Buildkite WHITEPAPER How does Semaphore Open Source work? Download Pricing Resources Menu Item Explore Cloud Docs Community Edition Docs Blog Engineering Talks 🎙️ Resources CI/CD Learning tool Newsletter Community Youtube ↗ Discord ↗ GitHub ↗ Semaphore Blog Explore Semaphore on YouTube Watch Login Let’s talk Start free Choose a language EN DE PT Semaphore Product Features Continuous Integration & Pipelines Deployments & Automation MCP Server 🆕 Metrics & Observability Security & Compliance Developer Toolkit Test reports Monorepos Self-hosted agents Menu Item Deployment options Open source Cloud Hybrid Enterprise Explore Customers Security Solutions Menu Item Use Cases iOS Docker & Kubernetes Android Compare Benchmark vs Jenkins vs GitHub Actions vs Travis CI vs Bitbucket vs GitLab CI vs CircleCI vs Buildkite Pricing Resources Menu Item Explore Cloud Docs Community Edition Docs Blog Engineering Talks 🎙️ Resources CI/CD Learning tool Newsletter Community Youtube ↗ Discord ↗ GitHub ↗ Get started Login Semaphore got cheaper The Most Cost-Effective CI/CD Platform Semaphore now separates compute pricing from support and success services. Pay only for what you use. Add services when you need them. Ubuntu ARM $0.003 /min 2vCPU $0.006 /min 4vCPU $0.012 /min 8vCPU Ubuntu x64 $0.0075 /min 2vCPU $0.015 /min 4vCPU MacOS $0.09 /min 4vCPU Self-Hosted $0.0025 /min any machine Let’s talk Start free Semaphore CI/CD Competitive Benchmark Avg build time and cost per job across leading CI/CD providers. Read proof Average Build Time (minutes) Semaphore 5m 01s Fastest Buildkite 7m 15s GitHub Actions 9m 44s GitLab 11m 15s CircleCI 13m 18s Cost per Job (USD) Semaphore $0.04 Cheapest GitHub Actions $0.06 CircleCI $0.08 Buildkite $0.09 GitLab $0.11 Concurrency Includes 20 concurrent jobs by default . Higher limits are available on request based on usage. Free Credits Get $15 free credits per month (≈ 2,000 Ubuntu x64 2-vCPU minutes). Credits can be used across available services. Artifacts Free up to 20 GB/month egress and 100 GB/month storage . Additional usage: $0.35/GB egress and $0.07/GB storage. Cache Each project includes 9.6 GB of cache storage to speed up builds and dependency installs. Dedicated Cache Server Optional add-on: $400/month for 10 Gbps bandwidth and 400 GB storage. Private Docker Registry Optional add-on: $400/month with 10 Gbps bandwidth and 400 GB storage. Annual Discounts Discounted pricing is available for annual billing . Contact support for details. Free Support Includes help with account setup, projects, and Git integration . Advanced CI troubleshooting is available through paid support plans . Add-ons Semaphore’s support and success options are fully modular. Add them at any time, scale them up or down, or bundle them for complete coverage. Support Success Engineering Support ✅ 98% customer satisfaction (CSAT) in 2025 ✅ 4.7 / 5 average rating on G2 Basic $50 /month ✔️ Email support ✔️ During business hours* ✔️ No guaranteed response times Best for small teams that can rely on business-hours support. Priority Response $250 /month ✔️ Email + Slack ✔️ Best-effort accelerated responses 24/5 Best for teams that need fast responses during work week. SLA Standard $500 /month ✔️ Email + Slack + Customer Portal ✔️ Guaranteed fast responses 24/5 ✔️ SLA: P1 4h, P2 1 day, P3 3 days, P4 1 week Best for production workloads needing predictable reliability SLA Premium $750 /month ✔️ Email + Slack + C
Recorded tool call
{
"type": "tool_call",
"seq": 40,
"ts": "2026-10-22T11:30:54.471Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://semaphore.io/pricing | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:4000])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a71c791f19c580c4",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 45,
"ts": "2026-10-22T11:30:55.075Z",
"turn": 1,
"output": {
"content": [
{
"text": "Semaphore CI/CD Platform Pricing - Semaphore\nSemaphore\nProduct\nFeatures\nContinuous Integration & Pipelines\nDeployments & Automation\nMCP Server 🆕\nMetrics & Observability\nSecurity & Compliance\nDeveloper Toolkit\nTest reports\nMonorepos\nSelf-hosted agents\nMenu Item\nDeployment options\nOpen source\nCloud\nHybrid\nEnterprise\nExplore\nCustomers\nSecurity\nnew\nSemaphore Product News\nRead the update s\nSolutions\nMenu Item\nUse Cases\niOS\nDocker & Kubernetes\nAndroid\nCompare\nBenchmark\nvs Jenkins\nvs GitHub Actions\nvs Travis CI\nvs Bitbucket\nvs GitLab CI\nvs CircleCI\nvs Buildkite\nWHITEPAPER\nHow does Semaphore Open Source work?\nDownload\nPricing\nResources\nMenu Item\nExplore\nCloud Docs\nCommunity Edition Docs\nBlog\nEngineering Talks 🎙️\nResources\nCI/CD Learning tool\nNewsletter\nCommunity\nYoutube ↗\nDiscord ↗\nGitHub ↗\nSemaphore Blog\nExplore\nSemaphore on YouTube\nWatch\nLogin\nLet’s talk\nStart free\nChoose a language\nEN\nDE\nPT\nSemaphore\nProduct\nFeatures\nContinuous Integration & Pipelines\nDeployments & Automation\nMCP Server 🆕\nMetrics & Observability\nSecurity & Compliance\nDeveloper Toolkit\nTest reports\nMonorepos\nSelf-hosted agents\nMenu Item\nDeployment options\nOpen source\nCloud\nHybrid\nEnterprise\nExplore\nCustomers\nSecurity\nSolutions\nMenu Item\nUse Cases\niOS\nDocker & Kubernetes\nAndroid\nCompare\nBenchmark\nvs Jenkins\nvs GitHub Actions\nvs Travis CI\nvs Bitbucket\nvs GitLab CI\nvs CircleCI\nvs Buildkite\nPricing\nResources\nMenu Item\nExplore\nCloud Docs\nCommunity Edition Docs\nBlog\nEngineering Talks 🎙️\nResources\nCI/CD Learning tool\nNewsletter\nCommunity\nYoutube ↗\nDiscord ↗\nGitHub ↗\nGet started\nLogin\nSemaphore got cheaper\nThe Most Cost-Effective CI/CD Platform\nSemaphore now separates compute pricing from support and success services. Pay only for what you use. Add services when you need them.\nUbuntu ARM\n$0.003\n/min 2vCPU\n$0.006\n/min 4vCPU\n$0.012\n/min 8vCPU\nUbuntu x64\n$0.0075\n/min 2vCPU\n$0.015\n/min 4vCPU\nMacOS\n$0.09\n/min 4vCPU\nSelf-Hosted\n$0.0025\n/min any machine\nLet’s talk\nStart free\nSemaphore CI/CD Competitive Benchmark\nAvg build time and cost per job across leading CI/CD providers. Read proof\nAverage Build Time (minutes)\nSemaphore\n5m 01s Fastest\nBuildkite\n7m 15s\nGitHub Actions\n9m 44s\nGitLab\n11m 15s\nCircleCI\n13m 18s\nCost per Job (USD)\nSemaphore\n$0.04 Cheapest\nGitHub Actions\n$0.06\nCircleCI\n$0.08\nBuildkite\n$0.09\nGitLab\n$0.11\nConcurrency\nIncludes 20 concurrent jobs by default . Higher limits are available on request based on usage.\nFree Credits\nGet $15 free credits per month (≈ 2,000 Ubuntu x64 2-vCPU minutes). Credits can be used across available services.\nArtifacts\nFree up to 20 GB/month egress and 100 GB/month storage . Additional usage: $0.35/GB egress and $0.07/GB storage.\nCache\nEach project includes 9.6 GB of cache storage to speed up builds and dependency installs.\nDedicated Cache Server\nOptional add-on: $400/month for 10 Gbps bandwidth and 400 GB storage.\nPrivate Docker Registry\nOptional add-on: $400/month with 10 Gbps bandwidth and 400 GB storage.\nAnnual Discounts\nDiscounted pricing is available for annual billing . Contact support for details.\nFree Support\nIncludes help with account setup, projects, and Git integration . Advanced CI troubleshooting is available through paid support plans .\nAdd-ons\nSemaphore’s support and success options are fully modular. Add them at any time, scale them up or down, or bundle them for complete coverage.\nSupport\nSuccess\nEngineering\nSupport\n✅ 98% customer satisfaction (CSAT) in 2025 ✅ 4.7 / 5 average rating on G2\nBasic\n$50\n/month\n✔️ Email support ✔️ During business hours* ✔️ No guaranteed response times\nBest for small teams that can rely on business-hours support.\nPriority Response\n$250\n/month\n✔️ Email + Slack ✔️ Best-effort accelerated responses 24/5\nBest for teams that need fast responses during work week.\nSLA Standard\n$500\n/month\n✔️ Email + Slack + Customer Portal ✔️ Guaranteed fast responses 24/5 ✔️ SLA: P1 4h, P2 1 day, P3 3 days, P4 1 week\nBest for production workloads needing predictable reliability\nSLA Premium\n$750\n/month\n✔️ Email + Slack + C\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a71c791f19c580c4",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && curl -sL -m 15 https://docs.semaphore.io/sitemap.xml | grep -oE "<loc>[^<]+</loc>" | sed 's/<[^>]*>//g' | grep -viE "/CE|/EE" | head -60
https://docs.semaphore.io/search
https://docs.semaphore.io/search https://docs.semaphore.io/ https://docs.semaphore.io/getting-started/about-semaphore https://docs.semaphore.io/getting-started/changelog https://docs.semaphore.io/getting-started/experiments https://docs.semaphore.io/getting-started/faq https://docs.semaphore.io/getting-started/features https://docs.semaphore.io/getting-started/install https://docs.semaphore.io/getting-started/migration-overview https://docs.semaphore.io/getting-started/migration/bitbucket https://docs.semaphore.io/getting-started/migration/circle https://docs.semaphore.io/getting-started/migration/github-actions https://docs.semaphore.io/getting-started/migration/jenkins https://docs.semaphore.io/getting-started/migration/travis https://docs.semaphore.io/getting-started/quickstart https://docs.semaphore.io/getting-started/tour/build-project https://docs.semaphore.io/getting-started/tour/deploy-project https://docs.semaphore.io/getting-started/tour/hello-world https://docs.semaphore.io/getting-started/tour/sign-up https://docs.semaphore.io/reference/agent-aws-stack https://docs.semaphore.io/reference/agent-yaml https://docs.semaphore.io/reference/api https://docs.semaphore.io/reference/audit-events https://docs.semaphore.io/reference/conditions-dsl https://docs.semaphore.io/reference/dashboard-yaml https://docs.semaphore.io/reference/deployment-target-yaml https://docs.semaphore.io/reference/env-vars https://docs.semaphore.io/reference/jobs-yaml https://docs.semaphore.io/reference/machine-types https://docs.semaphore.io/reference/notifications-yaml https://docs.semaphore.io/reference/openid https://docs.semaphore.io/reference/os-apple https://docs.semaphore.io/reference/os-ubuntu https://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2004-image https://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2204-image https://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2404-arm-image https://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2404-image https://docs.semaphore.io/reference/pipeline-yaml https://docs.semaphore.io/reference/project-yaml https://docs.semaphore.io/reference/resources-yaml https://docs.semaphore.io/reference/secret-yaml https://docs.semaphore.io/reference/self-hosted-config https://docs.semaphore.io/reference/sem-ai-cli https://docs.semaphore.io/reference/semaphore-cli https://docs.semaphore.io/reference/toolbox https://docs.semaphore.io/using-semaphore/ai/copilot-agent-cloud https://docs.semaphore.io/using-semaphore/ai/mcp-server https://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples https://docs.semaphore.io/using-semaphore/ai/self-healing-ci https://docs.semaphore.io/using-semaphore/ai/sem-ai https://docs.semaphore.io/using-semaphore/artifacts https://docs.semaphore.io/using-semaphore/billing https://docs.semaphore.io/using-semaphore/cache https://docs.semaphore.io/using-semaphore/connect-bitbucket https://docs.semaphore.io/using-semaphore/connect-github https://docs.semaphore.io/using-semaphore/connect-github-oauth https://docs.semaphore.io/using-semaphore/connect-gitlab https://docs.semaphore.io/using-semaphore/containers https://docs.semaphore.io/using-semaphore/containers/container-registry https://docs.semaphore.io/using-semaphore/containers/docker
Recorded tool call
{
"type": "tool_call",
"seq": 42,
"ts": "2026-10-22T11:30:54.471Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 15 https://docs.semaphore.io/sitemap.xml | grep -oE \"<loc>[^<]+</loc>\" | sed 's/<[^>]*>//g' | grep -viE \"/CE|/EE\" | head -60"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-baeab6980caa0e79",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 44,
"ts": "2026-10-22T11:30:54.767Z",
"turn": 1,
"output": {
"content": [
{
"text": "https://docs.semaphore.io/search\nhttps://docs.semaphore.io/\nhttps://docs.semaphore.io/getting-started/about-semaphore\nhttps://docs.semaphore.io/getting-started/changelog\nhttps://docs.semaphore.io/getting-started/experiments\nhttps://docs.semaphore.io/getting-started/faq\nhttps://docs.semaphore.io/getting-started/features\nhttps://docs.semaphore.io/getting-started/install\nhttps://docs.semaphore.io/getting-started/migration-overview\nhttps://docs.semaphore.io/getting-started/migration/bitbucket\nhttps://docs.semaphore.io/getting-started/migration/circle\nhttps://docs.semaphore.io/getting-started/migration/github-actions\nhttps://docs.semaphore.io/getting-started/migration/jenkins\nhttps://docs.semaphore.io/getting-started/migration/travis\nhttps://docs.semaphore.io/getting-started/quickstart\nhttps://docs.semaphore.io/getting-started/tour/build-project\nhttps://docs.semaphore.io/getting-started/tour/deploy-project\nhttps://docs.semaphore.io/getting-started/tour/hello-world\nhttps://docs.semaphore.io/getting-started/tour/sign-up\nhttps://docs.semaphore.io/reference/agent-aws-stack\nhttps://docs.semaphore.io/reference/agent-yaml\nhttps://docs.semaphore.io/reference/api\nhttps://docs.semaphore.io/reference/audit-events\nhttps://docs.semaphore.io/reference/conditions-dsl\nhttps://docs.semaphore.io/reference/dashboard-yaml\nhttps://docs.semaphore.io/reference/deployment-target-yaml\nhttps://docs.semaphore.io/reference/env-vars\nhttps://docs.semaphore.io/reference/jobs-yaml\nhttps://docs.semaphore.io/reference/machine-types\nhttps://docs.semaphore.io/reference/notifications-yaml\nhttps://docs.semaphore.io/reference/openid\nhttps://docs.semaphore.io/reference/os-apple\nhttps://docs.semaphore.io/reference/os-ubuntu\nhttps://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2004-image\nhttps://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2204-image\nhttps://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2404-arm-image\nhttps://docs.semaphore.io/reference/os-ubuntu-images/ubuntu-2404-image\nhttps://docs.semaphore.io/reference/pipeline-yaml\nhttps://docs.semaphore.io/reference/project-yaml\nhttps://docs.semaphore.io/reference/resources-yaml\nhttps://docs.semaphore.io/reference/secret-yaml\nhttps://docs.semaphore.io/reference/self-hosted-config\nhttps://docs.semaphore.io/reference/sem-ai-cli\nhttps://docs.semaphore.io/reference/semaphore-cli\nhttps://docs.semaphore.io/reference/toolbox\nhttps://docs.semaphore.io/using-semaphore/ai/copilot-agent-cloud\nhttps://docs.semaphore.io/using-semaphore/ai/mcp-server\nhttps://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples\nhttps://docs.semaphore.io/using-semaphore/ai/self-healing-ci\nhttps://docs.semaphore.io/using-semaphore/ai/sem-ai\nhttps://docs.semaphore.io/using-semaphore/artifacts\nhttps://docs.semaphore.io/using-semaphore/billing\nhttps://docs.semaphore.io/using-semaphore/cache\nhttps://docs.semaphore.io/using-semaphore/connect-bitbucket\nhttps://docs.semaphore.io/using-semaphore/connect-github\nhttps://docs.semaphore.io/using-semaphore/connect-github-oauth\nhttps://docs.semaphore.io/using-semaphore/connect-gitlab\nhttps://docs.semaphore.io/using-semaphore/containers\nhttps://docs.semaphore.io/using-semaphore/containers/container-registry\nhttps://docs.semaphore.io/using-semaphore/containers/docker\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-baeab6980caa0e79",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Alright — there's an MCP server documentation page and a Cloud API reference. Let me read through them:
- https://docs.semaphore.io/reference/api
- https://docs.semaphore.io/using-semaphore/ai/mcp-server
- https://docs.semaphore.io/getting-started/quickstart
Let me fetch these.
Found Cloud API + MCP server docs. Let me read them.
cd /tmp && for u in https://docs.semaphore.io/reference/api https://docs.semaphore.io/using-semaphore/ai/mcp-server; do echo "######## $u"; curl -sL -m 12 "$u" | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:5000])"; done######## https://docs.semaphore.io/reference/api
######## https://docs.semaphore.io/reference/api
Semaphore API | Semaphore
Skip to main content Version: Cloud (SaaS) On this page Semaphore API
This document describes all the resources that make up Semaphore API version v1alpha . If you have any problems or requests please contact support .
The root of the API can be found here: https://<organization-url>.semaphoreci.com/api/v1alpha .
Overview
Every API request and response must satisfy the following constraints:
All requests must use HTTPS.
All data is sent and received as JSON.
Blank fields are included as null instead of being omitted.
Timestamps are in different formats due to the historical circumstances of how these public APIs appeared. In the next release of the API, they will be standardized. Currently, there are the following formats:
Unixtime Epoch time: "create_time": "1571083003"
Unixtime Epoch time with nanoseconds: "created_at": {"seconds": 1571063401, "nanos": 559492000}
Custom format: YYYY-MM-DD HH:MM:SS.ffffffZ , e.g., "2019-10-14 12:11:47.824128Z"
All API requests must set the User-Agent to SemaphoreCI v2.0 Client .
Authentication
All API requests require authentication. To authenticate, you need an API Token. You can find your token by visiting your account settings .
Your API Token must be sent as an HTTP header in all requests, as shown below:
curl -H "Authorization: Token {api_token}" "https://<organization-url>.semaphoreci.com/api/v1alpha/{resource_name}"
Errors
You can receive several errors as a response to an API request
Error Message Meaning HTTP/1.1 401 Unauthorized Failure to authenticate HTTP/1.1 404 Not Found Resource doesn't exist or is not visible to user
Pagination
Every request that that returns more than 30 items will be paginated. To avoid this, you should form calls with link header values instead of constructing your own URLs.
A link header includes information about pagination, as shown below:
link: <http://<organization-url>.semaphoreci.com/api/v1alpha/?PAGE_PARAMS>; rel="first", <http://<organization-url>.semaphoreci.com/api/orgs?PAGE_PARAMS>; rel="next"
The possible rel values are:
next - The link for the next page of results.
prev - The link for the previous page of results.
first - The link for the first page of results.
Stability
There are two types of changes:
Compatible and emergency changes may be made with no advanced notice
Disruptive changes will not occur without advanced notice. In the event that a disruptive change is needed, a new major version will be developed
Emergency changes happen without notice. We will, however, try to notify users as best as possible
Compatible changes
Small in scope and unlikely to break or change semantics of existing methods.
Adding nested resources, methods, and attributes.
Change of documentation.
Change of undocumented behavior.
Disruptive changes
May have a larger impact and effort will be made to provide migration paths as needed.
Changing semantics of existing methods.
Removing resources, methods, and attributes.
Emergency change
May have larger impact, but is unavoidable due to legal compliance, security vulnerabilities, or violation of specification.
Organizations
Create an organization
Creates a new organization owned by the authenticated user.
Unlike every other endpoint, this one is account-scoped, not
organization-scoped : it is served on the me. host (there is no organization
subdomain yet) and is authenticated with your personal account API token. The
user resolved from the token becomes the owner of the new organization.
Request:
POST https://me.semaphoreci.com/api/v1alpha/organizations
Parameters:
username ( required ) - the organization's URL handle, used as its subdomain ( <username>.semaphoreci.com ). Must be unique and not already taken.
name ( optional ) - human-readable display name. Defaults to username when omitted.
Response:
HTTP status: 200 { "organization_id": "2b4e4c0f-c760-4f47-9dc4-b3e84f993f63", "name": "My Organization", "username": "my-org" }
A username that is missing or already taken returns a 4xx error describing
the problem.
Example:
curl -X POST --location "https://me.semaphoreci.com/api/v1alpha/organizations" \ -H "Authorization: Token {api_token}" \ -H "Content-Type: application/json" \ -d $'{ "username": "my-org", "name": "My Organization" }'
Workflows
Run workflow
Request:
POST <organization-url>.semaphoreci.com/api/v1alpha/plumber-workflows
Parameters:
project_id ( required ) - ID of a project.
reference ( required ) - git reference for the desired branch, tag, or pull request--e.g. refs/heads/master , refs/tags/v1.0 , or refs/pull/123 .
commit_sha ( optional ) - Commit sha of the desired commit.
pipeline_file ( optional ) - The path within the repository to the YAML file that contains the pipeline definition. The default value is .semaphore/semaphore.yml .
parameters : (key-values) specify parameter values that will be available in all jobs of the initial pipeline and can be used in the same way as the parameters from the parameterized promotions .
Res
######## https://docs.semaphore.io/using-semaphore/ai/mcp-server
MCP Server | Semaphore
Skip to main content Version: Cloud (SaaS) On this page MCP Server
This page explains how Semaphore's MCP Server works, how to configure your clients/agents, and what capabilities are available.
Request access to this feature If you want to try the MCP Server, contact support@semaphore.io so we can enable this feature in your organization at no cost.
Overview
MCP servers (Model Context Protocol servers) connect AI models to external data and tools through a standardized interface. They provide structured, real-time context-like data from APIs so models can retrieve accurate information and perform actions securely and consistently.
You can connect your favorite AI agent with Semaphore using the official MCP Server. This allows the AI agent or IDE to access the activity in your organization using a conversational interface.
With MCP, you can now ask your AI agent why your build failed and to provide a fix.
MCP Server Capabilities
Semaphore's MCP Server provides access to your Semaphore organization via the following tools and resources:
echo : Returns the provided message verbatim (handy for smoke tests)
organizations_list : Lists organizations that the user can access
projects_list : List projects that belong to a specific organization
projects_search : Search projects inside an organization by project name, repository URL, or description
workflows_search : Search recent workflows for a project (most recent first)
pipelines_list : List pipelines associated with a workflow (most recent first)
pipeline_jobs : List jobs belonging to a specific pipeline
jobs_describe : Describes a job, surfacing agent details, and lifecycle timestamps
jobs_logs : Fetches job logs. For cloud jobs, it streams loghub events. For self-hosted jobs, returns a URL where logs can be fetched
get_test_results : Returns a signed URL (gzip JSON) for JUnit-style test reports . Response includes URL, path, compression, and fetch instructions.
doc_tools : Allows AI to access a copy of the full Semaphore documentation site
The following tools require MCP Server write permissions in your organization. Contact support@semaphore.io to enable this feature.
workflow_run : Schedules a new workflow to be executed
workflow_rerun : Reruns an existing workflow with the original parameters
See example prompts to see a bit of what's possible.
Configure your AI Agent or IDE
Semaphore MCP supports two authentication methods:
OAuth 2.1 (recommended): clients that support MCP OAuth can sign in via browser and grant access without manually managing API tokens.
API token: personal or service account token via Authorization: Bearer <token> .
Both methods use the same endpoint: https://mcp.semaphoreci.com/mcp .
Connect with OAuth (recommended)
If your MCP client supports OAuth for remote MCP servers:
Add https://mcp.semaphoreci.com/mcp as the MCP server URL.
Start the connection flow in your client.
Complete browser sign-in and grant access to the MCP client.
OAuth-capable clients use discovery and dynamic client registration automatically.
Connect with an API token
If your client does not support OAuth yet, use an API token instead.
You can obtain an API token in two ways:
Personal API Token : if you don't know your personal API token, you can reset it and obtain a new one.
Service Account : create a service account with Member role, add it to projects that you want to access via the MCP server, and use its API token.
If you have problems connecting to the MCP Server, see troubleshooting .
API token setup examples
The client configurations below use API token authentication and continue to work for clients that do not support OAuth yet.
Claude Code
Export an environment variable with your API token
export SEMAPHORE_API_TOKEN=my-token
Add the MCP Server to Claude Code
claude mcp add semaphore https://mcp.semaphoreci.com/mcp \ --scope user --transport http \ --header "Authorization: Bearer $SEMAPHORE_API_TOKEN"
If you have a session open, restart Claude Code
Open a terminal and navigate to your repository
Start Claude Code and run the following command to update CLAUDE.md and add Semaphore project details to the repository. This step is optional but recommended to speed up AI tasks and reduce token usage
Initialize Claude configuration for MCP /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization
note If you get an error stating strategy requires thinking to be enabled . This error is caused by known bug in Claude Code. The workaround is to add ultrathink to the previous command: Initialize Claude configuration for MCP /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization ultrathink
OpenAI's Codex
Open $HOME/.codex/config.toml and add the following lines to the config file
Semaphore MCP Configuration for Codex [mcp_servers.semaphore] url = "https://mcp.semaphoreci.com/mcp" bearer_token_env_var = "SEMAPHORE_API_TOKEN" startup_timeout_sec = 30 tool_timeout_sec = 300
Environment variable nam
Recorded tool call
{
"type": "tool_call",
"seq": 49,
"ts": "2026-10-22T11:31:00.970Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && for u in https://docs.semaphore.io/reference/api https://docs.semaphore.io/using-semaphore/ai/mcp-server; do echo \"######## $u\"; curl -sL -m 12 \"$u\" | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:5000])\"; done"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8d3ab6c76b46a0ce",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 52,
"ts": "2026-10-22T11:31:02.291Z",
"turn": 1,
"output": {
"content": [
{
"text": "######## https://docs.semaphore.io/reference/api\nSemaphore API | Semaphore\nSkip to main content Version: Cloud (SaaS) On this page Semaphore API\nThis document describes all the resources that make up Semaphore API version v1alpha . If you have any problems or requests please contact support .\nThe root of the API can be found here: https://<organization-url>.semaphoreci.com/api/v1alpha .\nOverview \nEvery API request and response must satisfy the following constraints:\nAll requests must use HTTPS.\nAll data is sent and received as JSON.\nBlank fields are included as null instead of being omitted.\nTimestamps are in different formats due to the historical circumstances of how these public APIs appeared. In the next release of the API, they will be standardized. Currently, there are the following formats:\nUnixtime Epoch time: \"create_time\": \"1571083003\"\nUnixtime Epoch time with nanoseconds: \"created_at\": {\"seconds\": 1571063401, \"nanos\": 559492000}\nCustom format: YYYY-MM-DD HH:MM:SS.ffffffZ , e.g., \"2019-10-14 12:11:47.824128Z\"\nAll API requests must set the User-Agent to SemaphoreCI v2.0 Client .\nAuthentication \nAll API requests require authentication. To authenticate, you need an API Token. You can find your token by visiting your account settings .\nYour API Token must be sent as an HTTP header in all requests, as shown below:\ncurl -H \"Authorization: Token {api_token}\" \"https://<organization-url>.semaphoreci.com/api/v1alpha/{resource_name}\"\nErrors \nYou can receive several errors as a response to an API request\nError Message Meaning HTTP/1.1 401 Unauthorized Failure to authenticate HTTP/1.1 404 Not Found Resource doesn't exist or is not visible to user\nPagination \nEvery request that that returns more than 30 items will be paginated. To avoid this, you should form calls with link header values instead of constructing your own URLs.\nA link header includes information about pagination, as shown below:\nlink: <http://<organization-url>.semaphoreci.com/api/v1alpha/?PAGE_PARAMS>; rel=\"first\", <http://<organization-url>.semaphoreci.com/api/orgs?PAGE_PARAMS>; rel=\"next\"\nThe possible rel values are:\nnext - The link for the next page of results.\nprev - The link for the previous page of results.\nfirst - The link for the first page of results.\nStability \nThere are two types of changes:\nCompatible and emergency changes may be made with no advanced notice\nDisruptive changes will not occur without advanced notice. In the event that a disruptive change is needed, a new major version will be developed\nEmergency changes happen without notice. We will, however, try to notify users as best as possible\nCompatible changes \nSmall in scope and unlikely to break or change semantics of existing methods.\nAdding nested resources, methods, and attributes.\nChange of documentation.\nChange of undocumented behavior.\nDisruptive changes \nMay have a larger impact and effort will be made to provide migration paths as needed.\nChanging semantics of existing methods.\nRemoving resources, methods, and attributes.\nEmergency change \nMay have larger impact, but is unavoidable due to legal compliance, security vulnerabilities, or violation of specification.\nOrganizations \nCreate an organization \nCreates a new organization owned by the authenticated user.\nUnlike every other endpoint, this one is account-scoped, not\norganization-scoped : it is served on the me. host (there is no organization\nsubdomain yet) and is authenticated with your personal account API token. The\nuser resolved from the token becomes the owner of the new organization.\nRequest:\nPOST https://me.semaphoreci.com/api/v1alpha/organizations\nParameters:\nusername ( required ) - the organization's URL handle, used as its subdomain ( <username>.semaphoreci.com ). Must be unique and not already taken.\nname ( optional ) - human-readable display name. Defaults to username when omitted.\nResponse:\nHTTP status: 200 { \"organization_id\": \"2b4e4c0f-c760-4f47-9dc4-b3e84f993f63\", \"name\": \"My Organization\", \"username\": \"my-org\" }\nA username that is missing or already taken returns a 4xx error describing\nthe problem.\nExample:\ncurl -X POST --location \"https://me.semaphoreci.com/api/v1alpha/organizations\" \\ -H \"Authorization: Token {api_token}\" \\ -H \"Content-Type: application/json\" \\ -d $'{ \"username\": \"my-org\", \"name\": \"My Organization\" }'\nWorkflows \nRun workflow \nRequest:\nPOST <organization-url>.semaphoreci.com/api/v1alpha/plumber-workflows\nParameters:\nproject_id ( required ) - ID of a project.\nreference ( required ) - git reference for the desired branch, tag, or pull request--e.g. refs/heads/master , refs/tags/v1.0 , or refs/pull/123 .\ncommit_sha ( optional ) - Commit sha of the desired commit.\npipeline_file ( optional ) - The path within the repository to the YAML file that contains the pipeline definition. The default value is .semaphore/semaphore.yml .\nparameters : (key-values) specify parameter values that will be available in all jobs of the initial pipeline and can be used in the same way as the parameters from the parameterized promotions .\nRes\n######## https://docs.semaphore.io/using-semaphore/ai/mcp-server\nMCP Server | Semaphore\nSkip to main content Version: Cloud (SaaS) On this page MCP Server\nThis page explains how Semaphore's MCP Server works, how to configure your clients/agents, and what capabilities are available.\nRequest access to this feature If you want to try the MCP Server, contact support@semaphore.io so we can enable this feature in your organization at no cost.\nOverview \nMCP servers (Model Context Protocol servers) connect AI models to external data and tools through a standardized interface. They provide structured, real-time context-like data from APIs so models can retrieve accurate information and perform actions securely and consistently.\nYou can connect your favorite AI agent with Semaphore using the official MCP Server. This allows the AI agent or IDE to access the activity in your organization using a conversational interface.\nWith MCP, you can now ask your AI agent why your build failed and to provide a fix.\nMCP Server Capabilities \nSemaphore's MCP Server provides access to your Semaphore organization via the following tools and resources:\necho : Returns the provided message verbatim (handy for smoke tests)\norganizations_list : Lists organizations that the user can access\nprojects_list : List projects that belong to a specific organization\nprojects_search : Search projects inside an organization by project name, repository URL, or description\nworkflows_search : Search recent workflows for a project (most recent first)\npipelines_list : List pipelines associated with a workflow (most recent first)\npipeline_jobs : List jobs belonging to a specific pipeline\njobs_describe : Describes a job, surfacing agent details, and lifecycle timestamps\njobs_logs : Fetches job logs. For cloud jobs, it streams loghub events. For self-hosted jobs, returns a URL where logs can be fetched\nget_test_results : Returns a signed URL (gzip JSON) for JUnit-style test reports . Response includes URL, path, compression, and fetch instructions.\ndoc_tools : Allows AI to access a copy of the full Semaphore documentation site\nThe following tools require MCP Server write permissions in your organization. Contact support@semaphore.io to enable this feature.\nworkflow_run : Schedules a new workflow to be executed\nworkflow_rerun : Reruns an existing workflow with the original parameters\nSee example prompts to see a bit of what's possible.\nConfigure your AI Agent or IDE \nSemaphore MCP supports two authentication methods:\nOAuth 2.1 (recommended): clients that support MCP OAuth can sign in via browser and grant access without manually managing API tokens.\nAPI token: personal or service account token via Authorization: Bearer <token> .\nBoth methods use the same endpoint: https://mcp.semaphoreci.com/mcp .\nConnect with OAuth (recommended) \nIf your MCP client supports OAuth for remote MCP servers:\nAdd https://mcp.semaphoreci.com/mcp as the MCP server URL.\nStart the connection flow in your client.\nComplete browser sign-in and grant access to the MCP client.\nOAuth-capable clients use discovery and dynamic client registration automatically.\nConnect with an API token \nIf your client does not support OAuth yet, use an API token instead.\nYou can obtain an API token in two ways:\nPersonal API Token : if you don't know your personal API token, you can reset it and obtain a new one.\nService Account : create a service account with Member role, add it to projects that you want to access via the MCP server, and use its API token.\nIf you have problems connecting to the MCP Server, see troubleshooting .\nAPI token setup examples \nThe client configurations below use API token authentication and continue to work for clients that do not support OAuth yet.\nClaude Code \nExport an environment variable with your API token\nexport SEMAPHORE_API_TOKEN=my-token\nAdd the MCP Server to Claude Code\nclaude mcp add semaphore https://mcp.semaphoreci.com/mcp \\ --scope user --transport http \\ --header \"Authorization: Bearer $SEMAPHORE_API_TOKEN\"\nIf you have a session open, restart Claude Code\nOpen a terminal and navigate to your repository\nStart Claude Code and run the following command to update CLAUDE.md and add Semaphore project details to the repository. This step is optional but recommended to speed up AI tasks and reduce token usage\nInitialize Claude configuration for MCP /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization\nnote If you get an error stating strategy requires thinking to be enabled . This error is caused by known bug in Claude Code. The workaround is to add ultrathink to the previous command: Initialize Claude configuration for MCP /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization ultrathink\nOpenAI's Codex \nOpen $HOME/.codex/config.toml and add the following lines to the config file\nSemaphore MCP Configuration for Codex [mcp_servers.semaphore] url = \"https://mcp.semaphoreci.com/mcp\" bearer_token_env_var = \"SEMAPHORE_API_TOKEN\" startup_timeout_sec = 30 tool_timeout_sec = 300\nEnvironment variable nam\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8d3ab6c76b46a0ce",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && for u in https://docs.semaphore.io/getting-started/quickstart https://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples; do echo "######## $u"; curl -sL -m 12 "$u" | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:4500])"; done######## https://docs.semaphore.io/getting-started/quickstart
######## https://docs.semaphore.io/getting-started/quickstart
Quickstart | Semaphore
Skip to main content Version: Cloud (SaaS) Quickstart
Welcome 👋 Thank you for trying out Semaphore.
This 20-minute tutorial will give you 80% of the time needed to use Semaphore effectively. No strings attached. No credit card is required. This is our promise.
What will you learn?
Sign up : sign up and create an organization
First project : a "Hello, World!" of CI, invite your teammates to your project
Build your project : a real example of CI with build and test steps
Deploy your project : almost everything you need to release or deploy your apps
Let's dive in!
######## https://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples
MCP Usage Examples | Semaphore
Skip to main content Version: Cloud (SaaS) On this page MCP Usage Examples
Semaphore’s MCP Server unlocks practical, low-friction AI assistance for CI/CD. With an AI agent already connected, you can ask natural questions and get immediate insights from your Semaphore projects.
Below, we outline key use cases and examples for developers, each accompanied by example prompts, an explanation of what happens under the hood, and relevant technical details. These scenarios assume that you have MCP access enabled and are authenticated either with OAuth or an API token.
Organization and Project Context Discovery
Quickly identify which Semaphore organizations and projects your AI agent can access. This is useful for setting context before deeper queries. The agent can also cache organization and project IDs for later use, saving you time.
Example prompts:
List organizations you have access to
Find the current project’s organization ID and project ID and save them in AGENTS.md
Claude Users Claude users can automate project and organization ID discovery with the following command. See MCP Server configuration for more information: Initialize project's CLAUDE.md /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization
Under the hood, the MCP server will determine the user based on the provided API token and fetch organizations or projects that are accessible to that user. The response returns organization or project metadata (IDs, names, etc.). The AI agent then presents a list of organization or project names or IDs, and can optionally write these identifiers to a file for future reference.
Related API calls GET https://<your-org>.semaphoreci.com/api/v1alpha/projects Authorization: Bearer <YOUR_API_TOKEN>
For instance, the MCP response might return a JSON array of projects in your organization.
Example API response [ {"name": "hello-semaphore", "id": "proj-1234-..."}, {"name": "dockerizing-ruby", "id": "proj-5678-..."}, {"name": "golang-mathapp", "id": "proj-9012-..."} ]
The agent uses this data to confirm connectivity and context. With IDs known, subsequent commands (like triggering or inspecting pipelines) don’t require you to manually look up IDs, reducing friction.
Pipeline creation and edition
With the MCP Server, AI agents can create Semaphore pipelines from scratch using real project context and official documentation—without relying on guesswork or generic CI templates. This is especially useful when onboarding new projects, migrating CI systems, or iterating quickly on pipeline design.
In this scenario, the agent combines repository inspection with access to Semaphore’s documentation (via doc_tools``) to generate a valid semaphore.yml` tailored to the project.
Example prompts:
Create a Semaphore pipeline for this repository that runs linting and unit tests
Generate a semaphore.yml file for this project and cache dependencies
To create a pipeline, the agent typically performs the following steps:
Repository inspection: the agent analyzes the repository structure (language, package manager, test framework, etc.) to understand what the pipeline should do
Documentation lookup via doc_tools: using the doc_tools resource, the agent reads Semaphore’s official pipeline YAML documentation
Pipeline generation: based on the repository context and documentation guidance, the agent generates a complete semaphore.yml file aligned with Semaphore’s syntax and best practices
Validation through execution: after the YAML is committed and pushed, Semaphore validates and runs the pipeline. Because the agent relied on authoritative docs, the pipeline typically passes validation and executes successfully on the first run.
Pipeline Overview and Understanding
Get a high-level summary of what a given Semaphore pipeline does. This helps onboard to a new project or review pipeline structure without digging through YAML. The AI agent can describe the pipeline’s stages, jobs, and purpose in plain language.
The MCP server exposes pipeline queries that the agent uses to gather pipeline details. Typically, the agent will:
Use workflows_search to find the most recent workflow for the project (often filtered to the main branch or a specific workflow).
Call pipelines_list with that workflow’s ID to get the pipeline(s) in that run (usually one pipeline unless promotions are involved).
Invoke pipeline_jobs for the pipeline to list all jobs (and their statuses) defined in that pipeline.
Example prompts:
De
Recorded tool call
{
"type": "tool_call",
"seq": 50,
"ts": "2026-10-22T11:31:00.970Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && for u in https://docs.semaphore.io/getting-started/quickstart https://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples; do echo \"######## $u\"; curl -sL -m 12 \"$u\" | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:4500])\"; done"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-99284f7b59396d8b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 51,
"ts": "2026-10-22T11:31:01.879Z",
"turn": 1,
"output": {
"content": [
{
"text": "######## https://docs.semaphore.io/getting-started/quickstart\nQuickstart | Semaphore\nSkip to main content Version: Cloud (SaaS) Quickstart\nWelcome 👋 Thank you for trying out Semaphore.\nThis 20-minute tutorial will give you 80% of the time needed to use Semaphore effectively. No strings attached. No credit card is required. This is our promise.\nWhat will you learn?\nSign up : sign up and create an organization\nFirst project : a \"Hello, World!\" of CI, invite your teammates to your project\nBuild your project : a real example of CI with build and test steps\nDeploy your project : almost everything you need to release or deploy your apps\nLet's dive in!\n######## https://docs.semaphore.io/using-semaphore/ai/mcp-usage-examples\nMCP Usage Examples | Semaphore\nSkip to main content Version: Cloud (SaaS) On this page MCP Usage Examples\nSemaphore’s MCP Server unlocks practical, low-friction AI assistance for CI/CD. With an AI agent already connected, you can ask natural questions and get immediate insights from your Semaphore projects.\nBelow, we outline key use cases and examples for developers, each accompanied by example prompts, an explanation of what happens under the hood, and relevant technical details. These scenarios assume that you have MCP access enabled and are authenticated either with OAuth or an API token.\nOrganization and Project Context Discovery \nQuickly identify which Semaphore organizations and projects your AI agent can access. This is useful for setting context before deeper queries. The agent can also cache organization and project IDs for later use, saving you time.\nExample prompts:\nList organizations you have access to\nFind the current project’s organization ID and project ID and save them in AGENTS.md\nClaude Users Claude users can automate project and organization ID discovery with the following command. See MCP Server configuration for more information: Initialize project's CLAUDE.md /semaphore:mcp_setup (MCP) your-project-name your-semaphore-organization\nUnder the hood, the MCP server will determine the user based on the provided API token and fetch organizations or projects that are accessible to that user. The response returns organization or project metadata (IDs, names, etc.). The AI agent then presents a list of organization or project names or IDs, and can optionally write these identifiers to a file for future reference.\nRelated API calls GET https://<your-org>.semaphoreci.com/api/v1alpha/projects Authorization: Bearer <YOUR_API_TOKEN>\nFor instance, the MCP response might return a JSON array of projects in your organization.\nExample API response [ {\"name\": \"hello-semaphore\", \"id\": \"proj-1234-...\"}, {\"name\": \"dockerizing-ruby\", \"id\": \"proj-5678-...\"}, {\"name\": \"golang-mathapp\", \"id\": \"proj-9012-...\"} ]\nThe agent uses this data to confirm connectivity and context. With IDs known, subsequent commands (like triggering or inspecting pipelines) don’t require you to manually look up IDs, reducing friction.\nPipeline creation and edition \nWith the MCP Server, AI agents can create Semaphore pipelines from scratch using real project context and official documentation—without relying on guesswork or generic CI templates. This is especially useful when onboarding new projects, migrating CI systems, or iterating quickly on pipeline design.\nIn this scenario, the agent combines repository inspection with access to Semaphore’s documentation (via doc_tools``) to generate a valid semaphore.yml` tailored to the project.\nExample prompts:\nCreate a Semaphore pipeline for this repository that runs linting and unit tests\nGenerate a semaphore.yml file for this project and cache dependencies\nTo create a pipeline, the agent typically performs the following steps:\nRepository inspection: the agent analyzes the repository structure (language, package manager, test framework, etc.) to understand what the pipeline should do\nDocumentation lookup via doc_tools: using the doc_tools resource, the agent reads Semaphore’s official pipeline YAML documentation\nPipeline generation: based on the repository context and documentation guidance, the agent generates a complete semaphore.yml file aligned with Semaphore’s syntax and best practices\nValidation through execution: after the YAML is committed and pushed, Semaphore validates and runs the pipeline. Because the agent relied on authoritative docs, the pipeline typically passes validation and executes successfully on the first run.\nPipeline Overview and Understanding \nGet a high-level summary of what a given Semaphore pipeline does. This helps onboard to a new project or review pipeline structure without digging through YAML. The AI agent can describe the pipeline’s stages, jobs, and purpose in plain language.\nThe MCP server exposes pipeline queries that the agent uses to gather pipeline details. Typically, the agent will:\nUse workflows_search to find the most recent workflow for the project (often filtered to the main branch or a specific workflow).\nCall pipelines_list with that workflow’s ID to get the pipeline(s) in that run (usually one pipeline unless promotions are involved).\nInvoke pipeline_jobs for the pipeline to list all jobs (and their statuses) defined in that pipeline.\nExample prompts:\nDe\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-99284f7b59396d8b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Excellent. In other words, Semaphore Cloud has the following:
- A public REST API (v1alpha). Requires an API token from account settings. Notably, there is an API endpoint to create organizations:
POST https://me.semaphoreci.com/api/v1alpha/organizations— account-scoped, authenticated with a personal account API token. - An MCP server at https://mcp.semaphoreci.com/mcp (requires requesting access via support@semaphore.io).
The blocker: To obtain a personal API token, you need a Semaphore Cloud account. To create an account, you need to sign up via the Web UI (GitHub/Bitbucket OAuth or email). I don't have credentials in this environment. Let me check whether the API is reachable without authentication, and whether there's a signup endpoint.
Things to test:
- Accessing
https://me.semaphoreci.com/api/v1alpha/organizationswithout a token → expect 401. - Check whether there's a signup API.
Also, let me check whether the MCP endpoint is reachable, and whether a token is required.
Let's run these checks.
Semaphore Cloud has a public REST API and an MCP server. Let me probe what's reachable without credentials.
cd /tmp && echo "== orgs create, no auth"; curl -s -m 10 -w "\nHTTP:%{http_code}\n" -X POST "https://me.semaphoreci.com/api/v1alpha/organizations" -H "Authorization: Token nope" -H "Content-Type: application/json" -d '{"username":"probe-test"}' | head -5
echo "== MCP endpoint, no auth"; curl -s -m 10 -w "\nHTTP:%{http_code}\n" -X POST "https://mcp.semaphoreci.com/mcp" -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}' | head -10== orgs create, no auth
== orgs create, no auth
HTTP:401
== MCP endpoint, no auth
{"error":"invalid_token","error_description":"Missing bearer token"}
HTTP:401
Recorded tool call
{
"type": "tool_call",
"seq": 56,
"ts": "2026-10-22T11:31:09.692Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && echo \"== orgs create, no auth\"; curl -s -m 10 -w \"\\nHTTP:%{http_code}\\n\" -X POST \"https://me.semaphoreci.com/api/v1alpha/organizations\" -H \"Authorization: Token nope\" -H \"Content-Type: application/json\" -d '{\"username\":\"probe-test\"}' | head -5\necho \"== MCP endpoint, no auth\"; curl -s -m 10 -w \"\\nHTTP:%{http_code}\\n\" -X POST \"https://mcp.semaphoreci.com/mcp\" -H \"Content-Type: application/json\" -H \"Accept: application/json, text/event-stream\" -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"probe\",\"version\":\"1\"}}}' | head -10"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a44f6c57678806e4",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 62,
"ts": "2026-10-22T11:31:10.065Z",
"turn": 1,
"output": {
"content": [
{
"text": "== orgs create, no auth\n\nHTTP:401\n== MCP endpoint, no auth\n{\"error\":\"invalid_token\",\"error_description\":\"Missing bearer token\"}\nHTTP:401\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a44f6c57678806e4",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && echo "== sem CLI availability"; curl -s -m 10 -o /dev/null -w "storage.googleapis.com/sem-cli-releases HTTP:%{http_code}\n" "https://storage.googleapis.com/sem-cli-releases/get.sh"; echo "== sem-ai docs"; curl -sL -m 12 https://docs.semaphore.io/reference/sem-ai-cli | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:2200])"== sem CLI availability
== sem CLI availability storage.googleapis.com/sem-cli-releases HTTP:200 == sem-ai docs sem-ai Command Line | Semaphore Skip to main content Version: Cloud (SaaS) On this page sem-ai Command Line sem-ai is an agent-first CLI for Semaphore CI/CD. It's designed for AI agents and automation, with structured JSON output, self-discovery, and composable commands. It can also run as an MCP server for direct tool integration. Overview sem-ai provides full control over your Semaphore CI/CD from the terminal or from AI agents. Every command returns structured JSON by default. Key capabilities: Diagnose CI failures with parsed test results Pipeline topology analysis (critical path, blast radius) Test intelligence (flaky detection, test summaries) Testbox: run commands in real CI environments before pushing MCP server mode for native AI agent integration Installation curl -fsSL https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh | sh Installs the latest release for macOS / Linux on amd64 / arm64. The binary lands at $HOME/.local/bin/sem-ai (or $HOME/.semaphore-ai/bin/sem-ai if that's not on your $PATH ). Re-run the same command to upgrade. From source Requires Go 1.25+. git clone https://github.com/semaphoreio/sem-ai.git cd sem-ai make install Setup sem-ai connect Connect to your Semaphore organization. You need an API token . sem-ai connect <organization>.semaphoreci.com <API_TOKEN> For example: sem-ai connect myorg.semaphoreci.com NeUFkim46BCdpqCAyWXN The token is stored in ~/.sem.yaml , shared with the Semaphore CLI . If you already have sem configured, sem-ai uses the same credentials automatically. sem-ai signin Sign in without pasting a token. signin runs a device flow: it shows a one-time code and a verification URL, opening a browser when one is available. sem-ai signin Enter the code in the browser, sign in — or create an account there, if you do not have one — and approve. The terminal finishes on its own. signup and login are aliases for the same flow. The resulting token is stored in ~/.sem.yaml the same way connect stores one. Defaults to Semaphore Cloud. For another deployment, pass the host, and --id-host when the CLI-auth endpoints live elsewhere: sem-ai signin my-onprem.example.com --id-host id.my-onprem.example.co
Recorded tool call
{
"type": "tool_call",
"seq": 59,
"ts": "2026-10-22T11:31:09.692Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && echo \"== sem CLI availability\"; curl -s -m 10 -o /dev/null -w \"storage.googleapis.com/sem-cli-releases HTTP:%{http_code}\\n\" \"https://storage.googleapis.com/sem-cli-releases/get.sh\"; echo \"== sem-ai docs\"; curl -sL -m 12 https://docs.semaphore.io/reference/sem-ai-cli | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:2200])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-85bd71641b13ed7f",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 63,
"ts": "2026-10-22T11:31:10.727Z",
"turn": 1,
"output": {
"content": [
{
"text": "== sem CLI availability\nstorage.googleapis.com/sem-cli-releases HTTP:200\n== sem-ai docs\nsem-ai Command Line | Semaphore\nSkip to main content Version: Cloud (SaaS) On this page sem-ai Command Line\nsem-ai is an agent-first CLI for Semaphore CI/CD. It's designed for AI agents and automation, with structured JSON output, self-discovery, and composable commands. It can also run as an MCP server for direct tool integration.\nOverview \nsem-ai provides full control over your Semaphore CI/CD from the terminal or from AI agents. Every command returns structured JSON by default. Key capabilities:\nDiagnose CI failures with parsed test results\nPipeline topology analysis (critical path, blast radius)\nTest intelligence (flaky detection, test summaries)\nTestbox: run commands in real CI environments before pushing\nMCP server mode for native AI agent integration\nInstallation \ncurl -fsSL https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh | sh\nInstalls the latest release for macOS / Linux on amd64 / arm64. The binary lands at $HOME/.local/bin/sem-ai (or $HOME/.semaphore-ai/bin/sem-ai if that's not on your $PATH ). Re-run the same command to upgrade.\nFrom source \nRequires Go 1.25+.\ngit clone https://github.com/semaphoreio/sem-ai.git cd sem-ai make install\nSetup \nsem-ai connect \nConnect to your Semaphore organization. You need an API token .\nsem-ai connect <organization>.semaphoreci.com <API_TOKEN>\nFor example:\nsem-ai connect myorg.semaphoreci.com NeUFkim46BCdpqCAyWXN\nThe token is stored in ~/.sem.yaml , shared with the Semaphore CLI . If you already have sem configured, sem-ai uses the same credentials automatically.\nsem-ai signin \nSign in without pasting a token. signin runs a device flow: it shows a one-time code and a verification URL, opening a browser when one is available.\nsem-ai signin\nEnter the code in the browser, sign in — or create an account there, if you do not have one — and approve. The terminal finishes on its own. signup and login are aliases for the same flow. The resulting token is stored in ~/.sem.yaml the same way connect stores one.\nDefaults to Semaphore Cloud. For another deployment, pass the host, and --id-host when the CLI-auth endpoints live elsewhere:\nsem-ai signin my-onprem.example.com --id-host id.my-onprem.example.co\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-85bd71641b13ed7f",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
In other words, the API and MCP both require tokens. The sem-ai CLI has a device flow sign-in (sem-ai signin), which displays a one-time code and a verification URL — you can also create an account on the browser. This requires a human with a browser. This is a human-in-the-loop step, not a local service stack or long-running command.
So this is actually a path I can act on: install sem-ai (curl | sh — quick install), then run sem-ai signin. This displays a code for the user to approve in the browser. This is short-running and interactive — not a long-running stack.
However, this needs the user to click. Let me decide: the user said "let me know if you get stuck." The real blocker is the credentials. But the device flow is a legitimate lightweight path: install the CLI, initiate signin, hand the code to the user.
Hmm — but is the signin command interactive/blocking? It displays the code and waits for approval. It'll probably block until approved. This is a "long-running command" in the sense that it waits. But I can run it in the background, capture the code, and then poll the status.
Let me install sem-ai and check. This is lightweight — it's not a service stack, just a single static binary.
First let me check the installation script (don't blindly pipe curl into sh).
Let me proceed pragmatically: download install.sh, verify its contents, then install.
Let's do that. Also, let me further check the sem-ai docs for things like "testbox".
First, let me fetch and verify install.sh.
Both API and MCP need a token, but sem-ai has a device-flow signin. Let me inspect the installer before running it.
cd /tmp && curl -fsSL -m 20 https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh -o sem-ai-install.sh && wc -l sem-ai-install.sh && head -60 sem-ai-install.sh
134 sem-ai-install.sh
134 sem-ai-install.sh
#!/bin/sh
set -eu
GITHUB_OWNER=semaphoreio
GITHUB_REPO=sem-ai
API_URL="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest"
DL_BASE="https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download"
say() { printf '%s\n' "$*"; }
warn() { printf '%s\n' "$*" >&2; }
TMP=$(mktemp -d 2>/dev/null || mktemp -d -t sem-ai-install)
trap 'rm -rf "$TMP"' EXIT INT TERM
# Step 1 — platform detect
os=$(uname -s | tr '[:upper:]' '[:lower:]')
arch=$(uname -m)
case "$os" in
darwin|linux) ;;
*)
warn "unsupported platform: ${os}/${arch}"
exit 1
;;
esac
case "$arch" in
x86_64|amd64) arch=amd64 ;;
aarch64|arm64) arch=arm64 ;;
*)
warn "unsupported platform: ${os}/${arch}"
exit 1
;;
esac
# Step 2 — resolve latest release tag
tag=$(curl -fsSL "$API_URL" \
| grep '"tag_name"' \
| sed -E 's/.*"tag_name" *: *"([^"]+)".*/\1/')
if [ -z "$tag" ]; then
warn "failed to resolve latest release (network error or rate limit)"
exit 1
fi
# GoReleaser strips the leading v from {{ .Version }}
ver=${tag#v}
# Step 3 — install destination detect
path_hint=0
case ":${PATH}:" in
*":${HOME}/.local/bin:"*)
dest_dir="${HOME}/.local/bin"
;;
*)
dest_dir="${HOME}/.semaphore-ai/bin"
path_hint=1
;;
esac
Recorded tool call
{
"type": "tool_call",
"seq": 67,
"ts": "2026-10-22T11:31:18.149Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -fsSL -m 20 https://raw.githubusercontent.com/semaphoreio/sem-ai/main/install.sh -o sem-ai-install.sh && wc -l sem-ai-install.sh && head -60 sem-ai-install.sh"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8941377377d8bac9",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 71,
"ts": "2026-10-22T11:31:18.571Z",
"turn": 1,
"output": {
"content": [
{
"text": "134 sem-ai-install.sh\n#!/bin/sh\nset -eu\n\nGITHUB_OWNER=semaphoreio\nGITHUB_REPO=sem-ai\nAPI_URL=\"https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest\"\nDL_BASE=\"https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download\"\n\nsay() { printf '%s\\n' \"$*\"; }\nwarn() { printf '%s\\n' \"$*\" >&2; }\n\nTMP=$(mktemp -d 2>/dev/null || mktemp -d -t sem-ai-install)\ntrap 'rm -rf \"$TMP\"' EXIT INT TERM\n\n# Step 1 — platform detect\nos=$(uname -s | tr '[:upper:]' '[:lower:]')\narch=$(uname -m)\n\ncase \"$os\" in\n darwin|linux) ;;\n *)\n warn \"unsupported platform: ${os}/${arch}\"\n exit 1\n ;;\nesac\n\ncase \"$arch\" in\n x86_64|amd64) arch=amd64 ;;\n aarch64|arm64) arch=arm64 ;;\n *)\n warn \"unsupported platform: ${os}/${arch}\"\n exit 1\n ;;\nesac\n\n# Step 2 — resolve latest release tag\ntag=$(curl -fsSL \"$API_URL\" \\\n | grep '\"tag_name\"' \\\n | sed -E 's/.*\"tag_name\" *: *\"([^\"]+)\".*/\\1/')\n\nif [ -z \"$tag\" ]; then\n warn \"failed to resolve latest release (network error or rate limit)\"\n exit 1\nfi\n\n# GoReleaser strips the leading v from {{ .Version }}\nver=${tag#v}\n\n# Step 3 — install destination detect\npath_hint=0\n\ncase \":${PATH}:\" in\n *\":${HOME}/.local/bin:\"*)\n dest_dir=\"${HOME}/.local/bin\"\n ;;\n *)\n dest_dir=\"${HOME}/.semaphore-ai/bin\"\n path_hint=1\n ;;\nesac\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8941377377d8bac9",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/sem-ai-cli | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[2200:9000])"m
m Flag Description --headless or --device Never try to open a browser; print the code and URL --browser Open the verification page even when the terminal looks non-interactive --id-host Host serving the CLI-auth endpoints (defaults to <host> ) --org Also create a first organization with this name (new accounts only) --org-host Host for the --org organization's context An account has a single API token. If yours already has one, approving the sign-in resets it , after an explicit confirmation in the browser. The previous token then stops working everywhere it is used — CI secrets, scripts, other machines. To authenticate with a token you already hold, use connect instead. sem-ai context Each organization you connect to is stored in ~/.sem.yaml as a named context . List configured organizations: sem-ai context list Show the organization this invocation resolves to — the pin, if one is set, otherwise the active context: sem-ai context show Under a pin, show and list report different things by design: show names what this invocation will actually talk to, while list reports the file, marking the stored default active and the pinned entry pinned . Change the default organization used by unpinned commands: sem-ai context switch myorg_semaphoreci_com context switch rewrites the shared active-context key in ~/.sem.yaml . Unpinned invocations that set no credential environment variables read that key — including invocations of the legacy sem CLI — so switching affects sessions other than your own. It is meant for a person changing their own default, not for scoping a command, a script, or an agent. To target an organization for one command without changing what anyone else sees, pin it instead. Pinning an organization --context selects a named context for one invocation, without writing to the config file. It takes a context name , not a host: connect names each context after its host with the dots replaced by underscores, so myorg.semaphoreci.com is stored as myorg_semaphoreci_com . Run sem-ai context list to see the exact names. sem-ai --context myorg_semaphoreci_com project list SEM_CONTEXT does the same for a whole shell session: export SEM_CONTEXT=myorg_semaphoreci_com sem-ai project list A name that is not in ~/.sem.yaml fails immediately and lists the contexts that are, instead of quietly falling back to another organization. Pinning isolates the read path: a pinned invocation neither reads nor writes active-context , so pinned sessions cannot flip each other's organization. It does not make every operation concurrency-safe. connect , signin , and context switch still write ~/.sem.yaml ; each write replaces the whole file, and two overlapping writes are last-writer-wins, so a context one session adds can disappear when another lands. Pinning also covers sem-ai only — the legacy sem CLI reads neither --context nor SEM_CONTEXT and still follows active-context . When several agents run at once, sequence the commands that write, or give each agent its own HOME . signin , context switch , and context list ignore the pin entirely, so a pin naming a context that does not exist yet cannot block the command that is about to create it. connect ignores it when resolving credentials, but refuses outright when the pin resolves to a different host than its <host> argument, since that names two organizations at once. connect names the context it creates after its host, so re-pin to that name once onboarding finishes. Credential resolution order sem-ai resolves a host and a token from these sources, in order: Priority Source Scope Behavior 1 --context <name> A single invocation Replaces everything below 2 SEM_CONTEXT=<name> A shell session Replaces everything below 3 SEMAPHORE_HOST and/or SEMAPHORE_API_TOKEN The process environment Merged — each variable overrides only its own field 4 active-context in ~/.sem.yaml Shared by every session on the machine The starting point when nothing above applies The two halves of that table behave differently, and the difference matters. A context named by --context or SEM_CONTEXT supplies the host and the token together and fully replaces the sources below it, so its credentials are never mixed with the environment variables. Without a selector, rows 3 and 4 merge field by field . SEMAPHORE_HOST and SEMAPHORE_API_TOKEN are independent, so setting only one leaves the other coming from the active context. Exporting SEMAPHORE_API_TOKEN on its own — which the hosted MCP server setup instructs you to do — means every later sem-ai command in that shell sends that token to whichever host active-context currently names. Set both variables together, or pin a context, whenever the host and the token have to belong to the same organization. General syntax sem-ai <command> [subcommand] [flags] Global flags: Flag Description --context Run against a named context from ~/.sem.yaml without changing the active one. See pinning an organization --format or -f Output format: json (default), table , yaml --verbose or -v Show HTTP requests for debugging --examples Show usage examples for any command --help or -h Help for any command Self-discovery sem-ai discover Returns a structured map of every command, its flags, and examples. Designed for AI agents to self-orient without documentation. sem-ai discover sem-ai discover --format table sem-ai <command> --examples Every command supports --examples to show usage examples: sem-ai diagnose --examples sem-ai pipeline promote --examples Working with projects sem-ai project list List all projects in the organization: sem-ai project list sem-ai project show Show project details: sem-ai project show <project-name> sem-ai project create Create a project from a git repository. With no flags it uses the origin remote of the current directory and derives the name from the repo URL, then bootstraps an initial .semaphore/semaphore.yml (unless --skip-yaml ). If a project with the same name already exists it returns the existing one, unless --fail-on-exists is set. sem-ai project create sem-ai project create --repo-url git@github.com:org/repo.git sem-ai project create --name my-project --github-integration github_app Flag Default Description --repo-url origin of cwd git repository URL --name derived from repo URL project name --github-integration github_token GitHub integration: github_token or github_app --remote origin git remote to detect when --repo-url is not set --skip-yaml false don't generate .semaphore/semaphore.yml in cwd --fail-on-exists false exit non-zero if a project with the same name already exists sem-ai project update Update project settings: sem-ai project update <project-name> --visibility public sem-ai project update <project-name> --description "My app" sem-ai project delete Delete a project: s
Recorded tool call
{
"type": "tool_call",
"seq": 69,
"ts": "2026-10-22T11:31:18.149Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/sem-ai-cli | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[2200:9000])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b7c711f9295b6a45",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 72,
"ts": "2026-10-22T11:31:18.571Z",
"turn": 1,
"output": {
"content": [
{
"text": "m\nFlag Description --headless or --device Never try to open a browser; print the code and URL --browser Open the verification page even when the terminal looks non-interactive --id-host Host serving the CLI-auth endpoints (defaults to <host> ) --org Also create a first organization with this name (new accounts only) --org-host Host for the --org organization's context\nAn account has a single API token. If yours already has one, approving the sign-in resets it , after an explicit confirmation in the browser. The previous token then stops working everywhere it is used — CI secrets, scripts, other machines. To authenticate with a token you already hold, use connect instead.\nsem-ai context \nEach organization you connect to is stored in ~/.sem.yaml as a named context .\nList configured organizations:\nsem-ai context list\nShow the organization this invocation resolves to — the pin, if one is set, otherwise the active context:\nsem-ai context show\nUnder a pin, show and list report different things by design: show names what this invocation will actually talk to, while list reports the file, marking the stored default active and the pinned entry pinned .\nChange the default organization used by unpinned commands:\nsem-ai context switch myorg_semaphoreci_com\ncontext switch rewrites the shared active-context key in ~/.sem.yaml . Unpinned invocations that set no credential environment variables read that key — including invocations of the legacy sem CLI — so switching affects sessions other than your own. It is meant for a person changing their own default, not for scoping a command, a script, or an agent. To target an organization for one command without changing what anyone else sees, pin it instead.\nPinning an organization \n--context selects a named context for one invocation, without writing to the config file. It takes a context name , not a host: connect names each context after its host with the dots replaced by underscores, so myorg.semaphoreci.com is stored as myorg_semaphoreci_com . Run sem-ai context list to see the exact names.\nsem-ai --context myorg_semaphoreci_com project list\nSEM_CONTEXT does the same for a whole shell session:\nexport SEM_CONTEXT=myorg_semaphoreci_com sem-ai project list\nA name that is not in ~/.sem.yaml fails immediately and lists the contexts that are, instead of quietly falling back to another organization.\nPinning isolates the read path: a pinned invocation neither reads nor writes active-context , so pinned sessions cannot flip each other's organization. It does not make every operation concurrency-safe. connect , signin , and context switch still write ~/.sem.yaml ; each write replaces the whole file, and two overlapping writes are last-writer-wins, so a context one session adds can disappear when another lands. Pinning also covers sem-ai only — the legacy sem CLI reads neither --context nor SEM_CONTEXT and still follows active-context . When several agents run at once, sequence the commands that write, or give each agent its own HOME .\nsignin , context switch , and context list ignore the pin entirely, so a pin naming a context that does not exist yet cannot block the command that is about to create it. connect ignores it when resolving credentials, but refuses outright when the pin resolves to a different host than its <host> argument, since that names two organizations at once. connect names the context it creates after its host, so re-pin to that name once onboarding finishes.\nCredential resolution order \nsem-ai resolves a host and a token from these sources, in order:\nPriority Source Scope Behavior 1 --context <name> A single invocation Replaces everything below 2 SEM_CONTEXT=<name> A shell session Replaces everything below 3 SEMAPHORE_HOST and/or SEMAPHORE_API_TOKEN The process environment Merged — each variable overrides only its own field 4 active-context in ~/.sem.yaml Shared by every session on the machine The starting point when nothing above applies\nThe two halves of that table behave differently, and the difference matters.\nA context named by --context or SEM_CONTEXT supplies the host and the token together and fully replaces the sources below it, so its credentials are never mixed with the environment variables.\nWithout a selector, rows 3 and 4 merge field by field . SEMAPHORE_HOST and SEMAPHORE_API_TOKEN are independent, so setting only one leaves the other coming from the active context. Exporting SEMAPHORE_API_TOKEN on its own — which the hosted MCP server setup instructs you to do — means every later sem-ai command in that shell sends that token to whichever host active-context currently names. Set both variables together, or pin a context, whenever the host and the token have to belong to the same organization.\nGeneral syntax \nsem-ai <command> [subcommand] [flags]\nGlobal flags:\nFlag Description --context Run against a named context from ~/.sem.yaml without changing the active one. See pinning an organization --format or -f Output format: json (default), table , yaml --verbose or -v Show HTTP requests for debugging --examples Show usage examples for any command --help or -h Help for any command\nSelf-discovery \nsem-ai discover \nReturns a structured map of every command, its flags, and examples. Designed for AI agents to self-orient without documentation.\nsem-ai discover sem-ai discover --format table\nsem-ai <command> --examples \nEvery command supports --examples to show usage examples:\nsem-ai diagnose --examples sem-ai pipeline promote --examples\nWorking with projects \nsem-ai project list \nList all projects in the organization:\nsem-ai project list\nsem-ai project show \nShow project details:\nsem-ai project show <project-name>\nsem-ai project create \nCreate a project from a git repository. With no flags it uses the origin remote of the current directory and derives the name from the repo URL, then bootstraps an initial .semaphore/semaphore.yml (unless --skip-yaml ). If a project with the same name already exists it returns the existing one, unless --fail-on-exists is set.\nsem-ai project create sem-ai project create --repo-url git@github.com:org/repo.git sem-ai project create --name my-project --github-integration github_app\nFlag Default Description --repo-url origin of cwd git repository URL --name derived from repo URL project name --github-integration github_token GitHub integration: github_token or github_app --remote origin git remote to detect when --repo-url is not set --skip-yaml false don't generate .semaphore/semaphore.yml in cwd --fail-on-exists false exit non-zero if a project with the same name already exists\nsem-ai project update \nUpdate project settings:\nsem-ai project update <project-name> --visibility public sem-ai project update <project-name> --description \"My app\"\nsem-ai project delete \nDelete a project:\ns\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b7c711f9295b6a45",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The installer is clean and simple. Let's install sem-ai. Then check sem-ai discover. This works without authentication (self-discovery). Next, try sem-ai signin --headless. This will print a code and URL — that will be a handoff to the user.
Let's install.
Note: signin with the --org flag creates an initial organization — perfect for a new account.
Let's install and run discover.
Note: sem-ai signin --headless blocks until approved. Let's run it in the background with nohup and poll the code.
Let's install first.
Installer is clean. Installing sem-ai:
cd /tmp && sh sem-ai-install.sh 2>&1 | tail -15; echo "---"; ls -la ~/.local/bin/sem-ai ~/.semaphore-ai/bin/sem-ai 2>/dev/null; export PATH="$HOME/.local/bin:$HOME/.semaphore-ai/bin:$PATH"; sem-ai --version 2>&1 | head -3
claude plugin marketplace add semaphoreio/sem-ai # first time only
claude plugin marketplace add semaphoreio/sem-ai # first time only claude plugin install sem-ai@semaphoreio # first time claude plugin marketplace update semaphoreio # already installed — refresh catalog Codex CLI — install (or refresh) from the shell: codex plugin marketplace add semaphoreio/sem-ai # first time only codex plugin add sem-ai@semaphoreio # first time codex plugin marketplace upgrade semaphoreio # already installed — refresh catalog Then run /sem-ai:init in a repo to set up Semaphore CI/CD for it. Prefer Claude Desktop? Install the one-click MCPB extension instead of the CLI — download sem-ai_0.5.0_linux_amd64.mcpb from the release and open it: https://github.com/semaphoreio/sem-ai/releases/download/v0.5.0/sem-ai_0.5.0_linux_amd64.mcpb It bundles this same server; enter your org host + API token in the install dialog. --- -rwxr-xr-x 1 1001 1001 10240184 Oct 6 16:08 /sandbox/.semaphore-ai/bin/sem-ai Error: unknown flag: --version
Recorded tool call
{
"type": "tool_call",
"seq": 76,
"ts": "2026-10-22T11:31:21.534Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && sh sem-ai-install.sh 2>&1 | tail -15; echo \"---\"; ls -la ~/.local/bin/sem-ai ~/.semaphore-ai/bin/sem-ai 2>/dev/null; export PATH=\"$HOME/.local/bin:$HOME/.semaphore-ai/bin:$PATH\"; sem-ai --version 2>&1 | head -3"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-985470b07bbd6759",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 77,
"ts": "2026-10-22T11:31:24.746Z",
"turn": 1,
"output": {
"content": [
{
"text": " claude plugin marketplace add semaphoreio/sem-ai # first time only\n claude plugin install sem-ai@semaphoreio # first time\n claude plugin marketplace update semaphoreio # already installed — refresh catalog\n\nCodex CLI — install (or refresh) from the shell:\n codex plugin marketplace add semaphoreio/sem-ai # first time only\n codex plugin add sem-ai@semaphoreio # first time\n codex plugin marketplace upgrade semaphoreio # already installed — refresh catalog\n\nThen run /sem-ai:init in a repo to set up Semaphore CI/CD for it.\n\nPrefer Claude Desktop? Install the one-click MCPB extension instead of the CLI —\ndownload sem-ai_0.5.0_linux_amd64.mcpb from the release and open it:\n https://github.com/semaphoreio/sem-ai/releases/download/v0.5.0/sem-ai_0.5.0_linux_amd64.mcpb\nIt bundles this same server; enter your org host + API token in the install dialog.\n---\n-rwxr-xr-x 1 1001 1001 10240184 Oct 6 16:08 /sandbox/.semaphore-ai/bin/sem-ai\nError: unknown flag: --version\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-985470b07bbd6759",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Already installed at /sandbox/.semaphore-ai/bin/sem-ai. Let's run sem-ai discover and check the functionality without authentication.
Installed. Let me self-discover its capabilities (no auth needed):
export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && sem-ai discover 2>&1 | python3 -c "
import sys,json
d=json.load(sys.stdin)
def walk(o,p=''):
if isinstance(o,dict):
for k,v in o.items():
if k in ('summary','description','use'): print(p,k,'=',str(v)[:110])
else: walk(v,p+'.'+k if p else k)
elif isinstance(o,list):
for i in o: walk(i,p)
print(json.dumps(list(d.keys())))
walk(d)" 2>&1 | head -70["commands", "tips"]
["commands", "tips"] commands description = Self-hosted agent management commands description = Delete a self-hosted agent type commands description = List agents for a given type commands description = Show agent type details commands description = List self-hosted agent types commands description = Pipeline and workflow analytics over time commands description = Deploy frequency and promotion stats commands description = Pipeline duration trends (avg, p50, p95) commands description = Block-level failure rates commands description = Queue wait time analysis commands description = All-in-one analytics overview for a project commands description = Week-over-week trends for duration, pass rate, queue time, and failure reasons commands description = Artifact operations: list and download build artifacts commands description = Download an artifact via signed URL commands description = List artifacts for a job, workflow, or project commands description = Show which blocks were affected by failures in a pipeline commands description = Generate the autocompletion script for the specified shell commands description = Generate the autocompletion script for bash commands description = Generate the autocompletion script for fish commands description = Generate the autocompletion script for powershell commands description = Generate the autocompletion script for zsh commands description = Connect to a Semaphore organization commands description = Manage Semaphore contexts (orgs/auth) commands description = List all configured contexts commands description = Show active context details commands description = Switch active context commands description = Show the longest dependency chain (bottleneck) in a pipeline commands description = Deployment target operations: visibility and management commands description = Activate a deployment target commands description = Create a deployment target commands description = Deactivate a deployment target commands description = Delete a deployment target commands description = Show deployment history for a target commands description = Show deployment target details commands description = List deployment targets for a project commands description = Update a deployment target (PATCH) commands description = Failure diagnosis: one command, full root cause commands description = Introspect all registered commands: capability map for agents commands description = History-backed flaky-test signals (Superjerry) for a project commands description = List individual disruption occurrences for a flaky test commands description = Show the real failure for a flaky test by fetching its disruption job log commands description = List flaky tests for a project commands description = Show details for a single flaky test (per-context pass rate, p95, disruptions) commands description = Project-level flaky/disruption count time series (day, count) commands description = Group management — organize members into teams commands description = Create a group commands description = Delete a group commands description = List groups in the organization commands description = Update a group — add or remove members commands description = Project health summary: recent pass rate, failures, deployment status commands description = Help about any command commands description = Pipeline insights (Velocity): performance, reliability, frequency commands description = Pipeline run frequency commands description = Pipeline duration metrics (mean/median/p95 seconds) commands description = Pipeline pass/fail counts commands description = Job operations commands description = List jobs by state commands description = Fetch job logs commands description = Show job details commands description = Stop a running job commands description = Log out of a Semaphore context, clearing its stored API token commands description = Start MCP (Model Context Protocol) stdio server commands description = Notification management: Slack, email, webhook commands description = Create a notification rule from a YAML/JSON file commands description = Delete a notification rule commands description = List notification rules commands description = Show notification rule details commands description = Open workflow/pipeline in browser commands description = Organization management — members, roles
Recorded tool call
{
"type": "tool_call",
"seq": 81,
"ts": "2026-10-22T11:31:27.370Z",
"name": "bash",
"turn": 1,
"input": {
"command": "export PATH=\"$HOME/.semaphore-ai/bin:$PATH\"; cd /tmp && sem-ai discover 2>&1 | python3 -c \"\nimport sys,json\nd=json.load(sys.stdin)\ndef walk(o,p=''):\n if isinstance(o,dict):\n for k,v in o.items():\n if k in ('summary','description','use'): print(p,k,'=',str(v)[:110])\n else: walk(v,p+'.'+k if p else k)\n elif isinstance(o,list):\n for i in o: walk(i,p)\nprint(json.dumps(list(d.keys())))\nwalk(d)\" 2>&1 | head -70"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b384b778df07ead2",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 82,
"ts": "2026-10-22T11:31:27.370Z",
"turn": 1,
"output": {
"content": [
{
"text": "[\"commands\", \"tips\"]\ncommands description = Self-hosted agent management\ncommands description = Delete a self-hosted agent type\ncommands description = List agents for a given type\ncommands description = Show agent type details\ncommands description = List self-hosted agent types\ncommands description = Pipeline and workflow analytics over time\ncommands description = Deploy frequency and promotion stats\ncommands description = Pipeline duration trends (avg, p50, p95)\ncommands description = Block-level failure rates\ncommands description = Queue wait time analysis\ncommands description = All-in-one analytics overview for a project\ncommands description = Week-over-week trends for duration, pass rate, queue time, and failure reasons\ncommands description = Artifact operations: list and download build artifacts\ncommands description = Download an artifact via signed URL\ncommands description = List artifacts for a job, workflow, or project\ncommands description = Show which blocks were affected by failures in a pipeline\ncommands description = Generate the autocompletion script for the specified shell\ncommands description = Generate the autocompletion script for bash\ncommands description = Generate the autocompletion script for fish\ncommands description = Generate the autocompletion script for powershell\ncommands description = Generate the autocompletion script for zsh\ncommands description = Connect to a Semaphore organization\ncommands description = Manage Semaphore contexts (orgs/auth)\ncommands description = List all configured contexts\ncommands description = Show active context details\ncommands description = Switch active context\ncommands description = Show the longest dependency chain (bottleneck) in a pipeline\ncommands description = Deployment target operations: visibility and management\ncommands description = Activate a deployment target\ncommands description = Create a deployment target\ncommands description = Deactivate a deployment target\ncommands description = Delete a deployment target\ncommands description = Show deployment history for a target\ncommands description = Show deployment target details\ncommands description = List deployment targets for a project\ncommands description = Update a deployment target (PATCH)\ncommands description = Failure diagnosis: one command, full root cause\ncommands description = Introspect all registered commands: capability map for agents\ncommands description = History-backed flaky-test signals (Superjerry) for a project\ncommands description = List individual disruption occurrences for a flaky test\ncommands description = Show the real failure for a flaky test by fetching its disruption job log\ncommands description = List flaky tests for a project\ncommands description = Show details for a single flaky test (per-context pass rate, p95, disruptions)\ncommands description = Project-level flaky/disruption count time series (day, count)\ncommands description = Group management — organize members into teams\ncommands description = Create a group\ncommands description = Delete a group\ncommands description = List groups in the organization\ncommands description = Update a group — add or remove members\ncommands description = Project health summary: recent pass rate, failures, deployment status\ncommands description = Help about any command\ncommands description = Pipeline insights (Velocity): performance, reliability, frequency\ncommands description = Pipeline run frequency\ncommands description = Pipeline duration metrics (mean/median/p95 seconds)\ncommands description = Pipeline pass/fail counts\ncommands description = Job operations\ncommands description = List jobs by state\ncommands description = Fetch job logs\ncommands description = Show job details\ncommands description = Stop a running job\ncommands description = Log out of a Semaphore context, clearing its stored API token\ncommands description = Start MCP (Model Context Protocol) stdio server\ncommands description = Notification management: Slack, email, webhook\ncommands description = Create a notification rule from a YAML/JSON file\ncommands description = Delete a notification rule\ncommands description = List notification rules\ncommands description = Show notification rule details\ncommands description = Open workflow/pipeline in browser\ncommands description = Organization management — members, roles\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b384b778df07ead2",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
This is a rich, agent-first CLI. Everything requires authentication. Next, let's attempt the device flow sign-in that outputs the code and URL — that's the handoff to the user.
Let's run sem-ai signin --headless in the background and grab the code.
Let's do it. It'll block while waiting for approval. I'll run it with nohup and poll.
Also, should I include --org for creating the initial organization? That requires a name. Should I hold off and ask the user — no, better to just grab the code first. The org can be created later, or via the API after obtaining the token.
Actually, --org only applies to new accounts, and creating the org right away saves a step. But we need an org name. Let's start without --org, grab the code, and hand it off to the user. After that, once the token is obtained, we can use the API endpoint to create the org.
Let's run signin --headless in the background.
This is a genuinely agent-first CLI. Everything needs a token. Let me start the device-flow signin to get a handoff code:
export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && rm -f signin.log; nohup sem-ai signin --headless > signin.log 2>&1 & echo "pid=$!"; sleep 12; echo "--- log:"; cat signin.log
pid=4580
pid=4580
--- log:
Heads up: if your account already has an API token, completing this sign-in
lets you reset it (you confirm in the browser first). A reset token stops
working everywhere it is used: CI secrets, scripts, other machines.
First, copy your one-time code:
RKRK-DJBP
Open https://id.semaphoreci.com/device in a browser and enter the code.
Sign in there (or create your account), then approve. Waiting...
Recorded tool call
{
"type": "tool_call",
"seq": 86,
"ts": "2026-10-22T11:31:30.824Z",
"name": "bash",
"turn": 1,
"input": {
"command": "export PATH=\"$HOME/.semaphore-ai/bin:$PATH\"; cd /tmp && rm -f signin.log; nohup sem-ai signin --headless > signin.log 2>&1 &\necho \"pid=$!\"; sleep 12; echo \"--- log:\"; cat signin.log"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-bf9645dc88fad62b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 87,
"ts": "2026-10-22T11:31:43.550Z",
"turn": 1,
"output": {
"content": [
{
"text": "pid=4580\n--- log:\nHeads up: if your account already has an API token, completing this sign-in\nlets you reset it (you confirm in the browser first). A reset token stops\nworking everywhere it is used: CI secrets, scripts, other machines.\n\nFirst, copy your one-time code:\n\n RKRK-DJBP\n\nOpen https://id.semaphoreci.com/device in a browser and enter the code.\nSign in there (or create your account), then approve. Waiting...\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-bf9645dc88fad62b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Got the code. Now I need the user to approve it in a browser. That's the handoff. The process is waiting in the background.
Note: The signin process is running in the background. Should leave it running so the user can approve. But watch the timeout — device flow codes usually expire in about 5–15 minutes. Should tell the user to act quickly.
In the meantime, could I prepare the sample repo content (.semaphore/semaphore.yml) so it's ready to use right away once authenticated? But without knowing the project, it'd be too generic. Actually, the repo is empty (only .gitignore). Let me check what the repo is — there's a .git directory. Let me check the remote and contents.
Actually, ls -la only showed .git and .gitignore. Let me check git log/remote.
While waiting, let me prep the example scaffolding. This is useful and lightweight work.
Let me check the git repo.
Got a device code. Let me check the repo state and prep the example scaffolding while it waits:
cd /sandbox/repo && git log --oneline -5 2>&1 | head; echo "--- remotes"; git remote -v; echo "--- branches"; git branch -a; echo "--- gitignore"; cat .gitignore
9ebf5fb Initial workspace
9ebf5fb Initial workspace --- remotes --- branches * main --- gitignore # Keep generated and vendored files out of source diffs. # Logs / OS cruft *.log .DS_Store # JavaScript / TypeScript node_modules/ .next/ dist/ build/ coverage/ # Python .venv/ venv/ __pycache__/ *.py[cod] .pytest_cache/ *.egg-info/ # Rust target/ # Go / PHP / Ruby vendored deps vendor/ .bundle/ # Java / Kotlin / Scala .gradle/ # .NET bin/ obj/ # Elixir _build/ deps/ # Dart / Flutter .dart_tool/
Recorded tool call
{
"type": "tool_call",
"seq": 91,
"ts": "2026-10-22T11:31:49.253Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && git log --oneline -5 2>&1 | head; echo \"--- remotes\"; git remote -v; echo \"--- branches\"; git branch -a; echo \"--- gitignore\"; cat .gitignore"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b21fd25c10b1e20f",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 93,
"ts": "2026-10-22T11:31:49.253Z",
"turn": 1,
"output": {
"content": [
{
"text": "9ebf5fb Initial workspace\n--- remotes\n--- branches\n* main\n--- gitignore\n# Keep generated and vendored files out of source diffs.\n\n# Logs / OS cruft\n*.log\n.DS_Store\n\n# JavaScript / TypeScript\nnode_modules/\n.next/\ndist/\nbuild/\ncoverage/\n\n# Python\n.venv/\nvenv/\n__pycache__/\n*.py[cod]\n.pytest_cache/\n*.egg-info/\n\n# Rust\ntarget/\n\n# Go / PHP / Ruby vendored deps\nvendor/\n.bundle/\n\n# Java / Kotlin / Scala\n.gradle/\n\n# .NET\nbin/\nobj/\n\n# Elixir\n_build/\ndeps/\n\n# Dart / Flutter\n.dart_tool/\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b21fd25c10b1e20f",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && for u in https://docs.semaphore.io/using-semaphore/ai/testbox https://docs.semaphore.io/reference/pipeline-yaml; do echo "#### $u"; curl -sL -m 12 "$u" | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:1800])"; done#### https://docs.semaphore.io/using-semaphore/ai/testbox
#### https://docs.semaphore.io/using-semaphore/ai/testbox Semaphore Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken. #### https://docs.semaphore.io/reference/pipeline-yaml Pipeline YAML | Semaphore Skip to main content Version: Cloud (SaaS) On this page Pipeline YAML This page describes the formal pipeline YAML specification for Semaphore. Overview Semaphore uses YAML for the definition of pipeline . Every Semaphore project requires at least one pipeline to work. If you don't want to write pipelines by hand, you can use the visual workflow editor . Execution order You cannot assume that jobs in the same task run in any particular order. They run in parallel on a resource availability basis. To force execution order, you must use block dependencies . Semaphore only starts a block when all their dependencies are completed. Comments Lines beginning with # are considered comments and are ignored by the YAML parser. version The version of the pipeline YAML specification to be used. The only value supported is v1.0 Example version : v1.0 name A Unicode string for the pipeline name. It is strongly recommended that you give descriptive names to your Semaphore pipelines. Example name : The name of the Semaphore pipeline agent Defines the global agent's machine type and os_image to run jobs . See agents to learn more. The agent can contain the following properties: [ machine ]: VM machine type to run the jobs containers : optional Docker containers to run the jobs Example agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 note The default agent can be overridden inside tasks . machine Part of the agent definition. It defines the global VM machine type to run the jobs. It requires two properties: type os_image Example machine : type : e1 - standard - 2 os_image : ubuntu2404 type Part of the agent definition. It selects the hardware or self-hosted agent type that runs the jobs. The list of valid values for Semaphore Clou
Recorded tool call
{
"type": "tool_call",
"seq": 92,
"ts": "2026-10-22T11:31:49.253Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && for u in https://docs.semaphore.io/using-semaphore/ai/testbox https://docs.semaphore.io/reference/pipeline-yaml; do echo \"#### $u\"; curl -sL -m 12 \"$u\" | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:1800])\"; done"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8c49dfe2af11902e",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 94,
"ts": "2026-10-22T11:31:50.325Z",
"turn": 1,
"output": {
"content": [
{
"text": "#### https://docs.semaphore.io/using-semaphore/ai/testbox\nSemaphore\nSkip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken.\n#### https://docs.semaphore.io/reference/pipeline-yaml\nPipeline YAML | Semaphore\nSkip to main content Version: Cloud (SaaS) On this page Pipeline YAML\nThis page describes the formal pipeline YAML specification for Semaphore.\nOverview \nSemaphore uses YAML for the definition of pipeline . Every Semaphore project requires at least one pipeline to work. If you don't want to write pipelines by hand, you can use the visual workflow editor .\nExecution order \nYou cannot assume that jobs in the same task run in any particular order. They run in parallel on a resource availability basis.\nTo force execution order, you must use block dependencies . Semaphore only starts a block when all their dependencies are completed.\nComments \nLines beginning with # are considered comments and are ignored by the YAML parser.\nversion \nThe version of the pipeline YAML specification to be used. The only value supported is v1.0\nExample version : v1.0\nname \nA Unicode string for the pipeline name. It is strongly recommended that you give descriptive names to your Semaphore pipelines.\nExample name : The name of the Semaphore pipeline\nagent \nDefines the global agent's machine type and os_image to run jobs . See agents to learn more.\nThe agent can contain the following properties:\n[ machine ]: VM machine type to run the jobs\ncontainers : optional Docker containers to run the jobs\nExample agent : machine : type : e1 - standard - 2 os_image : ubuntu2404\nnote The default agent can be overridden inside tasks .\nmachine \nPart of the agent definition. It defines the global VM machine type to run the jobs.\nIt requires two properties:\ntype\nos_image\nExample machine : type : e1 - standard - 2 os_image : ubuntu2404\ntype \nPart of the agent definition. It selects the hardware or self-hosted agent type that runs the jobs.\nThe list of valid values for Semaphore Clou\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8c49dfe2af11902e",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The repo is essentially empty (a single "Initial workspace" commit, no remote). That means there's no GitHub repo to connect — sem-ai project create needs a git repo URL, and Semaphore requires either a GitHub or Bitbucket repository.
This is a second blocker: to create a project, you need a hosted git repo (GitHub/Bitbucket) and an integration connected to the Semaphore org. The empty local repo has no remote.
So the full blocker chain is:
- No Semaphore account/token → device flow requires a human browser operation (code RKRK-DJBP).
- No GitHub/Bitbucket repo → Semaphore projects must connect to a hosted git repo. The local repo has no remote, and I don't have GitHub credentials either.
Let me confirm I don't have GitHub credentials.
I'll check gh CLI / GITHUB_TOKEN.
Let me verify and also check whether the signin process is still alive.
env | grep -iE "github|gitlab|bitbucket|GH_" | sed 's/=.*/=<set>/'; echo "--- gh cli"; which gh 2>/dev/null || echo "no gh"; echo "--- signin still waiting?"; pgrep -f "sem-ai signin" >/dev/null && echo "alive (waiting for approval)" || echo "exited"; tail -3 /tmp/signin.log; echo "--- sem.yaml"; ls -la ~/.sem.yaml 2>/dev/null || echo "no ~/.sem.yaml yet"
--- gh cli
--- gh cli no gh --- signin still waiting? alive (waiting for approval) Open https://id.semaphoreci.com/device in a browser and enter the code. Sign in there (or create your account), then approve. Waiting... --- sem.yaml -rw------- 1 root root 0 Oct 22 11:31 /sandbox/.sem.yaml
Recorded tool call
{
"type": "tool_call",
"seq": 97,
"ts": "2026-10-22T11:31:55.252Z",
"name": "bash",
"turn": 1,
"input": {
"command": "env | grep -iE \"github|gitlab|bitbucket|GH_\" | sed 's/=.*/=<set>/'; echo \"--- gh cli\"; which gh 2>/dev/null || echo \"no gh\"; echo \"--- signin still waiting?\"; pgrep -f \"sem-ai signin\" >/dev/null && echo \"alive (waiting for approval)\" || echo \"exited\"; tail -3 /tmp/signin.log; echo \"--- sem.yaml\"; ls -la ~/.sem.yaml 2>/dev/null || echo \"no ~/.sem.yaml yet\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b0c36b644f0d146c",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 98,
"ts": "2026-10-22T11:31:55.252Z",
"turn": 1,
"output": {
"content": [
{
"text": "--- gh cli\nno gh\n--- signin still waiting?\nalive (waiting for approval)\n\nOpen https://id.semaphoreci.com/device in a browser and enter the code.\nSign in there (or create your account), then approve. Waiting...\n--- sem.yaml\n-rw------- 1 root root 0 Oct 22 11:31 /sandbox/.sem.yaml\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b0c36b644f0d146c",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Confirmed:
- No GitHub/GitLab/Bitbucket credentials, no gh CLI.
- Signin is still waiting (device flow) — needs human browser operation.
- Repo has no remote.
So, we're blocked on two points, both of which require human response:
- Approving the device code (account creation) — browser work, human only.
- A hosted GitHub/Bitbucket repo to connect as a Semaphore project. Semaphore can't build a local-only repo in the cloud.
The user said "let me know if you get blocked." So I should report it. However, I've also done useful groundwork: installed sem-ai, obtained the device code, validated the API surface.
Should I stop here, or should I do prep that doesn't require auth? I can write an example .semaphore/semaphore.yml and a small app, so that the moment auth comes through, we're ready to go. That's lightweight and useful, and it's the "easy example" part that I can do. Let's do that — make a small Python project with tests and a valid semaphore.yml.
Actually, let me think carefully: the user asked "help me build an easy example using Semaphore." Given that we're blocked, the most useful deliverable is:
- Example repo files (app + tests + .semaphore/semaphore.yml) — these work without auth
- Clear report of blockers
- Pricing info
- Assessment of ease of management
That's a solid response. Let's build the example files. I'll validate the YAML against the spec.
Let me look a bit more at the pipeline YAML spec for blocks/tasks/prologue/epilogue and agent types, and machine types for pricing correlation.
Let me grab the machine-types reference to get the type names right.
Confirmed blocked on two human-only steps. Let me still build the auth-free part of the example. First, exact agent type names:
cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/machine-types | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
print('\n'.join(l.strip() for l in t.split('\n') if l.strip())[:2500])"Machine Types | Semaphore
Machine Types | Semaphore Skip to main content Version: Cloud (SaaS) On this page Machine Types This page gives technical specifications for the machines available on Semaphore Cloud. Overview A machine type defines what virtualized hardware to use in your agents . The machines described in this page only apply to Semaphore Cloud. You can add more types of machines using self-hosted agents . Performance baseline and resource availability Each Semaphore machine type is provisioned with a guaranteed baseline of virtual CPUs, memory, and disk as listed in the specifications below. Depending on host utilization at any given moment, a machine may temporarily receive additional CPU time, disk throughput, network bandwidth, and memory speed beyond its baseline specification. This happens when the physical host running your VM has spare capacity available — for example, during off-peak hours when fewer jobs are running concurrently. As a result, you may notice that identical jobs sometimes complete faster than previous runs, even when nothing in the job configuration has changed. This is not a sign of performance degradation — these faster runs are benefiting from additional resources temporarily available on the host, and performance may vary between runs. Linux machines Linux machines are provided in two generations. The following table shows what operating systems each generation supports and if they support Docker environments . Generation Arch OS Supported Docker environments F1 Intel x86_64 Ubuntu 22.04 Ubuntu 24.04 Yes R1 ARM Ubuntu 24.04 ARM No For legacy machine generations still supported for existing usage, see Legacy Linux generations . F1 generation This generation is intended for compute-intensive tasks. They provide the most performance amongst the machines offered in Semaphore Cloud. Implementation details: CPU : hyperthreaded on a 4.6GHz 12th generation Intel i5 125000 Memory : DDR4 RAM Disk : NvME storage The F1 generation is presented in the following types: Type Virtual CPUs Memory (GB) Disk (GB) f1-standard-2 2 8 45 f1-standard-4 4 16 65 F1 machines can be paired with: ubuntu2204 ubuntu2404 Docker-based environments . R1 generation These are ARM machines. Implementation details: CPU : emulated ARM on Ampere Altra Q80-30 Memory : DDR4 RAM Disk : NvME storage The r1 generation is presented in the following types: Type Virtual CPUs Memory (GB) Disk (GB) r1-standard-2 2 4 50 r1-standard-4 4 8 65 r1-standard-8 8 16 75 R1 Generation machin
Recorded tool call
{
"type": "tool_call",
"seq": 102,
"ts": "2026-10-22T11:32:03.969Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/machine-types | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\nprint('\\n'.join(l.strip() for l in t.split('\\n') if l.strip())[:2500])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a30850c57cd30c92",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 106,
"ts": "2026-10-22T11:32:04.338Z",
"turn": 1,
"output": {
"content": [
{
"text": "Machine Types | Semaphore\nSkip to main content Version: Cloud (SaaS) On this page Machine Types\nThis page gives technical specifications for the machines available on Semaphore Cloud.\nOverview \nA machine type defines what virtualized hardware to use in your agents .\nThe machines described in this page only apply to Semaphore Cloud. You can add more types of machines using self-hosted agents .\nPerformance baseline and resource availability \nEach Semaphore machine type is provisioned with a guaranteed baseline of virtual CPUs, memory, and disk as listed in the specifications below.\nDepending on host utilization at any given moment, a machine may temporarily receive additional CPU time, disk throughput, network bandwidth, and memory speed beyond its baseline specification. This happens when the physical host running your VM has spare capacity available — for example, during off-peak hours when fewer jobs are running concurrently.\nAs a result, you may notice that identical jobs sometimes complete faster than previous runs, even when nothing in the job configuration has changed. This is not a sign of performance degradation — these faster runs are benefiting from additional resources temporarily available on the host, and performance may vary between runs.\nLinux machines \nLinux machines are provided in two generations. The following table shows what operating systems each generation supports and if they support Docker environments .\nGeneration Arch OS Supported Docker environments F1 Intel x86_64 Ubuntu 22.04 Ubuntu 24.04 Yes R1 ARM Ubuntu 24.04 ARM No\nFor legacy machine generations still supported for existing usage, see Legacy Linux generations .\nF1 generation \nThis generation is intended for compute-intensive tasks. They provide the most performance amongst the machines offered in Semaphore Cloud.\nImplementation details:\nCPU : hyperthreaded on a 4.6GHz 12th generation Intel i5 125000\nMemory : DDR4 RAM\nDisk : NvME storage\nThe F1 generation is presented in the following types:\nType Virtual CPUs Memory (GB) Disk (GB) f1-standard-2 2 8 45 f1-standard-4 4 16 65\nF1 machines can be paired with:\nubuntu2204\nubuntu2404\nDocker-based environments .\nR1 generation \nThese are ARM machines.\nImplementation details:\nCPU : emulated ARM on Ampere Altra Q80-30\nMemory : DDR4 RAM\nDisk : NvME storage\nThe r1 generation is presented in the following types:\nType Virtual CPUs Memory (GB) Disk (GB) r1-standard-2 2 4 50 r1-standard-4 4 8 65 r1-standard-8 8 16 75\nR1 Generation machin\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a30850c57cd30c92",
"is_error": false,
"tool_kind": "shell"
}cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/pipeline-yaml | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
o='\n'.join(l.strip() for l in t.split('\n') if l.strip())
i=o.find('blocks')
print(o[i:i+2600])"blocks and jobs , where a limit longer than the pipeline's is rejected when the pipeline is compiled. Jobs in an after_pipeline task instead inherit the pipelin
blocks and jobs , where a limit longer than the pipeline's is rejected when the pipeline is compiled. Jobs in an after_pipeline task instead inherit the pipeline limit unless they set their own. Example version : v1.0 name : Using execution_time_limit agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 execution_time_limit : hours : 3 fail_fast This optional property defines what happens when a job fails. It accepts the following properties: stop 'cancel' If both are set, stop is evaluated first. If fail_fast is not defined, jobs continue running following declared dependencies when a job fails. stop The stop property causes all running jobs to stop as soon as one job fails. It requires a when property that defines a condition according to Conditions DSL . In the following configuration, blocks A and B run in parallel. Block C runs after block B is finished. If Block A fails and the workflow is initiated from a non-master branch all running jobs stop immediately. Example version : v1.0 name : Setting fail fast stop policy agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 fail_fast : stop : when : "branch != 'master'" blocks : - name : Block A dependencies : [ ] task : jobs : - name : Job A commands : - sleep 10 - failing command - name : Block B dependencies : [ ] task : jobs : - name : Job B commands : - sleep 60 - name : Block C dependencies : [ "Block B" ] task : jobs : - name : Job C commands : - sleep 60 cancel The cancel property causes all non-started jobs to be canceled as soon as one job fails. Already running jobs are allowed to finish. This property requires a when property that defines a condition according to Conditions DSL . In the following configuration, blocks A and B run in parallel. Block C runs after block B is finished. If Block A fails in a workflow that was initiated from a non-master branch: Block B is allowed to finish Block C is canceled, i.e. it never starts Example version : v1.0 name : Setting fail fast cancel policy agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 fail_fast : cancel : when : "branch != 'master'" blocks : - name : Block A dependencies : [ ] task : jobs : - name : Job A commands : - sleep 10 - failing command - name : Block B dependencies : [ ] task : jobs : - name : Job B commands : - sleep 60 - name : Block C dependencies : [ "Block B" ] task : jobs : - name : Job C commands : - sleep 60 queue The optional queue property enables you to assign pipelines to custom execution queues or to configure the way the pipelines are processed when queuing happens. There
Recorded tool call
{
"type": "tool_call",
"seq": 104,
"ts": "2026-10-22T11:32:03.969Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://docs.semaphore.io/reference/pipeline-yaml | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\no='\\n'.join(l.strip() for l in t.split('\\n') if l.strip())\ni=o.find('blocks')\nprint(o[i:i+2600])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-9b8e805f77af927b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 107,
"ts": "2026-10-22T11:32:04.641Z",
"turn": 1,
"output": {
"content": [
{
"text": "blocks and jobs , where a limit longer than the pipeline's is rejected when the pipeline is compiled. Jobs in an after_pipeline task instead inherit the pipeline limit unless they set their own.\nExample version : v1.0 name : Using execution_time_limit agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 execution_time_limit : hours : 3\nfail_fast \nThis optional property defines what happens when a job fails. It accepts the following properties:\nstop\n'cancel'\nIf both are set, stop is evaluated first. If fail_fast is not defined, jobs continue running following declared dependencies when a job fails.\nstop \nThe stop property causes all running jobs to stop as soon as one job fails. It requires a when property that defines a condition according to Conditions DSL .\nIn the following configuration, blocks A and B run in parallel. Block C runs after block B is finished. If Block A fails and the workflow is initiated from a non-master branch all running jobs stop immediately.\nExample version : v1.0 name : Setting fail fast stop policy agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 fail_fast : stop : when : \"branch != 'master'\" blocks : - name : Block A dependencies : [ ] task : jobs : - name : Job A commands : - sleep 10 - failing command - name : Block B dependencies : [ ] task : jobs : - name : Job B commands : - sleep 60 - name : Block C dependencies : [ \"Block B\" ] task : jobs : - name : Job C commands : - sleep 60\ncancel \nThe cancel property causes all non-started jobs to be canceled as soon as one job fails. Already running jobs are allowed to finish. This property requires a when property that defines a condition according to Conditions DSL .\nIn the following configuration, blocks A and B run in parallel. Block C runs after block B is finished. If Block A fails in a workflow that was initiated from a non-master branch:\nBlock B is allowed to finish\nBlock C is canceled, i.e. it never starts\nExample version : v1.0 name : Setting fail fast cancel policy agent : machine : type : e1 - standard - 2 os_image : ubuntu2404 fail_fast : cancel : when : \"branch != 'master'\" blocks : - name : Block A dependencies : [ ] task : jobs : - name : Job A commands : - sleep 10 - failing command - name : Block B dependencies : [ ] task : jobs : - name : Job B commands : - sleep 60 - name : Block C dependencies : [ \"Block B\" ] task : jobs : - name : Job C commands : - sleep 60\nqueue \nThe optional queue property enables you to assign pipelines to custom execution queues or to configure the way the pipelines are processed when queuing happens.\nThere\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-9b8e805f77af927b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Now the spec is all lined up. Let's create a sample project. Write a small Python app and tests, plus a .semaphore/semaphore.yml that includes lint + test blocks and a promotion.
What to create:
greeter/__init__.py(small module)tests/test_greeter.pyrequirements-dev.txt(pytest, ruff).semaphore/semaphore.ymlREADME.mddocumenting what's remaining
After that, verify that the tests pass locally (fast, no service stack needed), and validate that the YAML parses.
Machine type: use f1-standard-2 and ubuntu2404. Note: the docs examples use e1-standard-2 (legacy). F1 is current gen x64. Going with f1-standard-2.
Let's write the files.
On pricing correlation:
- Ubuntu x64 $0.0075/min 2vCPU → f1-standard-2
- $15/month free credit ≈ 2000 minutes of x64 2vCPU
- Concurrency of 20 included
- Artifacts are free up to 20GB/month egress, 100GB storage; overages are $0.35/GB egress, $0.07/GB storage
- Cache is 9.6GB per project
- Dedicated cache server / private Docker registry add-on is $400/month each
- Support add-ons: Basic $50, Priority $250, SLA Standard $500, SLA Premium $750
- ARM Ubuntu $0.003/min 2vCPU, $0.006 4vCPU, $0.012 8vCPU
- x64: $0.0075 2vCPU, $0.015 4vCPU
- macOS $0.09/min 4vCPU
- Self-hosted $0.0025/min any machine
- MCP server: free if you contact support
Pay-as-you-go, credits-based. Good.
Alright, let's create the files.
The pipeline YAML will have two blocks: Lint and Test, with Test depending on Lint. Plus caching of pip deps. And add a promotion for deploy gated on master.
Going to keep it simple but realistic.
Let's write it.
For caching, Semaphore uses cache store / cache restore.
Let's write semaphore.yml:
version: v1.0
name: Greeter CI
agent:
machine:
type: f1-standard-2
os_image: ubuntu2404
blocks:
- name: Lint
task:
jobs:
- name: ruff
commands:
- checkout
- python3 -m venv .venv
- source .venv/bin/activate
- pip install -q -r requirements-dev.txt
- ruff check .
- name: Test
dependencies: [Lint]
task:
jobs:
- name: pytest
commands:
- checkout
- cache restore pip-$(checksum requirements-dev.txt) || true
- python3 -m venv .venv
- source .venv/bin/activate
- pip install -q -r requirements-dev.txt
- cache store pip-$(checksum requirements-dev.txt) .venv
- pytest -q --junitxml=report.xml
Hmm, caching .venv may be unreliable. But it's good enough as an example. Actually, more simply: caching the pip cache dir is better. Let's not over-engineer; cache with key by checksum and store .venv — Semaphore's docs do show caching of venv/dep dirs. Acceptable.
Add test report? Requires additional config. Skip to stay lightweight, but mention it.
Let's also add a promotion to demonstrate deploy gating. Keep it minimal — maybe skip promotion to avoid extra complexity. Actually, a small promotion is the characteristic Semaphore feature. Add one gated on master branch that just echoes (no real deploy). Hmm — this creates a .semaphore/deploy.yml. Let's include it; it's illustrative and cheap.
Let's write the files.
Then run pytest locally to verify the tests pass.
Alright, writing it all now.
Spec confirmed. Now building the auth-free part of the example:
File edits
1 file
Writtengreeter/__init__.pyView changes
/sandbox/repo/greeter/__init__.py
Written content · previous contents were not recorded
"""Tiny library used as a Semaphore CI example."""from __future__ import annotationsVALID_LANGS = {"en", "es", "de"}_GREETINGS = {"en": "Hello","es": "Hola","de": "Hallo",}def greet(name: str, lang: str = "en") -> str:"""Return a greeting for ``name`` in ``lang``.Raises:ValueError: if ``name`` is blank or ``lang`` is unsupported."""if not isinstance(name, str) or not name.strip():raise ValueError("name must be a non-empty string")if lang not in VALID_LANGS:raise ValueError(f"unsupported lang: {lang!r}")return f"{_GREETINGS[lang]}, {name.strip()}!"
Source: write · chatcmpl-tool-a55a03952c27146b
Recorded tool call
{
"type": "tool_call",
"seq": 111,
"ts": "2026-10-22T11:32:22.172Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/greeter/__init__.py",
"content": "\"\"\"Tiny library used as a Semaphore CI example.\"\"\"\n\nfrom __future__ import annotations\n\nVALID_LANGS = {\"en\", \"es\", \"de\"}\n\n_GREETINGS = {\n \"en\": \"Hello\",\n \"es\": \"Hola\",\n \"de\": \"Hallo\",\n}\n\n\ndef greet(name: str, lang: str = \"en\") -> str:\n \"\"\"Return a greeting for ``name`` in ``lang``.\n\n Raises:\n ValueError: if ``name`` is blank or ``lang`` is unsupported.\n \"\"\"\n if not isinstance(name, str) or not name.strip():\n raise ValueError(\"name must be a non-empty string\")\n if lang not in VALID_LANGS:\n raise ValueError(f\"unsupported lang: {lang!r}\")\n return f\"{_GREETINGS[lang]}, {name.strip()}!\"\n"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a55a03952c27146b",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 114,
"ts": "2026-10-22T11:32:22.172Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 637 bytes to /sandbox/repo/greeter/__init__.py",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a55a03952c27146b",
"is_error": false,
"tool_kind": "file_edit"
}File edits
1 file
Writtentests/test_greeter.pyView changes
/sandbox/repo/tests/test_greeter.py
Written content · previous contents were not recorded
import pytestfrom greeter import greet@pytest.mark.parametrize(("lang", "expected"),[("en", "Hello, Ada!"), ("es", "Hola, Ada!"), ("de", "Hallo, Ada!")],)def test_greet_supported_langs(lang, expected):assert greet("Ada", lang) == expecteddef test_greet_strips_whitespace():assert greet(" Ada ") == "Hello, Ada!"@pytest.mark.parametrize("bad", ["", " ", None, 42])def test_greet_rejects_blank_or_non_string(bad):with pytest.raises(ValueError):greet(bad)def test_greet_rejects_unknown_lang():with pytest.raises(ValueError, match="unsupported lang"):greet("Ada", "fr")
Source: write · chatcmpl-tool-b5b7f68d1845f777
Recorded tool call
{
"type": "tool_call",
"seq": 112,
"ts": "2026-10-22T11:32:22.172Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/tests/test_greeter.py",
"content": "import pytest\n\nfrom greeter import greet\n\n\n@pytest.mark.parametrize(\n (\"lang\", \"expected\"),\n [(\"en\", \"Hello, Ada!\"), (\"es\", \"Hola, Ada!\"), (\"de\", \"Hallo, Ada!\")],\n)\ndef test_greet_supported_langs(lang, expected):\n assert greet(\"Ada\", lang) == expected\n\n\ndef test_greet_strips_whitespace():\n assert greet(\" Ada \") == \"Hello, Ada!\"\n\n\n@pytest.mark.parametrize(\"bad\", [\"\", \" \", None, 42])\ndef test_greet_rejects_blank_or_non_string(bad):\n with pytest.raises(ValueError):\n greet(bad)\n\n\ndef test_greet_rejects_unknown_lang():\n with pytest.raises(ValueError, match=\"unsupported lang\"):\n greet(\"Ada\", \"fr\")\n"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b5b7f68d1845f777",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 113,
"ts": "2026-10-22T11:32:22.172Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 635 bytes to /sandbox/repo/tests/test_greeter.py",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b5b7f68d1845f777",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Next up are requirements-dev.txt, .semaphore/semaphore.yml, and .semaphore/deploy.yml.
File edits
1 file
Writtenrequirements-dev.txtView changes
/sandbox/repo/requirements-dev.txt
Written content · previous contents were not recorded
pytest==8.3.4ruff==0.8.4
Source: write · chatcmpl-tool-853beb23047bc622
Recorded tool call
{
"type": "tool_call",
"seq": 117,
"ts": "2026-10-22T11:32:27.218Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/requirements-dev.txt",
"content": "pytest==8.3.4\nruff==0.8.4\n"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-853beb23047bc622",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 119,
"ts": "2026-10-22T11:32:27.218Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 26 bytes to /sandbox/repo/requirements-dev.txt",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-853beb23047bc622",
"is_error": false,
"tool_kind": "file_edit"
}File edits
1 file
Written.semaphore/semaphore.ymlView changes
/sandbox/repo/.semaphore/semaphore.yml
Written content · previous contents were not recorded
version: v1.0name: Greeter CIagent:machine:# Ubuntu x64, 2 vCPU / 8 GB — billed at $0.0075 per minute.type: f1-standard-2os_image: ubuntu2404execution_time_limit:minutes: 15blocks:- name: Linttask:jobs:- name: ruffcommands:- checkout- python3 -m venv .venv- source .venv/bin/activate- pip install -q -r requirements-dev.txt- ruff check .- name: Testdependencies: [Lint]task:jobs:- name: pytestcommands:- checkout- cache restore pip-$SEMAPHORE_PROJECT_ID || true- python3 -m venv .venv- source .venv/bin/activate- pip install -q -r requirements-dev.txt- cache store pip-$SEMAPHORE_PROJECT_ID .venv- pytest -q --junitxml=report.xmlepilogue:always:commands:- test-results publish --name pytest report.xml || truepromotions:# Only offer the deploy path on the main branch, after everything passes.- name: Deploypipeline_file: .semaphore/deploy.ymlauto_promote_on:- result: passedbranch:- main
Source: write · chatcmpl-tool-aeb627f2502caf1a
Recorded tool call
{
"type": "tool_call",
"seq": 118,
"ts": "2026-10-22T11:32:27.218Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/.semaphore/semaphore.yml",
"content": "version: v1.0\nname: Greeter CI\n\nagent:\n machine:\n # Ubuntu x64, 2 vCPU / 8 GB — billed at $0.0075 per minute.\n type: f1-standard-2\n os_image: ubuntu2404\n\nexecution_time_limit:\n minutes: 15\n\nblocks:\n - name: Lint\n task:\n jobs:\n - name: ruff\n commands:\n - checkout\n - python3 -m venv .venv\n - source .venv/bin/activate\n - pip install -q -r requirements-dev.txt\n - ruff check .\n\n - name: Test\n dependencies: [Lint]\n task:\n jobs:\n - name: pytest\n commands:\n - checkout\n - cache restore pip-$SEMAPHORE_PROJECT_ID || true\n - python3 -m venv .venv\n - source .venv/bin/activate\n - pip install -q -r requirements-dev.txt\n - cache store pip-$SEMAPHORE_PROJECT_ID .venv\n - pytest -q --junitxml=report.xml\n epilogue:\n always:\n commands:\n - test-results publish --name pytest report.xml || true\n\npromotions:\n # Only offer the deploy path on the main branch, after everything passes.\n - name: Deploy\n pipeline_file: .semaphore/deploy.yml\n auto_promote_on:\n - result: passed\n branch:\n - main\n"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-aeb627f2502caf1a",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 120,
"ts": "2026-10-22T11:32:27.218Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 1250 bytes to /sandbox/repo/.semaphore/semaphore.yml",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-aeb627f2502caf1a",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Alright, next up is deploy.yml. Let's keep it to a minimum — just echoing a placeholder that shows where to incorporate the actual deploy.
File edits
1 file
Written.semaphore/deploy.ymlView changes
/sandbox/repo/.semaphore/deploy.yml
Written content · previous contents were not recorded
version: v1.0name: Deployagent:machine:type: f1-standard-2os_image: ubuntu2404blocks:- name: Shiptask:jobs:- name: publishcommands:- checkout# Placeholder: swap for your real release step, e.g.# - docker build -t myapp:$SEMAPHORE_GIT_SHA .# - docker push myregistry/myapp:$SEMAPHORE_GIT_SHA- echo "Would deploy $SEMAPHORE_PROJECT_NAME @ $SEMAPHORE_GIT_SHA"
Source: write · chatcmpl-tool-8d1d6af07c0b970e
Recorded tool call
{
"type": "tool_call",
"seq": 123,
"ts": "2026-10-22T11:32:30.107Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/.semaphore/deploy.yml",
"content": "version: v1.0\nname: Deploy\n\nagent:\n machine:\n type: f1-standard-2\n os_image: ubuntu2404\n\nblocks:\n - name: Ship\n task:\n jobs:\n - name: publish\n commands:\n - checkout\n # Placeholder: swap for your real release step, e.g.\n # - docker build -t myapp:$SEMAPHORE_GIT_SHA .\n # - docker push myregistry/myapp:$SEMAPHORE_GIT_SHA\n - echo \"Would deploy $SEMAPHORE_PROJECT_NAME @ $SEMAPHORE_GIT_SHA\"\n"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8d1d6af07c0b970e",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 124,
"ts": "2026-10-22T11:32:30.107Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 479 bytes to /sandbox/repo/.semaphore/deploy.yml",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8d1d6af07c0b970e",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Alright, let's verify: run pytest locally, run ruff, and verify that the YAML can be parsed. These are fast and don't require a service stack.
Let's create a venv, install pytest+ruff, and run them.
It might take about 20 seconds, but that's fine — it's not a long-running service.
Alright, let's execute.
Verifying locally (fast, no services):
cd /sandbox/repo && python3 -m venv .venv && . .venv/bin/activate && pip install -q -r requirements-dev.txt 2>&1 | tail -3; echo "== ruff"; ruff check . && echo "ruff OK"; echo "== pytest"; pytest -q --junitxml=/tmp/report.xml 2>&1 | tail -8
== ruff
== ruff
All checks passed!
ruff OK
== pytest
tests/test_greeter.py:3: in <module>
from greeter import greet
E ModuleNotFoundError: No module named 'greeter'
--------------------- generated xml file: /tmp/report.xml ----------------------
=========================== short test summary info ============================
ERROR tests/test_greeter.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.09s
Recorded tool call
{
"type": "tool_call",
"seq": 128,
"ts": "2026-10-22T11:32:33.041Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && python3 -m venv .venv && . .venv/bin/activate && pip install -q -r requirements-dev.txt 2>&1 | tail -3; echo \"== ruff\"; ruff check . && echo \"ruff OK\"; echo \"== pytest\"; pytest -q --junitxml=/tmp/report.xml 2>&1 | tail -8"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a4addf8e65619253",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 131,
"ts": "2026-10-22T11:32:41.930Z",
"turn": 1,
"output": {
"content": [
{
"text": "== ruff\nAll checks passed!\nruff OK\n== pytest\ntests/test_greeter.py:3: in <module>\n from greeter import greet\nE ModuleNotFoundError: No module named 'greeter'\n--------------------- generated xml file: /tmp/report.xml ----------------------\n=========================== short test summary info ============================\nERROR tests/test_greeter.py\n!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!\n1 error in 0.09s\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a4addf8e65619253",
"is_error": false,
"tool_kind": "shell"
}cd /sandbox/repo && python3 -c "
import yaml
for f in ['.semaphore/semaphore.yml','.semaphore/deploy.yml']:
d=yaml.safe_load(open(f))
print(f,'-> parsed OK; version',d['version'],'| agent',d['agent']['machine'],'| blocks',[b['name'] for b in d['blocks']])
" 2>&1 | tail -5.semaphore/semaphore.yml -> parsed OK; version v1.0 | agent {'type': 'f1-standard-2', 'os_image': 'ubuntu2404'} | blocks ['Lint', 'Test']
.semaphore/semaphore.yml -> parsed OK; version v1.0 | agent {'type': 'f1-standard-2', 'os_image': 'ubuntu2404'} | blocks ['Lint', 'Test']
.semaphore/deploy.yml -> parsed OK; version v1.0 | agent {'type': 'f1-standard-2', 'os_image': 'ubuntu2404'} | blocks ['Ship']
Recorded tool call
{
"type": "tool_call",
"seq": 129,
"ts": "2026-10-22T11:32:33.041Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && python3 -c \"\nimport yaml\nfor f in ['.semaphore/semaphore.yml','.semaphore/deploy.yml']:\n d=yaml.safe_load(open(f))\n print(f,'-> parsed OK; version',d['version'],'| agent',d['agent']['machine'],'| blocks',[b['name'] for b in d['blocks']])\n\" 2>&1 | tail -5"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b6d88f56efc4818d",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 130,
"ts": "2026-10-22T11:32:33.390Z",
"turn": 1,
"output": {
"content": [
{
"text": ".semaphore/semaphore.yml -> parsed OK; version v1.0 | agent {'type': 'f1-standard-2', 'os_image': 'ubuntu2404'} | blocks ['Lint', 'Test']\n.semaphore/deploy.yml -> parsed OK; version v1.0 | agent {'type': 'f1-standard-2', 'os_image': 'ubuntu2404'} | blocks ['Ship']\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b6d88f56efc4818d",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The reason pytest can't find greeter is that there's no packaging / rootdir configuration. This will also fail on CI. Need to fix it properly — either add a pyproject.toml containing pytest configuration (pythonpath), or make it an installable package. The cleanest and most minimal fix: add [tool.pytest.ini_options] pythonpath = ["."] to pyproject.toml. This works with pytest 8.
Let's add pyproject.toml. This will also be where we put the ruff configuration.
This is a bug I actually caught — good thing I ran it.
Alright, let's write pyproject.toml.
Real bug caught — pytest can't resolve the package. Adding proper config:
File edits
1 file
Writtenpyproject.tomlView changes
/sandbox/repo/pyproject.toml
Written content · previous contents were not recorded
[project]name = "greeter"version = "0.1.0"description = "Tiny library used as a Semaphore CI example"requires-python = ">=3.10"[tool.pytest.ini_options]testpaths = ["tests"]pythonpath = ["."]addopts = "-q"[tool.ruff]line-length = 100target-version = "py310"[tool.ruff.lint]select = ["E", "F", "I", "UP", "B"]
Source: write · chatcmpl-tool-a75c5f6d267951e0
Recorded tool call
{
"type": "tool_call",
"seq": 135,
"ts": "2026-10-22T11:32:46.662Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/pyproject.toml",
"content": "[project]\nname = \"greeter\"\nversion = \"0.1.0\"\ndescription = \"Tiny library used as a Semaphore CI example\"\nrequires-python = \">=3.10\"\n\n[tool.pytest.ini_options]\ntestpaths = [\"tests\"]\npythonpath = [\".\"]\naddopts = \"-q\"\n\n[tool.ruff]\nline-length = 100\ntarget-version = \"py310\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\", \"UP\", \"B\"]\n"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a75c5f6d267951e0",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 136,
"ts": "2026-10-22T11:32:46.662Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 325 bytes to /sandbox/repo/pyproject.toml",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a75c5f6d267951e0",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Note: The ruff "I" rule (isort) might warn about import ordering. Let's re-run both. Also, regarding the from __future__ import annotations and the use of pytest.mark.parametrize. Let me try running it.
cd /sandbox/repo && . .venv/bin/activate && echo "== ruff"; ruff check . && echo "ruff OK"; echo "== pytest"; pytest --junitxml=/tmp/report.xml 2>&1 | tail -6
== ruff
== ruff All checks passed! ruff OK == pytest ......... [100%] --------------------- generated xml file: /tmp/report.xml ---------------------- 9 passed in 0.02s
Recorded tool call
{
"type": "tool_call",
"seq": 139,
"ts": "2026-10-22T11:32:49.056Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && . .venv/bin/activate && echo \"== ruff\"; ruff check . && echo \"ruff OK\"; echo \"== pytest\"; pytest --junitxml=/tmp/report.xml 2>&1 | tail -6"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a7d790d7c760295f",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 140,
"ts": "2026-10-22T11:32:49.056Z",
"turn": 1,
"output": {
"content": [
{
"text": "== ruff\nAll checks passed!\nruff OK\n== pytest\n......... [100%]\n--------------------- generated xml file: /tmp/report.xml ----------------------\n9 passed in 0.02s\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a7d790d7c760295f",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
All 9 tests pass, and ruff is clean. The YAML also parses correctly.
Next, let's check whether the signin device code is still valid / pending, and add a README documenting the handoff. Also, let's verify that the test-results command in the epilogue is correct — test-results publish. Let's quickly validate this in the toolbox docs. Actually, this is a documented Semaphore toolbox command. I'll keep the || true guard, but let's quickly check the exact syntax.
I'll do a quick check of the test-results publish syntax and check the signin status.
All 9 tests pass, ruff clean, YAML valid. Let me verify the toolbox command syntax and signin status:
cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/test-reports 2>/dev/null | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
o='\n'.join(l.strip() for l in t.split('\n') if l.strip())
i=o.find('test-results publish'); print(o[max(0,i-400):i+500] if i>0 else o[:600])"Semaphore
Semaphore Skip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken.
Recorded tool call
{
"type": "tool_call",
"seq": 144,
"ts": "2026-10-22T11:32:54.262Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/test-reports 2>/dev/null | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\no='\\n'.join(l.strip() for l in t.split('\\n') if l.strip())\ni=o.find('test-results publish'); print(o[max(0,i-400):i+500] if i>0 else o[:600])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b4cfcf0361a4951f",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 148,
"ts": "2026-10-22T11:32:54.641Z",
"turn": 1,
"output": {
"content": [
{
"text": "Semaphore\nSkip to main content Page Not Found We could not find what you were looking for. Please contact the owner of the site that linked you to the original URL and let them know their link is broken.\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b4cfcf0361a4951f",
"is_error": false,
"tool_kind": "shell"
}pgrep -f "sem-ai signin" >/dev/null && echo "signin: STILL WAITING for browser approval" || echo "signin: exited"; echo "--- log tail"; tail -4 /tmp/signin.log; echo "--- token stored?"; cat ~/.sem.yaml 2>/dev/null | head -5; echo "(empty = not yet authenticated)"
signin: STILL WAITING for browser approval
signin: STILL WAITING for browser approval
--- log tail
RKRK-DJBP
Open https://id.semaphoreci.com/device in a browser and enter the code.
Sign in there (or create your account), then approve. Waiting...
--- token stored?
(empty = not yet authenticated)
Recorded tool call
{
"type": "tool_call",
"seq": 146,
"ts": "2026-10-22T11:32:54.262Z",
"name": "bash",
"turn": 1,
"input": {
"command": "pgrep -f \"sem-ai signin\" >/dev/null && echo \"signin: STILL WAITING for browser approval\" || echo \"signin: exited\"; echo \"--- log tail\"; tail -4 /tmp/signin.log; echo \"--- token stored?\"; cat ~/.sem.yaml 2>/dev/null | head -5; echo \"(empty = not yet authenticated)\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a92ac6ef0f0260ca",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 147,
"ts": "2026-10-22T11:32:54.262Z",
"turn": 1,
"output": {
"content": [
{
"text": "signin: STILL WAITING for browser approval\n--- log tail\n RKRK-DJBP\n\nOpen https://id.semaphoreci.com/device in a browser and enter the code.\nSign in there (or create your account), then approve. Waiting...\n--- token stored?\n(empty = not yet authenticated)\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a92ac6ef0f0260ca",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The URL for the test-reports documentation was wrong. Let's find the correct one from the sitemap. Earlier, I saw https://docs.semaphore.io/using-semaphore/... — let me grep the sitemap for test-report.
Let's check.
cd /tmp && curl -sL -m 15 https://docs.semaphore.io/sitemap.xml | grep -oE "<loc>[^<]+</loc>" | sed 's/<[^>]*>//g' | grep -viE "/CE|/EE" | grep -iE "test-report|toolbox|semaphore-cli"
https://docs.semaphore.io/reference/semaphore-cli
https://docs.semaphore.io/reference/semaphore-cli https://docs.semaphore.io/reference/toolbox https://docs.semaphore.io/using-semaphore/tests/test-reports
Recorded tool call
{
"type": "tool_call",
"seq": 151,
"ts": "2026-10-22T11:32:56.893Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 15 https://docs.semaphore.io/sitemap.xml | grep -oE \"<loc>[^<]+</loc>\" | sed 's/<[^>]*>//g' | grep -viE \"/CE|/EE\" | grep -iE \"test-report|toolbox|semaphore-cli\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b8f26d159130eee6",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 153,
"ts": "2026-10-22T11:32:58.052Z",
"turn": 1,
"output": {
"content": [
{
"text": "https://docs.semaphore.io/reference/semaphore-cli\nhttps://docs.semaphore.io/reference/toolbox\nhttps://docs.semaphore.io/using-semaphore/tests/test-reports\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b8f26d159130eee6",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The correct URL is https://docs.semaphore.io/using-semaphore/tests/test-reports. Let me fetch this and check the test-results publish syntax.
cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/tests/test-reports | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
o='\n'.join(l.strip() for l in t.split('\n') if l.strip())
i=o.find('test-results publish')
print(o[max(0,i-600):i+700] if i>0 else o[:900])"karma-junit-reporter JavaScript ESLint built-in JavaScript Jest jest-junit Ruby RSpec rspec_junit_formatter Ruby Cucumber built-in Elixir ExUnit junit-formatte
karma-junit-reporter JavaScript ESLint built-in JavaScript Jest jest-junit Ruby RSpec rspec_junit_formatter Ruby Cucumber built-in Elixir ExUnit junit-formatter Go GoTestSum built-in PHP PHPUnit built-in Python PyTest built-in Bash Bats built-in Rust Cargo Test junit-report Java Maven maven-surefire Step 2 - Publish The publishing step uploads all report files to the artifact store . This is accomplished using the test-results tool, which is part of the Semaphore toolbox . Assuming the file is called report.xml , you can publish the report with the following command. [[ -f report.xml ]] && test-results publish report.xml To simplify this step, you can add a pipeline epilogue to run the publish command after every job ends. Editor YAML Select the pipeline with the test jobs Add the publish command in the "Execute always" box of the Epilogue Open the pipeline file with the test jobs Add a global_job_config key at the YAML root Type the publish command under epilogue.always.commands .semaphore/semaphore.yml # ... global_job_config : epilogue : always : commands : - '[[ -f report.xml ]] && test-results publish report.xml' Step 3 - Merge The final step is to merge and process all report files. This is achieved using an after-pipeline job . Editor YAML Press +Add After Jobs Type a
Recorded tool call
{
"type": "tool_call",
"seq": 156,
"ts": "2026-10-22T11:33:00.103Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/tests/test-reports | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\no='\\n'.join(l.strip() for l in t.split('\\n') if l.strip())\ni=o.find('test-results publish')\nprint(o[max(0,i-600):i+700] if i>0 else o[:900])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-89bf57ca0f15465b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 158,
"ts": "2026-10-22T11:33:00.892Z",
"turn": 1,
"output": {
"content": [
{
"text": " karma-junit-reporter JavaScript ESLint built-in JavaScript Jest jest-junit Ruby RSpec rspec_junit_formatter Ruby Cucumber built-in Elixir ExUnit junit-formatter Go GoTestSum built-in PHP PHPUnit built-in Python PyTest built-in Bash Bats built-in Rust Cargo Test junit-report Java Maven maven-surefire\nStep 2 - Publish \nThe publishing step uploads all report files to the artifact store . This is accomplished using the test-results tool, which is part of the Semaphore toolbox .\nAssuming the file is called report.xml , you can publish the report with the following command.\n[[ -f report.xml ]] && test-results publish report.xml\nTo simplify this step, you can add a pipeline epilogue to run the publish command after every job ends.\nEditor YAML\nSelect the pipeline with the test jobs\nAdd the publish command in the \"Execute always\" box of the Epilogue\nOpen the pipeline file with the test jobs\nAdd a global_job_config key at the YAML root\nType the publish command under epilogue.always.commands\n.semaphore/semaphore.yml # ... global_job_config : epilogue : always : commands : - '[[ -f report.xml ]] && test-results publish report.xml'\nStep 3 - Merge \nThe final step is to merge and process all report files. This is achieved using an after-pipeline job .\nEditor YAML\nPress +Add After Jobs\nType a\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-89bf57ca0f15465b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The canonical syntax is [[ -f report.xml ]] && test-results publish report.xml — no --name flag is documented. Let's fix the epilogue to use the documented format, and move it into global_job_config as the docs recommend. That way is cleaner.
Let's update semaphore.yml: add global_job_config with the epilogue, and remove the per-job epilogue. Also, since a merge step for after_pipeline is needed for test reports to show up in the UI, let's add an after_pipeline block to properly merge the reports. The docs say "Add an after-pipeline job". Let's add that — this is the documented pattern and it makes the test reports actually work.
Let me check what the after_pipeline merge job looks like. Grab a bit more of that page.
Actually, let's be careful about scope creep. What the user asked for was a "simple example". Test report merging adds one more block. But this is the documented way to make test-results work, and I already introduced test-results publish. Options:
(a) Remove test-results entirely → simpler, no risk of getting the syntax wrong.
(b) Do it properly with global_job_config + after_pipeline merge.
I'll go with (b), but keep it compact, because this demonstrates a real Semaphore feature. Let me grab the exact after_pipeline merge syntax.
Let me fetch a bit more of the page.
The documented syntax differs from mine. Let me get the exact merge pattern:
cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/tests/test-reports | python3 -c "
import sys,html,re
t=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \t]+',' ',t)
o='\n'.join(l.strip() for l in t.split('\n') if l.strip())
i=o.find('Step 3 - Merge')
print(o[i:i+1200])"Step 3 - Merge
Step 3 - Merge The final step is to merge and process all report files. This is achieved using an after-pipeline job . Editor YAML Press +Add After Jobs Type a name for the job Type the command test-results gen-pipeline-report Open the pipeline with the test jobs Add an after_pipeline key at the YAML root Add the name under task.jobs Type the merge command test-results gen-pipeline-report in the commands section .semaphore/semaphore.yml # ... after_pipeline : task : jobs : - name : Merge reports commands : - test - results gen - pipeline - report How to view test reports To view test results, navigate to your pipeline and go to the Tests tab. info The test tab is only populated for the pipelines where test reports are configured . If you don't see anything yet, try rerunning the pipeline. The default Test view shows failed tests first. Click on "Failed first" to change the sort order. You can sort by "Slowest first" or alphabetically. Click on "View" to change display preferences. You can hide passed or skipped tests and change how the report looks. See also Semaphore toolbox reference How to use the flaky tests dashboard Plan job and block execution with pipelines Overview H
Recorded tool call
{
"type": "tool_call",
"seq": 162,
"ts": "2026-10-22T11:33:06.832Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /tmp && curl -sL -m 12 https://docs.semaphore.io/using-semaphore/tests/test-reports | python3 -c \"\nimport sys,html,re\nt=sys.stdin.read(); t=re.sub(r'(?s)<(script|style|svg|nav|footer).*?</\\1>','',t); t=re.sub(r'(?s)<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'[ \\t]+',' ',t)\no='\\n'.join(l.strip() for l in t.split('\\n') if l.strip())\ni=o.find('Step 3 - Merge')\nprint(o[i:i+1200])\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8601a6fa9f7f5d0c",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 164,
"ts": "2026-10-22T11:33:07.665Z",
"turn": 1,
"output": {
"content": [
{
"text": "Step 3 - Merge \nThe final step is to merge and process all report files. This is achieved using an after-pipeline job .\nEditor YAML\nPress +Add After Jobs\nType a name for the job\nType the command test-results gen-pipeline-report\nOpen the pipeline with the test jobs\nAdd an after_pipeline key at the YAML root\nAdd the name under task.jobs\nType the merge command test-results gen-pipeline-report in the commands section\n.semaphore/semaphore.yml # ... after_pipeline : task : jobs : - name : Merge reports commands : - test - results gen - pipeline - report\nHow to view test reports \nTo view test results, navigate to your pipeline and go to the Tests tab.\ninfo The test tab is only populated for the pipelines where test reports are configured . If you don't see anything yet, try rerunning the pipeline.\nThe default Test view shows failed tests first.\nClick on \"Failed first\" to change the sort order. You can sort by \"Slowest first\" or alphabetically.\nClick on \"View\" to change display preferences. You can hide passed or skipped tests and change how the report looks.\nSee also \nSemaphore toolbox reference\nHow to use the flaky tests dashboard\nPlan job and block execution with pipelines\nOverview H\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8601a6fa9f7f5d0c",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Alright, let's modify semaphore.yml to use the documented pattern:
global_job_config.epilogue.always.commands: ['[[ -f report.xml ]] && test-results publish report.xml']after_pipeline.task.jobs: [{name: Merge reports, commands: [test-results gen-pipeline-report]}]- Remove the per-job epilogue
Now let's make the edits.
Fixing to the documented pattern:
File edits
1 file
Modified.semaphore/semaphore.yml+16−5View changes
/sandbox/repo/.semaphore/semaphore.yml
Replacement excerpt
execution_time_limit:minutes: 15+# Runs in every job, even when it fails — uploads JUnit XML to the artifact store.+global_job_config:+ epilogue:+ always:+ commands:+ - "[[ -f report.xml ]] && test-results publish report.xml"+
Replacement excerpt
- - pytest -q --junitxml=report.xml- epilogue:- always:- commands:- - test-results publish --name pytest report.xml || true+ - pytest --junitxml=report.xml
Replacement excerpt
+# Merges the per-job reports so the Tests tab is populated in the UI.+after_pipeline:+ task:+ jobs:+ - name: Merge reports+ commands:+ - test-results gen-pipeline-report+promotions:
Source: edit · chatcmpl-tool-9fd634098767955b
Recorded tool call
{
"type": "tool_call",
"seq": 168,
"ts": "2026-10-22T11:33:12.746Z",
"name": "edit",
"turn": 1,
"input": {
"path": "/sandbox/repo/.semaphore/semaphore.yml",
"edits": [
{
"newText": "execution_time_limit:\n minutes: 15\n\n# Runs in every job, even when it fails — uploads JUnit XML to the artifact store.\nglobal_job_config:\n epilogue:\n always:\n commands:\n - \"[[ -f report.xml ]] && test-results publish report.xml\"\n",
"oldText": "execution_time_limit:\n minutes: 15\n"
},
{
"newText": " - pytest --junitxml=report.xml\n",
"oldText": " - pytest -q --junitxml=report.xml\n epilogue:\n always:\n commands:\n - test-results publish --name pytest report.xml || true\n"
},
{
"newText": "# Merges the per-job reports so the Tests tab is populated in the UI.\nafter_pipeline:\n task:\n jobs:\n - name: Merge reports\n commands:\n - test-results gen-pipeline-report\n\npromotions:",
"oldText": "promotions:"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-9fd634098767955b",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 169,
"ts": "2026-10-22T11:33:12.746Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully replaced 3 block(s) in /sandbox/repo/.semaphore/semaphore.yml.",
"type": "text"
}
],
"details": {
"diff": " ...\n 9 \n 10 execution_time_limit:\n 11 minutes: 15\n 12 \n+13 # Runs in every job, even when it fails — uploads JUnit XML to the artifact store.\n+14 global_job_config:\n+15 epilogue:\n+16 always:\n+17 commands:\n+18 - \"[[ -f report.xml ]] && test-results publish report.xml\"\n+19 \n 13 blocks:\n 14 - name: Lint\n 15 task:\n 16 jobs:\n ...\n 33 - python3 -m venv .venv\n 34 - source .venv/bin/activate\n 35 - pip install -q -r requirements-dev.txt\n 36 - cache store pip-$SEMAPHORE_PROJECT_ID .venv\n-37 - pytest -q --junitxml=report.xml\n-38 epilogue:\n-39 always:\n-40 commands:\n-41 - test-results publish --name pytest report.xml || true\n+44 - pytest --junitxml=report.xml\n 42 \n+46 # Merges the per-job reports so the Tests tab is populated in the UI.\n+47 after_pipeline:\n+48 task:\n+49 jobs:\n+50 - name: Merge reports\n+51 commands:\n+52 - test-results gen-pipeline-report\n+53 \n 43 promotions:\n 44 # Only offer the deploy path on the main branch, after everything passes.\n 45 - name: Deploy\n 46 pipeline_file: .semaphore/deploy.yml\n ...",
"patch": "--- /sandbox/repo/.semaphore/semaphore.yml\n+++ /sandbox/repo/.semaphore/semaphore.yml\n@@ -9,8 +9,15 @@\n \n execution_time_limit:\n minutes: 15\n \n+# Runs in every job, even when it fails — uploads JUnit XML to the artifact store.\n+global_job_config:\n+ epilogue:\n+ always:\n+ commands:\n+ - \"[[ -f report.xml ]] && test-results publish report.xml\"\n+\n blocks:\n - name: Lint\n task:\n jobs:\n@@ -33,14 +40,18 @@\n - python3 -m venv .venv\n - source .venv/bin/activate\n - pip install -q -r requirements-dev.txt\n - cache store pip-$SEMAPHORE_PROJECT_ID .venv\n- - pytest -q --junitxml=report.xml\n- epilogue:\n- always:\n- commands:\n- - test-results publish --name pytest report.xml || true\n+ - pytest --junitxml=report.xml\n \n+# Merges the per-job reports so the Tests tab is populated in the UI.\n+after_pipeline:\n+ task:\n+ jobs:\n+ - name: Merge reports\n+ commands:\n+ - test-results gen-pipeline-report\n+\n promotions:\n # Only offer the deploy path on the main branch, after everything passes.\n - name: Deploy\n pipeline_file: .semaphore/deploy.yml\n",
"firstChangedLine": 13
}
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-9fd634098767955b",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Now, let's validate the YAML once more and check the structure. Also, addopts in pyproject already has -q, so pytest --junitxml=report.xml is fine.
Let's do the validation.
cd /sandbox/repo && python3 -c "
import yaml
d=yaml.safe_load(open('.semaphore/semaphore.yml'))
print('keys:',list(d.keys()))
print('epilogue:',d['global_job_config']['epilogue']['always']['commands'])
print('after_pipeline:',d['after_pipeline']['task']['jobs'])
print('promotions:',d['promotions'])
" && echo "== re-run tests" && . .venv/bin/activate && pytest --junitxml=/tmp/report.xml 2>&1 | tail -3keys: ['version', 'name', 'agent', 'execution_time_limit', 'global_job_config', 'blocks', 'after_pipeline', 'promotions']
keys: ['version', 'name', 'agent', 'execution_time_limit', 'global_job_config', 'blocks', 'after_pipeline', 'promotions']
epilogue: ['[[ -f report.xml ]] && test-results publish report.xml']
after_pipeline: [{'name': 'Merge reports', 'commands': ['test-results gen-pipeline-report']}]
promotions: [{'name': 'Deploy', 'pipeline_file': '.semaphore/deploy.yml', 'auto_promote_on': [{'result': 'passed', 'branch': ['main']}]}]
== re-run tests
......... [100%]
--------------------- generated xml file: /tmp/report.xml ----------------------
9 passed in 0.02s
Recorded tool call
{
"type": "tool_call",
"seq": 172,
"ts": "2026-10-22T11:33:15.540Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && python3 -c \"\nimport yaml\nd=yaml.safe_load(open('.semaphore/semaphore.yml'))\nprint('keys:',list(d.keys()))\nprint('epilogue:',d['global_job_config']['epilogue']['always']['commands'])\nprint('after_pipeline:',d['after_pipeline']['task']['jobs'])\nprint('promotions:',d['promotions'])\n\" && echo \"== re-run tests\" && . .venv/bin/activate && pytest --junitxml=/tmp/report.xml 2>&1 | tail -3"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-bf4ee872cfbb4884",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 173,
"ts": "2026-10-22T11:33:15.852Z",
"turn": 1,
"output": {
"content": [
{
"text": "keys: ['version', 'name', 'agent', 'execution_time_limit', 'global_job_config', 'blocks', 'after_pipeline', 'promotions']\nepilogue: ['[[ -f report.xml ]] && test-results publish report.xml']\nafter_pipeline: [{'name': 'Merge reports', 'commands': ['test-results gen-pipeline-report']}]\npromotions: [{'name': 'Deploy', 'pipeline_file': '.semaphore/deploy.yml', 'auto_promote_on': [{'result': 'passed', 'branch': ['main']}]}]\n== re-run tests\n......... [100%]\n--------------------- generated xml file: /tmp/report.xml ----------------------\n9 passed in 0.02s\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-bf4ee872cfbb4884",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
All good, no problems. Now, let's create a README that records the handover content and the remaining steps. Also, make sure to add .venv to gitignore (already there? .venv/ is listed — yes).
Let's write a README.md that clearly documents the following:
- Contents of the repository
- How it corresponds to Semaphore
- The two blocked steps and the exact commands to complete them
Then commit? The repository has a git history ("Initial workspace"). Committing is reasonable, but pushing is not possible (no remote). Let's commit to get things into a clean state. Actually — should I commit? It's useful. But the user might want to review first. To keep things tidy, let's commit and tell them so. Hmm, let's play it safe: stage + commit is reversible and useful. Let's do it.
First let's write the README.
Then re-check the sign-in process — it's probably expired by now (device codes usually last about 5–15 minutes). Let's check and restart if necessary to provide a valid code. Actually, let's check the status and restart to get a new code at the end. Because the old code might be expired.
Let's write the README.
Now a README documenting the handoff:
File edits
1 file
WrittenREADME.mdView changes
/sandbox/repo/README.md
Written content · previous contents were not recorded
# Semaphore CI exampleA minimal Python project wired up for **Semaphore Cloud** (SaaS). It lints, tests,publishes JUnit test reports, and auto-promotes to a deploy pipeline on `main`.## Layout```greeter/__init__.py # the library under testtests/test_greeter.py # 9 pytest casespyproject.toml # pytest + ruff configrequirements-dev.txt # pytest, ruff.semaphore/semaphore.yml # main pipeline: Lint -> Test, then Merge reports.semaphore/deploy.yml # promotion target (placeholder deploy step)```## Pipeline shape| Stage | What runs | Depends on ||---|---|---|| `Lint` | `ruff check .` | — || `Test` | `pytest --junitxml=report.xml`, restores/stores a pip cache | `Lint` || `after_pipeline` | `test-results gen-pipeline-report` (merges reports) | all blocks || `Deploy` promotion | `.semaphore/deploy.yml` | passed + branch `main` |Agent is `f1-standard-2` (Ubuntu 24.04 x64, 2 vCPU / 8 GB) at **$0.0075/min**.A full run is roughly 2 jobs x ~1 min, well inside the $15/month free credit.## Verify locally```bashpython3 -m venv .venv && . .venv/bin/activatepip install -r requirements-dev.txtruff check . && pytest # -> 9 passed```## Remaining setup (needs a human + a browser)Semaphore Cloud builds code from a **hosted GitHub/Bitbucket repo**, so two stepscannot be done headless:1. **Create an account and get an API token.** Run `sem-ai signin --headless`,then open the printed URL, enter the one-time code, sign up, and approve.The token lands in `~/.sem.yaml`.2. **Push this repo to GitHub (or Bitbucket)**, then create the project:```bashgit remote add origin git@github.com:<you>/<repo>.gitgit push -u origin mainsem-ai project create # uses the origin remote, bootstraps YAML```After that, `sem-ai workflow list` / `sem-ai diagnose` drive everything from the CLI.
Source: write · chatcmpl-tool-a4b2c1b860a0a418
Recorded tool call
{
"type": "tool_call",
"seq": 177,
"ts": "2026-10-22T11:33:26.817Z",
"name": "write",
"turn": 1,
"input": {
"path": "/sandbox/repo/README.md",
"content": "# Semaphore CI example\n\nA minimal Python project wired up for **Semaphore Cloud** (SaaS). It lints, tests,\npublishes JUnit test reports, and auto-promotes to a deploy pipeline on `main`.\n\n## Layout\n\n```\ngreeter/__init__.py # the library under test\ntests/test_greeter.py # 9 pytest cases\npyproject.toml # pytest + ruff config\nrequirements-dev.txt # pytest, ruff\n.semaphore/semaphore.yml # main pipeline: Lint -> Test, then Merge reports\n.semaphore/deploy.yml # promotion target (placeholder deploy step)\n```\n\n## Pipeline shape\n\n| Stage | What runs | Depends on |\n|---|---|---|\n| `Lint` | `ruff check .` | — |\n| `Test` | `pytest --junitxml=report.xml`, restores/stores a pip cache | `Lint` |\n| `after_pipeline` | `test-results gen-pipeline-report` (merges reports) | all blocks |\n| `Deploy` promotion | `.semaphore/deploy.yml` | passed + branch `main` |\n\nAgent is `f1-standard-2` (Ubuntu 24.04 x64, 2 vCPU / 8 GB) at **$0.0075/min**.\nA full run is roughly 2 jobs x ~1 min, well inside the $15/month free credit.\n\n## Verify locally\n\n```bash\npython3 -m venv .venv && . .venv/bin/activate\npip install -r requirements-dev.txt\nruff check . && pytest # -> 9 passed\n```\n\n## Remaining setup (needs a human + a browser)\n\nSemaphore Cloud builds code from a **hosted GitHub/Bitbucket repo**, so two steps\ncannot be done headless:\n\n1. **Create an account and get an API token.** Run `sem-ai signin --headless`,\n then open the printed URL, enter the one-time code, sign up, and approve.\n The token lands in `~/.sem.yaml`.\n2. **Push this repo to GitHub (or Bitbucket)**, then create the project:\n\n ```bash\n git remote add origin git@github.com:<you>/<repo>.git\n git push -u origin main\n sem-ai project create # uses the origin remote, bootstraps YAML\n ```\n\nAfter that, `sem-ai workflow list` / `sem-ai diagnose` drive everything from the CLI.\n"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a4b2c1b860a0a418",
"tool_kind": "file_edit"
}Recorded tool result
{
"type": "tool_result",
"seq": 178,
"ts": "2026-10-22T11:33:26.817Z",
"turn": 1,
"output": {
"content": [
{
"text": "Successfully wrote 1896 bytes to /sandbox/repo/README.md",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a4b2c1b860a0a418",
"is_error": false,
"tool_kind": "file_edit"
}›Thinking…
Next, let's terminate the stale sign-in (the code has likely expired), start a new sign-in, and provide the user with a valid code. Let me check the elapsed time. Started around 11:31, and it's probably a few minutes later now. Device codes typically expire in 5-15 minutes. Let's terminate it and restart to get a new code.
Also, let's commit the work.
Let's do both.
Let me commit, and refresh the device code (the earlier one likely expired):
cd /sandbox/repo && git add -A && git -c user.email=agent@local -c user.name=agent commit -q -m "Add Semaphore CI example: lint/test pipeline, test reports, deploy promotion" && git log --oneline -3 && echo "--- files" && git ls-files
a126a18 Add Semaphore CI example: lint/test pipeline, test reports, deploy promotion
a126a18 Add Semaphore CI example: lint/test pipeline, test reports, deploy promotion 9ebf5fb Initial workspace --- files .gitignore .semaphore/deploy.yml .semaphore/semaphore.yml README.md greeter/__init__.py pyproject.toml requirements-dev.txt tests/test_greeter.py
Recorded tool call
{
"type": "tool_call",
"seq": 182,
"ts": "2026-10-22T11:33:31.161Z",
"name": "bash",
"turn": 1,
"input": {
"command": "cd /sandbox/repo && git add -A && git -c user.email=agent@local -c user.name=agent commit -q -m \"Add Semaphore CI example: lint/test pipeline, test reports, deploy promotion\" && git log --oneline -3 && echo \"--- files\" && git ls-files"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-87090b54f33c68be",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 185,
"ts": "2026-10-22T11:33:31.161Z",
"turn": 1,
"output": {
"content": [
{
"text": "a126a18 Add Semaphore CI example: lint/test pipeline, test reports, deploy promotion\n9ebf5fb Initial workspace\n--- files\n.gitignore\n.semaphore/deploy.yml\n.semaphore/semaphore.yml\nREADME.md\ngreeter/__init__.py\npyproject.toml\nrequirements-dev.txt\ntests/test_greeter.py\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-87090b54f33c68be",
"is_error": false,
"tool_kind": "shell"
}pkill -f "sem-ai signin" 2>/dev/null; sleep 1; export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && rm -f signin2.log && nohup sem-ai signin --headless --org semaphore-demo > signin2.log 2>&1 & sleep 12; cat /tmp/signin2.log | tail -8
(no output)
(no output)
Recorded tool call
{
"type": "tool_call",
"seq": 183,
"ts": "2026-10-22T11:33:31.161Z",
"name": "bash",
"turn": 1,
"input": {
"command": "pkill -f \"sem-ai signin\" 2>/dev/null; sleep 1; export PATH=\"$HOME/.semaphore-ai/bin:$PATH\"; cd /tmp && rm -f signin2.log && nohup sem-ai signin --headless --org semaphore-demo > signin2.log 2>&1 & sleep 12; cat /tmp/signin2.log | tail -8"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-ac23a146ece6644d",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 184,
"ts": "2026-10-22T11:33:31.161Z",
"turn": 1,
"output": {
"content": [
{
"text": "(no output)",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-ac23a146ece6644d",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The second sign-in didn't produce any output. Let me check.
sleep 5; echo "--- log:"; cat /tmp/signin2.log; echo "--- proc:"; pgrep -f "sem-ai signin" >/dev/null && echo alive || echo "exited"
--- log:
--- log: cat: /tmp/signin2.log: No such file or directory --- proc: alive
Recorded tool call
{
"type": "tool_call",
"seq": 188,
"ts": "2026-10-22T11:33:32.815Z",
"name": "bash",
"turn": 1,
"input": {
"command": "sleep 5; echo \"--- log:\"; cat /tmp/signin2.log; echo \"--- proc:\"; pgrep -f \"sem-ai signin\" >/dev/null && echo alive || echo \"exited\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b1d1af5afd5d8a4b",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 189,
"ts": "2026-10-22T11:33:38.327Z",
"turn": 1,
"output": {
"content": [
{
"text": "--- log:\ncat: /tmp/signin2.log: No such file or directory\n--- proc:\nalive\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-b1d1af5afd5d8a4b",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
Weird — the log file doesn't exist, but the process is alive. Maybe pkill killed the shell, or the nohup redirection raced. Or possibly the background job inherited a different cwd. Let me check where it is. Actually, cd /tmp && rm -f signin2.log && nohup ... > signin2.log — rm was executed, then nohup created it. But the file doesn't exist? Maybe pkill killed the entire background subshell including the redirection before creation, and the surviving process is from the first invocation that got re-executed? Let me investigate more carefully.
Let me look for the actual process and its output.
ps aux | grep -i "sem-ai" | grep -v grep; echo "--- tmp files"; ls -la /tmp/*.log /tmp/nohup.out 2>/dev/null; echo "--- sem.yaml"; cat ~/.sem.yaml 2>/dev/null; echo "(end)"
--- tmp files
--- tmp files -rw------- 1 root root 404 Oct 22 11:31 /tmp/signin.log --- sem.yaml (end)
Recorded tool call
{
"type": "tool_call",
"seq": 192,
"ts": "2026-10-22T11:33:41.351Z",
"name": "bash",
"turn": 1,
"input": {
"command": "ps aux | grep -i \"sem-ai\" | grep -v grep; echo \"--- tmp files\"; ls -la /tmp/*.log /tmp/nohup.out 2>/dev/null; echo \"--- sem.yaml\"; cat ~/.sem.yaml 2>/dev/null; echo \"(end)\""
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8bb3b17699110399",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 193,
"ts": "2026-10-22T11:33:41.351Z",
"turn": 1,
"output": {
"content": [
{
"text": "--- tmp files\n-rw------- 1 root root 404 Oct 22 11:31 /tmp/signin.log\n--- sem.yaml\n(end)\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-8bb3b17699110399",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
The process doesn't actually exist (was the earlier "alive" a false positive from pgrep matching pgrep's own pipeline? No wait…). Anyway, currently no sem-ai process is running, and no new log has been created either. It seems pkill killed it, and either the restart silently failed, or the background job got killed at the point when the bash tool invocation ended (each bash invocation may terminate its child processes).
Ah — that's the key insight: background processes started in a single bash tool invocation are likely to be killed when that invocation returns. The reason the first one survived was… hmm, actually, the first one survived across invocations (I checked "STILL WAITING" in a later invocation). So background processes do survive.
But the second one didn't. It's possible that pkill -f "sem-ai signin" matched, and then the new process got killed at startup… or there's a chance the pattern sem-ai signin in pkill also matched the command string of the new process launched in the same command line and killed it. Actually, no, pkill runs before the startup.
Wait — nohup sem-ai signin --headless --org semaphore-demo > signin2.log — the file wasn't created. Which means the shell redirection didn't happen, i.e., the command wasn't executed. Perhaps pkill returned nonzero… no, we're not using set -e.
Hmm, could sleep 12; cat /tmp/signin2.log | tail -8 have produced no output because the file was empty at that point and tail output nothing? No — cat says "No such file".
Let me retry cleanly with setsid and absolute paths.
Note: --org semaphore-demo may conflict with the org name. Let me retry with a unique org name.
I'll do a clean retry.
export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && setsid nohup sem-ai signin --headless --org semaphore-agent-demo > /tmp/signin3.log 2>&1 < /dev/null & disown; sleep 15; echo "--- log:"; cat /tmp/signin3.log; echo "--- proc:"; ps aux | grep sem-ai | grep -v grep | head -3
--- log:
--- log:
{
"code": "signin_error",
"error": true,
"message": "--org requires --org-host: the create API does not return the new org's host, so it must be given explicitly",
"status": 1
}
--- proc:
Recorded tool call
{
"type": "tool_call",
"seq": 196,
"ts": "2026-10-22T11:33:47.845Z",
"name": "bash",
"turn": 1,
"input": {
"command": "export PATH=\"$HOME/.semaphore-ai/bin:$PATH\"; cd /tmp && setsid nohup sem-ai signin --headless --org semaphore-agent-demo > /tmp/signin3.log 2>&1 < /dev/null & disown; sleep 15; echo \"--- log:\"; cat /tmp/signin3.log; echo \"--- proc:\"; ps aux | grep sem-ai | grep -v grep | head -3"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a12074669d5d8554",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 197,
"ts": "2026-10-22T11:34:03.734Z",
"turn": 1,
"output": {
"content": [
{
"text": "--- log:\n{\n \"code\": \"signin_error\",\n \"error\": true,\n \"message\": \"--org requires --org-host: the create API does not return the new org's host, so it must be given explicitly\",\n \"status\": 1\n}\n--- proc:\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-a12074669d5d8554",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
--org requires --org-host. Let's provide --org-host semaphore-agent-demo.semaphoreci.com. Retrying.
Nice — structured JSON error, very agent-friendly. Worth mentioning.
Structured JSON error (nice). --org needs --org-host:
export PATH="$HOME/.semaphore-ai/bin:$PATH"; cd /tmp && setsid nohup sem-ai signin --headless --org semaphore-agent-demo --org-host semaphore-agent-demo.semaphoreci.com > /tmp/signin4.log 2>&1 < /dev/null & disown; sleep 15; cat /tmp/signin4.log; echo "--- proc:"; ps aux|grep sem-ai|grep -v grep|head -2
Heads up: if your account already has an API token, completing this sign-in
Heads up: if your account already has an API token, completing this sign-in
lets you reset it (you confirm in the browser first). A reset token stops
working everywhere it is used: CI secrets, scripts, other machines.
First, copy your one-time code:
TRHR-RLCG
Open https://id.semaphoreci.com/device in a browser and enter the code.
Sign in there (or create your account), then approve. Waiting...
--- proc:
root 6742 0.2 0.1 1237512 15732 ? Ssl 11:34 0:00 sem-ai signin --headless --org semaphore-agent-demo --org-host semaphore-agent-demo.semaphoreci.com
Recorded tool call
{
"type": "tool_call",
"seq": 201,
"ts": "2026-10-22T11:34:06.185Z",
"name": "bash",
"turn": 1,
"input": {
"command": "export PATH=\"$HOME/.semaphore-ai/bin:$PATH\"; cd /tmp && setsid nohup sem-ai signin --headless --org semaphore-agent-demo --org-host semaphore-agent-demo.semaphoreci.com > /tmp/signin4.log 2>&1 < /dev/null & disown; sleep 15; cat /tmp/signin4.log; echo \"--- proc:\"; ps aux|grep sem-ai|grep -v grep|head -2"
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-98ac15bde1ddab39",
"tool_kind": "shell"
}Recorded tool result
{
"type": "tool_result",
"seq": 202,
"ts": "2026-10-22T11:34:21.706Z",
"turn": 1,
"output": {
"content": [
{
"text": "Heads up: if your account already has an API token, completing this sign-in\nlets you reset it (you confirm in the browser first). A reset token stops\nworking everywhere it is used: CI secrets, scripts, other machines.\n\nFirst, copy your one-time code:\n\n TRHR-RLCG\n\nOpen https://id.semaphoreci.com/device in a browser and enter the code.\nSign in there (or create your account), then approve. Waiting...\n--- proc:\nroot 6742 0.2 0.1 1237512 15732 ? Ssl 11:34 0:00 sem-ai signin --headless --org semaphore-agent-demo --org-host semaphore-agent-demo.semaphoreci.com\n",
"type": "text"
}
]
},
"run_id": "cmvjglyq300lb0jusacvf64rx",
"tool_id": "chatcmpl-tool-98ac15bde1ddab39",
"is_error": false,
"tool_kind": "shell"
}›Thinking…
A new valid code is active: TRHR-RLCG, and the process is running.
Alright, let me write up the final response:
- What I built (samples, verified locally)
- Blockers (2 steps that only a human can perform)
- Pricing
- Ease-of-management assessment
Let me confirm that the final state of semaphore.yml displays cleanly for the response, and do a final sanity check.
Let me read the final file.
✓read /sandbox/repo/.semaphore/semaphore.yml✓└{"content":[{"text":"version: v1.0\nname: Greeter CI\n\nagent:\n machine:\n # Ubuntu x64, 2 vCPU / 8 GB — billed at $0.0075 per minute.\n type: f1-standa…ctrl+o
{"content":[{"text":"version: v1.0\nname: Greeter CI\n\nagent:\n machine:\n # Ubuntu x64, 2 vCPU / 8 GB — billed at $0.0075 per minute.\n type: f1-standard-2\n os_image: ubuntu2404\n\nexecution_time_limit:\n minutes: 15\n\n# Runs in every job, even when it fails — uploads JUnit XML to the artifact store.\nglobal_job_config:\n epilogue:\n always:\n commands:\n - \"[[ -f report.xml ]] && test-results publish report.xml\"\n\nblocks:\n - name: Lint\n task:\n jobs:\n - name: ruff\n commands:\n - checkout\n - python3 -m venv .venv\n - source .venv/bin/activate\n - pip install -q -r requirements-dev.txt\n - ruff check .\n\n - name: Test\n dependencies: [Lint]\n task:\n jobs:\n - name: pytest\n commands:\n - checkout\n - cache restore pip-$SEMAPHORE_PROJECT_ID || true\n - python3 -m venv .venv\n - source .venv/bin/activate\n - pip install -q -r requirements-dev.txt\n - cache store pip-$SEMAPHORE_PROJECT_ID .venv\n - pytest --junitxml=report.xml\n\n# Merges the per-job reports so the Tests tab is populated in the UI.\nafter_pipeline:\n task:\n jobs:\n - name: Merge reports\n commands:\n - test-results gen-pipeline-report\n\npromotions:\n # Only offer the deploy path on the main branch, after everything passes.\n - name: Deploy\n pipeline_file: .semaphore/deploy.yml\n auto_promote_on:\n - result: passed\n branch:\n - main\n","type":"text"}]}›Thinking…
Everything's going smoothly. Now, let me write the final answer.
To summarize:
What I built — committed to /sandbox/repo:
- greeter library + 9 pytest tests
- pyproject.toml (pytest/ruff config)
- requirements-dev.txt
- .semaphore/semaphore.yml (Lint -> Test, epilogue for test reports, merging in after_pipeline, deploy promotion on main)
- .semaphore/deploy.yml
- README.md
- Verified: ruff clean, 9/9 tests pass, both YAMLs parse successfully
Found and fixed one real bug: without pyproject.toml, pytest couldn't resolve greeter — it would have failed in CI too.
Where I got stuck — two steps that only a human can do:
- No Semaphore account/token. The API (
me.semaphoreci.com,<org>.semaphoreci.com/api/v1alpha) and MCP (mcp.semaphoreci.com/mcp) both return 401 without a token. There's no way to create an account headlessly. I kicked off a device flow:- Code: TRHR-RLCG
- URL: https://id.semaphoreci.com/device
- Also creating an org named
semaphore-agent-demo - Process is running and waiting
- Semaphore Cloud builds from a hosted GitHub/Bitbucket repository. /sandbox/repo has no remote, and there are no GitHub credentials/gh CLI in this environment. So I can't create a project.
Note: the quickstart didn't require a local service stack, so that wasn't a reason to stop. I stayed within the hosted API/CLI. The blockers are auth + hosted git repo, both of which are human-handoff items, not infrastructure issues.
Worth noting: Semaphore also has a self-hosted Community Edition that requires a local stack — I didn't use that one.
Pricing — usage-based, compute separate from support:
- $15/month free credit (about 2000 minutes of Ubuntu x64 2 vCPU). No credit card required.
- Ubuntu x64: $0.0075/min (2 vCPU), $0.015 (4 vCPU)
- Ubuntu ARM: $0.003/min (2 vCPU), $0.006 (4), $0.012 (8)
- macOS: $0.09/min (4 vCPU)
- Self-hosted agents: $0.0025/min regardless of machine
- Concurrency: 20 jobs included, more on request
- Artifacts: free up to 20 GB/month egress + 100 GB storage; then $0.35/GB egress, $0.07/GB storage
- Cache: 9.6 GB per project included
- Optional add-ons: dedicated cache server $400/month, private Docker registry $400/month
- Support is separated out and modular: Basic $50/month, Priority $250, SLA Standard $500, SLA Premium $750
- MCP server: free, but requires an email to support@semaphore.io to enable
For this example: about 2 jobs × ~1 min on f1-standard-2 ≈ $0.015 per run — easily fits within the free tier.
Is it easy for me to operate? — honest assessment: pretty easy, better than most CIs. Pros:
sem-aiis explicitly agent-first:sem-ai discoverreturns a machine-readable capability map, all commands emit structured JSON by default, and every command has--examples. This is a rare thing.- Errors are structured JSON with clear messages (I hit one:
--org requires --org-host). - There's an MCP server, plus
sem-ai mcpstdio mode. sem-ai diagnosedoes root-cause analysis of failures; flaky test detection; analytics.- Context pinning via
--context/SEM_CONTEXTis designed for concurrent agents — docs explicitly warn about shared~/.sem.yamllast-writer-wins, which shows they've thought about multi-agent operation. - Pipelines are just YAML in the repo, so the normal read/edit/commit tools work.
Friction points:
- The initial auth requires a browser (device flow) — not fully headless.
- Requires a hosted GitHub/Bitbucket repo; can't build a local-only repo.
- MCP server write tools (
workflow_run,workflow_rerun) require an email to support to enable — a human gate. - Docs URLs have moved (docs.semaphoreci.com → docs.semaphore.io); several links 404, so I had to discover paths from the sitemap.
So: day-to-day operations = easy and pleasant. Onboarding = needs a human once.
Let me keep it reasonably concise and complete. I should present the code prominently.
Let me confirm the code is still valid — the process is running. Good.
I'll write the final answer.
Done with everything I can do headless. Here's where things stand.
What I built (committed in /sandbox/repo)
greeter/__init__.py # small library under test
tests/test_greeter.py # 9 pytest cases
pyproject.toml # pytest + ruff config
requirements-dev.txt
.semaphore/semaphore.yml # Lint -> Test, test reports, deploy promotion
.semaphore/deploy.yml # promotion target
README.md
The pipeline: Lint (ruff) → Test (pytest + pip cache) → after_pipeline merges JUnit reports → Deploy promotion auto-fires on passed main. Agent is f1-standard-2 (Ubuntu 24.04 x64).
Verified locally: ruff clean, 9/9 tests pass, both YAML files parse. I caught one real bug doing this — pytest couldn't resolve the greeter module, which would have failed in CI too; fixed via pythonpath in pyproject.toml. I also corrected my test-results syntax against the docs (canonical form is [[ -f report.xml ]] && test-results publish report.xml).
Where I'm blocked — two human-only steps
The quickstart did not need a local service stack, so that wasn't a stop reason. I stayed on the hosted API/CLI. But:
-
No account/token. Both the REST API (
<org>.semaphoreci.com/api/v1alpha,me.semaphoreci.com) and the MCP server (mcp.semaphoreci.com/mcp) return 401. There's no headless signup. I started a device flow that's live and waiting:Code
TRHR-RLCG→ https://id.semaphoreci.com/device Approving also creates an org namedsemaphore-agent-demo. -
No hosted git repo. Semaphore Cloud builds from GitHub/Bitbucket.
/sandbox/repohas no remote, and this environment has no GitHub credentials orghCLI. Once you push,sem-ai project createfinishes the wiring.
Pricing
Usage-based, with compute deliberately unbundled from support. $15 free credits/month (≈2,000 Ubuntu x64 2-vCPU minutes), no credit card.
| Compute | per minute |
|---|---|
| Ubuntu ARM | $0.003 (2 vCPU) / $0.006 (4) / $0.012 (8) |
| Ubuntu x64 | $0.0075 (2 vCPU) / $0.015 (4) |
| macOS | $0.09 (4 vCPU) |
| Self-hosted | $0.0025, any machine |
20 concurrent jobs included. Artifacts free to 20 GB/mo egress + 100 GB storage, then $0.35/GB and $0.07/GB. 9.6 GB cache per project. Optional add-ons: dedicated cache server $400/mo, private Docker registry $400/mo. Support is separate and modular — $50 / $250 / $500 / $750 per month. MCP server is free but must be enabled by emailing support.
This example costs roughly $0.015 per run — comfortably inside the free tier.
Will this be easy for me to manage?
Day-to-day, yes — unusually easy. Onboarding, no — it needs you once.
sem-ai is genuinely agent-first, which is rare: sem-ai discover returns a machine-readable capability map, every command defaults to structured JSON, and every command has --examples. Errors come back as structured JSON with actionable messages — I hit one (--org requires --org-host) and it told me exactly what to fix. It has diagnose for failure root-cause, flaky-test detection, and analytics. The docs even warn about ~/.sem.yaml last-writer-wins under concurrent agents and give --context pinning to isolate sessions — they've clearly thought about multi-agent use. Pipelines are plain YAML in-repo, so my normal read/edit/commit flow applies directly.
The friction is all at the edges: first auth needs a browser, a hosted git repo is mandatory, and the MCP write tools (workflow_run, workflow_rerun) require an email to support to unlock. Docs also moved from docs.semaphoreci.com to docs.semaphore.io with several 404s, so I had to recover paths from the sitemap.
One note: Semaphore also ships a self-hosted Community Edition whose quickstart does require standing up a local stack. I avoided that and used the hosted Cloud product, per your instruction.
AX Fundamentals
23 checks. Open an area for details.
This check is a quick preview. Test more tasks and models with Gauge Agents.
Test your product with Gauge Agents