Development Workflow
Purpose
Show the current local iteration loop for skills: scaffold, edit, validate, run, and sync.
Sources
- CLI skill commands:
apps/node/src/cli/commands/skills.ts,apps/node/src/cli/registry.ts - Runtime wrapper:
apps/node/src/domain/skills/runSkill.ts - Validation:
apps/node/src/domain/skills/validateSkill.ts - Registry resolution:
apps/node/src/domain/skills/skillsConfig.ts,apps/node/src/adapters/skills-repo/localSkillsRegistry.ts - Serve API skill endpoint:
apps/node/src/cli/commands/serve.ts
Local Development Flow
Recommended current loop:
- scaffold the skill
- edit
skill.json,SKILL.md, scripts, and any artifacts - validate the skill
- run the skill locally on a chosen device
- tighten assertions or artifact payloads
- sync the local skills repo if you need a different upstream ref
The code-backed defaults that shape this loop are:
- skill runtime timeout default:
120000 - default Operator package for skill runs:
com.clawperator.operator - install/update sync target:
main - default installed skills repo:
~/.clawperator/skills - default installed registry path:
~/.clawperator/skills/skills/skills-registry.json
Step 1: Scaffold
clawperator skills new com.example.app.do-thing --summary "Do one deterministic workflow"
This gives you:
- registry entry
SKILL.mdskill.jsonscripts/run.jsscripts/run.sh
Success response:
{
"created": true,
"skillId": "com.example.app.do-thing",
"registryPath": "/abs/path/to/skills/skills-registry.json",
"skillPath": "/abs/path/to/skills/com.example.app.do-thing",
"files": [
"/abs/path/to/skills/com.example.app.do-thing/SKILL.md",
"/abs/path/to/skills/com.example.app.do-thing/skill.json",
"/abs/path/to/skills/com.example.app.do-thing/scripts/run.js",
"/abs/path/to/skills/com.example.app.do-thing/scripts/run.sh"
],
"next": "Edit SKILL.md and scripts/run.js, then verify with: clawperator skills validate <skill_id>"
}
Verification pattern:
clawperator skills new com.example.app.do-thing --summary "Do one deterministic workflow" --json
clawperator skills get com.example.app.do-thing --json
Confirm:
createdistruefilesincludes all four scaffolded filesskills getreturns the new registry entry immediately
Common scaffold failures:
{
"code": "USAGE",
"message": "skills new <skill_id> [--summary <text>]"
}
{
"code": "SKILL_ID_INVALID",
"message": "skill_id must contain at least one dot so applicationId and intent can be derived"
}
{
"code": "SKILL_ALREADY_EXISTS",
"message": "Skill already exists: com.example.app.do-thing"
}
Step 2: Validate Structure
Single skill:
clawperator skills validate com.example.app.do-thing --dry-run
All skills:
clawperator skills validate --all --dry-run
Why --dry-run matters:
- it validates artifact payload JSON against the execution schema when artifacts exist
- it catches missing files and metadata mismatches before device execution
Single-skill success response:
{
"valid": true,
"skill": {
"id": "com.example.app.do-thing",
"applicationId": "com.example.app",
"intent": "do-thing",
"summary": "Do one deterministic workflow",
"path": "skills/com.example.app.do-thing",
"skillFile": "skills/com.example.app.do-thing/SKILL.md",
"scripts": [
"skills/com.example.app.do-thing/scripts/run.js",
"skills/com.example.app.do-thing/scripts/run.sh"
],
"artifacts": []
},
"registryPath": "/abs/path/to/skills/skills-registry.json",
"dryRun": {
"payloadValidation": "skipped",
"reason": "skill has no pre-compiled artifacts; payload is generated at runtime by the skill script"
},
"checks": {
"skillJsonPath": "/abs/path/to/skills/com.example.app.do-thing/skill.json",
"skillFilePath": "/abs/path/to/skills/com.example.app.do-thing/SKILL.md",
"scriptPaths": [
"/abs/path/to/skills/com.example.app.do-thing/scripts/run.js",
"/abs/path/to/skills/com.example.app.do-thing/scripts/run.sh"
],
"artifactPaths": []
}
}
All-skills success response:
{
"valid": true,
"totalSkills": 12,
"registryPath": "/abs/path/to/skills/skills-registry.json",
"validSkills": [
{
"skill": {
"id": "com.example.app.do-thing",
"applicationId": "com.example.app",
"intent": "do-thing",
"summary": "Do one deterministic workflow",
"path": "skills/com.example.app.do-thing",
"skillFile": "skills/com.example.app.do-thing/SKILL.md",
"scripts": [
"skills/com.example.app.do-thing/scripts/run.js",
"skills/com.example.app.do-thing/scripts/run.sh"
],
"artifacts": []
},
"checks": {
"skillJsonPath": "/abs/path/to/skills/com.example.app.do-thing/skill.json",
"skillFilePath": "/abs/path/to/skills/com.example.app.do-thing/SKILL.md",
"scriptPaths": [
"/abs/path/to/skills/com.example.app.do-thing/scripts/run.js",
"/abs/path/to/skills/com.example.app.do-thing/scripts/run.sh"
],
"artifactPaths": []
}
}
]
}
Verification pattern:
clawperator skills validate com.example.app.do-thing --dry-run --json
clawperator skills validate --all --dry-run --json
Check:
validistrueregistryPathis the registry you intended to validatechecks.scriptPathsandchecks.artifactPathspoint at files that actually exist
Common validation failures:
{
"code": "USAGE",
"message": "skills validate <skill_id> [--dry-run] | skills validate --all [--dry-run]"
}
{
"code": "SKILL_VALIDATION_FAILED",
"message": "Skill com.example.app.do-thing is missing required files",
"details": {
"skillJsonPath": "/abs/path/to/skills/com.example.app.do-thing/skill.json",
"missingFiles": [
"/abs/path/to/skills/com.example.app.do-thing/scripts/run.js"
]
}
}
{
"code": "SKILL_VALIDATION_FAILED",
"message": "1 of 12 registered skills failed validation",
"registryPath": "/abs/path/to/skills/skills-registry.json",
"details": {
"totalSkills": 12,
"validCount": 11,
"invalidCount": 1,
"failures": [
{
"skillId": "com.example.app.do-thing",
"code": "SKILL_VALIDATION_FAILED",
"message": "Skill com.example.app.do-thing metadata does not match the registry entry"
}
]
}
}
Step 3: Run Locally
Use explicit device targeting when more than one device is connected:
clawperator skills run com.example.app.do-thing --device <device_serial> --operator-package com.clawperator.operator.dev --timeout 90000 --json
Argument rules:
--device <serial>becomes the first positional script argument- arguments after
--are forwarded unchanged to the script
Example with forwarded args:
clawperator skills run com.example.app.do-thing --device <device_serial> -- --mode smoke --limit 3
Exact wrapper rules to keep in mind:
--device <serial>is prepended to the script arguments only when provided- the skill wrapper injects
CLAWPERATOR_BINandCLAWPERATOR_OPERATOR_PACKAGEinto the child environment .jsscripts run withprocess.execPath- the wrapper chooses
.jsfirst, then.sh, then the first listed script - JSON mode suppresses the pretty banner so stdout stays machine-readable
--timeoutand--timeout-msare accepted timeout flags for the wrapper
Verification pattern:
clawperator skills run com.example.app.do-thing --device <device_serial> --operator-package com.clawperator.operator.dev --timeout 90000 --json
First-time agent pitfall:
- pretty mode streams live output and prints a banner first
- JSON mode returns one parseable wrapper object with the child stdout captured under
output
Step 4: Verify Output
Current wrapper success data:
{
"skillId": "com.example.app.do-thing",
"output": "RESULT|status=success\n",
"exitCode": 0,
"durationMs": 8421,
"timeoutMs": 90000
}
Current wrapper failure data can include partial output:
{
"code": "SKILL_EXECUTION_FAILED",
"message": "Skill com.example.app.do-thing exited with code 1",
"skillId": "com.example.app.do-thing",
"exitCode": 1,
"stdout": "RESULT|status=partial\n",
"stderr": "Expected node not found\n"
}
During development, inspect:
outputstdoutstderr- wrapper timeout
- the actual device state after the run
Additional execution failures to expect:
{
"code": "SKILL_EXECUTION_TIMEOUT",
"message": "Skill com.example.app.do-thing timed out after 90000ms",
"skillId": "com.example.app.do-thing",
"stdout": "{\"stage\":\"before-timeout\"}\n"
}
{
"code": "SKILL_SCRIPT_NOT_FOUND",
"message": "Script not found: /abs/path/to/skills/com.example.app.do-thing/scripts/run.js",
"skillId": "com.example.app.do-thing"
}
--expect-contains
For lightweight output assertions:
clawperator skills run com.example.app.do-thing --device <device_serial> --expect-contains RESULT
Behavior:
- the wrapper still runs the full skill
- after success, it checks whether stdout contains the expected substring
- if not, it fails with
SKILL_OUTPUT_ASSERTION_FAILED
This is useful for smoke checks in CI or local iteration when the script emits stable markers.
Assertion failure shape:
{
"code": "SKILL_OUTPUT_ASSERTION_FAILED",
"message": "Skill com.example.app.do-thing output did not include expected text",
"skillId": "com.example.app.do-thing",
"output": "RESULT|status=success\n",
"expectedSubstring": "missing-value"
}
Verification pattern:
clawperator skills run com.example.app.do-thing --device <device_serial> --expect-contains RESULT --json
Check that:
expectedSubstringis echoed back in the success payloadoutputstill contains the raw skill stdout, not a transformed assertion result
HTTP Testing Pattern
When using clawperator serve, the matching route is:
POST /skills/:skillId/run
Request body:
{
"deviceId": "<device_serial>",
"args": ["--mode", "smoke"],
"timeoutMs": 90000,
"expectContains": "RESULT"
}
This is useful for local HTTP-based tests of the same wrapper contract.
Verification pattern:
- send
POST /skills/:skillId/runwithdeviceId,args,timeoutMs, andexpectContains - success response shape is:
{
"status": "success",
"ok": true,
"skillId": "com.example.app.do-thing",
"output": "RESULT|status=success\n",
"skillResult": null,
"exitCode": 0,
"durationMs": 8421,
"timeoutMs": 90000,
"expectedSubstring": "RESULT"
}
- declared-but-unproved verification response shape is:
{
"status": "indeterminate",
"ok": null,
"code": "SKILL_VERIFICATION_INDETERMINATE",
"message": "Declared verification was not proved.",
"skillId": "com.example.app.do-thing",
"output": "[Clawperator-Skill-Result]\n{\"status\":\"success\"}\n",
"skillResult": {
"status": "success"
},
"exitCode": 0,
"durationMs": 8421,
"timeoutMs": 90000,
"expectedSubstring": "RESULT"
}
- failure response shape is:
{
"status": "failed",
"ok": false,
"error": {
"code": "SKILL_EXECUTION_FAILED",
"message": "Skill com.example.app.do-thing exited with code 1",
"skillId": "com.example.app.do-thing",
"exitCode": 1,
"stdout": "RESULT|status=partial\n",
"stderr": "Expected node not found\n",
"skillResult": null
}
}
- unlike CLI
skills run, this route callsrunSkill()directly - it does not run the CLI pre-validation gate from
cmdSkillsRun() - it does not inject the CLI wrapper banner
- handle
error.codevalues such asSKILL_OUTPUT_ASSERTION_FAILED,SKILL_EXECUTION_FAILED,SKILL_EXECUTION_TIMEOUT, andSKILL_RESULT_PARSE_FAILEDthrough the nestederrorobject, not as the top-level response object
Skill Sync
Current sync commands:
clawperator skills install
clawperator skills update [--ref <git-ref>]
clawperator skills sync --ref <git-ref>
Behavior:
skills installsyncsmainskills updatesyncs the given ref or defaults tomainskills sync --ref ...pins the local skills repo to a specific git ref
Exact success shapes:
skills install --json:
{
"synced": true,
"message": "Skills synced to /Users/<local_user>/.clawperator/skills (ref: main)",
"registryPath": "/Users/<local_user>/.clawperator/skills/skills/skills-registry.json",
"envInstruction": "export CLAWPERATOR_SKILLS_REGISTRY=\"/Users/<local_user>/.clawperator/skills/skills/skills-registry.json\""
}
skills update --json and skills sync --ref <git-ref> --json:
{
"synced": true,
"message": "Skills synced to /Users/<local_user>/.clawperator/skills (ref: main)"
}
Verification pattern:
clawperator skills install --json
clawperator skills update --json
clawperator skills sync --ref main --json
Check:
syncedistrueregistryPathfromskills installends with~/.clawperator/skills/skills/skills-registry.json- after install, exporting
envInstructionmakesskills list --jsonsucceed in a fresh shell
Common sync failures:
{
"code": "USAGE",
"message": "skills sync --ref <git-ref>"
}
{
"code": "SKILLS_GIT_NOT_FOUND",
"message": "git is not installed or not on PATH. Install git to use skills install/update."
}
{
"code": "SKILLS_SYNC_FAILED",
"message": "Registry file not found or unreadable after sync: ENOENT: no such file or directory, open '/Users/<local_user>/.clawperator/skills/skills/skills-registry.json'. Expected at /Users/<local_user>/.clawperator/skills/skills/skills-registry.json"
}
Common Development Issues
REGISTRY_READ_FAILED
Cause:
CLAWPERATOR_SKILLS_REGISTRYmissing or wrong- registry file does not exist
Fix:
- set the env var to the correct registry
- or run
clawperator skills install
SKILL_SCRIPT_NOT_FOUND
Cause:
- registry entry points to a script that does not exist
Fix:
- update
skill.jsonand the registry entry together - rerun
skills validate
SKILL_VALIDATION_FAILED
Cause:
- missing files
skill.jsondoes not match the registry- artifact payload violates execution schema in
--dry-run
Fix:
- inspect the returned
details - correct metadata or payload shape
SKILL_EXECUTION_TIMEOUT
Cause:
- wrapper hit the timeout before the child exited
Fix:
- increase
--timeoutonly if the workflow really needs it - otherwise inspect whether the skill is hanging on an invalid selector or external state
EXECUTION_VALIDATION_FAILED
Cause:
--timeoutor--timeout-mswas present but not finite
Exact failure:
{
"code": "EXECUTION_VALIDATION_FAILED",
"message": "timeoutMs must be a finite number"
}
Fix:
- pass an actual number such as
--timeout 90000
USAGE
Cause:
- a required argument was omitted
- a value-taking flag such as
--timeout-msor--expect-containswas missing its value
Fix:
- rerun with the exact command shape shown in the error message
Recommended Local Loop
clawperator skills new com.example.app.do-thing --summary "Describe it"
clawperator skills validate com.example.app.do-thing --dry-run
clawperator skills run com.example.app.do-thing --device <device_serial> --operator-package com.clawperator.operator.dev --json
clawperator skills run com.example.app.do-thing --device <device_serial> --expect-contains RESULT --json
Repeat until:
- validation passes
- wrapper exits
0 - output contains the signals your agent will actually consume