@cohortapp/agent-sdk 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- package/workflows/weekly/strategic-memo.yaml +79 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/learning/skill-writer.mjs — the autonomous skill write-path (WS3).
|
|
3
|
+
*
|
|
4
|
+
* When the reflect pass (lib/learning/reflect.mjs) decides the agent should
|
|
5
|
+
* capture a repeatable method, it calls writeSkill() here. Maestro skills are
|
|
6
|
+
* FLAT `.md` files with `name`/`description` frontmatter (NOT `<name>/SKILL.md`)
|
|
7
|
+
* — Claude Code loads them straight off disk via the plugin.json manifest.
|
|
8
|
+
*
|
|
9
|
+
* QUARANTINE (W1/W3): a newly authored skill is attacker-influencable (its body
|
|
10
|
+
* is distilled from transcript content that can contain inbound, attacker-
|
|
11
|
+
* controlled text). To stop self-poisoning, a CREATE does NOT land live. It is
|
|
12
|
+
* written PENDING and is NOT registered in plugin.json, so Claude Code never
|
|
13
|
+
* loads it until an operator runs approveSkill():
|
|
14
|
+
*
|
|
15
|
+
* plugins/agent-skills/.pending/<id>.md (status: pending — NOT loaded)
|
|
16
|
+
* ── approveSkill() ──▶
|
|
17
|
+
* plugins/agent-skills/skills/<id>.md (live)
|
|
18
|
+
* plugins/agent-skills/plugin.json (skills[] manifest, create-or-replace)
|
|
19
|
+
*
|
|
20
|
+
* A PATCH of an ALREADY-LIVE (operator-approved) skill edits it in place; a
|
|
21
|
+
* "patch" of a skill that is not live is treated as a CREATE → pending.
|
|
22
|
+
*
|
|
23
|
+
* Governance is fully autonomous but AUDITED: every create / patch / approve /
|
|
24
|
+
* abort is appended to logs/audit/skills.jsonl, and a pending create also emits
|
|
25
|
+
* an operator notification line on stderr naming the skill + how to approve it.
|
|
26
|
+
* Before any byte is written, the rendered skill body is SECRET-SCANNED
|
|
27
|
+
* (lib/security/redact.mjs primitives + explicit token patterns: sk-ant / ghp_ /
|
|
28
|
+
* AKIA / bearer / password=); a hit ABORTS the write (no skill on disk) and logs
|
|
29
|
+
* the abort. We never git-commit — Claude Code reads the dir, not git.
|
|
30
|
+
*
|
|
31
|
+
* A usage sidecar (state/learning/skills.json) tracks lifecycle for the curator:
|
|
32
|
+
* { id, created_by, created_at, last_activity, state, pinned }
|
|
33
|
+
* where state is "pending" until approved, then "active".
|
|
34
|
+
*
|
|
35
|
+
* Everything is fail-open and atomic (tmp + rename in the same dir).
|
|
36
|
+
*
|
|
37
|
+
* @module lib/learning/skill-writer
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
"use strict";
|
|
41
|
+
|
|
42
|
+
import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, appendFileSync } from "node:fs";
|
|
43
|
+
import { join, dirname } from "node:path";
|
|
44
|
+
import { isSensitiveParamName } from "../security/redact.mjs";
|
|
45
|
+
|
|
46
|
+
const SKILLS_DIR_REL = join("plugins", "agent-skills", "skills");
|
|
47
|
+
const PENDING_DIR_REL = join("plugins", "agent-skills", ".pending");
|
|
48
|
+
const PLUGIN_JSON_REL = join("plugins", "agent-skills", "plugin.json");
|
|
49
|
+
const AUDIT_REL = join("logs", "audit", "skills.jsonl");
|
|
50
|
+
const SIDECAR_REL = join("state", "learning", "skills.json");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Explicit secret token patterns. The redact module's URL/param helpers catch
|
|
54
|
+
* `password=`, `apikey=`, `token=` style assignments; these catch bare credential
|
|
55
|
+
* literals an LLM might paste into a body verbatim.
|
|
56
|
+
* @type {Array<{label:string, re:RegExp}>}
|
|
57
|
+
*/
|
|
58
|
+
export const SECRET_PATTERNS = Object.freeze([
|
|
59
|
+
{ label: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{8,}/ },
|
|
60
|
+
{ label: "openai-key", re: /\bsk-[A-Za-z0-9]{20,}/ },
|
|
61
|
+
{ label: "github-pat", re: /\bgh[pousr]_[A-Za-z0-9]{20,}/ },
|
|
62
|
+
{ label: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{12,}/ },
|
|
63
|
+
{ label: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{30,}/ },
|
|
64
|
+
{ label: "slack-token", re: /\bxox[baprs]-[0-9A-Za-z-]{10,}/ },
|
|
65
|
+
{ label: "bearer-token", re: /\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/i },
|
|
66
|
+
{ label: "private-key-block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/** A `key=value` / `key: value` assignment whose key looks sensitive (password=, secret: …). */
|
|
70
|
+
const ASSIGNMENT_RE = /([A-Za-z0-9_.-]{2,40})\s*[:=]\s*("?[^\s"'`]{6,}"?)/g;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Scan a rendered skill body for embedded secrets. Returns the list of hits
|
|
74
|
+
* (label + the matched lead-in, never the full secret). Empty array = clean.
|
|
75
|
+
* Pure; never throws.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} body
|
|
78
|
+
* @returns {Array<{label:string, sample:string}>}
|
|
79
|
+
*/
|
|
80
|
+
export function scanForSecrets(body) {
|
|
81
|
+
const text = typeof body === "string" ? body : "";
|
|
82
|
+
if (!text) return [];
|
|
83
|
+
const hits = [];
|
|
84
|
+
for (const { label, re } of SECRET_PATTERNS) {
|
|
85
|
+
const m = text.match(re);
|
|
86
|
+
if (m) hits.push({ label, sample: redactSample(m[0]) });
|
|
87
|
+
}
|
|
88
|
+
// key=value / key: value where the key name reads as a secret (password, token…).
|
|
89
|
+
let a;
|
|
90
|
+
ASSIGNMENT_RE.lastIndex = 0;
|
|
91
|
+
while ((a = ASSIGNMENT_RE.exec(text)) !== null) {
|
|
92
|
+
const name = a[1];
|
|
93
|
+
if (isSensitiveParamName(name)) {
|
|
94
|
+
hits.push({ label: `assignment:${name.toLowerCase()}`, sample: `${name}=***` });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return hits;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Show only a non-reversible lead-in of a matched secret for the audit row. */
|
|
101
|
+
function redactSample(s) {
|
|
102
|
+
const str = String(s);
|
|
103
|
+
return str.length <= 10 ? "***" : str.slice(0, 6) + "…***";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Slugify a free-text name into a filesystem-safe skill id. Lowercase, hyphen-
|
|
108
|
+
* separated, alnum only, bounded length.
|
|
109
|
+
* @param {string} name
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
export function slugify(name) {
|
|
113
|
+
const base = String(name || "")
|
|
114
|
+
.toLowerCase()
|
|
115
|
+
.normalize("NFKD")
|
|
116
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
117
|
+
.replace(/^-+|-+$/g, "")
|
|
118
|
+
.slice(0, 60);
|
|
119
|
+
return base || "skill";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Directory holding LIVE (operator-approved) agent-authored skill .md files. */
|
|
123
|
+
export function skillsDir(agentRoot) {
|
|
124
|
+
return join(agentRoot, SKILLS_DIR_REL);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Directory holding PENDING (quarantined, not-yet-approved) skill .md files. */
|
|
128
|
+
export function pendingDir(agentRoot) {
|
|
129
|
+
return join(agentRoot, PENDING_DIR_REL);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** True if a skill id is already live (loaded by Claude Code). */
|
|
133
|
+
function isLiveSkill(agentRoot, id) {
|
|
134
|
+
return existsSync(join(skillsDir(agentRoot), `${id}.md`));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a unique skill id for a CREATE: slug, then -2, -3… if the id is
|
|
139
|
+
* already taken in EITHER the live or the pending dir (so a quarantined skill
|
|
140
|
+
* can't be silently clobbered by the next create). For a PATCH the caller passes
|
|
141
|
+
* the existing id and we keep it.
|
|
142
|
+
* @param {string} agentRoot
|
|
143
|
+
* @param {string} slug
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
function dedupeId(agentRoot, slug) {
|
|
147
|
+
const taken = (cand) =>
|
|
148
|
+
existsSync(join(skillsDir(agentRoot), `${cand}.md`)) ||
|
|
149
|
+
existsSync(join(pendingDir(agentRoot), `${cand}.md`));
|
|
150
|
+
if (!taken(slug)) return slug;
|
|
151
|
+
for (let n = 2; n < 1000; n++) {
|
|
152
|
+
const cand = `${slug}-${n}`;
|
|
153
|
+
if (!taken(cand)) return cand;
|
|
154
|
+
}
|
|
155
|
+
return `${slug}-${Date.now()}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** One-line frontmatter-safe value (no newlines, no leading list/colon traps). */
|
|
159
|
+
function fmValue(s) {
|
|
160
|
+
return String(s == null ? "" : s).replace(/\s+/g, " ").replace(/"/g, "'").trim();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Render the full flat-skill `.md` (frontmatter + body). Matches the maestro
|
|
165
|
+
* skill format consumed by Claude Code + generate-capability.mjs.
|
|
166
|
+
* @param {object} o - { id, name, description, body, sourceSession, createdAt, status }
|
|
167
|
+
* @returns {string}
|
|
168
|
+
*/
|
|
169
|
+
export function renderSkillMd(o) {
|
|
170
|
+
const fm = [
|
|
171
|
+
"---",
|
|
172
|
+
`name: ${fmValue(o.name || o.id)}`,
|
|
173
|
+
`description: ${fmValue(o.description)}`,
|
|
174
|
+
"created_by: agent",
|
|
175
|
+
`status: ${fmValue(o.status || "approved")}`,
|
|
176
|
+
`created_at: ${fmValue(o.createdAt)}`,
|
|
177
|
+
`source_session: ${fmValue(o.sourceSession || "")}`,
|
|
178
|
+
"---",
|
|
179
|
+
"",
|
|
180
|
+
].join("\n");
|
|
181
|
+
const body = String(o.body || "").trim();
|
|
182
|
+
return fm + body + "\n";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Rewrite the `status:` frontmatter line of a rendered skill .md (best-effort). */
|
|
186
|
+
function withStatus(md, status) {
|
|
187
|
+
const text = String(md);
|
|
188
|
+
if (/^status:.*$/m.test(text)) return text.replace(/^status:.*$/m, `status: ${status}`);
|
|
189
|
+
// Older skills may predate the status line — insert it after created_by.
|
|
190
|
+
if (/^created_by:.*$/m.test(text)) return text.replace(/^(created_by:.*)$/m, `$1\nstatus: ${status}`);
|
|
191
|
+
return text;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Atomic file write (tmp + rename in the same dir). Throws on hard FS failure. */
|
|
195
|
+
function writeAtomicFile(absPath, contents) {
|
|
196
|
+
mkdirSync(dirname(absPath), { recursive: true });
|
|
197
|
+
const tmp = `${absPath}.${process.pid}.${Date.now()}.tmp`;
|
|
198
|
+
writeFileSync(tmp, contents);
|
|
199
|
+
renameSync(tmp, absPath);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Append a row to logs/audit/skills.jsonl. Fail-open (never throws). */
|
|
203
|
+
export function appendSkillAudit(agentRoot, row) {
|
|
204
|
+
try {
|
|
205
|
+
const p = join(agentRoot, AUDIT_REL);
|
|
206
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
207
|
+
appendFileSync(p, JSON.stringify({ ts: new Date().toISOString(), ...row }) + "\n");
|
|
208
|
+
} catch { /* fail-open */ }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// plugin.json manifest (create-or-replace by name)
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
function readPluginJson(agentRoot) {
|
|
216
|
+
const p = join(agentRoot, PLUGIN_JSON_REL);
|
|
217
|
+
if (!existsSync(p)) {
|
|
218
|
+
return { name: "agent-skills", description: "Agent-authored skills", version: "0.1.0", skills: [] };
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
const obj = JSON.parse(readFileSync(p, "utf8"));
|
|
222
|
+
if (!obj || typeof obj !== "object") throw new Error("bad");
|
|
223
|
+
if (!Array.isArray(obj.skills)) obj.skills = [];
|
|
224
|
+
return obj;
|
|
225
|
+
} catch {
|
|
226
|
+
return { name: "agent-skills", description: "Agent-authored skills", version: "0.1.0", skills: [] };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Upsert a skill entry (matched by name) into plugin.json, atomically. */
|
|
231
|
+
function upsertPluginEntry(agentRoot, entry) {
|
|
232
|
+
const manifest = readPluginJson(agentRoot);
|
|
233
|
+
const idx = manifest.skills.findIndex((s) => s && s.name === entry.name);
|
|
234
|
+
if (idx >= 0) manifest.skills[idx] = { ...manifest.skills[idx], ...entry };
|
|
235
|
+
else manifest.skills.push(entry);
|
|
236
|
+
writeAtomicFile(join(agentRoot, PLUGIN_JSON_REL), JSON.stringify(manifest, null, 2) + "\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Usage sidecar (state/learning/skills.json) — the curator's source of truth
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
/** Read the usage sidecar (map id → record). Fail-open → {}. */
|
|
244
|
+
export function readSidecar(agentRoot) {
|
|
245
|
+
const p = join(agentRoot, SIDECAR_REL);
|
|
246
|
+
if (!existsSync(p)) return {};
|
|
247
|
+
try {
|
|
248
|
+
const obj = JSON.parse(readFileSync(p, "utf8"));
|
|
249
|
+
return obj && typeof obj === "object" && !Array.isArray(obj) ? obj : {};
|
|
250
|
+
} catch {
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Write the usage sidecar atomically. Fail-open → false. */
|
|
256
|
+
export function writeSidecar(agentRoot, sidecar) {
|
|
257
|
+
try {
|
|
258
|
+
writeAtomicFile(join(agentRoot, SIDECAR_REL), JSON.stringify(sidecar, null, 2) + "\n");
|
|
259
|
+
return true;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Bump last_activity for a skill id (called when the id appears in a transcript).
|
|
267
|
+
* Creates no new record if the id is unknown. Fail-open.
|
|
268
|
+
* @param {string} agentRoot
|
|
269
|
+
* @param {string} id
|
|
270
|
+
* @param {object} [opts] - { nowIso }
|
|
271
|
+
*/
|
|
272
|
+
export function touchSkill(agentRoot, id, opts = {}) {
|
|
273
|
+
if (!agentRoot || !id) return;
|
|
274
|
+
const sidecar = readSidecar(agentRoot);
|
|
275
|
+
if (!sidecar[id]) return;
|
|
276
|
+
sidecar[id].last_activity = opts.nowIso || new Date().toISOString();
|
|
277
|
+
writeSidecar(agentRoot, sidecar);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function upsertSidecar(agentRoot, id, patch) {
|
|
281
|
+
const sidecar = readSidecar(agentRoot);
|
|
282
|
+
const now = patch.now || new Date().toISOString();
|
|
283
|
+
const prev = sidecar[id] || {};
|
|
284
|
+
sidecar[id] = {
|
|
285
|
+
id,
|
|
286
|
+
created_by: prev.created_by || "agent",
|
|
287
|
+
created_at: prev.created_at || now,
|
|
288
|
+
last_activity: now,
|
|
289
|
+
state: patch.state || prev.state || "active",
|
|
290
|
+
pinned: prev.pinned === true,
|
|
291
|
+
};
|
|
292
|
+
writeSidecar(agentRoot, sidecar);
|
|
293
|
+
return sidecar[id];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// writeSkill — the public entry point
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create or patch an agent-authored skill on disk, with a pre-write secret scan,
|
|
302
|
+
* sidecar update, and audit logging.
|
|
303
|
+
*
|
|
304
|
+
* A CREATE is QUARANTINED: it lands in the `.pending/` dir (NOT registered in
|
|
305
|
+
* plugin.json, so Claude Code never loads it) with `state:"pending"`, and emits
|
|
306
|
+
* an operator notification (stderr + audit row) naming the skill and how to
|
|
307
|
+
* approve it. A PATCH of an already-live skill edits it in place and updates the
|
|
308
|
+
* manifest; a "patch" of a skill that is not live is downgraded to a pending
|
|
309
|
+
* create, since auto-loading unreviewed instructions is the exact vector we are
|
|
310
|
+
* closing (W1/W3).
|
|
311
|
+
*
|
|
312
|
+
* @param {object} o
|
|
313
|
+
* @param {string} o.agentRoot
|
|
314
|
+
* @param {"create"|"patch"} [o.action="create"]
|
|
315
|
+
* @param {string} o.name human skill name (also the manifest match key)
|
|
316
|
+
* @param {string} o.description one-line description (frontmatter + manifest)
|
|
317
|
+
* @param {string} o.body the skill body markdown (scanned before write)
|
|
318
|
+
* @param {string} [o.sourceSession] originating session id (provenance)
|
|
319
|
+
* @param {string} [o.id] explicit id (required-ish for patch; else slug)
|
|
320
|
+
* @param {boolean} [o.trusted=false] system-internal author: a create lands LIVE
|
|
321
|
+
* (skips quarantine). Set ONLY by trusted internal callers (curator umbrella
|
|
322
|
+
* consolidation), NEVER by the agent-authored/reflect path (the W1 vector).
|
|
323
|
+
* @param {string} [o.nowIso] injectable timestamp (tests)
|
|
324
|
+
* @returns {{ok:boolean, action:string, status:string, id:?string, path:?string, reason:string, secrets?:Array}}
|
|
325
|
+
*/
|
|
326
|
+
export function writeSkill(o = {}) {
|
|
327
|
+
const agentRoot = o.agentRoot;
|
|
328
|
+
const requested = o.action === "patch" ? "patch" : "create";
|
|
329
|
+
const name = String(o.name || "").trim();
|
|
330
|
+
const description = String(o.description || "").trim();
|
|
331
|
+
const body = String(o.body || "");
|
|
332
|
+
const nowIso = o.nowIso || new Date().toISOString();
|
|
333
|
+
|
|
334
|
+
if (!agentRoot) return { ok: false, action: requested, status: "rejected", id: null, path: null, reason: "no-agent-root" };
|
|
335
|
+
if (!name) return { ok: false, action: requested, status: "rejected", id: null, path: null, reason: "no-name" };
|
|
336
|
+
|
|
337
|
+
// 1. SECRET SCAN — before any byte is written. A hit aborts the skill.
|
|
338
|
+
const secrets = scanForSecrets(body);
|
|
339
|
+
if (secrets.length > 0) {
|
|
340
|
+
appendSkillAudit(agentRoot, {
|
|
341
|
+
action: "abort",
|
|
342
|
+
requested,
|
|
343
|
+
name,
|
|
344
|
+
reason: "secret-scan",
|
|
345
|
+
secrets: secrets.map((s) => s.label),
|
|
346
|
+
source_session: o.sourceSession || null,
|
|
347
|
+
});
|
|
348
|
+
return { ok: false, action: requested, status: "rejected", id: null, path: null, reason: "secret-scan", secrets };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 2. Resolve the id and decide live-patch vs. quarantined-create. A patch only
|
|
352
|
+
// stays live if it targets a skill an operator already approved; otherwise
|
|
353
|
+
// it is a create and must be quarantined.
|
|
354
|
+
let id;
|
|
355
|
+
let action = requested;
|
|
356
|
+
if (requested === "patch") {
|
|
357
|
+
id = String(o.id || slugify(name)).trim() || slugify(name);
|
|
358
|
+
if (!isLiveSkill(agentRoot, id)) action = "create";
|
|
359
|
+
}
|
|
360
|
+
if (action === "create") {
|
|
361
|
+
id = dedupeId(agentRoot, slugify(o.id || name));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// create → pending quarantine; patch → live edit. A TRUSTED create (o.trusted
|
|
365
|
+
// === true) is a system-internal author (e.g. curator umbrella consolidation),
|
|
366
|
+
// NOT untrusted agent-authored output, so it lands live. `trusted` must never
|
|
367
|
+
// be wired to the reflect/agent-authored path — that is the W1 vector.
|
|
368
|
+
const live = action === "patch" || (action === "create" && o.trusted === true);
|
|
369
|
+
const status = live ? "approved" : "pending";
|
|
370
|
+
|
|
371
|
+
// 3. Render + write the .md atomically into the live OR pending dir.
|
|
372
|
+
const md = renderSkillMd({ id, name, description, body, sourceSession: o.sourceSession, createdAt: nowIso, status });
|
|
373
|
+
const absPath = join(live ? skillsDir(agentRoot) : pendingDir(agentRoot), `${id}.md`);
|
|
374
|
+
try {
|
|
375
|
+
writeAtomicFile(absPath, md);
|
|
376
|
+
} catch (e) {
|
|
377
|
+
appendSkillAudit(agentRoot, { action: "abort", requested, name, id, reason: "write-failed", error: e && e.message ? e.message : String(e) });
|
|
378
|
+
return { ok: false, action, status: "rejected", id, path: null, reason: "write-failed" };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 4. Manifest: ONLY live (approved) skills are registered so Claude Code loads
|
|
382
|
+
// them. A pending skill is deliberately absent from plugin.json.
|
|
383
|
+
if (live) {
|
|
384
|
+
try { upsertPluginEntry(agentRoot, { name: id, description, path: `skills/${id}.md` }); } catch { /* fail-open */ }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 5. Update the usage sidecar with the lifecycle state.
|
|
388
|
+
let rec = null;
|
|
389
|
+
try { rec = upsertSidecar(agentRoot, id, { state: live ? "active" : "pending", now: nowIso }); } catch { /* fail-open */ }
|
|
390
|
+
|
|
391
|
+
// 6. Audit + (for a quarantined create) notify the operator.
|
|
392
|
+
appendSkillAudit(agentRoot, { action, status, name, id, description, source_session: o.sourceSession || null });
|
|
393
|
+
if (!live) notifyPendingSkill(agentRoot, { id, name, path: absPath });
|
|
394
|
+
|
|
395
|
+
return { ok: true, action, status, id, path: absPath, reason: "ok", record: rec };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Approve a quarantined (pending) skill: move it from `.pending/` into the live
|
|
400
|
+
* `skills/` dir, flip its frontmatter to `status: approved`, register it in
|
|
401
|
+
* plugin.json so Claude Code loads it, and mark the sidecar `active`. Audited.
|
|
402
|
+
* Fail-open on FS errors. This is the operator gate the autonomous loop cannot
|
|
403
|
+
* cross on its own.
|
|
404
|
+
*
|
|
405
|
+
* @param {object} o
|
|
406
|
+
* @param {string} o.agentRoot
|
|
407
|
+
* @param {string} o.id the pending skill id
|
|
408
|
+
* @param {string} [o.approvedBy] who approved (recorded in the audit row)
|
|
409
|
+
* @param {string} [o.nowIso] injectable timestamp (tests)
|
|
410
|
+
* @returns {{ok:boolean, id:string, path:?string, reason:string}}
|
|
411
|
+
*/
|
|
412
|
+
export function approveSkill(o = {}) {
|
|
413
|
+
const agentRoot = o.agentRoot;
|
|
414
|
+
const id = String(o.id || "").trim();
|
|
415
|
+
const nowIso = o.nowIso || new Date().toISOString();
|
|
416
|
+
if (!agentRoot) return { ok: false, id, path: null, reason: "no-agent-root" };
|
|
417
|
+
if (!id) return { ok: false, id, path: null, reason: "no-id" };
|
|
418
|
+
|
|
419
|
+
const pendingPath = join(pendingDir(agentRoot), `${id}.md`);
|
|
420
|
+
if (!existsSync(pendingPath)) return { ok: false, id, path: null, reason: "not-pending" };
|
|
421
|
+
|
|
422
|
+
let md;
|
|
423
|
+
try { md = readFileSync(pendingPath, "utf8"); }
|
|
424
|
+
catch (e) {
|
|
425
|
+
appendSkillAudit(agentRoot, { action: "abort", requested: "approve", id, reason: "read-failed", error: e && e.message ? e.message : String(e) });
|
|
426
|
+
return { ok: false, id, path: null, reason: "read-failed" };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const description = (md.match(/^description:\s*(.*)$/m) || [, ""])[1].trim();
|
|
430
|
+
const livePath = join(skillsDir(agentRoot), `${id}.md`);
|
|
431
|
+
try {
|
|
432
|
+
writeAtomicFile(livePath, withStatus(md, "approved"));
|
|
433
|
+
// Drop the pending copy once the live copy is in place.
|
|
434
|
+
try { renameSync(pendingPath, `${pendingPath}.approved.${Date.now()}.bak`); } catch { /* best-effort */ }
|
|
435
|
+
} catch (e) {
|
|
436
|
+
appendSkillAudit(agentRoot, { action: "abort", requested: "approve", id, reason: "write-failed", error: e && e.message ? e.message : String(e) });
|
|
437
|
+
return { ok: false, id, path: null, reason: "write-failed" };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try { upsertPluginEntry(agentRoot, { name: id, description, path: `skills/${id}.md` }); } catch { /* fail-open */ }
|
|
441
|
+
try { upsertSidecar(agentRoot, id, { state: "active", now: nowIso }); } catch { /* fail-open */ }
|
|
442
|
+
appendSkillAudit(agentRoot, { action: "approve", id, approved_by: o.approvedBy || null });
|
|
443
|
+
|
|
444
|
+
return { ok: true, id, path: livePath, reason: "ok" };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Emit an operator notification for a quarantined skill: a stderr line plus a
|
|
449
|
+
* dedicated audit row. Never throws (runs inside a fail-open write path).
|
|
450
|
+
*/
|
|
451
|
+
function notifyPendingSkill(agentRoot, { id, name, path }) {
|
|
452
|
+
const approveHint = `maestro skill approve ${id}`;
|
|
453
|
+
try {
|
|
454
|
+
process.stderr.write(
|
|
455
|
+
`[maestro:learning] PENDING skill authored — not loaded until approved: "${name}" (id=${id}) at ${path}. Approve with: ${approveHint}\n`,
|
|
456
|
+
);
|
|
457
|
+
} catch { /* stderr unavailable; the audit row below is the durable record */ }
|
|
458
|
+
appendSkillAudit(agentRoot, { action: "pending_review", id, name, path, approve_hint: approveHint });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export default {
|
|
462
|
+
writeSkill,
|
|
463
|
+
approveSkill,
|
|
464
|
+
scanForSecrets,
|
|
465
|
+
slugify,
|
|
466
|
+
renderSkillMd,
|
|
467
|
+
skillsDir,
|
|
468
|
+
pendingDir,
|
|
469
|
+
readSidecar,
|
|
470
|
+
writeSidecar,
|
|
471
|
+
touchSkill,
|
|
472
|
+
appendSkillAudit,
|
|
473
|
+
SECRET_PATTERNS,
|
|
474
|
+
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-writer.test.mjs — autonomous skill write-path tests.
|
|
3
|
+
* Run: node --test lib/learning/skill-writer.test.mjs
|
|
4
|
+
*
|
|
5
|
+
* Covers: flat-skill frontmatter, slug dedupe, secret-scan ABORT, plugin.json
|
|
6
|
+
* upsert, atomic write, audit rows, patch overwrite, sidecar lifecycle.
|
|
7
|
+
*/
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, existsSync, readFileSync, readdirSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
writeSkill, approveSkill, scanForSecrets, slugify, renderSkillMd, readSidecar, touchSkill,
|
|
18
|
+
} from "./skill-writer.mjs";
|
|
19
|
+
|
|
20
|
+
function tmpRoot() {
|
|
21
|
+
return mkdtempSync(join(tmpdir(), "maestro-skillwriter-"));
|
|
22
|
+
}
|
|
23
|
+
function skillPath(root, id) {
|
|
24
|
+
return join(root, "plugins", "agent-skills", "skills", `${id}.md`);
|
|
25
|
+
}
|
|
26
|
+
function pendingPath(root, id) {
|
|
27
|
+
return join(root, "plugins", "agent-skills", ".pending", `${id}.md`);
|
|
28
|
+
}
|
|
29
|
+
function pluginExists(root) {
|
|
30
|
+
return existsSync(join(root, "plugins", "agent-skills", "plugin.json"));
|
|
31
|
+
}
|
|
32
|
+
function pluginJson(root) {
|
|
33
|
+
return JSON.parse(readFileSync(join(root, "plugins", "agent-skills", "plugin.json"), "utf8"));
|
|
34
|
+
}
|
|
35
|
+
function auditRows(root) {
|
|
36
|
+
const p = join(root, "logs", "audit", "skills.jsonl");
|
|
37
|
+
if (!existsSync(p)) return [];
|
|
38
|
+
return readFileSync(p, "utf8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("slugify: filesystem-safe, hyphenated, bounded", () => {
|
|
42
|
+
assert.equal(slugify("Reconcile the Billing Export!"), "reconcile-the-billing-export");
|
|
43
|
+
assert.equal(slugify(" "), "skill");
|
|
44
|
+
assert.equal(slugify("a/b\\c"), "a-b-c");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("renderSkillMd: agent provenance frontmatter + body", () => {
|
|
48
|
+
const md = renderSkillMd({
|
|
49
|
+
id: "x", name: "Do X", description: "does x", body: "# Do X\nsteps", sourceSession: "sess-1", createdAt: "2026-06-09T00:00:00Z",
|
|
50
|
+
});
|
|
51
|
+
assert.match(md, /^---\n/);
|
|
52
|
+
assert.match(md, /name: Do X/);
|
|
53
|
+
assert.match(md, /description: does x/);
|
|
54
|
+
assert.match(md, /created_by: agent/);
|
|
55
|
+
assert.match(md, /source_session: sess-1/);
|
|
56
|
+
assert.match(md, /\n# Do X\nsteps\n$/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("writeSkill create: QUARANTINES to .pending — not live, not in plugin.json, sidecar pending, audit", () => {
|
|
60
|
+
const root = tmpRoot();
|
|
61
|
+
const res = writeSkill({
|
|
62
|
+
agentRoot: root, action: "create", name: "Reconcile Billing Export",
|
|
63
|
+
description: "Re-run the monthly billing reconciliation", body: "# Reconcile\n1. pull export\n2. diff", sourceSession: "sess-42",
|
|
64
|
+
nowIso: "2026-06-09T00:00:00Z",
|
|
65
|
+
});
|
|
66
|
+
assert.equal(res.ok, true);
|
|
67
|
+
assert.equal(res.status, "pending", "a create lands pending, not live");
|
|
68
|
+
assert.equal(res.id, "reconcile-billing-export");
|
|
69
|
+
|
|
70
|
+
// The file is in the quarantine dir, NOT the live dir Claude Code loads.
|
|
71
|
+
assert.ok(existsSync(pendingPath(root, res.id)), "skill .md in .pending");
|
|
72
|
+
assert.ok(!existsSync(skillPath(root, res.id)), "NOT in the live skills dir");
|
|
73
|
+
// No plugin.json at all → Claude Code never loads the pending skill.
|
|
74
|
+
assert.ok(!pluginExists(root), "pending skill is NOT registered in plugin.json");
|
|
75
|
+
|
|
76
|
+
const body = readFileSync(pendingPath(root, res.id), "utf8");
|
|
77
|
+
assert.match(body, /created_by: agent/);
|
|
78
|
+
assert.match(body, /status: pending/);
|
|
79
|
+
assert.match(body, /pull export/);
|
|
80
|
+
|
|
81
|
+
const sidecar = readSidecar(root);
|
|
82
|
+
assert.equal(sidecar[res.id].created_by, "agent");
|
|
83
|
+
assert.equal(sidecar[res.id].state, "pending");
|
|
84
|
+
assert.equal(sidecar[res.id].pinned, false);
|
|
85
|
+
|
|
86
|
+
// Audit records the create + an operator-notification row.
|
|
87
|
+
const audit = auditRows(root);
|
|
88
|
+
assert.ok(audit.some((r) => r.action === "create" && r.id === "reconcile-billing-export" && r.status === "pending"));
|
|
89
|
+
const notice = audit.find((r) => r.action === "pending_review" && r.id === "reconcile-billing-export");
|
|
90
|
+
assert.ok(notice, "operator pending_review row emitted");
|
|
91
|
+
assert.match(notice.approve_hint, /approve reconcile-billing-export/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("approveSkill: promotes a pending skill to live + plugin.json + sidecar active", () => {
|
|
95
|
+
const root = tmpRoot();
|
|
96
|
+
const res = writeSkill({ agentRoot: root, name: "Daily Recon", description: "the recon", body: "# Steps\n1. go" });
|
|
97
|
+
assert.equal(res.status, "pending");
|
|
98
|
+
|
|
99
|
+
const ap = approveSkill({ agentRoot: root, id: res.id, approvedBy: "layla", nowIso: "2026-06-10T00:00:00Z" });
|
|
100
|
+
assert.equal(ap.ok, true);
|
|
101
|
+
assert.equal(ap.id, res.id);
|
|
102
|
+
|
|
103
|
+
// Now live and registered; the raw pending .md is gone.
|
|
104
|
+
assert.ok(existsSync(skillPath(root, res.id)), "approved skill is live");
|
|
105
|
+
assert.ok(!existsSync(pendingPath(root, res.id)), "pending copy removed");
|
|
106
|
+
const live = readFileSync(skillPath(root, res.id), "utf8");
|
|
107
|
+
assert.match(live, /status: approved/);
|
|
108
|
+
|
|
109
|
+
const manifest = pluginJson(root);
|
|
110
|
+
assert.equal(manifest.skills.length, 1);
|
|
111
|
+
assert.deepEqual(manifest.skills[0], { name: res.id, description: "the recon", path: `skills/${res.id}.md` });
|
|
112
|
+
|
|
113
|
+
assert.equal(readSidecar(root)[res.id].state, "active");
|
|
114
|
+
assert.ok(auditRows(root).some((r) => r.action === "approve" && r.id === res.id && r.approved_by === "layla"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("approveSkill: unknown / not-pending id is a no-op failure", () => {
|
|
118
|
+
const root = tmpRoot();
|
|
119
|
+
const ap = approveSkill({ agentRoot: root, id: "never-authored" });
|
|
120
|
+
assert.equal(ap.ok, false);
|
|
121
|
+
assert.equal(ap.reason, "not-pending");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("writeSkill create: slug dedupe across BOTH pending and live", () => {
|
|
125
|
+
const root = tmpRoot();
|
|
126
|
+
const a = writeSkill({ agentRoot: root, name: "Daily Recon", description: "d", body: "v1" });
|
|
127
|
+
const b = writeSkill({ agentRoot: root, name: "Daily Recon", description: "d", body: "v2" });
|
|
128
|
+
assert.equal(a.id, "daily-recon");
|
|
129
|
+
assert.equal(b.id, "daily-recon-2", "second create dedupes the slug even in quarantine");
|
|
130
|
+
assert.ok(existsSync(pendingPath(root, "daily-recon")));
|
|
131
|
+
assert.ok(existsSync(pendingPath(root, "daily-recon-2")));
|
|
132
|
+
// Approving the first must still not collide with the pending second.
|
|
133
|
+
approveSkill({ agentRoot: root, id: "daily-recon" });
|
|
134
|
+
const c = writeSkill({ agentRoot: root, name: "Daily Recon", description: "d", body: "v3" });
|
|
135
|
+
assert.equal(c.id, "daily-recon-3", "dedupe checks the live dir too");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("writeSkill patch: a LIVE (approved) skill is edited in place and stays live", () => {
|
|
139
|
+
const root = tmpRoot();
|
|
140
|
+
const created = writeSkill({ agentRoot: root, name: "Patchable", description: "first", body: "OLD BODY" });
|
|
141
|
+
approveSkill({ agentRoot: root, id: created.id }); // operator approves before any patch
|
|
142
|
+
const b = writeSkill({ agentRoot: root, action: "patch", id: created.id, name: "Patchable", description: "second", body: "NEW BODY" });
|
|
143
|
+
assert.equal(b.ok, true);
|
|
144
|
+
assert.equal(b.status, "approved", "patch of a live skill stays live");
|
|
145
|
+
assert.equal(b.id, created.id, "patch keeps the id");
|
|
146
|
+
const body = readFileSync(skillPath(root, created.id), "utf8");
|
|
147
|
+
assert.match(body, /NEW BODY/);
|
|
148
|
+
assert.doesNotMatch(body, /OLD BODY/, "old body overwritten");
|
|
149
|
+
// create-or-replace by name → still one entry, updated description.
|
|
150
|
+
const manifest = pluginJson(root);
|
|
151
|
+
assert.equal(manifest.skills.length, 1);
|
|
152
|
+
assert.equal(manifest.skills[0].description, "second");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("writeSkill patch: a NON-live target is downgraded to a quarantined create", () => {
|
|
156
|
+
const root = tmpRoot();
|
|
157
|
+
// Patch a skill that was never approved (only pending) → must not go live.
|
|
158
|
+
const res = writeSkill({ agentRoot: root, action: "patch", id: "ghost", name: "Ghost", description: "d", body: "BODY" });
|
|
159
|
+
assert.equal(res.ok, true);
|
|
160
|
+
assert.equal(res.action, "create", "patch of a non-live skill becomes a create");
|
|
161
|
+
assert.equal(res.status, "pending");
|
|
162
|
+
assert.ok(existsSync(pendingPath(root, res.id)));
|
|
163
|
+
assert.ok(!pluginExists(root), "still not registered");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("scanForSecrets: flags credential literals and sensitive assignments", () => {
|
|
167
|
+
assert.deepEqual(scanForSecrets("nothing to see here"), []);
|
|
168
|
+
assert.ok(scanForSecrets("export KEY=sk-ant-" + "A".repeat(20)).some((h) => h.label === "anthropic-key"));
|
|
169
|
+
assert.ok(scanForSecrets("token gh" + "p_" + "B".repeat(30)).some((h) => h.label === "github-pat"));
|
|
170
|
+
assert.ok(scanForSecrets("AKIA" + "Z".repeat(16)).some((h) => h.label === "aws-access-key-id"));
|
|
171
|
+
assert.ok(scanForSecrets("Authorization: Bearer " + "c".repeat(24)).some((h) => h.label === "bearer-token"));
|
|
172
|
+
assert.ok(scanForSecrets("password=hunter2longenough").some((h) => /assignment:password/.test(h.label)));
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("writeSkill: ABORTS on a secret in the body — no file, no manifest, audit abort row", () => {
|
|
176
|
+
const root = tmpRoot();
|
|
177
|
+
const res = writeSkill({
|
|
178
|
+
agentRoot: root, name: "Leaky Skill", description: "should not land",
|
|
179
|
+
body: "# Setup\nRun with API_KEY=sk-ant-" + "S".repeat(24) + " then proceed.",
|
|
180
|
+
});
|
|
181
|
+
assert.equal(res.ok, false);
|
|
182
|
+
assert.equal(res.reason, "secret-scan");
|
|
183
|
+
assert.ok(res.secrets.length >= 1);
|
|
184
|
+
assert.ok(!existsSync(skillPath(root, "leaky-skill")), "no skill file written");
|
|
185
|
+
assert.ok(!existsSync(join(root, "plugins", "agent-skills", "plugin.json")), "no manifest written");
|
|
186
|
+
const audit = auditRows(root);
|
|
187
|
+
assert.equal(audit.length, 1);
|
|
188
|
+
assert.equal(audit[0].action, "abort");
|
|
189
|
+
assert.equal(audit[0].reason, "secret-scan");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("writeSkill: atomic — leaves no .tmp turds behind", () => {
|
|
193
|
+
const root = tmpRoot();
|
|
194
|
+
writeSkill({ agentRoot: root, name: "Clean Write", description: "d", body: "x" });
|
|
195
|
+
// A create quarantines, so the atomic write lands in .pending.
|
|
196
|
+
const dir = join(root, "plugins", "agent-skills", ".pending");
|
|
197
|
+
const names = existsSync(dir) ? readdirSync(dir) : [];
|
|
198
|
+
assert.ok(names.length >= 1);
|
|
199
|
+
assert.ok(names.every((n) => !n.endsWith(".tmp")), "no leftover tmp files");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("touchSkill: bumps last_activity only for known ids", () => {
|
|
203
|
+
const root = tmpRoot();
|
|
204
|
+
const a = writeSkill({ agentRoot: root, name: "Touchable", description: "d", body: "x", nowIso: "2026-06-01T00:00:00Z" });
|
|
205
|
+
touchSkill(root, a.id, { nowIso: "2026-06-09T00:00:00Z" });
|
|
206
|
+
assert.equal(readSidecar(root)[a.id].last_activity, "2026-06-09T00:00:00Z");
|
|
207
|
+
// Unknown id is a no-op (no new record created).
|
|
208
|
+
touchSkill(root, "does-not-exist", { nowIso: "2026-06-09T00:00:00Z" });
|
|
209
|
+
assert.ok(!readSidecar(root)["does-not-exist"]);
|
|
210
|
+
});
|