Workflow scripts are executable code that runs with your users' full privileges, so submissions are gated. CI runs the repository-only parser and scope analysis described below.
Nothing in the gate executes your script. The contract linter and diagram generator both parse source into an AST without evaluating the workflow body; only the workflow harness executes a workflow, after the user chooses to run it.
One PR, one new file: plugins/ultracode-workflows/workflows/<name>.js.
Do not regenerate docs/DIAGRAMS.md, the gallery images, or the README catalog. Those are derived artifacts, and a maintainer produces them after merge — a PR that touches them is harder to review, not easier to accept.
# install the repository-only static-analysis dependencies
npm ci --ignore-scripts
# your submission (accepts a single file or a directory)
node scripts/lint-workflows.mjs path/to/your-workflow.jsThat single-file lint is the pre-flight for a workflow submission. npm run check runs the whole repository gate — the bundled-artifact check and the full test suite alongside the linter — and is what CI runs on your PR:
npm run checkThe harness injects exactly eight globals and nothing else:
agent · parallel · pipeline · phase · log · args · budget · workflow
There is no process, no require, no import(), no fetch, no filesystem. Agents do I/O; the script orchestrates. If your workflow needs to read a file, run a command, or fetch a URL, that is an instruction you give an agent — not something the script does itself.
Referencing an unavailable capability fails the scan, because such a script is either broken (it cannot work) or hostile. CI wraps the body in the harness's async-function shape and uses ESLint scope analysis, so escaped identifiers, aliases, and optional calls do not bypass the check:
| Rejected | Why |
|---|---|
process, globalThis, global |
Reaching outside the injected scope |
require(), import() |
Scripts are not modules and cannot load code |
eval(), Function() |
Dynamic code execution |
fetch() |
No network; give an agent a web tool instead |
__dirname, __filename, Buffer |
No filesystem or binary surface |
Date.now(), Math.random(), new Date() |
Break resume — pass timestamps via args |
ESLint parses strings and comments as syntax rather than references, so ordinary English in prompts is fine: "Process each file and require an explicit fix" passes. Submitted eslint-disable comments are ignored.
This static gate is defense in depth, not a JavaScript sandbox or a proof that a workflow is safe. Review workflow changes as executable code even when every automated check passes.
export const meta = {...}first, a pure data literal (no calls, arrows, template literals, or expressions), withnamematching the filename and adescription.- Phase titles in
meta.phasesmust match thephase()calls and{phase: ...}opts exactly. - Composition nests one level: a workflow that calls
workflow()may only call leaf workflows. - Plain JavaScript, not TypeScript, and not a module beyond the
metaexport. Top-levelawaitandreturnare legal — the harness wraps the body.
The gate checks safety and contract; these are what get a PR merged:
- Every
parallel()barrier carries a comment defending it. If you can't state why stage N needs all of stage N−1, it should be apipeline(). - Verification is adversarial. Finders don't grade their own findings; skeptics are prompted to refute, with a stated default under uncertainty.
- Schemas at every agent boundary. Downstream stages consume validated structure, never re-parsed prose.
- Caps are loud. Log what was dropped and return it — silent truncation reads as "covered everything."
- Destructive behaviour is opt-in (
apply/fixdefault to report-only) and verified afterwards. - Honest exits. Bounded loops; non-convergence returns a truthful status rather than churning.
- A header comment stating use case, args, patterns demonstrated, cost profile, and output shape.
See docs/PATTERNS.md for the pattern vocabulary and docs/AUTHORING.md for the full contract. /ultracode-workflows:wf-new will draft a script that satisfies all of this.
Scripts under scripts/ are held to a stricter rule, because several of them read workflow files a user has not vetted: a tool that inspects a workflow must never execute it. CI enforces this with targeted semgrep rules:
uvx semgrep --config=.semgrep.yml plugins/Generic rulesets do not reliably catch this class. If you must spawn a subprocess or evaluate anything, annotate it // nosemgrep: <rule> -- <reason> so the audit stays at zero and new findings stand out. The directive must be the line immediately above the flagged call — put the explanation in lines above it, never between. A comment reflowed so that a wrapped line ends up touching the call silently disables the suppression, and the rule then fires with a message about untrusted input rather than about the comment. State the control in the reason (path constrained by isWorkflowScriptPath), not just that you looked at it.
Static parsing removes the code-execution risk, but transcript-derived script paths are still untrusted input. The run viewer keeps an explicit path allowlist so a stray path in tool output, fetched text, or agent output cannot make it inspect an unrelated local file and disclose source-derived metadata or topology. The lower-fidelity statusline does not resolve or read workflow source at all; it reads run artifacts and, when present, a plan cache already produced by the viewer.
CI pins its actions to full commit SHAs with the version in a trailing comment (@<sha> # v7.0.1), and semgrep to an exact release.
tests/lint-workflows.test.mjs asserts the parser and scope-analysis properties directly: hostile fixtures under tests/fixtures/ must be rejected, while a benign script whose prompts mention process/require/fetch in ordinary English must pass. tests/workflow-meta.test.mjs separately proves that the dependency-free metadata reader contains no source-evaluation primitive and rejects hostile metadata without a side effect. Those fixtures are deliberately kept as regression protection.