mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-25 08:28:02 +00:00
refactor(skills): make Skill Workshop a direct, versioned self-learning loop (#161057)
* refactor(skills): simplify Skill Workshop into a direct, versioned learning loop
* refactor(skills): replace weekly curator with unused-skill cleanup; add remove_file and used-skill review trigger
* fix(skills): record Code Mode skill reads as use; clearer learned-skill notice and undo
* fix(skills): give the next turn the exact revert call for a learned change
* chore(skills): log unused-skill cleanup archives
* fix(skills): pin learned-change undo to the pre-review version; split skill locks
* fix(skills): keep legacy skill descriptions editable; credit review changes to the reviewed conversation
* fix(skills): harden Skill Workshop writes, migration recovery, and review notices
Recheck live write authority under the skill lock, publish snapshots atomically, keep a review's undo target, cancel stale queued reviews, announce changes from failed reviews in the right thread and session, never replay dispatched CLI mutations locally, and recover unfinished proposal applies and unowned proposals during the Doctor retirement.
* fix(skills): check Workshop authority at each file effect, fence notices to the reviewed session generation
Recheck the caller's live authority immediately before every write, remove, rename and snapshot publish; pin only a review's own undo anchor so the ten-version limit holds across reviews; fence notices, transcript appends and channel sends on the reviewed lifecycle revision; keep proposal rollback records when an unfinished apply is in a mixed state; retire a Control UI mode write when the Gateway connection changes. Version storage moves to skill-versions.ts.
* fix(skills): pass Workshop authority into fs-safe, drop review version pinning
Hand the live-authority check to fs-safe's assertBeforeMutation for writes and removes; remove per-review version pinning in favor of the plain ten-version limit; clear mode switch state when the Gateway or agent changes; remove the empty directory left by an undone partial create during proposal retirement.
* test(e2e): treat the retired Workshop review table as healthy in the upgrade witness
* fix(skills): clear Skill Workshop simplification CI fallout
- Break the state worker contract import cycle: WorkshopChange now lives with its SQL owner (changes.kernel).
- Delete code orphaned by the proposal removal (multi-lease state worker path, apply-mutation path, attestation retire, frontmatter/usage helpers) and the unused ui diff dependency.
- Ratchet protocol module count (69) and public SDK export budget (4586); drop the stale enabled=true->propose doctor test.
- Fix mock Workshop lint and keep raw DDL allow comment where the Kysely guard reads it.
* test(qa-lab): read the mock tool-search target from the QA marker
The mock took the first target= anywhere in the request text. The
skill_workshop description ("Set target=personal ...") precedes the user
prompt, so the delegation real-gateway lane searched for "personal" and
never called openclaw.
* fix(skills): skip ACP sessions in reviews, ignore symlinked versions, keep drafts of removed agents
Adversarial review follow-ups: ACP child sessions no longer trigger background reviews; version listing ignores symlinked entries; Doctor keeps proposals whose recorded owner is no longer configured; remove the stale cron-disabled collection-review warning and retirement log text; docs point exported drafts at /learn.
* feat(skills): let background reviews look things up with read-only tools
Reviews can read files, list directories, search/fetch the web, and look up past sessions and memory; skill_workshop stays the only tool that changes anything. Hermes saw the same denial storm with a write-tool-only review.
* fix(skills): count a turn's assistant messages when the harness reports zero iterations
Codex reports model iterations from raw response events, which resumed threads do not
emit, so every turn after the first reported 0 and the review counter never reached ten.
* style(ui): compose Skill Workshop from the settings and Carapace primitives
Learning mode and learn-from-past become one settings group whose description states what the current mode does. Skills, archived skills, and the change feed use shared settings rows; the feed leads with the skill, shows the reason, then who/when, with Undo as the row control. The page now loads the hub stylesheet so its header matches Plugins and Skills on a cold load. The mock seeds a richer feed.
* fix(ui): name the learning Start button for assistive tech
* fix(skills): keep learned skills that a cron job names out of unused cleanup
* feat(skills): advisory authoring feedback and merge hints on Workshop writes
Each SKILL.md write reports up to three non-blocking findings it introduced (description length, sprawl, no-op words, negations, update narratives) and the closest overlapping live skill, with the merge recipe. Authoring standards and the review and /learn prompts adopt the writing-for-agents levers and make merging overlapping skills part of the normal flow.
* refactor(skills): let the agent judge duplicate skills instead of a word-overlap score
A create now lists the agent's other learned skills so the model decides whether to merge; the Jaccard overlap heuristic is removed.
* fix(skills): clear merge deadcode and route Workshop review test to database workers
Unexport helpers whose only non-test consumers were removed with the proposal machinery (resolvedSkillUsageMatch, lintSkillMarkdown, WORKSPACE_ATTESTATION_RECENT_MS), test the lint rules through skillWriteAdvisories, and run experience-review.test.ts in the database-worker lane now that review preparation is worker-backed.
* test(skills): align main's skill-read test with always-visible learned skills; drop dead max-lines suppression
* chore(skills): prune ratchet baselines and move the removed-hooks compat record into its own module
* chore(pr): list the removed-hooks compat record in the PR wrapper inventory
* test(skills): pass real exports through Workshop test mocks and prune the mock-export baseline
* fix(skills): swap restored skills by rename so a refused publish keeps the live skill; whole-bundle recovery guidance
* fix(skills): remove archived skills through the guarded removal owner; check authority right before the restore swap
* fix(skills): archive by one fenced rename so a revoked archive leaves the skill whole
* fix(doctor): name both recoveries for a Skill Workshop draft whose agent is gone
* test(state): create the retired Workshop table before indexing it in the relocated preflight test
* fix(skills): bind request authority to operator Workshop archive and restore
* test: route model directive tests through the SQLite host
Model selection now prepares placement reads through the shared-state broker.
Run the three existing directive consumers in the forked database-worker owner
while preserving production behavior and every fixture assertion.
Validation: 172 affected cases and 90 canonical routing cases passed; P2 review
found no actionable issues. The original broker failures also reproduce on
main 83a95401 and PR d5fb3076.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* test: await plugin maintenance before reload assertions
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* test: keep restart observer device identity separate
Reproduce the original authority rejection in the network-isolated E2E, then preserve the interrupted Control UI credential by giving the non-inline observer its own device identity. The same real process-restart case now passes with unchanged durable completion and single-final-reply assertions. No production authority or token-rotation changes. Independent P0/P1 review and scoped lint/format checks pass.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* test: preserve install hook coverage across Workshop cutover
Main moved the independent before_install priority/blocking test into the now-retired proposal lifecycle suite. Keep that exact case in its original owning file while removing the proposal hooks. Event and assertions are byte-identical to b300e0ed; focused test and lint pass. No production or compatibility changes.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* fix(transcripts): retain captured logical writer selectors
Keep physical database admission pinned to captured.path while preserving captured.ownerStorePath for writer authority. Reproduce the valid logical mirror rejection and protect revoked, foreign, and post-dispatch mutated requests. All 21 locked-write regressions and independent P0/P1 review pass; final style fixes only add required braces. Full local QA replay remains a coverage gap because the Crabline getMe prerequisite returned HTTP 500 before Gateway startup.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* fix(sessions): reuse settled baseline after delayed claim responses
Revalidate the authoritative generation inside the existing shared capture promise before starting another capture. Reuse durable successful and unavailable outcomes; keep settlement fencing unchanged. Deterministic before cases captured twice, after cases once. Focused 18 tests pass on the integrated candidate; 73 sibling tests and type graphs passed in the isolated repair checkout. Independent P0/P1 review is clean.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* test(doctor): assert future-version refusal at maintenance admission
The fixture ledger reader now admits its catalog before version validation, so the deliberately malformed index is its expected preliminary refusal. Preserve the outer exact newer-schema error and zero-service-stop assertions at Doctor admission. Original future-version case reproduced failure before reaching Doctor; all 45 maintenance cases pass after correcting the fixture. No production version policy changes.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* fix(state): retain current schema receipts on late reader observations
Carry already-read foreign schema markers through the existing notification. Preserve only receipts already validated for both markers; local DDL, mismatched markers, identity changes and revoked receipts remain refused. Real worker publication/adoption regression fails before and passes after. Integrated 98 focused tests pass, with 124 tested in the isolated repair checkout; independent P0/P1 review is clean. Exact QA4 causality remains unproven.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* test(codex): exercise real side-question sender policy
Distinguish explicit sender denial from missing managed search credentials. Keep disabled and cached controls with no managed search tool; production policy is unchanged.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* fix: restore CI coverage for nonblocking session titles
Align the deletion regression with metadata-only title generation and join its late writer before checking that the deleted row stays absent.
Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* test(ci): use a canonical unit-fast inventory fixture
Code Mode now belongs to agents-core and correctly runs on Node. Use a pure unit-fast fixture in the synthetic Bun envelope instead; keep every runtime inventory and conservation assertion. Original three cases fail before; all 221 runner tests pass after. No runtime selection or qualification policy changed.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* test(qa): show bounded node-load terminal errors
Include the sanitized terminal payload in the existing failing assertion without changing status, load, timeout or authority checks. The exact failing merge passed one isolated three-wave load run with this diagnostic and a disk-backed fixture temporary parent; this is non-reproduction, not a production repair or flakiness qualification.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
* fix: keep worker turns running during placement inventory refresh
Reuse the existing unconsumed-read preparation owner for maintenance inventory. Refresh reads invalidated before publication while retaining published-snapshot, unknown-write, cancellation and closure fences.
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: RomneyDa <6581799+RomneyDa@users.noreply.github.com>
This commit is contained in:
parent
bc162d9e05
commit
cfee851150
556 changed files with 9860 additions and 55400 deletions
52
apps/.i18n/native-source.json
generated
52
apps/.i18n/native-source.json
generated
|
|
@ -333,7 +333,6 @@
|
|||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsRoute.kt","kind":"ui-call","surface":"android","id":"native.android.dbe222ea8860b0cd","source":"Profile & appearance"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsRoute.kt","kind":"ui-call","surface":"android","id":"native.android.99ef92cbd4f7f8fe","source":"Providers & Models"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsRoute.kt","kind":"ui-call","surface":"android","id":"native.android.09f52861cf88d4f2","source":"Settings"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsRoute.kt","kind":"ui-call","surface":"android","id":"native.android.5ff7428eec141e51","source":"Skill Workshop"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsRoute.kt","kind":"ui-call","surface":"android","id":"native.android.1f5be29bbc17bce0","source":"Skills"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsRoute.kt","kind":"ui-call","surface":"android","id":"native.android.5e81b684e1330769","source":"System"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SettingsRoute.kt","kind":"ui-call","surface":"android","id":"native.android.fb5079a200a2f3ef","source":"Terminal"},
|
||||
|
|
@ -1369,16 +1368,13 @@
|
|||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.a9729f5cdbef366e","source":"Automation started."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.0fc5a74dbec866a1","source":"Automation updated."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.ca51a1c9a1b24826","source":"Connect the gateway to inspect ClawHub skills."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.2049005db60d0f04","source":"Connect the gateway to inspect Skill Workshop proposals."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.c8e0cc413546fde9","source":"Connect the gateway to inspect automation run history."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.b8d9b52e97ffe2c0","source":"Connect the gateway to inspect automations."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.ef4f2d1ad5de2f3b","source":"Connect the gateway to install ClawHub skills."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.02787d41f91fe185","source":"Connect the gateway to load Skill Workshop proposals."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.2fb1545cb43bf5f0","source":"Connect the gateway to load session catalogs."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.4eb8efe9ea8c2f9f","source":"Connect the gateway to manage automations."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.6367db46deb26f53","source":"Connect the gateway to open this session."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.304ffb8821c28a65","source":"Connect the gateway to search ClawHub skills."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.ae02a58e7b49306e","source":"Connect the gateway to update Skill Workshop proposals."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.a80b22b7a29180c3","source":"Connect the gateway to update skills."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.c5fd8089d1035820","source":"Connect to a Gateway to save wake words"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.837744e8aa3c8155","source":"Connected"},
|
||||
|
|
@ -1386,11 +1382,8 @@
|
|||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.7914e03e567f698c","source":"Connected (operator offline)"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.c21025024a07e967","source":"Connected (operator: $operator)"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.5e5d2fdf5bb9d763","source":"Connecting…"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.f7e64f9b9b8675e1","source":"Could not ${action.verb} Skill Workshop proposal."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.b23629d72bfebc10","source":"Could not inspect Skill Workshop proposal."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.d37c22282a1c0b5c","source":"Could not install ${slug} from ClawHub."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.cee7ae53f86ff1c8","source":"Could not load ClawHub details for ${skill.reference}."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.551ea7691d01ab36","source":"Could not load Skill Workshop proposals."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.ea55566c0ceef818","source":"Could not load approval details. Refresh and try again."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.bd645044e93a6877","source":"Could not load approvals."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.91cc378bbf16e120","source":"Could not load automation run history."},
|
||||
|
|
@ -1421,18 +1414,13 @@
|
|||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.a3909017b16578ca","source":"Gateway connection timed out. Check your network and that the Gateway is running, then retry."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.3aa7b171f760eaf3","source":"Gateway rejected the automation run."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.aa5a155373c94365","source":"Gateway returned an invalid automation."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.b4ee90cdbf0ebbfb","source":"Gateway returned status '$statusLabel' after ${action.verb}."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.3dd02e8b5c8d3052","source":"No prompt"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.dad3625d27b761a4","source":"Offline"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.59857d295b71b6f6","source":"Paired device removed."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.0e27fbb1014bad1d","source":"Pairing request rejected."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.221a6a9d33f0c721","source":"Proposal applied."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.a7ada63ca275548e","source":"Proposal quarantined."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.a5cfcdfeeee691fc","source":"Proposal rejected."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.4eb766157a1ba746","source":"Provider models loaded, but readiness is unavailable."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.377c08edb4a24718","source":"Reconnecting…"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.fa2fac51b4d2989d","source":"Resolution outcome unknown. Actions stay disabled until the Gateway record is verified."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.3981fcbf350402fd","source":"Skill Workshop proposal actions require operator.admin scope."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.44fb00ccaf9544cc","source":"Some models could not be refreshed. Tap Refresh to retry."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.a5916add82ec0498","source":"Start failed: $message"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.93c22448c6d9be7b","source":"The Gateway still shows this approval as pending. Review it before trying again."},
|
||||
|
|
@ -1444,10 +1432,6 @@
|
|||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.8072001855422732","source":"This session cannot be continued."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.8a282588633bbc18","source":"Update your Gateway to view provider model config."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.9ce3afc4d0d3732a","source":"Wake words saved"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.5731193296f6ca25","source":"apply"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.0253be221b0f9263","source":"quarantine"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.731933bdb3e5416f","source":"reject"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-call","surface":"android","id":"native.android.15e2709f1d0cc737","source":"unknown"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-named-argument","surface":"android","id":"native.android.837744e8aa3c8155","source":"Connected"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-named-argument","surface":"android","id":"native.android.5e5d2fdf5bb9d763","source":"Connecting…"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt","kind":"ui-named-argument","surface":"android","id":"native.android.dad3625d27b761a4","source":"Offline"},
|
||||
|
|
@ -1719,42 +1703,6 @@
|
|||
{"path":"apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessionSidebar.swift","kind":"ui-localized-call","surface":"apple","id":"native.apple.2d85575ed1bdaceb","source":"The thread and its transcript are removed from the gateway."},
|
||||
{"path":"apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSessionSidebar.swift","kind":"ui-localized-call","surface":"apple","id":"native.apple.2afc7addbd339c16","source":"Thread name"},
|
||||
{"path":"apps/ios/Sources/Status/VoiceWakeToast.swift","kind":"ui-modifier","surface":"apple","id":"native.apple.edd16a0d56c61dc2","source":"Voice Wake triggered"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.8d00775305a3fc74","source":"Applied"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.b973587e84c65063","source":"Apply"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.b758a4df60af280c","source":"Apply proposal?"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.56ba8fc553a399af","source":"Apply, reject, and quarantine require operator.admin scope. Reconnect with shared gateway auth or approve an operator.admin device scope upgrade to enable lifecycle actions."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.ac43ebebc177b67f","source":"Connect to a Gateway to load Skill Workshop proposals."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.b5fc14ea1bed1613","source":"Default agent"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.9246a0bf9cf3b80d","source":"Gateway offline"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.8e3561cf3f9ef366","source":"Held"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.2de1cf5173f6e5d4","source":"Inspect"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.40d6f39ca9212457","source":"Inspect this proposal to load its markdown."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.87987fee763f208a","source":"Inspecting"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.48079eb3e4909507","source":"Kind"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.ccc5760f719f1aac","source":"Loading"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.caa655dfb72453d2","source":"Matching proposals will appear here after agents create reusable skill drafts."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.2d831fd5829656fc","source":"No proposals"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.ba9093fc4494e812","source":"Open"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.d44392114f134b3b","source":"Pending"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.bbdbd5723a2288ef","source":"Quarantine"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.4f0253b22e3c699d","source":"Quarantine proposal?"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.762f31cc51a61c6a","source":"Refresh"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.0bba7f5a7f536bfc","source":"Refreshing"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.d6d7d3bb54de86f7","source":"Reject"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.2faa88096a79b17a","source":"Reject proposal?"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.b7d8a4c3e34cc092","source":"Rejected"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.3b7bab21e700d42b","source":"Review generated skill proposals before they become live skills."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.8263e0efaec65c14","source":"Search proposals"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.db30a49dac4dedb3","source":"Selected"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.df62ed35fb847c60","source":"Skill Workshop inspect and apply actions"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.edbfa88792176f3f","source":"Skill Workshop reject and quarantine actions"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.79c5d6232b243c1d","source":"Support Files"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.845376b224b1e70b","source":"This will apply \"$proposalTitle\" and refresh Skill Workshop state from the gateway."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.d19557a5bc60a77b","source":"This will quarantine \"$proposalTitle\" and refresh Skill Workshop state from the gateway."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.b3f73faa993fddb0","source":"This will reject \"$proposalTitle\" and refresh Skill Workshop state from the gateway."},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.39785345ba2c3bff","source":"Unknown"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.cb4541d4534f81bd","source":"Updated"},
|
||||
{"path":"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt","kind":"ui-call","surface":"android","id":"native.android.e15885377b28fa59","source":"Working"},
|
||||
{"path":"apps/ios/Sources/Design/ChatProTab.swift","kind":"conditional-branch","surface":"apple","id":"native.apple.98ff0a205ca51067","source":"Collapsed"},
|
||||
{"path":"apps/ios/Sources/Design/ChatProTab.swift","kind":"conditional-branch","surface":"apple","id":"native.apple.d22afdc8f4adfb5d","source":"Connected"},
|
||||
{"path":"apps/ios/Sources/Design/ChatProTab.swift","kind":"conditional-branch","surface":"apple","id":"native.apple.5936a4a11d4f206a","source":"Expanded"},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ OpenClaw Android is the officially released Google Play app. It connects to an O
|
|||
- Setup offers optional notifications, microphone, camera, and location access; other permissions are under **Additional features**. **Request all** asks for the available runtime permissions without enabling the camera or location features; Android handles consent through its system prompts. Notification access stays separate under **Special access**. Continue without granting access, or enable phone capabilities individually or later in Settings. Blocked individual setup requests offer Android Settings. Biometric locking, Gateway/chat notifications, and authenticated background presence are supported.
|
||||
- SMS is available only in third-party builds on supported telephony devices. Setup distinguishes partial read/send access. Android Settings can recover user denial, but installer or device-policy restrictions may leave SMS unavailable; OpenClaw cannot override them. Returning from Settings rereads the actual grants without restarting Gateway setup.
|
||||
- View the phone's memory and disk meters on the Control UI Devices page. Connected Android nodes report host resource stats immediately and every 60 seconds; disk meters require an available storage sample and a Gateway that supports host stats.
|
||||
- Manage installed skills and Gateway-verified ClawHub releases, review Skill Workshop proposals, and inspect or edit automations with the required Gateway access.
|
||||
- Manage installed skills and Gateway-verified ClawHub releases, and inspect or edit automations with the required Gateway access.
|
||||
- Use the Wear OS companion for sessions, replies, aborts, and realtime Talk through the paired phone without storing Gateway credentials on the watch.
|
||||
|
||||
## Open in Android Studio
|
||||
|
|
|
|||
|
|
@ -569,13 +569,6 @@ class MainViewModel internal constructor(
|
|||
val skillMutationKeys: StateFlow<Set<String>> = runtimeState(initial = emptySet()) { it.skillMutationKeys }
|
||||
val clawHubSkillSearchState: StateFlow<GatewayClawHubSkillSearchState> =
|
||||
runtimeState(initial = GatewayClawHubSkillSearchState()) { it.clawHubSkillSearchState }
|
||||
val skillWorkshopSummary: StateFlow<GatewaySkillWorkshopSummary> =
|
||||
runtimeState(initial = GatewaySkillWorkshopSummary(proposals = emptyList())) { it.skillWorkshopSummary }
|
||||
val skillWorkshopRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.skillWorkshopRefreshing }
|
||||
val skillWorkshopErrorText: StateFlow<String?> = runtimeState(initial = null) { it.skillWorkshopErrorText }
|
||||
val skillWorkshopNoticeText: StateFlow<String?> = runtimeState(initial = null) { it.skillWorkshopNoticeText }
|
||||
val skillWorkshopInspectingProposalId: StateFlow<String?> = runtimeState(initial = null) { it.skillWorkshopInspectingProposalId }
|
||||
val skillWorkshopMutatingProposalId: StateFlow<String?> = runtimeState(initial = null) { it.skillWorkshopMutatingProposalId }
|
||||
val nodesDevicesSummary: StateFlow<GatewayNodesDevicesSummary> =
|
||||
runtimeState(initial = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())) { it.nodesDevicesSummary }
|
||||
val nodesDevicesRefreshing: StateFlow<Boolean> = runtimeState(initial = false) { it.nodesDevicesRefreshing }
|
||||
|
|
@ -1461,30 +1454,6 @@ class MainViewModel internal constructor(
|
|||
|
||||
fun refreshSkills(): Unit = ensureRuntime().refreshSkills()
|
||||
|
||||
fun refreshSkillWorkshopProposals(agentId: String? = null): Unit = ensureRuntime().refreshSkillWorkshopProposals(agentId = agentId)
|
||||
|
||||
fun resetSkillWorkshopAgentScope(agentId: String? = null): Unit = ensureRuntime().resetSkillWorkshopAgentScope(agentId = agentId)
|
||||
|
||||
fun inspectSkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String? = null,
|
||||
): Unit = ensureRuntime().inspectSkillWorkshopProposal(proposalId = proposalId, agentId = agentId)
|
||||
|
||||
fun applySkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String? = null,
|
||||
): Unit = ensureRuntime().applySkillWorkshopProposal(proposalId = proposalId, agentId = agentId)
|
||||
|
||||
fun rejectSkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String? = null,
|
||||
): Unit = ensureRuntime().rejectSkillWorkshopProposal(proposalId = proposalId, agentId = agentId)
|
||||
|
||||
fun quarantineSkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String? = null,
|
||||
): Unit = ensureRuntime().quarantineSkillWorkshopProposal(proposalId = proposalId, agentId = agentId)
|
||||
|
||||
fun setSkillEnabled(
|
||||
skillKey: String,
|
||||
enabled: Boolean,
|
||||
|
|
|
|||
|
|
@ -422,35 +422,6 @@ internal fun verifyGatewayDevicePairingMutation(
|
|||
|
||||
internal fun buildGatewayDevicePairingMutationParams(mutation: GatewayDevicePairingMutation): JsonObject = buildJsonObject { put(mutation.action.idKey, JsonPrimitive(mutation.targetId)) }
|
||||
|
||||
internal enum class SkillWorkshopGatewayAction(
|
||||
val methodSuffix: String,
|
||||
val expectedStatus: String,
|
||||
val notice: NativeText,
|
||||
val verb: NativeText,
|
||||
) {
|
||||
Apply("apply", "applied", nativeText("Proposal applied."), nativeText("apply")),
|
||||
Reject("reject", "rejected", nativeText("Proposal rejected."), nativeText("reject")),
|
||||
Quarantine("quarantine", "quarantined", nativeText("Proposal quarantined."), nativeText("quarantine")),
|
||||
}
|
||||
|
||||
internal fun skillWorkshopUnexpectedStatusText(
|
||||
status: String?,
|
||||
action: SkillWorkshopGatewayAction,
|
||||
): NativeText {
|
||||
val statusText = status?.takeIf { it.isNotBlank() }?.let(::verbatimText) ?: nativeText("unknown")
|
||||
return nativeText(
|
||||
"Gateway returned status '\$statusLabel' after \${action.verb}.",
|
||||
statusText,
|
||||
action.verb,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun skillWorkshopActionFailureText(action: SkillWorkshopGatewayAction): NativeText =
|
||||
nativeText(
|
||||
"Could not \${action.verb} Skill Workshop proposal.",
|
||||
action.verb,
|
||||
)
|
||||
|
||||
internal data class PendingNotificationNodeEvent(
|
||||
val event: String,
|
||||
val payloadJson: String?,
|
||||
|
|
@ -1278,21 +1249,6 @@ class NodeRuntime internal constructor(
|
|||
private val clawHubSkillSearchSeq = AtomicLong(0)
|
||||
private val clawHubSkillReviewSeq = AtomicLong(0)
|
||||
private val clawHubSkillInstallMutex = Mutex()
|
||||
private val _skillWorkshopSummary = MutableStateFlow(GatewaySkillWorkshopSummary(proposals = emptyList()))
|
||||
val skillWorkshopSummary: StateFlow<GatewaySkillWorkshopSummary> = _skillWorkshopSummary.asStateFlow()
|
||||
private val _skillWorkshopRefreshing = MutableStateFlow(false)
|
||||
val skillWorkshopRefreshing: StateFlow<Boolean> = _skillWorkshopRefreshing.asStateFlow()
|
||||
private val _skillWorkshopErrorText = MutableStateFlow<NativeText?>(null)
|
||||
val skillWorkshopErrorText: StateFlow<String?> = _skillWorkshopErrorText.resolveOptionalNativeText()
|
||||
private val _skillWorkshopNoticeText = MutableStateFlow<NativeText?>(null)
|
||||
val skillWorkshopNoticeText: StateFlow<String?> = _skillWorkshopNoticeText.resolveOptionalNativeText()
|
||||
private val _skillWorkshopInspectingProposalId = MutableStateFlow<String?>(null)
|
||||
val skillWorkshopInspectingProposalId: StateFlow<String?> = _skillWorkshopInspectingProposalId.asStateFlow()
|
||||
private val _skillWorkshopMutatingProposalId = MutableStateFlow<String?>(null)
|
||||
val skillWorkshopMutatingProposalId: StateFlow<String?> = _skillWorkshopMutatingProposalId.asStateFlow()
|
||||
private val skillWorkshopListSeq = AtomicLong(0)
|
||||
private val skillWorkshopInspectSeq = AtomicLong(0)
|
||||
private val skillWorkshopMutationSeq = AtomicLong(0)
|
||||
private val emptyNodesDevicesSummary =
|
||||
GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())
|
||||
private val _nodesDevicesSummary = MutableStateFlow(emptyNodesDevicesSummary)
|
||||
|
|
@ -1789,15 +1745,6 @@ class NodeRuntime internal constructor(
|
|||
clawHubSkillSearchSeq.incrementAndGet()
|
||||
clawHubSkillReviewSeq.incrementAndGet()
|
||||
_clawHubSkillSearchState.value = GatewayClawHubSkillSearchState()
|
||||
_skillWorkshopSummary.value = GatewaySkillWorkshopSummary(proposals = emptyList())
|
||||
_skillWorkshopRefreshing.value = false
|
||||
_skillWorkshopErrorText.value = null
|
||||
_skillWorkshopNoticeText.value = null
|
||||
_skillWorkshopInspectingProposalId.value = null
|
||||
_skillWorkshopMutatingProposalId.value = null
|
||||
skillWorkshopListSeq.incrementAndGet()
|
||||
skillWorkshopInspectSeq.incrementAndGet()
|
||||
skillWorkshopMutationSeq.incrementAndGet()
|
||||
_nodesDevicesSummary.value = emptyNodesDevicesSummary
|
||||
_nodesDevicesRefreshing.value = false
|
||||
_nodesDevicesErrorText.value = null
|
||||
|
|
@ -2711,63 +2658,6 @@ class NodeRuntime internal constructor(
|
|||
)
|
||||
}
|
||||
|
||||
fun refreshSkillWorkshopProposals(agentId: String? = null) {
|
||||
scope.launch {
|
||||
refreshSkillWorkshopProposalsFromGateway(agentId = agentId)
|
||||
}
|
||||
}
|
||||
|
||||
fun resetSkillWorkshopAgentScope(agentId: String? = null) {
|
||||
val normalizedAgentId = normalizeSkillWorkshopAgentId(agentId)
|
||||
skillWorkshopListSeq.incrementAndGet()
|
||||
skillWorkshopInspectSeq.incrementAndGet()
|
||||
skillWorkshopMutationSeq.incrementAndGet()
|
||||
_skillWorkshopSummary.value = GatewaySkillWorkshopSummary(agentId = normalizedAgentId, proposals = emptyList())
|
||||
_skillWorkshopRefreshing.value = false
|
||||
_skillWorkshopErrorText.value = null
|
||||
_skillWorkshopNoticeText.value = null
|
||||
_skillWorkshopInspectingProposalId.value = null
|
||||
_skillWorkshopMutatingProposalId.value = null
|
||||
}
|
||||
|
||||
fun inspectSkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String? = null,
|
||||
) {
|
||||
val normalized = proposalId.trim()
|
||||
if (normalized.isEmpty()) return
|
||||
scope.launch {
|
||||
requestSkillWorkshopProposal(proposalId = normalized, agentId = agentId)
|
||||
}
|
||||
}
|
||||
|
||||
fun applySkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String? = null,
|
||||
) = mutateSkillWorkshopProposal(proposalId = proposalId, agentId = agentId, action = SkillWorkshopGatewayAction.Apply)
|
||||
|
||||
fun rejectSkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String? = null,
|
||||
) = mutateSkillWorkshopProposal(proposalId = proposalId, agentId = agentId, action = SkillWorkshopGatewayAction.Reject)
|
||||
|
||||
fun quarantineSkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String? = null,
|
||||
) = mutateSkillWorkshopProposal(proposalId = proposalId, agentId = agentId, action = SkillWorkshopGatewayAction.Quarantine)
|
||||
|
||||
private fun mutateSkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String?,
|
||||
action: SkillWorkshopGatewayAction,
|
||||
) {
|
||||
val normalized = proposalId.trim()
|
||||
if (normalized.isEmpty()) return
|
||||
scope.launch {
|
||||
requestSkillWorkshopProposal(proposalId = normalized, agentId = agentId, action = action)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshNodesDevices() = launchGatewayRefresh { refreshNodesDevicesFromGateway() }
|
||||
|
||||
fun approveNodeCapabilities(expectedRequestId: String) {
|
||||
|
|
@ -7308,184 +7198,6 @@ class NodeRuntime internal constructor(
|
|||
?: isClawHubSkillInstalledByReference(skills, slug)
|
||||
}
|
||||
|
||||
private suspend fun refreshSkillWorkshopProposalsFromGateway(agentId: String?) {
|
||||
val listSeq = skillWorkshopListSeq.incrementAndGet()
|
||||
val requestAgentId = normalizeSkillWorkshopAgentId(agentId)
|
||||
val gatewayScope = captureGatewayDataScope()
|
||||
if (gatewayScope == null || !operatorConnected) {
|
||||
_skillWorkshopSummary.value = GatewaySkillWorkshopSummary(agentId = requestAgentId, proposals = emptyList())
|
||||
_skillWorkshopRefreshing.value = false
|
||||
_skillWorkshopErrorText.value = nativeText("Connect the gateway to load Skill Workshop proposals.")
|
||||
return
|
||||
}
|
||||
publishGatewayData(gatewayScope) {
|
||||
_skillWorkshopRefreshing.value = true
|
||||
_skillWorkshopErrorText.value = null
|
||||
if (_skillWorkshopSummary.value.agentId != requestAgentId) {
|
||||
_skillWorkshopSummary.value = GatewaySkillWorkshopSummary(agentId = requestAgentId, proposals = emptyList())
|
||||
_skillWorkshopNoticeText.value = null
|
||||
_skillWorkshopInspectingProposalId.value = null
|
||||
_skillWorkshopMutatingProposalId.value = null
|
||||
skillWorkshopInspectSeq.incrementAndGet()
|
||||
skillWorkshopMutationSeq.incrementAndGet()
|
||||
}
|
||||
}
|
||||
try {
|
||||
val root =
|
||||
requestGatewayObject(
|
||||
gatewayScope,
|
||||
"skills.proposals.list",
|
||||
skillWorkshopParams(agentId = agentId).toString(),
|
||||
)
|
||||
val previousById =
|
||||
_skillWorkshopSummary.value
|
||||
.takeIf { it.agentId == requestAgentId }
|
||||
?.proposals
|
||||
?.associateBy { it.id }
|
||||
.orEmpty()
|
||||
val proposals = parseSkillWorkshopProposals(root?.get("proposals") as? JsonArray, previousById)
|
||||
publishGatewayData(gatewayScope) {
|
||||
if (skillWorkshopListSeq.get() == listSeq && _skillWorkshopSummary.value.agentId == requestAgentId) {
|
||||
_skillWorkshopSummary.value = GatewaySkillWorkshopSummary(agentId = requestAgentId, proposals = proposals)
|
||||
}
|
||||
}
|
||||
} catch (err: CancellationException) {
|
||||
throw err
|
||||
} catch (_: Throwable) {
|
||||
publishGatewayData(gatewayScope) {
|
||||
if (skillWorkshopListSeq.get() == listSeq && _skillWorkshopSummary.value.agentId == requestAgentId) {
|
||||
_skillWorkshopErrorText.value = nativeText("Could not load Skill Workshop proposals.")
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
publishGatewayData(gatewayScope) {
|
||||
if (skillWorkshopListSeq.get() == listSeq && _skillWorkshopSummary.value.agentId == requestAgentId) {
|
||||
_skillWorkshopRefreshing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestSkillWorkshopProposal(
|
||||
proposalId: String,
|
||||
agentId: String?,
|
||||
action: SkillWorkshopGatewayAction? = null,
|
||||
) {
|
||||
var requestSeq: Long? = null
|
||||
val requestAgentId = normalizeSkillWorkshopAgentId(agentId)
|
||||
if (action != null && !operatorAdminScopeAvailable.value) {
|
||||
_skillWorkshopErrorText.value = nativeText("Skill Workshop proposal actions require operator.admin scope.")
|
||||
return
|
||||
}
|
||||
val gatewayScope = captureGatewayDataScope()
|
||||
if (gatewayScope == null || !operatorConnected) {
|
||||
_skillWorkshopErrorText.value =
|
||||
if (action == null) {
|
||||
nativeText("Connect the gateway to inspect Skill Workshop proposals.")
|
||||
} else {
|
||||
nativeText("Connect the gateway to update Skill Workshop proposals.")
|
||||
}
|
||||
return
|
||||
}
|
||||
val sequence = if (action == null) skillWorkshopInspectSeq else skillWorkshopMutationSeq
|
||||
val activeProposal = if (action == null) _skillWorkshopInspectingProposalId else _skillWorkshopMutatingProposalId
|
||||
publishGatewayData(gatewayScope) {
|
||||
val currentSummary = _skillWorkshopSummary.value
|
||||
if (
|
||||
currentSummary.agentId == requestAgentId &&
|
||||
currentSummary.proposals.any { it.id == proposalId } &&
|
||||
_skillWorkshopMutatingProposalId.value == null
|
||||
) {
|
||||
requestSeq = sequence.incrementAndGet()
|
||||
if (action != null) {
|
||||
// Mutations retire detail reads before changing the proposal's lifecycle state.
|
||||
skillWorkshopInspectSeq.incrementAndGet()
|
||||
_skillWorkshopInspectingProposalId.value = null
|
||||
}
|
||||
activeProposal.value = proposalId
|
||||
_skillWorkshopErrorText.value = null
|
||||
if (action != null) _skillWorkshopNoticeText.value = null
|
||||
}
|
||||
}
|
||||
if (requestSeq == null) return
|
||||
|
||||
fun current() = sequence.get() == requestSeq && _skillWorkshopSummary.value.agentId == requestAgentId
|
||||
try {
|
||||
val root =
|
||||
requestGatewayObject(
|
||||
gatewayScope,
|
||||
"skills.proposals.${action?.methodSuffix ?: "inspect"}",
|
||||
skillWorkshopParams(agentId = agentId, proposalId = proposalId).toString(),
|
||||
)
|
||||
val previous =
|
||||
_skillWorkshopSummary.value
|
||||
.takeIf { it.agentId == requestAgentId }
|
||||
?.proposals
|
||||
?.firstOrNull { it.id == proposalId }
|
||||
if (action == null) {
|
||||
val inspected =
|
||||
parseSkillWorkshopProposalInspect(root, previous)
|
||||
?: throw IllegalStateException("skills.proposals.inspect returned no proposal")
|
||||
publishGatewayData(gatewayScope) {
|
||||
val currentSummary = _skillWorkshopSummary.value
|
||||
if (
|
||||
sequence.get() == requestSeq &&
|
||||
currentSummary.agentId == requestAgentId &&
|
||||
currentSummary.proposals.any { it.id == proposalId }
|
||||
) {
|
||||
_skillWorkshopSummary.value = _skillWorkshopSummary.value.withProposal(inspected)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val updatedProposal = parseSkillWorkshopProposalActionResult(root, previous)
|
||||
val mutationConfirmed =
|
||||
withCurrentGatewayData(gatewayScope) {
|
||||
if (!current()) return@withCurrentGatewayData false
|
||||
if (updatedProposal?.status == action.expectedStatus) {
|
||||
_skillWorkshopSummary.value = _skillWorkshopSummary.value.withProposal(updatedProposal)
|
||||
_skillWorkshopNoticeText.value = action.notice
|
||||
true
|
||||
} else {
|
||||
_skillWorkshopErrorText.value = skillWorkshopUnexpectedStatusText(updatedProposal?.status, action)
|
||||
false
|
||||
}
|
||||
}
|
||||
if (!mutationConfirmed) return
|
||||
if (withCurrentGatewayData(gatewayScope, ::current)) refreshSkillWorkshopProposalsFromGateway(agentId = agentId)
|
||||
}
|
||||
} catch (err: CancellationException) {
|
||||
throw err
|
||||
} catch (_: Throwable) {
|
||||
publishGatewayData(gatewayScope) {
|
||||
if (current()) {
|
||||
_skillWorkshopErrorText.value =
|
||||
if (action == null) {
|
||||
nativeText("Could not inspect Skill Workshop proposal.")
|
||||
} else {
|
||||
skillWorkshopActionFailureText(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
publishGatewayData(gatewayScope) {
|
||||
if (current()) activeProposal.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeSkillWorkshopAgentId(agentId: String?): String = agentId?.trim().orEmpty()
|
||||
|
||||
private fun skillWorkshopParams(
|
||||
agentId: String?,
|
||||
proposalId: String? = null,
|
||||
): JsonObject =
|
||||
buildJsonObject {
|
||||
val normalizedAgentId = agentId?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (normalizedAgentId != null) put("agentId", JsonPrimitive(normalizedAgentId))
|
||||
val normalizedProposalId = proposalId?.trim()?.takeIf { it.isNotEmpty() }
|
||||
if (normalizedProposalId != null) put("proposalId", JsonPrimitive(normalizedProposalId))
|
||||
}
|
||||
|
||||
private suspend fun mutateDevicePairingOnGateway(
|
||||
gatewayScope: GatewayDataScope,
|
||||
mutation: GatewayDevicePairingMutation,
|
||||
|
|
@ -8558,82 +8270,6 @@ class NodeRuntime internal constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun parseSkillWorkshopProposals(
|
||||
proposals: JsonArray?,
|
||||
previousById: Map<String, GatewaySkillWorkshopProposal>,
|
||||
): List<GatewaySkillWorkshopProposal> =
|
||||
proposals
|
||||
.mapObjects { record ->
|
||||
val id = record.nonBlankString("id") ?: return@mapObjects null
|
||||
parseSkillWorkshopProposalRecord(record, previousById[id], listEntry = true)
|
||||
}.sortedByDescending { it.updatedAt }
|
||||
|
||||
private fun parseSkillWorkshopProposalInspect(
|
||||
root: JsonObject?,
|
||||
previous: GatewaySkillWorkshopProposal?,
|
||||
): GatewaySkillWorkshopProposal? {
|
||||
val source = root ?: return null
|
||||
val record = source["record"].asObjectOrNull() ?: return null
|
||||
return parseSkillWorkshopProposalRecord(record, previous)?.copy(
|
||||
content = stripSkillWorkshopFrontmatter(source["content"].asStringOrNull().orEmpty()),
|
||||
supportFiles = parseSkillWorkshopSupportFiles(source["supportFiles"] as? JsonArray),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseSkillWorkshopProposalActionResult(
|
||||
root: JsonObject?,
|
||||
previous: GatewaySkillWorkshopProposal?,
|
||||
): GatewaySkillWorkshopProposal? {
|
||||
val record =
|
||||
root?.get("record").asObjectOrNull()
|
||||
?: root?.takeIf { it.nonBlankString("status") != null }
|
||||
?: return null
|
||||
val proposal = parseSkillWorkshopProposalRecord(record, previous) ?: return null
|
||||
return proposal.copy(scanState = record["scan"].asObjectOrNull().nonBlankString("state") ?: proposal.scanState)
|
||||
}
|
||||
|
||||
private fun parseSkillWorkshopProposalRecord(
|
||||
record: JsonObject,
|
||||
previous: GatewaySkillWorkshopProposal?,
|
||||
listEntry: Boolean = false,
|
||||
): GatewaySkillWorkshopProposal? {
|
||||
// Lists replace metadata; detail/action records may omit it. Only unchanged list
|
||||
// revisions retain inspected content, while action records preserve it across updates.
|
||||
val fallback = previous.takeUnless { listEntry }
|
||||
val id = record.nonBlankString("id") ?: fallback?.id ?: return null
|
||||
val target = if (listEntry) record else record["target"].asObjectOrNull()
|
||||
val updatedAt = record.nonBlankString("updatedAt").orEmpty()
|
||||
val details = previous?.takeIf { !listEntry || it.updatedAt == updatedAt }
|
||||
return GatewaySkillWorkshopProposal(
|
||||
id = id,
|
||||
kind = record.nonBlankString("kind") ?: fallback?.kind ?: "proposal",
|
||||
status = record.nonBlankString("status") ?: fallback?.status ?: "pending",
|
||||
title = record.nonBlankString("title") ?: target?.nonBlankString("skillName") ?: fallback?.title ?: id,
|
||||
description = record.nonBlankString("description") ?: fallback?.description,
|
||||
skillName = target?.nonBlankString("skillName") ?: fallback?.skillName ?: id,
|
||||
skillKey = target?.nonBlankString("skillKey") ?: fallback?.skillKey ?: id,
|
||||
createdAt = record.nonBlankString("createdAt") ?: fallback?.createdAt.orEmpty(),
|
||||
updatedAt = updatedAt.ifEmpty { fallback?.updatedAt.orEmpty() },
|
||||
scanState = record.nonBlankString("scanState") ?: fallback?.scanState,
|
||||
content = details?.content,
|
||||
supportFiles = details?.supportFiles.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseSkillWorkshopSupportFiles(files: JsonArray?): List<GatewaySkillWorkshopSupportFile> =
|
||||
files.mapObjects { obj ->
|
||||
val path = obj.nonBlankString("path") ?: return@mapObjects null
|
||||
GatewaySkillWorkshopSupportFile(
|
||||
path = path,
|
||||
content = obj["content"].asStringOrNull()?.takeIf { it.isNotEmpty() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun stripSkillWorkshopFrontmatter(content: String): String {
|
||||
val withoutFrontmatter = content.replace(Regex("(?s)^---\\r?\\n.*?\\r?\\n---\\r?\\n?"), "")
|
||||
return withoutFrontmatter.trim()
|
||||
}
|
||||
|
||||
private fun skillMissingCount(missing: JsonObject?): Int = listOf("bins", "env", "config", "os").sumOf { key -> (missing?.get(key) as? JsonArray)?.size ?: 0 }
|
||||
|
||||
private fun parsePendingDevices(devices: JsonArray?): List<GatewayPendingDeviceSummary> =
|
||||
|
|
@ -8977,38 +8613,6 @@ data class GatewaySkillsSummary(
|
|||
val skills: List<GatewaySkillSummary>,
|
||||
)
|
||||
|
||||
data class GatewaySkillWorkshopSummary(
|
||||
val agentId: String = "",
|
||||
val proposals: List<GatewaySkillWorkshopProposal>,
|
||||
) {
|
||||
fun withProposal(proposal: GatewaySkillWorkshopProposal): GatewaySkillWorkshopSummary =
|
||||
copy(
|
||||
proposals =
|
||||
(proposals.filterNot { it.id == proposal.id } + proposal)
|
||||
.sortedByDescending { it.updatedAt },
|
||||
)
|
||||
}
|
||||
|
||||
data class GatewaySkillWorkshopProposal(
|
||||
val id: String,
|
||||
val kind: String,
|
||||
val status: String,
|
||||
val title: String,
|
||||
val description: String?,
|
||||
val skillName: String,
|
||||
val skillKey: String,
|
||||
val createdAt: String,
|
||||
val updatedAt: String,
|
||||
val scanState: String?,
|
||||
val content: String? = null,
|
||||
val supportFiles: List<GatewaySkillWorkshopSupportFile> = emptyList(),
|
||||
)
|
||||
|
||||
data class GatewaySkillWorkshopSupportFile(
|
||||
val path: String,
|
||||
val content: String?,
|
||||
)
|
||||
|
||||
data class GatewaySkillSummary(
|
||||
val skillKey: String,
|
||||
val name: String,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ data class GatewayCredentials(
|
|||
|
||||
internal val defaultSidebarVisiblePages = listOf("home", "threads", "skills", "work")
|
||||
internal val defaultSidebarPageOrder =
|
||||
defaultSidebarVisiblePages + listOf("agents", "automations", "usage", "skill-workshop", "dreaming", "terminal", "desktop")
|
||||
defaultSidebarVisiblePages + listOf("agents", "automations", "usage", "dreaming", "terminal", "desktop")
|
||||
|
||||
internal fun sanitizeSidebarPageOrder(pageIds: List<String>): List<String> {
|
||||
val knownIds = defaultSidebarPageOrder.toSet()
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ internal enum class SettingsRoute(
|
|||
CronJobs(nativeText("Automations"), ClawIcons.Automations, SettingsCategory.Workspace),
|
||||
Usage(nativeText("Usage"), ClawIcons.Usage, SettingsCategory.Workspace),
|
||||
Skills(nativeText("Skills"), ClawIcons.Skills, SettingsCategory.Workspace),
|
||||
SkillWorkshop(nativeText("Skill Workshop"), ClawIcons.SkillWorkshop, SettingsCategory.Workspace),
|
||||
SystemAgent(nativeText("OpenClaw"), ClawIcons.OpenClaw, null),
|
||||
NodesDevices(nativeText("Nodes & Devices"), ClawIcons.Devices, SettingsCategory.Connection),
|
||||
Channels(nativeText("Channels"), ClawIcons.Channels, SettingsCategory.Connection),
|
||||
|
|
|
|||
|
|
@ -192,7 +192,6 @@ internal fun SettingsDetailScreen(
|
|||
SettingsRoute.CronJobs -> CronJobsSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.Usage -> UsageSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.Skills -> SkillsSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.SkillWorkshop -> SkillWorkshopSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.SystemAgent -> SystemAgentSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.NodesDevices -> NodesDevicesSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
SettingsRoute.Channels -> ChannelsSettingsScreen(viewModel = viewModel, onBack = onBack)
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@ internal enum class SidebarDestination(
|
|||
Agents(stableId = "agents", settingsRoute = SettingsRoute.Agents),
|
||||
Automations(stableId = "automations", settingsRoute = SettingsRoute.CronJobs),
|
||||
Usage(stableId = "usage", settingsRoute = SettingsRoute.Usage),
|
||||
SkillWorkshop(stableId = "skill-workshop", settingsRoute = SettingsRoute.SkillWorkshop),
|
||||
Dreaming(stableId = "dreaming", settingsRoute = SettingsRoute.Dreaming),
|
||||
Terminal(stableId = "terminal", settingsRoute = SettingsRoute.Terminal),
|
||||
Desktop(stableId = "desktop", settingsRoute = SettingsRoute.Desktop),
|
||||
|
|
|
|||
|
|
@ -1,624 +0,0 @@
|
|||
package ai.openclaw.app.ui
|
||||
|
||||
import ai.openclaw.app.GatewayAgentSummary
|
||||
import ai.openclaw.app.GatewaySkillWorkshopProposal
|
||||
import ai.openclaw.app.GatewaySkillWorkshopSummary
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.i18n.NativeText
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.i18n.nativeText
|
||||
import ai.openclaw.app.i18n.resolveNativeText
|
||||
import ai.openclaw.app.selectableAgents
|
||||
import ai.openclaw.app.ui.design.ClawPanel
|
||||
import ai.openclaw.app.ui.design.ClawPrimaryButton
|
||||
import ai.openclaw.app.ui.design.ClawSecondaryButton
|
||||
import ai.openclaw.app.ui.design.ClawSegmentedControl
|
||||
import ai.openclaw.app.ui.design.ClawStatus
|
||||
import ai.openclaw.app.ui.design.ClawStatusPill
|
||||
import ai.openclaw.app.ui.design.ClawTextField
|
||||
import ai.openclaw.app.ui.design.ClawTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private val skillWorkshopFilters =
|
||||
listOf(
|
||||
"pending" to "Pending",
|
||||
"held" to "Held",
|
||||
"applied" to "Applied",
|
||||
"rejected" to "Rejected",
|
||||
"all" to "All",
|
||||
)
|
||||
|
||||
@Composable
|
||||
internal fun SkillWorkshopSettingsScreen(
|
||||
viewModel: MainViewModel,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val summary by viewModel.skillWorkshopSummary.collectAsState()
|
||||
val refreshing by viewModel.skillWorkshopRefreshing.collectAsState()
|
||||
val errorText by viewModel.skillWorkshopErrorText.collectAsState()
|
||||
val noticeText by viewModel.skillWorkshopNoticeText.collectAsState()
|
||||
val inspectingProposalId by viewModel.skillWorkshopInspectingProposalId.collectAsState()
|
||||
val mutatingProposalId by viewModel.skillWorkshopMutatingProposalId.collectAsState()
|
||||
val isConnected by viewModel.isConnected.collectAsState()
|
||||
val operatorAdminScopeAvailable by viewModel.operatorAdminScopeAvailable.collectAsState()
|
||||
val agents by viewModel.gatewayAgents.collectAsState()
|
||||
val defaultAgentId by viewModel.gatewayDefaultAgentId.collectAsState()
|
||||
|
||||
var statusFilter by rememberSaveable { mutableStateOf("pending") }
|
||||
var query by rememberSaveable { mutableStateOf("") }
|
||||
var selectedAgentId by rememberSaveable { mutableStateOf("") }
|
||||
var selectedProposalId by rememberSaveable { mutableStateOf<String?>(null) }
|
||||
var pendingAction by remember { mutableStateOf<SkillWorkshopPendingAction?>(null) }
|
||||
val selectedAgentParam = selectedAgentId.trim().takeIf { it.isNotEmpty() }
|
||||
val visibleProposals = skillWorkshopVisibleProposals(summary, selectedAgentParam)
|
||||
val filteredProposals = skillWorkshopFilteredProposals(visibleProposals, statusFilter, query)
|
||||
val selectedProposal =
|
||||
filteredProposals.firstOrNull { it.id == selectedProposalId }
|
||||
?: filteredProposals.firstOrNull()
|
||||
|
||||
LaunchedEffect(isConnected, selectedAgentParam) {
|
||||
if (isConnected) {
|
||||
viewModel.refreshSkillWorkshopProposals(agentId = selectedAgentParam)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(filteredProposals.map { it.id }) {
|
||||
if (selectedProposalId == null || filteredProposals.none { it.id == selectedProposalId }) {
|
||||
selectedProposalId = filteredProposals.firstOrNull()?.id
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedProposal?.id, selectedProposal?.content, isConnected, selectedAgentParam) {
|
||||
if (isConnected && selectedProposal != null && selectedProposal.content == null) {
|
||||
viewModel.inspectSkillWorkshopProposal(proposalId = selectedProposal.id, agentId = selectedAgentParam)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedProposal?.id, pendingAction?.proposalId) {
|
||||
if (pendingAction != null && selectedProposal?.id != pendingAction?.proposalId) {
|
||||
pendingAction = null
|
||||
}
|
||||
}
|
||||
|
||||
pendingAction?.let { action ->
|
||||
SkillWorkshopActionConfirmDialog(
|
||||
action = action,
|
||||
onDismiss = { pendingAction = null },
|
||||
onConfirm = {
|
||||
pendingAction = null
|
||||
action.action.perform(viewModel, action.proposalId, selectedAgentParam)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
SettingsDetailFrame(
|
||||
subtitle = nativeString("Review generated skill proposals before they become live skills."),
|
||||
route = SettingsRoute.SkillWorkshop,
|
||||
onBack = onBack,
|
||||
) {
|
||||
SettingsMetricPanel(
|
||||
rows =
|
||||
listOf(
|
||||
SettingsMetric(nativeString("Pending"), visibleProposals.count { it.status == "pending" }.toString()),
|
||||
SettingsMetric(
|
||||
nativeString("Held"),
|
||||
visibleProposals.count { skillWorkshopStatusMatchesFilter(it.status, "held") }.toString(),
|
||||
),
|
||||
SettingsMetric(nativeString("Applied"), visibleProposals.count { it.status == "applied" }.toString()),
|
||||
SettingsMetric(nativeString("Rejected"), visibleProposals.count { it.status == "rejected" }.toString()),
|
||||
),
|
||||
)
|
||||
|
||||
SkillWorkshopControls(
|
||||
agents = agents,
|
||||
defaultAgentId = defaultAgentId,
|
||||
selectedAgentId = selectedAgentId,
|
||||
onAgentChange = { agentId ->
|
||||
selectedAgentId = agentId
|
||||
selectedProposalId = null
|
||||
viewModel.resetSkillWorkshopAgentScope(agentId = agentId.takeIf { it.isNotBlank() })
|
||||
},
|
||||
statusFilter = statusFilter,
|
||||
onStatusFilterChange = { filter ->
|
||||
statusFilter = filter
|
||||
selectedProposalId = null
|
||||
},
|
||||
query = query,
|
||||
onQueryChange = { value ->
|
||||
query = value
|
||||
selectedProposalId = null
|
||||
},
|
||||
refreshing = refreshing,
|
||||
isConnected = isConnected,
|
||||
onRefresh = { viewModel.refreshSkillWorkshopProposals(agentId = selectedAgentParam) },
|
||||
)
|
||||
|
||||
noticeText?.let { message ->
|
||||
SettingsMessagePanel(text = message, color = ClawTheme.colors.success)
|
||||
}
|
||||
errorText?.let { message ->
|
||||
SettingsMessagePanel(text = message, color = ClawTheme.colors.warning)
|
||||
}
|
||||
|
||||
when {
|
||||
!isConnected -> {
|
||||
SettingsMessagePanel(
|
||||
spacing = 4.dp,
|
||||
titleStyle = ClawTheme.type.title,
|
||||
title = nativeString("Gateway offline"),
|
||||
text = nativeString("Connect to a Gateway to load Skill Workshop proposals."),
|
||||
)
|
||||
}
|
||||
|
||||
filteredProposals.isEmpty() -> {
|
||||
SettingsMessagePanel(
|
||||
spacing = 4.dp,
|
||||
titleStyle = ClawTheme.type.title,
|
||||
title = nativeString("No proposals"),
|
||||
text = nativeString("Matching proposals will appear here after agents create reusable skill drafts."),
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
filteredProposals.forEach { proposal ->
|
||||
SkillWorkshopProposalRow(
|
||||
proposal = proposal,
|
||||
selected = proposal.id == selectedProposal?.id,
|
||||
busy = proposal.id == inspectingProposalId || proposal.id == mutatingProposalId,
|
||||
onClick = {
|
||||
selectedProposalId = proposal.id
|
||||
viewModel.inspectSkillWorkshopProposal(proposalId = proposal.id, agentId = selectedAgentParam)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
selectedProposal?.let { proposal ->
|
||||
SkillWorkshopProposalDetail(
|
||||
proposal = proposal,
|
||||
inspecting = inspectingProposalId == proposal.id,
|
||||
mutating = mutatingProposalId == proposal.id,
|
||||
isConnected = isConnected,
|
||||
operatorAdminScopeAvailable = operatorAdminScopeAvailable,
|
||||
onInspect = {
|
||||
viewModel.inspectSkillWorkshopProposal(proposalId = proposal.id, agentId = selectedAgentParam)
|
||||
},
|
||||
onAction = { action ->
|
||||
pendingAction =
|
||||
SkillWorkshopPendingAction(
|
||||
action = action,
|
||||
proposalId = proposal.id,
|
||||
title = proposal.title,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class SkillWorkshopProposalAction(
|
||||
val label: NativeText,
|
||||
val confirmationTitle: NativeText,
|
||||
val confirmationBody: (String) -> String,
|
||||
val perform: (MainViewModel, String, String?) -> Unit,
|
||||
) {
|
||||
Apply(
|
||||
nativeText("Apply"),
|
||||
nativeText("Apply proposal?"),
|
||||
{ nativeString("This will apply \"\$proposalTitle\" and refresh Skill Workshop state from the gateway.", it) },
|
||||
MainViewModel::applySkillWorkshopProposal,
|
||||
),
|
||||
Reject(
|
||||
nativeText("Reject"),
|
||||
nativeText("Reject proposal?"),
|
||||
{ nativeString("This will reject \"\$proposalTitle\" and refresh Skill Workshop state from the gateway.", it) },
|
||||
MainViewModel::rejectSkillWorkshopProposal,
|
||||
),
|
||||
Quarantine(
|
||||
nativeText("Quarantine"),
|
||||
nativeText("Quarantine proposal?"),
|
||||
{ nativeString("This will quarantine \"\$proposalTitle\" and refresh Skill Workshop state from the gateway.", it) },
|
||||
MainViewModel::quarantineSkillWorkshopProposal,
|
||||
),
|
||||
}
|
||||
|
||||
private data class SkillWorkshopPendingAction(
|
||||
val action: SkillWorkshopProposalAction,
|
||||
val proposalId: String,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun SkillWorkshopActionConfirmDialog(
|
||||
action: SkillWorkshopPendingAction,
|
||||
onDismiss: () -> Unit,
|
||||
onConfirm: () -> Unit,
|
||||
) {
|
||||
val dialogTitle = action.action.confirmationTitle.resolveNativeText()
|
||||
val dialogBody = action.action.confirmationBody(action.title)
|
||||
AppConfirmationDialog(
|
||||
title = dialogTitle,
|
||||
confirmLabel = action.action.label.resolveNativeText(),
|
||||
onConfirm = onConfirm,
|
||||
onDismiss = onDismiss,
|
||||
text = {
|
||||
Text(text = dialogBody)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillWorkshopControls(
|
||||
agents: List<GatewayAgentSummary>,
|
||||
defaultAgentId: String?,
|
||||
selectedAgentId: String,
|
||||
onAgentChange: (String) -> Unit,
|
||||
statusFilter: String,
|
||||
onStatusFilterChange: (String) -> Unit,
|
||||
query: String,
|
||||
onQueryChange: (String) -> Unit,
|
||||
refreshing: Boolean,
|
||||
isConnected: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
) {
|
||||
ClawPanel(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
SkillWorkshopAgentMenu(
|
||||
agents = agents,
|
||||
defaultAgentId = defaultAgentId,
|
||||
selectedAgentId = selectedAgentId,
|
||||
onAgentChange = onAgentChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
ClawSecondaryButton(
|
||||
text = if (refreshing) nativeString("Refreshing") else nativeString("Refresh"),
|
||||
onClick = onRefresh,
|
||||
enabled = isConnected && !refreshing,
|
||||
icon = Icons.Default.Refresh,
|
||||
)
|
||||
}
|
||||
ClawSegmentedControl(
|
||||
options = skillWorkshopFilters.map { (_, label) -> nativeString(label) },
|
||||
selected = nativeString(skillWorkshopFilters.firstOrNull { it.first == statusFilter }?.second ?: "All"),
|
||||
onSelect = { label ->
|
||||
onStatusFilterChange(skillWorkshopFilters.firstOrNull { nativeString(it.second) == label }?.first ?: "all")
|
||||
},
|
||||
maxOptionsPerRow = 4,
|
||||
)
|
||||
ClawTextField(
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
placeholder = nativeString("Search proposals"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillWorkshopAgentMenu(
|
||||
agents: List<GatewayAgentSummary>,
|
||||
defaultAgentId: String?,
|
||||
selectedAgentId: String,
|
||||
onAgentChange: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val selectableAgents = agents.selectableAgents()
|
||||
val label =
|
||||
skillWorkshopAgentLabel(
|
||||
agents = selectableAgents,
|
||||
defaultAgentId = defaultAgentId,
|
||||
selectedAgentId = selectedAgentId,
|
||||
)
|
||||
Box(modifier = modifier) {
|
||||
ClawSecondaryButton(
|
||||
text = label,
|
||||
onClick = { expanded = true },
|
||||
icon = Icons.Default.ArrowDropDown,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = selectableAgents.isNotEmpty(),
|
||||
)
|
||||
AppDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(nativeString("Default agent")) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onAgentChange("")
|
||||
},
|
||||
)
|
||||
selectableAgents
|
||||
.filter { agent -> agent.id.trim().isNotEmpty() && agent.id != defaultAgentId }
|
||||
.sortedBy { it.name ?: it.id }
|
||||
.forEach { agent ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(agent.name?.takeIf { it.isNotBlank() } ?: agent.id) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
onAgentChange(agent.id)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillWorkshopProposalRow(
|
||||
proposal: GatewaySkillWorkshopProposal,
|
||||
selected: Boolean,
|
||||
busy: Boolean,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
ClawPanel {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(9.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
Text(
|
||||
text = proposal.title,
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.text,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = proposal.description ?: proposal.skillKey,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textSubtle,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
ClawStatusPill(
|
||||
text = skillWorkshopStatusLabel(if (busy) "loading" else proposal.status),
|
||||
status = skillWorkshopStatusPill(proposal.status),
|
||||
)
|
||||
Text(
|
||||
text = skillWorkshopDateLabel(proposal.updatedAt),
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textSubtle,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
ClawSecondaryButton(
|
||||
text = if (selected) nativeString("Selected") else nativeString("Open"),
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !selected,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SkillWorkshopProposalDetail(
|
||||
proposal: GatewaySkillWorkshopProposal,
|
||||
inspecting: Boolean,
|
||||
mutating: Boolean,
|
||||
isConnected: Boolean,
|
||||
operatorAdminScopeAvailable: Boolean,
|
||||
onInspect: () -> Unit,
|
||||
onAction: (SkillWorkshopProposalAction) -> Unit,
|
||||
) {
|
||||
val actionEnabled =
|
||||
skillWorkshopProposalActionEnabled(
|
||||
isConnected = isConnected,
|
||||
operatorAdminScopeAvailable = operatorAdminScopeAvailable,
|
||||
busy = inspecting || mutating,
|
||||
status = proposal.status,
|
||||
)
|
||||
ClawPanel {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.Top,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = proposal.title,
|
||||
style = ClawTheme.type.title,
|
||||
color = ClawTheme.colors.text,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
text = proposal.skillKey,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textSubtle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
ClawStatusPill(
|
||||
text = skillWorkshopStatusLabel(proposal.status),
|
||||
status = skillWorkshopStatusPill(proposal.status),
|
||||
)
|
||||
}
|
||||
proposal.description?.let { description ->
|
||||
Text(text = description, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted)
|
||||
}
|
||||
SettingsMetricPanel(
|
||||
rows =
|
||||
listOf(
|
||||
SettingsMetric(nativeString("Kind"), proposal.kind),
|
||||
SettingsMetric(nativeString("Updated"), skillWorkshopDateLabel(proposal.updatedAt)),
|
||||
SettingsMetric(nativeString("Support Files"), proposal.supportFiles.size.toString()),
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = proposal.content ?: nativeString("Inspect this proposal to load its markdown."),
|
||||
style = ClawTheme.type.body,
|
||||
color = if (proposal.content == null) ClawTheme.colors.textSubtle else ClawTheme.colors.text,
|
||||
)
|
||||
if (!operatorAdminScopeAvailable) {
|
||||
Text(
|
||||
text = nativeString("Apply, reject, and quarantine require operator.admin scope. Reconnect with shared gateway auth or approve an operator.admin device scope upgrade to enable lifecycle actions."),
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.warning,
|
||||
)
|
||||
}
|
||||
if (proposal.supportFiles.isNotEmpty()) {
|
||||
HorizontalDivider(color = ClawTheme.colors.border)
|
||||
proposal.supportFiles.forEach { file ->
|
||||
Text(
|
||||
text = file.path,
|
||||
style = ClawTheme.type.caption,
|
||||
color = ClawTheme.colors.textMuted,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.semantics { contentDescription = nativeString("Skill Workshop inspect and apply actions") },
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ClawSecondaryButton(
|
||||
text = if (inspecting) nativeString("Inspecting") else nativeString("Inspect"),
|
||||
onClick = onInspect,
|
||||
enabled = isConnected && !inspecting && !mutating,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
ClawPrimaryButton(
|
||||
text = if (mutating) nativeString("Working") else nativeString("Apply"),
|
||||
onClick = { onAction(SkillWorkshopProposalAction.Apply) },
|
||||
enabled = actionEnabled,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.semantics { contentDescription = nativeString("Skill Workshop reject and quarantine actions") },
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
listOf(SkillWorkshopProposalAction.Reject, SkillWorkshopProposalAction.Quarantine).forEach { action ->
|
||||
ClawSecondaryButton(
|
||||
text = action.label.resolveNativeText(),
|
||||
onClick = { onAction(action) },
|
||||
enabled = actionEnabled,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun skillWorkshopVisibleProposals(
|
||||
summary: GatewaySkillWorkshopSummary,
|
||||
selectedAgentId: String?,
|
||||
): List<GatewaySkillWorkshopProposal> =
|
||||
when {
|
||||
summary.agentId == skillWorkshopAgentScope(selectedAgentId) -> summary.proposals
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
internal fun skillWorkshopAgentScope(agentId: String?): String = agentId?.trim().orEmpty()
|
||||
|
||||
internal fun skillWorkshopFilteredProposals(
|
||||
proposals: List<GatewaySkillWorkshopProposal>,
|
||||
statusFilter: String,
|
||||
query: String,
|
||||
): List<GatewaySkillWorkshopProposal> {
|
||||
val normalizedQuery = query.trim().lowercase()
|
||||
return proposals
|
||||
.filter { proposal ->
|
||||
if (!skillWorkshopStatusMatchesFilter(proposal.status, statusFilter)) return@filter false
|
||||
if (normalizedQuery.isEmpty()) return@filter true
|
||||
val pieces =
|
||||
listOf(proposal.title, proposal.description.orEmpty(), proposal.skillName, proposal.skillKey)
|
||||
val haystack = pieces.joinToString(" ").lowercase()
|
||||
haystack.contains(normalizedQuery)
|
||||
}.sortedByDescending { it.updatedAt }
|
||||
}
|
||||
|
||||
internal fun skillWorkshopStatusMatchesFilter(
|
||||
status: String,
|
||||
filter: String,
|
||||
): Boolean =
|
||||
when (filter) {
|
||||
"all" -> true
|
||||
"held" -> status == "quarantined" || status == "stale"
|
||||
else -> status == filter
|
||||
}
|
||||
|
||||
internal fun skillWorkshopProposalActionEnabled(
|
||||
isConnected: Boolean,
|
||||
operatorAdminScopeAvailable: Boolean,
|
||||
busy: Boolean,
|
||||
status: String,
|
||||
): Boolean = isConnected && operatorAdminScopeAvailable && !busy && status == "pending"
|
||||
|
||||
internal fun skillWorkshopStatusLabel(status: String): String =
|
||||
when (status) {
|
||||
"pending" -> nativeString("Pending")
|
||||
"quarantined", "stale" -> nativeString("Held")
|
||||
"applied" -> nativeString("Applied")
|
||||
"rejected" -> nativeString("Rejected")
|
||||
"loading" -> nativeString("Loading")
|
||||
else -> status
|
||||
}
|
||||
|
||||
private fun skillWorkshopAgentLabel(
|
||||
agents: List<GatewayAgentSummary>,
|
||||
defaultAgentId: String?,
|
||||
selectedAgentId: String,
|
||||
): String {
|
||||
val selected = selectedAgentId.trim()
|
||||
if (selected.isNotEmpty()) {
|
||||
return agents.firstOrNull { it.id == selected }?.name?.takeIf { it.isNotBlank() } ?: selected
|
||||
}
|
||||
val defaultId = defaultAgentId?.trim().orEmpty()
|
||||
if (defaultId.isNotEmpty()) {
|
||||
return agents.firstOrNull { it.id == defaultId }?.name?.takeIf { it.isNotBlank() }
|
||||
?: nativeString("Default agent")
|
||||
}
|
||||
return nativeString("Default agent")
|
||||
}
|
||||
|
||||
private fun skillWorkshopStatusPill(status: String): ClawStatus =
|
||||
when (status) {
|
||||
"pending", "quarantined", "stale" -> ClawStatus.Warning
|
||||
"applied" -> ClawStatus.Success
|
||||
else -> ClawStatus.Neutral
|
||||
}
|
||||
|
||||
private fun skillWorkshopDateLabel(value: String): String = value.trim().takeIf { it.isNotEmpty() }?.take(10) ?: nativeString("Unknown")
|
||||
|
|
@ -75,11 +75,6 @@ internal object ClawIcons {
|
|||
"M6 13h2M6 9h2",
|
||||
)
|
||||
|
||||
val SkillWorkshop: ImageVector by outline(
|
||||
"Wrench",
|
||||
"M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z",
|
||||
)
|
||||
|
||||
val OpenClaw: ImageVector by lazy {
|
||||
ImageVector
|
||||
.Builder("OpenClaw", 24.dp, 24.dp, 120f, 120f)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import ai.openclaw.app.ui.chat.chatThinkingOptionLabel
|
|||
import ai.openclaw.app.ui.formatApprovalDuration
|
||||
import ai.openclaw.app.ui.formatCronWake
|
||||
import ai.openclaw.app.ui.formatUsageUpdated
|
||||
import ai.openclaw.app.ui.skillWorkshopStatusLabel
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
|
@ -143,18 +142,10 @@ class AppLanguageTest {
|
|||
setAppLanguage(AppLanguage.English)
|
||||
assertEquals("Mic off", retained.value)
|
||||
assertEquals("Mic off · raw", retainedComposite.value)
|
||||
assertEquals("Pending", skillWorkshopStatusLabel("pending"))
|
||||
|
||||
setAppLanguage(AppLanguage.French)
|
||||
assertEquals("Micro désactivé", retained.value)
|
||||
assertEquals("Micro désactivé · raw", retainedComposite.value)
|
||||
assertEquals("En attente", skillWorkshopStatusLabel("pending"))
|
||||
assertEquals("Retenu", skillWorkshopStatusLabel("quarantined"))
|
||||
assertEquals("Retenu", skillWorkshopStatusLabel("stale"))
|
||||
assertEquals("Appliqué", skillWorkshopStatusLabel("applied"))
|
||||
assertEquals("Rejeté", skillWorkshopStatusLabel("rejected"))
|
||||
assertEquals("Chargement", skillWorkshopStatusLabel("loading"))
|
||||
assertEquals("future_status", skillWorkshopStatusLabel("future_status"))
|
||||
assertEquals("Élevé", chatThinkingOptionLabel(ChatThinkingLevelOption("high", "high")))
|
||||
assertEquals("Adaptatif", chatThinkingOptionLabel(ChatThinkingLevelOption("adaptive", "adaptive")))
|
||||
val androidRelease =
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ class SecurePrefsTest {
|
|||
val legacyOrder = listOf("threads", "home", "skills", "work")
|
||||
val storedOrder = prefs.sidebarPageOrder.value
|
||||
assertEquals(legacyOrder, storedOrder.take(legacyOrder.size))
|
||||
assertTrue("New workspace pages follow the saved order", "skill-workshop" in storedOrder.drop(legacyOrder.size))
|
||||
assertTrue("New workspace pages follow the saved order", "dreaming" in storedOrder.drop(legacyOrder.size))
|
||||
assertEquals(storedOrder.size, storedOrder.distinct().size)
|
||||
assertEquals(storedOrder, testPrefs(context).sidebarPageOrder.value)
|
||||
assertEquals(
|
||||
|
|
@ -179,9 +179,9 @@ class SecurePrefsTest {
|
|||
|
||||
assertEquals(defaultSidebarVisiblePages, prefs.sidebarVisiblePages.value)
|
||||
|
||||
prefs.setSidebarVisiblePages(listOf("settings", "threads", "skill-workshop", "home", "threads", "unknown"))
|
||||
prefs.setSidebarVisiblePages(listOf("settings", "threads", "dreaming", "home", "threads", "unknown"))
|
||||
|
||||
val expected = listOf("threads", "skill-workshop", "home")
|
||||
val expected = listOf("threads", "dreaming", "home")
|
||||
assertEquals(expected, prefs.sidebarVisiblePages.value)
|
||||
assertEquals(expected, testPrefs(context).sidebarVisiblePages.value)
|
||||
assertEquals(defaultSidebarVisiblePages, sanitizeSidebarVisiblePages(listOf("unknown")))
|
||||
|
|
|
|||
|
|
@ -1,285 +0,0 @@
|
|||
package ai.openclaw.app
|
||||
|
||||
import ai.openclaw.app.gateway.GatewayEndpoint
|
||||
import ai.openclaw.app.i18n.NativeText
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import java.lang.reflect.Field
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class SkillWorkshopAgentScopeRuntimeTest {
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
@Before
|
||||
fun clearPlainPrefs() {
|
||||
RuntimeEnvironment
|
||||
.getApplication()
|
||||
.getSharedPreferences("openclaw.node", android.content.Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.clear()
|
||||
.commit()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gatewayActionsCarryClosedLocalizedPresentation() {
|
||||
val cases =
|
||||
listOf(
|
||||
SkillWorkshopGatewayAction.Apply to Triple("apply", "applied", "Proposal applied."),
|
||||
SkillWorkshopGatewayAction.Reject to Triple("reject", "rejected", "Proposal rejected."),
|
||||
SkillWorkshopGatewayAction.Quarantine to Triple("quarantine", "quarantined", "Proposal quarantined."),
|
||||
)
|
||||
|
||||
for ((action, expected) in cases) {
|
||||
val (methodSuffix, expectedStatus, noticeSource) = expected
|
||||
assertEquals(methodSuffix, action.methodSuffix)
|
||||
assertEquals(expectedStatus, action.expectedStatus)
|
||||
assertEquals(NativeText.Resource(noticeSource, emptyList()), action.notice)
|
||||
assertEquals(NativeText.Resource(methodSuffix, emptyList()), action.verb)
|
||||
assertEquals(
|
||||
NativeText.Resource(
|
||||
source = "Gateway returned status '\$statusLabel' after \${action.verb}.",
|
||||
formatArgs = listOf(NativeText.Verbatim("future_status"), action.verb),
|
||||
),
|
||||
skillWorkshopUnexpectedStatusText("future_status", action),
|
||||
)
|
||||
assertEquals(
|
||||
NativeText.Resource(
|
||||
source = "Could not \${action.verb} Skill Workshop proposal.",
|
||||
formatArgs = listOf(action.verb),
|
||||
),
|
||||
skillWorkshopActionFailureText(action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingGatewayActionStatusUsesLocalizedUnknownFallback() {
|
||||
assertEquals(
|
||||
NativeText.Resource(
|
||||
source = "Gateway returned status '\$statusLabel' after \${action.verb}.",
|
||||
formatArgs =
|
||||
listOf(
|
||||
NativeText.Resource(source = "unknown", formatArgs = emptyList()),
|
||||
SkillWorkshopGatewayAction.Apply.verb,
|
||||
),
|
||||
),
|
||||
skillWorkshopUnexpectedStatusText(null, SkillWorkshopGatewayAction.Apply),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resetSkillWorkshopAgentScopeClearsRowsAndInFlightActionState() {
|
||||
val runtime = createTestRuntime()
|
||||
seedConnectedRuntime(runtime)
|
||||
runtime.resetSkillWorkshopAgentScope("main")
|
||||
readField<MutableStateFlow<GatewaySkillWorkshopSummary>>(runtime, "_skillWorkshopSummary").value =
|
||||
GatewaySkillWorkshopSummary(
|
||||
agentId = "main",
|
||||
proposals = listOf(skillWorkshopProposal("main-proposal")),
|
||||
)
|
||||
|
||||
runtime.resetSkillWorkshopAgentScope("ops")
|
||||
|
||||
assertEquals("ops", runtime.skillWorkshopSummary.value.agentId)
|
||||
assertEquals(emptyList<GatewaySkillWorkshopProposal>(), runtime.skillWorkshopSummary.value.proposals)
|
||||
assertFalse(runtime.skillWorkshopRefreshing.value)
|
||||
assertNull(runtime.skillWorkshopErrorText.value)
|
||||
assertNull(runtime.skillWorkshopNoticeText.value)
|
||||
assertNull(runtime.skillWorkshopInspectingProposalId.value)
|
||||
assertNull(runtime.skillWorkshopMutatingProposalId.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun inspectAndMutateDoNotStartForStaleSelectedAgentScope() {
|
||||
val runtime = createTestRuntime()
|
||||
seedConnectedRuntime(runtime)
|
||||
runtime.resetSkillWorkshopAgentScope("main")
|
||||
|
||||
runtime.inspectSkillWorkshopProposal(proposalId = "ops-proposal", agentId = "ops")
|
||||
readField<MutableStateFlow<List<String>>>(runtime, "_operatorScopes").value = listOf("operator.admin")
|
||||
waitUntil { runtime.operatorAdminScopeAvailable.value }
|
||||
runtime.applySkillWorkshopProposal(proposalId = "ops-proposal", agentId = "ops")
|
||||
Thread.sleep(100)
|
||||
|
||||
assertEquals("main", runtime.skillWorkshopSummary.value.agentId)
|
||||
assertNull(runtime.skillWorkshopInspectingProposalId.value)
|
||||
assertNull(runtime.skillWorkshopMutatingProposalId.value)
|
||||
assertNull(runtime.skillWorkshopErrorText.value)
|
||||
assertNull(runtime.skillWorkshopNoticeText.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun busyProposalActionDoesNotInvalidateActiveRequestGenerations() {
|
||||
val runtime = createTestRuntime()
|
||||
seedConnectedRuntime(runtime)
|
||||
readField<MutableStateFlow<GatewaySkillWorkshopSummary>>(runtime, "_skillWorkshopSummary").value =
|
||||
GatewaySkillWorkshopSummary(
|
||||
agentId = "main",
|
||||
proposals = listOf(skillWorkshopProposal("proposal-1")),
|
||||
)
|
||||
readField<MutableStateFlow<List<String>>>(runtime, "_operatorScopes").value = listOf("operator.admin")
|
||||
waitUntil { runtime.operatorAdminScopeAvailable.value }
|
||||
readField<MutableStateFlow<String?>>(runtime, "_skillWorkshopMutatingProposalId").value = "proposal-1"
|
||||
val mutationSeq = readField<AtomicLong>(runtime, "skillWorkshopMutationSeq").apply { set(41) }
|
||||
val inspectSeq = readField<AtomicLong>(runtime, "skillWorkshopInspectSeq").apply { set(17) }
|
||||
|
||||
runtime.applySkillWorkshopProposal(proposalId = "proposal-1", agentId = "main")
|
||||
runtime.inspectSkillWorkshopProposal(proposalId = "proposal-1", agentId = "main")
|
||||
Thread.sleep(100)
|
||||
|
||||
assertEquals(41, mutationSeq.get())
|
||||
assertEquals(17, inspectSeq.get())
|
||||
assertEquals("proposal-1", runtime.skillWorkshopMutatingProposalId.value)
|
||||
assertNull(runtime.skillWorkshopInspectingProposalId.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun proposalActionResultUsesGatewayReturnedStatusAndPreservesInspectedDetails() {
|
||||
val runtime = createTestRuntime()
|
||||
val supportFiles =
|
||||
listOf(GatewaySkillWorkshopSupportFile(path = "references/proof.md", content = "proof"))
|
||||
val previous =
|
||||
skillWorkshopProposal("proposal-1")
|
||||
.copy(
|
||||
status = "pending",
|
||||
content = "inspected markdown",
|
||||
supportFiles = supportFiles,
|
||||
)
|
||||
|
||||
val rejected =
|
||||
parseSkillWorkshopActionResult(
|
||||
runtime,
|
||||
"""
|
||||
{
|
||||
"record": {
|
||||
"id": "proposal-1",
|
||||
"kind": "create",
|
||||
"status": "rejected",
|
||||
"title": "Rejected proposal",
|
||||
"description": "Gateway action response",
|
||||
"createdAt": "2026-07-08T00:00:00Z",
|
||||
"updatedAt": "2026-07-09T00:00:00Z",
|
||||
"scan": { "state": "clean" },
|
||||
"target": { "skillName": "Rejected Skill", "skillKey": "rejected-skill" }
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
previous,
|
||||
)
|
||||
|
||||
assertEquals("rejected", rejected?.status)
|
||||
assertEquals("2026-07-09T00:00:00Z", rejected?.updatedAt)
|
||||
assertEquals("Rejected Skill", rejected?.skillName)
|
||||
assertEquals("clean", rejected?.scanState)
|
||||
assertEquals("inspected markdown", rejected?.content)
|
||||
assertEquals(supportFiles, rejected?.supportFiles)
|
||||
}
|
||||
|
||||
private fun createTestRuntime(): NodeRuntime {
|
||||
val app = RuntimeEnvironment.getApplication()
|
||||
val securePrefs =
|
||||
app.getSharedPreferences(
|
||||
"openclaw.node.secure.test.${UUID.randomUUID()}",
|
||||
android.content.Context.MODE_PRIVATE,
|
||||
)
|
||||
return NodeRuntime(app, SecurePrefs(app, securePrefsOverride = securePrefs))
|
||||
}
|
||||
|
||||
private fun seedConnectedRuntime(runtime: NodeRuntime) {
|
||||
writeField(runtime, "connectedEndpoint", GatewayEndpoint.manual("127.0.0.1", 18789))
|
||||
writeField(runtime, "operatorConnected", true)
|
||||
}
|
||||
|
||||
private fun skillWorkshopProposal(id: String): GatewaySkillWorkshopProposal =
|
||||
GatewaySkillWorkshopProposal(
|
||||
id = id,
|
||||
status = "pending",
|
||||
kind = "create",
|
||||
title = "Proposal $id",
|
||||
skillKey = id,
|
||||
skillName = "Proposal $id",
|
||||
description = "desc",
|
||||
createdAt = "2026-07-08T00:00:00Z",
|
||||
updatedAt = "2026-07-08T00:00:00Z",
|
||||
scanState = null,
|
||||
)
|
||||
|
||||
private fun parseSkillWorkshopActionResult(
|
||||
runtime: NodeRuntime,
|
||||
payloadJson: String,
|
||||
previous: GatewaySkillWorkshopProposal,
|
||||
): GatewaySkillWorkshopProposal? {
|
||||
val method =
|
||||
runtime.javaClass.getDeclaredMethod(
|
||||
"parseSkillWorkshopProposalActionResult",
|
||||
JsonObject::class.java,
|
||||
GatewaySkillWorkshopProposal::class.java,
|
||||
)
|
||||
method.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return method.invoke(
|
||||
runtime,
|
||||
json.parseToJsonElement(payloadJson).jsonObject,
|
||||
previous,
|
||||
) as GatewaySkillWorkshopProposal?
|
||||
}
|
||||
|
||||
private fun waitUntil(condition: () -> Boolean) {
|
||||
repeat(50) {
|
||||
if (condition()) return
|
||||
Thread.sleep(10)
|
||||
}
|
||||
error("Expected condition to become true")
|
||||
}
|
||||
|
||||
private fun writeField(
|
||||
target: Any,
|
||||
name: String,
|
||||
value: Any?,
|
||||
) {
|
||||
var type: Class<*>? = target.javaClass
|
||||
while (type != null) {
|
||||
try {
|
||||
val field: Field = type.getDeclaredField(name)
|
||||
field.isAccessible = true
|
||||
field.set(target, value)
|
||||
return
|
||||
} catch (_: NoSuchFieldException) {
|
||||
type = type.superclass
|
||||
}
|
||||
}
|
||||
error("Field $name not found on ${target.javaClass.name}")
|
||||
}
|
||||
|
||||
private fun <T> readField(
|
||||
target: Any,
|
||||
name: String,
|
||||
): T {
|
||||
var type: Class<*>? = target.javaClass
|
||||
while (type != null) {
|
||||
try {
|
||||
val field: Field = type.getDeclaredField(name)
|
||||
field.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return field.get(target) as T
|
||||
} catch (_: NoSuchFieldException) {
|
||||
type = type.superclass
|
||||
}
|
||||
}
|
||||
error("Field $name not found on ${target.javaClass.name}")
|
||||
}
|
||||
}
|
||||
|
|
@ -87,25 +87,25 @@ class CommandPaletteLogicTest {
|
|||
|
||||
@Test
|
||||
fun workspacePageRemainsReachableThroughPagesMenuAndSearchWithBackToOrigin() {
|
||||
val workshop = nativeString("Skill Workshop")
|
||||
val workshopDescription = nativeString("Review generated skill proposals before they become live skills.")
|
||||
val page = nativeString("Dreaming")
|
||||
val pageDescription = nativeString("Memory consolidation and dream diary.")
|
||||
withShell(HomeDestination.Connect) { backDispatcher, assertRuntimeUnchanged ->
|
||||
composeRule.onNodeWithTag("sidebar-open-overview").performClick()
|
||||
composeRule.onNodeWithTag("sidebar-pages-menu").performClick()
|
||||
composeRule.onNodeWithText(workshop).performScrollTo().performClick()
|
||||
composeRule.onNodeWithText(workshopDescription).assertIsDisplayed()
|
||||
composeRule.onNodeWithText(page).performScrollTo().performClick()
|
||||
composeRule.onNodeWithText(pageDescription).assertIsDisplayed()
|
||||
assertRuntimeUnchanged()
|
||||
composeRule.runOnIdle { backDispatcher.onBackPressed() }
|
||||
composeRule.onNodeWithTag("sidebar-open-overview").assertIsDisplayed()
|
||||
|
||||
composeRule.onNodeWithContentDescription(nativeString("Search")).performClick()
|
||||
composeRule.onNode(hasSetTextAction()).performTextReplacement(workshop)
|
||||
composeRule.onNode(hasSetTextAction()).performTextReplacement(page)
|
||||
val searchResults = hasScrollAction() and hasAnyDescendant(hasSetTextAction())
|
||||
composeRule
|
||||
.onNode(hasText(workshop) and hasClickAction() and hasSetTextAction().not() and hasAnyAncestor(searchResults))
|
||||
.onNode(hasText(page) and hasClickAction() and hasSetTextAction().not() and hasAnyAncestor(searchResults))
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
composeRule.onNodeWithText(workshopDescription).assertIsDisplayed()
|
||||
composeRule.onNodeWithText(pageDescription).assertIsDisplayed()
|
||||
composeRule.onNodeWithContentDescription(nativeString("Back")).performClick()
|
||||
composeRule.onNodeWithTag("sidebar-open-overview").assertIsDisplayed()
|
||||
assertRuntimeUnchanged()
|
||||
|
|
@ -167,7 +167,7 @@ class CommandPaletteLogicTest {
|
|||
assertEquals(providerSubtitle, categoryMatches.single { it.action == providerAction }.subtitle.resolveNativeText())
|
||||
val workspaceMatches = commandItems(query = nativeString("Workspace"), desktopObserveAvailable = false, providerSubtitle = providerSubtitle)
|
||||
assertTrue(workspaceMatches.any { it.action == CommandAction.Settings(SettingsRoute.CronJobs) })
|
||||
assertTrue(workspaceMatches.any { it.action == CommandAction.Settings(SettingsRoute.SkillWorkshop) })
|
||||
assertTrue(workspaceMatches.any { it.action == CommandAction.Settings(SettingsRoute.Dreaming) })
|
||||
assertFalse(workspaceMatches.any { it.action == providerAction })
|
||||
|
||||
// These destinations are outside the main Settings row group but still own routes.
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ import ai.openclaw.app.GatewayNodeCapabilityApproval
|
|||
import ai.openclaw.app.GatewayNodeSummary
|
||||
import ai.openclaw.app.GatewayNodesDevicesSummary
|
||||
import ai.openclaw.app.GatewayPendingDeviceSummary
|
||||
import ai.openclaw.app.GatewaySkillWorkshopProposal
|
||||
import ai.openclaw.app.GatewaySkillWorkshopSummary
|
||||
import ai.openclaw.app.chat.ChatSessionEntry
|
||||
import ai.openclaw.app.gatewayConnectionDisplay
|
||||
import ai.openclaw.app.i18n.resolveNativeText
|
||||
|
|
@ -273,96 +271,6 @@ class ShellScreenLogicTest {
|
|||
assertEquals(emptyList<String>(), rows.map { it.title })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skillWorkshopFilteringMatchesHeldAndSearchText() {
|
||||
val proposals =
|
||||
listOf(
|
||||
skillWorkshopProposal("pending", "pending", title = "Browser Playbook", skillKey = "browser-playbook"),
|
||||
skillWorkshopProposal("stale", "stale", title = "Old Draft", skillKey = "old-draft"),
|
||||
skillWorkshopProposal("quarantine", "quarantined", title = "Risky Skill", skillKey = "risky-skill"),
|
||||
)
|
||||
|
||||
assertEquals(listOf("stale", "quarantine"), skillWorkshopFilteredProposals(proposals, "held", "").map { it.id })
|
||||
assertEquals(listOf("pending"), skillWorkshopFilteredProposals(proposals, "all", "browser").map { it.id })
|
||||
assertTrue(skillWorkshopStatusMatchesFilter("stale", "held"))
|
||||
assertFalse(skillWorkshopStatusMatchesFilter("applied", "held"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skillWorkshopStatusLabelsMapKnownCodesAndPreserveUnknownValues() {
|
||||
assertEquals("Pending", skillWorkshopStatusLabel("pending"))
|
||||
assertEquals("Held", skillWorkshopStatusLabel("quarantined"))
|
||||
assertEquals("Held", skillWorkshopStatusLabel("stale"))
|
||||
assertEquals("Applied", skillWorkshopStatusLabel("applied"))
|
||||
assertEquals("Rejected", skillWorkshopStatusLabel("rejected"))
|
||||
assertEquals("Loading", skillWorkshopStatusLabel("loading"))
|
||||
assertEquals("future_status", skillWorkshopStatusLabel("future_status"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skillWorkshopVisibleProposalsAreKeyedBySelectedAgentScope() {
|
||||
val mainProposal = skillWorkshopProposal("main-proposal", "pending")
|
||||
val opsProposal = skillWorkshopProposal("ops-proposal", "pending")
|
||||
|
||||
assertEquals(
|
||||
listOf("main-proposal"),
|
||||
skillWorkshopVisibleProposals(
|
||||
GatewaySkillWorkshopSummary(agentId = "", proposals = listOf(mainProposal)),
|
||||
selectedAgentId = null,
|
||||
).map { it.id },
|
||||
)
|
||||
assertEquals(
|
||||
emptyList<String>(),
|
||||
skillWorkshopVisibleProposals(
|
||||
GatewaySkillWorkshopSummary(agentId = "main", proposals = listOf(mainProposal)),
|
||||
selectedAgentId = "ops",
|
||||
).map { it.id },
|
||||
)
|
||||
assertEquals(
|
||||
listOf("ops-proposal"),
|
||||
skillWorkshopVisibleProposals(
|
||||
GatewaySkillWorkshopSummary(agentId = "ops", proposals = listOf(opsProposal)),
|
||||
selectedAgentId = " ops ",
|
||||
).map { it.id },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun skillWorkshopProposalActionsRequireAdminScope() {
|
||||
assertTrue(
|
||||
skillWorkshopProposalActionEnabled(
|
||||
isConnected = true,
|
||||
operatorAdminScopeAvailable = true,
|
||||
busy = false,
|
||||
status = "pending",
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
skillWorkshopProposalActionEnabled(
|
||||
isConnected = true,
|
||||
operatorAdminScopeAvailable = false,
|
||||
busy = false,
|
||||
status = "pending",
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
skillWorkshopProposalActionEnabled(
|
||||
isConnected = true,
|
||||
operatorAdminScopeAvailable = true,
|
||||
busy = true,
|
||||
status = "pending",
|
||||
),
|
||||
)
|
||||
assertFalse(
|
||||
skillWorkshopProposalActionEnabled(
|
||||
isConnected = true,
|
||||
operatorAdminScopeAvailable = true,
|
||||
busy = false,
|
||||
status = "applied",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun operatorScopesNormalizeForStableAdminChecks() {
|
||||
assertEquals(
|
||||
|
|
@ -876,23 +784,4 @@ class ShellScreenLogicTest {
|
|||
pauseReconnect = false,
|
||||
retryable = false,
|
||||
)
|
||||
|
||||
private fun skillWorkshopProposal(
|
||||
id: String,
|
||||
status: String,
|
||||
title: String = id,
|
||||
skillKey: String = id,
|
||||
): GatewaySkillWorkshopProposal =
|
||||
GatewaySkillWorkshopProposal(
|
||||
id = id,
|
||||
kind = "create",
|
||||
status = status,
|
||||
title = title,
|
||||
description = null,
|
||||
skillName = title,
|
||||
skillKey = skillKey,
|
||||
createdAt = "2026-07-08T00:00:00.000Z",
|
||||
updatedAt = "2026-07-08T00:00:00.000Z",
|
||||
scanState = null,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ class SidebarCatalogGroupingTest {
|
|||
composeRule.runOnIdle {
|
||||
assertEquals(listOf(true, false), dragStates)
|
||||
assertEquals(
|
||||
listOf("home", "work", "agents", "skills", "threads", "automations", "usage", "skill-workshop", "dreaming", "terminal", "desktop"),
|
||||
listOf("home", "work", "agents", "skills", "threads", "automations", "usage", "dreaming", "terminal", "desktop"),
|
||||
prefs.sidebarPageOrder.value,
|
||||
)
|
||||
}
|
||||
|
|
@ -377,7 +377,7 @@ class SidebarCatalogGroupingTest {
|
|||
.performTouchInput(dragOnePageDown)
|
||||
composeRule.runOnIdle {
|
||||
assertEquals(
|
||||
listOf("home", "agents", "work", "skills", "threads", "automations", "usage", "skill-workshop", "dreaming", "terminal", "desktop"),
|
||||
listOf("home", "agents", "work", "skills", "threads", "automations", "usage", "dreaming", "terminal", "desktop"),
|
||||
prefs.sidebarPageOrder.value,
|
||||
)
|
||||
assertEquals(listOf("agents", "home", "skills", "threads"), prefs.sidebarVisiblePages.value)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ class SidebarShellLogicTest {
|
|||
),
|
||||
destinations.take(5),
|
||||
)
|
||||
assertTrue(SidebarDestination.SkillWorkshop in destinations.drop(5))
|
||||
assertTrue(SidebarDestination.Dreaming in destinations.drop(5))
|
||||
assertEquals(destinations.size, destinations.distinct().size)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1521,7 +1521,6 @@ src/agents/tools/sessions-search-tool.ts 1
|
|||
src/agents/tools/sessions-spawn-tool.ts 5
|
||||
src/agents/tools/sessions-tool.ts 1
|
||||
src/agents/tools/sessions-yield-tool.ts 1
|
||||
src/agents/tools/skill-workshop-tool-helpers.ts 1
|
||||
src/agents/tools/structured-output-tool.ts 2
|
||||
src/agents/tools/subagents-tool.ts 1
|
||||
src/agents/tools/system-agent-tool.ts 1
|
||||
|
|
@ -2609,9 +2608,6 @@ src/skills/loading/frontmatter.ts 1
|
|||
src/skills/loading/workspace-skill-sync.runtime.ts 1
|
||||
src/skills/runtime/remote-probe-utils.ts 2
|
||||
src/skills/runtime/remote-skills.ts 1
|
||||
src/skills/workshop/policy.ts 1
|
||||
src/skills/workshop/store-record.ts 3
|
||||
src/skills/workshop/store-sqlite-event.ts 1
|
||||
src/skills/workshop/tool-policy-diagnostic.ts 1
|
||||
src/snapshot/git-backup-codec.ts 7
|
||||
src/snapshot/git-backup.ts 2
|
||||
|
|
@ -2908,8 +2904,6 @@ ui/src/pages/portals/portal-reachability.ts 1
|
|||
ui/src/pages/profile/identity-section.ts 2
|
||||
ui/src/pages/secrets/view.ts 5
|
||||
ui/src/pages/sessions/view.ts 7
|
||||
ui/src/pages/skill-workshop/self-learning.ts 1
|
||||
ui/src/pages/skill-workshop/view.ts 1
|
||||
ui/src/pages/skills/view.ts 2
|
||||
ui/src/pages/usage/view-details.ts 5
|
||||
ui/src/pages/usage/view-overview.ts 2
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@ packages/ai/src/providers/openai-completions.test.ts
|
|||
packages/ai/src/providers/openai-responses-shared.test.ts
|
||||
packages/ai/src/transports/anthropic-transport-stream.test.ts
|
||||
packages/gateway-client/src/client.ts
|
||||
packages/gateway-protocol/src/schema/agents-models-skills.ts
|
||||
packages/markdown-core/src/ir.ts
|
||||
packages/memory-host-sdk/src/host/session-files.ts
|
||||
packages/tool-call-repair/src/stream-normalizer.ts
|
||||
|
|
@ -553,7 +552,6 @@ src/security/audit-extra.sync.ts
|
|||
src/security/audit.ts
|
||||
src/shared/json-schema-defaults.ts
|
||||
src/skills/lifecycle/clawhub.test.ts
|
||||
src/skills/workshop/service.test.ts
|
||||
src/snapshot/local-repository.test.ts
|
||||
src/snapshot/local-repository.ts
|
||||
src/state/openclaw-agent-db.test.ts
|
||||
|
|
|
|||
|
|
@ -2299,7 +2299,6 @@
|
|||
["src/agents/cli-runner/helpers.system-prompt.test.ts","../../tts/tts-settings.js","81788c2e4e5749722791d31983539afde68c65b0b716cd8c04b2ad0af131f811",1]
|
||||
["src/agents/cli-runner/helpers.system-prompt.test.ts","../git-coauthor-prompt.js","90887f66a0179bfc38dad4d1e7e8f884c0b17991c9ad299a3ac3f8946474bbe0",1]
|
||||
["src/agents/cli-runner/media-guard.test.ts","../bash-tools.exec-approval-request.js","17a7fae09b4bd8176a883e2be62a0d198d04b8fa90133581426401b98c43aff6",1]
|
||||
["src/agents/cli-runner/prepare.rooted-execution.test.ts","../sandbox.js","f8508379f40cc45da58313a2edb79619346536d4e544174667cca59f0e784499",1]
|
||||
["src/agents/cli-runner/prepare.test.ts","../../config/config.js","24ac4b71ec8afe6ce1365afa2e73e815b93cd06fa54f0b448956fa7f7c510783",1]
|
||||
["src/agents/cli-runner/prepare.test.ts","../../plugins/hook-runner-global.js","ebf8cfdafc39d1a03c94c982099c4f6ad4bb3dd8033299ca74820bb1f00ce0e4",1]
|
||||
["src/agents/cli-runner/prepare.test.ts","../../tts/tts-settings.js","81788c2e4e5749722791d31983539afde68c65b0b716cd8c04b2ad0af131f811",1]
|
||||
|
|
@ -2307,7 +2306,6 @@
|
|||
["src/agents/cli-runner/prepare.test.ts","../sandbox.js","9d16c909d44fee8b0a6f01f3cdb7eabf19dd2d3292b7215e239a55eb122ad203",1]
|
||||
["src/agents/cli-runner/prepare.web-search.test.ts","../bash-tools.js","7e330d0731389dd6c639869b9d7e6f0b000699bb5b0bfb4c55874a24e1c105b6",1]
|
||||
["src/agents/cli-runner/prepare.web-search.test.ts","../tools/gateway.js","1691b9251bdf21a1da4e1f820a511a6505c317ea8fb99066b6c00360e1ff9b40",1]
|
||||
["src/agents/cli-runner/rooted-execution.test.ts","../sandbox.js","dea033beccf964a810ef82ffee5cb61616b16308947fc95eddd1b02029c80893",1]
|
||||
["src/agents/cli-session-store.test.ts","./model-selection.js","c16be4a7bfe9a4c152cedc014045af71c6abc4ed84bb1a48843abc7df26b0d03",1]
|
||||
["src/agents/code-mode-executor.test.ts","../plugins/code-mode-executor.js","c12f9311bf695d8b987e966cb2f9439aed7cdd20e72c9881d16d739952079d33",1]
|
||||
["src/agents/code-mode-node.lifecycle.test.ts","../infra/runtime-worker-url.js","4423e746829ce417e7df67fc87bc03960ca9fbe2411bcdafe10fe9c808b8fa64",1]
|
||||
|
|
@ -2316,7 +2314,6 @@
|
|||
["src/agents/code-mode-swarm.lazy.test.ts","./code-mode-swarm.runtime.js","4c5df8df2f2d97e45e38783e3cfc90096eaffc765053c55aa42a6167e0a13186",1]
|
||||
["src/agents/code-mode-swarm.lazy.test.ts","./subagents/registry/subagent-registry.js","efe17eea321152030b35223b03dbaba21d4362510340da188fcc175fd69cb62d",1]
|
||||
["src/agents/code-mode-swarm.test.ts","./subagents/spawn/subagent-spawn.js","3a9a5926f454ac071d769c28cb3970e6ac5ee54e890f5ecda926b8575ad3298f",1]
|
||||
["src/agents/code-mode.import-boundary.test.ts","../skills/workshop/service-query.js","bef13acfe87d1dda8fb1cfb24dc8a08d1c0abb36183fb20902372a399f1290bd",1]
|
||||
["src/agents/code-mode.import-boundary.test.ts","./subagents/registry/subagent-registry.js","a9cb63aa92717f17b9621e0c467f064f27f454e227c451b88b2c57637389e201",1]
|
||||
["src/agents/code-mode.import-boundary.test.ts","./tools/agents-wait-tool.js","06f05128b97ce315df365ea767e3ad665ae82cfe604b63668505c3dbe60b0850",1]
|
||||
["src/agents/codex-mcp-config.test.ts","../infra/exec-approvals-store.js","5494cc9404b43d73dfeef6d6b53ff76d6ddb9d0bc7d215347ef82418a2700f0b",1]
|
||||
|
|
@ -2816,7 +2813,6 @@
|
|||
["src/agents/github-tool-identity.test.ts","../process/exec.js","6f01d6e791341bf8ff8d627d9841bb705010dbd9bac3174444a12281cb25cad3",1]
|
||||
["src/agents/github-tool-identity.test.ts","./github-oauth-records.js","1c9bc36bb2ae3c818b6c0af21f8cbc0e7b1cea68f0b09dc2bcfd3cf455ea9f04",1]
|
||||
["src/agents/github-tool-profile-cleanup.test.ts","./github-oauth-records.js","61b7b9db319a03c318828440061d8282c2a3d72248e62ff9878ffec5aa83ae94",1]
|
||||
["src/agents/harness/agent-end-side-effects.test.ts","../../skills/workshop/experience-review-default.js","24af769fe90a4b16cf671d2e1c5c4db322a8aa7081ea61932060a94878ebf08b",1]
|
||||
["src/agents/harness/agent-end-side-effects.test.ts","./lifecycle-hook-helpers.js","16d16cff63f168b08ce43eb08057d49bc03e018713f7bc98a73f3c61ef60f4ab",1]
|
||||
["src/agents/harness/builtin-openclaw.test.ts","../embedded-agent-runner/run/attempt.js","e8a12ed96055114b67422388214eebe76347abb574dbc9c8e63f8631554eb6f2",1]
|
||||
["src/agents/harness/builtin-openclaw.test.ts","../simple-completion-execution.js","9ed172637035eb8bc24253e5520f7b2a1b0dc4ec9fedff86972be23285113b1d",1]
|
||||
|
|
@ -3590,9 +3586,6 @@
|
|||
["src/agents/tools/sessions-spawn-tool.mocks.test-support.ts","../subagents/spawn/subagent-spawn.runtime.js","59663d4502964a14b3bd4bc4702e04fa29e1e7b92ef72d059999b58a1bfb9734",1]
|
||||
["src/agents/tools/sessions.test.ts","../../sessions/session-participant-recording.js","241d4b18e482838e88c516473f7f41997da33dc32afd0ea3e33dac0092caae7d",1]
|
||||
["src/agents/tools/sessions.test.ts","./sessions-send-tool.a2a.js","a317c2e95b7f90649b532a06c921c6d6eb7f13a8179360eaea3e8d03ea0f5e1e",1]
|
||||
["src/agents/tools/skill-workshop-tool.availability.test.ts","../openclaw-plugin-tools.js","5f4cec3553f307631c831e2fb9157c52af96afde0a174251d4e53d04a9ac3697",1]
|
||||
["src/agents/tools/skill-workshop-tool.evaluation.test.ts","../../plugins/hook-runner-global.js","11cefe0df5ac8ecda8c60abf96a2e1f6d5fbd468bbc3f0569bb22f245cc1164f",1]
|
||||
["src/agents/tools/skill-workshop-tool.list.test.ts","../../skills/workshop/target-lock.js","8840438efc3d2ef6f248751fa7054f155ecb9e33edb1c69464dbbecf55e99a9a",1]
|
||||
["src/agents/tools/subagents-tool.test.ts","../subagents/registry/subagent-control-scope.js","5f15bd92bb108db1abdc93396daa1494a34216c8e7f323e75dddcea702d5dee6",1]
|
||||
["src/agents/tools/subagents-tool.test.ts","../subagents/registry/subagent-control.js","faf0ec325f9a372ea275d29293dc4c4ddb6dfeeb57ee077df6373d95b7515f8e",1]
|
||||
["src/agents/tools/subagents-tool.test.ts","../subagents/registry/subagent-list.js","4269237b137a45deb7ae69d723dd78fa54c5b0d0114e9292c4ac2567b49f7c61",1]
|
||||
|
|
@ -5166,9 +5159,6 @@
|
|||
["src/cli/skills-cli.commands.test.ts","../skills/lifecycle/clawhub.js","c9a19f72cc1efba4854fcec398b7378853118fb859e5e2d5f44bceb08e47cf71",1]
|
||||
["src/cli/skills-cli.commands.test.ts","../skills/lifecycle/source-install.js","df0ce1c04f484c430749be933372f77fad98a45db3d08578171cae987a98d23a",1]
|
||||
["src/cli/skills-cli.commands.test.ts","./one-shot-exit.js","0f90b817161cda63805e4c2a6ef3b6156f4d91039c01232f7e05f16bfd004af1",1]
|
||||
["src/cli/skills-cli.curator.test.ts","../config/config.js","3977cc5bfbc4f5677816dd4467aaa1d8f1c1dee7635da57b7d4020804f6d1841",1]
|
||||
["src/cli/skills-cli.curator.test.ts","../gateway/call.js","2405ee18bc2b4fa6232d2cd0a002fabc0575a644165f8a61eb26ce12b803852f",1]
|
||||
["src/cli/skills-cli.curator.test.ts","../infra/gateway-lock.js","71eef2c99af4283b23e796895269a612b721ba4422f79523a23e82bb0d555f64",1]
|
||||
["src/cli/skills-cli.test.ts","openclaw/plugin-sdk/agent-sessions","548c67ecfcd1d666c41ef827b824edf897f6d8eb3464f76f6bf9ae23dffbded3",1]
|
||||
["src/cli/skills-cli.verify.test.ts","../agents/agent-scope.js","b117793af3d676d859052beb892f058481587162d102cd2400f51961e3e24b01",1]
|
||||
["src/cli/skills-cli.verify.test.ts","../config/config.js","f4448fa735e408442c07e554899b084f01ce1d51e6690718fc1983922cd5dbc0",1]
|
||||
|
|
@ -5176,9 +5166,6 @@
|
|||
["src/cli/skills-cli.verify.test.ts","../infra/clawhub-skills.js","22650f746cf121c1465529e0ada287c7fa52815bcabe992bb0e6abea3d9ccab1",1]
|
||||
["src/cli/skills-cli.verify.test.ts","../runtime.js","c16762af0d3584b5012d69f7d3cde52553a84f822e49a786d2be054e90712da3",1]
|
||||
["src/cli/skills-cli.verify.test.ts","./one-shot-exit.js","8f430f08b312eb1183ec570fd01634860c80dccfe3c672c92abd3fc7c7017c10",1]
|
||||
["src/cli/skills-cli.workshop-cache.test.ts","../agents/agent-scope.js","604b33cdc7f1d5954035e9bc006189ace978204299720e39c4213233adf8357b",1]
|
||||
["src/cli/skills-cli.workshop-cache.test.ts","../config/config.js","3977cc5bfbc4f5677816dd4467aaa1d8f1c1dee7635da57b7d4020804f6d1841",1]
|
||||
["src/cli/skills-cli.workshop-cache.test.ts","../gateway/call.js","88600357596afd112a9f3ba5156d4915589ac655a6248e47202f1f3b7466e8bc",1]
|
||||
["src/cli/skills-library-cli.test.ts","./gateway-rpc.runtime.js","dc9d2411497017aa35e2a7a3413a1e40c968980097ab7f6e347d940b8da0399e",1]
|
||||
["src/cli/skills-library-input.test.ts","./gateway-rpc.js","6d6a59c174a65bf7d1586632d2a7f1ef671013708d76a08a4ba5fbb045c86376",1]
|
||||
["src/cli/skills-search-cli.test.ts","../runtime.js","6f2065a804855b6327fec2a00c009de1c9eec2f59fc7272b94c3f7846306315b",1]
|
||||
|
|
@ -7348,8 +7335,6 @@
|
|||
["src/gateway/mcp-http.test.ts","../agents/node-exec-availability.js","67ced7d75c824807fcdc173f914d08f94b4d4e64635add13687a2413953ed9ad",1]
|
||||
["src/gateway/mcp-http.test.ts","./tool-resolution.js","2f639cb0decf41151a4c75c17b19c85464bb7b5bafa0a512b1592f98b28b1e8a",1]
|
||||
["src/gateway/mcp-http.web-search.test.ts","../agents/agent-tools.before-tool-call.js","c88c13076deab84767ed04af12524075e559aa15538ff18a6a693ff02c1014e8",1]
|
||||
["src/gateway/mcp-http/rooted-execution.test.ts","../../plugins/hook-runner-global.js","1bbcc11318d7e6fdb669b7837445d214b2cf63654843a9ab7517c5a4373a66e9",1]
|
||||
["src/gateway/mcp-http/rooted-tools.test.ts","../../agents/tools/gateway.js","4f8268c82aad8b0f6fe808a38b753edc7431413247f5675e3187c345defcf5a8",1]
|
||||
["src/gateway/mcp-oauth-callback.test.ts","../agents/mcp-oauth-store.js","dc691449bee52ab522216dbfe4713bc21dbb681f1ba9b7e4ee90a7014e41f1fd",1]
|
||||
["src/gateway/mcp-oauth-callback.test.ts","../agents/mcp-oauth.js","3c1bb3242b60222aa1a3bcd8c1275a1e3e98924a4f819347c01e201ffc7d0fdc",1]
|
||||
["src/gateway/mcp-oauth-callback.test.ts","../state/openclaw-state-worker-context.js","56e282604a5b5d2b8a2b9b01c0189c1034a9f0cb65377a9b6f4052a0d73b1f78",1]
|
||||
|
|
@ -7447,8 +7432,6 @@
|
|||
["src/gateway/server-maintenance.telemetry.test.ts","../infra/telemetry.js","c2a0e43bc1fceb877dfcb882832d8eceb8d0d381ada53adc903f5ea8e242391d",1]
|
||||
["src/gateway/server-maintenance.telemetry.test.ts","../media/store.js","b2b25bad1973aa63dba575502338cbec9710c7100860904b7241febe7578ae4e",1]
|
||||
["src/gateway/server-maintenance.telemetry.test.ts","../plugin-state/plugin-state-worker-client.js","22c2abd5624a05e74784356ec104f4470083f888e1d853c9cb94fe44b0f09020",1]
|
||||
["src/gateway/server-maintenance.telemetry.test.ts","../skills/workshop/store-sqlite-record.js","132b80ce02d91cdd489eeb5b8b27fe84017dac6db91c75846796a9b4b5dcd527",1]
|
||||
["src/gateway/server-maintenance.telemetry.test.ts","../skills/workshop/workspace-skill-read.js","a821256a1b374d995861441afb7011618060b0a30a21fe0d8f3bb6f10d13a2c7",1]
|
||||
["src/gateway/server-maintenance.telemetry.test.ts","./chat-abort.js","b025828acde25bc18c21cdf68bb814f994be7751e3565692acbf12d6a0e77975",1]
|
||||
["src/gateway/server-maintenance.telemetry.test.ts","./server-methods/session-active-runs.js","4762dc0a9243193c540ed67870afe774e67df48a8534135b0136aede19aab590",1]
|
||||
["src/gateway/server-maintenance.telemetry.test.ts","./server/health-state.js","9006fb3bf77d6e6473d8c2b0bdd92bdd1500930c32de8d151f40d27711583357",1]
|
||||
|
|
@ -7820,7 +7803,6 @@
|
|||
["src/gateway/server-methods/sessions.send-followup-status.test.ts","../session-utils.js","c991cfa1013d1fad11f6df188a5a4c7fb4c113531a5195760896a054568c0a75",1]
|
||||
["src/gateway/server-methods/sessions.send-followup-status.test.ts","./chat-send-external-entry.js","f34a4d3f270b12cc95eb6753ca2a125206177a45fd8b161933daaa13a179149e",1]
|
||||
["src/gateway/server-methods/sessions.send-yield-resume.test.ts","./chat-send-external-entry.js","ada44aa78f4d08227ff21aa7788e027794eb5d991187274cd179219eab61083b",1]
|
||||
["src/gateway/server-methods/skills-curator.test.ts","../../skills/workshop/curator.js","935aa6334ed7c5a2c58a691fa878f895abc07166e7e57a96d4c989c31a7eea77",1]
|
||||
["src/gateway/server-methods/skills-upload.test.ts","../../plugins/install-security-scan.js","eb888f0abaa57b42fa2dca7e31f92ea57356e75ded5810432d1897b7b9b10735",1]
|
||||
["src/gateway/server-methods/skills.clawhub.test.ts","../../agents/agent-scope.js","41b7c23048edd822918d9a6e6e9dfd3354a6ce202bea02f8e77a5c40983b9a77",1]
|
||||
["src/gateway/server-methods/skills.clawhub.test.ts","../../config/config.js","4df9f8727b72fce829834eea72a9efbf2967c9433b878d7da1cdca69740e367e",1]
|
||||
|
|
@ -7831,14 +7813,6 @@
|
|||
["src/gateway/server-methods/skills.clawhub.test.ts","../../skills/lifecycle/clawhub.js","35b083807cfc03410e0ee0226d4e582a6a2ec8fd33578b8987253e40a01367f6",1]
|
||||
["src/gateway/server-methods/skills.clawhub.test.ts","../../skills/lifecycle/install.js","70f8a7b89e6ed726e73a8a1b444be93cb4a86a48936e08030276f331462500b0",1]
|
||||
["src/gateway/server-methods/skills.install-upload-policy.test.ts","../../plugins/install-security-scan.js","8260033f1a8e51bdb882f810c8d9b607411e6a54a8010de0ecf58710816e3004",1]
|
||||
["src/gateway/server-methods/skills.proposals.test.ts","../../agents/agent-scope.js","6e51410ffd93690a81947258dcb98cc84c4c68a6afcf4b1fc773412904457622",1]
|
||||
["src/gateway/server-methods/skills.proposals.test.ts","../../config/config.js","7b4b6de0f3fc92e963040b8618c9175315bd951232592c6373673b09ce00372d",1]
|
||||
["src/gateway/server-methods/skills.proposals.test.ts","../../infra/clawhub-skills.js","5c6c350ad84263b2dac06cf228bb802e69b83b8b0facda663ca60f2c969b3cd9",1]
|
||||
["src/gateway/server-methods/skills.proposals.test.ts","../../skills/lifecycle/clawhub.js","835a04ac092a007bb2516c26b290fdfd4840671e205fe45307a4a961b05ca80b",1]
|
||||
["src/gateway/server-methods/skills.proposals.test.ts","../../skills/lifecycle/install.js","f2891a3e2279834bd13566a65539a67076711ad9e78ec3b299a8381acbdfbfc8",1]
|
||||
["src/gateway/server-methods/skills.proposals.test.ts","../../skills/lifecycle/upload-install.js","ecfde2a64fdce5b0e64744d3a930010f19104babe8003a87d70e7d18df0bc9da",1]
|
||||
["src/gateway/server-methods/skills.proposals.test.ts","../../skills/security/clawhub-verdicts.js","c1fee0b68a0ba97e94f9338843e8aa80b560f3da4ed55f10a188752471e04fd1",1]
|
||||
["src/gateway/server-methods/skills.proposals.test.ts","./chat-send-handler.js","c941fc433cf4ab6cb35a743c406cc1577d433a00dc6ca7183c51e961079ce18d",1]
|
||||
["src/gateway/server-methods/skills.publisher-identity.test.ts","../../agents/agent-scope.js","b6a5a88b92f44ee253011bb7e37dd99c4d4da69f4aa2caa815fef40a39b5ac11",1]
|
||||
["src/gateway/server-methods/skills.publisher-identity.test.ts","../../config/config.js","b3d1ec9dd3f7a4243541a42cdfb8a7dfe8a23104e792431c8d46a5072701366a",1]
|
||||
["src/gateway/server-methods/skills.publisher-identity.test.ts","../../skills/lifecycle/install.js","f2891a3e2279834bd13566a65539a67076711ad9e78ec3b299a8381acbdfbfc8",1]
|
||||
|
|
@ -9848,10 +9822,6 @@
|
|||
["src/skills/runtime/session-snapshot.test.ts","../loading/workspace-skill-prompt.js","c26cc28752b5b5247baef0a5ca540f92bf17a9c022ced2bf0b775f346fcfd39b",1]
|
||||
["src/skills/runtime/session-snapshot.test.ts","./refresh-state.js","2ee38c0b16913304704f47232c9f735f8faadc0e7b504480c5136d00fb816cee",1]
|
||||
["src/skills/runtime/session-snapshot.test.ts","./refresh.js","46743217bc462d49d0ef2ed28dd581cf8c4f9485d96c896acc110cc1d2df0d6a",1]
|
||||
["src/skills/workshop/collection-restore.test.ts","../lifecycle/skill-change-hook.js","a204b1a7a4c87b548e518df12b00dbf47c3de2f650cb341438543055a3c2d408",1]
|
||||
["src/skills/workshop/experience-review.apply.test.ts","../../agents/embedded-agent.js","f90a67daca3c760996fffe642110504bb733c4705d83c44770cda58acf78695e",1]
|
||||
["src/skills/workshop/service-evaluation.test.ts","../../plugins/hook-runner-global.js","7b2e4b2069647a977b79fe28f60408dc41197b8341fc93c80e2532d270ecf394",1]
|
||||
["src/skills/workshop/service-lifecycle-hooks.test.ts","../../plugins/hook-runner-global.js","e931e71519abeecf3554fe64b110c7647ba3589f35cf6641f2c045a4d073a8d2",1]
|
||||
["src/snapshot/local-repository.windows-acl-probe.test.ts","../process/exec.js","d6ef266d41c26ca79d6c5e5788c6a39504596ca03433571e5189474b4770a15c",1]
|
||||
["src/state/backup-run-records.test.ts","../../packages/terminal-core/src/note.js","f5864cdeb859b12765491a2066778d233cade85ee0bc133481fa4580397b105b",1]
|
||||
["src/state/onboarding-recommendations.worker-lifecycle.test.ts","./openclaw-state-read-worker.js","5f99f95d17588a8ba082399db09c21e9d60ce587c020f450e37703a64b6eddda",1]
|
||||
|
|
@ -10411,7 +10381,6 @@
|
|||
["ui/src/pages/sessions/sessions-page.scope-race.test.ts","../../components/confirm-dialog.ts","29010d4b48ecb6a8932036adcb5c2d910c9c76c242c0124481a10ac96470b5e9",1]
|
||||
["ui/src/pages/sessions/sessions-page.test.ts","../../components/confirm-dialog.ts","29010d4b48ecb6a8932036adcb5c2d910c9c76c242c0124481a10ac96470b5e9",1]
|
||||
["ui/src/pages/sessions/sessions-page.test.ts","../../components/input-dialog.ts","fbfe3fd11c857de65c62af1111b77bb349318f86081123667530c611cec07487",1]
|
||||
["ui/src/pages/skill-workshop/proposals.test.ts","../../lib/skill-workshop/diff-worker.ts","9e47ffd855b5620fe43264a4c229286d7b98b383512e8f64ddb3fecb271be354",1]
|
||||
["ui/src/pages/worktrees/worktrees-page.test.ts","../../components/confirm-dialog.ts","29010d4b48ecb6a8932036adcb5c2d910c9c76c242c0124481a10ac96470b5e9",1]
|
||||
["ui/src/plugins/control-ui-runtime.test.ts","./control-ui-loader.ts","442f9fbfb3799fd4b230a2d2ed48ae4ecc67ee61f13c13b19007eb38d2ed3709",1]
|
||||
["ui/src/test-helpers/app-sidebar.ts","../components/sidebar-attention.ts","3fd2562bf996958dad169dcaee8c9e073f98090f79687de0ba938af14b8982f8",1]
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ src/process/command-queue.publish-transaction.test.ts 3
|
|||
src/process/spawn-broker/command-startup.test.ts 1
|
||||
src/process/spawn-broker/startup.test.ts 1
|
||||
src/process/supervisor/service-child-relay-host.awaited-output.test.ts 1
|
||||
src/skills/workshop/experience-review.apply.test.ts 1
|
||||
src/state/openclaw-database-preflight.lifecycle.test.ts 4
|
||||
src/state/openclaw-state-lease-existing.test.ts 1
|
||||
src/system-agent/setup-lifetime.test.ts 1
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"core": 2495,
|
||||
"core": 2493,
|
||||
"channel": 3874,
|
||||
"plugin": 4527
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
1fd344bb5942819d8e4a4b2cfc22b40c8e6db8fa2630ae9330775e00edd76c3d config-baseline.json
|
||||
cedc5254457691642cb4d00ebdff5096e3732b53ef4a3862d56106c410243b20 config-baseline.core.json
|
||||
7da93e3e09afe03f8b2f4a59e68ba1213c4f76aa4e6021aaf8ab531825578d82 config-baseline.json
|
||||
81ce3d68adebc8403606d7c87caada27b450861072b08bb8747bb78e2c6f3e18 config-baseline.core.json
|
||||
452cba5854c9b6727126974a6525b8c8c0a359d76ccbfa6f2ebf9da40274fc7d config-baseline.channel.json
|
||||
1f1ecddd1eb972e4ec784e79e3f2676247b27faf807b1f4da734deb2fba9e0ba config-baseline.plugin.json
|
||||
|
|
|
|||
|
|
@ -22,10 +22,9 @@ Every job carries exactly one payload kind, chosen by flag:
|
|||
| Command | `--command <shell>` or `--command-argv <json>` | A shell/process on the Gateway host, no model call |
|
||||
| Script | `--script <file\|->` | A headless code-mode script using the owning agent's tools |
|
||||
|
||||
System-owned monitor jobs are gateway-converged and cannot be created or edited through the CLI or API. The `heartbeat` kind creates one heartbeat monitor job per heartbeat-enabled agent (see [Heartbeat](/gateway/heartbeat)). The weekly Skill Workshop review is a normal isolated `agentTurn` job with a reserved declaration key. Both appear in `openclaw cron list`; use `--all` to include disabled rows.
|
||||
The `skillCollectionReview` payload kind is not accepted. Stored rows that use it are replaced with the canonical review job.
|
||||
System-owned monitor jobs are gateway-converged and cannot be created or edited through the CLI or API. The `heartbeat` kind creates one heartbeat monitor job per heartbeat-enabled agent (see [Heartbeat](/gateway/heartbeat)). Monitor jobs appear in `openclaw cron list`; use `--all` to include disabled rows.
|
||||
|
||||
Skill collection review runs every 7 days. It is enabled when `skills.workshop.autonomous.mode` is `auto`; `propose` and `off` keep the system-owned job disabled. In `auto` mode, a review stays disabled when every statically resolvable model candidate is known to lack rooted execution support. Its display name includes `no-rooted-runtime`; inspect disabled rows with `openclaw cron list --all --json`. A supported fallback keeps the job enabled. Stored session model/runtime preferences and unknown eligibility also keep it enabled, with final checks at execution time. The Gateway converges these jobs at startup and after config reload. Convergence clears the reason and restores auto-mode enablement when the configured chain becomes eligible or unknown. Scheduled reviews require automations. When `cron.enabled` is `false` or `OPENCLAW_SKIP_CRON=1`, the Gateway logs a startup warning and does not run scheduled reviews. There is no separate weekly Gateway timer.
|
||||
The weekly Skill Workshop curator (declaration key `skill-collection-review:<agentId>`, and the older `skillCollectionReview` payload kind) is retired. The Gateway deletes those stored rows when it loads the cron store and creates no replacement; the declaration-key namespace stays reserved. Learned-skill cleanup now runs without a schedule; see [Unused-skill cleanup](/tools/skill-workshop#unused-skill-cleanup).
|
||||
|
||||
### Agent-turn options
|
||||
|
||||
|
|
|
|||
|
|
@ -126,16 +126,6 @@ catalog. Historical snapshot paths do not require cleanup or a session reset.
|
|||
|
||||
`core/doctor/local-audio-acceleration` reports the auto-selected local STT command, separate capable/requested/observed backend evidence, and fallback order without loading a speech model. It emits an informational finding, so include `--severity-min info` to display it.
|
||||
|
||||
`core/doctor/skill-workshop-relocation` distinguishes pending legacy collection
|
||||
backup roots from roots preserved for review. Eligible proposals or backup roots
|
||||
receive `openclaw doctor --fix` guidance, not a guarantee that every backup will
|
||||
be retired. Preserved roots require manual review of workspace ownership, backup
|
||||
manifests, and workspace migration blockers. If both kinds remain, Doctor reports
|
||||
both next steps. Do not delete preserved backups to clear the warning.
|
||||
The check uses the configured agent directories and actual filesystem paths even
|
||||
when lint reads a private state snapshot. Correctly placed Workshop targets do
|
||||
not need relocation merely because lint uses a temporary directory.
|
||||
|
||||
## Check selection
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ Its bounded summary lists observed refusals before derivative blocked steps;
|
|||
the full receipt list retains the complete chain.
|
||||
|
||||
Doctor imports recognized legacy workspace setup files during preflight, before
|
||||
Workshop migration accesses workspace state. An existing canonical SQLite setup record wins,
|
||||
other migrations access workspace state. An existing canonical SQLite setup record wins,
|
||||
including milestones that are absent in SQLite. Doctor does not replay stale
|
||||
milestones over it. Before removing a validated setup file or interrupted claim,
|
||||
Doctor preserves its exact bytes beside the original as
|
||||
|
|
@ -172,11 +172,26 @@ run has no workspace setup migration to repeat. Invalid files and workspace
|
|||
identity/version conflicts remain blocked for inspection.
|
||||
|
||||
Update rehearsals write only inside their copied state directory. Workspace
|
||||
files are not copied by the rehearsal, so absolute paths retained in proposal,
|
||||
rollback, and backup records remain read-only inventory. Doctor reports how many
|
||||
legacy workspace files it left untouched; it does not retire their files or
|
||||
proposal history. After the candidate is installed, the real Doctor runs the
|
||||
normal import, archival, and relocation against the operator's state.
|
||||
files are not copied by the rehearsal, so absolute paths retained in legacy
|
||||
records remain read-only inventory. After the candidate is installed, the real
|
||||
Doctor runs the normal import and archival against the operator's state.
|
||||
|
||||
Skill Workshop proposals were removed. Doctor exports each pending or
|
||||
quarantined proposal draft, with its support files, to
|
||||
`<state-dir>/agents/<agentId>/agent/workshop-skills/.archive/.retired-proposals/<proposal-id>/`,
|
||||
then drops the proposal tables. Exported drafts are not live skills; to keep
|
||||
one, ask the agent to save it with `/learn` so it goes through the normal
|
||||
validated, versioned Workshop write.
|
||||
|
||||
Doctor keeps the proposal tables and files, with a recoverable warning, when no
|
||||
configured agent owns a proposal, it cannot export a draft, or it finds a
|
||||
half-finished apply it cannot safely undo. That is a deliberate holdback, not a
|
||||
failed migration, and Doctor still exits successfully: follow the warning. For
|
||||
an unowned draft, add its agent back to the config, or save the whole proposal
|
||||
directory (draft and support files) with `/learn` and delete it; then rerun
|
||||
`openclaw doctor --fix` to finish the retirement. The pre-apply contents of a
|
||||
half-finished apply are in the `skill_workshop_proposal_rollbacks` table until
|
||||
then.
|
||||
|
||||
Plugin migrations with declared files outside the copied state are deferred as
|
||||
one plugin operation. Doctor leaves their files and pending markers intact and
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ openclaw [--dev] [--profile <name>] <command>
|
|||
install
|
||||
update
|
||||
verify
|
||||
workshop list|inspect|propose-create|propose-update|revise|apply|reject|quarantine
|
||||
workshop list|changes|show|archive|restore
|
||||
list
|
||||
info
|
||||
check
|
||||
|
|
|
|||
|
|
@ -67,16 +67,11 @@ openclaw skills info <name> --agent <id>
|
|||
openclaw skills check
|
||||
openclaw skills check --agent <id>
|
||||
openclaw skills check --json
|
||||
openclaw skills curator status
|
||||
openclaw skills curator status --json
|
||||
openclaw skills workshop propose-create --name "qa-check" --description "QA checklist" --proposal ./PROPOSAL.md
|
||||
openclaw skills workshop propose-update qa-check --proposal ./PROPOSAL.md
|
||||
openclaw skills workshop list
|
||||
openclaw skills workshop inspect <proposal-id>
|
||||
openclaw skills workshop revise <proposal-id> --proposal ./PROPOSAL.md
|
||||
openclaw skills workshop apply <proposal-id>
|
||||
openclaw skills workshop reject <proposal-id> --reason "Not reusable"
|
||||
openclaw skills workshop quarantine <proposal-id> --reason "Needs security review"
|
||||
openclaw skills workshop changes --limit 20
|
||||
openclaw skills workshop show <name>
|
||||
openclaw skills workshop archive <name> --reason "Superseded"
|
||||
openclaw skills workshop restore <name>
|
||||
```
|
||||
|
||||
`search`, `update`, and `verify` use ClawHub directly. `install @owner/<slug>`
|
||||
|
|
@ -111,8 +106,7 @@ characters.
|
|||
|
||||
`info` resolves an exact skill name before a metadata key. Key, case-insensitive,
|
||||
and separator-normalized matches must identify one skill; ambiguous selectors
|
||||
fail instead of choosing discovery order. Workshop reads and update targeting
|
||||
use the same lookup.
|
||||
fail instead of choosing discovery order.
|
||||
|
||||
`check` separates **inventory**, **readiness**, and **visibility**:
|
||||
|
||||
|
|
@ -132,10 +126,10 @@ in inventory but not currently eligible or visible to the agent; install or
|
|||
configure the reported prerequisite before expecting the model to use it
|
||||
successfully.
|
||||
|
||||
Curator `status`, `pin`, `unpin`, and `restore`, plus Workshop `apply`, preserve
|
||||
the same target boundary. They never read or mutate client-local state after an
|
||||
explicitly selected Gateway fails; intentional offline behavior remains
|
||||
available only for an implicitly selected local Gateway.
|
||||
Workshop commands preserve the same target boundary. They never read or mutate
|
||||
client-local state after an explicitly selected Gateway fails; intentional
|
||||
offline behavior remains available only for an implicitly selected local
|
||||
Gateway.
|
||||
|
||||
Git and local directory installs expect `SKILL.md` at the source root. The
|
||||
install slug comes from `SKILL.md` frontmatter `name` when it is valid, then
|
||||
|
|
@ -185,24 +179,8 @@ Notes:
|
|||
| Skill Card fingerprint | Installed ClawHub bundles can include a generated `skill-card.md`. OpenClaw treats verification as a ClawHub server decision and does not reject an installed skill just because that generated card changes the bundle fingerprint. |
|
||||
| `check --agent <id>` | Checks the selected agent's workspace and reports which ready skills are actually visible to that agent's prompt or command surface. |
|
||||
| `workshop --agent <id>` | Accepted before or after a Workshop leaf command, for example `workshop --agent <id> list` or `workshop list --agent <id>`. If both are provided, the leaf value wins. |
|
||||
| `curator --json` | Accepted before or after a Curator leaf command, for example `curator --json status` or `curator status --json`. |
|
||||
| `list` | Default action when no subcommand is provided. |
|
||||
| `list`/`info`/`check` output | Rendered output goes to stdout. With `--json`, the machine-readable payload stays on stdout for pipes and scripts. |
|
||||
| `curator status --json` | Reports live Workshop skill usage recorded from trusted `skill.used` events, collection review outcomes per agent, and experience review outcomes per agent and workspace. |
|
||||
| `curator pin`/`unpin`/`restore` | Retired commands remain registered but return an error explaining that weekly collection review manages the skill collection. |
|
||||
|
||||
### Workshop inventory and upgrades
|
||||
|
||||
`openclaw skills curator status` requests current Workshop inventory from the
|
||||
selected Gateway. With a compatible Gateway, JSON includes
|
||||
`"inventory": "live-workshop"`. Local status uses the current configuration too.
|
||||
Missing usage displays as `not recorded`, not proof that a skill was never used.
|
||||
|
||||
An older Gateway can return an unmarked legacy response. The CLI accepts it
|
||||
without switching to local state and prints a limited-coverage notice in text
|
||||
output. JSON preserves the absence of the marker. See
|
||||
[Workshop inventory and usage](/tools/skill-workshop/reference#workshop-inventory-and-usage)
|
||||
for membership, tracking limits, unknown dates, and upgrade behavior.
|
||||
|
||||
On servers supporting full scanner reports, verification JSON includes `security.scannerReports.aig` (the full upstream SARIF report)
|
||||
and `security.scannerReports.skillspector` (the full upstream JSON report) when ClawHub
|
||||
|
|
@ -353,32 +331,32 @@ existing session. Removal preserves already selected revisions. See
|
|||
|
||||
## Skill Workshop
|
||||
|
||||
`openclaw skills workshop` manages pending skill proposals for the selected
|
||||
agent. Proposals are not active skills until applied. For proposal
|
||||
storage, support-file safeguards, Gateway methods, and approval policy, see
|
||||
`openclaw skills workshop` inspects and manages the learned (Workshop) skills
|
||||
of the selected agent. Agents create and update these skills themselves; every
|
||||
change applies immediately and saves the previous version first. See
|
||||
[Skill Workshop](/tools/skill-workshop).
|
||||
|
||||
```bash
|
||||
openclaw skills workshop propose-create \
|
||||
--name "qa-check" \
|
||||
--description "Repeatable QA checklist" \
|
||||
--proposal ./PROPOSAL.md
|
||||
openclaw skills workshop propose-create \
|
||||
--name "qa-check" \
|
||||
--description "Repeatable QA checklist" \
|
||||
--proposal-dir ./qa-check-proposal
|
||||
openclaw skills workshop propose-update qa-check --proposal ./PROPOSAL.md
|
||||
openclaw skills workshop list
|
||||
openclaw skills workshop inspect <proposal-id>
|
||||
openclaw skills workshop revise <proposal-id> --proposal ./PROPOSAL.md
|
||||
openclaw skills workshop apply <proposal-id>
|
||||
openclaw skills workshop reject <proposal-id> --reason "Duplicate"
|
||||
openclaw skills workshop quarantine <proposal-id> --reason "Needs security review"
|
||||
openclaw skills workshop changes --limit 20
|
||||
openclaw skills workshop show release-notes
|
||||
openclaw skills workshop show release-notes --file references/checklist.md
|
||||
openclaw skills workshop show release-notes --version <version-id>
|
||||
openclaw skills workshop archive release-notes --reason "Superseded by deploy-notes"
|
||||
openclaw skills workshop restore release-notes
|
||||
openclaw skills workshop restore release-notes --version <version-id>
|
||||
```
|
||||
|
||||
`propose-create`, `propose-update`, and `revise` also accept `--goal <text>`
|
||||
and `--evidence <text>` to record the proposal's motivation and supporting
|
||||
notes alongside the `--proposal`/`--proposal-dir` content.
|
||||
| Command | Description |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `list` | Lists live learned skills with usage counts, plus archived skills. Shows the autonomous mode and skills root. |
|
||||
| `changes [--limit <n>]` | Shows recent changes, newest first: who made them (`agent`, `review`, `curator`, `user`), the action, and the saved version id. |
|
||||
| `show <name> [--file <path>] [--version <id>]` | Prints `SKILL.md` or one support file, from the live skill or a saved version. |
|
||||
| `archive <name> [--reason <text>]` | Hides the skill from its agent. The skill stays restorable. |
|
||||
| `restore <name> [--version <id>]` | Restores the newest saved version, or the named one. The current live copy is saved first, so restore is undoable. |
|
||||
|
||||
Every Workshop command accepts `--agent <id>` and `--json`. `archive` and
|
||||
`restore` act as `user` in the change feed.
|
||||
|
||||
## Related
|
||||
|
||||
|
|
|
|||
|
|
@ -858,6 +858,34 @@
|
|||
"source": "/tools/skills-workshop",
|
||||
"destination": "/tools/skill-workshop"
|
||||
},
|
||||
{
|
||||
"source": "/tools/skill-workshop/how-it-works",
|
||||
"destination": "/tools/skill-workshop"
|
||||
},
|
||||
{
|
||||
"source": "/tools/skill-workshop/collection-review",
|
||||
"destination": "/tools/skill-workshop#unused-skill-cleanup"
|
||||
},
|
||||
{
|
||||
"source": "/tools/skill-workshop/authoring",
|
||||
"destination": "/tools/skill-workshop#learn"
|
||||
},
|
||||
{
|
||||
"source": "/tools/skill-workshop/proposals",
|
||||
"destination": "/tools/skill-workshop#upgrading-from-earlier-releases"
|
||||
},
|
||||
{
|
||||
"source": "/tools/skill-workshop/configuration",
|
||||
"destination": "/tools/skill-workshop#configuration"
|
||||
},
|
||||
{
|
||||
"source": "/tools/skill-workshop/reference",
|
||||
"destination": "/tools/skill-workshop#operator-surfaces"
|
||||
},
|
||||
{
|
||||
"source": "/tools/skill-workshop/troubleshooting",
|
||||
"destination": "/tools/skill-workshop#troubleshooting"
|
||||
},
|
||||
{
|
||||
"source": "/skills-config",
|
||||
"destination": "/tools/skills-config"
|
||||
|
|
@ -1886,19 +1914,7 @@
|
|||
"tools/skills",
|
||||
"tools/custodian-skills",
|
||||
"tools/skill-workshop",
|
||||
{
|
||||
"group": "Skill Workshop",
|
||||
"pages": [
|
||||
"tools/skill-workshop/personal-library",
|
||||
"tools/skill-workshop/how-it-works",
|
||||
"tools/skill-workshop/collection-review",
|
||||
"tools/skill-workshop/authoring",
|
||||
"tools/skill-workshop/proposals",
|
||||
"tools/skill-workshop/configuration",
|
||||
"tools/skill-workshop/reference",
|
||||
"tools/skill-workshop/troubleshooting"
|
||||
]
|
||||
},
|
||||
"tools/skill-workshop/personal-library",
|
||||
"tools/self-learning",
|
||||
"tools/creating-skills",
|
||||
"tools/skills-config",
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
|
|||
- `entries.<skillKey>.enabled: false` disables a skill even if bundled/installed.
|
||||
- `entries.<skillKey>.apiKey`: convenience for skills declaring a primary env var (plaintext string or SecretRef object).
|
||||
- `limits.maxCandidatesPerRoot`, `limits.maxSkillsLoadedPerSource`, `limits.maxSkillsInPrompt`, `limits.maxSkillsPromptChars`, `limits.maxSkillFileBytes`: bound skill discovery and the model-facing skills prompt.
|
||||
- Skill Workshop autonomy/approval settings (`workshop.autonomous.mode`, `workshop.approvalPolicy`, `workshop.maxPending`, `workshop.maxSkillBytes`) are documented in [Skills configuration](/tools/skills-config).
|
||||
- Skill Workshop settings (`workshop.autonomous.mode`, `workshop.maxSkillBytes`) are documented in [Skills configuration](/tools/skills-config).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -957,6 +957,7 @@ against the current SQLite owners before the import can rename profiles.
|
|||
| `session.maintenance.rotateBytes` | removed (deprecated) |
|
||||
| Runtime and channel tuning knobs retired in 2026.7 | removed (built-in production defaults apply) |
|
||||
| `diagnostics.memoryPressureSnapshot`, legacy `diagnostics.memoryPressureBundle` | removed (automatic critical-memory snapshots were retired; no replacement automatic capture) |
|
||||
| `skills.workshop.autonomous.mode: "propose"`, `skills.workshop.approvalPolicy`, `skills.workshop.maxPending` | `"off"`; proposal settings removed (Skill Workshop proposals were retired) |
|
||||
|
||||
Doctor migrates MCP `type: "http"` to `transport: "streamable-http"` and `type: "sse"` to `transport: "sse"` in both server maps. An existing `transport` wins. For command-based servers, Doctor removes `type: "stdio"`; the command still selects stdio. The update-time Doctor pass uses the same backed-up config repair. Plugin bundle files keep their external `type` format: bundle loading translates recognized types, and CLI exports use the destination's required format. An unknown bundle HTTP transport is rejected instead of being treated as SSE; its original `type` remains available to the destination CLI.
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ auth health, sandbox images, and plugin installs.
|
|||
|
||||
The JSON plugin index at `plugins/installs.json` is retired. Doctor leaves it unchanged and directs operators to run `openclaw doctor --fix` on `2026.9.5` with a pre-update backup. Updates also check this original file before stopping the running Gateway. Current SQLite plugin indexes and install records remain supported.
|
||||
|
||||
Legacy Skill Workshop proposal imports, skill relocation, and collection-backup repair belong to Doctor, including the repair pass during `openclaw update`. Normal Gateway and local CLI startup leave those artifacts untouched and do not discover Workshop backup roots or scan proposals for repair. Run `openclaw doctor --fix` or `openclaw doctor --yes` against the same state and config to complete a legacy Workshop migration. Plain Doctor can report remaining Workshop artifacts with repair guidance.
|
||||
Retired Skill Workshop proposals belong to Doctor, including the repair pass during `openclaw update`. Doctor exports each pending or quarantined draft to `<agentDir>/workshop-skills/.archive/.retired-proposals/<proposal-id>/`, drops the retired proposal tables, and removes `<state-dir>/skill-workshop/proposals/`. Normal Gateway and local CLI startup leave those artifacts untouched. A failed export keeps the tables and files and reports a warning; rerun `openclaw doctor --fix` after fixing the cause.
|
||||
|
||||
A rewritten or truncated legacy audit raw archive does not stop Doctor or update finalization. Doctor quarantines it beside itself, preserves the sanitized archive and existing SQLite records, and reports the quarantined path once. Later repairs continue. See [legacy audit recovery](/cli/update/repair-and-recovery#skipped-legacy-audit-recovery) for retained backups and recovery limits.
|
||||
|
||||
|
|
|
|||
|
|
@ -352,20 +352,6 @@ Runtime caps such as cron `toolsAllow` are normalized and group-expanded by
|
|||
OpenClaw before this contract is built. Native tools are disabled, and a
|
||||
backend without a complete declared enforcement path fails before execution.
|
||||
|
||||
Rooted runs such as [Skill Workshop reviews](/tools/skill-workshop) also require
|
||||
`isolatesInstructionsWithExactTools: true` on the backend registration. Declare
|
||||
this optional capability only when exact-tool execution suppresses ambient
|
||||
instruction files, skills, hooks, and plugins for both fresh and resumed runs.
|
||||
The host-prepared instruction snapshot must remain authoritative. OpenClaw
|
||||
rejects rooted runs when this declaration is absent, even if the backend can
|
||||
enforce exact tools. Existing non-rooted runs do not require this field.
|
||||
|
||||
The bundled Claude CLI backend declares this capability. Rooted execution
|
||||
disables its native tools and serves the selected OpenClaw tools through the
|
||||
host-owned MCP grant, which retains the root, filesystem policy, and configured
|
||||
sandbox. The declaration does not grant filesystem or approval authority to
|
||||
the backend.
|
||||
|
||||
A backend whose native tools are model-callable may declare
|
||||
`projectNativeToolAuthority(nativeTools)` so that automations created from its
|
||||
sessions keep the creator's native capabilities. For Claude stream-JSON, the
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ resolve here.
|
|||
|
||||
- <a id="registration-and-execution"></a>[Registration and execution](/plugins/hooks/reference#registration-and-execution)
|
||||
- <a id="hook-catalog"></a>[Hook catalog](/plugins/hooks/reference#hook-catalog)
|
||||
- <a id="skill-lifecycle-and-evaluation"></a>[Skill lifecycle and evaluation](/plugins/hooks/reference#skill-lifecycle-and-evaluation)
|
||||
- <a id="skill-lifecycle-and-evaluation"></a>[Skill lifecycle](/plugins/hooks/reference#skill-lifecycle)
|
||||
- <a id="channel-pairing-requests"></a>[Channel pairing requests](/plugins/hooks/reference#channel-pairing-requests)
|
||||
|
||||
### Tool call policy hooks
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ summary: "How plugin hooks register and execute, plus the complete typed hook ca
|
|||
read_when:
|
||||
- You are wiring a typed handler and need the ordering, timeout, and failure rules
|
||||
- You want to know which typed hook exists for a surface
|
||||
- You are registering a Skill Workshop evaluator or a pairing observer
|
||||
- You are registering a skill lifecycle or pairing observer
|
||||
title: "Hook reference"
|
||||
sidebarTitle: "Hook reference"
|
||||
---
|
||||
|
|
@ -92,10 +92,9 @@ The standard runner applies these defaults **per handler**:
|
|||
| -------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------- |
|
||||
| `before_agent_run`, `before_tool_call`, `before_install` | 15 seconds | Fail closed: block the run, tool call, or install |
|
||||
| `before_agent_finalize`, `before_prompt_build`, `message_sending`, `reply_payload_sending`, `resolve_exec_env` | 15 seconds | Log and skip the failed handler; retain other successful results |
|
||||
| `agent_end`, `before_compaction`, `after_compaction`, `skill_changed`, `skill_proposal_changed` | 30 seconds | Log and continue |
|
||||
| `agent_end`, `before_compaction`, `after_compaction`, `skill_changed` | 30 seconds | Log and continue |
|
||||
| `channel_pairing_requested` | 2 seconds | Log and continue |
|
||||
| `gateway_stop` | 5 seconds | Log and continue shutdown |
|
||||
| `skill_proposal_evaluate` | 120 seconds | Record an attributed error outcome |
|
||||
| Other asynchronous hooks, including claim hooks | No runner timeout unless configured | Log and continue |
|
||||
| `tool_result_persist`, `before_message_write` | No asynchronous timeout | Synchronous errors are logged; failed results are ignored |
|
||||
|
||||
|
|
@ -242,69 +241,14 @@ For `sessions.create` calls with `parentSessionKey` and `emitCommandHooks: true`
|
|||
| `cron_reconciled` | Observe | Reconcile against the complete Gateway cron state after startup or reload |
|
||||
| `cron_changed` | Observe | Observe Gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled) |
|
||||
| `before_install` | Modify / gate | Inspect staged skill or plugin install material from a loaded plugin runtime |
|
||||
| `skill_proposal_evaluate` | Evaluate | Evaluate one exact Skill Workshop draft and return attributed findings, metrics, or a decision |
|
||||
| `skill_proposal_changed` | Observe | Observe durable Skill Workshop proposal lifecycle events after they commit |
|
||||
| `skill_changed` | Observe | Observe committed live-skill create, update, and removal events |
|
||||
|
||||
### Skill lifecycle and evaluation
|
||||
### Skill lifecycle
|
||||
|
||||
Use `skill_proposal_evaluate` for static analyzers, security scanners,
|
||||
benchmarks, model-based graders, or other third-party evaluators. OpenClaw
|
||||
passes an immutable candidate bundle with file hashes and a tree hash. Update
|
||||
proposals also include the complete current skill as `baseline`. Text files use
|
||||
UTF-8 content; binary files use base64.
|
||||
|
||||
Evaluator registrations run concurrently. Give each evaluator a stable
|
||||
`registrationId`:
|
||||
|
||||
```typescript
|
||||
api.on(
|
||||
"skill_proposal_evaluate",
|
||||
async (event) => {
|
||||
const score = await evaluateBundle(event.candidate, event.baseline);
|
||||
return {
|
||||
evaluatorVersion: "rules-2026-07",
|
||||
mode: "baseline-comparison",
|
||||
decision: score.regressed ? "revise" : "pass",
|
||||
summary: score.summary,
|
||||
metrics: score.metrics,
|
||||
findings: score.findings,
|
||||
};
|
||||
},
|
||||
{ registrationId: "quality-regression", timeoutMs: 90_000 },
|
||||
);
|
||||
```
|
||||
|
||||
When evaluation input includes `correlationId`, OpenClaw forwards it to the
|
||||
evaluator event for both manual and apply-triggered evaluations. This value is
|
||||
caller-supplied correlation metadata, not authenticated identity or proof of
|
||||
authorization. An authorization plugin must mint or replace the value through
|
||||
a trusted entry point, bind it to the intended operation, and validate and
|
||||
consume it itself.
|
||||
|
||||
Stored outcomes identify the evaluator, plugin id, plugin package version,
|
||||
status, and returned result. Timeouts and thrown errors are recorded as
|
||||
attributed error outcomes; they do not fail the whole evaluation. Among
|
||||
evaluator outcomes, only a completed `decision: "block"` vetoes apply. Other
|
||||
Workshop validation and ownership checks still apply. Apply revalidates the
|
||||
evaluated target tree under the Workshop mutation lock, so any live skill asset
|
||||
drift requires reevaluation.
|
||||
The complete persisted evaluation envelope is capped at 512 KiB.
|
||||
|
||||
`skill_proposal_changed` fires after the matching proposal row and append-only
|
||||
lifecycle event commit. It carries the event id, sequence, exact proposal
|
||||
revision hash, optional correlation id, and evaluation outcomes.
|
||||
`skill_changed` fires after a live skill create, update, or removal commits and
|
||||
includes optional before/after artifacts with content and tree hashes, plus
|
||||
declared and source versions when available.
|
||||
|
||||
These hooks are primitives, not an optimization scheduler. A plugin or external
|
||||
controller can observe a durable proposal event, evaluate its exact revision hash,
|
||||
revise with that hash and a correlation id, then repeat. OpenClaw does not
|
||||
automatically revise proposals or run an unbounded evaluation loop.
|
||||
Event replay is byte-bounded and returns `nextSequence` when another page is
|
||||
available.
|
||||
|
||||
### Channel pairing requests
|
||||
|
||||
Use `channel_pairing_requested` when a plugin needs to notify an operator or
|
||||
|
|
|
|||
|
|
@ -97,8 +97,3 @@ The method is optional for existing SDK implementations; automatic one-shot
|
|||
recovery treats a missing method as uncertain cleanup. A native facade that owns
|
||||
no transport may resolve immediately when its enclosing runtime separately owns
|
||||
and verifies the process lifetime.
|
||||
|
||||
Harnesses that forward embedded attempt params should pass
|
||||
`skillWorkshopProposalOnly` through. Proposal-only skill-workshop runs are
|
||||
deliberately narrow single-tool runs, and the runtime keeps them on the raw
|
||||
tool surface instead of engaging code mode or a tool-search catalog.
|
||||
|
|
|
|||
|
|
@ -75,6 +75,25 @@ api.on("gateway_stop", async (event, ctx) => {
|
|||
});
|
||||
```
|
||||
|
||||
### Skill Workshop proposal hooks
|
||||
|
||||
The `skill_proposal_evaluate` and `skill_proposal_changed` hooks were removed
|
||||
together with Skill Workshop proposals. Workshop now applies each change
|
||||
immediately and keeps a restorable version, so there is no pending draft to
|
||||
evaluate and no proposal lifecycle to observe. The hook runner methods
|
||||
`runSkillProposalEvaluate` and `runSkillProposalChanged` were removed, and
|
||||
`openclaw/plugin-sdk/plugin-entry` no longer exports
|
||||
`PluginHookSkillProposalEvaluateEvent`, `PluginHookSkillProposalEvaluateResult`,
|
||||
`PluginHookSkillProposalEvaluationOutcome`, `PluginHookSkillProposalChangedEvent`,
|
||||
`PluginHookSkillProposalKind`, `PluginHookSkillEvaluationFinding`,
|
||||
`PluginHookSkillBundleFile`, or `PluginHookSkillBundleSnapshot`. The optional
|
||||
`proposal` field on `PluginHookSkillChangedEvent` was removed too.
|
||||
|
||||
To observe committed Workshop skill writes, register `skill_changed` and filter
|
||||
on `source: "workshop"`. There is no replacement for pre-apply evaluation.
|
||||
Registering a removed hook name logs an `unknown typed hook` warning and the
|
||||
handler never runs.
|
||||
|
||||
### Private testing barrel
|
||||
|
||||
`openclaw/plugin-sdk/testing` was repo-local and excluded from shipped package
|
||||
|
|
|
|||
|
|
@ -78,6 +78,34 @@ remove this delivery fence: older runtimes must refuse migrated state. Restoring
|
|||
a pre-upgrade backup loses later receipt facts and does not undo external sends;
|
||||
reconcile those effects before retrying an automation.
|
||||
|
||||
### Skill Workshop proposal retirement (state schema 20, same version)
|
||||
|
||||
Skill Workshop no longer stores proposals. The optional `skill_workshop_proposals`,
|
||||
`skill_workshop_proposal_events`, `skill_workshop_proposal_rollbacks`, and
|
||||
`skill_workshop_collection_reviews` tables leave the canonical schema without a
|
||||
version bump; they were lazy additive tables, so older same-version readers
|
||||
already tolerated their absence. Opening a database that still has them is
|
||||
unchanged: the tables are no longer validated, and a runtime open never drops
|
||||
them.
|
||||
|
||||
`openclaw doctor --fix`, including the Doctor run that `openclaw update` starts,
|
||||
first repairs the known malformed
|
||||
`idx_skill_workshop_collection_reviews_workspace_time` index during its schema
|
||||
prelude. Its final Workshop step then writes every `pending` or `quarantined`
|
||||
proposal draft, plus its support files, to
|
||||
`<agentDir>/workshop-skills/.archive/.retired-proposals/<proposalId>/SKILL.md`
|
||||
for the recorded owner agent, or the default agent when none was recorded.
|
||||
Pre-SQLite bundles under `<state-dir>/skill-workshop/proposals/<id>/` with a
|
||||
`proposal.json` are exported the same way. An existing export is never
|
||||
overwritten. After every export succeeds, Doctor drops the four tables and their
|
||||
indexes in one write transaction, then removes the legacy proposal files. Any
|
||||
failure keeps the tables and files and records a warning; rerunning Doctor
|
||||
resumes. Applied, rejected, and stale proposals, event history, rollback
|
||||
metadata, and collection-review outcomes are discarded.
|
||||
|
||||
Downgrading to a build that still reads proposals needs the pre-upgrade backup;
|
||||
the exported drafts remain plain files that can be recreated as skills.
|
||||
|
||||
### State schema 19
|
||||
|
||||
Schema 19 adds nullable `authorization_id TEXT` and
|
||||
|
|
@ -194,8 +222,8 @@ absent. Per-agent schemas and native companion tables do not change; native
|
|||
clients can continue validating and reading their existing owned tables at
|
||||
state schema 17 without performing migrations.
|
||||
|
||||
Startup and `openclaw doctor --fix` apply the schema-16 Skill Workshop migration
|
||||
before the prepared-worker migration when opening a schema-15 database. A
|
||||
Startup and `openclaw doctor --fix` apply the prepared-worker migration when
|
||||
opening a schema-15 or schema-16 database. A
|
||||
schema-16 database receives only the prepared-worker migration. The tuple
|
||||
constraint belongs to the last added column, so migration scans existing
|
||||
environments once and preserves the table instead of rebuilding it. Leases,
|
||||
|
|
@ -207,8 +235,7 @@ database changes from that attempt. Both published markers normally advance to
|
|||
17 in that transaction. When an older updater still owns trailing ledger reads,
|
||||
the existing [version-publication deferral](/reference/database-schemas/versioning#schema-bumps-and-older-updaters)
|
||||
keeps the published markers at their earlier version and records applied content
|
||||
17 separately. Reopening does not repeat that content migration. Workshop
|
||||
directory relocation runs afterward, outside the schema transaction.
|
||||
17 separately. Reopening does not repeat that content migration.
|
||||
|
||||
Before upgrading, stop older writers and create a verified, WAL-aware backup.
|
||||
Builds supporting state schema 16 or earlier refuse the published schema 17.
|
||||
|
|
@ -227,6 +254,10 @@ history and adds `owner_agent_id` plus its owner/time index. Proposal rows remai
|
|||
collection review had released becomes `stale` with a status reason, so the
|
||||
skill path it once created stays user-owned and Doctor never relocates it.
|
||||
|
||||
The same-version proposal retirement above removes this migration. A schema-15
|
||||
database keeps its legacy Workshop columns until Doctor exports its pending
|
||||
drafts and drops the proposal tables.
|
||||
|
||||
Skill Workshop ownership is now the physical
|
||||
`<state-dir>/agents/<agentId>/agent/workshop-skills` directory. Startup and `openclaw doctor --fix`
|
||||
drop the retired columns and index in the shared schema transaction. Both then
|
||||
|
|
|
|||
|
|
@ -2108,19 +2108,11 @@ Process identity caches retain their existing database-path and identity-key
|
|||
scope; warm cached values need no database operation. Schemas and update behavior
|
||||
are unchanged; no migration or operator action is required.
|
||||
|
||||
Skill Workshop proposal reads, publication, evaluation, rollback metadata, and
|
||||
status transitions execute in the existing shared-state worker. Record and event
|
||||
writes remain one synchronous transaction, including revision comparisons and
|
||||
pending-proposal limits. The host retains filesystem work and the collection and
|
||||
target leases through settlement; worker transactions verify every held lease
|
||||
before effects and commit. A failed reply is reconciled before discarding a
|
||||
staged generation or restoring live files.
|
||||
|
||||
Collection history reads and experience-review outcomes use the same worker.
|
||||
Doctor awaits legacy proposal imports before deleting their source sidecars.
|
||||
Transaction-bound relocation kernels and read-only migration readers retain their
|
||||
supplied connections. Proposal generations, schemas, limits, retention, and
|
||||
rollback ordering are unchanged.
|
||||
Skill Workshop change-feed reads and writes and skill usage reads and writes
|
||||
execute in the existing shared-state worker. The retired proposal tables have no
|
||||
runtime worker operations; Doctor reads them once in its one-shot export before
|
||||
dropping them. See the
|
||||
[proposal retirement](/reference/database-schemas/state-schema-history#skill-workshop-proposal-retirement-state-schema-20-same-version).
|
||||
|
||||
Cron receipt execution identity binding runs in the shared-state worker. Its
|
||||
transaction rereads the exact live receipt owner and rechecks the caller's current
|
||||
|
|
|
|||
|
|
@ -23,6 +23,11 @@ cannot parse its catalog. A known legacy index defect must not replace that
|
|||
refusal with advice to repair the database using the older build. Doctor checks
|
||||
compatibility before stopping the Gateway service.
|
||||
|
||||
Target-release checks distinguish the target's schema support from the running
|
||||
inspector's capabilities. When shared state is too new for the target but still
|
||||
readable by the inspector, preflight continues checking agent stores and reports
|
||||
all incompatibilities without modifying the source files.
|
||||
|
||||
Changes may stay at the same schema version only when downgraded readers remain safe. New tables qualify because older builds ignore them. An explicitly compatible column on an existing table qualifies only when its declaration is exactly one bare nullable SQLite `STRICT` datatype: `ANY`, `BLOB`, `INT`, `INTEGER`, `REAL`, or `TEXT`. The declaration cannot have a default, `NOT NULL`, a primary or unique key, a check, a reference, a collation, a generated expression, or another suffix. Constrained existing-table additions require a schema-version bump or a companion table instead.
|
||||
|
||||
Linux Node worker cleanup uses the additive `node_worker_launch_process_scopes`
|
||||
|
|
@ -456,8 +461,8 @@ The runner records the applied content version in the existing
|
|||
deferred, new code uses that content version, and both `PRAGMA user_version` and
|
||||
`schema_meta.schema_version` retain the previous published version. Content and
|
||||
its marker commit together. Reopening skips migration steps already covered by
|
||||
the marker, including the schema-16 Skill Workshop rebuild; it does not infer
|
||||
completion from table shape or repeat the rebuild. This requires no new table,
|
||||
the marker; it does not infer completion from table shape or repeat a
|
||||
completed rebuild. This requires no new table,
|
||||
configuration option, or environment override.
|
||||
|
||||
Current content is ready for readers even while its version is unpublished.
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ title: "Database worker migration inventory"
|
|||
|
||||
<!-- Generated by scripts/database-worker-inventory.mjs. Edit its classification evidence, then regenerate. -->
|
||||
|
||||
This snapshot contains **633 non-test files and 2531 call expressions** for the five primitives below. The campaign previously reported 404 files; that is a historical estimate, not a fixed target or a count of call expressions. This inventory follows current source and excludes import-only matches, comments, tests, fixtures, and test support. Its scan scope and exclusions are explicit below.
|
||||
This snapshot contains **624 non-test files and 2496 call expressions** for the five primitives below. The campaign previously reported 404 files; that is a historical estimate, not a fixed target or a count of call expressions. This inventory follows current source and excludes import-only matches, comments, tests, fixtures, and test support. Its scan scope and exclusions are explicit below.
|
||||
|
||||
Regenerate with `pnpm db:worker-inventory:gen`; verify with `pnpm db:worker-inventory:check`. `node scripts/database-worker-inventory.mjs --json` emits every call's primitive, line, column, lexical operation path, optional variable-initializer binding, file owner, tier, and classification evidence. The script uses the repository's TypeScript parser and `rg`; it does not load application code or open a database.
|
||||
|
||||
|
|
@ -32,10 +32,10 @@ The scan covers JavaScript/TypeScript files under `src/`, `extensions/`, `packag
|
|||
|
||||
| Tier | Files | Call expressions |
|
||||
| ---- | ----: | ---------------: |
|
||||
| T1 | 261 | 797 |
|
||||
| T2 | 90 | 372 |
|
||||
| T3 | 75 | 269 |
|
||||
| W | 281 | 1093 |
|
||||
| T1 | 263 | 802 |
|
||||
| T2 | 88 | 370 |
|
||||
| T3 | 71 | 255 |
|
||||
| W | 274 | 1069 |
|
||||
|
||||
## Profile priority and current cutover status
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/config/sessions** · `src/config/sessions/session-transcript-search.ts` | 1/1/0/0/2 | 86 | Async durable search uses worker; process-held incognito remains native |
|
||||
| **src/config/sessions** · `src/config/sessions/session-sharing-store.kernel.ts` | 1/1/0/0/0 | 38 | Member-row kernel shared by session readers |
|
||||
| **src/config/sessions** · `src/config/sessions/session-sharing-store.ts` | 0/0/0/0/1 | 28 | Session-list member reads and membership mutations |
|
||||
| **src/acp** · `src/acp/event-ledger.ts` | 1/1/1/0/0 | 265 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/acp** · `src/acp/event-ledger.ts` | 1/1/1/0/0 | 264 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/acp** · `src/acp/runtime/session-meta-keys.ts` | 2/1/0/0/0 | 51 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/acp** · `src/acp/runtime/session-meta-write.kernel.ts` | 1/0/0/0/0 | 79 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/acp** · `src/acp/runtime/session-meta-write.native.ts` | 0/0/2/0/0 | 96 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
|
|
@ -97,7 +97,7 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/agents** · `src/agents/agent-lifecycle-registry.ts` | 0/0/3/0/0 | 250 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/agents** · `src/agents/plugin-model-catalog.kernel.ts` | 1/0/0/0/1 | 29 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/agents** · `src/agents/workspace-state-store.kernel.ts` | 8/0/0/0/0 | 114 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/agents** · `src/agents/workspace-state-store.ts` | 1/1/1/0/0 | 316 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/agents** · `src/agents/workspace-state-store.ts` | 1/1/1/0/0 | 290 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/agents/auth-profiles** · `src/agents/auth-profiles/candidate-stores.ts` | 0/0/0/1/0 | 147 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/agents/auth-profiles** · `src/agents/auth-profiles/shared-store-bootstrap.ts` | 0/3/0/0/0 | 108 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/agents/auth-profiles** · `src/agents/auth-profiles/sqlite-json.ts` | 5/2/0/0/0 | 40 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
|
|
@ -171,11 +171,11 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-pending-inputs-repair.ts` | 5/0/0/0/0 | 35 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-pending-inputs.ts` | 6/2/0/0/0 | 365 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-projection-read.ts` | 1/0/0/0/0 | 120 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-projection.ts` | 0/0/0/2/0 | 372 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-projection.ts` | 0/0/0/2/0 | 373 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-provenance.ts` | 0/2/0/0/0 | 39 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-raw-delta-read.ts` | 2/0/0/0/0 | 194 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-read.ts` | 3/4/0/0/4 | 152 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-reclamation.ts` | 4/0/0/3/0 | 97 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-reclamation.ts` | 4/0/0/3/0 | 94 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-references.ts` | 2/0/0/0/0 | 104 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-replacement-projection.ts` | 0/0/0/1/0 | 300 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/config/sessions** · `src/config/sessions/session-accessor.sqlite-replacement-read.ts` | 1/0/0/0/0 | 31 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
|
|
@ -310,6 +310,8 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/sessions** · `src/sessions/session-upstream-links.kernel.ts` | 3/0/0/0/0 | 94 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/sessions** · `src/sessions/session-upstream-links.ts` | 1/0/2/0/0 | 48 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/skills/library** · `src/skills/library/selection-read.kernel.ts` | 1/0/0/0/0 | 35 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/skills/workshop** · `src/skills/workshop/changes.kernel.ts` | 3/0/0/0/0 | 58 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/skills/workshop** · `src/skills/workshop/skill-usage.kernel.ts` | 2/0/0/0/0 | 26 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/agent-deletion-journal-history.ts` | 0/2/0/0/0 | 20 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/agent-deletion-journal-recovery.ts` | 1/0/0/0/0 | 118 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/agent-deletion-journal.read.ts` | 1/0/0/0/0 | 121 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
|
|
@ -324,9 +326,9 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/state** · `src/state/openclaw-agent-canonical-validation-receipt.ts` | 0/1/0/0/0 | 53 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/openclaw-agent-db-metadata-write.ts` | 1/0/0/0/0 | 20 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/openclaw-agent-db-registry.read.ts` | 1/1/0/0/0 | 30 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/openclaw-agent-db-registry.ts` | 3/0/2/0/0 | 52 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/openclaw-agent-db-registry.ts` | 3/0/2/0/0 | 54 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/openclaw-quarantine-store.ts` | 5/2/0/0/0 | 100 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/openclaw-state-db-maintenance.ts` | 1/0/0/0/0 | 599 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/openclaw-state-db-maintenance.ts` | 1/0/0/0/0 | 457 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/openclaw-state-db.ts` | 0/0/1/0/0 | 397 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/user-github-connections.ts` | 1/0/3/0/0 | 188 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
| **src/state** · `src/state/user-model-accounts.ts` | 2/1/3/0/0 | 98 | Runtime/mixed candidate; main-thread reachability needs tracing |
|
||||
|
|
@ -348,12 +350,11 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **extensions/codex** · `extensions/codex/src/migration/native-task-assignments.ts` | 6/0/0/0/0 | 132 | Schema/startup/migration candidate; verify no runtime caller |
|
||||
| **src/acp** · `src/acp/runtime/session-meta-migration-embedded.ts` | 0/1/1/0/1 | 102 | Schema/startup/migration candidate; verify no runtime caller |
|
||||
| **src/acp** · `src/acp/runtime/session-meta-migration-keys.ts` | 1/0/0/0/0 | 112 | Schema/startup/migration candidate; verify no runtime caller |
|
||||
| **src/acp** · `src/acp/runtime/session-meta-startup.ts` | 0/0/0/0/1 | 152 | Schema/startup/migration candidate; verify no runtime caller |
|
||||
| **src/acp** · `src/acp/runtime/session-meta-startup.ts` | 0/0/0/0/1 | 137 | Schema/startup/migration candidate; verify no runtime caller |
|
||||
| **src/agents** · `src/agents/plugin-model-catalog.kernel.ts` | 4/0/0/0/0 | 68 | Native replacement is Doctor import only: src/commands/doctor-plugin-model-catalog.ts:103 -> plugin-model-catalog.ts:495,517 -> :162. The other caller is plugin-model-catalog.worker.ts:94; readPluginModelCatalogEntries remains T1. |
|
||||
| **src/agents** · `src/agents/plugin-model-catalog.ts` | 3/0/0/3/0 | 111 | Only doctor-plugin-model-catalog.ts:103,126 reaches repair/import/receipt retirement; runtime replacement dispatches at plugin-model-catalog.ts:584; ModelRegistry synchronous kernel reads stay T1 |
|
||||
| **src/agents** · `src/agents/workspace-alias-rebind.ts` | 5/4/1/0/0 | 86 | Only src/commands/doctor-workspace-alias.ts:67,107,131 invokes detect/rebind; readWorkspaceMoveState is private at :166,282. Doctor health/lint reaches detection via src/flows/doctor-health-contributions-final.ts:269 and doctor-health-contribution-runners.workspace.ts:77; repair is installed by src/commands/doctor-config-flow.ts:83,93. |
|
||||
| **src/agents** · `src/agents/workspace-state-store.kernel.ts` | 2/2/0/0/0 | 172 | Worker runtime/read dispatch plus Doctor workspace-alias-rebind.ts:83,324, migration workspace-setup-store.ts:528 and relocation retirement workspace-state-store.ts:256; native identity/deletion stay T1 |
|
||||
| **src/agents** · `src/agents/workspace-state-store.ts` | 1/0/0/0/0 | 264 | Only commands/doctor-skill-workshop-workspaces.ts:266 retires migration attestations |
|
||||
| **src/agents/worktrees** · `src/agents/worktrees/registry.ts` | 2/0/1/0/0 | 73 | Only migration discovery src/infra/state-migrations.doctor-discovery.ts:84 and Doctor repair src/config/sessions/worktree-workspace-migration.ts:116 call the list; the latter requires doctor-fix mode. The private runRegistryMigration transaction is called only by discardLegacyRegistryWorktrees and rewriteRegistryWorktreePathsForMigration; their sole production caller is the managed-worktrees step at src/infra/state-migrations.doctor.ts:1256,1258 (Doctor/startup). It acquires the existing schema-maintenance owner before opening the transaction database. |
|
||||
| **src/channels** · `src/channels/message/ingress-queue.migration.ts` | 1/1/2/0/0 | 116 | Schema/startup/migration candidate; verify no runtime caller |
|
||||
| **src/config/sessions** · `src/config/sessions/legacy-main-session-migration-claims.ts` | 1/0/0/0/2 | 128 | Schema/startup/migration candidate; verify no runtime caller |
|
||||
|
|
@ -419,7 +420,6 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/plugin-state** · `src/plugin-state/plugin-state-store.kernel.ts` | 1/0/0/0/0 | 260 | Native raw-row inspection only: src/infra/state-migrations.plugin-doctor-context.ts:416 -> plugin-state-store.sqlite.ts:394. Other callers are plugin-state.worker.ts:104 -> plugin-state-store.reads.ts:111 and plugin-state.worker.ts:173 -> plugin-state-store.journal.ts:122; public key-range facade is asynchronous at plugin-state-store.ts:580. |
|
||||
| **src/plugin-state** · `src/plugin-state/plugin-state-store.retention.ts` | 0/1/0/0/0 | 94 | Only migration/Doctor batch creates retention: src/infra/state-migrations.plugin-doctor-context.ts:409 -> plugin-state-store.ts:597 -> plugin-state-store.sqlite.ts:133,139. The conditional retention refresh at plugin-state-store.retention.ts:228 shares that batch; native ordinary registration (:107) and worker registration (plugin-state.worker.ts:190) omit retention. |
|
||||
| **src/plugin-state** · `src/plugin-state/plugin-state-store.sqlite.ts` | 1/0/0/0/0 | 356 | Only src/infra/state-migrations.plugin-doctor-context.ts:466 calls exact-row deletion, after repair-authority validation at :464. Context creation is limited to state-migrations.plugin-doctor.ts:200,325; no runtime keyed-store method exposes this repair operation. |
|
||||
| **src/skills/workshop** · `src/skills/workshop/store-sqlite-schema.ts` | 0/0/1/0/0 | 70 | Schema/startup/migration candidate; verify no runtime caller |
|
||||
| **src/state** · `src/state/agent-deletion-journal.ts` | 0/0/1/0/0 | 153 | Pre-open registration/schema/lease admission at openclaw-agent-db-registry.ts:79, schema.ts:519 and lease.ts:161,495,699; runtime journal mutation/fence reads stay T1 |
|
||||
| **src/state** · `src/state/config-machine-state-write.ts` | 1/1/1/0/0 | 155 | Only migrations src/infra/state-migrations.config-machine-state.ts:71 and state-migrations.update-check.ts:66; Doctor roots doctor-config-preflight.cron.ts:27 and state-migrations.doctor.ts:910,1394. |
|
||||
| **src/state** · `src/state/openclaw-agent-db-lease.ts` | 9/3/7/0/0 | 173 | Boot or lock/lease primitive; exception is operation-scoped |
|
||||
|
|
@ -467,12 +467,9 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/commands** · `src/commands/doctor-session-sqlite-verification.ts` | 0/1/0/0/1 | 139 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-session-transcript-headers.ts` | 0/2/0/1/0 | 122 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-session-transcript-labels.ts` | 0/0/0/1/0 | 229 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-skill-workshop-read.kernel.ts` | 1/0/0/0/0 | 23 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-skill-workshop-sidecars.ts` | 0/1/0/0/0 | 320 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-skill-workshop-sqlite.ts` | 2/1/1/0/0 | 147 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-skill-workshop-workspaces.ts` | 2/3/2/0/0 | 123 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-skill-workshop-retire-proposals.ts` | 0/0/1/0/0 | 570 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-telegram-general-topic-conversations.ts` | 12/0/0/1/1 | 64 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-update-rehearsal-inventory.ts` | 0/1/0/0/0 | 330 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-update-rehearsal-inventory.ts` | 0/1/0/0/0 | 328 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-update-run.ts` | 1/1/0/0/0 | 65 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor-update-schema-guard.ts` | 1/1/0/0/0 | 70 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
| **src/commands** · `src/commands/doctor/auth-alias-receipt.ts` | 0/0/2/0/0 | 151 | CLI/Doctor/developer one-shot; reclassify if called by Gateway |
|
||||
|
|
@ -504,9 +501,8 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/infra** · `src/infra/update-managed-service-handoff-database-recovery.ts` | 2/0/0/0/0 | 137 | Explicit update recover CLI -> recoverImmutableUpdate -> withImmutableUpdateOwner({ recover: true }) only; ordinary lease and Gateway readers never call this cold-journal admission |
|
||||
| **src/infra** · `src/infra/update-run-ledger.ts` | 1/3/0/0/0 | 137 | active initializer requires stale-run opt-in only from src/cli/update-cli/update-command-run.ts:304,310,313; guard src/infra/update-run-ledger.ts:127 excludes Gateway/campaign callers. Repairs: update-repair-command.ts:141; interruption: update-command-mutable-signals.ts:158. |
|
||||
| **src/secrets** · `src/secrets/store/secret-store.ts` | 1/0/1/0/0 | 364 | Only cli/secrets-store-cli.ts:251 mutates allowed hosts; runtime reads and other writes remain T1 |
|
||||
| **src/skills/workshop** · `src/skills/workshop/store-sqlite-record.ts` | 1/1/0/0/0 | 44 | Doctor native read/update src/commands/doctor-skill-workshop-sqlite.ts:333,349; other callers use src/skills/workshop/store.worker.ts:68,129,149,167,182,188. |
|
||||
| **src/state** · `src/state/openclaw-agent-db-path-repair.ts` | 3/0/0/0/0 | 31 | src/commands/doctor-agent-database-paths.ts:24 under Doctor repair; registered health runner src/flows/doctor-health-contribution-runners.state.ts:122. |
|
||||
| **src/state** · `src/state/openclaw-agent-db-registry.ts` | 1/0/1/0/0 | 179 | Plural unregister: CLI claw removal src/claws/lifecycle-delete-support.ts:519 → cli/claws-cli.runtime.ts:562; CLI agent deletion src/commands/agents.commands.delete.ts:450 → cli/program/register.agent.ts:183. |
|
||||
| **src/state** · `src/state/openclaw-agent-db-registry.ts` | 1/0/1/0/0 | 190 | Plural unregister: CLI claw removal src/claws/lifecycle-delete-support.ts:519 → cli/claws-cli.runtime.ts:562; CLI agent deletion src/commands/agents.commands.delete.ts:450 → cli/program/register.agent.ts:183. |
|
||||
| **src/state** · `src/state/openclaw-state-ownership-operations.ts` | 1/0/1/0/0 | 76 | Only src/cli/program/register.database.ts:56 invokes claim; its private claimOwnershipRow calls remain within this CLI operation. |
|
||||
| **src/state** · `src/state/openclaw-state-snapshot-sanitizer.ts` | 2/0/0/0/0 | 49 | CLI SQLite/git backups: src/snapshot/local-repository.ts:148, git-backup.ts:310 → openclaw-snapshot-copy.ts:68; archive/Doctor backup via src/infra/backup-create.ts:320,332,352 → backup-sqlite-snapshot.ts:287; developer reliability scripts also use the provider. |
|
||||
| **src/state** · `src/state/user-channel-identities.ts` | 6/3/2/0/0 | 120 | Gateway uses user-channel-identities.worker.ts and state-read; native resolver serves channel-operator-authority.ts CLI/updater capture via update-requester-authority.ts |
|
||||
|
|
@ -621,7 +617,7 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/config/sessions** · `src/config/sessions/session-pending-input-history-read.worker.ts` | 0/0/0/0/1 | 12 | Worker implementation; keep SQL in this owner |
|
||||
| **src/config/sessions** · `src/config/sessions/session-pending-input-withdrawal.worker.ts` | 2/1/0/1/0 | 33 | Worker implementation; keep SQL in this owner |
|
||||
| **src/config/sessions** · `src/config/sessions/session-transcript-hydration-read.worker.ts` | 0/0/0/0/1 | 17 | Worker implementation; keep SQL in this owner |
|
||||
| **src/config/sessions** · `src/config/sessions/session-transcript-index-status.worker.ts` | 7/5/0/0/0 | 157 | Worker implementation; keep SQL in this owner |
|
||||
| **src/config/sessions** · `src/config/sessions/session-transcript-index-status.worker.ts` | 7/5/0/0/0 | 116 | Worker implementation; keep SQL in this owner |
|
||||
| **src/config/sessions** · `src/config/sessions/session-transcript-projection-publication.worker.ts` | 0/1/0/0/0 | 98 | Worker implementation; keep SQL in this owner |
|
||||
| **src/config/sessions** · `src/config/sessions/session-transcript.worker.ts` | 0/0/0/0/13 | 105 | Worker implementation; keep SQL in this owner |
|
||||
| **src/cron** · `src/cron/store/dispatch.worker.ts` | 0/0/4/0/0 | 138 | Worker implementation; keep SQL in this owner |
|
||||
|
|
@ -666,7 +662,7 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/gateway/worker-environments** · `src/gateway/worker-environments/placement-row-codec.ts` | 5/0/0/0/0 | 157 | Snapshots use openclaw-state-read.worker.ts; placement-lifecycle.worker.ts serves point lookups on the existing placement actor. Native reconciliation guards remain T1.; placement-dispatch-store.worker.ts:38 and placement-turn-claims.ts:112 claim path; claims only invoked at placement-turn-claims.worker.ts:160,175,345. Native placement-store.ts:75,76 selects clear/wait/validate methods that do not claim.; Transitions run only in placement-transitions.worker.ts and prepared binding in placement-lifecycle.worker.ts. |
|
||||
| **src/gateway/worker-environments** · `src/gateway/worker-environments/placement-session-tool-operations.kernel.ts` | 12/0/0/0/0 | 36 | Factory runs in placement-session-tool-operations.worker.ts; claim, reconcile and terminal-failure cleanup now only run through placement-turn-claims.worker.ts |
|
||||
| **src/gateway/worker-environments** · `src/gateway/worker-environments/placement-session-tool-operations.worker.ts` | 0/0/1/0/0 | 24 | Worker implementation; keep SQL in this owner |
|
||||
| **src/gateway/worker-environments** · `src/gateway/worker-environments/placement-transitions.worker.ts` | 3/0/0/0/0 | 147 | Worker implementation; keep SQL in this owner |
|
||||
| **src/gateway/worker-environments** · `src/gateway/worker-environments/placement-transitions.worker.ts` | 3/0/0/0/0 | 168 | Worker implementation; keep SQL in this owner |
|
||||
| **src/gateway/worker-environments** · `src/gateway/worker-environments/placement-turn-claims.ts` | 7/0/0/0/0 | 77 | placement-store.ts:102 selects only native restart/wait/validation; claim/release/cancel mutations run in placement-turn-claims.worker.ts:102,117,191,200,287,355,360; placement-store.ts:132 supplies worker mutations; placement-turn-claims.worker.ts:299,355,360 executes release/manifest methods |
|
||||
| **src/gateway/worker-environments** · `src/gateway/worker-environments/placement-turn-claims.worker.ts` | 1/0/1/0/0 | 66 | Worker implementation; keep SQL in this owner |
|
||||
| **src/gateway/worker-environments** · `src/gateway/worker-environments/placement-workspace-journal.ts` | 8/0/0/0/0 | 62 | Read dispatch at state-read.worker.ts:653 and journal mutation factory at placement-workspace-journal.worker.ts:30; Acceptance at placement-turn-claims.worker.ts:190, journal abort via placement-workspace-journal.worker.ts:30 and manifest drain via placement-transitions.worker.ts:82; native move drain supplies no manifest and skips cleanup |
|
||||
|
|
@ -758,14 +754,7 @@ Counts use `Q/F/S/A/R` in that order. Source locations are available in `--json`
|
|||
| **src/skills/lifecycle** · `src/skills/lifecycle/upload-store-commit.ts` | 2/1/1/0/0 | 100 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/lifecycle** · `src/skills/lifecycle/upload-store.kernel.ts` | 7/4/5/0/0 | 67 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/lifecycle** · `src/skills/lifecycle/upload-store.sqlite.ts` | 4/5/3/0/0 | 64 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/workshop** · `src/skills/workshop/collection-review.kernel.ts` | 2/0/0/0/0 | 130 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/workshop** · `src/skills/workshop/curator.kernel.ts` | 3/0/0/0/0 | 23 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/workshop** · `src/skills/workshop/store-proposal.kernel.ts` | 3/4/0/0/0 | 52 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/workshop** · `src/skills/workshop/store-sqlite-event.ts` | 2/2/0/0/0 | 49 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/workshop** · `src/skills/workshop/store-sqlite-record.ts` | 1/0/0/0/0 | 86 | Only src/skills/workshop/store-proposal.kernel.ts:73,168 inserts; sole executors store.worker.ts:139,157. |
|
||||
| **src/skills/workshop** · `src/skills/workshop/store-sqlite-rollback.ts` | 3/3/0/0/0 | 39 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/workshop** · `src/skills/workshop/store-sqlite-transition.ts` | 1/0/0/0/0 | 45 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/workshop** · `src/skills/workshop/store.worker.ts` | 0/0/2/0/0 | 64 | Worker implementation; keep SQL in this owner |
|
||||
| **src/skills/workshop** · `src/skills/workshop/changes.worker.ts` | 0/0/2/0/0 | 19 | Worker implementation; keep SQL in this owner |
|
||||
| **src/state** · `src/state/agent-deletion-journal-authority.worker.ts` | 0/1/0/0/0 | 12 | Worker implementation; keep SQL in this owner |
|
||||
| **src/state** · `src/state/agent-deletion-journal.read.ts` | 0/1/0/0/0 | 72 | src/state/openclaw-state-read-registry.ts:65 under read worker :679; other executor src/state/openclaw-agent-execution.worker.ts:190. |
|
||||
| **src/state** · `src/state/agent-deletion-journal.snapshot.worker.ts` | 1/0/0/0/0 | 25 | Worker implementation; keep SQL in this owner |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ summary: "Build, test, and publish custom SKILL.md workspace skills or personal
|
|||
read_when:
|
||||
- You are creating a new custom skill
|
||||
- You need a quick starter workflow for SKILL.md-based skills
|
||||
- You want to use Skill Workshop to propose a skill for agent review
|
||||
- You want the agent to save or update a skill for you with Skill Workshop
|
||||
---
|
||||
|
||||
Skills teach the agent how and when to use tools. Each skill is a directory
|
||||
|
|
@ -98,9 +98,8 @@ You do not need host shell access or permission to edit shared Gateway settings.
|
|||
|
||||
You can also ask the agent to create or improve a personal skill. Its
|
||||
`skill_workshop` tool uses the Gateway's authorized library service; it does not
|
||||
write managed revision directories directly. The result distinguishes a
|
||||
published skill from a pending proposal and explains when the session can use
|
||||
it. Ask explicitly to use the new revision in the current session or share it
|
||||
write managed revision directories directly. The result names the published
|
||||
revision and explains when the session can use it. Ask explicitly to use the new revision in the current session or share it
|
||||
with the team.
|
||||
|
||||
The single-admin default remains the workspace workflow above. Extra channel
|
||||
|
|
@ -189,46 +188,21 @@ metadata: { "openclaw": { "requires": { "bins": ["gemini"] }, "primaryEnv": "GEM
|
|||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Propose via Skill Workshop
|
||||
## Learned skills (Skill Workshop)
|
||||
|
||||
For agent-drafted skills or when you want operator review before a skill goes
|
||||
live, use [Skill Workshop](/tools/skill-workshop) proposals instead of writing
|
||||
`SKILL.md` directly.
|
||||
Agents also write their own skills through [Skill Workshop](/tools/skill-workshop).
|
||||
Those learned skills apply immediately and keep their previous versions, so any
|
||||
change can be undone. Inspect and manage them from the CLI:
|
||||
|
||||
```bash
|
||||
# Propose a brand-new skill
|
||||
openclaw skills workshop propose-create \
|
||||
--name "hello-world" \
|
||||
--description "A simple skill that prints a greeting." \
|
||||
--proposal ./PROPOSAL.md
|
||||
|
||||
# Propose an update to an existing skill
|
||||
openclaw skills workshop propose-update hello-world \
|
||||
--proposal ./PROPOSAL.md \
|
||||
--description "Updated greeting skill"
|
||||
openclaw skills workshop list
|
||||
openclaw skills workshop changes
|
||||
openclaw skills workshop show hello-world
|
||||
openclaw skills workshop archive hello-world --reason "Replaced by greeting"
|
||||
openclaw skills workshop restore hello-world
|
||||
```
|
||||
|
||||
Use `--proposal-dir` when the proposal includes support files:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop propose-create \
|
||||
--name "hello-world" \
|
||||
--description "A simple skill that prints a greeting." \
|
||||
--proposal-dir ./hello-world-proposal/
|
||||
```
|
||||
|
||||
The directory must contain `PROPOSAL.md` at its root. Support files go under
|
||||
`assets/`, `examples/`, `references/`, `scripts/`, or `templates/`.
|
||||
|
||||
After review:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop inspect <proposal-id>
|
||||
openclaw skills workshop evaluate <proposal-id>
|
||||
openclaw skills workshop apply <proposal-id>
|
||||
```
|
||||
|
||||
See [Skill Workshop](/tools/skill-workshop) for the full proposal lifecycle.
|
||||
See [`openclaw skills workshop`](/cli/skills#skill-workshop) for every option.
|
||||
|
||||
## Publishing to ClawHub
|
||||
|
||||
|
|
@ -280,7 +254,7 @@ personal owner or an organization where you have publisher access.
|
|||
Loading order, gating, allowlists, and SKILL.md format.
|
||||
</Card>
|
||||
<Card title="Skill Workshop" href="/tools/skill-workshop" icon="flask">
|
||||
Proposal queue for agent-drafted skills.
|
||||
Skills your agent saves and updates on its own, with undo.
|
||||
</Card>
|
||||
<Card title="Skills config" href="/tools/skills-config" icon="gear">
|
||||
Full `skills.*` config schema.
|
||||
|
|
|
|||
|
|
@ -1,350 +1,169 @@
|
|||
---
|
||||
summary: "Turn corrections and successful work into reusable skills through Skill Workshop"
|
||||
summary: "How OpenClaw reviews finished work in the background and saves lessons as learned skills"
|
||||
read_when:
|
||||
- You want OpenClaw to learn reusable procedures from completed conversations
|
||||
- You are choosing between off, propose, and auto self-learning modes
|
||||
- You need to understand self-learning safety, cost, privacy, or troubleshooting
|
||||
- You want OpenClaw to learn reusable procedures from its conversations
|
||||
- You are deciding whether to leave self-learning on or turn it off
|
||||
- You need to know what a background review sees, costs, or changes
|
||||
- Nothing is being learned and you want to know why
|
||||
title: "Self-learning"
|
||||
sidebarTitle: "Self-learning"
|
||||
---
|
||||
|
||||
Self-learning turns corrections and successful work into reusable skills. Skills
|
||||
are the durable unit: they hold procedures that future sessions can discover and
|
||||
follow. [Skill Workshop](/tools/skill-workshop) owns the agent's learned skills.
|
||||
Self-learning turns corrections and hard-won procedures into
|
||||
[learned skills](/tools/skill-workshop) that future sessions load. It is on by
|
||||
default (`skills.workshop.autonomous.mode: "auto"`).
|
||||
|
||||
The default mode is `auto`. Background learning uses normal agent file tools to
|
||||
maintain the Workshop directory, like weekly collection review. Choose `propose`
|
||||
to stage drafts for review instead, or `off` to disable autonomous learning.
|
||||
Learning happens in three places:
|
||||
|
||||
## Immediate repair
|
||||
- **The foreground agent** patches a learned skill that misled it and saves a
|
||||
procedure after hard multi-step work, in the same turn.
|
||||
- **A background review** looks back over a conversation after enough work and
|
||||
saves what the foreground agent did not. This page covers it.
|
||||
- **Unused-skill cleanup** archives learned skills unused for 30 days. See
|
||||
[Unused-skill cleanup](/tools/skill-workshop#unused-skill-cleanup).
|
||||
|
||||
When the foreground agent discovers that a skill it used is wrong or incomplete,
|
||||
it reads the current live skill and drafts a targeted patch through Skill
|
||||
Workshop in the same turn. If the complete skill does not fit the selected
|
||||
model's read budget, `prepare_patch` can authorize one non-empty unique exact
|
||||
span and return bounded surrounding context. The next `patch` must quote that
|
||||
same span, and the authorization expires after one attempt or any target change.
|
||||
A second `prepare_patch` for that skill is rejected until the active authorization
|
||||
is consumed or invalidated. A runtime usage receipt prevents foreground repair of
|
||||
skills that the run did not use. Autonomous mode controls the outcome: `off`
|
||||
disables the repair, `propose` leaves it pending for explicit review and apply,
|
||||
and `auto` scans and applies it immediately. The repair still goes through
|
||||
proposal storage, hash binding, the security scanner, and rollback capture.
|
||||
Every change applies immediately, is announced in the conversation, and can be
|
||||
undone. See [Undo](/tools/skill-workshop#undo).
|
||||
|
||||
Immediate repair changes the live skill for new sessions. It does not rewrite the
|
||||
skill snapshot already loaded into the running session. The delayed experience
|
||||
review remains a fallback for durable learning that the foreground agent did not
|
||||
repair itself.
|
||||
## When a review runs
|
||||
|
||||
## Experience review
|
||||
OpenClaw counts model iterations per session, across turns. When a session
|
||||
reaches 10 since its last review, a review is queued and the count starts over.
|
||||
The count also resets when the foreground turn itself changed a learned skill,
|
||||
so work the agent already saved is not reviewed again.
|
||||
|
||||
Every learning decision comes from a model reviewing real evidence, not a
|
||||
template or pattern-matching path. The conversation and skill files are evidence,
|
||||
not permission to resume tasks or execute the procedures under review.
|
||||
A turn that read or viewed a learned skill also queues a review, whatever the
|
||||
count, so a skill that just misled or helped the agent gets a fresh look. The
|
||||
same eligibility rules apply, and a turn that changed a learned skill itself
|
||||
still skips the review.
|
||||
|
||||
After substantial work, OpenClaw can run one detached background review to find
|
||||
a reusable recovery technique or a stable procedure that would remove at least
|
||||
two future model or tool round trips. Deep turns the user interrupted qualify
|
||||
too: the wrong path and its correction are exactly the evidence worth keeping.
|
||||
The reviewer is told when a turn was interrupted and captures only procedures
|
||||
that visibly worked before the stop. Turns that ended in a provider or prompt
|
||||
error never schedule a review. That failure is transient environment noise, and
|
||||
a review on the same model would likely hit it again.
|
||||
A queued review starts after 30 seconds with no agent or reply run active; later
|
||||
activity in the same session restarts that wait. Reviews run one at a time.
|
||||
The foreground reply never waits for a review.
|
||||
|
||||
Experience review starts only when all of these conditions hold:
|
||||
These turns do not count:
|
||||
|
||||
- the foreground turn completed or was interrupted, but did not end in a
|
||||
provider or prompt error.
|
||||
- the current turn used at least 10 model iterations.
|
||||
- the run was an eligible foreground conversation, not Incognito, cron,
|
||||
heartbeat, memory, overflow, hook, subagent, or review work.
|
||||
- the runtime reported the resolved provider, model, and actual availability of
|
||||
`skill_workshop`.
|
||||
- the system has been quiet for 30 seconds.
|
||||
- no agent or reply run is still active.
|
||||
- turns that ended in a provider or prompt error. Turns you interrupted still
|
||||
count.
|
||||
- cron, heartbeat, hook, subagent, memory, and Incognito sessions, and
|
||||
background Workshop runs themselves.
|
||||
- turns on a runtime that does not report its resolved model and whether
|
||||
`skill_workshop` was available. The embedded runner and the Codex app-server
|
||||
harness report both.
|
||||
- turns where tool policy hides `skill_workshop`.
|
||||
|
||||
A later foreground completion in the same session restarts the quiet period.
|
||||
It does not replace the saved evidence unless that turn also qualifies for review.
|
||||
Pending reviews belong to an agent and session together, so agents using `global`
|
||||
retain separate candidates. Experience reviews use one Workshop slot within the
|
||||
[shared background work budget](/concepts/queue#background-work).
|
||||
The foreground answer never waits for the model's review.
|
||||
Compacted sessions are eligible. Counts are kept in memory, so a Gateway restart
|
||||
starts them over.
|
||||
|
||||
OpenClaw records where the completed turn ends, then reads its full model context
|
||||
asynchronously after the quiet period. The reviewer connects earlier requirements
|
||||
and corrections with observed results across that retained conversation, even
|
||||
when the latest turn is routine. Later messages are excluded. If the saved
|
||||
turn was rewritten or removed, the review records a failure instead of using
|
||||
different evidence. The review runs under a private detached session identity.
|
||||
Its messages never enter the foreground transcript or session record. Reviews
|
||||
retain the foreground session's sandbox policy.
|
||||
## What a review does
|
||||
|
||||
In `auto` mode, the reviewer uses ordinary directory, file, patch, and shell
|
||||
tools under the source session's permissions. It can inspect complete skills and
|
||||
supporting files, correct several connected files, and verify the result. Its
|
||||
file tools are rooted at the Workshop directory. Shell commands retain the
|
||||
operator's existing approval policy. An enabled sandbox must provide writable
|
||||
access to that directory. The run uses the normal configured agent timeout.
|
||||
The review forks the conversation through the finished turn and appends one
|
||||
instruction, reusing the foreground provider, model, auth profile, and tool
|
||||
schemas so the provider's prompt cache applies. Model fallbacks are disabled. It
|
||||
runs on the embedded OpenClaw harness under a private session that never
|
||||
appears in your transcript.
|
||||
|
||||
The reviewer shares the weekly collection's maintenance instructions: audit
|
||||
before editing, give a procedure one home, preserve distinct tasks, and verify
|
||||
the resulting files. New learning should replace a misleading rule or strengthen
|
||||
an existing one rather than append another copy. A covered lesson needs no edit.
|
||||
The runtime receipt identifies skills actually used in the foreground turn.
|
||||
Ordinary directory reads discover the current collection without a separate
|
||||
clipped inventory.
|
||||
The review can look things up with `read`, `ls`, `view_image`, `web_search`,
|
||||
`web_fetch`, `sessions_history`, `sessions_search`, `memory_search`, and
|
||||
`memory_get`; `skill_workshop` is the only tool that changes anything. Tools that
|
||||
act, such as `exec`, `write`, or `message`, return a normal result such as
|
||||
`exec is not available in this background run.` That result is not a failure,
|
||||
and the review never re-runs your task. The review must `view` a skill
|
||||
before it patches, writes, or archives it, and an archive needs `absorbed_into`
|
||||
or `reason`.
|
||||
|
||||
In `propose` mode, only `skill_workshop` executes. The reviewer can inspect and
|
||||
read before staging one create, patch, update, or revision. Existing proposal
|
||||
read/hash/size validation remains in effect. The draft stays pending even if the
|
||||
operator enables automatic learning during that review.
|
||||
The reviewer looks for:
|
||||
|
||||
Each review gets one attempt. A failure is recorded instead of retried. Automatic
|
||||
maintenance records `completed` when the agent run succeeds. That status is not
|
||||
a claim that a file changed. Completed file edits survive later failure or
|
||||
cancellation, and future sessions receive a refreshed skill snapshot. Source
|
||||
session deletion, replacement, or a permission-mode change fences retained tools.
|
||||
- a correction to your approach, output, or style.
|
||||
- a non-obvious technique, fix, or command sequence that worked after trial and
|
||||
error.
|
||||
- a learned skill that was wrong, missing a step, or outdated.
|
||||
|
||||
Good candidates include:
|
||||
It lists learned skills first, then prefers patching a skill that was used or
|
||||
already covers the task, then adding a `references/`, `templates/`, or
|
||||
`scripts/` file to one, and creates a new skill only when none covers the task.
|
||||
Listed skills that cover the same class of task get merged into one umbrella
|
||||
skill: the review patches the survivor and archives the rest with
|
||||
`absorbed_into`. When the turn was interrupted, it keeps only steps that visibly
|
||||
worked before the stop.
|
||||
|
||||
- a reliable recovery after repeated tool or model failures.
|
||||
- a durable user correction or standing instruction ("from now on," "always,"
|
||||
"never," "stop doing X"), embedded as a procedure step in the skill governing
|
||||
that work.
|
||||
- a non-obvious ordering constraint that prevented a recurring error.
|
||||
- a stable multi-step workflow that required repeated discovery.
|
||||
- a reusable preflight that would avoid several future calls.
|
||||
It does not capture environment-specific or transient failures, negative claims
|
||||
about tools, unresolved failures or guesses, one-off tasks, personal facts,
|
||||
secrets, or generic advice without concrete commands, paths, or ids. When
|
||||
nothing durable was learned, it changes nothing.
|
||||
|
||||
The reviewer should abstain for:
|
||||
## What you see
|
||||
|
||||
- routine successful work or a one-time request.
|
||||
- personal facts and simple preferences.
|
||||
- transient environment or service failures.
|
||||
- generic advice without concrete supporting evidence.
|
||||
- unsupported negative claims.
|
||||
- secrets and credential material.
|
||||
If the review changed a skill, one line is posted to the conversation that
|
||||
triggered it and mirrored into the session transcript:
|
||||
|
||||
## Mode policy
|
||||
```text
|
||||
💾 Learned: updated `deploy-staging` (tightened the rollback step). Say "undo" to revert this skill change.
|
||||
```
|
||||
|
||||
| Mode | Capture behavior |
|
||||
| --------- | ---------------------------------------------------------------------------- |
|
||||
| `off` | Does not create experience-review captures. |
|
||||
| `propose` | Creates or revises pending proposals. Nothing applies automatically. |
|
||||
| `auto` | Maintains Workshop skills with normal agent file tools. This is the default. |
|
||||
Channel-less Control UI sessions get the line as a transcript entry. Nothing is
|
||||
posted when nothing changed. Reply "undo" and the agent restores the previous
|
||||
version.
|
||||
|
||||
Set the mode with the CLI:
|
||||
A review stops without changing anything further if you turn learning off, or
|
||||
if the source session is deleted, replaced, or changes permission mode while it
|
||||
runs. Changes it already made stay, each with a saved previous version.
|
||||
|
||||
## Turn it on or off
|
||||
|
||||
```bash
|
||||
openclaw config set skills.workshop.autonomous.mode auto
|
||||
openclaw config set skills.workshop.autonomous.mode propose
|
||||
openclaw config set skills.workshop.autonomous.mode off
|
||||
openclaw config set skills.workshop.autonomous.mode auto
|
||||
```
|
||||
|
||||
Or edit `~/.openclaw/openclaw.json`:
|
||||
|
||||
```json5
|
||||
{
|
||||
skills: {
|
||||
workshop: {
|
||||
autonomous: {
|
||||
mode: "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Changing the mode does not alter existing proposals or applied skills. Manual
|
||||
learning sessions, `/learn`, and explicit Workshop requests remain available in all
|
||||
three modes.
|
||||
|
||||
## Why auto is safe to default
|
||||
|
||||
Automatic background learning follows the same normal file-edit semantics as
|
||||
weekly collection maintenance:
|
||||
|
||||
- **Workshop ownership:** file tools stay in
|
||||
`<state-dir>/agents/<agentId>/agent/workshop-skills`. Other skill roots remain
|
||||
outside the maintenance task.
|
||||
- **Existing permissions:** the run preserves the source session's permission
|
||||
mode, tool restrictions, and shell approval policy. Conversation evidence does
|
||||
not grant extra access.
|
||||
- **Independent lifecycle:** foreground work does not await the review. Gateway
|
||||
drain and source invalidation close the review's authority.
|
||||
- **Editorial judgment:** the agent reads complete relevant files, preserves
|
||||
useful meaning, and checks its changes rather than targeting a size or count.
|
||||
|
||||
Direct maintenance does not create proposals, run a post-turn scanner, or record
|
||||
automatic rollback snapshots. Use backups for recovery from unwanted direct
|
||||
edits. Explicit proposals and immediate foreground repair retain their existing
|
||||
scanner, hash binding, size validation, and rollback metadata.
|
||||
|
||||
Reject a pending miscapture with one command:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop reject <proposal-id> --reason "Not reusable"
|
||||
```
|
||||
|
||||
Proposal captures remain visible in `openclaw skills workshop list`. Direct
|
||||
maintenance changes appear in the installed Workshop skills, not as proposal
|
||||
records. Weekly review results remain in automation history. Retained legacy
|
||||
backups keep their [restore path](/tools/skill-workshop#changes-and-recovery).
|
||||
|
||||
Residual risk remains: an agent can make an incorrect edit. Inspect installed
|
||||
skills in Workshop, or choose `propose` when every capture needs human review.
|
||||
|
||||
## Runtime support
|
||||
|
||||
Delayed experience review requires the runtime to report its resolved model and
|
||||
actual `skill_workshop` availability. The embedded runner and Codex app-server
|
||||
harness report those facts. Codex also reports its exact model-iteration count.
|
||||
Other CLI-backed runtimes fail closed until they provide the same runtime facts.
|
||||
`/learn` does not depend on delayed review and continues to work on those
|
||||
runtimes.
|
||||
The Control UI **Plugins → Skill workshop** page has the same switch. `off`
|
||||
stops background reviews and unused-skill cleanup. The agent can still create or
|
||||
update learned skills when you ask, through `/learn`, or in a
|
||||
**Learn from past conversations** session.
|
||||
|
||||
## Cost and privacy
|
||||
|
||||
Experience review adds one model run on the configured provider only after a
|
||||
substantial turn, not after every message. The review can make several requests
|
||||
while it inspects, edits, and verifies skills.
|
||||
A review is one extra model run on the foreground provider and model after
|
||||
substantial work, not after every message. It can make several requests while
|
||||
it views and edits skills. Prompt-cache reuse lowers the cost of re-reading the
|
||||
conversation; provider pricing still applies.
|
||||
|
||||
The review creates a detached view of the foreground model context and appends
|
||||
one small user message. Storage-only native prompt payloads stay in the original
|
||||
transcript, whose stored bytes the review does not change. It uses a private
|
||||
detached session identity while preserving the
|
||||
foreground provider, model, auth profile, bootstrap context, tool schemas, and
|
||||
prompt-cache affinity. Removing unavailable skill guidance changes the prompt,
|
||||
so only compatible prefixes can be reused. The review never becomes part of
|
||||
the foreground session.
|
||||
|
||||
The reviewer reuses the foreground provider, model, and available auth identity,
|
||||
with model fallbacks disabled. Provider pricing and data-handling terms apply to
|
||||
the additional run.
|
||||
|
||||
Weekly [collection review](/tools/skill-workshop#collection-review) uses the
|
||||
agent's configured model and normal cron scheduling. Skill bodies remain review
|
||||
material, not active instructions. Completed edits persist. There is no
|
||||
collection-wide transaction or automatic rollback.
|
||||
|
||||
**Learn from past conversations** in Workshop opens a normal agent session with
|
||||
the learning instructions. The agent uses its configured model, permitted tools,
|
||||
existing skills, and accessible conversation history. It chooses what to read.
|
||||
There is no separate scan threshold, transcript bundle, or batch cursor.
|
||||
|
||||
The session follows the current Workshop mode: `auto` permits direct skill
|
||||
improvements, while `propose` leaves suggestions for approval. You can watch,
|
||||
steer, or stop the run in chat. Starting it does not enable automatic learning
|
||||
or change settings. Like other sessions, it shares the agent's normal capacity.
|
||||
**Learn from past conversations** opens a normal chat in which the agent reads
|
||||
earlier conversations it can access, with its configured model and tools. You
|
||||
can watch, steer, or stop it. Starting one does not change the learning mode.
|
||||
|
||||
<Warning>
|
||||
Experience review and manual learning sessions can send conversation
|
||||
content, including tool inputs and results, to the configured model provider.
|
||||
Choose a provider and mode that match the workspace privacy and data-handling
|
||||
requirements.
|
||||
Background reviews and learning sessions send conversation content, including
|
||||
tool inputs and results, to the configured model provider. Choose a provider
|
||||
and mode that match the workspace's privacy and data-handling requirements.
|
||||
</Warning>
|
||||
|
||||
## Review and revert learning
|
||||
|
||||
List and inspect every pending, applied, rejected, quarantined, or stale capture:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop list
|
||||
openclaw skills workshop inspect <proposal-id>
|
||||
```
|
||||
|
||||
Stop a pending capture from becoming active or quarantine it for safety review:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop reject <proposal-id> --reason "Too specific"
|
||||
openclaw skills workshop quarantine <proposal-id> --reason "Needs security review"
|
||||
```
|
||||
|
||||
Use `/learn` when you want an explicit proposal from the current conversation or
|
||||
named sources:
|
||||
|
||||
```text
|
||||
/learn
|
||||
/learn docs/runbook.md; focus on recovery
|
||||
```
|
||||
|
||||
`/learn` first revises a matching pending proposal or updates a matching live
|
||||
skill. It creates a new pending proposal only when no skill owns the procedure,
|
||||
and never auto-applies the result.
|
||||
|
||||
To learn from older work, open **Plugins -> Workshop** and select
|
||||
**Learn from past conversations**. The new chat follows your current Workshop
|
||||
mode and shows the agent's work and results.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
| Setting | Default | Effect |
|
||||
| --------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `skills.workshop.autonomous.mode` | `"auto"` | Chooses capture behavior; `auto` also enables weekly collection review. |
|
||||
| `skills.workshop.approvalPolicy` | `"auto"` | Controls prompts for normal agent-initiated lifecycle calls. It never expands the isolated reviewer tool surface. |
|
||||
| `skills.workshop.maxPending` | `50` | Caps pending and quarantined proposals per agent. |
|
||||
| `skills.workshop.maxSkillBytes` | `40000` | Caps proposal body size in bytes. |
|
||||
|
||||
See [Skills config](/tools/skills-config#workshop-skills-workshop) for ranges and
|
||||
the complete `skills.*` schema.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No capture appears
|
||||
### Nothing is learned
|
||||
|
||||
Check the following:
|
||||
1. `skills.workshop.autonomous.mode` is `auto` in the active Gateway config.
|
||||
2. The session did at least 10 model iterations since its last review, without
|
||||
provider or prompt errors.
|
||||
3. The session is an ordinary conversation, not cron, heartbeat, hook,
|
||||
subagent, or Incognito.
|
||||
4. Tool policy allows `skill_workshop` and the session is not sandboxed. In
|
||||
`auto` mode, `openclaw doctor` reports the setting that hides it.
|
||||
5. The Gateway stayed up and idle for the 30-second wait.
|
||||
|
||||
1. `skills.workshop.autonomous.mode` is `propose` or `auto` in the active Gateway
|
||||
config.
|
||||
2. The turn reached at least 10 model iterations without ending in a provider or
|
||||
prompt error.
|
||||
3. The conversation is eligible foreground work.
|
||||
4. The runtime reported the resolved model and actual `skill_workshop`
|
||||
availability.
|
||||
5. Tool policy permits Workshop. Automatic maintenance also needs normal file
|
||||
access. An enabled sandbox must expose its Workshop directory as writable.
|
||||
6. The Gateway stayed running and idle through the 30-second quiet period.
|
||||
A review that ran can still decide nothing is worth saving. That is the
|
||||
expected result for routine work.
|
||||
|
||||
An eligible experience review can still abstain. No proposal is the expected
|
||||
result when the evidence does not clear the reusable-procedure bar.
|
||||
Use `openclaw skills curator status` to inspect experience review outcomes and
|
||||
live skill usage. Current weekly collection results are in automation run history.
|
||||
That CLI retains only the earlier collection records. It does not archive or
|
||||
expire skills by age. The `curator pin`, `unpin`, and `restore` commands return an
|
||||
error explaining that weekly collection review manages the skill collection.
|
||||
### Too many or unwanted changes
|
||||
|
||||
### Doctor reports that Workshop is hidden
|
||||
|
||||
In `propose` and `auto` modes, `openclaw doctor` checks whether the default agent
|
||||
tool policy permits `skill_workshop`. Apply the reported `tools.allow` or
|
||||
`tools.alsoAllow` change, or set the autonomous mode to `off`.
|
||||
|
||||
### A proposal remains pending in auto mode
|
||||
|
||||
Automatic apply runs once. Inspect the proposal and its scanner state:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop inspect <proposal-id>
|
||||
```
|
||||
|
||||
A normal write failure leaves it pending for manual review. A critical scanner
|
||||
result moves it to quarantine. Fix the cause and apply manually. Do not build a
|
||||
retry loop around automatic capture.
|
||||
|
||||
### Too many low-value captures appear
|
||||
|
||||
Switch to `propose` to review every capture, or `off` to disable autonomous
|
||||
capture:
|
||||
|
||||
```bash
|
||||
openclaw config set skills.workshop.autonomous.mode propose
|
||||
openclaw config set skills.workshop.autonomous.mode off
|
||||
```
|
||||
|
||||
Existing proposals and applied skills remain visible after the mode changes.
|
||||
Undo individual changes with "undo", the Control UI **Undo** button, or
|
||||
`openclaw skills workshop restore <name>`. Archive a skill you do not want with
|
||||
`openclaw skills workshop archive <name>`. To stop background learning, set the
|
||||
mode to `off`.
|
||||
|
||||
## Related
|
||||
|
||||
- [Skill Workshop](/tools/skill-workshop) for proposal lifecycle and storage
|
||||
- [Creating skills](/tools/creating-skills) for hand-authored skills
|
||||
- [Skills config](/tools/skills-config) for every `skills.*` setting
|
||||
- [Skills CLI](/cli/skills) for Workshop commands
|
||||
- [Skill Workshop](/tools/skill-workshop) for the tool, storage, undo, and unused-skill cleanup
|
||||
- [Creating skills](/tools/creating-skills) for hand-written skills
|
||||
- [Skills config](/tools/skills-config#workshop-skills-workshop) for `skills.workshop`
|
||||
- [Skills CLI](/cli/skills#skill-workshop) for `openclaw skills workshop`
|
||||
|
|
|
|||
|
|
@ -1,86 +1,268 @@
|
|||
---
|
||||
summary: "Index of the Skill Workshop documentation, one page per reader job"
|
||||
summary: "Skills your agent learns on its own: how they are saved, undo, unused-skill cleanup, /learn, config, storage, and operator surfaces"
|
||||
read_when:
|
||||
- You want the agent to create or update a skill from chat
|
||||
- You need to review, apply, reject, or quarantine a generated skill draft
|
||||
- You are configuring Skill Workshop approval, autonomy, storage, or limits
|
||||
- You are looking for the Skill Workshop page that matches your task
|
||||
- You want to know how your agent saves and updates its own skills
|
||||
- You want to undo, archive, or restore a learned skill
|
||||
- You are turning Skill Workshop learning on or off
|
||||
title: "Skill Workshop"
|
||||
sidebarTitle: "Skill Workshop"
|
||||
---
|
||||
|
||||
Skill Workshop is OpenClaw's governed path for creating and updating its own
|
||||
generated skills. Through this path, agents and operators create a **proposal** (pending
|
||||
draft with content, target binding, scanner state, hashes, and rollback
|
||||
metadata) that becomes a live skill only when applied.
|
||||
Skill Workshop holds the skills an agent writes for itself, called **learned
|
||||
skills**. The agent saves a procedure after hard multi-step work and fixes a
|
||||
learned skill that misled it; skills nobody uses for 30 days are archived. Every
|
||||
change applies immediately, saves the previous version first, and can be undone.
|
||||
|
||||
Automatic background learning and weekly collection review instead maintain the
|
||||
Workshop directory with normal agent file tools. These direct edits do not create
|
||||
proposals or automatic rollback snapshots. Choose `propose` mode when each new
|
||||
skill change needs review before publication.
|
||||
Learned skills belong to one agent and are always visible to it: they bypass
|
||||
`agents.defaults.skills` and `agents.entries.<id>.skills` allowlists. To hide
|
||||
one, archive it.
|
||||
|
||||
By default, Skill Workshop writes only under the active agent's
|
||||
`<state-dir>/agents/<agentId>/agent/workshop-skills`. When `agents.entries.<id>.agentDir` is
|
||||
configured, it writes under `<agentDir>/workshop-skills` instead. Operators edit
|
||||
bundled, plugin, ClawHub, extra-root, managed, personal-agent, project, and
|
||||
workspace skills through their owning tools or files. The same authoring tool
|
||||
also supports [personal library skills](/tools/skills#personal-skills-on-a-shared-gateway)
|
||||
when the Gateway supplies an authorized library target. Those operations publish
|
||||
managed revisions rather than Workshop proposals.
|
||||
Skills you write yourself (workspace, project, managed, ClawHub, plugin, and
|
||||
bundled skills) are not Workshop skills. Edit them at their source; see
|
||||
[Creating skills](/tools/creating-skills). For profile-owned skills on a shared
|
||||
Gateway, see [Personal library authoring](/tools/skill-workshop/personal-library).
|
||||
|
||||
Workshop storage is installation-managed and separate from the session
|
||||
workspace and managed skill library. `OPENCLAW_STATE_DIR` selects the state
|
||||
directory. `~/.openclaw` is the default.
|
||||
## How the agent learns
|
||||
|
||||
This page is an index. Skill Workshop is documented on eight pages, one per
|
||||
reader job. Open the page that matches your task.
|
||||
- **During a turn:** when a learned skill the agent used turns out wrong or
|
||||
incomplete, it views the skill and patches the misleading step. After hard
|
||||
multi-step work you are likely to repeat, it saves the working procedure,
|
||||
patching the skill that covers that kind of task or creating one when none does.
|
||||
- **Background review:** after enough model work in a conversation, or right
|
||||
after a turn that used a learned skill, a background run reviews it and saves
|
||||
anything worth keeping. See [Self-learning](/tools/self-learning).
|
||||
- **`/learn [request]`:** asks the agent to save a skill now, from the current
|
||||
conversation or from sources you name. See [`/learn`](#learn).
|
||||
- **Learn from past conversations:** the Control UI button opens a normal chat
|
||||
in which the agent reviews earlier conversations and saves what it finds.
|
||||
- **Unused-skill cleanup:** learned skills nobody used for 30 days are archived.
|
||||
See [Unused-skill cleanup](#unused-skill-cleanup).
|
||||
|
||||
| Page | Read it when |
|
||||
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| [Personal library authoring](/tools/skill-workshop/personal-library) | You are authoring a profile-owned personal skill on a shared Gateway. |
|
||||
| [How Skill Workshop works](/tools/skill-workshop/how-it-works) | You need the proposal rules, the Control UI review surfaces, or the state diagram. |
|
||||
| [Collection review](/tools/skill-workshop/collection-review) | You are enabling weekly review, or recovering a skill from a retained backup. |
|
||||
| [Chat and CLI authoring](/tools/skill-workshop/authoring) | You are creating, revising, or applying a proposal from chat or the CLI. |
|
||||
| [Proposal content, support files, and the agent tool](/tools/skill-workshop/proposals) | You are writing `PROPOSAL.md`, adding support files, or calling `skill_workshop`. |
|
||||
| [Self-learning and approval settings](/tools/skill-workshop/configuration) | You are configuring autonomy, approval policy, or Workshop size caps. |
|
||||
| [Gateway methods, storage, and limits](/tools/skill-workshop/reference) | You need the Gateway method list, the on-disk layout, or the hard limits. |
|
||||
| [Skill Workshop troubleshooting](/tools/skill-workshop/troubleshooting) | A proposal fails, does not appear, or the agent cannot call the tool. |
|
||||
Changed skills load in new sessions. A running session keeps the skill snapshot
|
||||
it started with.
|
||||
|
||||
## Where each section moved
|
||||
<a id="changes-and-recovery" />
|
||||
|
||||
Every section heading id from the previous single-page version keeps its
|
||||
anchor here, so an existing link such as
|
||||
`/tools/skill-workshop#collection-review` still resolves. Each entry points at
|
||||
the page that now holds the content.
|
||||
## Undo
|
||||
|
||||
- <a id="personal-library-authoring" />[Personal library authoring](/tools/skill-workshop/personal-library#personal-library-authoring)
|
||||
- <a id="how-it-works" />[How it works](/tools/skill-workshop/how-it-works#how-it-works)
|
||||
- <a id="review-in-the-control-ui" />[Review in the Control UI](/tools/skill-workshop/how-it-works#review-in-the-control-ui)
|
||||
- <a id="lifecycle" />[Lifecycle](/tools/skill-workshop/how-it-works#lifecycle)
|
||||
- <a id="collection-review" />[Collection review](/tools/skill-workshop/collection-review#collection-review)
|
||||
- <a id="changes-and-recovery" />[Changes and recovery](/tools/skill-workshop/collection-review#changes-and-recovery)
|
||||
- <a id="when-an-older-backup-cannot-be-restored-automatically" />[When an older backup cannot be restored automatically](/tools/skill-workshop/collection-review#when-an-older-backup-cannot-be-restored-automatically)
|
||||
- <a id="chat" />[Chat](/tools/skill-workshop/authoring#chat)
|
||||
- <a id="learn-from-recent-work" />[Learn from recent work](/tools/skill-workshop/authoring#learn-from-recent-work)
|
||||
- <a id="cli" />[CLI](/tools/skill-workshop/authoring#cli)
|
||||
- <a id="plugin-evaluation-and-lifecycle-hooks" />[Plugin evaluation and lifecycle hooks](/tools/skill-workshop/proposals#plugin-evaluation-and-lifecycle-hooks)
|
||||
- <a id="proposal-content" />[Proposal content](/tools/skill-workshop/proposals#proposal-content)
|
||||
- <a id="support-files" />[Support files](/tools/skill-workshop/proposals#support-files)
|
||||
- <a id="agent-tool" />[Agent tool](/tools/skill-workshop/proposals#agent-tool)
|
||||
- <a id="self-learning" />[Self-learning](/tools/skill-workshop/configuration#self-learning)
|
||||
- <a id="scan-past-sessions" />[Scan past sessions](/tools/skill-workshop/configuration#scan-past-sessions)
|
||||
- <a id="approval-and-autonomy" />[Approval and autonomy](/tools/skill-workshop/configuration#approval-and-autonomy)
|
||||
- <a id="gateway-methods" />[Gateway methods](/tools/skill-workshop/reference#gateway-methods)
|
||||
- <a id="storage" />[Storage](/tools/skill-workshop/reference#storage)
|
||||
- <a id="limits" />[Limits](/tools/skill-workshop/reference#limits)
|
||||
- <a id="troubleshooting" />[Troubleshooting](/tools/skill-workshop/troubleshooting#troubleshooting)
|
||||
- <a id="tool-policy-diagnostic" />[Tool-policy diagnostic](/tools/skill-workshop/troubleshooting#tool-policy-diagnostic)
|
||||
When a background run changes a skill, OpenClaw posts one line to the
|
||||
conversation that triggered it:
|
||||
|
||||
```text
|
||||
💾 Learned: updated `deploy-staging` (tightened the rollback step). Say "undo" to revert this skill change.
|
||||
```
|
||||
|
||||
Reply "undo" and the agent restores the previous version with `skill_workshop`.
|
||||
Nothing is posted when the review changed nothing. Channel-less Control UI
|
||||
sessions get the same line as a transcript entry.
|
||||
|
||||
You can also undo from the Control UI (**Undo** next to a recent change) or the
|
||||
CLI:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop restore deploy-staging
|
||||
openclaw skills workshop restore deploy-staging --version <version-id>
|
||||
```
|
||||
|
||||
Restore saves the current copy before replacing it, so an undo can itself be
|
||||
undone. A skill that was just created has no earlier version; archive it instead.
|
||||
|
||||
<a id="collection-review" />
|
||||
<a id="weekly-curator" />
|
||||
|
||||
## Unused-skill cleanup
|
||||
|
||||
When learning is on, OpenClaw archives a learned skill with no activity for 30
|
||||
days, with the reason `unused for 30 days` and actor `curator` in the change
|
||||
feed. Activity is the latest of: a recorded read of its `SKILL.md`, a
|
||||
foreground `skill_workshop` `view`, and its last change (create, patch,
|
||||
restore). A skill younger than 30 days is never archived. The check runs at
|
||||
most once a day per agent, after a finished turn, and never delays the turn.
|
||||
Archive is the normal versioned archive: restore it any time.
|
||||
|
||||
Cleanup only runs for agents whose default runtime is the embedded OpenClaw
|
||||
harness, in the Gateway process. The Codex app-server harness reads skills with
|
||||
its native shell, which OpenClaw cannot attribute to a skill, so cleanup stays
|
||||
off for Codex agents rather than archiving skills that are in use.
|
||||
|
||||
Earlier versions ran a weekly curator automation
|
||||
(`skill-collection-review:<agentId>`). It is retired: the Gateway deletes those
|
||||
cron rows on upgrade and creates no replacement.
|
||||
|
||||
<a id="learn" />
|
||||
|
||||
## `/learn`
|
||||
|
||||
```text
|
||||
/learn
|
||||
/learn docs/runbook.md; focus on recovery
|
||||
```
|
||||
|
||||
`/learn` is a normal foreground turn. With no request, the agent saves the
|
||||
reusable workflow from the current conversation. With a request, it gathers the
|
||||
named paths, URLs, notes, or conversation references with its normal tools and
|
||||
honors any focus, scope, or naming you give. It views related skills first,
|
||||
patches the one that covers the task, and creates a new skill only when none
|
||||
does. Related skills that cover the same class of task get merged into one
|
||||
umbrella skill: it patches the survivor and archives the rest with
|
||||
`absorbed_into`. Then it tells you which skill changed. If there is nothing
|
||||
durable to learn, it changes nothing.
|
||||
|
||||
`/learn` works in both learning modes. It replies with an explanation instead
|
||||
when `skill_workshop` is unavailable, for example in a sandboxed session or
|
||||
when tool policy hides the tool.
|
||||
|
||||
## Agent tool
|
||||
|
||||
The built-in `skill_workshop` tool is how every learned-skill change is made.
|
||||
It is part of `tools.profile: "coding"`; with a stricter policy, add it to
|
||||
`tools.allow` or `tools.alsoAllow`.
|
||||
|
||||
| Action | Parameters | Effect |
|
||||
| ------------- | ---------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `list` | — | Lists live skills and archived skills |
|
||||
| `view` | `name`, optional `file_path`, `version` | Reads a file, current or from a saved version |
|
||||
| `create` | `name`, `content` (full `SKILL.md`) | Creates a new skill |
|
||||
| `patch` | `name`, `old_text`, `new_text`, optional `file_path` | Replaces one exact, unique span |
|
||||
| `write_file` | `name`, `file_path`, `content` | Writes a support file, or rewrites `SKILL.md` |
|
||||
| `remove_file` | `name`, `file_path` | Deletes one support file; `SKILL.md` goes only through `archive` |
|
||||
| `archive` | `name`, optional `absorbed_into`, `reason` | Hides the skill; `absorbed_into` names the live skill that now covers it |
|
||||
| `restore` | `name`, optional `version` | Restores the newest saved version, or the one named |
|
||||
|
||||
Every mutating action accepts `reason`, one short line that appears in the
|
||||
change feed and the chat notice. Every change saves the previous version first.
|
||||
|
||||
Writes are validated before they land:
|
||||
|
||||
- Names use 1-63 lowercase letters, digits, or hyphens and start with a letter
|
||||
or digit.
|
||||
- `SKILL.md` needs frontmatter whose `name` matches the skill directory and a
|
||||
`description` of 1-1024 bytes (aim for about 160). It must fit within `skills.workshop.maxSkillBytes`.
|
||||
- Support files go under `references/`, `templates/`, `scripts/`, or `assets/`,
|
||||
up to 256 KiB each. Absolute paths, traversal, and symlinks are refused.
|
||||
- A critical security-scanner finding, including a literal secret, refuses the
|
||||
write and names the file, line, and rule.
|
||||
|
||||
The background review must `view` an existing skill before it can `patch`,
|
||||
`write_file`, `remove_file`, or `archive` it, and its archives need
|
||||
`absorbed_into` or `reason`. A foreground `view` counts as using the skill.
|
||||
|
||||
A successful `create`, `patch`, or `write_file` of `SKILL.md` may end with up to
|
||||
three `Advisory (not blocking)` lines: authoring issues the write introduced (a
|
||||
description over 160 bytes or opening with "This skill", a body over 250 lines
|
||||
or 12 KB, emphasis words, three or more Never/Don't steps, update notes or
|
||||
dates). The write has already landed. A `create` also lists the agent's other
|
||||
learned skills, so the agent itself decides whether the new skill duplicates
|
||||
one and should be merged.
|
||||
|
||||
## Configuration
|
||||
|
||||
```json5
|
||||
{
|
||||
skills: {
|
||||
workshop: {
|
||||
autonomous: { mode: "auto" },
|
||||
maxSkillBytes: 40000,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| Setting | Default | Effect |
|
||||
| --------------------------------- | -------- | --------------------------------------------------------------------------------------- |
|
||||
| `skills.workshop.autonomous.mode` | `"auto"` | `"auto"` enables the background review and unused-skill cleanup. `"off"` disables both. |
|
||||
| `skills.workshop.maxSkillBytes` | `40000` | Maximum `SKILL.md` size in bytes (1024-200000). |
|
||||
|
||||
```bash
|
||||
openclaw config set skills.workshop.autonomous.mode off
|
||||
openclaw config set skills.workshop.autonomous.mode auto
|
||||
```
|
||||
|
||||
With `off`, the agent can still create and update learned skills when you ask,
|
||||
through `/learn`, or in a **Learn from past conversations** session. See
|
||||
[Skills config](/tools/skills-config#workshop-skills-workshop) for the schema.
|
||||
|
||||
## Where files live
|
||||
|
||||
```text
|
||||
<agentDir>/workshop-skills/
|
||||
<name>/
|
||||
SKILL.md
|
||||
references/ templates/ scripts/ assets/
|
||||
.archive/
|
||||
<name>/<versionId>/ # full copy of the skill before each change
|
||||
```
|
||||
|
||||
`<agentDir>` defaults to `<state-dir>/agents/<agentId>/agent`, or
|
||||
`agents.entries.<id>.agentDir` when set. `<state-dir>` is `~/.openclaw` unless
|
||||
`OPENCLAW_STATE_DIR` overrides it.
|
||||
|
||||
A version is saved before every change, including archive and restore. The
|
||||
newest 10 versions per skill are kept. An archived skill has no live directory;
|
||||
its newest version restores it. The change feed (who changed which skill, when,
|
||||
and why) lives in the state database and keeps the newest 500 entries per agent.
|
||||
|
||||
## Operator surfaces
|
||||
|
||||
- **Control UI:** open **Plugins → Skill workshop** to see learned skills with
|
||||
their use counts, recent changes with **Undo**, a file viewer with saved
|
||||
versions, archive and restore, the learning mode switch, and
|
||||
**Learn from past conversations**.
|
||||
- **CLI:** `openclaw skills workshop list | changes | show | archive | restore`.
|
||||
See [Skills CLI](/cli/skills#skill-workshop).
|
||||
- **Plugins:** the [`skill_changed`](/plugins/hooks/reference#skill-lifecycle)
|
||||
hook observes each committed Workshop change.
|
||||
|
||||
Gateway methods take an optional `agentId` (default agent when omitted):
|
||||
|
||||
| Method | Scope | Params | Returns |
|
||||
| ------------------------- | ---------------- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| `skills.workshop.list` | `operator.read` | — | `agentId`, `mode`, `root`, live `skills` (with `useCount`, `lastUsedAtMs`), `archived` versions |
|
||||
| `skills.workshop.changes` | `operator.read` | `limit` (up to 500), `beforeMs` | `changes`, newest first |
|
||||
| `skills.workshop.read` | `operator.read` | `name`, `filePath`, `versionId` | `name`, `filePath`, `content`, `files` |
|
||||
| `skills.workshop.archive` | `operator.admin` | `name`, `reason` | `change` |
|
||||
| `skills.workshop.restore` | `operator.admin` | `name`, `versionId` | `change` |
|
||||
|
||||
Archive and restore from the CLI, Control UI, or Gateway are recorded as `user`
|
||||
changes.
|
||||
|
||||
<a id="when-an-older-backup-cannot-be-restored-automatically" />
|
||||
|
||||
## Upgrading from earlier releases
|
||||
|
||||
Earlier releases staged learned skills as drafts for review. `openclaw doctor
|
||||
--fix` exports any pending drafts and removes the old settings; see
|
||||
[State migrations](/cli/doctor/state-migrations) and
|
||||
[Skills config](/tools/skills-config#workshop-skills-workshop). Exported drafts
|
||||
are not loaded. Ask the agent to save one with `/learn` if you still want it.
|
||||
|
||||
The earlier `skills.proposals.*` and `skills.curator.*` Gateway methods stay
|
||||
registered but return an error that points to the methods above. The
|
||||
`skill_proposal_evaluate` and `skill_proposal_changed` plugin hooks were removed;
|
||||
see [Removed surfaces](/plugins/sdk-migration/removed-surfaces#skill-workshop-proposal-hooks).
|
||||
|
||||
Backups written by the earlier weekly review under
|
||||
`<agentDir>/skill-workshop/collection-backups/` are no longer read. Copy any
|
||||
files you need from them by hand; OpenClaw does not restore them.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Check |
|
||||
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Nothing is ever learned | `skills.workshop.autonomous.mode` is `auto`, the conversation is eligible, and tool policy allows `skill_workshop`. See [Self-learning](/tools/self-learning#troubleshooting). |
|
||||
| Agent cannot call `skill_workshop` | Sandboxed runs do not get the tool. Use a non-sandboxed session or the CLI. Otherwise add the tool to `tools.allow` or `tools.alsoAllow`. |
|
||||
| A write is refused | The error names the fix: rename the skill, correct the frontmatter, shorten the description or `SKILL.md`, or remove the flagged line. |
|
||||
| An unwanted change was made | Say "undo", press **Undo** in the Control UI, or run `openclaw skills workshop restore <name>`. |
|
||||
| A skill was archived unexpectedly | Unused-skill cleanup archives skills with no activity for 30 days. Run `openclaw skills workshop restore <name>`. |
|
||||
|
||||
In `auto` mode, `openclaw doctor` runs the `core/doctor/skill-workshop-tool-policy`
|
||||
check for each agent. It names the sandbox setting or the config layer that
|
||||
hides `skill_workshop` and the exact `allow` or `alsoAllow` change to make.
|
||||
|
||||
## Related
|
||||
|
||||
- [Skills](/tools/skills) for load order, precedence, and visibility
|
||||
- [Self-learning](/tools/self-learning) for conservative post-run skill proposals
|
||||
- [Self-learning](/tools/self-learning) for the background review
|
||||
- [Skills](/tools/skills) for load order and visibility
|
||||
- [Creating skills](/tools/creating-skills) for hand-written `SKILL.md`
|
||||
basics
|
||||
- [Skills config](/tools/skills-config) for the full `skills.workshop` schema
|
||||
- [Skills CLI](/cli/skills) for `openclaw skills` commands
|
||||
- [Skills config](/tools/skills-config#workshop-skills-workshop) for the `skills.workshop` schema
|
||||
- [Skills CLI](/cli/skills#skill-workshop) for `openclaw skills workshop`
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
---
|
||||
summary: "Create, revise, and close out Workshop proposals from chat or the CLI"
|
||||
title: "Chat and CLI authoring"
|
||||
read_when:
|
||||
- You want the agent to create or update a skill from chat
|
||||
- You are using /learn to capture recent work
|
||||
- You need the openclaw skills workshop commands and flags
|
||||
---
|
||||
|
||||
## Chat
|
||||
|
||||
For Workshop authoring, ask the agent for the skill you want; it calls
|
||||
`skill_workshop` and returns a proposal id. Personal library authoring instead
|
||||
returns the managed publication receipt described in
|
||||
[Personal library authoring](/tools/skill-workshop/personal-library).
|
||||
|
||||
### Learn from recent work
|
||||
|
||||
Use `/learn` to route the current conversation or named sources into the best
|
||||
matching pending proposal or live skill, creating a skill only when needed:
|
||||
|
||||
```text
|
||||
/learn
|
||||
/learn docs/runbook.md and https://example.com/guide; focus on recovery
|
||||
```
|
||||
|
||||
With no request, `/learn` asks the agent to distill the reusable workflow from
|
||||
the current conversation. With a request, the agent treats paths, URLs, pasted
|
||||
notes, and conversation references as sources while honoring focus, scope, and
|
||||
naming requirements. It gathers the sources with its existing tools, then calls
|
||||
`skill_workshop` to revise a matching pending proposal, update a matching live
|
||||
skill, or create a proposal when neither exists.
|
||||
|
||||
The resulting proposal stays `pending`; `/learn` never applies it. Review and
|
||||
apply it through the normal approval flow or with `openclaw skills workshop`.
|
||||
|
||||
When the actual turn supports only personal publication, including paired-node
|
||||
personal CLI authoring, `/learn` stops without changing a skill. Ask normally
|
||||
for explicit personal creation if you want to publish a revision, or use the
|
||||
existing administrator UI or CLI for Workshop proposal review. Personal turns
|
||||
cannot stage a pending draft.
|
||||
|
||||
Create:
|
||||
|
||||
```text
|
||||
Make a skill called morning-catchup that runs my Monday inbox routine.
|
||||
```
|
||||
|
||||
Update an existing Workshop-generated skill:
|
||||
|
||||
```text
|
||||
Update trip-planning to also check seat maps before booking.
|
||||
```
|
||||
|
||||
If a skill used in the current turn proves wrong or incomplete, the agent reads
|
||||
the live skill and creates a targeted patch proposal. When the complete skill
|
||||
does not fit the selected model's read budget, the agent can prepare one unique
|
||||
exact span and review its bounded surrounding context before patching it. A
|
||||
runtime receipt limits this flow to skills used in that run. Autonomous mode
|
||||
`off` disables repair, `propose` leaves the patch pending until explicitly
|
||||
applied, and `auto` scans and applies it immediately. The repaired skill is
|
||||
loaded by new sessions; the running session keeps its original skill snapshot.
|
||||
|
||||
Iterate on a pending proposal:
|
||||
|
||||
```text
|
||||
Show me the morning-catchup proposal.
|
||||
Revise it to also flag anything marked urgent.
|
||||
Apply the morning-catchup proposal.
|
||||
```
|
||||
|
||||
Agent-initiated `apply`, `reject`, and `quarantine` run without an additional
|
||||
approval prompt by default. Set `skills.workshop.approvalPolicy` to `"pending"`
|
||||
to require operator approval before those actions.
|
||||
|
||||
When approval is required, the prompt identifies the proposal id and target
|
||||
skill, and shows the proposal description, support-file count, and body size.
|
||||
Approval requests are bounded to finish before the agent tool watchdog. If no
|
||||
decision arrives before the prompt expires, the lifecycle action does not run:
|
||||
the proposal stays pending and unchanged. Decide later in the Skill Workshop UI or run
|
||||
`openclaw skills workshop apply|reject|quarantine <proposal-id>`. Agents should
|
||||
not retry an expired lifecycle action in a loop.
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
# Create
|
||||
openclaw skills workshop propose-create \
|
||||
--name morning-catchup \
|
||||
--description "Daily inbox catch-up: triage, archive, surface, draft, plan" \
|
||||
--proposal ./PROPOSAL.md
|
||||
|
||||
# Update an existing Workshop-generated skill
|
||||
openclaw skills workshop propose-update trip-planning --proposal ./PROPOSAL.md
|
||||
|
||||
# List and inspect
|
||||
openclaw skills workshop list
|
||||
openclaw skills workshop inspect <proposal-id>
|
||||
|
||||
# Revise before approval
|
||||
openclaw skills workshop revise <proposal-id> --proposal ./PROPOSAL.md
|
||||
|
||||
# Run installed plugin evaluators against the exact current draft
|
||||
openclaw skills workshop evaluate <proposal-id>
|
||||
|
||||
# Close out
|
||||
openclaw skills workshop apply <proposal-id>
|
||||
openclaw skills workshop reject <proposal-id> --reason "Duplicate"
|
||||
openclaw skills workshop quarantine <proposal-id> --reason "Needs security review"
|
||||
```
|
||||
|
||||
Every subcommand takes `--agent <id>` (agent context; defaults to
|
||||
cwd-inferred, then the default agent) and `--json` (structured output).
|
||||
Proposals and generated skill targets are scoped to the selected agent.
|
||||
`propose-create`, `propose-update`, and `revise` also take `--goal <text>` and
|
||||
`--evidence <text>` to record proposal context alongside `--proposal`.
|
||||
`evaluate` runs through the live Gateway plugin registry, snapshots the current
|
||||
proposal revision before dispatch, and accepts `--correlation-id <id>` for external
|
||||
orchestration.
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
---
|
||||
summary: "Weekly Workshop collection review, its change semantics, and backup recovery"
|
||||
title: "Collection review"
|
||||
read_when:
|
||||
- You are enabling or debugging the weekly collection review
|
||||
- You need to know what a review can change and what it cannot roll back
|
||||
- You are recovering a Workshop skill from a retained collection backup
|
||||
---
|
||||
|
||||
## Collection review
|
||||
|
||||
In `auto` mode, the Gateway maintains one weekly automation per agent. It is
|
||||
a normal isolated agent turn: cron owns scheduling, cancellation, and run
|
||||
history. `propose` and `off` disable these reviews.
|
||||
|
||||
The reviewer reads and edits the agent's Workshop directory with normal file
|
||||
tools. Directory listings are paged to fit the selected model instead of putting
|
||||
every file path into the initial prompt. The reviewer follows each continuation
|
||||
before changing that directory.
|
||||
Skill contents are review material, not active instructions. It keeps useful
|
||||
procedures, simplifies bloated skills, consolidates overlap, and removes obsolete
|
||||
files. Absence of use in the current run never justifies removal. Usage tracking
|
||||
and experience review remain active; weekly cleanup does not receive a separate
|
||||
usage table.
|
||||
|
||||
The file tools stay rooted at the Workshop directory. Shell commands use the
|
||||
operator's existing cron execution and approval policy; enabling review does not
|
||||
grant additional shell access. An approval-required policy can refuse unattended
|
||||
shell commands; a full-access policy permits them. File discovery does not need a shell.
|
||||
|
||||
Reviews support the embedded runtime and CLI runtimes that declare instruction
|
||||
isolation, disable their native tools, and use only the Gateway's restricted
|
||||
OpenClaw tool set, including Claude CLI. These CLI reviews retain the host-selected instruction snapshot;
|
||||
Workshop skill contents remain review material. OpenClaw carries the Workshop
|
||||
file root and prepared sandbox to the mediated tools. Changing the CLI working
|
||||
directory alone does not provide containment.
|
||||
|
||||
Runtimes without those guarantees, including undeclared CLI backends, the Codex harness, and
|
||||
node-placed CLI execution, remain unsupported for rooted reviews. When the configured
|
||||
model chain is provably unsupported, the weekly job remains visible but disabled
|
||||
with `no-rooted-runtime` in its display name. Configure a supported runtime or fallback;
|
||||
Gateway startup and config reload reconcile enablement and clear that reason.
|
||||
Reconciliation uses prepared policy and processes one agent at a time, yielding
|
||||
between updates so fleet-wide review preparation does not block Gateway health requests.
|
||||
Stored session model or runtime preferences are preserved; reviews with these
|
||||
preferences remain enabled because configuration alone cannot prove their execution path.
|
||||
If eligibility cannot be determined statically, the job remains enabled and runtime
|
||||
admission still rejects unsupported execution with an explanation. No runtime is
|
||||
substituted to bypass that check. If an enabled sandbox has
|
||||
`workspaceAccess: "ro"` or `"none"`, the turn refuses to run rather than editing
|
||||
a disposable copy. A writable sandbox uses the agent's Workshop directory.
|
||||
Sandbox backends must support directory reads to provide shell-free discovery.
|
||||
Bundled backends use their existing filesystem permissions for these reads.
|
||||
|
||||
### Changes and recovery
|
||||
|
||||
Collection review follows normal agent file-edit semantics. Completed edits
|
||||
remain if a later step fails or the turn is cancelled. There is no collection-wide
|
||||
transaction, post-turn scanner, automatic rollback, or separate review history
|
||||
writer. This also prevents a failed review from restoring an old tree over
|
||||
concurrent operator edits. Per-skill proposal validation, scanning, and apply
|
||||
behavior described in [How Skill Workshop works](/tools/skill-workshop/how-it-works)
|
||||
are unchanged.
|
||||
|
||||
The reviewer ends with a summary of changes and removal reasons, or why no change
|
||||
was needed. Find it in the automation's run history. Reviews do not announce into
|
||||
a conversation. Future sessions load changed skills; running sessions retain
|
||||
their existing instruction snapshot.
|
||||
|
||||
Existing collection backups are preserved. The `restore_collection` action
|
||||
can restore a retained backup from the previous review implementation, but new
|
||||
reviews do not create collection backups. The `history` action reads those
|
||||
historical review records; current results belong to automation history.
|
||||
Restore refuses to overwrite affected skills changed after that backup.
|
||||
|
||||
### When an older backup cannot be restored automatically
|
||||
|
||||
Restore also refuses when it cannot completely read the included content in a
|
||||
current result tree or its saved original, including content beyond sixteen path
|
||||
components. This leaves the current skill files and retained backup intact. Older
|
||||
backups may contain hashes that omitted deeper files, so deleting that content
|
||||
from the current tree cannot make the backup verifiable. A skill dropped by the
|
||||
cleanup can still be restored to its absent path, including deep support files.
|
||||
Do not edit backup hashes or delete or flatten live files merely to make restore pass.
|
||||
|
||||
For operator-led recovery:
|
||||
|
||||
1. Pause writes to the agent's Workshop, including collection review, before comparing or restoring files.
|
||||
2. Locate the backup under
|
||||
`<agentDir>/skill-workshop/collection-backups/<backup-id>/`.
|
||||
Its `manifest.json` identifies the affected Workshop-relative directories in
|
||||
`skillDirs` and `resultSkillDirs`.
|
||||
3. Create a new private inspection directory outside the Workshop and state
|
||||
directory. Copy the entire backup directory, including `manifest.json` and
|
||||
all saved content, into it. Current backups use `skills/`; history-only imports
|
||||
use `history/workspace/`. Separately copy each existing affected current directory
|
||||
into a `current/` subtree, preserving its Workshop-relative path. Include hidden
|
||||
files and all nested content. If any file cannot be copied, stop rather than use
|
||||
a partial copy.
|
||||
4. Compare the inspection copies to select the intended content. Keep the live
|
||||
tree and original backup unchanged during review, and retain unedited copies
|
||||
of both versions before carrying out any operator-approved recovery.
|
||||
|
||||
Retained legacy backups may instead live under
|
||||
`<state-dir>/skill-workshop/collection-backups/<workspace-hash>/<backup-id>/`
|
||||
and contain a `workspace/` subtree. Preserve that original layout in inspection
|
||||
copies; do not rewrite the manifest to make an old backup look current.
|
||||
|
||||
Doctor retains the legacy source after importing a history-only copy. Once it
|
||||
verifies a matching archive and saved workspace contents in a configured agent's
|
||||
backup directory, later runs stop reporting that backup as pending, even if the
|
||||
original workspace is no longer configured. Missing, incomplete, or mismatched
|
||||
copies still require review; the original source remains intact.
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
---
|
||||
summary: "Self-learning modes and the skills.workshop approval, autonomy, and size settings"
|
||||
title: "Self-learning and approval settings"
|
||||
read_when:
|
||||
- You are configuring Skill Workshop approval, autonomy, or limits
|
||||
- You want to understand where self-learning proposals are reviewed
|
||||
- You are scanning past sessions from the Control UI
|
||||
---
|
||||
|
||||
## Self-learning
|
||||
|
||||
After substantial work, a detached background review can turn corrections and
|
||||
successful procedures into reusable Workshop skills; see
|
||||
[Self-learning](/tools/self-learning). Set `skills.workshop.autonomous.mode` to
|
||||
`propose` to create pending proposals, or to `auto` to maintain complete skills
|
||||
with normal agent tools. The Control UI Workshop tab shows
|
||||
whether self-learning is on; use the config setting to choose all three modes.
|
||||
|
||||
### Scan past sessions
|
||||
|
||||
The Control UI can review older work without enabling autonomous self-learning.
|
||||
Open **Plugins → Workshop** and select **Learn from past conversations**. A normal
|
||||
session opens with the mining instructions, using the selected agent's configured
|
||||
model, permitted tools, existing skills, and accessible conversation history.
|
||||
|
||||
The agent decides what to read and whether the evidence warrants a skill change.
|
||||
It follows the current Workshop mode: `auto` permits direct improvements and
|
||||
`propose` leaves suggestions for approval. No separate scanner selects transcripts
|
||||
or limits the run to a fixed number of conversations or proposals.
|
||||
|
||||
Watch, steer, or stop the work in chat. The session keeps its instructions and
|
||||
outcome instead of separate coverage counters. A manual request does not change
|
||||
the self-learning setting. Normal session capacity, permissions, provider pricing,
|
||||
and data-handling terms apply.
|
||||
|
||||
In `propose` and `auto` modes, OpenClaw can review one finished substantial turn
|
||||
after the agent system becomes idle. It records the finished turn's boundary and
|
||||
reads that turn's model context asynchronously with the same provider and model.
|
||||
Review transcript and session metadata stay detached from foreground work.
|
||||
Incognito turns are excluded from automatic experience review.
|
||||
In `propose` mode, only `skill_workshop` executes and the reviewer can stage one
|
||||
pending mutation. In `auto` mode, ordinary file tools can inspect, edit, and
|
||||
verify several connected files in the Workshop directory. The review inherits
|
||||
source permissions and shell approvals. Its `process` tool cannot control
|
||||
foreground jobs; the Workshop file root is not a shell sandbox.
|
||||
A failed review is recorded after one attempt; completed direct edits remain.
|
||||
|
||||
See [Self-learning](/tools/self-learning) for enablement, eligibility, privacy and cost details,
|
||||
the proposal threshold, and troubleshooting.
|
||||
|
||||
## Approval and autonomy
|
||||
|
||||
```json5
|
||||
{
|
||||
skills: {
|
||||
workshop: {
|
||||
autonomous: {
|
||||
mode: "auto",
|
||||
},
|
||||
approvalPolicy: "auto",
|
||||
maxPending: 50,
|
||||
maxSkillBytes: 40000,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
| Setting | Default | Effect |
|
||||
| ----------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `autonomous.mode` | `"auto"` | `"off"` disables autonomous capture, `"propose"` creates pending proposals, and `"auto"` enables direct per-turn and weekly Workshop maintenance. |
|
||||
| `approvalPolicy` | `"auto"` | `"auto"` skips an additional prompt for agent-initiated `apply`, `reject`, or `quarantine` (the agent still has to call the action). `"pending"` requires approval. |
|
||||
| `maxPending` | `50` | Caps pending and quarantined proposals per agent (1-200). |
|
||||
| `maxSkillBytes` | `40000` | Caps proposal body size in bytes (1024-200000). Autonomous proposals also have a 10,000-character cap; direct maintenance does not use proposal limits. |
|
||||
|
||||
The selected model reviews retained evidence before deciding whether a durable
|
||||
procedure needs an update. Foreground work does not wait for that review. It
|
||||
starts only when the foreground runtime reports its resolved model and actual
|
||||
`skill_workshop` availability; restrictive or unknown tool policy fails closed.
|
||||
|
||||
In `auto` mode, the reviewer uses the same direct-maintenance guidance as weekly
|
||||
review. File tools stay rooted at Workshop; shell commands retain the source
|
||||
session's execution policy. Source deletion, replacement, or permission changes
|
||||
invalidate retained review authority. Direct maintenance does not run a post-turn
|
||||
proposal scanner or create rollback snapshots. Use backups for unwanted edits.
|
||||
|
||||
In `propose` mode, the reviewer can read or prepare an exact span before staging
|
||||
one pending mutation. Existing-skill proposals retain read receipts, content-hash
|
||||
binding, size validation, and normal apply-time scanning and rollback metadata.
|
||||
Immediate foreground repair also retains the normal proposal apply path in
|
||||
`auto` mode; it is separate from direct background maintenance.
|
||||
|
||||
See [Self-learning](/tools/self-learning) for the complete autonomous review behavior and safety
|
||||
model.
|
||||
|
||||
Proposal descriptions are always capped at 160 bytes, independent of
|
||||
`maxSkillBytes`.
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
---
|
||||
summary: "Workshop proposal rules, Control UI review, and the proposal state diagram"
|
||||
title: "How Skill Workshop works"
|
||||
read_when:
|
||||
- You want the proposal rules before applying a generated skill
|
||||
- You are reviewing installed skills or suggestions in the Control UI
|
||||
- You need the proposal state transitions
|
||||
---
|
||||
|
||||
## How it works
|
||||
|
||||
The following lifecycle applies to Workshop proposals. When you ask your agent to
|
||||
change a skill you own (repository skill source, the workspace `skills/`
|
||||
directory, project `.agents/skills/`, or a configured extra skill directory), it
|
||||
edits that file directly; Workshop is for skills the agent captures on its own.
|
||||
Bundled, ClawHub-installed, and plugin-provided skills are replaced by their
|
||||
owners' updates, so the agent says so instead of editing them.
|
||||
|
||||
- **Proposal first:** generated content is stored as `PROPOSAL.md`, not
|
||||
`SKILL.md`.
|
||||
- **Apply is the only live write:** create, update, and revise never change
|
||||
active skills.
|
||||
- **Directory-owned updates:** creates and updates stay inside
|
||||
`<state-dir>/agents/<agentId>/agent/workshop-skills`. A skill is Workshop-owned
|
||||
exactly when it is contained in that agent's directory.
|
||||
- **No clobber:** create fails if the target already exists in that agent's
|
||||
Workshop directory. Skills from other sources are never changed.
|
||||
For same-named skills, [loading order](/tools/skills#loading-order) determines
|
||||
which definition is used.
|
||||
- **Hash bound:** update proposals bind to the current target hash and go
|
||||
`stale` if the live skill changes before apply.
|
||||
- **Scanner gated:** apply reruns the security scanner before writing. Only
|
||||
critical findings block apply; warn-level findings remain visible but do not
|
||||
block it. Prompt-related keywords are not scanner findings: mentioning hidden
|
||||
instructions or tool approval does not establish an instruction override.
|
||||
Literal credential rejection, approval policy, and blocking evaluator decisions
|
||||
remain enforced.
|
||||
- **Recoverable:** apply writes rollback metadata before touching live files.
|
||||
- **Revision atomic:** create and revise flush a complete immutable proposal
|
||||
generation, publish it with an atomic rename, then sync its parent directory
|
||||
where supported before publishing the SQLite record and event together.
|
||||
Process interruption exposes either the complete previous generation or the
|
||||
complete new one.
|
||||
- **Consistent surfaces:** chat, CLI, and Gateway all call the same service.
|
||||
|
||||
## Review in the Control UI
|
||||
|
||||
Open **Plugins → Workshop** and select the agent whose skills you want to inspect.
|
||||
|
||||
- **Skills** opens by default and lists the skills currently installed in that
|
||||
agent's Workshop directory. Skills with instruction changes appear first.
|
||||
Select one to read its complete current instructions. Changed skills show
|
||||
additions and removals inline with all unchanged sections in one scrollable
|
||||
comparison. Unchanged skills show their current instructions as Markdown.
|
||||
Current instructions remain readable while saved versions are compared.
|
||||
- **Suggestions** contains pending proposals that you can evaluate, revise,
|
||||
apply, or reject.
|
||||
|
||||
Action confirmations dismiss automatically. A warning that a suggestion changed
|
||||
stays visible so you can review the updated draft.
|
||||
|
||||
Past applied, rejected, quarantined, and stale proposals remain available through
|
||||
CLI and Gateway inspection. They are not listed as a separate Control UI section
|
||||
or counted as installed skills.
|
||||
|
||||
Comparisons use retained applied versions, not a complete edit timeline.
|
||||
Relative dates identify the saved baseline, not when later edits occurred.
|
||||
Supporting files and frontmatter are not compared. Missing versions are labeled;
|
||||
the complete current instructions remain readable without a saved version.
|
||||
No historical content is reconstructed.
|
||||
|
||||
Removing an installed skill does not remove its proposal history. Reading a
|
||||
historical draft does not restore or reinstall it. Handwritten and externally
|
||||
installed skills remain on their owning skills surfaces.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```text
|
||||
create/update -> pending
|
||||
revise -> pending
|
||||
evaluate -> pending
|
||||
apply -> applied
|
||||
reject -> rejected
|
||||
quarantine -> quarantined
|
||||
target change -> stale
|
||||
```
|
||||
|
||||
Only a `pending` proposal can be revised, applied, rejected, or quarantined.
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
---
|
||||
summary: "Proposal file format, support-file rules, plugin evaluation, and skill_workshop parameters"
|
||||
title: "Proposal content, support files, and the agent tool"
|
||||
read_when:
|
||||
- You are writing PROPOSAL.md or its support files
|
||||
- You are building a plugin evaluator or lifecycle hook
|
||||
- You need the skill_workshop action and parameter reference
|
||||
---
|
||||
|
||||
## Plugin evaluation and lifecycle hooks
|
||||
|
||||
Gateway plugins can extend Skill Workshop without owning proposal storage or
|
||||
live skill writes:
|
||||
|
||||
- `skill_proposal_evaluate` receives an exact candidate bundle and, for update
|
||||
proposals, the complete baseline skill. It returns attributed findings,
|
||||
metrics, and an optional `pass`, `revise`, or `block` decision.
|
||||
- `skill_proposal_changed` observes durable `created`, `revised`,
|
||||
`evaluation_completed`, `applied`, `rejected`, `quarantined`, and `stale`
|
||||
events.
|
||||
- `skill_changed` observes committed live skill `created`, `updated`, and
|
||||
`removed` events from Workshop and supported install/uninstall paths.
|
||||
|
||||
Evaluations are explicit from the CLI, Control UI, Gateway
|
||||
`skills.proposals.evaluate` method, or agent `skill_workshop` action. Results
|
||||
are stored on the exact proposal revision and in the append-only proposal event
|
||||
ledger. Evaluator failures remain attributed results; only a completed
|
||||
`decision: "block"` prevents apply. Apply also revalidates the evaluated target
|
||||
tree, so any live skill asset drift requires a fresh evaluation.
|
||||
|
||||
The lifecycle supports external optimization loops without embedding one.
|
||||
Controllers can consume `skills.proposals.events.list`, evaluate an exact
|
||||
`revisionHash`, revise with `expectedRevisionHash` and `correlationId`, then continue
|
||||
from the returned event sequence. OpenClaw does not schedule, auto-revise, or
|
||||
decide when such a loop should stop.
|
||||
|
||||
## Proposal content
|
||||
|
||||
While pending, the proposal is stored as `PROPOSAL.md` with proposal-only
|
||||
frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: "morning-catchup"
|
||||
description: "Daily inbox catch-up: triage, archive, surface, draft, plan"
|
||||
status: proposal
|
||||
version: "v1"
|
||||
date: "2026-05-30T00:00:00.000Z"
|
||||
---
|
||||
```
|
||||
|
||||
On apply, Skill Workshop writes the active `SKILL.md` and removes the
|
||||
proposal-only fields: `status`, proposal `version`, and proposal `date`.
|
||||
|
||||
## Support files
|
||||
|
||||
Use `--proposal-dir` when the proposed skill needs files beside
|
||||
`PROPOSAL.md`:
|
||||
|
||||
```bash
|
||||
openclaw skills workshop propose-create \
|
||||
--name weekly-update \
|
||||
--description "Friday wrap-up: stats, highlights, next week's top three" \
|
||||
--proposal-dir ./weekly-update-proposal
|
||||
```
|
||||
|
||||
The directory must contain `PROPOSAL.md`. Support files must live under
|
||||
`assets/`, `examples/`, `references/`, `scripts/`, or `templates/`. Skill
|
||||
Workshop scans, hashes, and stores them with the proposal, then writes them
|
||||
beside the live `SKILL.md` only on apply.
|
||||
|
||||
Rejected support-file paths: absolute paths, hidden path segments, path
|
||||
traversal, overlapping paths, executable files, non-UTF-8 text, null bytes,
|
||||
and paths outside the standard support folders.
|
||||
|
||||
Directory drafts must be completely readable and fit within eight path
|
||||
components, including the filename. Evaluator bundles require all included target
|
||||
content to be readable and within sixteen path components. Root `.clawhub`,
|
||||
`.clawdhub`, and `.openclaw` metadata entries are excluded; those names nested
|
||||
elsewhere remain included. Unreadable included directories or deeper content
|
||||
produce an error. Fix the reported directory or reduce its nesting, then retry.
|
||||
For a collection restore failure, follow the
|
||||
[manual recovery guidance](/tools/skill-workshop/collection-review#when-an-older-backup-cannot-be-restored-automatically)
|
||||
instead of restructuring the live tree.
|
||||
|
||||
## Agent tool
|
||||
|
||||
For personal library operations, `skill_workshop` exposes
|
||||
`list | read | create | update | share | unshare | transfer | activate | remove | rollback`.
|
||||
The Gateway chooses the authorized namespace. When Workshop authoring is also
|
||||
available, `target: "personal"` selects the personal library. Reads return a
|
||||
stable skill ID and revision. Personal `list` takes only `action` and `target`;
|
||||
it returns up to 20 entries and points to My skills for the complete library.
|
||||
Use an entry's `skillId` as `skill_id` for `read`, not its name or slug.
|
||||
Personal operations do not accept Workshop-only fields such as `query`, `limit`,
|
||||
or `skill_name`, or proposal actions such as `inspect` and `prepare_patch`.
|
||||
Updates require `skill_id` and `expected_revision`;
|
||||
omit `proposal_content` to preserve the instructions. Use `files` for named
|
||||
support-file upserts and `delete_files` for explicit removals. Unmentioned
|
||||
support files are preserved. Large instructions are returned whole or explicitly
|
||||
omitted with directions to the operator workflow; binary supporting content is
|
||||
not injected into model context.
|
||||
|
||||
For Workshop proposals, omit `target`. The tool uses one required `action`:
|
||||
`create | read | prepare_patch | patch | update | revise | list | inspect | evaluate | apply | reject | quarantine | history | restore_collection`.
|
||||
Other Workshop parameters apply depending on the action:
|
||||
|
||||
| Parameter | Used by | Notes |
|
||||
| -------------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `name` | `create`, `inspect`, `revise` | Required for `create`; resolves a pending proposal by name otherwise |
|
||||
| `description` | `create`, `update`, `revise` | Max 160 bytes |
|
||||
| `skill_name` | `read`, `prepare_patch`, `patch`, `update` | Existing skill name or key |
|
||||
| `old_string` | `prepare_patch`, `patch` | Exact current text; prepare it when the complete skill cannot be read |
|
||||
| `new_string` | `patch` | Replacement for the exact current text |
|
||||
| `proposal_content` | `create`, `update`, `revise` | Required for create/update; omit on revise to preserve the body |
|
||||
| `support_files` | `create`, `update`, `revise` | Array of `{ path, content }` |
|
||||
| `goal`, `evidence` | `create`, `update`, `revise` | Free-text context |
|
||||
| `proposal_id` | `inspect`, `revise`, `evaluate`, `apply`, `reject`, `quarantine` | Target proposal |
|
||||
| `artifact_path` | `inspect` | `PROPOSAL.md` or one listed support-file path |
|
||||
| `expected_revision_hash` | `evaluate`, `apply`, `reject`, `quarantine` | Rejects a stale orchestration step |
|
||||
| `correlation_id` | `evaluate`, `revise`, `apply`, `reject`, `quarantine` | External run or experiment correlation |
|
||||
| `reason` | `apply`, `reject`, `quarantine` | Optional |
|
||||
| `query`, `status`, `limit` | `list` | Filter/paginate; `limit` max 50, default 20 |
|
||||
|
||||
`read` and `prepare_patch` return the resolved `skillName`. Reuse that name as
|
||||
`skill_name` in follow-up calls; a metadata `skillKey` can match a different
|
||||
skill's exact name. Update proposals and revisions preserve the existing skill's
|
||||
frontmatter name.
|
||||
|
||||
Only one prepared patch span may be active per skill. A second
|
||||
`prepare_patch` is rejected until a `patch` attempt consumes or invalidates the
|
||||
active authorization.
|
||||
|
||||
`inspect` returns proposal metadata, a bounded artifact manifest, and one
|
||||
complete artifact when it fits the selected model's context budget. It selects
|
||||
`PROPOSAL.md` by default. Set `artifact_path` to read one support file
|
||||
separately. When the selected artifact does not fit, the result omits its body,
|
||||
reports the original size, and points to smaller per-artifact reads or the
|
||||
unbounded operator [CLI command](/tools/skill-workshop/authoring#cli).
|
||||
|
||||
Agents must use `skill_workshop` for generated skill work and must not create or
|
||||
change skill or proposal files directly during foreground authoring. Automatic
|
||||
background maintenance uses the rooted file-tool path described in
|
||||
[Self-learning and approval settings](/tools/skill-workshop/configuration) instead.
|
||||
The foreground rule is advisory and prompt-enforced. The tool-policy seam does
|
||||
not enforce it.
|
||||
|
||||
<Note>
|
||||
`skill_workshop` is a built-in agent tool and is included in
|
||||
`tools.profile: "coding"`. If a stricter policy hides it, add
|
||||
`skill_workshop` to the active `tools.allow` list, or use
|
||||
`tools.alsoAllow: ["skill_workshop"]` when the scope uses a profile without an
|
||||
explicit `tools.allow`. Sandboxed runs do not construct the host-side
|
||||
Workshop proposal tool. When an authorized personal-library capability is
|
||||
available, sandbox and cloud runs use its Gateway-backed authoring surface
|
||||
instead; the library and database are not mounted writable into the worker.
|
||||
Use a normal host-side session or the CLI for Workshop proposal review.
|
||||
</Note>
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
---
|
||||
summary: "Skill Workshop Gateway methods, on-disk storage layout, migration, and hard limits"
|
||||
title: "Gateway methods, storage, and limits"
|
||||
read_when:
|
||||
- You are calling the skills.proposals.* or skills.workshop.* Gateway methods
|
||||
- You need the Workshop on-disk layout or its migration behavior
|
||||
- You are hitting a Workshop size or count limit
|
||||
---
|
||||
|
||||
## Gateway methods
|
||||
|
||||
| Method | Scope |
|
||||
| ---------------------------------- | ---------------- |
|
||||
| `skills.proposals.list` | `operator.read` |
|
||||
| `skills.workshop.read` | `operator.read` |
|
||||
| `skills.proposals.inspect` | `operator.read` |
|
||||
| `skills.proposals.historyStatus` | `operator.read` |
|
||||
| `skills.proposals.historyScan` | `operator.admin` |
|
||||
| `skills.proposals.create` | `operator.admin` |
|
||||
| `skills.proposals.update` | `operator.admin` |
|
||||
| `skills.proposals.revise` | `operator.admin` |
|
||||
| `skills.proposals.requestRevision` | `operator.admin` |
|
||||
| `skills.proposals.apply` | `operator.admin` |
|
||||
| `skills.proposals.reject` | `operator.admin` |
|
||||
| `skills.proposals.quarantine` | `operator.admin` |
|
||||
| `skills.curator.status` | `operator.read` |
|
||||
| `skills.curator.pin` | `operator.admin` |
|
||||
| `skills.curator.unpin` | `operator.admin` |
|
||||
| `skills.curator.restore` | `operator.admin` |
|
||||
|
||||
`skills.proposals.list` includes `installedSkills`, the current Workshop inventory
|
||||
for the selected agent. Each entry contains `name`, `skillKey`, and `description`.
|
||||
The separate `proposals` array remains the proposal history and pending queue.
|
||||
|
||||
`skills.workshop.read` accepts `name` and optional `agentId`. It returns the
|
||||
current installed skill's `name`, `skillKey`, `description`, and complete `content`.
|
||||
An unknown agent or a skill outside that agent's Workshop inventory returns an
|
||||
error. It never reads a retained proposal as a substitute for a missing skill.
|
||||
|
||||
### Workshop inventory and usage
|
||||
|
||||
`skills.curator.status` reports live skill usage recorded from trusted
|
||||
`skill.used` events, retained pre-cron collection review records, and per-workspace
|
||||
experience review outcomes. Current collection reviews use automation run history.
|
||||
No skill is archived or expired by age. Every skill reports as `active`.
|
||||
|
||||
Clients that advertise the `skill-curator-live-inventory` connection capability
|
||||
receive `inventory: "live-workshop"`. This view discovers current skills in each
|
||||
configured agent's Workshop root, including custom agent directories and directly
|
||||
created skills with no proposal history. Disabled skills and skills filtered from
|
||||
an agent's prompt remain inventory members. Discovery keeps the existing loader's
|
||||
file-size, per-source count, and link limits. Invalid skills are excluded; a read
|
||||
failure is an error, not an empty inventory.
|
||||
|
||||
Membership and names come from current files. Usage joins by canonical absolute
|
||||
file path, not skill name. Renaming metadata at the same path retains usage;
|
||||
moving a file does not transfer its history. Removed roots, agents, and files do
|
||||
not return through retained proposals. Applied create proposals supply the
|
||||
earliest known creation date only. Without that history, `createdAtMs` and
|
||||
`stateChangedAtMs` are `null`; filesystem dates are not substitutes.
|
||||
|
||||
A zero `useCount` or null `lastUsedAtMs` means use is not recorded, not that the
|
||||
skill was never used. Tracking includes successful known skill reads through
|
||||
OpenClaw tools registered with the Codex dynamic-tool bridge and explicit
|
||||
tool-dispatched skill commands. It does not infer native Codex skill activation
|
||||
or reads made outside the OpenClaw tool boundary. There is no historical backfill.
|
||||
|
||||
During upgrades, clients without the capability receive the unchanged legacy
|
||||
response shape: no marker, numeric creation dates, and only current entries whose
|
||||
dates are known. Counts describe that returned subset. New clients also accept
|
||||
older Gateways' unmarked responses. The legacy view has limited coverage and must
|
||||
not be treated as a complete inventory or used to infer inactivity.
|
||||
|
||||
`skills.curator.pin`, `skills.curator.unpin`, and `skills.curator.restore` remain
|
||||
registered for existing clients, but always return an error explaining that the
|
||||
weekly collection review manages the skill collection.
|
||||
|
||||
### Revision and history methods
|
||||
|
||||
`requestRevision` is Gateway-only (no CLI or agent-tool equivalent): it
|
||||
forwards free-text revision instructions to the owning agent's chat session
|
||||
instead of replacing `PROPOSAL.md` directly, for UIs that ask the agent to
|
||||
revise rather than submit literal new content.
|
||||
|
||||
`historyStatus` and `historyScan` remain registered for existing clients but
|
||||
return an error explaining that historical batch scans are retired. They do not
|
||||
start a run or change skills.
|
||||
|
||||
In Workshop, **Learn from past conversations** creates and opens a normal agent
|
||||
session through `sessions.create`. Its opening message asks the agent to inspect
|
||||
existing skills, choose useful conversations, and follow the current learning
|
||||
mode. The chat shows the work and its outcome; there is no separate scan progress
|
||||
store. See [Self-learning](/tools/self-learning#cost-and-privacy) for model usage
|
||||
and privacy.
|
||||
|
||||
## Storage
|
||||
|
||||
```text
|
||||
<state-dir>/
|
||||
state/openclaw.sqlite
|
||||
agents/<agentId>/
|
||||
agent/workshop-skills/<skill-name>/
|
||||
SKILL.md
|
||||
assets/
|
||||
examples/
|
||||
references/
|
||||
scripts/
|
||||
templates/
|
||||
skill-workshop/proposals/<proposal-id>/
|
||||
generations/<generation-id>/
|
||||
PROPOSAL.md
|
||||
assets/
|
||||
examples/
|
||||
references/
|
||||
scripts/
|
||||
templates/
|
||||
```
|
||||
|
||||
Unless overridden, `<state-dir>` is `~/.openclaw`.
|
||||
|
||||
- `state/openclaw.sqlite`: canonical proposal records and provenance, the active
|
||||
generation reference, proposal status, recorded skill usage, collection and
|
||||
experience review outcomes, and apply rollback metadata.
|
||||
- Each generation contains one `PROPOSAL.md` and all of that revision's support
|
||||
files. Revision publication never overwrites the active generation in place.
|
||||
- Generation files are flushed before publication. After the complete bundle is
|
||||
renamed into place, OpenClaw syncs the `generations/` parent directory where
|
||||
the platform supports directory flushing, before committing SQLite state.
|
||||
Platforms that report directory synchronization as unsupported retain atomic
|
||||
rename and process-interruption safety, but do not claim power-loss durability
|
||||
for that directory entry.
|
||||
- Support files remain beside their generation's `PROPOSAL.md` so operators can
|
||||
review the proposed skill as a normal directory.
|
||||
|
||||
Proposals created by older releases can still reference the earlier root-level
|
||||
`PROPOSAL.md` layout. The stored record identifies that bundle directly; the
|
||||
next successful revision moves the proposal onto the generation layout and
|
||||
retires the previous bundle.
|
||||
|
||||
Startup and `openclaw doctor --fix` use the same Workshop migration. It imports
|
||||
the previous `proposals.json`, `proposal.json`, and `rollback.json` metadata into
|
||||
SQLite after verifying each proposal, then removes the migrated JSON files.
|
||||
It moves applied legacy Workshop creates into `workshop-skills`, retargets
|
||||
eligible pending creates, and marks outside updates stale before normal use.
|
||||
Pending updates follow their relocated skill in the same database commit.
|
||||
Ownership-only moves preserve the proposal's existing edit time.
|
||||
Interrupted moves resume without discarding those pending updates.
|
||||
If older workspace setup files remain, run `openclaw doctor --fix`.
|
||||
Startup defers the affected skill moves and backup conversion until Doctor
|
||||
has imported that workspace state.
|
||||
The migration infers each legacy proposal's owner from its row, origin metadata,
|
||||
or a unique workspace owner. Ambiguous ownership, or an owner that is no
|
||||
longer in the agent roster, stays in place and becomes stale.
|
||||
Legacy collection backups move under the owner agent's backup root together
|
||||
with their post-cleanup snapshot. A dropped skill remains restorable when its
|
||||
saved review and create proposal prove its owner, original path, and backup.
|
||||
Backups without enough ownership evidence remain history-only; their legacy
|
||||
files stay in place, and restore reports why it cannot use them. Completed
|
||||
history archives do not block migration of the remaining backups.
|
||||
If cleanup stops after publishing a restorable backup, the next migration
|
||||
verifies the saved manifest and all copied files before removing the old copy.
|
||||
If no configured agent uses a legacy backup's recorded workspace, Doctor checks
|
||||
its post-cleanup result hashes against every configured agent's Workshop skills.
|
||||
Exactly one complete match identifies the owner even when the old workspace is
|
||||
gone. Empty, partial, changed, or ambiguous matches stay preserved; Doctor names
|
||||
the agents considered and their verification results. Follow the
|
||||
[manual backup recovery procedure](/tools/skill-workshop/collection-review#when-an-older-backup-cannot-be-restored-automatically)
|
||||
without changing the current workspace or rewriting the backup manifest.
|
||||
After a successful repair, restart the Gateway to clear its startup warning.
|
||||
Skills that were symlinked into a workspace stay where they are as workspace
|
||||
skills; the migration marks their proposals stale instead of moving them.
|
||||
|
||||
Doctor also checks saved automation command arguments, working directories,
|
||||
condition scripts, and agent messages for literal references to relocated
|
||||
Workshop skills. It names each affected automation and field during Doctor
|
||||
repair and on later checks. Retained apply history must establish the original
|
||||
skill directory; Doctor does not guess it from the current workspace or skill
|
||||
name when that history is unavailable.
|
||||
|
||||
Review each reported field before updating the automation. Doctor offers a
|
||||
replacement for a complete argument or working-directory path only when the
|
||||
mapped target exists inside the relocated skill. For paths embedded in scripts
|
||||
or messages, it reports the directory relocation separately and leaves the
|
||||
complete target unresolved for manual review. Missing or ambiguous targets also
|
||||
stay unresolved. Doctor does not rewrite automation content, change schedules,
|
||||
or run the automation to check it.
|
||||
|
||||
If moving the skills empties a workspace, migration retires obsolete
|
||||
workspace-survival evidence only when saved pre-move facts prove that the same
|
||||
directory contained only those skills and every moved file is intact.
|
||||
Missing or replaced workspaces, ordinary project files, and newer workspace
|
||||
attestations keep their protection.
|
||||
|
||||
If a proposal's draft is missing, Suggestions marks it unavailable. You can
|
||||
reject it, but cannot apply, evaluate, or revise content that is no longer there.
|
||||
Run `openclaw doctor --fix` to mark these proposals stale and remove them from
|
||||
actionable Suggestions. Doctor preserves their metadata and remaining files.
|
||||
If a proposal has unfinished apply recovery, Reject and Quarantine refuse to
|
||||
dismiss it. Doctor leaves it pending and asks you to restore the draft before
|
||||
retrying; it does not discard rollback evidence
|
||||
or change the installed skill.
|
||||
|
||||
## Limits
|
||||
|
||||
| Limit | Value |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| Description | 160 bytes |
|
||||
| Proposal body | `skills.workshop.maxSkillBytes` (default 40,000; hard ceiling 200,000 bytes) |
|
||||
| Autonomous proposal `SKILL.md` | 10,000 characters, or strictly shorter when already over the cap |
|
||||
| Support files | 64 per proposal |
|
||||
| Support file size | 256 KiB each, 2 MiB total |
|
||||
| Pending + quarantined proposals | `skills.workshop.maxPending` per agent (default 50) |
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
summary: "Common Skill Workshop errors and the tool-policy doctor check"
|
||||
title: "Skill Workshop troubleshooting"
|
||||
read_when:
|
||||
- A Workshop proposal fails to create, apply, or appear in the list
|
||||
- The agent cannot call skill_workshop
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Resolution |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Skill proposal description is too large` | Shorten `description` to 160 bytes or less. |
|
||||
| `Skill proposal content is too large` | Shorten the proposal body or raise `skills.workshop.maxSkillBytes`. |
|
||||
| `Target skill changed after proposal creation` | Revise the proposal against the current target, or create a new proposal. |
|
||||
| `Proposal scan failed` | Inspect scanner findings, then revise or quarantine the proposal. |
|
||||
| `Support file paths must be under one of...` | Move support files under `assets/`, `examples/`, `references/`, `scripts/`, or `templates/`. |
|
||||
| Proposal does not show in list | Check the selected agent and `OPENCLAW_STATE_DIR`. |
|
||||
| Agent cannot call `skill_workshop` | Check the active tool policy and run mode. `coding` includes the tool; restrictive `tools.allow` policies must list it explicitly, and sandboxed runs must use a normal host-side agent session or the CLI. |
|
||||
|
||||
### Legacy ownership warnings during an update
|
||||
|
||||
Doctor discovers historical workspace setup files from retained Workshop proposals
|
||||
and collection backups, even when the agent now uses a different workspace. It
|
||||
imports valid setup history before relocating Workshop records, preserving the
|
||||
original timestamps and an archived copy without changing workspace configuration.
|
||||
|
||||
If an unconfigured historical workspace has missing or unreadable setup state,
|
||||
Doctor preserves its files and reports a warning. Obsolete proposals become stale;
|
||||
proposals with unfinished apply recovery and ambiguous collection backups remain
|
||||
available for manual review. Configured workspace failures and failures after an
|
||||
import has begun still block repair.
|
||||
|
||||
Doctor leaves legacy proposal metadata and collection backups in place when
|
||||
their workspace has no configured owner or maps to more than one agent. The
|
||||
warning names the retained path and candidate agents. These ownership warnings
|
||||
do not stop the other migrations or later Doctor repairs.
|
||||
|
||||
Review the retained proposal or backup manifest alongside the configured agent
|
||||
workspaces. Correct a workspace mapping only when it identifies the actual
|
||||
owner; do not assign an arbitrary agent or delete the artifacts to clear the
|
||||
warning. Rerun Doctor after resolving ownership. Outside the historical-workspace
|
||||
deferrals above, invalid metadata, failed writes, and unfinished recovery remain
|
||||
migration failures.
|
||||
|
||||
A retarget count describes proposals changed during that pass. Any remaining
|
||||
external targets can belong to different proposals whose migration is blocked.
|
||||
Review their identifiers, paths, and migration warnings before retrying the same
|
||||
repair.
|
||||
|
||||
Package rollback does not reverse the state-schema migration or relocated skill
|
||||
files. An older package can therefore be incompatible with the retained state.
|
||||
See [Schema bumps and older updaters](/reference/database-schemas#schema-bumps-and-older-updaters)
|
||||
and [Downgrade recovery](/reference/database-schemas#downgrade-recovery).
|
||||
|
||||
### Tool-policy diagnostic
|
||||
|
||||
In `propose` and `auto` modes, `openclaw doctor` runs the
|
||||
`core/doctor/skill-workshop-tool-policy` check for each configured agent. Doctor
|
||||
checks the sandbox construction gate before tool policy: sandboxed runs without
|
||||
host-granted library-authoring authority do not construct `skill_workshop`, even
|
||||
when `alsoAllow` lists it. The warning names the effective sandbox setting and
|
||||
recommends a non-sandboxed session, a human turn with library-authoring authority,
|
||||
or the `openclaw skills workshop` CLI. For `non-main` mode, this restriction applies
|
||||
to non-main sessions. Library authority is granted by the host for an authenticated
|
||||
human turn; it is not an allowlist entry or a permission autonomous reviews can
|
||||
grant themselves. See [Personal library authoring](/tools/skill-workshop/personal-library).
|
||||
|
||||
When the tool can be constructed but policy hides it, the warning names the first
|
||||
excluding config layer and the exact `allow` or `alsoAllow` change to make. A run
|
||||
whose explicit allowlist leaves no callable tools also names the sandbox gate
|
||||
when it excludes the requested Workshop tool. Older runbooks may still use
|
||||
`openclaw plugins inspect skill-workshop`; that command now explains that Skill
|
||||
Workshop is built in and prints the same policy hint when applicable.
|
||||
|
|
@ -5,7 +5,7 @@ summary: "Full reference for the skills.* config schema, agent allowlists, works
|
|||
read_when:
|
||||
- Configuring skill loading, install, or gating behavior
|
||||
- Setting per-agent skill visibility
|
||||
- Adjusting Skill Workshop limits or approval policy
|
||||
- Adjusting Skill Workshop learning mode or size limits
|
||||
---
|
||||
|
||||
Most skills configuration lives under `skills` in
|
||||
|
|
@ -28,8 +28,6 @@ Most skills configuration lives under `skills` in
|
|||
},
|
||||
workshop: {
|
||||
autonomous: { mode: "auto" },
|
||||
approvalPolicy: "auto",
|
||||
maxPending: 50,
|
||||
maxSkillBytes: 40000,
|
||||
},
|
||||
entries: {
|
||||
|
|
@ -365,36 +363,31 @@ different visible skill set per agent.
|
|||
|
||||
## Workshop (`skills.workshop`)
|
||||
|
||||
<ParamField path="skills.workshop.autonomous.mode" type='"off" | "propose" | "auto"' default='"auto"'>
|
||||
`off` disables autonomous capture while keeping the durable-instruction
|
||||
suggestion nudge. `propose` creates pending proposals from corrections and
|
||||
substantial completed work. `auto` uses normal agent tools for direct per-turn
|
||||
and weekly Workshop maintenance, without proposal scanning or automatic rollback
|
||||
snapshots. Immediate foreground repairs still use scanner-gated proposal apply.
|
||||
User-prompted skill creation,
|
||||
`/learn`, and manual learning sessions continue to work in every mode.
|
||||
<ParamField path="skills.workshop.autonomous.mode" type='"off" | "auto"' default='"auto"'>
|
||||
`auto` lets agents save and update Workshop skills: a background review runs
|
||||
after substantial work, and learned skills unused for 30 days are archived.
|
||||
Review changes are announced in the conversation, and every change can be
|
||||
undone. `off` disables the background review and unused-skill cleanup.
|
||||
User-prompted skill creation, `/learn`, and manual learning sessions work in
|
||||
both modes.
|
||||
</ParamField>
|
||||
|
||||
See [Self-learning](/tools/self-learning) for eligibility, privacy, cost,
|
||||
proposal-only permissions, and troubleshooting.
|
||||
|
||||
<ParamField path="skills.workshop.approvalPolicy" type='"pending" | "auto"' default='"auto"'>
|
||||
`auto` allows agent-initiated apply, reject, or quarantine without an
|
||||
additional approval prompt. `pending` requires operator approval.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="skills.workshop.maxPending" type="number" default="50">
|
||||
Maximum pending and quarantined proposals retained per agent (allowed
|
||||
range: 1-200).
|
||||
</ParamField>
|
||||
and troubleshooting.
|
||||
|
||||
<ParamField path="skills.workshop.maxSkillBytes" type="number" default="40000">
|
||||
Maximum proposal body size in bytes (allowed range: 1024-200000). Proposal
|
||||
descriptions are hard-capped at 160 bytes separately, because they appear
|
||||
in discovery and listing output.
|
||||
Maximum `SKILL.md` size in bytes for Workshop skills (allowed range:
|
||||
1024-200000). Skill descriptions are capped at 1024 bytes separately; keep
|
||||
them near 160 bytes because they appear in discovery and listing output.
|
||||
</ParamField>
|
||||
|
||||
See [Skill Workshop](/tools/skill-workshop) for the proposal lifecycle, CLI
|
||||
`openclaw doctor --fix` migrates configs from the removed proposal flow: it
|
||||
changes `autonomous.mode: "propose"` to `"off"` and deletes
|
||||
`skills.workshop.approvalPolicy` and `skills.workshop.maxPending`. Run
|
||||
`openclaw config set skills.workshop.autonomous.mode auto` to turn automatic
|
||||
learning back on.
|
||||
|
||||
See [Skill Workshop](/tools/skill-workshop) for the skill lifecycle, CLI
|
||||
commands, agent tool parameters, and Gateway methods this config controls.
|
||||
|
||||
## Symlinked skill roots
|
||||
|
|
@ -478,10 +471,10 @@ for when changes become visible.
|
|||
Authoring custom workspace skills.
|
||||
</Card>
|
||||
<Card title="Skill Workshop" href="/tools/skill-workshop" icon="flask">
|
||||
Proposal queue for agent-drafted skills.
|
||||
Agent-learned skills, change history, and undo.
|
||||
</Card>
|
||||
<Card title="Self-learning" href="/tools/self-learning" icon="brain">
|
||||
Conservative, opt-in proposals from completed work.
|
||||
Automatic, undoable skill learning from completed work.
|
||||
</Card>
|
||||
<Card title="Slash commands" href="/tools/slash-commands" icon="terminal">
|
||||
Native slash-command catalog and chat directives.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ binary presence.
|
|||
Build and test a custom skill from scratch.
|
||||
</Card>
|
||||
<Card title="Skill Workshop" href="/tools/skill-workshop" icon="flask">
|
||||
Review and approve agent-drafted skill proposals.
|
||||
Skills your agent learns on its own, with change history and undo.
|
||||
</Card>
|
||||
<Card title="Skills config" href="/tools/skills-config" icon="gear">
|
||||
Full `skills.*` config schema and agent allowlists.
|
||||
|
|
@ -314,22 +314,22 @@ skill from model-initiated selection.
|
|||
|
||||
## Skill Workshop
|
||||
|
||||
[Skill Workshop](/tools/skill-workshop) is a proposal queue between the agent
|
||||
and its `<state-dir>/agents/<agentId>/agent/workshop-skills` directory. When the agent spots
|
||||
reusable work, it drafts a proposal instead of writing directly to `SKILL.md`.
|
||||
The scheduled weekly collection review is the scoped exception: its normal
|
||||
isolated turn may edit `SKILL.md` files directly inside the Workshop directory.
|
||||
Operators edit skills outside that directory through their owning tools or files.
|
||||
[Skill Workshop](/tools/skill-workshop) lets an agent save and update its own
|
||||
skills in `<state-dir>/agents/<agentId>/agent/workshop-skills`. Every change
|
||||
applies immediately, is announced, and saves the previous version so it can be
|
||||
undone. Learned skills are always visible to their agent; archive one to hide
|
||||
it. Operators edit skills outside that directory through their owning tools or
|
||||
files.
|
||||
|
||||
```bash
|
||||
openclaw skills workshop list
|
||||
openclaw skills workshop inspect <proposal-id>
|
||||
openclaw skills workshop evaluate <proposal-id>
|
||||
openclaw skills workshop apply <proposal-id>
|
||||
openclaw skills workshop changes
|
||||
openclaw skills workshop archive <name>
|
||||
openclaw skills workshop restore <name>
|
||||
```
|
||||
|
||||
See [Skill Workshop](/tools/skill-workshop) for the full lifecycle, CLI
|
||||
reference, and configuration.
|
||||
See [Skill Workshop](/tools/skill-workshop) for how agents learn skills and
|
||||
[`openclaw skills workshop`](/cli/skills#skill-workshop) for the CLI reference.
|
||||
|
||||
## Installing from ClawHub
|
||||
|
||||
|
|
@ -978,7 +978,7 @@ read every admitted skill. Native harnesses retain their own prompt policy.
|
|||
Step-by-step guide to authoring a custom skill.
|
||||
</Card>
|
||||
<Card title="Skill Workshop" href="/tools/skill-workshop" icon="flask">
|
||||
Proposal queue for agent-drafted skills.
|
||||
Skills your agent saves and updates on its own, with undo.
|
||||
</Card>
|
||||
<Card title="Skills config" href="/tools/skills-config" icon="gear">
|
||||
Full `skills.*` config schema and agent allowlists.
|
||||
|
|
|
|||
|
|
@ -281,8 +281,8 @@ Copy controls. Reading a bundle requires `operator.read`.
|
|||
|
||||
The **Skills** tab keeps the skill status report, enable/disable toggles, API
|
||||
key entry, and inline ClawHub skill search, scoped to the selected agent. The
|
||||
**Skill workshop** tab shows installed skills and pending
|
||||
[skill proposals](/tools/skill-workshop). **Learn from past conversations** opens
|
||||
**Skill workshop** tab shows [learned skills](/tools/skill-workshop) with their
|
||||
use counts and recent changes with **Undo**. **Learn from past conversations** opens
|
||||
a normal session with the selected agent's configured model and permitted tools.
|
||||
The agent chooses which history and skills to inspect, following the current
|
||||
Workshop mode. Chat shows progress, results, and normal stop and follow-up controls.
|
||||
|
|
|
|||
|
|
@ -177,7 +177,6 @@ export function buildAnthropicCliBackend(
|
|||
nativeToolMode: "selectable",
|
||||
hostOwnedTools: ["exec", "process"],
|
||||
toolAvailabilityEnforcement: "execution-args",
|
||||
isolatesInstructionsWithExactTools: true,
|
||||
projectNativeToolAuthority: projectClaudeNativeToolAuthority,
|
||||
sideQuestionToolMode: "disabled",
|
||||
ownsNativeCompaction: true,
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import { describe, expect, it } from "vitest";
|
|||
import { buildAnthropicCliBackend } from "./cli-backend.js";
|
||||
|
||||
describe("Claude CLI instruction isolation", () => {
|
||||
it.each([false, true])("isolates declared exact-tool execution (resume=%s)", (useResume) => {
|
||||
it.each([false, true])("isolates exact-tool execution (resume=%s)", (useResume) => {
|
||||
const backend = buildAnthropicCliBackend();
|
||||
expect(backend.isolatesInstructionsWithExactTools).toBe(true);
|
||||
expect(
|
||||
backend.resolveExecutionArgs?.({
|
||||
workspaceDir: "/tmp",
|
||||
|
|
|
|||
|
|
@ -1100,24 +1100,39 @@ describe("runCodexAppServerSideQuestion", () => {
|
|||
},
|
||||
);
|
||||
|
||||
it("disables hosted search when side-question sender policy removes managed web_search", async () => {
|
||||
createOpenClawCodingToolsMock.mockReturnValue([]);
|
||||
it.each([
|
||||
{ senderId: "restricted-sender", webSearchMode: "disabled" },
|
||||
{ senderId: "allowed-sender", webSearchMode: "cached" },
|
||||
])(
|
||||
"applies side-question search policy for $senderId without managed search",
|
||||
async (testCase) => {
|
||||
// Missing managed credentials are not a denial; hosted search uses the policy owner.
|
||||
createOpenClawCodingToolsMock.mockReturnValue([]);
|
||||
|
||||
const { forkConfig } = await runSideQuestionWithManagedWebSearchCall(
|
||||
sideParams({
|
||||
senderId: "restricted-sender",
|
||||
cfg: {
|
||||
tools: { toolsBySender: { "id:restricted-sender": { deny: ["web_search"] } } },
|
||||
},
|
||||
}),
|
||||
{ preserveToolFactory: true },
|
||||
);
|
||||
const { forkConfig, toolResponse } = await runSideQuestionWithManagedWebSearchCall(
|
||||
sideParams({
|
||||
senderId: testCase.senderId,
|
||||
cfg: {
|
||||
tools: { toolsBySender: { "id:restricted-sender": { deny: ["web_search"] } } },
|
||||
},
|
||||
}),
|
||||
{ preserveToolFactory: true },
|
||||
);
|
||||
|
||||
expect(forkConfig).toMatchObject({
|
||||
"features.standalone_web_search": false,
|
||||
web_search: "disabled",
|
||||
});
|
||||
});
|
||||
expect(createOpenClawCodingToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ senderId: testCase.senderId }),
|
||||
);
|
||||
expect(forkConfig).toMatchObject({
|
||||
"features.standalone_web_search": false,
|
||||
web_search: testCase.webSearchMode,
|
||||
});
|
||||
expect(toolResponse).toEqual({
|
||||
success: false,
|
||||
contentItems: [{ type: "inputText", text: "Unknown OpenClaw tool: web_search" }],
|
||||
});
|
||||
expect(toolExecuteMock).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects side questions before forking when the tool allowlist excludes native tools", async () => {
|
||||
await expect(
|
||||
|
|
|
|||
|
|
@ -59,8 +59,6 @@ const schemaMigrationDescriptions = {
|
|||
"creator-namespace-v14": "cron creators -> explicit principal namespaces",
|
||||
"conversation-binding-targets-v15":
|
||||
"conversation bindings -> exact target keys without agent/session projections",
|
||||
"skill-workshop-directory-ownership-v16":
|
||||
"Skill Workshop proposals -> per-agent Workshop directory ownership",
|
||||
"prepared-worker-ownership-v17":
|
||||
"prepared workers -> one-use capacity and fixed workspace ownership",
|
||||
"github-publication-requester-authority-v18":
|
||||
|
|
|
|||
|
|
@ -81,7 +81,6 @@ export const GATEWAY_CLIENT_CAPS = {
|
|||
MODEL_SELECTION_POLICY: "model-selection-policy",
|
||||
RUN_TOOL_BINDINGS: "run-tool-bindings",
|
||||
SESSION_SCOPED_EVENTS: "session-scoped-events",
|
||||
SKILL_CURATOR_LIVE_INVENTORY: "skill-curator-live-inventory",
|
||||
PLUGIN_APPROVALS: "plugin-approvals",
|
||||
TASK_SUGGESTIONS: "task-suggestions",
|
||||
TERMINAL_OFFSET_SEQ: "terminal-offset-seq",
|
||||
|
|
|
|||
|
|
@ -6,12 +6,10 @@ export * from "./system-agent-error-details.js";
|
|||
export {
|
||||
ErrorCodes,
|
||||
GatewayErrorDetailCodes,
|
||||
buildSkillProposalRevisionChangedErrorDetails,
|
||||
isMcpAppViewExpiredError,
|
||||
readCronJobNotFoundError,
|
||||
readMissingScopeError,
|
||||
readMissingScopeErrorDetails,
|
||||
readSkillProposalRevisionChangedError,
|
||||
} from "./gateway-error-details.js";
|
||||
export type {
|
||||
CronJobNotFoundErrorDetails,
|
||||
|
|
@ -19,7 +17,6 @@ export type {
|
|||
McpAppViewExpiredErrorDetails,
|
||||
OutboundDeliveryQueuedErrorDetails,
|
||||
MissingScopeErrorDetails,
|
||||
SkillProposalRevisionChangedErrorDetails,
|
||||
UserPrefsLimitExceededErrorDetails,
|
||||
ProjectCloneErrorDetails,
|
||||
ProjectCloneFailureCause,
|
||||
|
|
@ -34,7 +31,6 @@ export {
|
|||
OutboundDeliveryQueuedErrorDetailsSchema,
|
||||
UserPrefsLimitExceededErrorDetailsSchema,
|
||||
ProjectCloneErrorDetailsSchema,
|
||||
SkillProposalRevisionChangedErrorDetailsSchema,
|
||||
WizardNotFoundErrorDetailsSchema,
|
||||
SetupAdmissionBusyErrorDetailsSchema,
|
||||
SessionWorkspaceRecoveryRequiredErrorDetailsSchema,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ export const GatewayErrorDetailCodes = {
|
|||
OUTBOUND_DELIVERY_QUEUED: "OUTBOUND_DELIVERY_QUEUED",
|
||||
USER_PREFS_LIMIT_EXCEEDED: "USER_PREFS_LIMIT_EXCEEDED",
|
||||
SESSION_COMPANION_BUSY: "SESSION_COMPANION_BUSY",
|
||||
SKILL_PROPOSAL_REVISION_CHANGED: "SKILL_PROPOSAL_REVISION_CHANGED",
|
||||
PROJECT_CLONE_FAILED: "PROJECT_CLONE_FAILED",
|
||||
UNKNOWN_AGENT_ID: "UNKNOWN_AGENT_ID",
|
||||
WIZARD_NOT_FOUND: "WIZARD_NOT_FOUND",
|
||||
|
|
@ -108,13 +107,6 @@ export type ProjectCloneErrorDetails = {
|
|||
cause: ProjectCloneFailureCause;
|
||||
};
|
||||
|
||||
/** Optimistic-concurrency mismatch for an operator-reviewed Skill Workshop draft. */
|
||||
export type SkillProposalRevisionChangedErrorDetails = {
|
||||
code: typeof GatewayErrorDetailCodes.SKILL_PROPOSAL_REVISION_CHANGED;
|
||||
expectedRevisionHash: string;
|
||||
currentRevisionHash: string;
|
||||
};
|
||||
|
||||
/** Exact retained workspace owner that must be recovered or explicitly abandoned. */
|
||||
export type SessionWorkspaceRecoveryRequiredErrorDetails = {
|
||||
code: typeof GatewayErrorDetailCodes.SESSION_WORKSPACE_RECOVERY_REQUIRED;
|
||||
|
|
@ -137,7 +129,6 @@ export type GatewayErrorDetails =
|
|||
| McpAppViewExpiredErrorDetails
|
||||
| OutboundDeliveryQueuedErrorDetails
|
||||
| UserPrefsLimitExceededErrorDetails
|
||||
| SkillProposalRevisionChangedErrorDetails
|
||||
| ProjectCloneErrorDetails
|
||||
| UnknownAgentIdErrorDetails
|
||||
| WizardNotFoundErrorDetails
|
||||
|
|
@ -148,7 +139,6 @@ export type GatewayErrorDetails =
|
|||
| { code: typeof GatewayErrorDetailCodes.TASK_HISTORY_PREVIEW_CAPACITY };
|
||||
|
||||
const LEGACY_MISSING_SCOPE_PATTERN = /\bmissing scope:\s*([a-z0-9._-]+)/i;
|
||||
const SHA256_PATTERN = /^[a-fA-F0-9]{64}$/;
|
||||
|
||||
export function readGitHubPublicationSelectionRejectedError(
|
||||
error: unknown,
|
||||
|
|
@ -175,40 +165,6 @@ export function readCronJobNotFoundError(error: unknown): CronJobNotFoundErrorDe
|
|||
return jobId ? { code: GatewayErrorDetailCodes.CRON_JOB_NOT_FOUND, jobId } : null;
|
||||
}
|
||||
|
||||
/** Builds the canonical stale-draft details shared by Skill Workshop RPCs. */
|
||||
export function buildSkillProposalRevisionChangedErrorDetails(params: {
|
||||
expectedRevisionHash: string;
|
||||
currentRevisionHash: string;
|
||||
}): SkillProposalRevisionChangedErrorDetails {
|
||||
return {
|
||||
code: GatewayErrorDetailCodes.SKILL_PROPOSAL_REVISION_CHANGED,
|
||||
expectedRevisionHash: params.expectedRevisionHash,
|
||||
currentRevisionHash: params.currentRevisionHash,
|
||||
};
|
||||
}
|
||||
|
||||
/** Reads a stale Skill Workshop decision without parsing operator-facing prose. */
|
||||
export function readSkillProposalRevisionChangedError(
|
||||
error: unknown,
|
||||
): SkillProposalRevisionChangedErrorDetails | null {
|
||||
const record = asProtocolRecord(error);
|
||||
const details = asProtocolRecord(record?.details);
|
||||
if (details?.code !== GatewayErrorDetailCodes.SKILL_PROPOSAL_REVISION_CHANGED) {
|
||||
return null;
|
||||
}
|
||||
const expectedRevisionHash =
|
||||
typeof details.expectedRevisionHash === "string" ? details.expectedRevisionHash : "";
|
||||
const currentRevisionHash =
|
||||
typeof details.currentRevisionHash === "string" ? details.currentRevisionHash : "";
|
||||
if (!SHA256_PATTERN.test(expectedRevisionHash) || !SHA256_PATTERN.test(currentRevisionHash)) {
|
||||
return null;
|
||||
}
|
||||
return buildSkillProposalRevisionChangedErrorDetails({
|
||||
expectedRevisionHash,
|
||||
currentRevisionHash,
|
||||
});
|
||||
}
|
||||
|
||||
/** Reads validated missing-scope details from an untrusted protocol payload. */
|
||||
export function readMissingScopeErrorDetails(details: unknown): MissingScopeErrorDetails | null {
|
||||
const record = asProtocolRecord(details);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import {
|
|||
validateWakeParams,
|
||||
type ValidationError,
|
||||
} from "./index.js";
|
||||
import { ProtocolSchemas } from "./schema/protocol-schemas.js";
|
||||
|
||||
const makeError = (overrides: Partial<ValidationError>): ValidationError => ({
|
||||
keyword: "type",
|
||||
|
|
@ -41,21 +40,6 @@ const expectRejected = (validate: ProtocolValidator, values: readonly unknown[])
|
|||
expectValidationCases(validate, false, values);
|
||||
|
||||
describe("lazy protocol validators", () => {
|
||||
it("registers Skill Workshop evaluation and lifecycle replay schemas", () => {
|
||||
expect(ProtocolSchemas.SkillsProposalEvaluateParams).toBe(
|
||||
protocol.SkillsProposalEvaluateParamsSchema,
|
||||
);
|
||||
expect(ProtocolSchemas.SkillsProposalEvaluateResult).toBe(
|
||||
protocol.SkillsProposalEvaluateResultSchema,
|
||||
);
|
||||
expect(ProtocolSchemas.SkillsProposalEventsListParams).toBe(
|
||||
protocol.SkillsProposalEventsListParamsSchema,
|
||||
);
|
||||
expect(ProtocolSchemas.SkillsProposalEventsListResult).toBe(
|
||||
protocol.SkillsProposalEventsListResultSchema,
|
||||
);
|
||||
});
|
||||
|
||||
it("validates through exported lazy validators", () => {
|
||||
expectAccepted(validateCommandsListParams, [{}, { includeArgs: true }]);
|
||||
expectRejected(validateCommandsListParams, [{ includeArgs: "yes" }]);
|
||||
|
|
@ -94,22 +78,27 @@ describe("lazy protocol validators", () => {
|
|||
expect(validateConnectParams.errors).toBeNull();
|
||||
});
|
||||
|
||||
it("rejects caller-provided hidden prompts in Skill Workshop revisions", () => {
|
||||
const request = {
|
||||
proposalId: "proposal-1",
|
||||
expectedRevisionHash: "a".repeat(64),
|
||||
targetAgentId: "writer",
|
||||
instructions: "Revise the support files",
|
||||
sessionKey: "agent:main:session:skill-workshop",
|
||||
idempotencyKey: "revision-run-1",
|
||||
};
|
||||
expect(protocol.validateSkillsProposalRequestRevisionParams(request)).toBe(true);
|
||||
expect(
|
||||
protocol.validateSkillsProposalRequestRevisionParams({
|
||||
...request,
|
||||
hiddenPrompt: "do not accept caller-provided hidden prompts",
|
||||
}),
|
||||
).toBe(false);
|
||||
it("validates Skill Workshop request params", () => {
|
||||
expectAccepted(protocol.validateSkillsWorkshopChangesParams, [
|
||||
{},
|
||||
{ agentId: "main", limit: 500, beforeMs: 1_700_000_000_000 },
|
||||
]);
|
||||
expectRejected(protocol.validateSkillsWorkshopChangesParams, [{ limit: 0 }, { limit: 501 }]);
|
||||
expectAccepted(protocol.validateSkillsWorkshopReadParams, [
|
||||
{ name: "deploy-notes", filePath: "references/api.md", versionId: "v1" },
|
||||
]);
|
||||
expectRejected(protocol.validateSkillsWorkshopReadParams, [{}, { name: "" }]);
|
||||
expectAccepted(protocol.validateSkillsWorkshopArchiveParams, [
|
||||
{ name: "deploy-notes", reason: "superseded" },
|
||||
]);
|
||||
expectRejected(protocol.validateSkillsWorkshopArchiveParams, [
|
||||
{ name: "deploy-notes", reason: "" },
|
||||
{ name: "deploy-notes", absorbedInto: "other" },
|
||||
]);
|
||||
expectAccepted(protocol.validateSkillsWorkshopRestoreParams, [{ name: "deploy-notes" }]);
|
||||
expectRejected(protocol.validateSkillsWorkshopRestoreParams, [
|
||||
{ name: "deploy-notes", expectedRevisionHash: "a".repeat(64) },
|
||||
]);
|
||||
});
|
||||
|
||||
it("can still compile every exported protocol validator", () => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ export * from "./schema/worker-inference.js";
|
|||
export * from "./schema/worker-gateway-tool.js";
|
||||
export * from "./schema/worker-computer.js";
|
||||
export * from "./schema/computer.js";
|
||||
export * from "./schema/skill-history.js";
|
||||
export * from "./schema/skill-library.js";
|
||||
export * from "./schema/plugin-credentials.js";
|
||||
export * from "./schema/web-search.js";
|
||||
|
|
|
|||
|
|
@ -502,35 +502,22 @@ export {
|
|||
ToolsEffectiveParamsSchema,
|
||||
ToolsInvokeParamsSchema,
|
||||
SkillsInstallParamsSchema,
|
||||
SkillsCuratorActionParamsSchema,
|
||||
SkillsCuratorActionResultSchema,
|
||||
SkillsCuratorStatusParamsSchema,
|
||||
SkillsCuratorStatusResultSchema,
|
||||
SkillsSearchParamsSchema,
|
||||
SkillsSearchResultSchema,
|
||||
SkillsDetailParamsSchema,
|
||||
SkillsDetailResultSchema,
|
||||
SkillsProposalsListParamsSchema,
|
||||
SkillsProposalsListResultSchema,
|
||||
SkillWorkshopChangeSchema,
|
||||
SkillWorkshopSkillSummarySchema,
|
||||
SkillWorkshopArchivedSkillSchema,
|
||||
SkillsWorkshopListParamsSchema,
|
||||
SkillsWorkshopListResultSchema,
|
||||
SkillsWorkshopChangesParamsSchema,
|
||||
SkillsWorkshopChangesResultSchema,
|
||||
SkillsWorkshopReadParamsSchema,
|
||||
SkillsWorkshopReadResultSchema,
|
||||
SkillsProposalInspectParamsSchema,
|
||||
SkillsProposalInspectResultSchema,
|
||||
SkillsProposalCreateParamsSchema,
|
||||
SkillsProposalUpdateParamsSchema,
|
||||
SkillsProposalReviseParamsSchema,
|
||||
SkillsProposalRequestRevisionParamsSchema,
|
||||
SkillsProposalRequestRevisionResultSchema,
|
||||
SkillsProposalDecisionParamsSchema,
|
||||
SkillsProposalActionParamsSchema,
|
||||
SkillProposalEvaluationSchema,
|
||||
SkillsProposalEvaluateParamsSchema,
|
||||
SkillsProposalEvaluateResultSchema,
|
||||
SkillProposalLifecycleEventSchema,
|
||||
SkillsProposalEventsListParamsSchema,
|
||||
SkillsProposalEventsListResultSchema,
|
||||
SkillsProposalApplyResultSchema,
|
||||
SkillsProposalRecordResultSchema,
|
||||
SkillsWorkshopArchiveParamsSchema,
|
||||
SkillsWorkshopRestoreParamsSchema,
|
||||
SkillsWorkshopChangeResultSchema,
|
||||
SkillsSecurityVerdictsParamsSchema,
|
||||
SkillsSecurityVerdictsResultSchema,
|
||||
SkillsSkillCardParamsSchema,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ export * from "./schema/sessions-sharing.js";
|
|||
export * from "./schema/sessions-suggestions.js";
|
||||
export * from "./schema/sessions-reactions.js";
|
||||
export * from "./schema/sessions-catalog.js";
|
||||
export * from "./schema/skill-history.js";
|
||||
export * from "./schema/snapshot.js";
|
||||
export * from "./schema/storage.js";
|
||||
export * from "./schema/backup.js";
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
import { Value } from "typebox/value";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
SkillsProposalActionParamsSchema,
|
||||
SkillsProposalDecisionParamsSchema,
|
||||
SkillsProposalRequestRevisionParamsSchema,
|
||||
} from "./agents-models-skills.js";
|
||||
|
||||
describe("Skill Workshop revision-bound request schemas", () => {
|
||||
it("requires exact revision hashes for operator decisions and natural revisions", () => {
|
||||
const decision = { proposalId: "proposal-1", expectedRevisionHash: "a".repeat(64) };
|
||||
|
||||
expect(Value.Check(SkillsProposalDecisionParamsSchema, decision)).toBe(true);
|
||||
expect(Value.Check(SkillsProposalDecisionParamsSchema, { proposalId: "proposal-1" })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(Value.Check(SkillsProposalActionParamsSchema, { proposalId: "proposal-1" })).toBe(true);
|
||||
expect(
|
||||
Value.Check(SkillsProposalRequestRevisionParamsSchema, {
|
||||
...decision,
|
||||
instructions: "Revise the draft",
|
||||
sessionKey: "agent:main:revision",
|
||||
idempotencyKey: "revision-1",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
Value.Check(SkillsProposalRequestRevisionParamsSchema, {
|
||||
proposalId: "proposal-1",
|
||||
instructions: "Revise the draft",
|
||||
sessionKey: "agent:main:revision",
|
||||
idempotencyKey: "revision-1",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -12,18 +12,7 @@ import {
|
|||
ModelsListResultSchema,
|
||||
ModelsProbeParamsSchema,
|
||||
ModelsProbeResultSchema,
|
||||
SkillProposalEvaluationSchema,
|
||||
SkillProposalLifecycleEventSchema,
|
||||
SkillsCuratorStatusResultSchema,
|
||||
SkillsCuratorLiveStatusResultSchema,
|
||||
SkillsCuratorActionResultSchema,
|
||||
SkillsDetailResultSchema,
|
||||
SkillsProposalEvaluateParamsSchema,
|
||||
SkillsProposalEvaluateResultSchema,
|
||||
SkillsProposalEventsListParamsSchema,
|
||||
SkillsProposalEventsListResultSchema,
|
||||
SkillsProposalInspectResultSchema,
|
||||
SkillsProposalRequestRevisionResultSchema,
|
||||
ToolsInvokeParamsSchema,
|
||||
} from "./agents-models-skills.js";
|
||||
import { ModelsListParamsSchema } from "./model-catalog.js";
|
||||
|
|
@ -42,52 +31,6 @@ const expectAccepted = (schema: ProtocolSchema, ...values: readonly unknown[]) =
|
|||
const expectRejected = (schema: ProtocolSchema, ...values: readonly unknown[]) =>
|
||||
expectSchemaCases(schema, false, values);
|
||||
|
||||
const cleanProposalScan = {
|
||||
state: "clean",
|
||||
scannedAt: "2026-05-30T00:00:00.000Z",
|
||||
critical: 0,
|
||||
warn: 0,
|
||||
info: 0,
|
||||
findings: [],
|
||||
};
|
||||
|
||||
const proposalTarget = (overrides: Record<string, unknown> = {}) => ({
|
||||
skillName: "weather-helper",
|
||||
skillDir: "/tmp/workspace/skills/weather-helper",
|
||||
skillFile: "/tmp/workspace/skills/weather-helper/SKILL.md",
|
||||
skillKey: "weather-helper",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const proposalRecord = (overrides: Record<string, unknown> = {}) => ({
|
||||
id: "proposal-1",
|
||||
kind: "create",
|
||||
status: "pending",
|
||||
title: "weather-helper",
|
||||
description: "Improve weather checks",
|
||||
schema: "openclaw.skill-workshop.proposal.v1",
|
||||
createdAt: "2026-05-30T00:00:00.000Z",
|
||||
updatedAt: "2026-05-30T00:00:00.000Z",
|
||||
createdBy: "gateway",
|
||||
proposedVersion: "v2",
|
||||
draftFile: "PROPOSAL.md",
|
||||
draftHash: "b".repeat(64),
|
||||
target: proposalTarget(),
|
||||
scan: cleanProposalScan,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const proposalEvaluation = (overrides: Record<string, unknown> = {}) => ({
|
||||
id: "evaluation-1",
|
||||
proposedVersion: "v2",
|
||||
revisionHash: "b".repeat(64),
|
||||
trigger: "apply",
|
||||
startedAt: "2026-05-30T00:01:00.000Z",
|
||||
completedAt: "2026-05-30T00:01:01.000Z",
|
||||
outcomes: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe("AgentsDeleteResultSchema", () => {
|
||||
it("accepts per-path cleanup outcomes", () => {
|
||||
expectAccepted(AgentsDeleteResultSchema, {
|
||||
|
|
@ -113,7 +56,7 @@ describe("AgentsDeleteResultSchema", () => {
|
|||
});
|
||||
|
||||
/**
|
||||
* Schema regression tests for agent metadata, skill proposals, and effective
|
||||
* Schema regression tests for agent metadata, skills, and effective
|
||||
* tool catalogs. These payloads are UI-facing but also consumed by runtime
|
||||
* guards, so the fixtures exercise strictness at the public gateway boundary.
|
||||
*/
|
||||
|
|
@ -570,307 +513,6 @@ describe("ToolsInvokeParamsSchema", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("SkillsProposalInspectResultSchema", () => {
|
||||
it("accepts support metadata and the latest bounded evaluation", () => {
|
||||
const result = {
|
||||
record: proposalRecord({
|
||||
kind: "update",
|
||||
createdBy: "skill-workshop",
|
||||
proposedVersion: "v1",
|
||||
target: proposalTarget({
|
||||
currentContentHash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
}),
|
||||
draftHash: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
||||
evaluation: proposalEvaluation({
|
||||
proposedVersion: "v1",
|
||||
revisionHash: "a".repeat(64),
|
||||
trigger: "manual",
|
||||
correlationId: "correlation-1",
|
||||
outcomes: [
|
||||
{
|
||||
pluginId: "quality-plugin",
|
||||
pluginVersion: "1.2.3",
|
||||
evaluatorId: "quality",
|
||||
status: "completed",
|
||||
result: {
|
||||
summary: "Ready to apply.",
|
||||
findings: [],
|
||||
metrics: { score: 0.98, deterministic: true, profile: "strict" },
|
||||
evaluatorVersion: "rules-v2",
|
||||
mode: "static",
|
||||
decision: "pass",
|
||||
decisionReason: "No blocking findings.",
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
supportFiles: [
|
||||
{
|
||||
path: "references/weather.md",
|
||||
sizeBytes: 42,
|
||||
hash: "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
|
||||
targetExisted: true,
|
||||
targetContentHash: "123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0",
|
||||
},
|
||||
],
|
||||
}),
|
||||
revisionHash: "a".repeat(64),
|
||||
content: "# Weather Helper\n",
|
||||
supportFiles: [
|
||||
{
|
||||
path: "references/weather.md",
|
||||
content: "Use current weather before recommendations.\n",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expectAccepted(SkillsProposalInspectResultSchema, result, {
|
||||
record: result.record,
|
||||
content: result.content,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SkillsCuratorStatusResultSchema", () => {
|
||||
const entry = {
|
||||
skillFile: "/workshop/direct/SKILL.md",
|
||||
skillKey: "direct",
|
||||
skillName: "direct",
|
||||
state: "active",
|
||||
pinned: false,
|
||||
createdAtMs: 100,
|
||||
stateChangedAtMs: 100,
|
||||
lastUsedAtMs: null,
|
||||
useCount: 0,
|
||||
archivedReason: null,
|
||||
};
|
||||
const legacy = {
|
||||
lastAttemptAtMs: null,
|
||||
lastSuccessAtMs: null,
|
||||
lastError: null,
|
||||
counts: { active: 1, stale: 0, archived: 0 },
|
||||
skills: [entry],
|
||||
overlaps: [],
|
||||
};
|
||||
|
||||
it("keeps legacy numeric dates closed while live inventory requires its marker", () => {
|
||||
const unknownEntry = { ...entry, createdAtMs: null, stateChangedAtMs: null };
|
||||
const full = { ...legacy, inventory: "live-workshop", skills: [unknownEntry] };
|
||||
expectAccepted(SkillsCuratorStatusResultSchema, legacy);
|
||||
expectAccepted(SkillsCuratorActionResultSchema, entry);
|
||||
expectRejected(
|
||||
SkillsCuratorStatusResultSchema,
|
||||
full,
|
||||
{ ...legacy, skills: [unknownEntry] },
|
||||
{ ...legacy, inventory: "live-workshop" },
|
||||
);
|
||||
expectRejected(SkillsCuratorActionResultSchema, unknownEntry);
|
||||
expectAccepted(SkillsCuratorLiveStatusResultSchema, full, {
|
||||
...legacy,
|
||||
inventory: "live-workshop",
|
||||
});
|
||||
expectRejected(
|
||||
SkillsCuratorLiveStatusResultSchema,
|
||||
legacy,
|
||||
{ ...full, inventory: "unknown" },
|
||||
{ ...full, extra: true },
|
||||
{ ...full, skills: [{ ...unknownEntry, extra: true }] },
|
||||
);
|
||||
});
|
||||
it("accepts typed collection and experience outcomes while rejecting invalid review records", () => {
|
||||
const legacyResult = {
|
||||
...legacy,
|
||||
lastAttemptAtMs: 100,
|
||||
lastSuccessAtMs: 101,
|
||||
skills: [],
|
||||
};
|
||||
const result = {
|
||||
...legacyResult,
|
||||
collectionReview: {
|
||||
workspace: { attemptedAtMs: 100, succeededAtMs: 101 },
|
||||
},
|
||||
experienceReview: {
|
||||
workspace: {
|
||||
attemptedAtMs: 102,
|
||||
outcome: "proposed",
|
||||
proposalId: "proposal-1",
|
||||
usage: { inputTokens: 40, cachedInputTokens: 20, outputTokens: 10 },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expectAccepted(SkillsCuratorStatusResultSchema, result, legacyResult);
|
||||
expectRejected(
|
||||
SkillsCuratorStatusResultSchema,
|
||||
{
|
||||
...result,
|
||||
collectionReview: { workspace: { attemptedAtMs: 100, unexpected: true } },
|
||||
},
|
||||
{
|
||||
...result,
|
||||
experienceReview: { workspace: { attemptedAtMs: 102, outcome: "archived" } },
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SkillProposalEvaluationSchema", () => {
|
||||
const completedOutcome = {
|
||||
pluginId: "quality-plugin",
|
||||
evaluatorId: "quality",
|
||||
status: "completed",
|
||||
result: {
|
||||
findings: [
|
||||
{
|
||||
ruleId: "skill.structure",
|
||||
severity: "warn",
|
||||
message: "Add a troubleshooting section.",
|
||||
file: "SKILL.md",
|
||||
line: 12,
|
||||
},
|
||||
],
|
||||
decision: "revise",
|
||||
},
|
||||
};
|
||||
const evaluation = proposalEvaluation({
|
||||
targetTreeSha256: "c".repeat(64),
|
||||
outcomes: [completedOutcome],
|
||||
});
|
||||
|
||||
it("accepts bounded evaluator outcomes", () => {
|
||||
expectAccepted(SkillProposalEvaluationSchema, evaluation);
|
||||
});
|
||||
|
||||
it("accepts the service result wrapper", () => {
|
||||
const record = proposalRecord({
|
||||
evaluation,
|
||||
});
|
||||
|
||||
expectAccepted(SkillsProposalEvaluateResultSchema, { record, evaluation });
|
||||
});
|
||||
|
||||
it("rejects non-primitive metrics and unknown decisions", () => {
|
||||
expectRejected(SkillProposalEvaluationSchema, {
|
||||
...evaluation,
|
||||
outcomes: [
|
||||
{
|
||||
...completedOutcome,
|
||||
result: {
|
||||
findings: [],
|
||||
metrics: { nested: { score: 1 } },
|
||||
decision: "approve",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["", "x".repeat(129)])("rejects invalid metric key %j", (key) => {
|
||||
expectRejected(SkillProposalEvaluationSchema, {
|
||||
...evaluation,
|
||||
outcomes: [
|
||||
{
|
||||
...completedOutcome,
|
||||
result: { findings: [], metrics: { [key]: true } },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("enforces status-specific result and error fields", () => {
|
||||
expectRejected(
|
||||
SkillProposalEvaluationSchema,
|
||||
{
|
||||
...evaluation,
|
||||
outcomes: [{ pluginId: "quality-plugin", evaluatorId: "quality", status: "completed" }],
|
||||
},
|
||||
{
|
||||
...evaluation,
|
||||
outcomes: [{ pluginId: "quality-plugin", evaluatorId: "quality", status: "error" }],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("skill proposal evaluation and event replay params", () => {
|
||||
it("validates optimistic evaluation and bounded event cursors", () => {
|
||||
expectAccepted(SkillsProposalEvaluateParamsSchema, {
|
||||
agentId: "main",
|
||||
proposalId: "proposal-1",
|
||||
expectedRevisionHash: "c".repeat(64),
|
||||
correlationId: "correlation-1",
|
||||
});
|
||||
expectAccepted(SkillsProposalEventsListParamsSchema, {
|
||||
proposalId: "proposal-1",
|
||||
afterSequence: 10,
|
||||
limit: 200,
|
||||
});
|
||||
expectRejected(SkillsProposalEventsListParamsSchema, { limit: 201 });
|
||||
});
|
||||
|
||||
it("accepts sequence-ordered lifecycle replay pages", () => {
|
||||
const event = {
|
||||
sequence: 11,
|
||||
eventId: "event-11",
|
||||
proposalId: "proposal-1",
|
||||
proposedVersion: "v2",
|
||||
revisionHash: "d".repeat(64),
|
||||
type: "evaluation_completed",
|
||||
occurredAt: "2026-05-30T00:01:01.000Z",
|
||||
actor: { type: "plugin", id: "quality-plugin" },
|
||||
correlationId: "correlation-1",
|
||||
payload: { trigger: "manual", outcomeCount: 1, blocking: false, note: null },
|
||||
evaluation: proposalEvaluation({
|
||||
id: "evaluation-11",
|
||||
revisionHash: "d".repeat(64),
|
||||
trigger: "manual",
|
||||
}),
|
||||
};
|
||||
|
||||
expectAccepted(SkillProposalLifecycleEventSchema, event);
|
||||
expectAccepted(SkillsProposalEventsListResultSchema, {
|
||||
events: [event],
|
||||
nextSequence: 11,
|
||||
});
|
||||
expectRejected(
|
||||
SkillProposalLifecycleEventSchema,
|
||||
{
|
||||
...event,
|
||||
actor: { type: "operator" },
|
||||
},
|
||||
{
|
||||
...event,
|
||||
payload: { note: "x".repeat(4_001) },
|
||||
},
|
||||
);
|
||||
for (const key of ["", "x".repeat(81)]) {
|
||||
expectRejected(SkillProposalLifecycleEventSchema, {
|
||||
...event,
|
||||
payload: { [key]: true },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("SkillsProposalRequestRevisionResultSchema", () => {
|
||||
it.each(["started", "in_flight", "ok", "timeout", "error"])(
|
||||
"accepts forwarded chat.send ack status %s",
|
||||
(status) => {
|
||||
expectAccepted(SkillsProposalRequestRevisionResultSchema, {
|
||||
runId: "run-revision",
|
||||
status,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects unknown forwarded chat.send ack statuses", () => {
|
||||
expectRejected(SkillsProposalRequestRevisionResultSchema, {
|
||||
runId: "run-revision",
|
||||
status: "queued",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SkillsDetailResultSchema", () => {
|
||||
it("accepts official ClawHub skill publisher metadata", () => {
|
||||
const result = {
|
||||
|
|
|
|||
|
|
@ -416,402 +416,117 @@ export const SkillsSkillCardResultSchema = closedObject({
|
|||
content: Type.String(),
|
||||
});
|
||||
|
||||
const SkillProposalStatusSchema = Type.Union([
|
||||
Type.Literal("pending"),
|
||||
Type.Literal("applied"),
|
||||
Type.Literal("rejected"),
|
||||
Type.Literal("quarantined"),
|
||||
Type.Literal("stale"),
|
||||
const SkillWorkshopChangeActionSchema = Type.Union([
|
||||
Type.Literal("create"),
|
||||
Type.Literal("patch"),
|
||||
Type.Literal("write_file"),
|
||||
Type.Literal("remove_file"),
|
||||
Type.Literal("archive"),
|
||||
Type.Literal("restore"),
|
||||
]);
|
||||
/** Skill proposal operation type: new skill or update to an existing skill. */
|
||||
const SkillProposalKindSchema = Type.Union([Type.Literal("create"), Type.Literal("update")]);
|
||||
/** Scan state for proposed skill content before it can be applied. */
|
||||
const SkillProposalScanStateSchema = Type.Union([
|
||||
Type.Literal("pending"),
|
||||
Type.Literal("clean"),
|
||||
Type.Literal("failed"),
|
||||
Type.Literal("quarantined"),
|
||||
const SkillWorkshopActorSchema = Type.Union([
|
||||
Type.Literal("agent"),
|
||||
Type.Literal("review"),
|
||||
Type.Literal("curator"),
|
||||
Type.Literal("user"),
|
||||
]);
|
||||
/** Source that created the skill proposal record. */
|
||||
const SkillProposalSourceSchema = Type.Union([
|
||||
Type.Literal("skill-workshop"),
|
||||
Type.Literal("cli"),
|
||||
Type.Literal("gateway"),
|
||||
]);
|
||||
const SkillProposalContentString = Type.String({ minLength: 1, maxLength: 1_048_576 });
|
||||
/** Support file payload accepted from proposal create/revise requests. */
|
||||
const SkillProposalSupportFileInputSchema = closedObject({
|
||||
path: NonEmptyString,
|
||||
content: Type.String({ maxLength: 262_144 }),
|
||||
});
|
||||
/** Stored support file metadata, including target conflict hashes for updates. */
|
||||
const SkillProposalSupportFileSchema = closedObject({
|
||||
path: NonEmptyString,
|
||||
sizeBytes: Type.Integer({ minimum: 0, maximum: 262_144 }),
|
||||
hash: Sha256String,
|
||||
targetExisted: Type.Optional(Type.Boolean()),
|
||||
targetContentHash: Type.Optional(Sha256String),
|
||||
});
|
||||
const TimestampMsSchema = Type.Number({ minimum: 0 });
|
||||
|
||||
/** One static-scan finding against proposed skill content. */
|
||||
const SkillProposalFindingSchema = closedObject({
|
||||
ruleId: NonEmptyString,
|
||||
severity: Type.Union([Type.Literal("info"), Type.Literal("warn"), Type.Literal("critical")]),
|
||||
file: NonEmptyString,
|
||||
line: Type.Integer({ minimum: 1 }),
|
||||
message: NonEmptyString,
|
||||
evidence: Type.String(),
|
||||
});
|
||||
|
||||
/** Aggregated scan report attached to a proposal record. */
|
||||
const SkillProposalScanSchema = closedObject({
|
||||
state: SkillProposalScanStateSchema,
|
||||
scannedAt: NonEmptyString,
|
||||
critical: Type.Integer({ minimum: 0 }),
|
||||
warn: Type.Integer({ minimum: 0 }),
|
||||
info: Type.Integer({ minimum: 0 }),
|
||||
findings: Type.Array(SkillProposalFindingSchema),
|
||||
});
|
||||
|
||||
/** Skill file target that a proposal creates or updates. */
|
||||
const SkillProposalTargetSchema = closedObject({
|
||||
/** One applied Workshop change; `versionId` names the snapshot taken before it, for undo. */
|
||||
export const SkillWorkshopChangeSchema = closedObject({
|
||||
id: NonEmptyString,
|
||||
agentId: NonEmptyString,
|
||||
skillName: NonEmptyString,
|
||||
skillKey: NonEmptyString,
|
||||
skillDir: NonEmptyString,
|
||||
skillFile: NonEmptyString,
|
||||
source: Type.Optional(NonEmptyString),
|
||||
currentContentHash: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
/** Optional runtime origin tying a proposal back to an agent turn. */
|
||||
const SkillProposalOriginSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
action: SkillWorkshopChangeActionSchema,
|
||||
actor: SkillWorkshopActorSchema,
|
||||
summary: Type.String(),
|
||||
versionId: Type.Optional(NonEmptyString),
|
||||
sessionKey: Type.Optional(NonEmptyString),
|
||||
runId: Type.Optional(NonEmptyString),
|
||||
messageId: Type.Optional(NonEmptyString),
|
||||
createdAtMs: TimestampMsSchema,
|
||||
});
|
||||
|
||||
const SkillProposalEvaluationFindingSchema = closedObject({
|
||||
ruleId: Type.String({ minLength: 1, maxLength: 256 }),
|
||||
severity: Type.Union([Type.Literal("info"), Type.Literal("warn"), Type.Literal("critical")]),
|
||||
message: Type.String({ minLength: 1, maxLength: 4_000 }),
|
||||
file: Type.Optional(Type.String({ minLength: 1, maxLength: 1_024 })),
|
||||
line: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
/** Live Workshop skill with optional usage counters. */
|
||||
export const SkillWorkshopSkillSummarySchema = closedObject({
|
||||
name: NonEmptyString,
|
||||
description: Type.String(),
|
||||
updatedAtMs: TimestampMsSchema,
|
||||
sizeBytes: Type.Integer({ minimum: 0 }),
|
||||
files: Type.Array(NonEmptyString),
|
||||
useCount: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
lastUsedAtMs: Type.Optional(TimestampMsSchema),
|
||||
});
|
||||
|
||||
const SkillProposalEvaluationResultSchema = closedObject({
|
||||
summary: Type.Optional(Type.String({ maxLength: 8_000 })),
|
||||
findings: Type.Optional(Type.Array(SkillProposalEvaluationFindingSchema, { maxItems: 200 })),
|
||||
metrics: Type.Optional(
|
||||
Type.Record(
|
||||
Type.String(),
|
||||
Type.Union([Type.String({ maxLength: 4_000 }), Type.Number(), Type.Boolean()]),
|
||||
{
|
||||
maxProperties: 64,
|
||||
propertyNames: Type.String({ minLength: 1, maxLength: 128 }),
|
||||
},
|
||||
),
|
||||
/** Saved versions of one Workshop skill; `live: false` means the skill is archived. */
|
||||
export const SkillWorkshopArchivedSkillSchema = closedObject({
|
||||
name: NonEmptyString,
|
||||
live: Type.Boolean(),
|
||||
versions: Type.Array(
|
||||
closedObject({
|
||||
id: NonEmptyString,
|
||||
action: SkillWorkshopChangeActionSchema,
|
||||
createdAtMs: TimestampMsSchema,
|
||||
}),
|
||||
),
|
||||
evaluatorVersion: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
||||
mode: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
||||
decision: Type.Optional(
|
||||
Type.Union([Type.Literal("pass"), Type.Literal("revise"), Type.Literal("block")]),
|
||||
),
|
||||
decisionReason: Type.Optional(Type.String({ maxLength: 2_000 })),
|
||||
});
|
||||
|
||||
const SkillProposalEvaluationOutcomeAttribution = {
|
||||
pluginId: Type.String({ minLength: 1, maxLength: 128 }),
|
||||
pluginVersion: Type.Optional(Type.String({ minLength: 1, maxLength: 128 })),
|
||||
evaluatorId: Type.String({ minLength: 1, maxLength: 128 }),
|
||||
};
|
||||
|
||||
const SkillProposalEvaluationOutcomeSchema = Type.Union([
|
||||
closedObject({
|
||||
...SkillProposalEvaluationOutcomeAttribution,
|
||||
status: Type.Literal("completed"),
|
||||
result: SkillProposalEvaluationResultSchema,
|
||||
}),
|
||||
closedObject({
|
||||
...SkillProposalEvaluationOutcomeAttribution,
|
||||
status: Type.Literal("skipped"),
|
||||
}),
|
||||
closedObject({
|
||||
...SkillProposalEvaluationOutcomeAttribution,
|
||||
status: Type.Literal("error"),
|
||||
error: Type.String({ minLength: 1, maxLength: 2_000 }),
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Latest completed evaluator run attached to a proposal record. */
|
||||
export const SkillProposalEvaluationSchema = closedObject({
|
||||
id: NonEmptyString,
|
||||
proposedVersion: NonEmptyString,
|
||||
revisionHash: Sha256String,
|
||||
trigger: Type.Union([Type.Literal("manual"), Type.Literal("apply")]),
|
||||
startedAt: NonEmptyString,
|
||||
completedAt: NonEmptyString,
|
||||
correlationId: Type.Optional(NonEmptyString),
|
||||
targetTreeSha256: Type.Optional(Sha256String),
|
||||
outcomes: Type.Array(SkillProposalEvaluationOutcomeSchema, { maxItems: 64 }),
|
||||
});
|
||||
|
||||
/** Full persisted skill proposal record. */
|
||||
const SkillProposalRecordSchema = closedObject({
|
||||
schema: Type.Literal("openclaw.skill-workshop.proposal.v1"),
|
||||
id: NonEmptyString,
|
||||
kind: SkillProposalKindSchema,
|
||||
status: SkillProposalStatusSchema,
|
||||
title: NonEmptyString,
|
||||
description: NonEmptyString,
|
||||
createdAt: NonEmptyString,
|
||||
updatedAt: NonEmptyString,
|
||||
createdBy: SkillProposalSourceSchema,
|
||||
origin: Type.Optional(SkillProposalOriginSchema),
|
||||
proposedVersion: NonEmptyString,
|
||||
draftFile: Type.Literal("PROPOSAL.md"),
|
||||
draftHash: NonEmptyString,
|
||||
supportFiles: Type.Optional(Type.Array(SkillProposalSupportFileSchema, { maxItems: 64 })),
|
||||
target: SkillProposalTargetSchema,
|
||||
scan: SkillProposalScanSchema,
|
||||
goal: Type.Optional(Type.String()),
|
||||
evidence: Type.Optional(Type.String()),
|
||||
appliedAt: Type.Optional(NonEmptyString),
|
||||
rejectedAt: Type.Optional(NonEmptyString),
|
||||
quarantinedAt: Type.Optional(NonEmptyString),
|
||||
staleAt: Type.Optional(NonEmptyString),
|
||||
statusReason: Type.Optional(Type.String()),
|
||||
evaluation: Type.Optional(SkillProposalEvaluationSchema),
|
||||
});
|
||||
|
||||
/** Condensed proposal manifest entry for list views. */
|
||||
const SkillProposalManifestEntrySchema = closedObject({
|
||||
id: NonEmptyString,
|
||||
kind: SkillProposalKindSchema,
|
||||
status: SkillProposalStatusSchema,
|
||||
title: NonEmptyString,
|
||||
description: NonEmptyString,
|
||||
skillName: NonEmptyString,
|
||||
skillKey: NonEmptyString,
|
||||
createdAt: NonEmptyString,
|
||||
updatedAt: NonEmptyString,
|
||||
scanState: SkillProposalScanStateSchema,
|
||||
revisionHash: Type.Optional(Sha256String),
|
||||
degradedState: Type.Optional(Type.Literal("draft-missing")),
|
||||
});
|
||||
|
||||
/** Lists skill-workshop proposals for the selected agent scope. */
|
||||
export const SkillsProposalsListParamsSchema = closedObject({
|
||||
/** Lists the selected agent's Workshop skills and saved versions. */
|
||||
export const SkillsWorkshopListParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
/** Proposal manifest response for dashboard/workshop list views. */
|
||||
export const SkillsProposalsListResultSchema = closedObject({
|
||||
schema: Type.Literal("openclaw.skill-workshop.proposals-manifest.v1"),
|
||||
updatedAt: NonEmptyString,
|
||||
proposals: Type.Array(SkillProposalManifestEntrySchema),
|
||||
installedSkills: Type.Array(
|
||||
closedObject({ name: NonEmptyString, skillKey: NonEmptyString, description: Type.String() }),
|
||||
),
|
||||
export const SkillsWorkshopListResultSchema = closedObject({
|
||||
agentId: NonEmptyString,
|
||||
mode: Type.Union([Type.Literal("off"), Type.Literal("auto")]),
|
||||
root: NonEmptyString,
|
||||
skills: Type.Array(SkillWorkshopSkillSummarySchema),
|
||||
archived: Type.Array(SkillWorkshopArchivedSkillSchema),
|
||||
});
|
||||
|
||||
/** Reads the current agent-owned Workshop skill, independently of its proposal history. */
|
||||
/** Pages the Workshop change feed newest first. */
|
||||
export const SkillsWorkshopChangesParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 500 })),
|
||||
beforeMs: Type.Optional(TimestampMsSchema),
|
||||
});
|
||||
|
||||
export const SkillsWorkshopChangesResultSchema = closedObject({
|
||||
changes: Type.Array(SkillWorkshopChangeSchema),
|
||||
});
|
||||
|
||||
/** Reads one file of a live Workshop skill, or of a saved version when `versionId` is set. */
|
||||
export const SkillsWorkshopReadParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
name: NonEmptyString,
|
||||
filePath: Type.Optional(NonEmptyString),
|
||||
versionId: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
export const SkillsWorkshopReadResultSchema = closedObject({
|
||||
name: NonEmptyString,
|
||||
skillKey: NonEmptyString,
|
||||
description: Type.String(),
|
||||
filePath: NonEmptyString,
|
||||
content: Type.String(),
|
||||
files: Type.Array(NonEmptyString),
|
||||
});
|
||||
|
||||
/** Reads a proposal record plus editable draft/support content. */
|
||||
export const SkillsProposalInspectParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
proposalId: NonEmptyString,
|
||||
});
|
||||
|
||||
/** Full proposal inspection result used before apply/revise decisions. */
|
||||
export const SkillsProposalInspectResultSchema = closedObject({
|
||||
record: SkillProposalRecordSchema,
|
||||
revisionHash: Type.Optional(Sha256String),
|
||||
content: Type.String(),
|
||||
supportFiles: Type.Optional(Type.Array(SkillProposalSupportFileInputSchema, { maxItems: 64 })),
|
||||
});
|
||||
|
||||
/** Creates a proposal for a new skill. */
|
||||
export const SkillsProposalCreateParamsSchema = closedObject({
|
||||
/** Archives a live Workshop skill; the snapshot stays restorable. */
|
||||
export const SkillsWorkshopArchiveParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
name: NonEmptyString,
|
||||
description: NonEmptyString,
|
||||
content: SkillProposalContentString,
|
||||
supportFiles: Type.Optional(Type.Array(SkillProposalSupportFileInputSchema, { maxItems: 64 })),
|
||||
goal: Type.Optional(Type.String()),
|
||||
evidence: Type.Optional(Type.String()),
|
||||
reason: Type.Optional(Type.String({ minLength: 1, maxLength: 1_000 })),
|
||||
});
|
||||
|
||||
/** Creates a proposal to update an existing skill. */
|
||||
export const SkillsProposalUpdateParamsSchema = closedObject({
|
||||
/** Restores a saved version (newest when omitted); the current live copy is versioned first. */
|
||||
export const SkillsWorkshopRestoreParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
skillName: NonEmptyString,
|
||||
description: Type.Optional(NonEmptyString),
|
||||
content: SkillProposalContentString,
|
||||
supportFiles: Type.Optional(Type.Array(SkillProposalSupportFileInputSchema, { maxItems: 64 })),
|
||||
goal: Type.Optional(Type.String()),
|
||||
evidence: Type.Optional(Type.String()),
|
||||
name: NonEmptyString,
|
||||
versionId: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
/** Replaces draft content/support files for an existing proposal. */
|
||||
export const SkillsProposalReviseParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
proposalId: NonEmptyString,
|
||||
expectedRevisionHash: Type.Optional(Sha256String),
|
||||
correlationId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
||||
content: Type.Optional(SkillProposalContentString),
|
||||
supportFiles: Type.Optional(Type.Array(SkillProposalSupportFileInputSchema, { maxItems: 64 })),
|
||||
description: Type.Optional(NonEmptyString),
|
||||
goal: Type.Optional(Type.String()),
|
||||
evidence: Type.Optional(Type.String()),
|
||||
export const SkillsWorkshopChangeResultSchema = closedObject({
|
||||
change: SkillWorkshopChangeSchema,
|
||||
});
|
||||
|
||||
/** Starts an agent turn that revises a pending proposal from natural-language instructions. */
|
||||
export const SkillsProposalRequestRevisionParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
targetAgentId: Type.Optional(NonEmptyString),
|
||||
proposalId: NonEmptyString,
|
||||
expectedRevisionHash: Sha256String,
|
||||
instructions: Type.String({ minLength: 1, maxLength: 32_768 }),
|
||||
sessionKey: NonEmptyString,
|
||||
sessionId: Type.Optional(NonEmptyString),
|
||||
idempotencyKey: NonEmptyString,
|
||||
});
|
||||
|
||||
/** Chat-run acknowledgement returned after queueing a Skill Workshop revision request. */
|
||||
export const SkillsProposalRequestRevisionResultSchema = Type.Object(
|
||||
{
|
||||
runId: NonEmptyString,
|
||||
status: Type.Union([
|
||||
Type.Literal("started"),
|
||||
Type.Literal("in_flight"),
|
||||
Type.Literal("ok"),
|
||||
Type.Literal("timeout"),
|
||||
Type.Literal("error"),
|
||||
]),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
);
|
||||
|
||||
/** Apply/reject payload bound to the exact proposal revision reviewed by the operator. */
|
||||
export const SkillsProposalDecisionParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
proposalId: NonEmptyString,
|
||||
expectedRevisionHash: Sha256String,
|
||||
correlationId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
||||
reason: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
/** Quarantine payload with optional optimistic-concurrency evidence. */
|
||||
export const SkillsProposalActionParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
proposalId: NonEmptyString,
|
||||
expectedRevisionHash: Type.Optional(Sha256String),
|
||||
correlationId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
||||
reason: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
/** Runs configured proposal evaluators against the current draft. */
|
||||
export const SkillsProposalEvaluateParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
proposalId: NonEmptyString,
|
||||
expectedRevisionHash: Type.Optional(Sha256String),
|
||||
correlationId: Type.Optional(Type.String({ minLength: 1, maxLength: 256 })),
|
||||
});
|
||||
|
||||
/** Updated proposal record and completed evaluator run returned by manual evaluation. */
|
||||
export const SkillsProposalEvaluateResultSchema = closedObject({
|
||||
record: SkillProposalRecordSchema,
|
||||
evaluation: SkillProposalEvaluationSchema,
|
||||
});
|
||||
|
||||
const SkillProposalLifecycleEventTypeSchema = Type.Union([
|
||||
Type.Literal("created"),
|
||||
Type.Literal("revised"),
|
||||
Type.Literal("evaluation_completed"),
|
||||
Type.Literal("applied"),
|
||||
Type.Literal("rejected"),
|
||||
Type.Literal("quarantined"),
|
||||
Type.Literal("stale"),
|
||||
]);
|
||||
|
||||
const SkillProposalLifecycleEventActorSchema = closedObject({
|
||||
type: Type.Union([
|
||||
Type.Literal("agent"),
|
||||
Type.Literal("gateway"),
|
||||
Type.Literal("plugin"),
|
||||
Type.Literal("system"),
|
||||
]),
|
||||
id: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
const SkillProposalLifecycleEventPayloadSchema = Type.Record(
|
||||
Type.String(),
|
||||
Type.Union([Type.String({ maxLength: 4_000 }), Type.Number(), Type.Boolean(), Type.Null()]),
|
||||
{
|
||||
maxProperties: 32,
|
||||
propertyNames: Type.String({ minLength: 1, maxLength: 80 }),
|
||||
},
|
||||
);
|
||||
|
||||
/** Durable Skill Workshop lifecycle event returned for replay. */
|
||||
export const SkillProposalLifecycleEventSchema = closedObject({
|
||||
sequence: Type.Integer({ minimum: 1 }),
|
||||
eventId: NonEmptyString,
|
||||
proposalId: NonEmptyString,
|
||||
proposedVersion: NonEmptyString,
|
||||
revisionHash: Sha256String,
|
||||
type: SkillProposalLifecycleEventTypeSchema,
|
||||
occurredAt: NonEmptyString,
|
||||
actor: SkillProposalLifecycleEventActorSchema,
|
||||
correlationId: Type.Optional(NonEmptyString),
|
||||
payload: Type.Optional(SkillProposalLifecycleEventPayloadSchema),
|
||||
evaluation: Type.Optional(SkillProposalEvaluationSchema),
|
||||
});
|
||||
|
||||
/** Lists durable proposal lifecycle events after an optional sequence cursor. */
|
||||
export const SkillsProposalEventsListParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
proposalId: Type.Optional(NonEmptyString),
|
||||
afterSequence: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 200 })),
|
||||
});
|
||||
|
||||
/** Sequence-ordered proposal lifecycle replay page. */
|
||||
export const SkillsProposalEventsListResultSchema = closedObject({
|
||||
events: Type.Array(SkillProposalLifecycleEventSchema, { maxItems: 200 }),
|
||||
nextSequence: Type.Optional(Type.Integer({ minimum: 1 })),
|
||||
});
|
||||
|
||||
/** Result returned after applying a skill proposal to disk. */
|
||||
export const SkillsProposalApplyResultSchema = closedObject({
|
||||
record: SkillProposalRecordSchema,
|
||||
targetSkillFile: NonEmptyString,
|
||||
});
|
||||
|
||||
/** Proposal record result returned after non-apply proposal actions. */
|
||||
export const SkillsProposalRecordResultSchema = SkillProposalRecordSchema;
|
||||
|
||||
export {
|
||||
SkillsCuratorStatusParamsSchema,
|
||||
SkillsCuratorStatusResultSchema,
|
||||
SkillsCuratorActionParamsSchema,
|
||||
SkillsCuratorActionResultSchema,
|
||||
SkillCuratorLiveEntrySchema,
|
||||
SkillsCuratorLiveStatusResultSchema,
|
||||
} from "./skill-curator.js";
|
||||
|
||||
export const GitHubIdentityScopeSchema = Type.Union([
|
||||
Type.Literal("system"),
|
||||
Type.Literal("agent"),
|
||||
|
|
@ -1080,37 +795,18 @@ export type SkillsBinsParams = Static<typeof SkillsBinsParamsSchema>;
|
|||
export type SkillsBinsResult = Static<typeof SkillsBinsResultSchema>;
|
||||
export type SkillsDetailParams = Static<typeof SkillsDetailParamsSchema>;
|
||||
export type SkillsDetailResult = Static<typeof SkillsDetailResultSchema>;
|
||||
export type SkillsProposalsListParams = Static<typeof SkillsProposalsListParamsSchema>;
|
||||
export type SkillsProposalsListResult = Static<typeof SkillsProposalsListResultSchema>;
|
||||
export type SkillsProposalInspectParams = Static<typeof SkillsProposalInspectParamsSchema>;
|
||||
export type SkillsProposalInspectResult = Static<typeof SkillsProposalInspectResultSchema>;
|
||||
export type SkillsProposalCreateParams = Static<typeof SkillsProposalCreateParamsSchema>;
|
||||
export type SkillsProposalUpdateParams = Static<typeof SkillsProposalUpdateParamsSchema>;
|
||||
export type SkillsProposalReviseParams = Static<typeof SkillsProposalReviseParamsSchema>;
|
||||
export type SkillsProposalRequestRevisionParams = Static<
|
||||
typeof SkillsProposalRequestRevisionParamsSchema
|
||||
>;
|
||||
export type SkillsProposalRequestRevisionResult = Static<
|
||||
typeof SkillsProposalRequestRevisionResultSchema
|
||||
>;
|
||||
export type SkillsProposalDecisionParams = Static<typeof SkillsProposalDecisionParamsSchema>;
|
||||
export type SkillsProposalActionParams = Static<typeof SkillsProposalActionParamsSchema>;
|
||||
export type SkillProposalEvaluation = Static<typeof SkillProposalEvaluationSchema>;
|
||||
export type SkillsProposalEvaluateParams = Static<typeof SkillsProposalEvaluateParamsSchema>;
|
||||
export type SkillsProposalEvaluateResult = Static<typeof SkillsProposalEvaluateResultSchema>;
|
||||
export type SkillProposalLifecycleEvent = Static<typeof SkillProposalLifecycleEventSchema>;
|
||||
export type SkillsProposalEventsListParams = Static<typeof SkillsProposalEventsListParamsSchema>;
|
||||
export type SkillsProposalEventsListResult = Static<typeof SkillsProposalEventsListResultSchema>;
|
||||
export type SkillsProposalApplyResult = Static<typeof SkillsProposalApplyResultSchema>;
|
||||
export type SkillsProposalRecordResult = Static<typeof SkillsProposalRecordResultSchema>;
|
||||
export type {
|
||||
SkillsCuratorStatusParams,
|
||||
SkillsCuratorStatusResult,
|
||||
SkillsCuratorLiveStatusResult,
|
||||
SkillsCuratorCompatibleStatusResult,
|
||||
SkillsCuratorActionParams,
|
||||
SkillsCuratorActionResult,
|
||||
} from "./skill-curator.js";
|
||||
export type SkillWorkshopChange = Static<typeof SkillWorkshopChangeSchema>;
|
||||
export type SkillWorkshopSkillSummary = Static<typeof SkillWorkshopSkillSummarySchema>;
|
||||
export type SkillWorkshopArchivedSkill = Static<typeof SkillWorkshopArchivedSkillSchema>;
|
||||
export type SkillsWorkshopListParams = Static<typeof SkillsWorkshopListParamsSchema>;
|
||||
export type SkillsWorkshopListResult = Static<typeof SkillsWorkshopListResultSchema>;
|
||||
export type SkillsWorkshopChangesParams = Static<typeof SkillsWorkshopChangesParamsSchema>;
|
||||
export type SkillsWorkshopChangesResult = Static<typeof SkillsWorkshopChangesResultSchema>;
|
||||
export type SkillsWorkshopReadParams = Static<typeof SkillsWorkshopReadParamsSchema>;
|
||||
export type SkillsWorkshopReadResult = Static<typeof SkillsWorkshopReadResultSchema>;
|
||||
export type SkillsWorkshopArchiveParams = Static<typeof SkillsWorkshopArchiveParamsSchema>;
|
||||
export type SkillsWorkshopRestoreParams = Static<typeof SkillsWorkshopRestoreParamsSchema>;
|
||||
export type SkillsWorkshopChangeResult = Static<typeof SkillsWorkshopChangeResultSchema>;
|
||||
export type SkillsSecurityVerdictsParams = Static<typeof SkillsSecurityVerdictsParamsSchema>;
|
||||
export type SkillsSecurityVerdictsResult = Static<typeof SkillsSecurityVerdictsResultSchema>;
|
||||
export type SkillsSkillCardParams = Static<typeof SkillsSkillCardParamsSchema>;
|
||||
|
|
@ -1120,4 +816,3 @@ export type SkillsUploadChunkParams = Static<typeof SkillsUploadChunkParamsSchem
|
|||
export type SkillsUploadCommitParams = Static<typeof SkillsUploadCommitParamsSchema>;
|
||||
export type SkillsInstallParams = Static<typeof SkillsInstallParamsSchema>;
|
||||
export type SkillsUpdateParams = Static<typeof SkillsUpdateParamsSchema>;
|
||||
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ import {
|
|||
OutboundDeliveryQueuedErrorDetailsSchema,
|
||||
SessionWorkspaceRecoveryRequiredErrorDetailsSchema,
|
||||
ProjectCloneErrorDetailsSchema,
|
||||
SkillProposalRevisionChangedErrorDetailsSchema,
|
||||
missingScopeErrorShape,
|
||||
readSkillProposalRevisionChangedError,
|
||||
readMissingScopeError,
|
||||
readMissingScopeErrorDetails,
|
||||
readCronJobNotFoundError,
|
||||
|
|
@ -158,23 +156,6 @@ describe("gateway error details", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("validates and reads changed skill proposal revisions", () => {
|
||||
const details = {
|
||||
code: GatewayErrorDetailCodes.SKILL_PROPOSAL_REVISION_CHANGED,
|
||||
expectedRevisionHash: "A".repeat(64),
|
||||
currentRevisionHash: "b".repeat(64),
|
||||
};
|
||||
|
||||
expect(Value.Check(SkillProposalRevisionChangedErrorDetailsSchema, details)).toBe(true);
|
||||
expect(Value.Check(GatewayErrorDetailsSchema, details)).toBe(true);
|
||||
expect(readSkillProposalRevisionChangedError({ details })).toEqual(details);
|
||||
expect(
|
||||
readSkillProposalRevisionChangedError({
|
||||
details: { ...details, currentRevisionHash: "not-a-sha256" },
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("builds a distinct forbidden missing-scope response", () => {
|
||||
expect(
|
||||
missingScopeErrorShape({
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export {
|
|||
type McpAppViewExpiredErrorDetails,
|
||||
type OutboundDeliveryQueuedErrorDetails,
|
||||
type MissingScopeErrorDetails,
|
||||
type SkillProposalRevisionChangedErrorDetails,
|
||||
type UserPrefsLimitExceededErrorDetails,
|
||||
type ProjectCloneErrorDetails,
|
||||
type ProjectCloneFailureCause,
|
||||
|
|
@ -34,8 +33,6 @@ export {
|
|||
isMcpAppViewExpiredError,
|
||||
readMissingScopeError,
|
||||
readMissingScopeErrorDetails,
|
||||
buildSkillProposalRevisionChangedErrorDetails,
|
||||
readSkillProposalRevisionChangedError,
|
||||
} from "../gateway-error-details.js";
|
||||
|
||||
export const CronJobNotFoundErrorDetailsSchema = closedObject({
|
||||
|
|
@ -89,14 +86,6 @@ export const ProjectCloneErrorDetailsSchema = closedObject({
|
|||
}),
|
||||
});
|
||||
|
||||
const RevisionHashSchema = Type.String({ pattern: "^[a-fA-F0-9]{64}$" });
|
||||
|
||||
export const SkillProposalRevisionChangedErrorDetailsSchema = closedObject({
|
||||
code: Type.Literal(GatewayErrorDetailCodes.SKILL_PROPOSAL_REVISION_CHANGED),
|
||||
expectedRevisionHash: RevisionHashSchema,
|
||||
currentRevisionHash: RevisionHashSchema,
|
||||
});
|
||||
|
||||
export const SessionWorkspaceRecoveryRequiredErrorDetailsSchema = closedObject({
|
||||
code: Type.Literal(GatewayErrorDetailCodes.SESSION_WORKSPACE_RECOVERY_REQUIRED),
|
||||
cause: Type.Literal("device_offline"),
|
||||
|
|
@ -122,7 +111,6 @@ export const GatewayErrorDetailsSchema = Type.Union([
|
|||
McpAppViewExpiredErrorDetailsSchema,
|
||||
OutboundDeliveryQueuedErrorDetailsSchema,
|
||||
UserPrefsLimitExceededErrorDetailsSchema,
|
||||
SkillProposalRevisionChangedErrorDetailsSchema,
|
||||
ProjectCloneErrorDetailsSchema,
|
||||
UnknownAgentIdErrorDetailsSchema,
|
||||
WizardNotFoundErrorDetailsSchema,
|
||||
|
|
|
|||
|
|
@ -107,8 +107,6 @@ const EXCLUDED_SCHEMA_EXPORTS = [
|
|||
"SessionsReclaimParamsSchema",
|
||||
"SessionsReclaimResultPlacementSchema",
|
||||
"SessionsReclaimResultSchema",
|
||||
"SkillProposalEvaluationSchema",
|
||||
"SkillProposalLifecycleEventSchema",
|
||||
"SystemAgentChatQuestionSchema",
|
||||
"SystemAgentWizardCancelSchema",
|
||||
"TranscriptUtteranceSchema",
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
import type { Static } from "typebox";
|
||||
import { Type } from "typebox";
|
||||
import { closedObject } from "./closed-object.js";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
const SkillLifecycleStateSchema = Type.Union([
|
||||
Type.Literal("active"),
|
||||
Type.Literal("stale"),
|
||||
Type.Literal("archived"),
|
||||
]);
|
||||
|
||||
const SkillCuratorEntrySchema = closedObject({
|
||||
skillFile: NonEmptyString,
|
||||
skillKey: NonEmptyString,
|
||||
skillName: NonEmptyString,
|
||||
state: SkillLifecycleStateSchema,
|
||||
pinned: Type.Boolean(),
|
||||
createdAtMs: Type.Number(),
|
||||
stateChangedAtMs: Type.Number(),
|
||||
lastUsedAtMs: Type.Union([Type.Number(), Type.Null()]),
|
||||
useCount: Type.Number(),
|
||||
archivedReason: Type.Union([Type.String(), Type.Null()]),
|
||||
});
|
||||
|
||||
const SkillOverlapCandidateSchema = closedObject({
|
||||
left: NonEmptyString,
|
||||
right: NonEmptyString,
|
||||
score: Type.Number(),
|
||||
});
|
||||
|
||||
const SkillCollectionReviewStatusSchema = closedObject({
|
||||
attemptedAtMs: Type.Number(),
|
||||
succeededAtMs: Type.Optional(Type.Number()),
|
||||
error: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
const SkillExperienceReviewStatusSchema = closedObject({
|
||||
attemptedAtMs: Type.Number(),
|
||||
outcome: Type.Union([
|
||||
Type.Literal("completed"),
|
||||
Type.Literal("applied"),
|
||||
Type.Literal("proposed"),
|
||||
Type.Literal("nothing"),
|
||||
Type.Literal("failed"),
|
||||
]),
|
||||
proposalId: Type.Optional(Type.String()),
|
||||
error: Type.Optional(Type.String()),
|
||||
usage: Type.Optional(
|
||||
closedObject({
|
||||
inputTokens: Type.Number(),
|
||||
cachedInputTokens: Type.Number(),
|
||||
outputTokens: Type.Number(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
/** Reads persisted skill usage and collection review state. */
|
||||
export const SkillsCuratorStatusParamsSchema = closedObject({});
|
||||
|
||||
export const SkillsCuratorStatusResultSchema = closedObject({
|
||||
lastAttemptAtMs: Type.Union([Type.Number(), Type.Null()]),
|
||||
lastSuccessAtMs: Type.Union([Type.Number(), Type.Null()]),
|
||||
lastError: Type.Union([Type.String(), Type.Null()]),
|
||||
collectionReview: Type.Optional(Type.Record(NonEmptyString, SkillCollectionReviewStatusSchema)),
|
||||
experienceReview: Type.Optional(Type.Record(NonEmptyString, SkillExperienceReviewStatusSchema)),
|
||||
counts: closedObject({
|
||||
active: Type.Number(),
|
||||
stale: Type.Number(),
|
||||
archived: Type.Number(),
|
||||
}),
|
||||
skills: Type.Array(SkillCuratorEntrySchema),
|
||||
overlaps: Type.Array(SkillOverlapCandidateSchema),
|
||||
});
|
||||
|
||||
/** Preserves retired curator action methods so clients receive an actionable error. */
|
||||
export const SkillsCuratorActionParamsSchema = closedObject({ skill: NonEmptyString });
|
||||
|
||||
export const SkillsCuratorActionResultSchema = SkillCuratorEntrySchema;
|
||||
|
||||
export const SkillCuratorLiveEntrySchema = closedObject({
|
||||
...SkillCuratorEntrySchema.properties,
|
||||
createdAtMs: Type.Union([Type.Number(), Type.Null()]),
|
||||
stateChangedAtMs: Type.Union([Type.Number(), Type.Null()]),
|
||||
});
|
||||
|
||||
export const SkillsCuratorLiveStatusResultSchema = closedObject({
|
||||
...SkillsCuratorStatusResultSchema.properties,
|
||||
inventory: Type.Literal("live-workshop"),
|
||||
skills: Type.Array(SkillCuratorLiveEntrySchema),
|
||||
});
|
||||
|
||||
export type SkillsCuratorStatusParams = Static<typeof SkillsCuratorStatusParamsSchema>;
|
||||
export type SkillsCuratorStatusResult = Static<typeof SkillsCuratorStatusResultSchema>;
|
||||
export type SkillsCuratorLiveStatusResult = Static<typeof SkillsCuratorLiveStatusResultSchema>;
|
||||
export type SkillsCuratorCompatibleStatusResult =
|
||||
| SkillsCuratorStatusResult
|
||||
| SkillsCuratorLiveStatusResult;
|
||||
export type SkillsCuratorActionParams = Static<typeof SkillsCuratorActionParamsSchema>;
|
||||
export type SkillsCuratorActionResult = Static<typeof SkillsCuratorActionResultSchema>;
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import type { Static } from "typebox";
|
||||
import { Type } from "typebox";
|
||||
import { lazyCompile } from "../protocol-validator.js";
|
||||
import { closedObject } from "./closed-object.js";
|
||||
import { NonEmptyString } from "./primitives.js";
|
||||
|
||||
export const SkillsProposalHistoryStatusParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
export const SkillsProposalHistoryScanParamsSchema = closedObject({
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
direction: Type.Optional(Type.Union([Type.Literal("older"), Type.Literal("newer")])),
|
||||
});
|
||||
|
||||
export const SkillsProposalHistoryScanResultSchema = closedObject({
|
||||
schema: Type.Literal("openclaw.skill-workshop.history-scan.v1"),
|
||||
hasScanned: Type.Boolean(),
|
||||
reviewedSessions: Type.Integer({ minimum: 0 }),
|
||||
ideasFound: Type.Integer({ minimum: 0 }),
|
||||
hasMore: Type.Boolean(),
|
||||
lastScanReviewed: Type.Integer({ minimum: 0 }),
|
||||
lastScanIdeas: Type.Integer({ minimum: 0 }),
|
||||
lastScanAt: Type.Optional(NonEmptyString),
|
||||
oldestReviewedAt: Type.Optional(NonEmptyString),
|
||||
newestReviewedAt: Type.Optional(NonEmptyString),
|
||||
});
|
||||
|
||||
export type SkillsProposalHistoryStatusParams = Static<
|
||||
typeof SkillsProposalHistoryStatusParamsSchema
|
||||
>;
|
||||
export type SkillsProposalHistoryScanParams = Static<typeof SkillsProposalHistoryScanParamsSchema>;
|
||||
export type SkillsProposalHistoryScanResult = Static<typeof SkillsProposalHistoryScanResultSchema>;
|
||||
|
||||
export const validateSkillsProposalHistoryStatusParams = lazyCompile(
|
||||
SkillsProposalHistoryStatusParamsSchema,
|
||||
);
|
||||
export const validateSkillsProposalHistoryScanParams = lazyCompile(
|
||||
SkillsProposalHistoryScanParamsSchema,
|
||||
);
|
||||
|
|
@ -473,23 +473,11 @@ export const validateSkillsUploadCommitParams = compile(S.SkillsUploadCommitPara
|
|||
export const validateSkillsUpdateParams = compile(S.SkillsUpdateParamsSchema);
|
||||
export const validateSkillsSearchParams = compile(S.SkillsSearchParamsSchema);
|
||||
export const validateSkillsDetailParams = compile(S.SkillsDetailParamsSchema);
|
||||
export const validateSkillsCuratorStatusParams = compile(S.SkillsCuratorStatusParamsSchema);
|
||||
export const validateSkillsCuratorActionParams = compile(S.SkillsCuratorActionParamsSchema);
|
||||
export const validateSkillsProposalsListParams = compile(S.SkillsProposalsListParamsSchema);
|
||||
export const validateSkillsWorkshopListParams = compile(S.SkillsWorkshopListParamsSchema);
|
||||
export const validateSkillsWorkshopChangesParams = compile(S.SkillsWorkshopChangesParamsSchema);
|
||||
export const validateSkillsWorkshopReadParams = compile(S.SkillsWorkshopReadParamsSchema);
|
||||
export const validateSkillsProposalInspectParams = compile(S.SkillsProposalInspectParamsSchema);
|
||||
export const validateSkillsProposalCreateParams = compile(S.SkillsProposalCreateParamsSchema);
|
||||
export const validateSkillsProposalUpdateParams = compile(S.SkillsProposalUpdateParamsSchema);
|
||||
export const validateSkillsProposalReviseParams = compile(S.SkillsProposalReviseParamsSchema);
|
||||
export const validateSkillsProposalRequestRevisionParams = compile(
|
||||
S.SkillsProposalRequestRevisionParamsSchema,
|
||||
);
|
||||
export const validateSkillsProposalDecisionParams = compile(S.SkillsProposalDecisionParamsSchema);
|
||||
export const validateSkillsProposalActionParams = compile(S.SkillsProposalActionParamsSchema);
|
||||
export const validateSkillsProposalEvaluateParams = compile(S.SkillsProposalEvaluateParamsSchema);
|
||||
export const validateSkillsProposalEventsListParams = compile(
|
||||
S.SkillsProposalEventsListParamsSchema,
|
||||
);
|
||||
export const validateSkillsWorkshopArchiveParams = compile(S.SkillsWorkshopArchiveParamsSchema);
|
||||
export const validateSkillsWorkshopRestoreParams = compile(S.SkillsWorkshopRestoreParamsSchema);
|
||||
export const validateSkillsSecurityVerdictsParams = compile(S.SkillsSecurityVerdictsParamsSchema);
|
||||
export const validateSkillsSkillCardParams = compile(S.SkillsSkillCardParamsSchema);
|
||||
export const validateCronListParams = compile(S.CronListParamsSchema);
|
||||
|
|
|
|||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
|
|
@ -2915,9 +2915,6 @@ importers:
|
|||
'@tanstack/virtual-core':
|
||||
specifier: 3.17.11
|
||||
version: 3.17.11
|
||||
diff:
|
||||
specifier: 9.0.0
|
||||
version: 9.0.0
|
||||
dompurify:
|
||||
specifier: 3.4.16
|
||||
version: 3.4.16
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ scenario:
|
|||
- plugins.activation-skills
|
||||
secondary:
|
||||
- channels.qa-channel-outbound
|
||||
objective: Verify a live model changes a user-owned workspace skill in place when its owner asks in chat, instead of refusing or only filing a Workshop proposal.
|
||||
objective: Verify a live model changes a user-owned workspace skill in place when its owner asks in chat, instead of refusing or copying it into the Workshop.
|
||||
gatewayConfigPatch:
|
||||
commands:
|
||||
ownerAllowFrom: [qa-channel:alice]
|
||||
|
|
@ -20,7 +20,7 @@ scenario:
|
|||
- docs/tools/skill-workshop/how-it-works.md
|
||||
codeRefs:
|
||||
- src/agents/skill-workshop-prompt.ts
|
||||
- src/agents/tools/skill-workshop-tool-description.ts
|
||||
- src/agents/tools/skill-workshop-tool.ts
|
||||
execution:
|
||||
kind: flow
|
||||
summary: Seed a workspace skill, ask a live model to change it, and read the skill file afterward.
|
||||
|
|
|
|||
|
|
@ -487,10 +487,6 @@ const ALLOWED_UI_LITERALS = new Map<string, ReadonlySet<string>>([
|
|||
]),
|
||||
],
|
||||
["apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", new Set(["Off"])],
|
||||
[
|
||||
"apps/android/app/src/main/java/ai/openclaw/app/ui/SkillWorkshopSettingsScreen.kt",
|
||||
new Set(["all", "applied", "held", "pending", "rejected"]),
|
||||
],
|
||||
[
|
||||
"apps/android/app/src/main/java/ai/openclaw/app/ui/GatewayDiagnostics.kt",
|
||||
new Set(["$versionName-dev"]),
|
||||
|
|
|
|||
|
|
@ -155,8 +155,8 @@ const ownerModules = [
|
|||
...schemaModulesSource.matchAll(/^export \* from "\.\/schema\/([^"]+)\.js";$/gmu),
|
||||
].map(([, moduleName = ""]) => moduleName);
|
||||
check(
|
||||
ownerModules.length === 74 && new Set(ownerModules).size === ownerModules.length,
|
||||
"schema-modules.ts must contain one unique 74-module owner list",
|
||||
ownerModules.length === 73 && new Set(ownerModules).size === ownerModules.length,
|
||||
"schema-modules.ts must contain one unique 73-module owner list",
|
||||
);
|
||||
check(
|
||||
schemaModulesSource.split("\n").filter(Boolean).length === ownerModules.length,
|
||||
|
|
|
|||
|
|
@ -74,10 +74,7 @@ import {
|
|||
} from "./control-ui-mock-plugins.ts";
|
||||
import { createControlUiPreviewInitScript } from "./control-ui-mock-preview.ts";
|
||||
import { skillLibraryMockInitScript } from "./control-ui-mock-skill-library.ts";
|
||||
import {
|
||||
buildSkillWorkshopMocks,
|
||||
skillWorkshopMockInitScript,
|
||||
} from "./control-ui-mock-skill-workshop.js";
|
||||
import { skillWorkshopMockInitScript } from "./control-ui-mock-skill-workshop.js";
|
||||
import { buildProfileUsageMocks } from "./control-ui-mock-usage.ts";
|
||||
|
||||
const FIXTURES = [
|
||||
|
|
@ -2069,7 +2066,6 @@ async function createChatPickerScenario(
|
|||
// heatmap stay filled no matter when the mock harness runs.
|
||||
const profileUsage = buildProfileUsageMocks(Date.now());
|
||||
const modelProviders = buildModelProviderMocks(Date.now());
|
||||
const skillWorkshop = buildSkillWorkshopMocks(Date.now());
|
||||
const richAttention = fixture === "approval";
|
||||
const cronMocks = buildCronMocks(Date.now(), {
|
||||
richAttention,
|
||||
|
|
@ -2294,15 +2290,11 @@ async function createChatPickerScenario(
|
|||
"sessions.search",
|
||||
"session.reactions.list",
|
||||
"session.reactions.set",
|
||||
"skills.workshop.archive",
|
||||
"skills.workshop.changes",
|
||||
"skills.workshop.list",
|
||||
"skills.workshop.read",
|
||||
"skills.proposals.apply",
|
||||
"skills.proposals.evaluate",
|
||||
"skills.proposals.historyScan",
|
||||
"skills.proposals.historyStatus",
|
||||
"skills.proposals.inspect",
|
||||
"skills.proposals.list",
|
||||
"skills.proposals.reject",
|
||||
"skills.proposals.requestRevision",
|
||||
"skills.workshop.restore",
|
||||
"skills.library.activate",
|
||||
"skills.library.import",
|
||||
"skills.library.list",
|
||||
|
|
@ -2756,7 +2748,6 @@ async function createChatPickerScenario(
|
|||
"wizard.start": channelWizard.start,
|
||||
"wizard.next": channelWizard.next,
|
||||
"wizard.cancel": { status: "cancelled" },
|
||||
"skills.proposals.requestRevision": skillWorkshop.requestRevision,
|
||||
"usage.cost": profileUsage.cost,
|
||||
"sessions.usage": profileUsage.sessions,
|
||||
"models.authStatus": modelAuthStatus,
|
||||
|
|
|
|||
|
|
@ -1,273 +1,298 @@
|
|||
import type {
|
||||
SkillsProposalEvaluateResult,
|
||||
SkillsProposalInspectResult,
|
||||
SkillsProposalRecordResult,
|
||||
SkillsProposalsListResult,
|
||||
SkillWorkshopChange,
|
||||
SkillWorkshopSkillSummary,
|
||||
SkillsWorkshopChangesResult,
|
||||
SkillsWorkshopListResult,
|
||||
} from "../packages/gateway-protocol/src/schema/agents-models-skills.js";
|
||||
import type { ControlUiMockGateway } from "../ui/src/test-helpers/control-ui-e2e.ts";
|
||||
|
||||
export function buildSkillWorkshopMocks(baseTime: number) {
|
||||
type MockWorkshopSkill = { summary: SkillWorkshopSkillSummary; content: string };
|
||||
|
||||
type MockWorkshopVersion = {
|
||||
id: string;
|
||||
action: SkillWorkshopChange["action"];
|
||||
createdAtMs: number;
|
||||
skill: MockWorkshopSkill;
|
||||
};
|
||||
|
||||
type SkillWorkshopMockSeed = {
|
||||
skills: MockWorkshopSkill[];
|
||||
versions: Record<string, MockWorkshopVersion[]>;
|
||||
changes: SkillWorkshopChange[];
|
||||
};
|
||||
|
||||
function buildSkillWorkshopMocks(baseTime: number): SkillWorkshopMockSeed {
|
||||
const hour = 60 * 60 * 1000;
|
||||
const day = 24 * hour;
|
||||
const proposals = [
|
||||
const skill = (
|
||||
name: string,
|
||||
description: string,
|
||||
steps: string[],
|
||||
updatedAtMs: number,
|
||||
useCount = 3,
|
||||
) => {
|
||||
const content = [
|
||||
"---",
|
||||
`name: ${name}`,
|
||||
`description: ${description}`,
|
||||
"---",
|
||||
"",
|
||||
...steps.map((step, index) => `${index + 1}. ${step}`),
|
||||
"",
|
||||
].join("\n");
|
||||
return {
|
||||
summary: {
|
||||
name,
|
||||
description,
|
||||
updatedAtMs,
|
||||
sizeBytes: content.length,
|
||||
files: ["SKILL.md"],
|
||||
useCount,
|
||||
lastUsedAtMs: updatedAtMs + hour,
|
||||
},
|
||||
content,
|
||||
};
|
||||
};
|
||||
const skills = [
|
||||
skill(
|
||||
"release-notes",
|
||||
"Use when drafting release notes from merged PRs; group by user impact.",
|
||||
[
|
||||
"List merged PRs since the last tag with `gh pr list --state merged`.",
|
||||
"Group entries by user-visible impact, not by package.",
|
||||
"Link each entry to its PR once.",
|
||||
],
|
||||
baseTime - 2 * hour,
|
||||
12,
|
||||
),
|
||||
skill(
|
||||
"flaky-test-triage",
|
||||
"Use when a CI test fails intermittently; rerun in isolation before blaming the change.",
|
||||
[
|
||||
"Rerun the failing test alone three times before reading the diff.",
|
||||
"Compare timing and ordering between passing and failing runs.",
|
||||
"Quarantine only with a linked issue.",
|
||||
],
|
||||
baseTime - 5 * hour,
|
||||
0,
|
||||
),
|
||||
skill(
|
||||
"budget-reconciliation",
|
||||
"Use when reconciling the monthly budget; match bank CSV rows before categorizing.",
|
||||
[
|
||||
"Import the bank CSV before editing categories.",
|
||||
"Match transfers by amount and date within one day.",
|
||||
"Flag unmatched rows instead of guessing a category.",
|
||||
],
|
||||
baseTime - day,
|
||||
),
|
||||
];
|
||||
const changes: SkillWorkshopChange[] = [
|
||||
{
|
||||
id: "prop-release-tweets",
|
||||
kind: "update",
|
||||
status: "pending",
|
||||
title: "Tighten release tweet drafting",
|
||||
description: "Capture the changelog-to-tweet flow the agent keeps re-deriving.",
|
||||
skillName: "release-tweets",
|
||||
skillKey: "release-tweets",
|
||||
createdAt: new Date(baseTime - 2 * hour).toISOString(),
|
||||
updatedAt: new Date(baseTime - hour).toISOString(),
|
||||
scanState: "clean",
|
||||
id: "change-release-notes-patch",
|
||||
agentId: "main",
|
||||
skillName: "release-notes",
|
||||
action: "patch",
|
||||
actor: "review",
|
||||
summary: "tightened PR grouping step",
|
||||
versionId: "20260101T000000000Z-patch",
|
||||
createdAtMs: baseTime - 2 * hour,
|
||||
},
|
||||
{
|
||||
id: "prop-crawler-etiquette",
|
||||
kind: "create",
|
||||
status: "pending",
|
||||
title: "Add crawler etiquette skill",
|
||||
description: "Rate limits and robots.txt handling learned during the docs sweep.",
|
||||
skillName: "crawler-etiquette",
|
||||
skillKey: "crawler-etiquette",
|
||||
createdAt: new Date(baseTime - 3 * day).toISOString(),
|
||||
updatedAt: new Date(baseTime - 2 * day).toISOString(),
|
||||
scanState: "clean",
|
||||
id: "change-flaky-test-triage-create",
|
||||
agentId: "main",
|
||||
skillName: "flaky-test-triage",
|
||||
action: "create",
|
||||
actor: "review",
|
||||
summary: "learned from a CI flake hunt",
|
||||
createdAtMs: baseTime - 5 * hour,
|
||||
},
|
||||
{
|
||||
id: "prop-changelog-style",
|
||||
kind: "update",
|
||||
status: "applied",
|
||||
title: "Changelog bullet style",
|
||||
description: "One bullet per entry, no hard wraps.",
|
||||
skillName: "changelog-style",
|
||||
skillKey: "changelog-style",
|
||||
createdAt: new Date(baseTime - 6 * day).toISOString(),
|
||||
updatedAt: new Date(baseTime - 5 * day).toISOString(),
|
||||
scanState: "clean",
|
||||
id: "change-budget-create",
|
||||
agentId: "main",
|
||||
skillName: "budget-reconciliation",
|
||||
action: "create",
|
||||
actor: "agent",
|
||||
summary: "monthly budget reconciliation",
|
||||
createdAtMs: baseTime - day,
|
||||
},
|
||||
] satisfies SkillsProposalsListResult["proposals"];
|
||||
const revisionHash = "b".repeat(64);
|
||||
const recordFor = (proposal: (typeof proposals)[number]): SkillsProposalRecordResult => ({
|
||||
schema: "openclaw.skill-workshop.proposal.v1",
|
||||
id: proposal.id,
|
||||
kind: proposal.kind,
|
||||
status: proposal.status,
|
||||
title: proposal.title,
|
||||
description: proposal.description,
|
||||
createdAt: proposal.createdAt,
|
||||
updatedAt: proposal.updatedAt,
|
||||
createdBy: "skill-workshop",
|
||||
proposedVersion: "2",
|
||||
draftFile: "PROPOSAL.md",
|
||||
draftHash: "a".repeat(64),
|
||||
target: {
|
||||
source: "openclaw-workshop",
|
||||
skillName: proposal.skillName,
|
||||
skillKey: proposal.skillKey,
|
||||
skillDir: `.agents/skills/${proposal.skillKey}`,
|
||||
skillFile: `.agents/skills/${proposal.skillKey}/SKILL.md`,
|
||||
{
|
||||
id: "change-standup-archive",
|
||||
agentId: "main",
|
||||
skillName: "standup-summary",
|
||||
action: "archive",
|
||||
actor: "user",
|
||||
summary: "no longer posting standups",
|
||||
versionId: "20251229T000000000Z-archive",
|
||||
createdAtMs: baseTime - 3 * day,
|
||||
},
|
||||
scan: {
|
||||
state: proposal.scanState,
|
||||
scannedAt: new Date(baseTime - hour).toISOString(),
|
||||
critical: 0,
|
||||
warn: 0,
|
||||
info: 0,
|
||||
findings: [],
|
||||
},
|
||||
});
|
||||
const evaluation: SkillsProposalEvaluateResult["evaluation"] = {
|
||||
id: "evaluation-control-ui-mock",
|
||||
proposedVersion: "2",
|
||||
revisionHash,
|
||||
trigger: "manual",
|
||||
startedAt: new Date(baseTime - 20_000).toISOString(),
|
||||
completedAt: new Date(baseTime - 18_000).toISOString(),
|
||||
outcomes: [
|
||||
];
|
||||
// The review's patch saved the pre-change copy, so its Undo has something to restore.
|
||||
const releaseNotesBeforePatch = skill(
|
||||
"release-notes",
|
||||
"Use when drafting release notes from merged PRs.",
|
||||
["List merged PRs since the last tag.", "Group entries by package."],
|
||||
baseTime - 3 * day,
|
||||
);
|
||||
const versions = {
|
||||
"release-notes": [
|
||||
{
|
||||
pluginId: "fixture-quality",
|
||||
pluginVersion: "1.0.0",
|
||||
evaluatorId: "readability",
|
||||
status: "completed",
|
||||
result: {
|
||||
summary: "The workflow is bounded and includes a recovery step.",
|
||||
decision: "pass",
|
||||
decisionReason: "No blocking findings in the sanitized fixture.",
|
||||
},
|
||||
id: "20260101T000000000Z-patch",
|
||||
action: "patch" as const,
|
||||
createdAtMs: baseTime - 2 * hour,
|
||||
skill: releaseNotesBeforePatch,
|
||||
},
|
||||
],
|
||||
"standup-summary": [
|
||||
{
|
||||
id: "20251229T000000000Z-archive",
|
||||
action: "archive" as const,
|
||||
createdAtMs: baseTime - 3 * day,
|
||||
skill: skill(
|
||||
"standup-summary",
|
||||
"Use when summarizing yesterday's work for the team standup.",
|
||||
["Collect merged PRs and closed issues from the last day.", "Keep it to three bullets."],
|
||||
baseTime - 9 * day,
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
return {
|
||||
list: {
|
||||
schema: "openclaw.skill-workshop.proposals-manifest.v1",
|
||||
updatedAt: new Date(baseTime - hour).toISOString(),
|
||||
proposals,
|
||||
installedSkills: proposals
|
||||
.filter((proposal) => proposal.status === "applied")
|
||||
.map((proposal) => ({
|
||||
name: proposal.skillName,
|
||||
skillKey: proposal.skillKey,
|
||||
description: proposal.description,
|
||||
})),
|
||||
} satisfies SkillsProposalsListResult,
|
||||
inspect: {
|
||||
cases: proposals.map((proposal) => ({
|
||||
match: { proposalId: proposal.id },
|
||||
response: {
|
||||
record: {
|
||||
...recordFor(proposal),
|
||||
...(proposal.id === "prop-release-tweets" ? { evaluation } : {}),
|
||||
},
|
||||
revisionHash,
|
||||
content: [
|
||||
`# ${proposal.title}`,
|
||||
"",
|
||||
proposal.description,
|
||||
"",
|
||||
"## Steps",
|
||||
"1. Gather the source material.",
|
||||
"2. Apply the documented workflow.",
|
||||
].join("\n"),
|
||||
supportFiles: [],
|
||||
},
|
||||
})),
|
||||
},
|
||||
evaluation,
|
||||
requestRevision: { runId: "skill-workshop-revision-mock", status: "started" },
|
||||
};
|
||||
return { skills, versions, changes };
|
||||
}
|
||||
|
||||
/** Each agent's proposal records own both mutation replies and subsequent reads. */
|
||||
function installSkillWorkshopMock(seed: ReturnType<typeof buildSkillWorkshopMocks>): void {
|
||||
/** Each agent's Workshop owns its skills, versions, and change feed. */
|
||||
function installSkillWorkshopMock(seed: SkillWorkshopMockSeed): void {
|
||||
const gateway = (window as Window & { openclawControlUiE2eGateway?: ControlUiMockGateway })
|
||||
.openclawControlUiE2eGateway;
|
||||
if (!gateway) {
|
||||
return;
|
||||
}
|
||||
const scopes = new Map<
|
||||
string,
|
||||
{
|
||||
list: SkillsProposalsListResult;
|
||||
details: Map<string, SkillsProposalInspectResult>;
|
||||
type Scope = {
|
||||
skills: Map<string, MockWorkshopSkill>;
|
||||
versions: Map<string, MockWorkshopVersion[]>;
|
||||
changes: SkillWorkshopChange[];
|
||||
};
|
||||
const scopes = new Map<string, Scope>();
|
||||
const scopeFor = (agentId: string): Scope => {
|
||||
let scope = scopes.get(agentId);
|
||||
if (!scope) {
|
||||
scope = {
|
||||
skills: new Map(seed.skills.map((entry) => [entry.summary.name, structuredClone(entry)])),
|
||||
versions: new Map(Object.entries(structuredClone(seed.versions))),
|
||||
changes: structuredClone(seed.changes).map((change) => Object.assign(change, { agentId })),
|
||||
};
|
||||
scopes.set(agentId, scope);
|
||||
}
|
||||
>();
|
||||
for (const method of [
|
||||
"list",
|
||||
"inspect",
|
||||
"evaluate",
|
||||
"apply",
|
||||
"reject",
|
||||
"historyStatus",
|
||||
"historyScan",
|
||||
"read",
|
||||
]) {
|
||||
gateway.setRequestHandler(
|
||||
method === "read" ? "skills.workshop.read" : `skills.proposals.${method}`,
|
||||
({ params: input, respond }) => {
|
||||
const reject = (message: string) =>
|
||||
respond({ __mockError: { code: "INVALID_REQUEST", message } });
|
||||
if (method === "historyStatus" || method === "historyScan") {
|
||||
return reject(
|
||||
"Historical batch scans are retired. Start a learning session from Workshop to review past conversations.",
|
||||
);
|
||||
}
|
||||
const params = (input ?? {}) as {
|
||||
agentId?: string;
|
||||
proposalId?: string;
|
||||
expectedRevisionHash?: string;
|
||||
name?: string;
|
||||
};
|
||||
const agentId = params.agentId ?? "main";
|
||||
let scope = scopes.get(agentId);
|
||||
if (!scope) {
|
||||
scope = {
|
||||
list: structuredClone(seed.list),
|
||||
details: new Map(
|
||||
seed.inspect.cases.map((entry) => [
|
||||
entry.match.proposalId,
|
||||
structuredClone(entry.response),
|
||||
]),
|
||||
),
|
||||
};
|
||||
scopes.set(agentId, scope);
|
||||
}
|
||||
if (method === "list") {
|
||||
respond(scope.list);
|
||||
return;
|
||||
}
|
||||
if (method === "read") {
|
||||
const installed = scope.list.installedSkills.find((skill) => skill.name === params.name);
|
||||
const detail =
|
||||
installed &&
|
||||
Array.from(scope.details.values()).find(
|
||||
(candidate) =>
|
||||
candidate.record.status === "applied" &&
|
||||
candidate.record.target.skillKey === installed.skillKey,
|
||||
);
|
||||
if (!installed || !detail) {
|
||||
return reject("Mock Workshop skill not found.");
|
||||
}
|
||||
respond({ ...installed, content: detail.content });
|
||||
return;
|
||||
}
|
||||
const detail = params.proposalId ? scope.details.get(params.proposalId) : undefined;
|
||||
if (!detail) {
|
||||
return reject("Unknown mock proposal; refresh the Workshop.");
|
||||
}
|
||||
if (method === "inspect") {
|
||||
respond(detail);
|
||||
return;
|
||||
}
|
||||
if (detail.record.status !== "pending") {
|
||||
return reject("Only pending proposals can be evaluated, applied, or rejected.");
|
||||
}
|
||||
if (params.expectedRevisionHash && params.expectedRevisionHash !== detail.revisionHash) {
|
||||
return reject("The proposal revision changed; refresh the proposal before retrying.");
|
||||
}
|
||||
const now = new Date().toISOString();
|
||||
detail.record.updatedAt = now;
|
||||
if (method === "evaluate") {
|
||||
const evaluation = structuredClone(seed.evaluation);
|
||||
evaluation.startedAt = now;
|
||||
evaluation.completedAt = now;
|
||||
detail.record.evaluation = evaluation;
|
||||
} else if (method === "apply") {
|
||||
detail.record.status = "applied";
|
||||
detail.record.appliedAt = now;
|
||||
const skillKey = detail.record.target.skillKey;
|
||||
const installed = {
|
||||
name: detail.record.kind === "create" ? skillKey : detail.record.target.skillName,
|
||||
skillKey,
|
||||
description: detail.record.description,
|
||||
};
|
||||
scope.list.installedSkills = [
|
||||
...scope.list.installedSkills.filter((skill) => skill.skillKey !== skillKey),
|
||||
installed,
|
||||
];
|
||||
} else {
|
||||
detail.record.status = "rejected";
|
||||
detail.record.rejectedAt = now;
|
||||
}
|
||||
const entry = scope.list.proposals.find((proposal) => proposal.id === detail.record.id);
|
||||
if (entry) {
|
||||
entry.status = detail.record.status;
|
||||
entry.updatedAt = now;
|
||||
entry.revisionHash = detail.revisionHash;
|
||||
}
|
||||
scope.list.updatedAt = now;
|
||||
if (method === "evaluate") {
|
||||
respond({ record: detail.record, evaluation: detail.record.evaluation });
|
||||
} else if (method === "apply") {
|
||||
respond({
|
||||
record: detail.record,
|
||||
targetSkillFile: `.agents/skills/${detail.record.target.skillKey}/SKILL.md`,
|
||||
});
|
||||
} else {
|
||||
respond(detail.record);
|
||||
}
|
||||
},
|
||||
);
|
||||
return scope;
|
||||
};
|
||||
const reject = (respond: (payload: unknown) => void, message: string) =>
|
||||
respond({ __mockError: { code: "INVALID_REQUEST", message } });
|
||||
const record = (
|
||||
scope: Scope,
|
||||
agentId: string,
|
||||
name: string,
|
||||
action: SkillWorkshopChange["action"],
|
||||
summary: string,
|
||||
): SkillWorkshopChange => {
|
||||
const now = Date.now();
|
||||
const live = scope.skills.get(name);
|
||||
let versionId: string | undefined;
|
||||
if (live) {
|
||||
versionId = `${new Date(now).toISOString().replace(/[-:.]/g, "")}-${action}`;
|
||||
scope.versions.set(name, [
|
||||
{ id: versionId, action, createdAtMs: now, skill: structuredClone(live) },
|
||||
...(scope.versions.get(name) ?? []),
|
||||
]);
|
||||
}
|
||||
const change: SkillWorkshopChange = {
|
||||
id: `change-${name}-${now}`,
|
||||
agentId,
|
||||
skillName: name,
|
||||
action,
|
||||
actor: "user",
|
||||
summary,
|
||||
...(versionId ? { versionId } : {}),
|
||||
createdAtMs: now,
|
||||
};
|
||||
scope.changes.unshift(change);
|
||||
return change;
|
||||
};
|
||||
const handlers: Record<
|
||||
string,
|
||||
(params: Record<string, unknown>, agentId: string, respond: (payload: unknown) => void) => void
|
||||
> = {
|
||||
"skills.workshop.list": (_params, agentId, respond) => {
|
||||
const scope = scopeFor(agentId);
|
||||
const result: SkillsWorkshopListResult = {
|
||||
agentId,
|
||||
mode: "auto",
|
||||
root: `~/.openclaw/agents/${agentId}/agent/workshop-skills`,
|
||||
skills: [...scope.skills.values()].map((entry) => entry.summary),
|
||||
archived: [...scope.versions.entries()].map(([name, versions]) => ({
|
||||
name,
|
||||
live: scope.skills.has(name),
|
||||
versions: versions.map(({ id, action, createdAtMs }) => ({ id, action, createdAtMs })),
|
||||
})),
|
||||
};
|
||||
respond(result);
|
||||
},
|
||||
"skills.workshop.changes": (params, agentId, respond) => {
|
||||
const limit = typeof params.limit === "number" ? params.limit : 50;
|
||||
const beforeMs = typeof params.beforeMs === "number" ? params.beforeMs : Infinity;
|
||||
const result: SkillsWorkshopChangesResult = {
|
||||
changes: scopeFor(agentId)
|
||||
.changes.filter((change) => change.createdAtMs < beforeMs)
|
||||
.slice(0, limit),
|
||||
};
|
||||
respond(result);
|
||||
},
|
||||
"skills.workshop.read": (params, agentId, respond) => {
|
||||
const scope = scopeFor(agentId);
|
||||
const name = typeof params.name === "string" ? params.name : "";
|
||||
const skill =
|
||||
typeof params.versionId === "string"
|
||||
? scope.versions.get(name)?.find((version) => version.id === params.versionId)?.skill
|
||||
: scope.skills.get(name);
|
||||
if (!skill) {
|
||||
reject(respond, `Mock Workshop skill not found: ${name}`);
|
||||
return;
|
||||
}
|
||||
respond({ name, filePath: "SKILL.md", content: skill.content, files: skill.summary.files });
|
||||
},
|
||||
"skills.workshop.archive": (params, agentId, respond) => {
|
||||
const scope = scopeFor(agentId);
|
||||
const name = typeof params.name === "string" ? params.name : "";
|
||||
if (!scope.skills.has(name)) {
|
||||
reject(respond, `No live Workshop skill named ${name}.`);
|
||||
return;
|
||||
}
|
||||
const reason = typeof params.reason === "string" ? params.reason : "archived";
|
||||
const change = record(scope, agentId, name, "archive", reason);
|
||||
scope.skills.delete(name);
|
||||
respond({ change });
|
||||
},
|
||||
"skills.workshop.restore": (params, agentId, respond) => {
|
||||
const scope = scopeFor(agentId);
|
||||
const name = typeof params.name === "string" ? params.name : "";
|
||||
const versions = scope.versions.get(name) ?? [];
|
||||
const version =
|
||||
typeof params.versionId === "string"
|
||||
? versions.find((entry) => entry.id === params.versionId)
|
||||
: versions[0];
|
||||
if (!version) {
|
||||
reject(respond, `No saved version of ${name} to restore.`);
|
||||
return;
|
||||
}
|
||||
const change = record(scope, agentId, name, "restore", `restored ${version.id}`);
|
||||
scope.skills.set(name, structuredClone(version.skill));
|
||||
respond({ change });
|
||||
},
|
||||
};
|
||||
for (const [method, handler] of Object.entries(handlers)) {
|
||||
gateway.setRequestHandler(method, ({ params, respond }) => {
|
||||
const input = (params ?? {}) as Record<string, unknown>;
|
||||
handler(input, typeof input.agentId === "string" ? input.agentId : "main", respond);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -691,17 +691,6 @@ const reviewedOperations = new Map([
|
|||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"src/agents/workspace-state-store.ts",
|
||||
[
|
||||
{
|
||||
tier: "T2",
|
||||
operations: ["retireWorkspaceRelocationAttestation"],
|
||||
evidence:
|
||||
"Only commands/doctor-skill-workshop-workspaces.ts:266 retires migration attestations",
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"src/agents/workspace-state-store.kernel.ts",
|
||||
[
|
||||
|
|
@ -1719,23 +1708,6 @@ const reviewedOperations = new Map([
|
|||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"src/skills/workshop/store-sqlite-record.ts",
|
||||
[
|
||||
{
|
||||
tier: "T3",
|
||||
operations: ["readStoredProposalInDatabase", "updateProposal"],
|
||||
evidence:
|
||||
"Doctor native read/update src/commands/doctor-skill-workshop-sqlite.ts:333,349; other callers use src/skills/workshop/store.worker.ts:68,129,149,167,182,188.",
|
||||
},
|
||||
{
|
||||
tier: "W",
|
||||
operations: ["insertProposal"],
|
||||
evidence:
|
||||
"Only src/skills/workshop/store-proposal.kernel.ts:73,168 inserts; sole executors store.worker.ts:139,157.",
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
"src/state/agent-deletion-journal.read.ts",
|
||||
[
|
||||
|
|
@ -2066,13 +2038,6 @@ const workerModules = new Set([
|
|||
"src/skills/lifecycle/upload-store.kernel.ts", // Skill-upload worker dispatcher only.
|
||||
"src/skills/lifecycle/upload-store.sqlite.ts", // Skill-upload worker kernels; host imports pure options only.
|
||||
|
||||
"src/skills/workshop/collection-review.kernel.ts", // Skill-workshop worker collection-review reads only.
|
||||
"src/skills/workshop/curator.kernel.ts", // Skill-workshop worker curator and usage commands only.
|
||||
"src/skills/workshop/store-proposal.kernel.ts", // Skill-workshop worker proposal commands only.
|
||||
"src/skills/workshop/store-sqlite-event.ts", // Skill-workshop and shared-state Doctor worker commands only.
|
||||
"src/skills/workshop/store-sqlite-rollback.ts", // Skill-workshop worker rollback commands only.
|
||||
"src/skills/workshop/store-sqlite-transition.ts", // Skill-workshop worker transition commands only.
|
||||
|
||||
"src/state/backup-run-records.kernel.ts", // Backup record writes are called only by the shared-state worker runtime.
|
||||
"src/state/github-personal-publication-lifecycle.ts", // Receipt SQL runs in shared-state worker dispatch; host helper enqueues commands.
|
||||
"src/state/openclaw-state-lease-worker.ts", // Lease transaction dispatch is called only by the shared-state worker backend.
|
||||
|
|
|
|||
|
|
@ -667,25 +667,15 @@ function assertRuntime(artifacts, gatewayLog) {
|
|||
readJson(path.join(artifacts, "dreaming-cron-runtime-converged.json")),
|
||||
"Settled plugin reload rewrote an already-converged dreaming job",
|
||||
);
|
||||
// Gateway retains a disabled Workshop monitor even when autonomous work is off.
|
||||
const workshop = current.rows.filter(
|
||||
(row) =>
|
||||
row.store_key === runtime.storeKey && row.declaration_key === "skill-collection-review:main",
|
||||
// The weekly Workshop curator is retired; the Gateway must not recreate its rows.
|
||||
assert.equal(
|
||||
current.rows.filter((row) => row.declaration_key?.startsWith("skill-collection-review:"))
|
||||
.length,
|
||||
0,
|
||||
"Runtime recreated a retired Workshop curator row",
|
||||
);
|
||||
assert.equal(workshop.length, 1);
|
||||
assert.equal(workshop[0].enabled, 0);
|
||||
const workshopJob = JSON.parse(workshop[0].job_json);
|
||||
assert.equal(workshopJob.name, "skill-collection-review-main");
|
||||
assert.equal(workshopJob.agentId, "main");
|
||||
assert.equal(workshopJob.enabled, false);
|
||||
assert.equal(workshopJob.payload.kind, "agentTurn");
|
||||
assert.equal(workshopJob.sessionTarget, "isolated");
|
||||
assert.equal(workshopJob.delivery.mode, "none");
|
||||
const workshopState = JSON.parse(workshop[0].state_json);
|
||||
assert.equal(workshopState.nextRunAtMs, undefined);
|
||||
assert.equal(workshopState.lastRunAtMs, undefined);
|
||||
assert.deepEqual(
|
||||
current.rows.filter((row) => row !== workshop[0]).map((row) => [row.store_key, row.job_id]),
|
||||
current.rows.map((row) => [row.store_key, row.job_id]),
|
||||
runtime.before.rows.map((row) => [row.store_key, row.job_id]),
|
||||
"Runtime changed cron row membership or order",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2478,7 +2478,7 @@ if [ "$SCENARIO" = "workshop-doctor-recovery" ]; then
|
|||
phase assert-workshop-candidate-repair node scripts/e2e/lib/upgrade-survivor/workshop-doctor-recovery.mjs doctor "$workshop_doctor_observation_root" candidate
|
||||
phase assert-workshop-recovery node scripts/e2e/lib/upgrade-survivor/workshop-doctor-recovery.mjs complete
|
||||
run_completed="1"
|
||||
echo "Workshop Doctor recovery passed: published updater refused unchanged malformed state; explicit baseline Doctor, recovered upgrade, and explicit candidate Doctor succeeded."
|
||||
echo "Workshop Doctor recovery passed: published updater refused unchanged malformed state; explicit baseline Doctor repaired it; the recovered upgrade exported pending proposal drafts and retired proposal storage; explicit candidate Doctor repaired and retired restored legacy state."
|
||||
exit 0
|
||||
fi
|
||||
if [ "$SCENARIO" = "custom-plugin-siblings" ]; then
|
||||
|
|
|
|||
|
|
@ -5,17 +5,29 @@ import fs from "node:fs";
|
|||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { stripVTControlCharacters } from "node:util";
|
||||
import { isMainThread } from "node:worker_threads";
|
||||
import {
|
||||
assertWorkshopLegacyImported,
|
||||
assertWorkshopLegacyWarning,
|
||||
assertWorkshopProposalsRetired,
|
||||
captureWorkshopLegacyState,
|
||||
RETIRED_WORKSHOP_TABLES,
|
||||
seedWorkshopLegacyProposals,
|
||||
} from "./workshop-legacy-proposals.mjs";
|
||||
|
||||
const PHYSICAL_INDEX = "idx_audit_events_kind_sequence";
|
||||
const INDEX = "idx_skill_workshop_collection_reviews_workspace_time";
|
||||
const INDEX_SQL = `CREATE INDEX ${INDEX} ON skill_workshop_collection_reviews(workspace_dir, create_time DESC, review_id DESC)`;
|
||||
// Published 2026.9.4 DDL for the retired table that carries the malformed index.
|
||||
const COLLECTION_REVIEWS_SQL = `CREATE TABLE skill_workshop_collection_reviews (
|
||||
review_id TEXT NOT NULL PRIMARY KEY,
|
||||
owner_agent_id TEXT NOT NULL,
|
||||
backup_id TEXT NOT NULL,
|
||||
create_time INTEGER NOT NULL,
|
||||
kept_names_json TEXT NOT NULL,
|
||||
written_names_json TEXT NOT NULL,
|
||||
dropped_json TEXT NOT NULL
|
||||
) STRICT`;
|
||||
const RETIREMENT_OUTPUT = /Skill Workshop proposal/u;
|
||||
const REVIEW = {
|
||||
review_id: "survivor-workshop-review",
|
||||
owner_agent_id: "main",
|
||||
|
|
@ -87,13 +99,16 @@ function hasMalformedWorkshopIndex(filename) {
|
|||
database.prepare("SELECT review_id FROM skill_workshop_collection_reviews LIMIT 1").get();
|
||||
return false;
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof Error &&
|
||||
error.message.includes("malformed database schema") &&
|
||||
error.message.includes(INDEX)
|
||||
) {
|
||||
if (!(error instanceof Error)) {
|
||||
throw error;
|
||||
}
|
||||
if (error.message.includes("malformed database schema") && error.message.includes(INDEX)) {
|
||||
return true;
|
||||
}
|
||||
// Candidate Doctor retires the table that carries the malformed index.
|
||||
if (error.message.includes("no such table: skill_workshop_collection_reviews")) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
database.close();
|
||||
|
|
@ -148,25 +163,18 @@ export function seedWorkshopIndex(stateDir, artifactRoot, stage) {
|
|||
const filename = databasePath(stateDir);
|
||||
const database = new DatabaseSync(filename);
|
||||
try {
|
||||
if (stage === "baseline") {
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO skill_workshop_collection_reviews (
|
||||
review_id, owner_agent_id, backup_id, create_time, kept_names_json, written_names_json, dropped_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(...Object.values(REVIEW));
|
||||
} else {
|
||||
assert.deepEqual(
|
||||
{
|
||||
...database
|
||||
.prepare("SELECT * FROM skill_workshop_collection_reviews WHERE review_id = ?")
|
||||
.get(REVIEW.review_id),
|
||||
},
|
||||
REVIEW,
|
||||
"Candidate reseeding must preserve the upgraded review",
|
||||
);
|
||||
if (stage === "candidate") {
|
||||
assert.deepEqual(retiredTables(database), [], "Candidate reseeding requires retired tables");
|
||||
// Restore published-build state (e.g. from a backup) onto the retired candidate schema.
|
||||
database.exec(COLLECTION_REVIEWS_SQL);
|
||||
}
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO skill_workshop_collection_reviews (
|
||||
review_id, owner_agent_id, backup_id, create_time, kept_names_json, written_names_json, dropped_json
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(...Object.values(REVIEW));
|
||||
database.exec(
|
||||
`CREATE INDEX ${INDEX} ON skill_workshop_collection_reviews(review_id, create_time DESC);`,
|
||||
);
|
||||
|
|
@ -417,6 +425,15 @@ function assertPhysicalDoctorRepair(stateDir, artifactRoot, observations, logPat
|
|||
physicalIndexCorruptAtStart: true,
|
||||
},
|
||||
);
|
||||
// The first explicit candidate Doctor after the update must not touch retired Workshop state.
|
||||
const legacy = readJson(path.join(artifactRoot, "workshop-legacy-seeded.json"));
|
||||
assert.equal(doctor.legacyExitCaptureError, undefined, "Candidate Doctor exit capture failed");
|
||||
assert.deepEqual(
|
||||
doctor.legacyAtExit,
|
||||
legacy.after,
|
||||
"Second candidate Doctor changed retired Workshop state",
|
||||
);
|
||||
assert(!RETIREMENT_OUTPUT.test(normalizeOutput(log)), "Second candidate Doctor was not a no-op");
|
||||
writeJson(path.join(artifactRoot, "physical-candidate-repair.json"), {
|
||||
status: "explicit-candidate-physical-index-repaired",
|
||||
index: PHYSICAL_INDEX,
|
||||
|
|
@ -569,7 +586,21 @@ function processWitness(observations, identity, expected) {
|
|||
return witness;
|
||||
}
|
||||
|
||||
function assertRepairedState(stateDir) {
|
||||
function retiredTables(database) {
|
||||
return RETIRED_WORKSHOP_TABLES.filter((table) =>
|
||||
database.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?").get(table),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeOutput(text) {
|
||||
return stripVTControlCharacters(text)
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.replace(/^\s*[│|]\s?/u, "").replace(/\s*[│|]\s*$/u, ""))
|
||||
.join(" ")
|
||||
.replace(/\s+/gu, " ");
|
||||
}
|
||||
|
||||
function assertRepairedState(stateDir, retired) {
|
||||
const database = new DatabaseSync(databasePath(stateDir), { readOnly: true });
|
||||
try {
|
||||
assert.equal(
|
||||
|
|
@ -577,15 +608,19 @@ function assertRepairedState(stateDir) {
|
|||
undefined,
|
||||
"Doctor left the malformed Workshop index behind",
|
||||
);
|
||||
assert.deepEqual(
|
||||
{
|
||||
...database
|
||||
.prepare("SELECT * FROM skill_workshop_collection_reviews WHERE review_id = ?")
|
||||
.get(REVIEW.review_id),
|
||||
},
|
||||
REVIEW,
|
||||
"Doctor changed the retained Workshop review",
|
||||
);
|
||||
if (retired) {
|
||||
assert.deepEqual(retiredTables(database), [], "Doctor left retired Workshop tables behind");
|
||||
} else {
|
||||
assert.deepEqual(
|
||||
{
|
||||
...database
|
||||
.prepare("SELECT * FROM skill_workshop_collection_reviews WHERE review_id = ?")
|
||||
.get(REVIEW.review_id),
|
||||
},
|
||||
REVIEW,
|
||||
"Doctor changed the retained Workshop review",
|
||||
);
|
||||
}
|
||||
assert.deepEqual(
|
||||
database
|
||||
.prepare("PRAGMA integrity_check")
|
||||
|
|
@ -643,7 +678,7 @@ export function assertWorkshopUpdateRefusal(
|
|||
export function assertWorkshopDoctorRepair(stateDir, artifactRoot, observations, stage) {
|
||||
assert(["baseline", "candidate"].includes(stage));
|
||||
const identity = readJson(path.join(artifactRoot, `workshop-${stage}.json`));
|
||||
assertRepairedState(stateDir);
|
||||
assertRepairedState(stateDir, stage === "candidate");
|
||||
const doctor = processWitness(observations, identity, {
|
||||
role: "doctor",
|
||||
exitCode: 0,
|
||||
|
|
@ -651,33 +686,24 @@ export function assertWorkshopDoctorRepair(stateDir, artifactRoot, observations,
|
|||
updateInProgress: false,
|
||||
});
|
||||
let legacy;
|
||||
let legacyWarning;
|
||||
if (
|
||||
stage === "candidate" &&
|
||||
fs.existsSync(path.join(artifactRoot, "workshop-legacy-seeded.json"))
|
||||
) {
|
||||
if (stage === "candidate") {
|
||||
// Doctor repairs the index before retiring the restored table; exports stay untouched.
|
||||
const seeded = readJson(path.join(artifactRoot, "workshop-legacy-seeded.json"));
|
||||
assert.equal(doctor.legacyExitCaptureError, undefined, "Candidate Doctor exit capture failed");
|
||||
legacy = assertWorkshopLegacyImported(stateDir, seeded, doctor.legacyAtExit);
|
||||
assert.deepEqual(
|
||||
legacy,
|
||||
readJson(path.join(artifactRoot, "workshop-recovered-upgrade.json")).legacy.after,
|
||||
"Second candidate Doctor changed imported or recoverable Workshop state",
|
||||
);
|
||||
legacy = assertWorkshopProposalsRetired(seeded, doctor.legacyAtExit);
|
||||
assert.deepEqual(
|
||||
captureWorkshopLegacyState(stateDir, seeded),
|
||||
legacy,
|
||||
"Workshop state changed after second Doctor exit",
|
||||
"Workshop state changed after candidate Doctor exit",
|
||||
);
|
||||
legacyWarning = assertWorkshopLegacyWarning(seeded, [
|
||||
fs.readFileSync(path.join(artifactRoot, "doctor.log"), "utf8"),
|
||||
]);
|
||||
const log = normalizeOutput(fs.readFileSync(path.join(artifactRoot, "doctor.log"), "utf8"));
|
||||
assert(
|
||||
log.includes("Retired the Skill Workshop proposal tables."),
|
||||
"Candidate Doctor did not retire the restored Workshop table",
|
||||
);
|
||||
assert(!log.includes("Exported"), "Candidate Doctor re-exported retired proposals");
|
||||
}
|
||||
const repair = {
|
||||
status: "explicit-doctor-repaired",
|
||||
doctor,
|
||||
...(legacy ? { legacy, legacyWarning } : {}),
|
||||
};
|
||||
const repair = { status: "explicit-doctor-repaired", doctor, ...(legacy ? { legacy } : {}) };
|
||||
writeJson(path.join(artifactRoot, `workshop-${stage}-doctor.json`), repair);
|
||||
return repair;
|
||||
}
|
||||
|
|
@ -690,7 +716,7 @@ export function assertWorkshopRecoveredUpgrade(stateDir, artifactRoot, observati
|
|||
candidate,
|
||||
"Updater did not install the exact candidate",
|
||||
);
|
||||
assertRepairedState(stateDir);
|
||||
assertRepairedState(stateDir, true);
|
||||
const updater = processWitness(observations, baseline, {
|
||||
role: "update",
|
||||
exitCode: 0,
|
||||
|
|
@ -706,29 +732,38 @@ export function assertWorkshopRecoveredUpgrade(stateDir, artifactRoot, observati
|
|||
assert.deepEqual(
|
||||
updater.legacyAtStart,
|
||||
seeded.before,
|
||||
"Published updater did not receive original legacy sidecars",
|
||||
"Published updater did not receive the original proposal state",
|
||||
);
|
||||
assert.deepEqual(
|
||||
doctor.legacyAtStart,
|
||||
seeded.before,
|
||||
"Candidate Doctor did not receive original legacy sidecars",
|
||||
"Candidate Doctor did not receive the original proposal state",
|
||||
);
|
||||
assert.equal(doctor.legacyExitCaptureError, undefined, "Candidate Doctor exit capture failed");
|
||||
const after = assertWorkshopLegacyImported(stateDir, seeded, doctor.legacyAtExit);
|
||||
const after = assertWorkshopProposalsRetired(seeded, doctor.legacyAtExit);
|
||||
assert.deepEqual(
|
||||
captureWorkshopLegacyState(stateDir, seeded),
|
||||
after,
|
||||
"Workshop state changed after candidate Doctor exit",
|
||||
);
|
||||
const warning = assertWorkshopLegacyWarning(seeded, doctor.doctorResultAtExit?.warnings);
|
||||
const legacy = { seeded, after, warning };
|
||||
const upgraded = { status: "upgraded-after-explicit-repair", updater, doctor, legacy };
|
||||
const warnings = doctor.doctorResultAtExit?.warnings;
|
||||
assert(Array.isArray(warnings), "Missing candidate Doctor result");
|
||||
assert(
|
||||
!warnings.some((warning) => RETIREMENT_OUTPUT.test(warning)),
|
||||
"Candidate Doctor reported a Skill Workshop proposal retirement warning",
|
||||
);
|
||||
const upgraded = {
|
||||
status: "upgraded-after-explicit-repair",
|
||||
updater,
|
||||
doctor,
|
||||
legacy: { seeded, after },
|
||||
};
|
||||
writeJson(path.join(artifactRoot, "workshop-recovered-upgrade.json"), upgraded);
|
||||
return upgraded;
|
||||
}
|
||||
|
||||
export function completeWorkshopRecovery(stateDir, artifactRoot) {
|
||||
assertRepairedState(stateDir);
|
||||
assertRepairedState(stateDir, true);
|
||||
const firstAttempt = readJson(path.join(artifactRoot, "workshop-published-refusal.json"));
|
||||
const baselineDoctor = readJson(path.join(artifactRoot, "workshop-baseline-doctor.json"));
|
||||
const upgrade = readJson(path.join(artifactRoot, "workshop-recovered-upgrade.json"));
|
||||
|
|
@ -741,9 +776,8 @@ export function completeWorkshopRecovery(stateDir, artifactRoot) {
|
|||
assert.deepEqual(
|
||||
candidateDoctor.legacy,
|
||||
upgrade.legacy.after,
|
||||
"Missing candidate Doctor idempotence evidence for the imported legacy proposals",
|
||||
"Candidate Doctor changed exported Workshop proposals",
|
||||
);
|
||||
assert.equal(candidateDoctor.legacyWarning.warning, upgrade.legacy.warning.warning);
|
||||
const result = { firstAttempt, baselineDoctor, upgrade, candidateDoctor };
|
||||
writeJson(path.join(artifactRoot, "workshop-doctor-recovery.json"), result);
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -3,18 +3,50 @@ import { createHash } from "node:crypto";
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { stripVTControlCharacters } from "node:util";
|
||||
|
||||
const NOW = "2026-07-29T00:00:00.000Z";
|
||||
const WORKSHOP_ROOT = "agents/main/agent/workshop-skills";
|
||||
const EXPORT_ROOT = `${WORKSHOP_ROOT}/.archive/.retired-proposals`;
|
||||
const PROPOSALS_DIR = "skill-workshop/proposals";
|
||||
export const RETIRED_WORKSHOP_TABLES = [
|
||||
"skill_workshop_collection_reviews",
|
||||
"skill_workshop_proposal_events",
|
||||
"skill_workshop_proposal_rollbacks",
|
||||
"skill_workshop_proposals",
|
||||
];
|
||||
// Every proposal shape the published build can hold: SQLite rows (incl. a generation draft) and a pre-SQLite JSON bundle.
|
||||
const PROPOSALS = [
|
||||
{
|
||||
key: "survivor-pending",
|
||||
status: "pending",
|
||||
json: false,
|
||||
draftFile: "generations/0f5c2a8e-6d1b-4c3a-9e7f-2b8d4a6c1e90/PROPOSAL.md",
|
||||
support: true,
|
||||
},
|
||||
{ key: "survivor-quarantined", status: "quarantined", json: false, draftFile: "PROPOSAL.md" },
|
||||
{
|
||||
key: "survivor-applied",
|
||||
status: "applied",
|
||||
json: false,
|
||||
draftFile: "PROPOSAL.md",
|
||||
support: true,
|
||||
},
|
||||
{
|
||||
key: "survivor-legacy-json",
|
||||
status: "pending",
|
||||
json: true,
|
||||
draftFile: "PROPOSAL.md",
|
||||
support: true,
|
||||
},
|
||||
];
|
||||
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
||||
const readJson = (filename) => JSON.parse(fs.readFileSync(filename, "utf8"));
|
||||
|
||||
function writeFile(filename, content) {
|
||||
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
||||
fs.writeFileSync(filename, content, { flag: "wx", mode: 0o600 });
|
||||
function databasePath(stateDir) {
|
||||
return path.join(stateDir, "state", "openclaw.sqlite");
|
||||
}
|
||||
|
||||
/** Independently seed the shipped v1 sidecar contract, without importing candidate code. */
|
||||
/** Independently seed published-build proposal state, without importing candidate code. */
|
||||
export function seedWorkshopLegacyProposals(stateDir, artifactRoot) {
|
||||
assert.equal(
|
||||
readJson(path.join(artifactRoot, "workshop-baseline-doctor.json")).status,
|
||||
|
|
@ -22,267 +54,181 @@ export function seedWorkshopLegacyProposals(stateDir, artifactRoot) {
|
|||
"Seed legacy proposals only after the published baseline Doctor repair",
|
||||
);
|
||||
const files = {};
|
||||
const bundles = [
|
||||
{ key: "survivor-recovered", owner: "main", recover: true },
|
||||
{ key: "survivor-retained", owner: "retired-agent", recover: false },
|
||||
].map(({ key, owner, recover }) => {
|
||||
const id = `${key}-20260729-1234567890`;
|
||||
const directory = `skill-workshop/proposals/${id}`;
|
||||
const target = recover
|
||||
? `workspace/skills/${key}`
|
||||
: `agents/${owner}/agent/workshop-skills/${key}`;
|
||||
const destination = `agents/main/agent/workshop-skills/${key}`;
|
||||
const support = `Preserved ${key} support.\n`;
|
||||
const header = `name: "${key}"\ndescription: "Published updater Workshop fixture"`;
|
||||
const body = `# ${key}\n\nPreserve the original proposal and support bytes.\n`;
|
||||
const content = `---\n${header}\nstatus: proposal\nversion: "v1"\ndate: "${NOW}"\n---\n\n${body}`;
|
||||
const installed = `---\n${header}\n---\n\n${body}`;
|
||||
const record = {
|
||||
schema: "openclaw.skill-workshop.proposal.v1",
|
||||
id,
|
||||
kind: "create",
|
||||
status: "pending",
|
||||
title: `Create ${key}`,
|
||||
description: "Published updater Workshop fixture",
|
||||
createdAt: NOW,
|
||||
updatedAt: NOW,
|
||||
createdBy: "skill-workshop",
|
||||
origin: { agentId: owner, runId: `${key}-run` },
|
||||
originRunIds: [`${key}-run`],
|
||||
originRunMutationCounts: { [`${key}-run`]: 1 },
|
||||
proposedVersion: "v1",
|
||||
draftFile: "PROPOSAL.md",
|
||||
draftHash: sha256(content),
|
||||
supportFiles: [
|
||||
{
|
||||
path: "references/proof.md",
|
||||
sizeBytes: Buffer.byteLength(support),
|
||||
hash: sha256(support),
|
||||
},
|
||||
],
|
||||
target: {
|
||||
skillName: key,
|
||||
skillKey: key,
|
||||
skillDir: path.join(stateDir, target),
|
||||
skillFile: path.join(stateDir, target, "SKILL.md"),
|
||||
source: "openclaw-workspace",
|
||||
},
|
||||
scan: { state: "clean", scannedAt: NOW, critical: 0, warn: 0, info: 0, findings: [] },
|
||||
};
|
||||
const rollback = recover
|
||||
? {
|
||||
schema: "openclaw.skill-workshop.rollback.v1",
|
||||
proposalId: id,
|
||||
writtenAt: NOW,
|
||||
targetSkillFile: record.target.skillFile,
|
||||
action: "create",
|
||||
supportFiles: [{ path: "references/proof.md", existed: false }],
|
||||
}
|
||||
: undefined;
|
||||
const contentByPath = {
|
||||
[`${directory}/proposal.json`]: `${JSON.stringify(record, null, 2)}\n`,
|
||||
[`${directory}/PROPOSAL.md`]: content,
|
||||
[`${directory}/references/proof.md`]: support,
|
||||
[`${target}/SKILL.md`]: installed,
|
||||
[`${target}/references/proof.md`]: support,
|
||||
...(rollback
|
||||
? { [`${directory}/rollback.json`]: `${JSON.stringify(rollback, null, 2)}\n` }
|
||||
: {}),
|
||||
};
|
||||
for (const [relative, bytes] of Object.entries(contentByPath)) {
|
||||
writeFile(path.join(stateDir, relative), bytes);
|
||||
files[relative] = sha256(bytes);
|
||||
}
|
||||
if (recover) {
|
||||
for (const relative of [`${destination}/SKILL.md`, `${destination}/references/proof.md`]) {
|
||||
assert.equal(fs.existsSync(path.join(stateDir, relative)), false);
|
||||
files[relative] = null;
|
||||
const exported = {};
|
||||
const put = (relative, bytes) => {
|
||||
const filename = path.join(stateDir, relative);
|
||||
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
||||
fs.writeFileSync(filename, bytes, { flag: "wx", mode: 0o600 });
|
||||
files[relative] = sha256(bytes);
|
||||
};
|
||||
const database = new DatabaseSync(databasePath(stateDir));
|
||||
try {
|
||||
database.exec("BEGIN IMMEDIATE");
|
||||
for (const { key, status, json, draftFile, support } of PROPOSALS) {
|
||||
const id = `${key}-20260729`;
|
||||
const draft = `---\nname: "${key}"\ndescription: "Published updater Workshop fixture"\n---\n\n# ${key}\n\nPreserve the ${status} draft bytes.\n`;
|
||||
const supportBytes = `Preserved ${key} support.\n`;
|
||||
const bundle = path.posix.join(PROPOSALS_DIR, id, path.posix.dirname(draftFile));
|
||||
const record = {
|
||||
schema: "openclaw.skill-workshop.proposal.v1",
|
||||
id,
|
||||
kind: "create",
|
||||
status,
|
||||
title: `Create ${key}`,
|
||||
createdAt: NOW,
|
||||
updatedAt: NOW,
|
||||
draftFile,
|
||||
draftHash: sha256(draft),
|
||||
supportFiles: support
|
||||
? [
|
||||
{
|
||||
path: "references/proof.md",
|
||||
sizeBytes: Buffer.byteLength(supportBytes),
|
||||
hash: sha256(supportBytes),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
target: { skillName: key },
|
||||
};
|
||||
put(`${bundle}/PROPOSAL.md`, draft);
|
||||
if (support) {
|
||||
put(`${bundle}/references/proof.md`, supportBytes);
|
||||
}
|
||||
if (json) {
|
||||
put(`${PROPOSALS_DIR}/${id}/proposal.json`, `${JSON.stringify(record, null, 2)}\n`);
|
||||
} else {
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO skill_workshop_proposals (
|
||||
proposal_id, record_json, owner_agent_id, kind, status, created_at, updated_at, draft_hash,
|
||||
applied_at, quarantined_at
|
||||
) VALUES (?, ?, 'main', 'create', ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
id,
|
||||
JSON.stringify(record),
|
||||
status,
|
||||
NOW,
|
||||
NOW,
|
||||
record.draftHash,
|
||||
status === "applied" ? NOW : null,
|
||||
status === "quarantined" ? NOW : null,
|
||||
);
|
||||
}
|
||||
if (status === "applied") {
|
||||
// Applied proposals already live as Workshop skills; retirement must leave them alone.
|
||||
put(`${WORKSHOP_ROOT}/${key}/SKILL.md`, draft);
|
||||
put(`${WORKSHOP_ROOT}/${key}/references/proof.md`, supportBytes);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO skill_workshop_proposal_rollbacks
|
||||
(proposal_id, written_at, target_skill_file, action) VALUES (?, ?, ?, 'create')`,
|
||||
)
|
||||
.run(id, NOW, path.join(stateDir, WORKSHOP_ROOT, key, "SKILL.md"));
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO skill_workshop_proposal_events
|
||||
(event_id, proposal_id, proposed_version, revision_hash, event_type, occurred_at, actor_json)
|
||||
VALUES (?, ?, 'v1', ?, 'applied', ?, '{"type":"system"}')`,
|
||||
)
|
||||
.run(`${id}-applied`, id, record.draftHash, NOW);
|
||||
continue;
|
||||
}
|
||||
exported[`${EXPORT_ROOT}/${id}/SKILL.md`] = sha256(draft);
|
||||
if (support) {
|
||||
exported[`${EXPORT_ROOT}/${id}/references/proof.md`] = sha256(supportBytes);
|
||||
}
|
||||
}
|
||||
return { record, rollback, directory, target, destination, recover };
|
||||
});
|
||||
const manifest = "skill-workshop/proposals.json";
|
||||
const manifestBytes = `${JSON.stringify({ schema: "openclaw.skill-workshop.proposals-manifest.v1", updatedAt: NOW, proposals: [] })}\n`;
|
||||
writeFile(path.join(stateDir, manifest), manifestBytes);
|
||||
files[manifest] = sha256(manifestBytes);
|
||||
const fixture = { bundles, files };
|
||||
const before = captureWorkshopLegacyState(stateDir, fixture);
|
||||
assert.deepEqual(before.files, files);
|
||||
assert.deepEqual(before.database, { proposals: [], rollbacks: [], events: [] });
|
||||
const retained = bundles.find((bundle) => !bundle.recover);
|
||||
const retainedWarning = `Preserved Skill Workshop proposal ${path.join(stateDir, retained.directory)} for manual review: owning agent "retired-agent" is not configured; target ${retained.record.target.skillDir} (candidate agents: none). Review the retained metadata and configured workspace ownership before retrying Doctor.`;
|
||||
assert(
|
||||
retainedWarning.length <= 500,
|
||||
"Fixture warning exceeds the Doctor IPC limit; use a shorter isolated state root",
|
||||
);
|
||||
const seeded = { ...fixture, before, retainedWarning };
|
||||
writeFile(
|
||||
database.exec("COMMIT");
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
put("skill-workshop/proposals.json", `${JSON.stringify({ proposals: [] })}\n`);
|
||||
const fixture = {
|
||||
paths: [...Object.keys(files), ...Object.keys(exported)],
|
||||
before: {
|
||||
files: { ...files, ...Object.fromEntries(Object.keys(exported).map((key) => [key, null])) },
|
||||
exports: null,
|
||||
proposalsDir: true,
|
||||
tables: {
|
||||
skill_workshop_collection_reviews: 1,
|
||||
skill_workshop_proposal_events: 1,
|
||||
skill_workshop_proposal_rollbacks: 1,
|
||||
skill_workshop_proposals: 3,
|
||||
},
|
||||
},
|
||||
after: {
|
||||
files: Object.fromEntries(
|
||||
Object.entries({ ...files, ...exported }).map(([relative, hash]) => [
|
||||
relative,
|
||||
relative.startsWith("skill-workshop/") ? null : hash,
|
||||
]),
|
||||
),
|
||||
exports: PROPOSALS.filter(({ status }) => status !== "applied")
|
||||
.map(({ key }) => `${key}-20260729`)
|
||||
.toSorted(),
|
||||
proposalsDir: false,
|
||||
tables: {},
|
||||
},
|
||||
};
|
||||
assert.deepEqual(captureWorkshopLegacyState(stateDir, fixture), fixture.before);
|
||||
fs.writeFileSync(
|
||||
path.join(artifactRoot, "workshop-legacy-seeded.json"),
|
||||
`${JSON.stringify(seeded, null, 2)}\n`,
|
||||
`${JSON.stringify(fixture, null, 2)}\n`,
|
||||
{ flag: "wx", mode: 0o600 },
|
||||
);
|
||||
return seeded;
|
||||
return fixture;
|
||||
}
|
||||
|
||||
export function captureWorkshopLegacyState(stateDir, fixture) {
|
||||
const files = Object.fromEntries(
|
||||
Object.keys(fixture.files).map((relative) => {
|
||||
fixture.paths.map((relative) => {
|
||||
const filename = path.join(stateDir, relative);
|
||||
if (!fs.existsSync(filename)) {
|
||||
return [relative, null];
|
||||
}
|
||||
assert(
|
||||
fs.lstatSync(filename).isFile(),
|
||||
`Legacy fixture path is not a regular file: ${relative}`,
|
||||
);
|
||||
assert(fs.lstatSync(filename).isFile(), `Workshop path is not a regular file: ${relative}`);
|
||||
return [relative, sha256(fs.readFileSync(filename))];
|
||||
}),
|
||||
);
|
||||
const database = new DatabaseSync(path.join(stateDir, "state", "openclaw.sqlite"), {
|
||||
readOnly: true,
|
||||
});
|
||||
const exportRoot = path.join(stateDir, EXPORT_ROOT);
|
||||
const database = new DatabaseSync(databasePath(stateDir), { readOnly: true });
|
||||
try {
|
||||
const ids = fixture.bundles.map(({ record }) => record.id);
|
||||
const rows = (table, order) =>
|
||||
database
|
||||
.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?")
|
||||
.get(table)
|
||||
? database
|
||||
.prepare(`SELECT * FROM ${table} WHERE proposal_id IN (?, ?) ORDER BY ${order}`)
|
||||
.all(...ids)
|
||||
.map((row) => Object.assign({}, row))
|
||||
: [];
|
||||
const tables = {};
|
||||
for (const table of RETIRED_WORKSHOP_TABLES) {
|
||||
if (
|
||||
database
|
||||
.prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?")
|
||||
.get(table)
|
||||
) {
|
||||
tables[table] = Number(database.prepare(`SELECT count(*) AS n FROM ${table}`).get().n);
|
||||
}
|
||||
}
|
||||
return {
|
||||
files,
|
||||
database: {
|
||||
proposals: rows("skill_workshop_proposals", "proposal_id"),
|
||||
rollbacks: rows("skill_workshop_proposal_rollbacks", "proposal_id"),
|
||||
events: rows("skill_workshop_proposal_events", "sequence"),
|
||||
},
|
||||
exports: fs.existsSync(exportRoot) ? fs.readdirSync(exportRoot).toSorted() : null,
|
||||
proposalsDir: fs.existsSync(path.join(stateDir, PROPOSALS_DIR)),
|
||||
tables,
|
||||
};
|
||||
} finally {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
export function assertWorkshopLegacyImported(stateDir, fixture, current) {
|
||||
assert(current, "Missing Doctor legacy exit snapshot");
|
||||
const recovered = fixture.bundles.find((bundle) => bundle.recover);
|
||||
const expectedFiles = { ...fixture.files, "skill-workshop/proposals.json": null };
|
||||
for (const name of ["proposal.json", "rollback.json"]) {
|
||||
expectedFiles[`${recovered.directory}/${name}`] = null;
|
||||
}
|
||||
for (const name of ["SKILL.md", "references/proof.md"]) {
|
||||
expectedFiles[`${recovered.destination}/${name}`] =
|
||||
fixture.files[`${recovered.target}/${name}`];
|
||||
expectedFiles[`${recovered.target}/${name}`] = null;
|
||||
}
|
||||
/** Pending/quarantined drafts are exported, applied skills stay live, retired storage is gone. */
|
||||
export function assertWorkshopProposalsRetired(fixture, current) {
|
||||
assert(current, "Missing Doctor Workshop exit snapshot");
|
||||
assert.deepEqual(current.tables, {}, "Doctor left retired Workshop tables behind");
|
||||
assert.equal(current.proposalsDir, false, "Doctor left retired proposal files behind");
|
||||
assert.deepEqual(
|
||||
current.exports,
|
||||
fixture.after.exports,
|
||||
"Doctor must export exactly the pending and quarantined proposals",
|
||||
);
|
||||
assert.deepEqual(
|
||||
current.files,
|
||||
expectedFiles,
|
||||
"Doctor changed or lost retained Workshop artifact bytes",
|
||||
);
|
||||
const { proposals, rollbacks, events } = current.database;
|
||||
assert.equal(
|
||||
proposals.length,
|
||||
1,
|
||||
"Candidate Doctor did not import exactly the owned legacy proposal",
|
||||
);
|
||||
const row = proposals[0];
|
||||
const record = JSON.parse(row.record_json);
|
||||
assert.match(record.appliedAt, /^\d{4}-\d{2}-\d{2}T/u);
|
||||
assert.equal(record.updatedAt, record.appliedAt);
|
||||
assert.deepEqual(record, {
|
||||
...recovered.record,
|
||||
status: "applied",
|
||||
updatedAt: record.appliedAt,
|
||||
appliedAt: record.appliedAt,
|
||||
target: {
|
||||
...recovered.record.target,
|
||||
skillDir: path.join(stateDir, recovered.destination),
|
||||
skillFile: path.join(stateDir, recovered.destination, "SKILL.md"),
|
||||
source: "openclaw-workshop",
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
row,
|
||||
{
|
||||
proposal_id: record.id,
|
||||
record_json: row.record_json,
|
||||
owner_agent_id: "main",
|
||||
kind: record.kind,
|
||||
status: record.status,
|
||||
created_at: record.createdAt,
|
||||
updated_at: record.updatedAt,
|
||||
draft_hash: record.draftHash,
|
||||
origin_agent_id: record.origin?.agentId ?? null,
|
||||
origin_session_key: record.origin?.sessionKey ?? null,
|
||||
origin_run_id: record.origin?.runId ?? null,
|
||||
origin_message_id: record.origin?.messageId ?? null,
|
||||
applied_at: record.appliedAt ?? null,
|
||||
rejected_at: record.rejectedAt ?? null,
|
||||
quarantined_at: record.quarantinedAt ?? null,
|
||||
stale_at: record.staleAt ?? null,
|
||||
status_reason: record.statusReason ?? null,
|
||||
},
|
||||
"Authoritative Workshop proposal columns disagree with the recovered record",
|
||||
);
|
||||
assert.equal(rollbacks.length, 1, "Imported rollback metadata was lost or duplicated");
|
||||
assert.deepEqual(
|
||||
{ ...rollbacks[0] },
|
||||
{
|
||||
proposal_id: recovered.record.id,
|
||||
written_at: recovered.rollback.writtenAt,
|
||||
target_skill_file: recovered.rollback.targetSkillFile,
|
||||
action: "create",
|
||||
previous_content_hash: null,
|
||||
previous_content: null,
|
||||
support_files_json: JSON.stringify(recovered.rollback.supportFiles),
|
||||
},
|
||||
"Imported rollback payload changed",
|
||||
);
|
||||
assert.equal(events.length, 1, "Recovery must record exactly one applied event");
|
||||
const event = events[0];
|
||||
assert.equal(event.proposal_id, recovered.record.id);
|
||||
assert.equal(event.event_type, "applied");
|
||||
assert.equal(event.proposed_version, "v1");
|
||||
assert.equal(event.occurred_at, record.appliedAt);
|
||||
assert.deepEqual(JSON.parse(event.actor_json), { type: "system" });
|
||||
assert.deepEqual(
|
||||
JSON.parse(event.payload_json),
|
||||
[1, { recovered: true }, null],
|
||||
"Recovered Workshop event payload changed",
|
||||
);
|
||||
assert.equal(
|
||||
event.revision_hash,
|
||||
sha256(
|
||||
JSON.stringify({
|
||||
proposedVersion: "v1",
|
||||
contentSha256: record.draftHash,
|
||||
supportFiles: record.supportFiles.map((file) => ({
|
||||
path: file.path,
|
||||
sha256: file.hash,
|
||||
sizeBytes: file.sizeBytes,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
fixture.after.files,
|
||||
"Doctor changed exported drafts or live Workshop skill bytes",
|
||||
);
|
||||
return current;
|
||||
}
|
||||
|
||||
export function assertWorkshopLegacyWarning(fixture, outputs) {
|
||||
assert(
|
||||
Array.isArray(outputs) && outputs.every((output) => typeof output === "string"),
|
||||
"Missing Doctor warning output",
|
||||
);
|
||||
const normalize = (text) =>
|
||||
stripVTControlCharacters(text)
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.replace(/^\s*[│|]\s?/u, "").replace(/\s*[│|]\s*$/u, ""))
|
||||
.join(" ")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
const expected = normalize(fixture.retainedWarning);
|
||||
const observed = outputs.find((output) => normalize(output).includes(expected));
|
||||
assert(observed !== undefined, "Missing complete recoverable Workshop manual-review warning");
|
||||
return { warning: fixture.retainedWarning, outputSha256: sha256(observed) };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -787,7 +787,6 @@ const STORAGE_MODULE_WORK_SECONDS = new Map<string, number>([
|
|||
["src/channels/message-access/operator-authority.test.ts", 31.775],
|
||||
["src/agents/subagents/registry/subagent-registry.persistence.test.ts", 23.639],
|
||||
["src/auto-reply/reply/session.acp-reset-routing.test.ts", 8.938],
|
||||
["src/agents/tools/skill-workshop-tool.support-paths.test.ts", 1.686],
|
||||
["src/claws/package-update.test.ts", 4.164],
|
||||
["src/cli/update-cli.git-service.test.ts", 24.578],
|
||||
["src/flows/doctor-health.fleet-preflight.test.ts", 8.648],
|
||||
|
|
|
|||
|
|
@ -558,13 +558,6 @@ const policyTestWatches: readonly PolicyTestWatch[] = [
|
|||
"src/state/openclaw-state.worker.ts",
|
||||
],
|
||||
},
|
||||
...[
|
||||
"src/commands/doctor-skill-workshop-relocation.reservations.test.ts",
|
||||
"src/commands/doctor-skill-workshop-sqlite.relocation.test.ts",
|
||||
].map((testFile): PolicyTestWatch => ({
|
||||
testFile,
|
||||
watchGlobs: ["src/skills/workshop/store.worker.ts"],
|
||||
})),
|
||||
{
|
||||
testFile: "src/commands/models/auth.minimax-chat.test.ts",
|
||||
watchGlobs: [
|
||||
|
|
@ -1198,17 +1191,6 @@ const policyTestWatches: readonly PolicyTestWatch[] = [
|
|||
testFile: "src/skills/library/persistence.test.ts",
|
||||
watchGlobs: ["src/skills/library/persistence-child.test-support.ts"],
|
||||
},
|
||||
{
|
||||
testFile: "src/skills/workshop/experience-review.apply.test.ts",
|
||||
watchGlobs: [
|
||||
"src/infra/sqlite-store.worker.ts",
|
||||
"src/skills/workshop/curator.kernel.ts",
|
||||
"src/skills/workshop/store-proposal.kernel.ts",
|
||||
"src/skills/workshop/store.worker.ts",
|
||||
"src/state/openclaw-state-worker-runtime.ts",
|
||||
"src/state/openclaw-state.worker.ts",
|
||||
],
|
||||
},
|
||||
{
|
||||
testFile: "src/state/openclaw-agent-participants-migration.test.ts",
|
||||
watchGlobs: ["test/fixtures/sqlite/openclaw-agent-schema-v22.sql"],
|
||||
|
|
@ -1742,16 +1724,9 @@ const policyTestWatches: readonly PolicyTestWatch[] = [
|
|||
watchGlobs: ["src/infra/state-migrations.snapshot.worker.ts"],
|
||||
},
|
||||
{
|
||||
testFile: "src/infra/state-migrations.skill-workshop.test.ts",
|
||||
watchGlobs: ["src/state/openclaw-state.worker.ts"],
|
||||
},
|
||||
...[
|
||||
"src/infra/update-candidate-state.test.ts",
|
||||
"src/infra/update-candidate-workspace-rehearsal.test.ts",
|
||||
].map((testFile): PolicyTestWatch => ({
|
||||
testFile,
|
||||
testFile: "src/infra/update-candidate-state.test.ts",
|
||||
watchGlobs: ["src/infra/update-candidate-state.worker.ts"],
|
||||
})),
|
||||
},
|
||||
{
|
||||
testFile: "src/state/agent-database-admission.test.ts",
|
||||
watchGlobs: [
|
||||
|
|
|
|||
|
|
@ -1441,13 +1441,6 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/agents/tools/sessions-spawn-tool.test.ts",
|
||||
"src/agents/tools/sessions-tool.batch.test.ts",
|
||||
"src/agents/tools/sessions.test.ts",
|
||||
"src/agents/tools/skill-workshop-tool.collection-restore.test.ts",
|
||||
"src/agents/tools/skill-workshop-tool.evaluation.test.ts",
|
||||
"src/agents/tools/skill-workshop-tool.lifecycle.test.ts",
|
||||
"src/agents/tools/skill-workshop-tool.projection.test.ts",
|
||||
"src/agents/tools/skill-workshop-tool.review.test.ts",
|
||||
"src/agents/tools/skill-workshop-tool.revision-constraint.test.ts",
|
||||
"src/agents/tools/skill-workshop-tool.skill-identity.test.ts",
|
||||
"src/agents/tools/skill-workshop-tool.test.ts",
|
||||
"src/agents/tools/subagents-tool.test.ts",
|
||||
"src/agents/tools/swarm-tools.integration.test.ts",
|
||||
|
|
@ -1804,10 +1797,9 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/cli/secrets-store-cli.test.ts",
|
||||
"src/cli/send-runtime/channel-outbound-send.test.ts",
|
||||
"src/cli/skills-cli.commands.test.ts",
|
||||
"src/cli/skills-cli.curator.test.ts",
|
||||
"src/cli/skills-cli.sag.process.test.ts",
|
||||
"src/cli/skills-cli.verify.process.test.ts",
|
||||
"src/cli/skills-cli.workshop-cache.test.ts",
|
||||
"src/cli/skills-cli.workshop.test.ts",
|
||||
"src/cli/skills-library-cli.test.ts",
|
||||
"src/cli/state-dir-gateway-check.server.test.ts",
|
||||
"src/cli/update-cli.admission-ledger.test.ts",
|
||||
|
|
@ -2046,7 +2038,7 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/commands/doctor-session-transcripts.incident.test.ts",
|
||||
"src/commands/doctor-session-transcripts.missing-index.test.ts",
|
||||
"src/commands/doctor-session-worktree-workspace.test.ts",
|
||||
"src/commands/doctor-skill-workshop-workspace.test.ts",
|
||||
"src/commands/doctor-skill-workshop-retire-proposals.test.ts",
|
||||
"src/commands/doctor-state-migrations.test.ts",
|
||||
"src/commands/doctor-update-refusal.test.ts",
|
||||
"src/commands/doctor-update-rehearsal-producer.test.ts",
|
||||
|
|
@ -2396,7 +2388,6 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/cron/service/timer.test.ts",
|
||||
"src/cron/service/timer.timeout-watchdog.test.ts",
|
||||
"src/cron/session-reaper.test.ts",
|
||||
"src/cron/skill-collection-review-monitor.test.ts",
|
||||
"src/cron/store.retired-review.test.ts",
|
||||
"src/cron/store.test.ts",
|
||||
"src/cron/store/run-receipt-store.test.ts",
|
||||
|
|
@ -2544,6 +2535,7 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/gateway/mcp-http.question-authority.test.ts",
|
||||
"src/gateway/mcp-http.runtime.test.ts",
|
||||
"src/gateway/mcp-http.test.ts",
|
||||
"src/gateway/mcp-http/mediated-exec-policy.test.ts",
|
||||
"src/gateway/mention-inbox.sharing-target.test.ts",
|
||||
"src/gateway/method-scopes.own-session.test.ts",
|
||||
"src/gateway/models-http.test.ts",
|
||||
|
|
@ -2572,7 +2564,6 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/gateway/server-close.port-reservation.test.ts",
|
||||
"src/gateway/server-close.test.ts",
|
||||
"src/gateway/server-connection-state.test.ts",
|
||||
"src/gateway/server-cron-skill-review-jobs.test.ts",
|
||||
"src/gateway/server-cron.test.ts",
|
||||
"src/gateway/server-http.control-ui.test.ts",
|
||||
"src/gateway/server-http.openai-sandbox.test.ts",
|
||||
|
|
@ -2792,7 +2783,6 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/gateway/server-methods/sessions.reclaim.test.ts",
|
||||
"src/gateway/server-methods/sessions.send-followup-status.test.ts",
|
||||
"src/gateway/server-methods/sessions.send-yield-resume.test.ts",
|
||||
"src/gateway/server-methods/skills.proposals.test.ts",
|
||||
"src/gateway/server-methods/skills.remote.test.ts",
|
||||
"src/gateway/server-methods/system-agent-approval-authority.test.ts",
|
||||
"src/gateway/server-methods/system-agent-approval.test.ts",
|
||||
|
|
@ -3313,7 +3303,6 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/infra/state-migrations.receipts.test.ts",
|
||||
"src/infra/state-migrations.retained-deleted-agent.test.ts",
|
||||
"src/infra/state-migrations.shared-auth-store.test.ts",
|
||||
"src/infra/state-migrations.skill-workshop.test.ts",
|
||||
"src/infra/state-migrations.test.ts",
|
||||
"src/infra/state-migrations.transcript-directives.test.ts",
|
||||
"src/infra/state-migrations.web-push.test.ts",
|
||||
|
|
@ -3336,7 +3325,6 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/infra/update-candidate-state.online-backup.process.test.ts",
|
||||
"src/infra/update-candidate-state.schema-lifetime.test.ts",
|
||||
"src/infra/update-candidate-state.schema.test.ts",
|
||||
"src/infra/update-candidate-workspace-rehearsal.test.ts",
|
||||
"src/infra/update-failure-report-prepare.test.ts",
|
||||
"src/infra/update-failure-report.test.ts",
|
||||
"src/infra/update-global.bun-owner.test.ts",
|
||||
|
|
@ -3577,13 +3565,8 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"src/skills/runtime/session-snapshot.test.ts",
|
||||
"src/skills/runtime/tool-dispatch.test.ts",
|
||||
"src/skills/runtime/workspace-worker.test.ts",
|
||||
"src/skills/workshop/collection-restore.test.ts",
|
||||
"src/skills/workshop/experience-review.apply.test.ts",
|
||||
"src/skills/workshop/policy.test.ts",
|
||||
"src/skills/workshop/revision-atomicity.test.ts",
|
||||
"src/skills/workshop/service-evaluation.test.ts",
|
||||
"src/skills/workshop/service-lifecycle-hooks.test.ts",
|
||||
"src/skills/workshop/service.test.ts",
|
||||
"src/skills/workshop/experience-review-scheduler.test.ts",
|
||||
"src/skills/workshop/library.test.ts",
|
||||
"src/snapshot/git-backup-streaming.test.ts",
|
||||
"src/snapshot/git-backup.test.ts",
|
||||
"src/snapshot/local-repository.test.ts",
|
||||
|
|
@ -4373,7 +4356,6 @@ export const PR_PROTECTED_RUNTIME_TEST_FILES: readonly string[] = [
|
|||
"ui/src/e2e/sidebar-roster.e2e.test.ts",
|
||||
"ui/src/e2e/sidebar-session-stability.e2e.test.ts",
|
||||
"ui/src/e2e/sidebar-settings.e2e.test.ts",
|
||||
"ui/src/e2e/skill-workshop-collection.e2e.test.ts",
|
||||
"ui/src/e2e/skills-library.e2e.test.ts",
|
||||
"ui/src/e2e/subagent-task-notice.e2e.test.ts",
|
||||
"ui/src/e2e/systems-workspace.e2e.test.ts",
|
||||
|
|
@ -4869,11 +4851,6 @@ const PR_EXEMPT_RUNTIME_TEST_FILES = [
|
|||
"src/commands/doctor-config-health-freshness.test.ts",
|
||||
"src/commands/doctor-lint.native-capture.test.ts",
|
||||
"src/commands/doctor-session-transcript-labels.test.ts",
|
||||
"src/commands/doctor-skill-workshop-collection-backups.test.ts",
|
||||
"src/commands/doctor-skill-workshop-relocation.atomicity.test.ts",
|
||||
"src/commands/doctor-skill-workshop-relocation.reservations.test.ts",
|
||||
"src/commands/doctor-skill-workshop-sqlite.relocation-conflicts.test.ts",
|
||||
"src/commands/doctor-skill-workshop-sqlite.relocation.test.ts",
|
||||
"src/commands/doctor-state-integrity.transcripts.test.ts",
|
||||
"src/commands/onboard-agent.persistence.test.ts",
|
||||
"src/commands/onboard-config-provenance.integration.test.ts",
|
||||
|
|
@ -5154,7 +5131,6 @@ const PR_EXEMPT_RUNTIME_TEST_FILES = [
|
|||
"src/skills/library/resource-read.test.ts",
|
||||
"src/skills/lifecycle/upload-store-lifecycle.test.ts",
|
||||
"src/skills/runtime/refresh.symbolic-source.integration.test.ts",
|
||||
"src/skills/workshop/store.test.ts",
|
||||
"src/state/agent-database-admission.test.ts",
|
||||
"src/state/agent-deletion-journal-initialization.test.ts",
|
||||
"src/state/onboarding-recommendations.test.ts",
|
||||
|
|
@ -5429,7 +5405,6 @@ const PR_EXEMPT_RUNTIME_TEST_FILES = [
|
|||
"ui/src/pages/plugins/plugins-catalog.e2e.test.ts",
|
||||
"ui/src/pages/plugins/plugins-lifecycle.e2e.test.ts",
|
||||
"ui/src/pages/profile/model-accounts.test.ts",
|
||||
"ui/src/pages/skill-workshop/revision-admission.e2e.test.ts",
|
||||
"ui/src/pages/usage/usage-page-detail-identity.test.ts",
|
||||
"ui/src/pages/usage/usage-page-query-ownership.test.ts",
|
||||
"ui/src/pages/usage/usage-page.test.ts",
|
||||
|
|
|
|||
|
|
@ -321,7 +321,6 @@
|
|||
"src/skills/lifecycle/gh-config-discovery.test.ts": "889782c1738b129d442e2d49901a8cc9171840e392421b937ad70907e2e789d3",
|
||||
"src/skills/lifecycle/install-output.test.ts": "2a55cfedc31aa2470f3c7d2a743def176b37bc2f17ba705c35be740e35409775",
|
||||
"src/skills/workshop/config.test.ts": "7ff46a2786f1928f5c53c80cd9951377d0e7373d2f53d73e45d8c54c4f2ad254",
|
||||
"src/skills/workshop/learn-prompt.test.ts": "16f6bb26dba951b0d479c1499ef0a9a3cb60b3e6239a870a6523677966755e27",
|
||||
"src/utils/chunk-items.test.ts": "4b165806a3c07eb758b3b78da6ad7d77427db9781fb7d4c9d2cf725a192f9a88",
|
||||
"src/utils/normalize-secret-input.test.ts": "2b474185847ecfca939b16f82f380007fc276c640bc7b5d5f8d300e4c7e4499a",
|
||||
"src/utils/reaction-level.test.ts": "19e5367b157e9875742dc851f4c69adc73dec6fbcc38b920ffc711073d8f2236",
|
||||
|
|
|
|||
|
|
@ -2127,10 +2127,6 @@ export const UI_E2E_OWNER_WATCHES: readonly UiE2eOwnerWatch[] = [
|
|||
["chat", "config"],
|
||||
sessionMenuOwnerRoots,
|
||||
),
|
||||
pageWatch("ui/src/e2e/skill-workshop-revision-integrity.e2e.test.ts", [
|
||||
"skill-workshop",
|
||||
"skills",
|
||||
]),
|
||||
pageWatch(
|
||||
"ui/src/e2e/skills-any-bin-requirements.e2e.test.ts",
|
||||
["skills"],
|
||||
|
|
|
|||
|
|
@ -199,7 +199,8 @@ export function readPluginSdkSurfaceBudgets(env: NodeJS.ProcessEnv = process.env
|
|||
// +4: executor controller, binding, context, and resolver.
|
||||
// +1: required session cleanup failure preserves native ownership before host reset.
|
||||
// +2: approved async upstream-link writes with released sync compatibility.
|
||||
3649,
|
||||
// -8: retired Skill Workshop proposal hook types.
|
||||
3641,
|
||||
env,
|
||||
),
|
||||
publicFunctionExports: readPluginSdkSurfaceBudgetEnv(
|
||||
|
|
|
|||
|
|
@ -1245,6 +1245,7 @@ src/plugins/compat/registry-records.ts
|
|||
src/plugins/compat/registry.ts
|
||||
src/plugins/compat/reply-tool-authority-record.ts
|
||||
src/plugins/compat/session-persistence-records.ts
|
||||
src/plugins/compat/skill-proposal-hooks-record.ts
|
||||
src/plugins/compat/tts-preferences-record.ts
|
||||
src/plugins/compat/watched-sessions.ts
|
||||
src/plugins/compat/workspace-mutation-guard.ts
|
||||
|
|
|
|||
|
|
@ -87,7 +87,6 @@ const OPTIONAL_LIVE_SHARD_FILE_ENVS = new Map([
|
|||
],
|
||||
["extensions/openai/realtime-quicksilver.live.test.ts", ["OPENCLAW_LIVE_GPT_LIVE"]],
|
||||
["extensions/openai/realtime-talk-defaults.live.test.ts", ["OPENCLAW_LIVE_GPT_LIVE"]],
|
||||
["src/skills/workshop/experience-review.live.test.ts", ["OPENCLAW_LIVE_SKILL_EXPERIENCE_REVIEW"]],
|
||||
["src/system-agent/rescue-channel.live.test.ts", ["OPENCLAW_LIVE_SYSTEM_AGENT_RESCUE_CHANNEL"]],
|
||||
["src/gateway/android-node.capabilities.live.test.ts", ["OPENCLAW_LIVE_ANDROID_NODE"]],
|
||||
["src/gateway/gateway-acp-bind.live.test.ts", ["OPENCLAW_LIVE_ACP_BIND"]],
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
* timeout classification, and owner-provided approval outcomes.
|
||||
*/
|
||||
import { addTimerTimeoutGraceMs } from "@openclaw/normalization-core/number-coercion";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import { GatewayClientRequestError } from "../gateway/client.js";
|
||||
import { sanitizeApprovalScope } from "../infra/approval-scope.js";
|
||||
import { isEmbeddedMode } from "../infra/embedded-mode.js";
|
||||
|
|
@ -26,7 +25,6 @@ import {
|
|||
type PluginApprovalResolution,
|
||||
type PluginHookBeforeToolCallResult,
|
||||
} from "../plugins/types.js";
|
||||
import { resolveSkillWorkshopToolApproval } from "../skills/workshop/policy.js";
|
||||
import { isPlainObject } from "../utils.js";
|
||||
import { resolveToolErrorDiagnostic } from "./agent-tools.before-tool-call.diagnostics.js";
|
||||
import type {
|
||||
|
|
@ -506,32 +504,3 @@ export async function resolveBeforeToolCallApprovalOutcome(params: {
|
|||
overrideParams: overrideParamsSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveSkillWorkshopApprovalForFinalParams(params: {
|
||||
toolName: string;
|
||||
params: unknown;
|
||||
approvalMode?: "request" | "report" | "deny" | "defer";
|
||||
toolCallId?: string;
|
||||
ctx?: HookContext;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<HookOutcome | undefined> {
|
||||
if (params.toolName !== "skill_workshop") {
|
||||
return undefined;
|
||||
}
|
||||
const result = await resolveSkillWorkshopToolApproval({
|
||||
toolName: params.toolName,
|
||||
toolParams: isPlainObject(params.params) ? params.params : {},
|
||||
config: params.ctx?.config ?? getRuntimeConfig(),
|
||||
...(params.ctx?.agentId ? { agentId: params.ctx.agentId } : {}),
|
||||
...(params.ctx?.workspaceDir ? { workspaceDir: params.ctx.workspaceDir } : {}),
|
||||
});
|
||||
return await resolveBeforeToolCallApprovalOutcome({
|
||||
result,
|
||||
approvalMode: params.approvalMode,
|
||||
toolName: params.toolName,
|
||||
...(params.toolCallId ? { toolCallId: params.toolCallId } : {}),
|
||||
...(params.ctx ? { ctx: params.ctx } : {}),
|
||||
signal: params.signal,
|
||||
baseParams: params.params,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,10 +37,12 @@ import { redactToolDetail } from "../logging/redact.js";
|
|||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { getPluginToolMeta } from "../plugins/tool-metadata.js";
|
||||
import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js";
|
||||
import type { Skill } from "../skills/loading/skill-contract.js";
|
||||
import {
|
||||
resolveSkillTelemetrySource,
|
||||
resolveSkillTelemetrySourceValue,
|
||||
} from "../skills/loading/source.js";
|
||||
import { recordRunSkillUsage } from "../skills/runtime/run-usage.js";
|
||||
import { resolveSkillFileHost } from "../skills/skill-file-host.js";
|
||||
import type { SkillSnapshot, SkillTelemetrySource } from "../skills/types.js";
|
||||
import {
|
||||
|
|
@ -296,7 +298,7 @@ export function resolveToolDiagnosticIdentity(tool: AnyAgentTool): ToolDiagnosti
|
|||
return { toolSource: "core" };
|
||||
}
|
||||
|
||||
type SkillUsageMatch = {
|
||||
export type SkillUsageMatch = {
|
||||
skillFile?: string;
|
||||
skillName: string;
|
||||
skillSource: SkillTelemetrySource;
|
||||
|
|
@ -312,7 +314,7 @@ function canonicalSkillFile(value: string | undefined): string | undefined {
|
|||
|
||||
function resolvedSkillUsageMatch(params: {
|
||||
activation: SkillUsageMatch["activation"];
|
||||
skill: NonNullable<SkillSnapshot["resolvedSkills"]>[number];
|
||||
skill: Pick<Skill, "name" | "filePath"> & Partial<Pick<Skill, "source" | "sourceInfo">>;
|
||||
}): SkillUsageMatch {
|
||||
const skillFile = canonicalSkillFile(params.skill.filePath);
|
||||
return {
|
||||
|
|
@ -466,12 +468,23 @@ export function findSkillUsageMatch(params: {
|
|||
: undefined;
|
||||
}
|
||||
|
||||
export function emitSkillUsedDiagnostic(params: {
|
||||
ctx?: HookContext;
|
||||
/**
|
||||
* Records one demonstrated skill use: this run's usage receipt (review trigger) and the
|
||||
* trusted skill.used event (skill_usage rows, unused-skill archive clock).
|
||||
*/
|
||||
export function recordSkillUsed(params: {
|
||||
ctx?: Pick<HookContext, "runId" | "sessionKey" | "sessionId" | "agentId" | "trace">;
|
||||
match: SkillUsageMatch;
|
||||
toolName: string;
|
||||
toolCallId?: string;
|
||||
}): void {
|
||||
recordRunSkillUsage({
|
||||
runId: params.ctx?.runId,
|
||||
name: params.match.skillName,
|
||||
source: params.match.skillSource,
|
||||
activation: params.match.activation,
|
||||
...(params.match.skillFile ? { skillFile: params.match.skillFile } : {}),
|
||||
});
|
||||
const trace = params.ctx?.trace
|
||||
? freezeDiagnosticTraceContext(createChildDiagnosticTraceContext(params.ctx.trace))
|
||||
: undefined;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import fs from "node:fs/promises";
|
||||
import { expectDefined } from "@openclaw/normalization-core";
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } from "../config/config.js";
|
||||
import { setEmbeddedMode } from "../infra/embedded-mode.js";
|
||||
import {
|
||||
EmbeddedPluginApprovalBroker,
|
||||
|
|
@ -17,13 +15,7 @@ import {
|
|||
type PluginHookBeforeToolCallResult,
|
||||
} from "../plugins/types.js";
|
||||
import { createDeferredCore } from "../shared/deferred.js";
|
||||
import { proposeUpdateSkill } from "../skills/workshop/service.js";
|
||||
import { resolveWorkshopSkillsDir } from "../skills/workshop/skills-root.js";
|
||||
import { createOpenClawTestState } from "../test-utils/openclaw-test-state.js";
|
||||
import {
|
||||
resolveBeforeToolCallApprovalOutcome,
|
||||
resolveSkillWorkshopApprovalForFinalParams,
|
||||
} from "./agent-tools.before-tool-call.approval.js";
|
||||
import { resolveBeforeToolCallApprovalOutcome } from "./agent-tools.before-tool-call.approval.js";
|
||||
import { runBeforeToolCallHook } from "./agent-tools.before-tool-call.js";
|
||||
import { callGatewayTool } from "./tools/gateway.js";
|
||||
|
||||
|
|
@ -43,10 +35,6 @@ vi.mock("./tools/gateway.js", () => ({
|
|||
const mockGetGlobalHookRunner = vi.mocked(getGlobalHookRunner);
|
||||
const mockCallGatewayTool = vi.mocked(callGatewayTool);
|
||||
|
||||
function pendingWorkshopConfig() {
|
||||
return { skills: { workshop: { approvalPolicy: "pending" as const } } };
|
||||
}
|
||||
|
||||
function approvalResult(
|
||||
approval: Partial<NonNullable<PluginHookBeforeToolCallResult["requireApproval"]>> = {},
|
||||
params?: Record<string, unknown>,
|
||||
|
|
@ -112,7 +100,7 @@ function requireBeforeToolCall(
|
|||
}
|
||||
|
||||
describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
||||
let hookRunner: Pick<HookRunner, "hasHooks" | "runBeforeToolCall" | "runSkillProposalChanged">;
|
||||
let hookRunner: Pick<HookRunner, "hasHooks" | "runBeforeToolCall">;
|
||||
let runBeforeToolCallMock: ReturnType<typeof vi.fn<HookRunner["runBeforeToolCall"]>>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -121,7 +109,6 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
|||
hookRunner = {
|
||||
hasHooks: vi.fn<HookRunner["hasHooks"]>().mockReturnValue(true),
|
||||
runBeforeToolCall: runBeforeToolCallMock,
|
||||
runSkillProposalChanged: vi.fn<HookRunner["runSkillProposalChanged"]>(),
|
||||
};
|
||||
mockGetGlobalHookRunner.mockReturnValue(hookRunner as HookRunner);
|
||||
mockCallGatewayTool.mockReset();
|
||||
|
|
@ -129,7 +116,6 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
setEmbeddedPluginApprovalBroker(null);
|
||||
setEmbeddedMode(false);
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
|
|
@ -180,19 +166,13 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
|||
|
||||
it("resolves embedded approvals through the in-process TUI broker", async () => {
|
||||
const broker = embeddedBroker();
|
||||
runBeforeToolCallMock.mockResolvedValue({
|
||||
params: { action: "apply", proposal_id: "weather" },
|
||||
});
|
||||
runBeforeToolCallMock.mockResolvedValue(approvalResult({}, { path: "notes.md" }));
|
||||
|
||||
const resultPromise = runBeforeToolCallHook({
|
||||
toolName: "skill_workshop",
|
||||
params: { action: "apply", proposal_id: "weather" },
|
||||
toolCallId: "call-skill-local",
|
||||
ctx: {
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main:main",
|
||||
config: pendingWorkshopConfig(),
|
||||
},
|
||||
toolName: "demo_write",
|
||||
params: { path: "notes.md" },
|
||||
toolCallId: "call-demo-local",
|
||||
ctx: { agentId: "main", sessionKey: "agent:main:main" },
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
expect(broker.listPending()).toHaveLength(1);
|
||||
|
|
@ -201,12 +181,12 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
|||
broker.listPending()[0],
|
||||
"broker.listPending()[0] test invariant",
|
||||
);
|
||||
expect(approval?.request.toolName).toBe("skill_workshop");
|
||||
expect(approval?.request.toolName).toBe("demo_write");
|
||||
expect(broker.resolve(approval?.id, "allow-once")).toBe(true);
|
||||
|
||||
await expect(resultPromise).resolves.toEqual({
|
||||
blocked: false,
|
||||
params: { action: "apply", proposal_id: "weather" },
|
||||
params: { path: "notes.md" },
|
||||
approvalResolution: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
expect(mockCallGatewayTool).not.toHaveBeenCalled();
|
||||
|
|
@ -227,9 +207,9 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
|||
);
|
||||
|
||||
const resultPromise = runBeforeToolCallHook({
|
||||
toolName: "skill_workshop",
|
||||
params: { action: "apply", proposal_id: "weather" },
|
||||
toolCallId: "call-skill-stop",
|
||||
toolName: "demo_write",
|
||||
params: { path: "notes.md" },
|
||||
toolCallId: "call-demo-stop",
|
||||
ctx: { agentId: "main", sessionKey: "agent:main:main" },
|
||||
});
|
||||
await vi.waitFor(() => {
|
||||
|
|
@ -357,174 +337,6 @@ describe("runBeforeToolCallHook — embedded mode approvals", () => {
|
|||
expect(runBeforeToolCallMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("requires approval when a hook rewrites skill_workshop inspection into applying a proposal", async () => {
|
||||
const params = { action: "apply", proposal_id: "weather-20260530-a1b2c3d4e5" };
|
||||
runBeforeToolCallMock.mockResolvedValue({ params });
|
||||
mockCallGatewayTool.mockResolvedValueOnce({
|
||||
id: "skill-workshop-approval",
|
||||
decision: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
const result = await runBeforeToolCallHook({
|
||||
toolName: "skill_workshop",
|
||||
params: { ...params, action: "inspect" },
|
||||
toolCallId: "call-skill-hook-apply",
|
||||
ctx: { config: pendingWorkshopConfig() },
|
||||
});
|
||||
expect(result).toEqual({
|
||||
blocked: false,
|
||||
params,
|
||||
approvalResolution: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
const approvalCall = requireApprovalRequestCall("skill_workshop adjusted approval request");
|
||||
expect(approvalCall.request.pluginId).toBeUndefined();
|
||||
expect(approvalCall.request).toMatchObject({
|
||||
title: "Apply Skill Workshop proposal",
|
||||
description: "Apply a pending proposal inside your agent's Workshop directory.",
|
||||
severity: "warning",
|
||||
allowedDecisions: ["allow-once", "deny"],
|
||||
timeoutMs: 70_000,
|
||||
toolName: "skill_workshop",
|
||||
toolCallId: "call-skill-hook-apply",
|
||||
});
|
||||
expect(approvalCall.timeoutParams.timeoutMs).toBe(80_000);
|
||||
expect(runBeforeToolCallMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("does not expose another agent's proposal metadata in final approval", async () => {
|
||||
const testState = await createOpenClawTestState({
|
||||
layout: "state-only",
|
||||
prefix: "openclaw-agent-approval-scope-",
|
||||
});
|
||||
try {
|
||||
const config = {
|
||||
skills: { workshop: { approvalPolicy: "pending" as const } },
|
||||
};
|
||||
const skillsRoot = resolveWorkshopSkillsDir(config, "agent-a", testState.env);
|
||||
const skillDir = `${skillsRoot}/agent-a-private-procedure`;
|
||||
await fs.mkdir(skillDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
`${skillDir}/SKILL.md`,
|
||||
"---\nname: agent-a-private-procedure\ndescription: Agent A private description\n---\n\n# Agent A private body\n",
|
||||
"utf8",
|
||||
);
|
||||
const proposal = await proposeUpdateSkill({
|
||||
config,
|
||||
agentId: "agent-a",
|
||||
workspaceDir: testState.workspaceDir,
|
||||
env: testState.env,
|
||||
skillName: "agent-a-private-procedure",
|
||||
description: "Agent A private description",
|
||||
content: "# Agent A private body\n",
|
||||
});
|
||||
mockCallGatewayTool.mockResolvedValueOnce({
|
||||
id: "agent-b-approval",
|
||||
decision: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
|
||||
const result = await resolveSkillWorkshopApprovalForFinalParams({
|
||||
toolName: "skill_workshop",
|
||||
params: { action: "apply", proposal_id: proposal.record.id },
|
||||
toolCallId: "call-agent-b-apply",
|
||||
ctx: {
|
||||
agentId: "agent-b",
|
||||
workspaceDir: testState.workspaceDir,
|
||||
config,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
blocked: false,
|
||||
approvalResolution: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
const approvalCall = requireApprovalRequestCall(
|
||||
"agent-b final skill_workshop approval request",
|
||||
);
|
||||
expect(approvalCall.request.description).toBe(
|
||||
"Apply a pending proposal inside your agent's Workshop directory.",
|
||||
);
|
||||
expect(approvalCall.request.description).not.toContain("Agent A private");
|
||||
} finally {
|
||||
await testState.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("returns an actionable pending outcome when skill_workshop approval expires", async () => {
|
||||
mockCallGatewayTool.mockResolvedValueOnce({
|
||||
id: "skill-workshop-timeout",
|
||||
status: "accepted",
|
||||
});
|
||||
mockCallGatewayTool.mockResolvedValueOnce({
|
||||
id: "skill-workshop-timeout",
|
||||
decision: null,
|
||||
});
|
||||
|
||||
const result = await runBeforeToolCallHook({
|
||||
toolName: "skill_workshop",
|
||||
params: { action: "apply", proposal_id: "weather-20260530-a1b2c3d4e5" },
|
||||
toolCallId: "call-skill-timeout",
|
||||
ctx: {
|
||||
agentId: "main",
|
||||
sessionKey: "main",
|
||||
config: pendingWorkshopConfig(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
blocked: true,
|
||||
kind: "veto",
|
||||
deniedReason: "plugin-approval",
|
||||
reason:
|
||||
"The Skill Workshop approval request expired without a decision. This lifecycle call left the proposal unchanged and pending; check its current status in case another operator acted on it. Decide in the Skill Workshop UI or run `openclaw skills workshop apply|reject|quarantine <id>`. Do not retry this tool call in a loop.",
|
||||
});
|
||||
});
|
||||
|
||||
it("runs trusted policies before skill_workshop lifecycle approval", async () => {
|
||||
trustedPolicy({ block: true, blockReason: "trusted policy blocked skill workshop" });
|
||||
(hookRunner.hasHooks as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
||||
|
||||
const result = await runBeforeToolCallHook({
|
||||
toolName: "skill_workshop",
|
||||
params: { action: "apply", proposal_id: "weather-20260530-a1b2c3d4e5" },
|
||||
toolCallId: "call-skill-apply",
|
||||
ctx: {
|
||||
config: pendingWorkshopConfig(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
blocked: true,
|
||||
kind: "veto",
|
||||
deniedReason: "plugin-before-tool-call",
|
||||
reason: "trusted policy blocked skill workshop",
|
||||
params: { action: "apply", proposal_id: "weather-20260530-a1b2c3d4e5" },
|
||||
});
|
||||
expect(mockCallGatewayTool).not.toHaveBeenCalled();
|
||||
expect(runBeforeToolCallMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses runtime config for skill_workshop pending mode when hook context config is absent", async () => {
|
||||
(hookRunner.hasHooks as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
||||
setRuntimeConfigSnapshot(pendingWorkshopConfig());
|
||||
mockCallGatewayTool.mockResolvedValueOnce({
|
||||
id: "skill-workshop-runtime-approval",
|
||||
decision: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
|
||||
const result = await runBeforeToolCallHook({
|
||||
toolName: "skill_workshop",
|
||||
params: { action: "apply", proposal_id: "weather-20260530-a1b2c3d4e5" },
|
||||
ctx: { agentId: "main", sessionKey: "main" },
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
blocked: false,
|
||||
params: { action: "apply", proposal_id: "weather-20260530-a1b2c3d4e5" },
|
||||
approvalResolution: PluginApprovalResolutions.ALLOW_ONCE,
|
||||
});
|
||||
expect(mockCallGatewayTool).toHaveBeenCalledTimes(1);
|
||||
expect(runBeforeToolCallMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves trusted policy params when before_tool_call hooks leave params unchanged", async () => {
|
||||
trustedPolicy({ params: { command: "patched" } });
|
||||
runBeforeToolCallMock.mockResolvedValue(undefined);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
* remain in this sequence.
|
||||
*/
|
||||
import type { ToolLoopWarning } from "@openclaw/agent-core";
|
||||
import { getRuntimeConfig } from "../config/config.js";
|
||||
import { freezeDiagnosticTraceContext } from "../infra/diagnostic-trace-context.js";
|
||||
import { getGlobalHookRunnerRegistry } from "../plugins/hook-runner-global-state.js";
|
||||
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
|
|
@ -21,7 +20,6 @@ import type {
|
|||
PluginHookToolInputKind,
|
||||
PluginHookToolKind,
|
||||
} from "../plugins/types.js";
|
||||
import { resolveSkillWorkshopToolApproval } from "../skills/workshop/policy.js";
|
||||
import {
|
||||
checkClientVoiceToolConfirmationPolicy,
|
||||
consumeClientVoiceToolConfirmationPolicy,
|
||||
|
|
@ -34,7 +32,6 @@ import { isPlainObject } from "../utils.js";
|
|||
import {
|
||||
mergeParamsWithApprovalOverrides,
|
||||
resolveBeforeToolCallApprovalOutcome,
|
||||
resolveSkillWorkshopApprovalForFinalParams,
|
||||
} from "./agent-tools.before-tool-call.approval.js";
|
||||
import {
|
||||
beforeToolCallLog as log,
|
||||
|
|
@ -177,16 +174,6 @@ export async function runBeforeToolCallHook(args: {
|
|||
const policyRegistry = getGlobalHookRunnerRegistry() ?? undefined;
|
||||
const shouldRunTrustedPolicies = hasTrustedToolPolicies(policyRegistry);
|
||||
const normalizedParams = isPlainObject(params) ? params : {};
|
||||
const initialCorePolicyResult =
|
||||
toolName === "skill_workshop"
|
||||
? await resolveSkillWorkshopToolApproval({
|
||||
toolName,
|
||||
toolParams: normalizedParams,
|
||||
config: args.ctx?.config ?? getRuntimeConfig(),
|
||||
...(args.ctx?.workspaceDir ? { workspaceDir: args.ctx.workspaceDir } : {}),
|
||||
...(args.ctx?.agentId ? { agentId: args.ctx.agentId } : {}),
|
||||
})
|
||||
: undefined;
|
||||
const voiceRun = resolveClientVoiceRunBinding(args.ctx?.runId);
|
||||
// Nested catalog calls are gated individually; the script wrapper is not itself an action.
|
||||
const voiceConfirmation = isCodeModeExecToolKind(args.toolKind)
|
||||
|
|
@ -209,7 +196,7 @@ export async function runBeforeToolCallHook(args: {
|
|||
params,
|
||||
};
|
||||
}
|
||||
if (!initialCorePolicyResult && !shouldRunTrustedPolicies && !hasBeforeToolCallHooks) {
|
||||
if (!shouldRunTrustedPolicies && !hasBeforeToolCallHooks) {
|
||||
return withLoopWarning({ blocked: false, params });
|
||||
}
|
||||
const deriveOptions =
|
||||
|
|
@ -405,17 +392,6 @@ export async function runBeforeToolCallHook(args: {
|
|||
});
|
||||
}
|
||||
}
|
||||
const finalApprovalOutcome = await resolveSkillWorkshopApprovalForFinalParams({
|
||||
toolName,
|
||||
params: finalParams,
|
||||
approvalMode: args.approvalMode,
|
||||
...(args.toolCallId ? { toolCallId: args.toolCallId } : {}),
|
||||
...(args.ctx ? { ctx: args.ctx } : {}),
|
||||
signal: args.signal,
|
||||
});
|
||||
if (finalApprovalOutcome) {
|
||||
return withLoopWarning(finalApprovalOutcome);
|
||||
}
|
||||
const allowed: HookOutcome = {
|
||||
blocked: false as const,
|
||||
params: finalParams,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
} from "../infra/diagnostic-trace-context.js";
|
||||
import { pruneMapToMaxSize } from "../infra/map-size.js";
|
||||
import { getPluginToolMeta } from "../plugins/tool-metadata.js";
|
||||
import { recordRunSkillUsage } from "../skills/runtime/run-usage.js";
|
||||
import { copyBeforeToolCallWrapperMetadata } from "./agent-tool-metadata.js";
|
||||
import {
|
||||
captureAgentToolExecutionBudget,
|
||||
|
|
@ -27,12 +26,12 @@ import {
|
|||
} from "./agent-tools.before-tool-call.decision.js";
|
||||
import {
|
||||
buildToolContentPrivateData,
|
||||
emitSkillUsedDiagnostic,
|
||||
emitToolBlockedSecurityEvent,
|
||||
findSkillUsageMatch,
|
||||
prepareToolTerminalPresentation,
|
||||
reconcileLoopCallExecutionParams,
|
||||
recordLoopOutcome,
|
||||
recordSkillUsed,
|
||||
rememberPendingTerminalPresentation,
|
||||
resolveToolDiagnosticIdentity,
|
||||
resolveToolErrorDiagnostic,
|
||||
|
|
@ -549,19 +548,7 @@ export function wrapToolWithBeforeToolCallHook(
|
|||
ctx,
|
||||
});
|
||||
if (skillMatch && terminalDiagnostic.type === "tool.execution.completed") {
|
||||
recordRunSkillUsage({
|
||||
runId: ctx?.runId,
|
||||
name: skillMatch.skillName,
|
||||
source: skillMatch.skillSource,
|
||||
activation: skillMatch.activation,
|
||||
...(skillMatch.skillFile ? { skillFile: skillMatch.skillFile } : {}),
|
||||
});
|
||||
emitSkillUsedDiagnostic({
|
||||
ctx,
|
||||
match: skillMatch,
|
||||
toolName: normalizedToolName,
|
||||
toolCallId,
|
||||
});
|
||||
recordSkillUsed({ ctx, match: skillMatch, toolName: normalizedToolName, toolCallId });
|
||||
}
|
||||
if (hookOptions.emitDiagnostics) {
|
||||
emitTrustedDiagnosticEventWithPrivateData(
|
||||
|
|
|
|||
|
|
@ -202,7 +202,6 @@ describe("resolveCliBackendConfig", () => {
|
|||
config: { command: "setup-acme", args: ["run"] },
|
||||
parseJsonlEvent,
|
||||
resolveModelId,
|
||||
isolatesInstructionsWithExactTools: true,
|
||||
});
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
resolveRuntimeCliBackends: () => [],
|
||||
|
|
@ -218,7 +217,6 @@ describe("resolveCliBackendConfig", () => {
|
|||
expect(resolved.resolveModelId?.({ modelId: "acme-large", contextWindow: "1m" })).toBe(
|
||||
"acme-large[1m]",
|
||||
);
|
||||
expect(resolved.isolatesInstructionsWithExactTools).toBe(true);
|
||||
});
|
||||
|
||||
it("returns null when no plugin owns the backend", () => {
|
||||
|
|
@ -251,7 +249,6 @@ describe("resolveCliBackendConfig", () => {
|
|||
manualCompaction,
|
||||
nativeToolMode: "selectable",
|
||||
toolAvailabilityEnforcement: "execution-args",
|
||||
isolatesInstructionsWithExactTools: true,
|
||||
sideQuestionToolMode: "disabled",
|
||||
}),
|
||||
],
|
||||
|
|
@ -266,7 +263,6 @@ describe("resolveCliBackendConfig", () => {
|
|||
expect(resolved.manualCompaction).toBe(manualCompaction);
|
||||
expect(resolved.nativeToolMode).toBe("selectable");
|
||||
expect(resolved.toolAvailabilityEnforcement).toBe("execution-args");
|
||||
expect(resolved.isolatesInstructionsWithExactTools).toBe(true);
|
||||
expect(resolved.sideQuestionToolMode).toBe("disabled");
|
||||
});
|
||||
|
||||
|
|
@ -283,7 +279,6 @@ describe("resolveCliBackendConfig", () => {
|
|||
});
|
||||
|
||||
expect(requireBackend().toolAvailabilityEnforcement).toBeUndefined();
|
||||
expect(requireBackend().isolatesInstructionsWithExactTools).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export type ResolvedCliBackend = Pick<
|
|||
| "parseJsonlEvent"
|
||||
| "parseJsonlLifecycleEvent"
|
||||
| "toolAvailabilityEnforcement"
|
||||
| "isolatesInstructionsWithExactTools"
|
||||
| "projectNativeToolAuthority"
|
||||
| "nativeToolMode"
|
||||
| "hostOwnedTools"
|
||||
|
|
@ -264,7 +263,6 @@ export function resolveCliBackendConfig(
|
|||
parseJsonlEvent: backend.parseJsonlEvent,
|
||||
parseJsonlLifecycleEvent: backend.parseJsonlLifecycleEvent,
|
||||
toolAvailabilityEnforcement: backend.toolAvailabilityEnforcement,
|
||||
isolatesInstructionsWithExactTools: backend.isolatesInstructionsWithExactTools,
|
||||
projectNativeToolAuthority: backend.projectNativeToolAuthority,
|
||||
nativeToolMode: backend.nativeToolMode,
|
||||
hostOwnedTools: backend.hostOwnedTools,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue