task-orphan-check
Resolve orphan tasks left in `<workspace>/tasks/` from a previous session that crashed mid-execution.
Skill by sonichi
Chalk Labs
Need help deploying task-orphan-check?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/sonichi-task-orphan-check
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: productivity
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
- Runs as
- Python package
Network hosts it talks to 1
codeload.github.com· found in install command
Files it touches
~/.claude/skills/task-orphan-check
What's inside
- Skill name
task-orphan-check- Description
- Resolve orphan tasks left in `<workspace>/tasks/` from a previous session that crashed mid-execution. Classifies each live task as done / fresh / stale by cross-referencing per-side-effect markers, then archives or recovers as appropriate. Runs once on startup; safe to re-invoke.
- License
- MIT
- Instructions
- 268 lines in SKILL.md · view on GitHub
Before installing
- Ships executable scripts
Agent Skills spec issues
- files: referenced but missing: scripts/unanswered-tasks.py
Files (3)
- SKILL.md
- manifest.json
- classify.py · scripts/
SKILL.md (first lines)
# Task orphan check
Recovery half of the post-#1049 task-bridge redesign. Replaces the brittle attempts-counter (#1049 + #1066's followup) with a startup-time classification pass that uses existing side-effect markers (PR #1048's `.sending` files for Discord, result files in `results/`, archive presence) to decide what to do with each live task in `<workspace>/tasks/`.
**Usage**: `/task-orphan-check`
Designed to be invoked from `/startup` (PR #1072) as step 1, before `/schedule-crons` starts the task watcher. Also callable standalone for manual recovery.
## Why this exists
If the agent crashes mid-task with non-idempotent side effects already executed (Discord message sent, file written, API call made) but the archive of result + task files never ran, on restart the task file is still in `tasks/`. The watcher re-emits it. The agent re-processes. The side effect fires a second time.
PR #1049 tried to solve this with an `attempts: N` counter inside the task file — but the bumper-write fired the watcher's own `Renamed` event, creating an infinite self-trigger loop. PR #1066 tried to patch the loop by switching to in-place writes — but on macOS, `open(file, 'w')` STILL fires the `Created` event because `O_WRONLY|O_CREAT|O_TRUNC` flips the ItemCreated bit. Both PRs are working around the wrong layer.
This skill moves the dedup logic out of the watcher's event surface entirely. The agent does a single classification pass at startup, cross-references markers that already exist (PR #1048 ships them for Discord delivery; result files in `results/` mark "this task was completed"), and decides per-task what to do. No counter, no in-band writes, no self-trigger loop.
## On Activation
The procedure below is non-LLM where possible — mechanical file checks + side-effect marker reads. The LLM-judgment parts are bounded (per-task classification with explicit decision rules).
### Step 1 — List live tasks
```bash
WS="$(bash scripts/sutando-config.sh workspace)"
ls "$WS/tasks/"task-*.txt 2>/dev/null | head -200
```
If no live tasks, emit "orphan-check: no live tasks, nothing to recover" and idle.
### Step 2 — Classify each task
**Run the classifier; do not re-derive the verdicts by hand:**
```bash
python3 skills/task-orphan-check/scripts/classify.py # add --workspace DIR to override
```
It prints one JSON object — `tasks[]` with `id`, `source`, `access_tier`, `channel_id`, `label` (the readable channel label step 3b uses), `age_s`, `import` (with `import_intent`; `import_task_id` — the `task_id` the current `status.json` carries, `null` when it has none; and for a bound or unbound run `import_phase` / `import_idle_s`), `verdict` (`done` / `held` / `worker-held` / `unknown` / `fresh` / `orphan` / `import-resume` / `import-stalled` / `import-unbound`; a `worker-held` row also carries `holder`, the recipient id) and a `reason` naming the marker, sentinel or age line it matched — plus `counts`. Read-only: it moves and writes nothing; step 3 acts on its verdicts. The rules it encodes are the ones below (pinned by `tests/task-orphan-check-classify.test.py`); if you change a rule, change the script and its test, then this prose.
For each file in `tasks/`, let `<id>` be the value of the `id:` header line (e.g. `task-1779570142563`). The file is `tasks/<id>.txt`. Per-task paths below use `<id>` consistently — note `<id>` already includes the `task-` prefix; do NOT add it again.
1. **Parse the header** — extract `id`, `timestamp`, `source`, `channel_id` (Discord channel or ag2.space room id — both producers supply it, and the later `contextNotFrom`/requeue workflow needs the exact id, so never discard it in favor of the friendly name), `chat_id` (Telegram), `room_name` / `channel_name` (whichever the surface carries — ag2.space sends `room_name` and no `channel_name`, so a reader that parses only the latter gets nothing and step 3b falls back to the raw id), `user_id`, `access_tier` (`owner` / `team` / `other`; default to `owner` if the field is absent — pre-tier task files predate the field and were authored by the owner). The classifier reads them with `local_task_protocol.parse_task_headers_lenient` — the shape-union parser — because this pass sees files from every writer and era: the desktop's legacy Claude-import writer is task-mid (`task:` before `channel_id:`), and the strict task-last parser would read its `channel_id` and `priority` as absent.
2. **Cross-reference completion markers** (any single match = task already completed):
- **`<workspace>/results/<id>.txt`** exists → **DONE**. The result file is the canonical completion marker; if it exists the task was processed.
- **`<workspace>/results/archive/<id>.txt`** exists → **DONE** (post-archive case).
- **`<workspace>/results/proactive-<id>.txt`** OR `.sending` variant exists → see step 2b below for the in-progress-vs-done split.
**Step 2b — `.sending` contract clarification** (per qingyun-sutando review of #1074):
- `results/<id>.txt` (no suffix) → task completed AND result body written. **DONE.**
- `results/proactive-<id>.txt[.sending]` → the bridge claimed the proactive DM by rename and is mid-delivery. Treat as **DONE** for orphan-check purposes — the bridge owns post-crash recovery via its own startup `.sending` sweep, so we don't second-guess. Read-only either way.
- **`results/<id>.txt.sending` (a TASK result) does not occur — do not classify on it.** Every claim-by-rename site gates on the proactive family *before* applying the suffix, so no `task-*` result is ever renamed:
| site | gate applied before `.sending` |
|---|---|
| `src/discord-bridge.py` claim loop | `f.name.startswith("proactive-")` |
| `src/slack-bridge.py` claim loop | `f.name.startswith("proactive-")` |
| `src/telegram-bridge.py` claim loop | `PROACTIVE_PREFIXES` = `("proactive-", "briefing-", "insight-", "friction-")` |
This line previously described the task form as a live mid-delivery state. It is a dead branch, and not a harmless one: on 2026-08-02 it was cited as a real completion namespace while reviewing #2525, which would have added handling for a case that cannot arise. `tests/sending-suffix-is-proactive-only.test.py` pins the invariant; if a future change *does* start claiming task results by rename, that test fails and this row must be restored **with the producing site named**.
**Step 2c — a task a worker holds is not the core's orphan.** The pool router delegates a task by writing a sentinel `deliveries/<recipient>/<id>{.txt,.accepted,.claimed}`; the payload stays in `tasks/`, where the recipient reads it. Once ANY worker holds a sentinel — even an unaccepted one — the task is that worker's to answer, and the core's age line says nothing about it: on 2026-09-18 the boot pass archived four owner DMs held by two worker seats (6–26 min old, no result yet because the workers were mid-answer) and reported them to the owner as unanswered. The predicate is `skills/worker-pool/scripts/worker_delivery.py:holder_of` (the worker-pool skill is optional: absent skill ⇒ no router ⇒ nobody holds anything) — the same one the Stop hook (`src/check-pending-tasks.sh`) and `scripts/unanswered-tasks.py` apply — never a local re-scan of `deliveries/`. It answers `None` only when the directory or sentinel is genuinely absent (ENOENT); any other stat failure propagates, and the classifier turns that into **UNKNOWN**, because reading an unreadable `deliveries/` as "nobody holds it" is exactly what hands a worker's task to the core. A completion marker (step 2) still wins: a held task with a result is done.
3. **Compute age** — use the IMMUTABLE arrival time, NOT file mtime (mtime gets reset by rsync, `git checkout`, `touch`, or workspace sync, which would make a genuinely old orphan look FRESH and re-fire its side effect — exactly the bug this skill exists to prevent):
- Preferred: the `local_time:` header's ISO value (the first token; the IANA zone after it is display only) → `task_age_s = now - parse(local_time)`. The local writer stamps it at intake (task-bridge.ts from the timestamp it has just taken; a requeued task keeps the original's), so it is when the task entered THIS inbox. Only a `local_time:` above `task:` counts (the strict parse): for a writer that never stamps it, a `local_time:` line in the user's body is not a header and is ignored. `timestamp:` is when the sender sent it: a message held at the broker while the app was down for more than 5 min arrives with an old `timestamp:`, and aging from it archived fresh tasks unanswered on relaunch (2026-10-26).
- Next: the header `timestamp:` ISO field → `task_age_s = now - parse(timestamp)`. (status-ack's deadline, `status_ack_policy.intake_at`, deliberately stays on `timestamp:`: it measures how long the sender has waited.)
- Fallback: extract epoch-ms from the id (id format is `task-<epoch-ms>`) → `task_age_s = now - (epoch_ms/1000)`.
- Last resort only if both unparseable: `task_age_s = now - mtime(tasks/<id>.txt)`.
- If <300s (5 min) → FRESH (genuinely just arrived; watcher will pick it up normally).
- Else → ORPHAN (no completion marker AND old enough to be from a previous session).
**Step 3a — consented Claude-Code import tasks are not orphans at 5 minutes.** A task is an *import task* when it is owner-tier and carries a **run intent**: the header `channel_id: onboarding-wizard` (the desktop's legacy Import-button writer, `claude_import.rs`); the slash command `/import-claude-context` as a standalone token; or one of the documented trigger sentences, case-insensitive — "import my Claude history", "import my Claude Code history" (the onboarding DM message and the Settings sentence both contain it), "read my Claude Code sessions", "bring my Claude context along". A path or a bare skill-name mention is **not** an intent: an owner review task reading "Review PR 4177 which touches skills/import-claude-context/SKILL.md" classifies like any other task (orphan at 5 min, archived with a re-queue line) — the first cut matched the bare substring and would have parked that task in `tasks/` forever, invisible to the recovery DM (#4177 review). The import is consented, idempotent and resumable (`skills/import-claude-context/SKILL.md`), and its main task is expected to take seconds — but on 2026-09-11 (desktop v0.6.8-rc3, engine 4b02fbaf) the core answered the greeting, ran its boot recap and went idle without starting it, so at 322 s the prose rule above would have archived a task the owner had just consented to and posted it in the recovery DM instead of letting the watcher's startup sweep re-emit it. Its marker is `<workspace>/data/claude-import/status.json` — written by `index.py` in the same seconds as the acknowledgement DM (`results/proactive-<ts>.txt`, which the bridge claims and archives, so it cannot serve as a marker). **It is read as *this task's* only by identity, never by timestamp:** `status.json` is one global file, and every phase the import scripts write carries the `task_id` the run was started with (`index.py --task-id <id>` — `skills/import-claude-context/SKILL.md` step 1 — stores `run: {task_id, run_id}` in `state.json`, and `_common.write_status` stamps both onto every status). A status is **bound** to a task when `status.task_id == <id>`. A status bound to a *different* task is, for this task, the same as no status (#4177 review: an older run A ending after a genuine new request B was queued satisfied "newer than the task" and B was archived as done without ever executing — the `absent` row classified `orphan` correctly, the timestamp was never a receipt). A status with **no** `task_id` (a pre-#4177 writer, or `index.py` run without `--task-id`) that post-dates the task may be this task's run or another's: not enough to archive, enough to report — fail toward recovery, since a spurious DM line costs a line and a wrong archive costs the owner's import.
- bound and at a **terminal phase** — `done`, `staged`, `discarded`, `forgot` — → **DONE** (the run reached its end; archive). `write_status` produces exactly eight phases across `skills/import-claude-context/scripts/`: `indexed` (index.py), `extracted` (extract.py), `summarizing` / `rolling-up` (progress.py), `staged` (progress.py, finalize.py stage and partial commit), `done` (finalize.py commit), `discarded`, `forgot`. `staged` is terminal because the digest has been posted and the next step is an owner reply, which arrives as a *new* task; `discarded` / `forgot` are the owner ending the run. The classifier's test pins this set against the scripts, so a new writer phase fails the suite until it is placed on one side; an unknown phase at runtime is treated as resumable.
- bound, resumable phase, and `status.json` last moved **≥ 3600 s (1 h)** ago (`IMPORT_STALL_S`; precedent `schedule-crons` `active_stale_minutes`) → **IMPORT-STALLED**: leave `tasks/<id>.txt` alone (the watcher's sweep still re-emits it and the run resumes from disk — a machine that slept mid-run self-heals), but **list it in step 3b's DM** so the owner learns about a run that can never advance (corrupt source, a phase that never moves). Log: `import-stalled: consented import stalled at phase <p>, status.json last moved <idle>s ago`.
- bound, resumable phase, moved within the hour → **IMPORT-RESUME**: leave `tasks/<id>.txt` alone whatever its age, never archive it, never list it in step 3b's DM. The watcher's sweep re-emits it and the skill resumes from disk. Log: `import-resume: consented import already started (phase <p>)`.
- **unbound** — `status.json` newer than the task but with no `task_id` → **IMPORT-UNBOUND**: leave `tasks/<id>.txt` alone (never archive it; the sweep re-emits it and the idempotent skill re-runs or resumes), but **list it in step 3b's DM** with the re-run line. Log: `import-unbound: an import run started after this task was queued (phase <p>) but status.json carries no task_id`.
- not started — no `status.json`, one bound to another task, or an unbound one older than the task — and younger than **1800 s (30 min)** → FRESH.
- not started and older → ORPHAN like any other task (step 3 aggregates it; the re-queue line in the DM restarts it, and so does the owner saying "import my Claude history"). This is the verdict for request B in the interleaving above: it was never executed, which is exactly what the owner must hear.
**The "never orphaned" guarantee is bounded, and here is the window.** `index.py` writes `status.json` (`indexed`) *after* the index files and state (`index.py` line ~482–490), so the durable "started" flag lands after step 1's effect, not before it. An import that dies mid-index — between the acknowledgement and that write — leaves no marker, gets the 30-minute line above, and is then orphaned like any other task: archived, listed in the DM with its re-queue line, restartable by the owner saying "import my Claude history". That outcome is recoverable, so the write is not reordered; but a started import is only never-orphaned from the `indexed` write onward.
4. **Classify outcome**:
- **DONE** → archive the task file: `mv tasks/<id>.txt tasks/archive/<id>.txt`. Log: `done: completion marker found at <path>`.
- **FRESH** → leave alone. Log: `fresh: arrived <N>s ago, watcher will handle`.
- **HELD** → leave alone; never archive, never list in step 3b's DM. It arrived while the core was paused for a provider limit (its id is in `held_tasks` of `state/core-limit-hold.json`, read through `src/core_limit_hold.py`), and the owner was told it waits; the watcher's sweep delivers it. Log: `held: arrived during a core-limit hold`.
- **WORKER-HELD** → leave alone; never archive, never list in step 3b's DM (see step 2c). Log: `worker-held: sentinel held by <holder>`.
- **UNKNOWN** → leave alone, but append it to the in-pass `unknown_holds` list so step 3b names it in the DM (see step 2c). Log: `unknown: deliveries/ unreadable (<error>)`.
- **IMPORT-RESUME** → leave alone (see step 3a). Log: `import-resume: consented import already started (phase <p>)`.
- **IMPORT-STALLED** → leave alone, but append it to the in-pass `stalled_imports` list so step 3b names it in the DM (see step 3a). Log: `import-stalled: consented import stalled at phase <p>, status.json last moved <idle>s ago`.
- **IMPORT-UNBOUND** → leave alone, but append it to the in-pass `unbound_imports` list so step 3b names it in the DM (see step 3a). Log: `import-unbound: import run at phase <p> cannot be matched to this request (status.json has no task_id)`.
- **ORPHAN** → write a recovery result: see step 3.
### Step 3 — Recover orphan tasks (tier-aware)
ORPHAN handling depends on `source` because text-side recovery only makes sense for surfaces where the owner can still see and act on the DM. Voice/phone conversations don't replay in text. The flat per-task sentinel that v0.1.1 used was the wrong default for high-volume team-tier orphans whose conversation threads had moved on — see `feedback_orphan_check_tier_classify_before_sentinel` for the 2026-05-26 post-mortem (22-message blast across 5 channels, 13 of them into one active episode thread).
**Invocation contract.** orphan-check runs once at `/startup` step 1 (PR #1072), BEFORE the task watcher attaches. Under that contract the deferred branch (row 2 of the table below) has no race window — the watcher isn't running between step 3's defer-decision and step 3b's archive mv. If you invoke `/task-orphan-check` standalone with a live watcher already attached, the deferred branch is racy: the watcher may pick up the un-archived `tasks/<id>.txt` between step 3 and step 3b and re-classify it as FRESH or ORPHAN on its own pass (no `results/<id>.txt` exists yet, so the completion-marker check in step 2.2 won't catch it). Safe workaround for standalone invocation: rename `tasks/<id>.txt` → `tasks/<id>.txt.deferred` at defer-time so the watcher's `task-*.txt` glob skips it, then have step 3b mv `tasks/<id>.txt.deferred` → `tasks/archive/<id>.txt` atomically. Verify the watcher's glob actually excludes the suffix on your installation before relying on it.
**Decision table — apply per orphan, first match wins:**
| `source` | Action |
|----------|--------|
| `voice` / `phone` (any tier) | **Silent archive.** Text recovery to a voice/phone surface is the wrong shape; the conversation has hung up or moved on. `mv tasks/<id>.txt tasks/archive/<id>.txt`. No result write. Log: `archived-silent: voice/phone source`. |
| any other source (incl. `discord` / `telegram` / `slack` / `chat` / `whatsapp` / `email` / missing field / future surfaces) | **Defer; aggregate in step 3b.** Append `<id>` to an in-pass `deferred_orphans` list (along with its `access_tier` for per-tier counting). Do NOT write a per-task result. Do NOT archive yet — leave the task file in `tasks/` so step 3b consumes it (see "Invocation contract" above for the standalone-invocation `.deferred` suffix workaround). Log: `deferred: queued for consolidated DM (source=<source>, tier=<tier>)`. |
**All tiers (owner / team / other) flow through the same aggregated DM** so the owner has visibility into stale tasks across all tiers — previously, team/other-tier orphans were `[no-send]`-archived and the owner had no record. Preview-extraction in step 3b strips the bridge-injected system-instructions block so non-owner previews show the actual user ask, not boilerplate.
#### Step 3b — Aggregate deferred orphans into ONE proactive DM
Run once at the end of the orphan pass, after every orphan has been classified by the table above. If `deferred_orphans`, `stalled_imports`, `unbound_imports` **and** `unknown_holds` are all empty, skip. (A stalled or unbound import alone still earns the DM — it is the only surface that run reaches.)
Otherwise:
1. `ts=$(date +%s)`.
2. **Extract preview body for each orphan, stripping any in-band system-instructions block.** Non-owner-tier task files have a `===SUTANDO SYSTEM INSTRUCTIONS===` block injected at the FRONT by the bridge (`src/discord-bridge.py`). Previewing the first 100 chars without stripping would leak the boilerplate, not the user's actual ask. For each `<id>`:
```python
body = read("tasks/<id>.txt")From sonichi/sutando, MIT.
More from sonichi/sutando
- 0.757score
owner-agent-consult
sonichi
Before answering your OWNER, when the answer genuinely depends on something another agent of the same owner holds and you do not, ask that one agent once…
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
marketplace
sonichi
Install marketplace skills, activate cloud tools, check whether installed skills are up to date, or remove them — without the owner opening the Marketplace.
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
tutorial
sonichi
Show a new owner what to try first.
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
macos-tools
sonichi
macOS native integrations: screen capture, calendar, reminders, contacts, email (Mail.app), Spotlight search.
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
import-claude-context
sonichi
Bring the owner's Claude Code history into Sutando: index, extract and haiku-summarise the stock ~/.claude/projects transcripts, stage the result, show the…
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
image-generation
sonichi
Generate an image from a prompt or edit an existing one (Gemini image model, standard library only, any Gemini key the resolver finds) and attach the file to…
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
connect-apps
sonichi
Use when the owner asks about their calendar, email, meetings, Linear, Notion, Slack, Google Drive, GitHub or any other third-party app or the data in it…
SkillAFreshness: A★ 400Claude Code +9 - 0.754score
deal-finder
sonichi
Scan configured sources (Craigslist now; eBay + Facebook Marketplace planned) for used-item listings matching the owner's criteria.
SkillAFreshness: A★ 400Claude Code +9 - 0.754score
quota-tracker
sonichi
Track Claude Code quota usage via Anthropic API rate limit headers.
SkillAFreshness: A★ 400Claude Code +9 - 0.754score
model-switch
sonichi
Change the core's model without the CLI: send /model to the live core's pane through the shared sender, wait for the CLI to accept it (answering its confirm…
SkillAFreshness: A★ 400Claude Code +9 - 0.754score
claude-gemini
sonichi
Use the local Antigravity CLI (agy, Gemini-backed) from Claude Code with the user's existing Gemini authentication or API configuration.
SkillAFreshness: A★ 400Claude Code +9 - 0.754score
phone-conversation
sonichi
Make conversational phone calls and join Zoom meetings via Twilio + Gemini.
SkillAFreshness: A★ 400Claude Code +9
Signals
- Stars
- 400
- Forks
- 98
- Open issues
- 1,202
- Last commit (this folder)
- Sep 19, 2026 (1 month ago)
- Last release
- Aug 20, 2026 (2 months ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/sonichi/sutando
- Website
- sutando.ai
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from sonichi
- 0.757score
connect-apps
sonichi
Use when the owner asks about their calendar, email, meetings, Linear, Notion, Slack, Google Drive, GitHub or any other third-party app or the data in it…
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
image-generation
sonichi
Generate an image from a prompt or edit an existing one (Gemini image model, standard library only, any Gemini key the resolver finds) and attach the file to…
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
import-claude-context
sonichi
Bring the owner's Claude Code history into Sutando: index, extract and haiku-summarise the stock ~/.claude/projects transcripts, stage the result, show the…
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
macos-tools
sonichi
macOS native integrations: screen capture, calendar, reminders, contacts, email (Mail.app), Spotlight search.
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
marketplace
sonichi
Install marketplace skills, activate cloud tools, check whether installed skills are up to date, or remove them — without the owner opening the Marketplace.
SkillAFreshness: A★ 400Claude Code +9 - 0.757score
owner-agent-consult
sonichi
Before answering your OWNER, when the answer genuinely depends on something another agent of the same owner holds and you do not, ask that one agent once…
SkillAFreshness: A★ 400Claude Code +9
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get task-orphan-check running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
task-orphan-check skill. Agent Mart. https://agentmart.dev/skills/sonichi-task-orphan-check (last updated 2026-10-24)