Install
$ agentstack add skill-ethanhq-cc-fleet-workflow ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
workflow — multi-phase JS orchestration over provider subagents
Wrong lane? A flat one-shot fan-out of independent tasks → /cc-fleet:subagent; interactive collaboration you message back and forth → /cc-fleet:team; arbitration in cc-fleet-shared/routing.md.
When this skill cites cc-fleet-shared/.md, OPEN it with the Read tool at ../cc-fleet-shared/.md relative to this SKILL.md — the cited content is load-bearing, not optional background.
A workflow is a JavaScript script that fans out provider cc-fleet subagent leaves and runs in a cc-fleet process, OFF the main session's context. You write the script; cc-fleet workflow run executes it. The orchestration plan lives in script variables (CPU, ~0 of your tokens) — you are invoked only when authoring the script, not on every scheduling decision. The API mirrors the native Claude Code Workflow tool — write the script exactly as you would a native workflow; the only addition is the provider option on agent().
When to use it
- Multi-phase or dynamic orchestration over many provider subagents: fan-out + barrier, per-item pipeline, loop-until-dry, branch-on-result, with a board run-tree.
- A single flat batch of independent one-shots is not a workflow — that's /cc-fleet:subagent. Don't write a script for it.
The provider ask ladder (ask at most once per task)
- The user named a provider or model → use it.
- Else run
cc-fleet default --json: if it returns a provider (source "configured" or "auto"), use it and STATE it in your kickoff line (e.g. "using glm (default)"). - Else (several providers, none default) ask the user ONCE which to use — list the enabled providers from
cc-fleet list --json(name + default_model + the one-line note in cc-fleet-shared/providers.md). After they pick, runcc-fleet defaultso you never ask again. (cc-fleet defaultis user-layer; only run it to FILL a blank default, never with --force.) - A mid-task provider failure (insufficient balance / rate limit / auth) → STOP, tell the user what happened, propose the next provider, and WAIT for their confirmation. Never switch providers silently.
Model tier within a provider: fan-out / leaf work → omit --model (or --model fast); judge / synthesis / sustained work → --model strong. The provider's roster decides the actual model — see cc-fleet-shared/providers.md.
In a script, agent()'s opts.provider is optional: omitted, the leaf uses the run's default provider, resolved ONCE at launch and recorded with the run — so --resume stays stable even if the default changes later. A script meant to be shared or reproducible should still pin provider explicitly.
The script API (mirrors the native Workflow tool)
const meta = {name, description, whenToUse?, model?, phases?: [{title, detail?}]}— a top-level pure literal (no calls/vars/spreads; the nativeexport const metaform is also accepted).name+descriptionare required;modelis the default for agents that omit it. Read statically before the run → the board shows the named, phase-skeletoned run immediately.agent(prompt, opts) → Promise— runs ONE provider subagent leaf.opts.provideris optional (omitted → the run's default provider, above);provider: "claude"runs the officialclaudeCLI on the user's OWN Claude Code login (subscription OAuth) instead of a configured provider — a literalmodelid (opus/sonnet/a full id, omitted → claude's login default, typically the costliest tier so name one), no roster keywords, no key material. The rest are optional:model,schema,label,phase,timeout(seconds),max_budget_usd,max_turns,isolation: "worktree",profile("slim" default / "slim-ro" / "full"),tools,skills,mcp. An unknown option key throws (typos fail loudly). On a leaf failure the promise rejects — an un-caught top-levelawait agent()aborts the run; insideparallel/pipelinea failed element degrades tonull. Leaf failures classify like subagent failures — dispatch table in "Leaf failures" below.schema(a plain object) goes to the claude child via--json-schema: claude injects a forcedStructuredOutputtool and enforces that it is CALLED (the native mechanism — no JSON instruction is added to the prompt); the promise resolves with the parsed structured payload. The three rules:- a validation failure — or a result envelope without a structured payload — FAILS the leaf; there is NO automatic retry;
- the forced
StructuredOutputcall costs turns — give a schema'd leafmax_turns≥ 3 (a budget of 1 starves it); - needs claude ≥ 2.1.88 (the slim-profile floor); an older claude fails the leaf with a classified usage error.
Client-side validation backstops with a recursive JSON-Schema subset: type (object/array/string/number/integer/boolean/null; integer accepts 5.0), required, nested properties, array items, scalar enum, string pattern (RE2 best-effort — the wire enforces the authoritative ECMA regex) / format (email/uri/uuid/date/date-time), additionalProperties, allOf/anyOf/oneOf, and intra-document $ref (#/… pointers; an external URI is unsupported and fails).
isolation: "worktree"runs the leaf with cwd = a fresh git worktree (torn down after), so parallel file-editing leaves don't collide (requires a git repo).profile:"slim"(the default; write-capable) /"slim-ro"(read-only research) /"full"(ONLY to compare against a full session or diagnose a suspected slim regression). Writes files →slim, read-only →slim-ro.tools,skills(defaulttrue) andmcprefine a slim leaf, are rejected withprofile: "full", andtoolsREPLACES the whole set, never appends. Tool whitelists / per-profilemcpdefaults / the pre-2.1.88 fail-open downgrade: cc-fleet-shared/providers.md. The run journal folds the effective profile + tools, so a--resumere-runs a leaf whose shape changed.- Background = an unawaited promise. There is no
run_in_background/wait(): start a leaf withconst p = agent(...), keep working,await plater (Promise.allfor a batch). Every leaf — awaited or not — is pool-bounded, journaled at completion, and the run only finalizes after all of them settle. A leaf that rejects with nobody ever handling it fails the run (a silently dropped failure is still a failure); fire-and-forget tolerance is an explicitp.catch(() => null). parallel(thunks) → Promise— run each 0-arg thunk concurrently; BARRIER (settles once all finish),nullwhere an element failed:await parallel([() => agent("a", {provider: "glm"}), () => agent("b", {provider: "glm"})]). Concurrent execs stay ~pool size even for a huge list (excess queues).pipeline(items, ...stages) → Promise— push each item through all stages independently with NO inter-stage barrier (item A can be in stage 3 while B is in stage 1). Each stage is(prev, item, index) => …(sync or async; its return value is awaited). A failing stage drops that item tonulland skips its remaining stages. DEFAULT topipelineoverparallel— only useparallelwhen a stage genuinely needs ALL prior results together.workflow(path, args?) → Promise— run another.jsinline on the same engine (shared pool/journal/budget), one level deep only; resolves with the child's top-levelreturnvalue.budget— two parallel cap surfaces. USD:budget.total(the--budget-usdcap in USD, ornull),budget.spent(),budget.remaining()(Infinitywhen uncapped) — USD floats (an Anthropic list-price estimate). Tokens:budget.tokens_total(the--budget-tokenscap, ornull),budget.tokens_spent(),budget.tokens_remaining()— ints (input+output, cache-read excluded).agent()throws once either cap is reached; awhile (budget.remaining() > N)loop scales depth to the cap. (Native'sbudget.totalis a token target; here it is USD —--budget-usdis the cross-provider cap since providers price tokens differently — and tokens are the separatetokens_*surface.) Aprovider: "claude"leaf spends the lead session's own subscription window, not a metered provider — use it for one or two synthesis / judgement nodes, never a wide fan-out. Its usage still flows into the run's token / USD surfaces, but the USD is claude's notional list-price (a subscription is not metered per token);max_budget_usd/--budget-usdstill gate against that notional figure.phase(title, detail?)— name the current phase (tags subsequent agents lacking an explicitphase; the detail shows on the board row).log(msg)— a narrator line (board live log + stderr);console.log/info/warn/error/debugalias onto it (non-strings render as JSON, Errors by message).args— the parsed--args-json ''value (or theworkflow(child, args)value);undefinedwhen none was given.
What a workflow script can NOT use (determinism — the journal depends on it)
Date/Math.random()throw;eval/Function/ dynamic code are removed; there is nosetTimeout/require/fs/ ESMimport— pass timestamps or randomness in viaargs.- Plain script statements only (the body runs inside an async wrapper, so top-level
awaitandreturnwork); async generators (async function*) are not supported.
Running it
RUN=$(cc-fleet workflow run audit.js) # detached; prints ONLY the bare run id
cc-fleet workflow status "$RUN" --json # manifest + every tagged leaf (run→phase→agent)
cc-fleet workflow list --json # all runs, newest first
cc-fleet workflow stop "$RUN" # reap a running run (engine + in-flight leaves)
cc-fleet workflow stop "$RUN" --leaf # hold ONE agent in place (run keeps going); --phase holds a phase
cc-fleet workflow restart "$RUN" --leaf # re-run a held/running agent in place; --phase a phase;
# on a FINISHED run: keyed re-run (whole run, --leaf, or --phase)
cc-fleet workflow wait "$RUN" --timeout 3m --json # block silently until the run settles ("Waiting on a run" below)
# or watch the board's Dynamic Workflows view: live log, token/cost columns, prompt/answer drill-in.
# x/r there are level-scoped: run row = the run, Phases pane = the phase, agent pane = the leaf
# (a held agent shows ▶ until you restart it). --foreground runs inline (debug).
# `held` in status output = parked by the control plane: an operator paused it (board
# x, stop --leaf/--phase) or a restart was refused (budget gate); a restart in flight
# may show it briefly. Not an error/retry/backoff — the run waits on it indefinitely.
# If held persists across polls, resume it with restart --leaf/--phase or tell the
# user it is parked; never wait it out.
# --max-concurrency N overrides the default pool (min(16, cores-2));
# --budget-usd N caps total spend; --no-persist-io disables the prompt/answer drill-in.
The run is detached so it outlives this call and your session stays responsive.
Waiting on a run: arm wait in a backgrounded Bash (push, not poll)
Right after launching, arm the notifier — a backgrounded Bash whose EXIT is your wake-up:
RUN=$(cc-fleet workflow run audit.js)
# Bash tool with run_in_background=true; the harness wakes you when it exits:
cc-fleet workflow wait "$RUN" --timeout 3m --json
End your turn and keep working — never spawn an agent (or loop yourself) to poll a run. On the wake, dispatch on the envelope's wait_outcome (+ exit code):
terminal(exit 0 done/stopped · 1 failed) — fetch the detail withworkflow status "$RUN" --json(it carriesrun_errorand the per-leaf list; the wait envelope deliberately doesn't) and report.engine_gone(1) — the engine died without finalizing; proposecc-fleet workflow run --resume "$RUN"(the journal replays the finished leaves).parked(3) — every remaining leaf is held. FIRST re-checkworkflow status: leaves running/queued again means it was a transient (the engine was between leaves) — re-arm silently. Still parked → name the envelope'sheldleaves to the user and proposerestart --leaf; never wait it out.timeout(124) — a heartbeat, not a verdict. Comparecounts/spent_*with the previous snapshot: progress → one short progress line and re-arm with a longer window; zero delta → inspect (workflow status; is one long leaf still inside its owntimeout?) and escalate only on a real anomaly, else re-arm.
Window sizing: make the FIRST window short (2–3m — a provider auth/balance failure surfaces on the first leaf call), then 10–15m per re-arm. One wait per run; they are independent. After a session restart, re-arm every running run from workflow list --json.
For a human live view: cc-fleet workflow watch "$RUN" streams the run's events as text (in a terminal, or a backgrounded shell → the /tasks panel) and cc-fleet watch streams the whole fleet; the board's Dynamic Workflows view has the rich drill-in. Both print only canonical status — never a provider reply.
Leaf failures — dispatch on error_code (do not parse prose)
A failed leaf's error_code is in workflow status --json (jobs[]) and in the rejection that reaches the script. Same vocabulary as a one-shot subagent (the full table with context lives in /cc-fleet:subagent); the dispatch:
| error_code | What you do | |---|---| | INSUFFICIENT_BALANCE / KEY_INVALID / RATE_LIMITED | STOP — provider ask ladder, step 4 (never switch silently). KEY_INVALID → the user rotates the key; RATE_LIMITED → brief wait, one retry. | | NO_DEFAULT_PROVIDER / DEFAULT_PROVIDER_DISABLED / DEFAULT_PROVIDER_UNKNOWN / DEFAULT_PROVIDER_RESERVED | No usable default for a provider-less agent() (RESERVED = default_provider hand-set to claude, explicit-only — the user unsets/re-pins) — apply the provider ask ladder, then re-run. | | MODEL_NOT_FOUND | cc-fleet refresh , or drop the leaf's model to use the provider default. | | SUBAGENT_TIMEOUT | Raise the leaf's timeout or split the task; a leaf with no timeout defaults to 300s. | | SUBAGENT_OUTPUT_TOO_LARGE | The leaf's output exceeded the byte cap — have it write to a file and answer concisely; a blind retry overflows again. | | SUBAGENT_STOPPED | An operator stopped it (stop --leaf / run stop) — terminal, NOT a failure; never auto-retry. | | SUBAGENT_FAILED / PROVIDER_API_ERROR | Inspect (workflow status); restart --leaf once, or propose a provider switch (ask first). A provider: "claude" leaf on a logged-out machine fails here (the error preview names the login problem, no dedicated code) — tell the user to log in to Claude Code interactively. | | FINGERPRINT_MISSING / FINGERPRINT_STALE | Self-heal flow in cc-fleet-shared/troubleshooting.md (STALE = no claude binary — the flow can't help; fix Claude Code / PATH). | | CODEX_PROXY_UNAVAILABLE / CODEX_CLOUDFLARE_BLOCKED | cc-fleet codex login / free the port; a Cloudflare block → switch network, don't rotate credentials. | | UNKNOWN_PROVIDER / PROVIDER_DISABLED / CONFIG_LOAD_FAILED | Config problem — cc-fleet list --json, cc-fleet add / edit --enable; CONFIG_LOAD_FAILED → cc-fleet doctor. | | PROVIDER_RESERVED | A providers.toml row is named claude (reserved for the native leaf) — the user renames or removes it. | | SUBAGENT_BAD_ARGS | Bad leaf options — fix the script, re-run. |
Resume (content-hash journal)
Each run records a content-hash journal of its completed leaves. Re-run the same script under an existing run id to replay:
cc-fleet workflow run audit.js --resume "$RUN" # journaled leaves return cached (no provider exec); only un-run leaves run
A leaf is keyed by its determinant (provider + model + prompt + schema + slim shape), so an unchanged re-run is ~100% cache hits, a leaf whose prompt you e
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ethanhq
- Source: ethanhq/cc-fleet
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.