xcoder-dna 0.0.0-stage → 1.3.1
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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/agents/ai-chat-debate.d.ts +36 -0
- package/dist/agents/ai-chat-debate.js +300 -0
- package/dist/agents/ai-chat-report-writer.d.ts +7 -0
- package/dist/agents/ai-chat-report-writer.js +100 -0
- package/dist/agents/duplicate-detector.d.ts +41 -0
- package/dist/agents/duplicate-detector.js +65 -0
- package/dist/agents/genesis-agent.d.ts +61 -0
- package/dist/agents/genesis-agent.js +340 -0
- package/dist/agents/samurai-agent.d.ts +53 -0
- package/dist/agents/samurai-agent.js +279 -0
- package/dist/agents/sub-agent.d.ts +56 -0
- package/dist/agents/sub-agent.js +353 -0
- package/dist/backlog/backlog-requirement-validator.d.ts +10 -0
- package/dist/backlog/backlog-requirement-validator.js +60 -0
- package/dist/backlog/backlog-store.d.ts +29 -0
- package/dist/backlog/backlog-store.js +148 -0
- package/dist/backlog/po-agent.d.ts +16 -0
- package/dist/backlog/po-agent.js +121 -0
- package/dist/backlog/types.d.ts +25 -0
- package/dist/backlog/types.js +4 -0
- package/dist/bench/run.d.ts +11 -0
- package/dist/bench/run.js +185 -0
- package/dist/bench/types.d.ts +50 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/swarm.d.ts +10 -0
- package/dist/cli/swarm.js +100 -0
- package/dist/cli/task.d.ts +14 -0
- package/dist/cli/task.js +121 -0
- package/dist/cli/wbc-arg.d.ts +6 -0
- package/dist/cli/wbc-arg.js +40 -0
- package/dist/cli/wbc.d.ts +45 -0
- package/dist/cli/wbc.js +98 -0
- package/dist/commands/license.d.ts +9 -0
- package/dist/commands/license.js +140 -0
- package/dist/commands/release.d.ts +44 -0
- package/dist/commands/release.js +180 -0
- package/dist/commands/test.d.ts +50 -0
- package/dist/commands/test.js +385 -0
- package/dist/config/loadConfig.d.ts +82 -0
- package/dist/config/loadConfig.js +240 -0
- package/dist/config/secure-mode.d.ts +29 -0
- package/dist/config/secure-mode.js +63 -0
- package/dist/config/user-config.d.ts +39 -0
- package/dist/config/user-config.js +202 -0
- package/dist/engine/reasoning-engine.d.ts +21 -0
- package/dist/engine/reasoning-engine.js +59 -0
- package/dist/events/bus.d.ts +22 -0
- package/dist/events/bus.js +18 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +77 -0
- package/dist/indexing/ignoreRules.d.ts +1 -0
- package/dist/indexing/ignoreRules.js +53 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.d.ts +57 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.js +204 -0
- package/dist/instance-swarm/protocol-name-resolver.d.ts +23 -0
- package/dist/instance-swarm/protocol-name-resolver.js +64 -0
- package/dist/license/gate.d.ts +31 -0
- package/dist/license/gate.js +188 -0
- package/dist/license/http-client.d.ts +43 -0
- package/dist/license/http-client.js +195 -0
- package/dist/license/manager.d.ts +39 -0
- package/dist/license/manager.js +175 -0
- package/dist/license/receipt.d.ts +27 -0
- package/dist/license/receipt.js +108 -0
- package/dist/license/state-codec.d.ts +72 -0
- package/dist/license/state-codec.js +218 -0
- package/dist/license/state-paths.d.ts +17 -0
- package/dist/license/state-paths.js +64 -0
- package/dist/license/store.d.ts +44 -0
- package/dist/license/store.js +223 -0
- package/dist/license/trust-anchor.d.ts +24 -0
- package/dist/license/trust-anchor.js +74 -0
- package/dist/license/types.d.ts +34 -0
- package/dist/license/types.js +19 -0
- package/dist/llm/budget.d.ts +23 -0
- package/dist/llm/budget.js +58 -0
- package/dist/llm/client.d.ts +53 -0
- package/dist/llm/client.js +122 -0
- package/dist/llm/factory.d.ts +14 -0
- package/dist/llm/factory.js +32 -0
- package/dist/orchestrator/defect-logger.d.ts +31 -0
- package/dist/orchestrator/defect-logger.js +101 -0
- package/dist/orchestrator/defect-manager.d.ts +118 -0
- package/dist/orchestrator/defect-manager.js +338 -0
- package/dist/orchestrator/defect-migration.d.ts +14 -0
- package/dist/orchestrator/defect-migration.js +174 -0
- package/dist/orchestrator/defect-store.d.ts +48 -0
- package/dist/orchestrator/defect-store.js +76 -0
- package/dist/orchestrator/healing.d.ts +14 -0
- package/dist/orchestrator/healing.js +11 -0
- package/dist/orchestrator/interactive-steering.d.ts +64 -0
- package/dist/orchestrator/interactive-steering.js +107 -0
- package/dist/orchestrator/interrupt-handler.d.ts +30 -0
- package/dist/orchestrator/interrupt-handler.js +70 -0
- package/dist/orchestrator/lesson-manager.d.ts +25 -0
- package/dist/orchestrator/lesson-manager.js +122 -0
- package/dist/orchestrator/progress-summary.d.ts +15 -0
- package/dist/orchestrator/progress-summary.js +106 -0
- package/dist/orchestrator/reasoning-score.d.ts +49 -0
- package/dist/orchestrator/reasoning-score.js +117 -0
- package/dist/orchestrator/swarm.d.ts +36 -0
- package/dist/orchestrator/swarm.js +77 -0
- package/dist/orchestrator/task-factory.d.ts +15 -0
- package/dist/orchestrator/task-factory.js +176 -0
- package/dist/orchestrator/validation-gate.d.ts +23 -0
- package/dist/orchestrator/validation-gate.js +155 -0
- package/dist/protocol/react-contract.d.ts +136 -0
- package/dist/protocol/react-contract.js +698 -0
- package/dist/protocol/wks-protocol-manager.d.ts +22 -0
- package/dist/protocol/wks-protocol-manager.js +72 -0
- package/dist/sdlc-runtime/conversation-runner.d.ts +8 -0
- package/dist/sdlc-runtime/conversation-runner.js +64 -0
- package/dist/sdlc-runtime/dag-utils.d.ts +15 -0
- package/dist/sdlc-runtime/dag-utils.js +67 -0
- package/dist/sdlc-runtime/defect-integration.d.ts +7 -0
- package/dist/sdlc-runtime/defect-integration.js +99 -0
- package/dist/sdlc-runtime/execution-loop.d.ts +38 -0
- package/dist/sdlc-runtime/execution-loop.js +274 -0
- package/dist/sdlc-runtime/lesson-recorder.d.ts +9 -0
- package/dist/sdlc-runtime/lesson-recorder.js +27 -0
- package/dist/sdlc-runtime/plan-initializer.d.ts +47 -0
- package/dist/sdlc-runtime/plan-initializer.js +135 -0
- package/dist/sdlc-runtime/progress-persister.d.ts +10 -0
- package/dist/sdlc-runtime/progress-persister.js +34 -0
- package/dist/sdlc-runtime/protocol-loader.d.ts +10 -0
- package/dist/sdlc-runtime/protocol-loader.js +21 -0
- package/dist/sdlc-runtime/reporting.d.ts +24 -0
- package/dist/sdlc-runtime/reporting.js +68 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.d.ts +59 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.js +170 -0
- package/dist/sdlc-runtime/sdlc-skill.d.ts +44 -0
- package/dist/sdlc-runtime/sdlc-skill.js +148 -0
- package/dist/sdlc-runtime/steering-handler.d.ts +14 -0
- package/dist/sdlc-runtime/steering-handler.js +51 -0
- package/dist/sdlc-runtime/trace-recorder.d.ts +10 -0
- package/dist/sdlc-runtime/trace-recorder.js +7 -0
- package/dist/security/index.d.ts +16 -0
- package/dist/security/index.js +12 -0
- package/dist/security/input-screen.d.ts +30 -0
- package/dist/security/input-screen.js +75 -0
- package/dist/security/tool-call-guard.d.ts +16 -0
- package/dist/security/tool-call-guard.js +163 -0
- package/dist/security/untrusted-content.d.ts +34 -0
- package/dist/security/untrusted-content.js +63 -0
- package/dist/skills/advanced-dfir/SKILL.md +85 -0
- package/dist/skills/advanced-pentest-lab/SKILL.md +88 -0
- package/dist/skills/analyst/SKILL.md +55 -0
- package/dist/skills/architect/SKILL.md +54 -0
- package/dist/skills/aws/SKILL.md +92 -0
- package/dist/skills/azure/SKILL.md +90 -0
- package/dist/skills/bylaw-context.d.ts +45 -0
- package/dist/skills/bylaw-context.js +61 -0
- package/dist/skills/bylaw-review.d.ts +20 -0
- package/dist/skills/bylaw-review.js +101 -0
- package/dist/skills/conversation/SKILL.md +29 -0
- package/dist/skills/design-patterns/SKILL.md +190 -0
- package/dist/skills/devops/SKILL.md +55 -0
- package/dist/skills/directives.d.ts +20 -0
- package/dist/skills/directives.js +313 -0
- package/dist/skills/docker/SKILL.md +76 -0
- package/dist/skills/docker-expert/SKILL.md +56 -0
- package/dist/skills/enterprise-systems-analyst/SKILL.md +83 -0
- package/dist/skills/enterprise-systems-analyst/references/crm.md +41 -0
- package/dist/skills/enterprise-systems-analyst/references/erp.md +40 -0
- package/dist/skills/enterprise-systems-analyst/references/financial-systems.md +35 -0
- package/dist/skills/enterprise-systems-analyst/references/hcm.md +38 -0
- package/dist/skills/enterprise-systems-analyst/references/mrp.md +37 -0
- package/dist/skills/enterprise-systems-analyst/references/scm.md +32 -0
- package/dist/skills/ethical-hacking-methodology/SKILL.md +119 -0
- package/dist/skills/externalSkillLoader.d.ts +25 -0
- package/dist/skills/externalSkillLoader.js +65 -0
- package/dist/skills/externalSkillRegistry.d.ts +33 -0
- package/dist/skills/externalSkillRegistry.js +94 -0
- package/dist/skills/filesystem-management/SKILL.md +61 -0
- package/dist/skills/first-run-seed.d.ts +26 -0
- package/dist/skills/first-run-seed.js +75 -0
- package/dist/skills/git-vcs/SKILL.md +84 -0
- package/dist/skills/kafka/SKILL.md +89 -0
- package/dist/skills/kubernetes/SKILL.md +93 -0
- package/dist/skills/kubernetes-expert/SKILL.md +54 -0
- package/dist/skills/openshift/SKILL.md +93 -0
- package/dist/skills/pentester/SKILL.md +59 -0
- package/dist/skills/performance-tester/SKILL.md +57 -0
- package/dist/skills/playwright-ui-tester/SKILL.md +67 -0
- package/dist/skills/programmer/SKILL.md +58 -0
- package/dist/skills/project-scaffolding/SKILL.md +133 -0
- package/dist/skills/qa-engineer/SKILL.md +91 -0
- package/dist/skills/rca/SKILL.md +58 -0
- package/dist/skills/redhat/SKILL.md +92 -0
- package/dist/skills/rosa/SKILL.md +80 -0
- package/dist/skills/scrum-framework/SKILL.md +74 -0
- package/dist/skills/scrum-framework/references/artifacts_and_quality.md +79 -0
- package/dist/skills/scrum-framework/references/roles_and_events.md +105 -0
- package/dist/skills/scrum-master-agent/SKILL.md +74 -0
- package/dist/skills/scrum-master-agent/references/behaviors.md +56 -0
- package/dist/skills/sdlc-skill.d.ts +1 -0
- package/dist/skills/sdlc-skill.js +4 -0
- package/dist/skills/secops/SKILL.md +52 -0
- package/dist/skills/secure-architect.bylaws.d.ts +68 -0
- package/dist/skills/secure-architect.bylaws.js +525 -0
- package/dist/skills/seed-skills.d.ts +48 -0
- package/dist/skills/seed-skills.js +213 -0
- package/dist/skills/skill-authoring/SKILL.md +123 -0
- package/dist/skills/skill-header.d.ts +51 -0
- package/dist/skills/skill-header.js +163 -0
- package/dist/skills/skill-path-guard.d.ts +41 -0
- package/dist/skills/skill-path-guard.js +95 -0
- package/dist/skills/skillRouter.d.ts +51 -0
- package/dist/skills/skillRouter.js +90 -0
- package/dist/skills/skills-root.d.ts +64 -0
- package/dist/skills/skills-root.js +168 -0
- package/dist/skills/software-architect/SKILL.md +77 -0
- package/dist/skills/software-engineer/SKILL.md +74 -0
- package/dist/skills/solid-principles/SKILL.md +94 -0
- package/dist/skills/task-planning/SKILL.md +115 -0
- package/dist/skills/tester/SKILL.md +54 -0
- package/dist/skills/ubuntu/SKILL.md +87 -0
- package/dist/skills/ui-ux-design/SKILL.md +114 -0
- package/dist/skills/workspace-context/SKILL.md +103 -0
- package/dist/sprint/controller.d.ts +37 -0
- package/dist/sprint/controller.js +220 -0
- package/dist/sprint/dag-builder.d.ts +14 -0
- package/dist/sprint/dag-builder.js +105 -0
- package/dist/sprint/reporter.d.ts +11 -0
- package/dist/sprint/reporter.js +97 -0
- package/dist/sprint/state-store.d.ts +10 -0
- package/dist/sprint/state-store.js +71 -0
- package/dist/sprint/types.d.ts +24 -0
- package/dist/sprint/types.js +4 -0
- package/dist/store/file-version-store.d.ts +24 -0
- package/dist/store/file-version-store.js +73 -0
- package/dist/store/task-store.d.ts +90 -0
- package/dist/store/task-store.js +166 -0
- package/dist/task-ingest/architect-detailer.d.ts +43 -0
- package/dist/task-ingest/architect-detailer.js +141 -0
- package/dist/task-ingest/detail-schema.d.ts +9 -0
- package/dist/task-ingest/detail-schema.js +195 -0
- package/dist/task-ingest/detailed-run.d.ts +36 -0
- package/dist/task-ingest/detailed-run.js +108 -0
- package/dist/task-ingest/extract-id.d.ts +9 -0
- package/dist/task-ingest/extract-id.js +24 -0
- package/dist/task-ingest/index.d.ts +23 -0
- package/dist/task-ingest/index.js +14 -0
- package/dist/task-ingest/ingest.d.ts +45 -0
- package/dist/task-ingest/ingest.js +163 -0
- package/dist/task-ingest/list-ingested.d.ts +13 -0
- package/dist/task-ingest/list-ingested.js +30 -0
- package/dist/task-ingest/pending-filter.d.ts +30 -0
- package/dist/task-ingest/pending-filter.js +59 -0
- package/dist/task-ingest/scanner.d.ts +56 -0
- package/dist/task-ingest/scanner.js +216 -0
- package/dist/task-ingest/source-reader.d.ts +25 -0
- package/dist/task-ingest/source-reader.js +93 -0
- package/dist/task-ingest/spec-builder.d.ts +34 -0
- package/dist/task-ingest/spec-builder.js +89 -0
- package/dist/task-ingest/wbs-extract.d.ts +50 -0
- package/dist/task-ingest/wbs-extract.js +226 -0
- package/dist/task-ingest/wbs-parser.d.ts +36 -0
- package/dist/task-ingest/wbs-parser.js +170 -0
- package/dist/tools/apiTestTool.d.ts +37 -0
- package/dist/tools/apiTestTool.js +128 -0
- package/dist/tools/ast-tools.d.ts +183 -0
- package/dist/tools/ast-tools.js +853 -0
- package/dist/tools/browse-tool.d.ts +105 -0
- package/dist/tools/browse-tool.js +394 -0
- package/dist/tools/credential.d.ts +17 -0
- package/dist/tools/credential.js +32 -0
- package/dist/tools/dependency-graph.d.ts +23 -0
- package/dist/tools/dependency-graph.js +164 -0
- package/dist/tools/edit-tool.d.ts +34 -0
- package/dist/tools/edit-tool.js +95 -0
- package/dist/tools/file-header-manager.d.ts +27 -0
- package/dist/tools/file-header-manager.js +153 -0
- package/dist/tools/fs-tools.d.ts +154 -0
- package/dist/tools/fs-tools.js +337 -0
- package/dist/tools/git-tools.d.ts +79 -0
- package/dist/tools/git-tools.js +348 -0
- package/dist/tools/github-tool.d.ts +48 -0
- package/dist/tools/github-tool.js +147 -0
- package/dist/tools/globTool.d.ts +20 -0
- package/dist/tools/globTool.js +34 -0
- package/dist/tools/grepTool.d.ts +19 -0
- package/dist/tools/grepTool.js +44 -0
- package/dist/tools/index-freshness.d.ts +58 -0
- package/dist/tools/index-freshness.js +190 -0
- package/dist/tools/index-glob.d.ts +48 -0
- package/dist/tools/index-glob.js +175 -0
- package/dist/tools/load-skill-tool.d.ts +15 -0
- package/dist/tools/load-skill-tool.js +114 -0
- package/dist/tools/patch-tools.d.ts +58 -0
- package/dist/tools/patch-tools.js +153 -0
- package/dist/tools/playwright-tool.d.ts +66 -0
- package/dist/tools/playwright-tool.js +121 -0
- package/dist/tools/project-detect.d.ts +20 -0
- package/dist/tools/project-detect.js +87 -0
- package/dist/tools/prompt-guidance.d.ts +22 -0
- package/dist/tools/prompt-guidance.js +23 -0
- package/dist/tools/scheduleTool.d.ts +19 -0
- package/dist/tools/scheduleTool.js +181 -0
- package/dist/tools/search-tools.d.ts +27 -0
- package/dist/tools/search-tools.js +78 -0
- package/dist/tools/sed-tool.d.ts +66 -0
- package/dist/tools/sed-tool.js +402 -0
- package/dist/tools/shell-result.d.ts +11 -0
- package/dist/tools/shell-result.js +1 -0
- package/dist/tools/shell-tool.d.ts +45 -0
- package/dist/tools/shell-tool.js +141 -0
- package/dist/tools/siteCrawlerTool.d.ts +32 -0
- package/dist/tools/siteCrawlerTool.js +205 -0
- package/dist/tools/tool-registry.d.ts +67 -0
- package/dist/tools/tool-registry.js +596 -0
- package/dist/tools/validate-tool.d.ts +23 -0
- package/dist/tools/validate-tool.js +201 -0
- package/dist/tools/workspace-index.d.ts +46 -0
- package/dist/tools/workspace-index.js +173 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +7 -0
- package/dist/updater/index.d.ts +14 -0
- package/dist/updater/index.js +120 -0
- package/dist/util/banner.d.ts +25 -0
- package/dist/util/banner.js +140 -0
- package/dist/util/command-suggester.d.ts +6 -0
- package/dist/util/command-suggester.js +25 -0
- package/dist/util/confirm-interrupt.d.ts +10 -0
- package/dist/util/confirm-interrupt.js +31 -0
- package/dist/util/continue-trigger.d.ts +11 -0
- package/dist/util/continue-trigger.js +20 -0
- package/dist/util/env-info.d.ts +18 -0
- package/dist/util/env-info.js +37 -0
- package/dist/util/file-access-log.d.ts +35 -0
- package/dist/util/file-access-log.js +140 -0
- package/dist/util/footer-manager.d.ts +124 -0
- package/dist/util/footer-manager.js +195 -0
- package/dist/util/footer-renderer.d.ts +67 -0
- package/dist/util/footer-renderer.js +319 -0
- package/dist/util/footer-state.d.ts +34 -0
- package/dist/util/footer-state.js +18 -0
- package/dist/util/footer-text.d.ts +51 -0
- package/dist/util/footer-text.js +74 -0
- package/dist/util/ids.d.ts +1 -0
- package/dist/util/ids.js +4 -0
- package/dist/util/levenshtein.d.ts +6 -0
- package/dist/util/levenshtein.js +34 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.js +41 -0
- package/dist/util/logs.d.ts +26 -0
- package/dist/util/logs.js +88 -0
- package/dist/util/output-handler.d.ts +89 -0
- package/dist/util/output-handler.js +193 -0
- package/dist/util/output-interceptor.d.ts +18 -0
- package/dist/util/output-interceptor.js +49 -0
- package/dist/util/output-paths.d.ts +43 -0
- package/dist/util/output-paths.js +121 -0
- package/dist/util/package-json.d.ts +6 -0
- package/dist/util/package-json.js +36 -0
- package/dist/util/process-kill.d.ts +27 -0
- package/dist/util/process-kill.js +148 -0
- package/dist/util/protocol-error-logger.d.ts +33 -0
- package/dist/util/protocol-error-logger.js +73 -0
- package/dist/util/redact.d.ts +25 -0
- package/dist/util/redact.js +103 -0
- package/dist/util/scroll-region-guard.d.ts +33 -0
- package/dist/util/scroll-region-guard.js +63 -0
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.js +49 -0
- package/dist/util/terminal-output.d.ts +34 -0
- package/dist/util/terminal-output.js +1 -0
- package/dist/util/terminal-silent.d.ts +6 -0
- package/dist/util/terminal-silent.js +20 -0
- package/dist/util/terminal.d.ts +56 -0
- package/dist/util/terminal.js +124 -0
- package/dist/util/text-sanitize.d.ts +8 -0
- package/dist/util/text-sanitize.js +45 -0
- package/dist/util/thinking-spinner.d.ts +27 -0
- package/dist/util/thinking-spinner.js +189 -0
- package/dist/util/tool-error-logger.d.ts +33 -0
- package/dist/util/tool-error-logger.js +77 -0
- package/dist/util/tty.d.ts +13 -0
- package/dist/util/tty.js +16 -0
- package/dist/util/xcoder-paths.d.ts +56 -0
- package/dist/util/xcoder-paths.js +63 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/worker-agent/to-subtask-node.d.ts +35 -0
- package/dist/worker-agent/to-subtask-node.js +41 -0
- package/dist/worker-agent/worker-orchestrator.d.ts +104 -0
- package/dist/worker-agent/worker-orchestrator.js +201 -0
- package/package.json +38 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// xcoder:version 5 | xcoder:task task-f9cf18 | xcoder:updated 2026-11-02T16:15:28.268Z | xcoder:subtask code-st-077ea2
|
|
2
|
+
/**
|
|
3
|
+
* Idempotent, non-destructive seeding of the home skills directory.
|
|
4
|
+
*
|
|
5
|
+
* On first run the shipped packs are copied to ~/.xcoder/skills so the user
|
|
6
|
+
* owns an editable tree that survives uninstall. Precision matters here, so
|
|
7
|
+
* ownership is tracked with a per-file SHA-256 manifest:
|
|
8
|
+
*
|
|
9
|
+
* absent -> write (created)
|
|
10
|
+
* present, hashes match -> leave alone (unchanged)
|
|
11
|
+
* present, differs, IN manifest -> overwrite (repaired)
|
|
12
|
+
* present, differs, NOT in manifest -> never touch (preserved)
|
|
13
|
+
* extra pack not in source -> never touch (preserved)
|
|
14
|
+
*
|
|
15
|
+
* There are NO deletes anywhere in this module — the one failure mode that
|
|
16
|
+
* cannot be undone is deleting a file the user wrote.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import fsp from "node:fs/promises";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { getUserHomeSkillsDir, probeSkillsDir } from "./skills-root.js";
|
|
23
|
+
import { MAX_PACK_DIRS, validateSkillName } from "./skill-path-guard.js";
|
|
24
|
+
/** Ceiling on files considered inside one pack (defensive, not a real limit). */
|
|
25
|
+
const MAX_PACK_FILES = 2048;
|
|
26
|
+
/** Ceiling on directory nesting inside one pack. */
|
|
27
|
+
const MAX_DEPTH = 8;
|
|
28
|
+
function sha256(buf) {
|
|
29
|
+
return createHash("sha256").update(buf).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
/** Walk `baseDir` and return posix-relative file paths -> sha256, all bounded. */
|
|
32
|
+
async function hashTree(baseDir) {
|
|
33
|
+
const out = new Map();
|
|
34
|
+
async function walk(dir, prefix, depth) {
|
|
35
|
+
if (depth > MAX_DEPTH)
|
|
36
|
+
return;
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (out.size >= MAX_PACK_FILES)
|
|
46
|
+
return;
|
|
47
|
+
if (entry.isSymbolicLink())
|
|
48
|
+
continue;
|
|
49
|
+
if (!validateSkillName(entry.name).ok)
|
|
50
|
+
continue;
|
|
51
|
+
const abs = path.join(dir, entry.name);
|
|
52
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
53
|
+
if (entry.isDirectory()) {
|
|
54
|
+
await walk(abs, rel, depth + 1);
|
|
55
|
+
}
|
|
56
|
+
else if (entry.isFile()) {
|
|
57
|
+
try {
|
|
58
|
+
out.set(rel, sha256(await fsp.readFile(abs)));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Unreadable source file: skip rather than abort the whole seed.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await walk(baseDir, "", 0);
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/** Read and schema-check an existing manifest; a bad manifest means "no manifest". */
|
|
70
|
+
function readManifest(manifestPath) {
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = fs.readFileSync(manifestPath, "utf-8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
81
|
+
return undefined;
|
|
82
|
+
const m = parsed;
|
|
83
|
+
if (m["schemaVersion"] !== 1)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (typeof m["packs"] !== "object" || m["packs"] === null)
|
|
86
|
+
return undefined;
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Seed the home skills directory from the shipped packs.
|
|
95
|
+
*
|
|
96
|
+
* Never throws: every failure is recorded in `errors` and the run continues.
|
|
97
|
+
*/
|
|
98
|
+
export async function seedSkillsHome(opts) {
|
|
99
|
+
const homeDir = opts.homeDir;
|
|
100
|
+
const targetSkillsDir = getUserHomeSkillsDir(homeDir);
|
|
101
|
+
const manifestPath = opts.manifestPath ?? path.join(path.dirname(targetSkillsDir), "skills-manifest.json");
|
|
102
|
+
const result = {
|
|
103
|
+
action: "skipped",
|
|
104
|
+
written: [],
|
|
105
|
+
updated: [],
|
|
106
|
+
preserved: [],
|
|
107
|
+
errors: [],
|
|
108
|
+
manifestPath,
|
|
109
|
+
};
|
|
110
|
+
let sourcePacks;
|
|
111
|
+
try {
|
|
112
|
+
sourcePacks = fs.readdirSync(opts.sourceDir, { withFileTypes: true });
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
result.errors.push({ path: opts.sourceDir, reason: "source directory unreadable" });
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
const targetExisted = probeSkillsDir(targetSkillsDir).count > 0 || fs.existsSync(targetSkillsDir);
|
|
119
|
+
const manifest = readManifest(manifestPath);
|
|
120
|
+
const nextManifest = {
|
|
121
|
+
schemaVersion: 1,
|
|
122
|
+
generatedAt: new Date().toISOString(),
|
|
123
|
+
generator: "first-run",
|
|
124
|
+
packs: { ...(manifest?.packs ?? {}) },
|
|
125
|
+
};
|
|
126
|
+
let visited = 0;
|
|
127
|
+
for (const entry of sourcePacks) {
|
|
128
|
+
if (visited >= MAX_PACK_DIRS)
|
|
129
|
+
break;
|
|
130
|
+
if (!entry.isDirectory())
|
|
131
|
+
continue;
|
|
132
|
+
visited++;
|
|
133
|
+
if (!validateSkillName(entry.name).ok)
|
|
134
|
+
continue;
|
|
135
|
+
const sourcePackDir = path.join(opts.sourceDir, entry.name);
|
|
136
|
+
const targetPackDir = path.join(targetSkillsDir, entry.name);
|
|
137
|
+
const files = await hashTree(sourcePackDir);
|
|
138
|
+
if (files.size === 0) {
|
|
139
|
+
result.preserved.push(entry.name);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const ownedByUs = manifest?.packs?.[entry.name] !== undefined;
|
|
143
|
+
const targetPackExists = fs.existsSync(targetPackDir);
|
|
144
|
+
let touchedThisPack = false;
|
|
145
|
+
for (const [rel, sourceHash] of files) {
|
|
146
|
+
const targetFile = path.join(targetPackDir, ...rel.split("/"));
|
|
147
|
+
const exists = fs.existsSync(targetFile);
|
|
148
|
+
if (exists) {
|
|
149
|
+
const targetHash = sha256(await fsp.readFile(targetFile));
|
|
150
|
+
if (targetHash === sourceHash)
|
|
151
|
+
continue;
|
|
152
|
+
if (!ownedByUs) {
|
|
153
|
+
result.preserved.push(`${entry.name}/${rel}`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!opts.dryRun) {
|
|
157
|
+
try {
|
|
158
|
+
await fsp.mkdir(path.dirname(targetFile), { recursive: true });
|
|
159
|
+
await fsp.writeFile(targetFile, await fsp.readFile(path.join(sourcePackDir, ...rel.split("/"))));
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
result.errors.push({ path: targetFile, reason: e.message });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
touchedThisPack = true;
|
|
167
|
+
result.updated.push(`${entry.name}/${rel}`);
|
|
168
|
+
nextManifest.packs[entry.name] = { ...(nextManifest.packs[entry.name] ?? {}), [rel]: sourceHash };
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (!opts.dryRun) {
|
|
172
|
+
try {
|
|
173
|
+
await fsp.mkdir(path.dirname(targetFile), { recursive: true });
|
|
174
|
+
await fsp.writeFile(targetFile, await fsp.readFile(path.join(sourcePackDir, ...rel.split("/"))));
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
result.errors.push({ path: targetFile, reason: e.message });
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
touchedThisPack = true;
|
|
182
|
+
result.written.push(`${entry.name}/${rel}`);
|
|
183
|
+
nextManifest.packs[entry.name] = { ...(nextManifest.packs[entry.name] ?? {}), [rel]: sourceHash };
|
|
184
|
+
}
|
|
185
|
+
// A pack that already exists on disk whose files we neither wrote nor
|
|
186
|
+
// repaired is the user's, not ours — record it and drop any manifest claim.
|
|
187
|
+
if (targetPackExists && !touchedThisPack) {
|
|
188
|
+
result.preserved.push(entry.name);
|
|
189
|
+
delete nextManifest.packs[entry.name];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (opts.dryRun) {
|
|
193
|
+
result.action = "skipped";
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
await fsp.mkdir(path.dirname(manifestPath), { recursive: true });
|
|
198
|
+
await fsp.writeFile(manifestPath, `${JSON.stringify(nextManifest, null, 2)}\n`, "utf-8");
|
|
199
|
+
}
|
|
200
|
+
catch (e) {
|
|
201
|
+
result.errors.push({ path: manifestPath, reason: e.message });
|
|
202
|
+
}
|
|
203
|
+
if (result.updated.length > 0) {
|
|
204
|
+
result.action = "repaired";
|
|
205
|
+
}
|
|
206
|
+
else if (result.written.length > 0) {
|
|
207
|
+
result.action = targetExisted ? "repaired" : "created";
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
result.action = "unchanged";
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: skill-authoring
|
|
3
|
+
role: meta
|
|
4
|
+
description: Author new role skills for xcoder's own hot-pluggable skill system (agent/skills/<name>/SKILL.md) — correct frontmatter, well-scoped triggers, and a body that actually changes agent behavior instead of restating generic advice.
|
|
5
|
+
triggers:
|
|
6
|
+
- "create a skill"
|
|
7
|
+
- "new skill"
|
|
8
|
+
- "add a skill"
|
|
9
|
+
- "author a skill"
|
|
10
|
+
- "build a skill"
|
|
11
|
+
- "skill for"
|
|
12
|
+
- "write a skill"
|
|
13
|
+
- "skill.md"
|
|
14
|
+
- "skill registry"
|
|
15
|
+
version: "1.0.0"
|
|
16
|
+
requires_tools:
|
|
17
|
+
- read_tool
|
|
18
|
+
- write_edit_tool
|
|
19
|
+
- glob_tool
|
|
20
|
+
- grep_tool
|
|
21
|
+
composes_with:
|
|
22
|
+
- ui-ux-design
|
|
23
|
+
- task-planning
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Purpose
|
|
27
|
+
|
|
28
|
+
This skill is for when the task is to extend xcoder itself — adding a new role skill so future
|
|
29
|
+
tasks route to specialized instructions instead of the generic base ReAct loop. It is a meta-skill:
|
|
30
|
+
it does not solve the user's underlying problem, it produces a well-formed `SKILL.md` that will.
|
|
31
|
+
|
|
32
|
+
## How xcoder actually loads skills (read this before writing anything)
|
|
33
|
+
|
|
34
|
+
`src/core/skillRegistry.ts` is the source of truth — re-read it if this ever drifts:
|
|
35
|
+
|
|
36
|
+
- Skills live at `agent/skills/<name>/SKILL.md` (one directory per skill; the directory name
|
|
37
|
+
doesn't have to match `name` in the frontmatter, but keep them identical to avoid confusion).
|
|
38
|
+
- The file MUST match `^---\n(yaml)\n---\n(body)$` — a YAML frontmatter block bounded by `---`
|
|
39
|
+
lines, then a markdown body. `loadHeaders()` parses ONLY the frontmatter (cheap, called often);
|
|
40
|
+
`loadSkill()` parses frontmatter + body (only when a skill is actually selected).
|
|
41
|
+
- `route(taskDescription)` lowercases the task text and, for every skill, counts how many of its
|
|
42
|
+
`triggers` are substrings of it. Skills with score > 0 are returned sorted by score descending.
|
|
43
|
+
Multiple skills can and do fire for the same task — that's intentional (`composes_with` documents
|
|
44
|
+
which skills are expected to be selected together).
|
|
45
|
+
- There is no dedup, no stemming, no fuzzy match. `"deploy"` will not match `"deployment"` unless
|
|
46
|
+
you also list `"deployment"`. Write triggers as the literal substrings a real task description
|
|
47
|
+
would contain, including short synonyms and common phrasings — but not words so short/common
|
|
48
|
+
they'd false-positive on unrelated tasks (a bad trigger like `"add"` or `"fix"` will make a skill
|
|
49
|
+
fire on nearly everything).
|
|
50
|
+
|
|
51
|
+
## The frontmatter schema (exact — matches `SkillHeader` in `src/core/types.ts`)
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
name: kebab-case-unique-id # must be unique across agent/skills/*
|
|
55
|
+
role: short-noun-phrase # e.g. "design", "planning", "meta", "devops" — used for display
|
|
56
|
+
description: one sentence, third person, states what the skill is for and when it applies
|
|
57
|
+
triggers:
|
|
58
|
+
- "phrase one"
|
|
59
|
+
- "phrase two" # lowercase; substring match against the lowercased task text
|
|
60
|
+
version: "1.0.0" # semver as a string
|
|
61
|
+
requires_tools: # tool names this skill's instructions assume are available
|
|
62
|
+
- some_tool
|
|
63
|
+
composes_with: # names of OTHER skills this one is commonly selected alongside
|
|
64
|
+
- other-skill-name
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
All seven keys are required by the `SkillHeader` interface — an omitted key will still parse as
|
|
68
|
+
YAML but will be `undefined` at runtime and can break anything that reads it (e.g. `role` showing
|
|
69
|
+
as `undefined` in `xcoder --skills`). Fill in every field even if a list is empty (`[]`).
|
|
70
|
+
|
|
71
|
+
## Writing a body that changes behavior (the part that's easy to get wrong)
|
|
72
|
+
|
|
73
|
+
A skill body that just restates domain knowledge the model already has adds tokens without adding
|
|
74
|
+
value. A good skill body encodes the things that are true of *this specific codebase or workflow*
|
|
75
|
+
that the model can't otherwise infer:
|
|
76
|
+
|
|
77
|
+
- **Concrete conventions**: exact file paths, exact tool names, exact naming patterns this project
|
|
78
|
+
uses (e.g. "phase reports go to `tasks/<slug>-phase-<N>.md`", not "save your progress somewhere").
|
|
79
|
+
- **Decision rules tied to how the agent is actually scored or evaluated** — if there's a scoring
|
|
80
|
+
or validation mechanism (see the `task-planning` skill for this project's health-score system),
|
|
81
|
+
reference it by name and describe the specific behaviors it rewards/penalizes, not generic
|
|
82
|
+
"write good code" advice.
|
|
83
|
+
- **Failure patterns worth naming** — a skill earns its keep by preventing a mistake the model
|
|
84
|
+
would otherwise make on this exact class of task.
|
|
85
|
+
|
|
86
|
+
Recommended section structure (matches the `LoadedSkill.body` doc comment — "Process / Strategies
|
|
87
|
+
/ Instructions / Planning / Experience" — use the subset that's actually useful, don't pad):
|
|
88
|
+
|
|
89
|
+
```markdown
|
|
90
|
+
## Process
|
|
91
|
+
The concrete step order for this kind of task.
|
|
92
|
+
|
|
93
|
+
## Instructions
|
|
94
|
+
Non-negotiable rules — the things that are wrong if skipped.
|
|
95
|
+
|
|
96
|
+
## Strategies
|
|
97
|
+
Judgment calls / tradeoffs / "prefer X over Y when...".
|
|
98
|
+
|
|
99
|
+
## Experience
|
|
100
|
+
Known failure modes and how to recognize/avoid them.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Checklist before considering a new skill done
|
|
104
|
+
|
|
105
|
+
1. Frontmatter has all 7 keys, `name` is unique, `triggers` are realistic lowercase substrings.
|
|
106
|
+
2. `requires_tools` lists only tools that actually exist (check `src/tools/toolSchemas.ts` — grep
|
|
107
|
+
for `"_tool"` to get the current list rather than assuming).
|
|
108
|
+
3. `composes_with` references skills that actually exist under `agent/skills/`.
|
|
109
|
+
4. The body contains at least one thing a generalist model would NOT already know or would get
|
|
110
|
+
wrong without it. If you can't identify that thing, the task probably doesn't need a new skill.
|
|
111
|
+
5. Sanity-check by running `xcoder --skills` (uses `loadHeaders()`) to confirm it parses and shows
|
|
112
|
+
up with the right role/triggers, and test that the intended task phrasing actually routes to it
|
|
113
|
+
via `SkillRegistry.route(...)` logic (substring match, case-insensitive).
|
|
114
|
+
|
|
115
|
+
## Common mistakes
|
|
116
|
+
|
|
117
|
+
- Triggers that are single common words (`"design"`, `"plan"`) with no accompanying context words —
|
|
118
|
+
causes the skill to fire on unrelated tasks and dilutes routing quality for everything else.
|
|
119
|
+
- Forgetting `version`/`requires_tools`/`composes_with` because they "don't seem important" — they
|
|
120
|
+
parse fine as `undefined` and the failure only shows up later, somewhere else.
|
|
121
|
+
- A body that's a generic essay instead of project-specific, actionable instructions.
|
|
122
|
+
- Copy-pasting another skill's triggers "just in case" — this makes two skills fire together on
|
|
123
|
+
tasks that only need one, bloating the context sent to the LLM for no benefit.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, schema-validated SKILL.md frontmatter parsing.
|
|
3
|
+
*
|
|
4
|
+
* The previous implementation did an unbounded `readFileSync` and then
|
|
5
|
+
* dereferenced `header.name` straight off whatever `yaml.load` returned. A
|
|
6
|
+
* SKILL.md whose frontmatter was a YAML scalar (or an array) therefore threw
|
|
7
|
+
* a TypeError that took the whole scan down. This module closes that by:
|
|
8
|
+
*
|
|
9
|
+
* 1. reading at most `MAX_FRONTMATTER_BYTES` before parsing anything,
|
|
10
|
+
* 2. parsing with `JSON_SCHEMA` so YAML cannot construct arbitrary values,
|
|
11
|
+
* 3. refusing prototype-poisoning keys, and
|
|
12
|
+
* 4. normalising the result into a `SkillHeader` that downstream code can
|
|
13
|
+
* dereference without a guard.
|
|
14
|
+
*
|
|
15
|
+
* Every failure returns `{ ok: false, reason }` instead of throwing.
|
|
16
|
+
*/
|
|
17
|
+
import type { SkillHeader } from "../types.js";
|
|
18
|
+
/** Why a SKILL.md header could not be turned into a `SkillHeader`. */
|
|
19
|
+
export type HeaderParseFailure = "unreadable" | "too-large" | "no-frontmatter" | "yaml-error" | "not-object" | "schema-invalid" | "unsafe-key";
|
|
20
|
+
export interface HeaderParseResult {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
header?: SkillHeader;
|
|
23
|
+
body?: string;
|
|
24
|
+
reason?: HeaderParseFailure;
|
|
25
|
+
/** How many bytes of the file were actually read for the header. */
|
|
26
|
+
bytesRead: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Collapse a header value onto one line and cap its length, so a description
|
|
30
|
+
* containing newlines or control characters cannot restructure the prompt
|
|
31
|
+
* block the skill list is injected into (LLM01).
|
|
32
|
+
*/
|
|
33
|
+
export declare function sanitizeHeaderText(value: string, maxLen: number): string;
|
|
34
|
+
/** True when `value` can be safely dereferenced as a `SkillHeader`. */
|
|
35
|
+
export declare function isSkillHeader(value: unknown): value is SkillHeader;
|
|
36
|
+
/**
|
|
37
|
+
* Parse a SKILL.md header from an in-memory string.
|
|
38
|
+
* Pure — no filesystem access, so it is cheap to unit test.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseSkillHeaderBytes(raw: string): HeaderParseResult;
|
|
41
|
+
/**
|
|
42
|
+
* Parse a SKILL.md from disk.
|
|
43
|
+
*
|
|
44
|
+
* The header is always parsed from a bounded prefix (8 KiB by default); the
|
|
45
|
+
* body is only re-read in full when `withBody` is set, so the header path can
|
|
46
|
+
* never be made to allocate an arbitrary amount of memory.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseSkillHeaderFile(skillPath: string, opts?: {
|
|
49
|
+
withBody?: boolean;
|
|
50
|
+
maxBytes?: number;
|
|
51
|
+
}): HeaderParseResult;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// xcoder:version 1 | xcoder:task task-f9cf18 | xcoder:updated 2026-11-02T16:14:11.010Z | xcoder:subtask code-st-077ea2
|
|
2
|
+
/**
|
|
3
|
+
* Bounded, schema-validated SKILL.md frontmatter parsing.
|
|
4
|
+
*
|
|
5
|
+
* The previous implementation did an unbounded `readFileSync` and then
|
|
6
|
+
* dereferenced `header.name` straight off whatever `yaml.load` returned. A
|
|
7
|
+
* SKILL.md whose frontmatter was a YAML scalar (or an array) therefore threw
|
|
8
|
+
* a TypeError that took the whole scan down. This module closes that by:
|
|
9
|
+
*
|
|
10
|
+
* 1. reading at most `MAX_FRONTMATTER_BYTES` before parsing anything,
|
|
11
|
+
* 2. parsing with `JSON_SCHEMA` so YAML cannot construct arbitrary values,
|
|
12
|
+
* 3. refusing prototype-poisoning keys, and
|
|
13
|
+
* 4. normalising the result into a `SkillHeader` that downstream code can
|
|
14
|
+
* dereference without a guard.
|
|
15
|
+
*
|
|
16
|
+
* Every failure returns `{ ok: false, reason }` instead of throwing.
|
|
17
|
+
*/
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import * as yaml from "js-yaml";
|
|
20
|
+
import { MAX_FRONTMATTER_BYTES } from "./skill-path-guard.js";
|
|
21
|
+
/** Keys that would let a crafted header reach `Object.prototype`. */
|
|
22
|
+
const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
23
|
+
/** Matches `---` ... `---` followed by the body. Applied to a bounded prefix. */
|
|
24
|
+
const FRONTMATTER_RE = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
25
|
+
/** Longest single-line value kept in one header field (defensive cap). */
|
|
26
|
+
const MAX_FIELD_LEN = 4096;
|
|
27
|
+
/**
|
|
28
|
+
* Collapse a header value onto one line and cap its length, so a description
|
|
29
|
+
* containing newlines or control characters cannot restructure the prompt
|
|
30
|
+
* block the skill list is injected into (LLM01).
|
|
31
|
+
*/
|
|
32
|
+
export function sanitizeHeaderText(value, maxLen) {
|
|
33
|
+
return value
|
|
34
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
35
|
+
.replace(/\s+/g, " ")
|
|
36
|
+
.trim()
|
|
37
|
+
.slice(0, Math.max(0, maxLen));
|
|
38
|
+
}
|
|
39
|
+
/** True when `value` can be safely dereferenced as a `SkillHeader`. */
|
|
40
|
+
export function isSkillHeader(value) {
|
|
41
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
42
|
+
return false;
|
|
43
|
+
const v = value;
|
|
44
|
+
return (typeof v["name"] === "string" &&
|
|
45
|
+
v["name"].length > 0 &&
|
|
46
|
+
typeof v["description"] === "string");
|
|
47
|
+
}
|
|
48
|
+
function toStringArray(value) {
|
|
49
|
+
if (!Array.isArray(value))
|
|
50
|
+
return [];
|
|
51
|
+
return value
|
|
52
|
+
.filter((item) => typeof item === "string")
|
|
53
|
+
.map((item) => sanitizeHeaderText(item, 256))
|
|
54
|
+
.filter((item) => item.length > 0);
|
|
55
|
+
}
|
|
56
|
+
function asString(value) {
|
|
57
|
+
return typeof value === "string" ? sanitizeHeaderText(value, MAX_FIELD_LEN) : "";
|
|
58
|
+
}
|
|
59
|
+
/** Read at most `maxBytes` from the head of a file. */
|
|
60
|
+
function readPrefix(filePath, maxBytes) {
|
|
61
|
+
const fd = fs.openSync(filePath, "r");
|
|
62
|
+
try {
|
|
63
|
+
const buf = Buffer.allocUnsafe(maxBytes);
|
|
64
|
+
const bytesRead = fs.readSync(fd, buf, 0, maxBytes, 0);
|
|
65
|
+
return { text: buf.subarray(0, bytesRead).toString("utf-8"), bytesRead };
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
fs.closeSync(fd);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse a SKILL.md header from an in-memory string.
|
|
73
|
+
* Pure — no filesystem access, so it is cheap to unit test.
|
|
74
|
+
*/
|
|
75
|
+
export function parseSkillHeaderBytes(raw) {
|
|
76
|
+
const bytesRead = Buffer.byteLength(raw, "utf-8");
|
|
77
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return { ok: false, reason: "no-frontmatter", bytesRead };
|
|
80
|
+
}
|
|
81
|
+
const yamlBlock = match[1] ?? "";
|
|
82
|
+
if (Buffer.byteLength(yamlBlock, "utf-8") > MAX_FRONTMATTER_BYTES) {
|
|
83
|
+
return { ok: false, reason: "too-large", bytesRead };
|
|
84
|
+
}
|
|
85
|
+
let loaded;
|
|
86
|
+
try {
|
|
87
|
+
loaded = yaml.load(yamlBlock, { schema: yaml.JSON_SCHEMA });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return { ok: false, reason: "yaml-error", bytesRead };
|
|
91
|
+
}
|
|
92
|
+
if (typeof loaded !== "object" || loaded === null || Array.isArray(loaded)) {
|
|
93
|
+
return { ok: false, reason: "not-object", bytesRead };
|
|
94
|
+
}
|
|
95
|
+
const record = loaded;
|
|
96
|
+
for (const key of Object.keys(record)) {
|
|
97
|
+
if (UNSAFE_KEYS.has(key)) {
|
|
98
|
+
return { ok: false, reason: "unsafe-key", bytesRead };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!isSkillHeader(record)) {
|
|
102
|
+
return { ok: false, reason: "schema-invalid", bytesRead };
|
|
103
|
+
}
|
|
104
|
+
const header = {
|
|
105
|
+
name: asString(record["name"]),
|
|
106
|
+
role: asString(record["role"]),
|
|
107
|
+
description: asString(record["description"]),
|
|
108
|
+
triggers: toStringArray(record["triggers"]),
|
|
109
|
+
version: asString(record["version"]),
|
|
110
|
+
requires_tools: toStringArray(record["requires_tools"]),
|
|
111
|
+
composes_with: toStringArray(record["composes_with"]),
|
|
112
|
+
};
|
|
113
|
+
if (header.name.length === 0) {
|
|
114
|
+
return { ok: false, reason: "schema-invalid", bytesRead };
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
ok: true,
|
|
118
|
+
header,
|
|
119
|
+
body: raw.slice(match[0].length).trim(),
|
|
120
|
+
bytesRead,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Parse a SKILL.md from disk.
|
|
125
|
+
*
|
|
126
|
+
* The header is always parsed from a bounded prefix (8 KiB by default); the
|
|
127
|
+
* body is only re-read in full when `withBody` is set, so the header path can
|
|
128
|
+
* never be made to allocate an arbitrary amount of memory.
|
|
129
|
+
*/
|
|
130
|
+
export function parseSkillHeaderFile(skillPath, opts) {
|
|
131
|
+
const maxBytes = opts?.maxBytes ?? MAX_FRONTMATTER_BYTES;
|
|
132
|
+
let prefix;
|
|
133
|
+
try {
|
|
134
|
+
prefix = readPrefix(skillPath, maxBytes);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { ok: false, reason: "unreadable", bytesRead: 0 };
|
|
138
|
+
}
|
|
139
|
+
const parsed = parseSkillHeaderBytes(prefix.text);
|
|
140
|
+
if (!parsed.ok) {
|
|
141
|
+
// A file that starts with `---` but whose header delimiter never arrives
|
|
142
|
+
// inside the window is a too-large header, not a missing one.
|
|
143
|
+
const truncated = prefix.bytesRead >= maxBytes;
|
|
144
|
+
const looksLikeFrontmatter = /^(?:\uFEFF)?---\r?\n/.test(prefix.text);
|
|
145
|
+
if (truncated && looksLikeFrontmatter) {
|
|
146
|
+
return { ok: false, reason: "too-large", bytesRead: prefix.bytesRead };
|
|
147
|
+
}
|
|
148
|
+
return { ...parsed, bytesRead: prefix.bytesRead };
|
|
149
|
+
}
|
|
150
|
+
if (!opts?.withBody) {
|
|
151
|
+
return { ok: true, header: parsed.header, bytesRead: prefix.bytesRead };
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const fullParsed = parseSkillHeaderBytes(fs.readFileSync(skillPath, "utf-8"));
|
|
155
|
+
if (!fullParsed.ok) {
|
|
156
|
+
return { ...fullParsed, bytesRead: prefix.bytesRead };
|
|
157
|
+
}
|
|
158
|
+
return { ...fullParsed, bytesRead: prefix.bytesRead };
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return { ok: false, reason: "unreadable", bytesRead: prefix.bytesRead };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill path guard — validates a skill directory NAME in code, before any
|
|
3
|
+
* path join happens, and then proves containment with realpath.
|
|
4
|
+
*
|
|
5
|
+
* A skills directory is a trust boundary: whatever sits inside it is fed to
|
|
6
|
+
* the model. A directory called `../../.ssh` or `..\..\Windows` must never be
|
|
7
|
+
* joined onto the skills root, so policy lives here (PRM-09: enforce in code,
|
|
8
|
+
* never in a prompt) and failures are closed, never guessed.
|
|
9
|
+
*/
|
|
10
|
+
/** Longest accepted skill directory name. Shipped packs are <= 24 chars. */
|
|
11
|
+
export declare const MAX_SKILL_NAME_LENGTH = 64;
|
|
12
|
+
/** Hard ceiling on how many pack directories a single probe will visit. */
|
|
13
|
+
export declare const MAX_PACK_DIRS = 512;
|
|
14
|
+
/**
|
|
15
|
+
* Hard ceiling on YAML frontmatter size. A skill whose header exceeds this is
|
|
16
|
+
* rejected outright — a soft cap would re-introduce the unbounded read this
|
|
17
|
+
* limit exists to prevent.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_FRONTMATTER_BYTES = 8192;
|
|
20
|
+
/** Why a candidate skill directory name was refused. */
|
|
21
|
+
export type SkillNameRejectReason = "empty" | "too-long" | "separator" | "traversal" | "absolute" | "dot-prefixed" | "trailing-dot-or-space" | "reserved-device" | "illegal-char";
|
|
22
|
+
export interface SkillNameCheck {
|
|
23
|
+
ok: boolean;
|
|
24
|
+
reason?: SkillNameRejectReason;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Decide whether `name` is safe to use as a single path segment.
|
|
28
|
+
*
|
|
29
|
+
* This is pure string inspection — it never touches the filesystem, so it is
|
|
30
|
+
* cheap enough to call for every pack on every scan.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateSkillName(name: string): SkillNameCheck;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve `parentDir/dirName` and prove the result is a *direct child* of
|
|
35
|
+
* `parentDir` both lexically and after symlink resolution.
|
|
36
|
+
*
|
|
37
|
+
* Returns `undefined` when the name is unsafe, when the entry does not exist,
|
|
38
|
+
* or when the real path escapes the parent (a symlinked pack). Callers skip
|
|
39
|
+
* anything that comes back `undefined` — fail closed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveSkillDir(parentDir: string, dirName: string): string | undefined;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// xcoder:version 1 | xcoder:task task-f9cf18 | xcoder:updated 2026-11-02T16:13:48.255Z | xcoder:subtask code-st-077ea2
|
|
2
|
+
/**
|
|
3
|
+
* Skill path guard — validates a skill directory NAME in code, before any
|
|
4
|
+
* path join happens, and then proves containment with realpath.
|
|
5
|
+
*
|
|
6
|
+
* A skills directory is a trust boundary: whatever sits inside it is fed to
|
|
7
|
+
* the model. A directory called `../../.ssh` or `..\..\Windows` must never be
|
|
8
|
+
* joined onto the skills root, so policy lives here (PRM-09: enforce in code,
|
|
9
|
+
* never in a prompt) and failures are closed, never guessed.
|
|
10
|
+
*/
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
/** Longest accepted skill directory name. Shipped packs are <= 24 chars. */
|
|
14
|
+
export const MAX_SKILL_NAME_LENGTH = 64;
|
|
15
|
+
/** Hard ceiling on how many pack directories a single probe will visit. */
|
|
16
|
+
export const MAX_PACK_DIRS = 512;
|
|
17
|
+
/**
|
|
18
|
+
* Hard ceiling on YAML frontmatter size. A skill whose header exceeds this is
|
|
19
|
+
* rejected outright — a soft cap would re-introduce the unbounded read this
|
|
20
|
+
* limit exists to prevent.
|
|
21
|
+
*/
|
|
22
|
+
export const MAX_FRONTMATTER_BYTES = 8192;
|
|
23
|
+
/** Windows device names are resolved by the OS even with an extension. */
|
|
24
|
+
const RESERVED_DEVICE_NAMES = new Set([
|
|
25
|
+
"con",
|
|
26
|
+
"prn",
|
|
27
|
+
"aux",
|
|
28
|
+
"nul",
|
|
29
|
+
...Array.from({ length: 9 }, (_, i) => `com${i + 1}`),
|
|
30
|
+
...Array.from({ length: 9 }, (_, i) => `lpt${i + 1}`),
|
|
31
|
+
]);
|
|
32
|
+
/** Only these characters may appear in a skill directory name. */
|
|
33
|
+
const SAFE_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
34
|
+
/**
|
|
35
|
+
* Decide whether `name` is safe to use as a single path segment.
|
|
36
|
+
*
|
|
37
|
+
* This is pure string inspection — it never touches the filesystem, so it is
|
|
38
|
+
* cheap enough to call for every pack on every scan.
|
|
39
|
+
*/
|
|
40
|
+
export function validateSkillName(name) {
|
|
41
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
42
|
+
return { ok: false, reason: "empty" };
|
|
43
|
+
}
|
|
44
|
+
if (name.length > MAX_SKILL_NAME_LENGTH) {
|
|
45
|
+
return { ok: false, reason: "too-long" };
|
|
46
|
+
}
|
|
47
|
+
if (path.isAbsolute(name) || /^[A-Za-z]:/.test(name)) {
|
|
48
|
+
return { ok: false, reason: "absolute" };
|
|
49
|
+
}
|
|
50
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
51
|
+
return { ok: false, reason: "separator" };
|
|
52
|
+
}
|
|
53
|
+
if (name === "." || name === ".." || name.includes("..")) {
|
|
54
|
+
return { ok: false, reason: "traversal" };
|
|
55
|
+
}
|
|
56
|
+
if (name.startsWith(".")) {
|
|
57
|
+
return { ok: false, reason: "dot-prefixed" };
|
|
58
|
+
}
|
|
59
|
+
if (name.endsWith(".") || name.endsWith(" ")) {
|
|
60
|
+
return { ok: false, reason: "trailing-dot-or-space" };
|
|
61
|
+
}
|
|
62
|
+
if (RESERVED_DEVICE_NAMES.has(name.split(".")[0].toLowerCase())) {
|
|
63
|
+
return { ok: false, reason: "reserved-device" };
|
|
64
|
+
}
|
|
65
|
+
if (!SAFE_NAME_RE.test(name)) {
|
|
66
|
+
return { ok: false, reason: "illegal-char" };
|
|
67
|
+
}
|
|
68
|
+
return { ok: true };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve `parentDir/dirName` and prove the result is a *direct child* of
|
|
72
|
+
* `parentDir` both lexically and after symlink resolution.
|
|
73
|
+
*
|
|
74
|
+
* Returns `undefined` when the name is unsafe, when the entry does not exist,
|
|
75
|
+
* or when the real path escapes the parent (a symlinked pack). Callers skip
|
|
76
|
+
* anything that comes back `undefined` — fail closed.
|
|
77
|
+
*/
|
|
78
|
+
export function resolveSkillDir(parentDir, dirName) {
|
|
79
|
+
if (!validateSkillName(dirName).ok)
|
|
80
|
+
return undefined;
|
|
81
|
+
const parent = path.resolve(parentDir);
|
|
82
|
+
const candidate = path.resolve(parent, dirName);
|
|
83
|
+
if (candidate === parent || path.dirname(candidate) !== parent)
|
|
84
|
+
return undefined;
|
|
85
|
+
try {
|
|
86
|
+
const realParent = fs.realpathSync(parent);
|
|
87
|
+
const realCandidate = fs.realpathSync(candidate);
|
|
88
|
+
if (path.dirname(realCandidate) !== realParent)
|
|
89
|
+
return undefined;
|
|
90
|
+
return realCandidate;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|