Defect 1 (proven, fixed here): the three dynamic workflows OrchestKit ships through the plugin workflows/ directory were named .mjs. Claude Code's plugin workflow scanner accepts only .js and discards everything else by returning null β no warning, no log, no telemetry. The files shipped, installed, and executed fine by explicit path, but Workflow({name:"ork:skill-fitness"}) answered "not found", because as far as the registry was concerned the workflow never existed.
Defect 2 (hypothesis, fixed separately): the push-gating security suite has a helper that rewrites an empty payload to {}. A transient jq failure therefore turns a deny assertion into a silent abstain β a security test that fails open and reports 13/14 instead of crashing.
Both are the same shape: a failure path that produces a plausible-looking success instead of an error.
Claude Code has two loaders that scan a directory for workflow scripts. They are nearly identical. The 24-byte difference between them is the entire bug.
if(!(l.isFile()||l.isSymbolicLink()))
return null;
if(!l.name.endsWith(".js"))
return null; // β silent drop
return Zvp(join(e,l.name), ...)
No near-miss branch. No warning. .mjs ceases to exist.
if(!a.name.endsWith(".js")){
if(/\.(mjs|cjs|ts)$/.test(a.name))
r.nearMissExt++; // β counted!
return null
}
Same mistake, tracked and reported as near_miss_ext in workflow_discover telemetry.
The claim "the plugin loader filters on .js" is only worth anything if you can show which loader is which. The evidence is positional: two of the four endsWith(".js") sites sit 24 bytes before a near-miss regex, and two do not.
| Byte offset | What is there | Near-miss branch? | Which loader |
|---|---|---|---|
| 264075910 | endsWith(".js") | none | Xvp β plugin dir scanner |
| 264077420 | a.endsWith(".js") | none | plugin custom-path branch |
| 264078444 | endsWith(".js") | yes, at 264078468 | nEp β user/project |
| 264079380 | endsWith(".js") | yes, at 264079404 | UDb β userConfigDir |
Corroborating strings confirmed present in the same binary by direct probe: nearMissExt, near_miss_ext, workflow_discover, Total plugin workflows loaded.
skill-fitness.mjs in the installed alpha.23 cache was copied to skill-fitness.js and the plugins reloaded. The name ork:skill-fitness appeared in the skill registry and Workflow({name:"ork:skill-fitness"}) executed. Sixty seconds earlier the identical call returned "not found. Available: deep-research, code-review". Nothing else changed.
Every check we ran was structurally incapable of catching it, and each one produced a green result that felt like evidence:
| What we verified | Result | Why it could not have caught this |
|---|---|---|
| Files present in the installed cache | β all 3 | Presence is not registration. The loader reads the directory and discards. |
plugin.json has "workflows" | β correct | Field shape was never the problem. |
Manifest paths + meta.name | β correct | meta is parsed after the extension filter, so it was never reached. |
| Workflow executes from the cache | β ran, spawned an agent | Ran via scriptPath β a different loader with no extension filter. |
| CI: build, manifests, skills, drift | β green | No gate knew plugin workflows existed as a component type. |
/ork:<name>"). The only test that could settle it was calling the thing by name.
The push-gating suite reported 19 | Passed: 18 | Failed: 1, then 19/19 on an immediate rerun of the identical tree. Standalone the test passes 14/14. There is no inter-test race: the runner is serial and each test gets a private temp dir. The mechanism is a degradation path inside the shared helper.
That chain produces exactly the observed signature: one deny assertion fails, every abstain assertion still passes, no crash, 13/14. Three things make it worse:
/dev/null.tail -20 of a 31-line log, cutting off exactly the section whose assertion failed.cleanup_test_env instead of chaining it).abstain step is verified; the transient jq failure that would trigger it is notjq path is real, the fail-loud change eliminates the flake. If it is not, the un-silenced warning and the un-truncated log make the next occurrence diagnosable in one run instead of another multi-hour hunt. There is no version of the truth where these are the wrong edits β which is exactly what makes them safe to ship without a reproduction.
| # | Change | File |
|---|---|---|
| 1 | Rename the three workflow scripts .mjs β .js | src/skills/{audit-full,bare-eval,cover}/workflows/*.js |
| 2 | Update the manifest's workflows[] paths | manifests/ork.json |
| 3 | Document the trap: plugin workflows MUST be .js; absence of an error is not evidence of loading | chain-patterns/references/dynamic-workflow-patterns.md |
| 4 | Teach the docs-drift gate that workflows exist β it derived ground truth from skills + agents only, so documenting a real /ork:skill-fitness was reported as a dead reference. Names now come from each script's meta.name. | tests/skills/structure/test-docs-site-drift.mjs |
| 5 | (separate PR) Security harness: fail loudly on an empty payload, stop discarding the watchdog warning, print the failing assertion, chain the cleanup trap | tests/fixtures/test-helpers.sh Β· tests/security/* |
Fix #4 was not planned β it surfaced because of fix #3. Writing the correct documentation broke a gate that had silently assumed a two-component world, which is its own small instance of the same theme.