talos-code 0.0.1 → 0.2.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/CHANGELOG.md +87 -0
- package/LICENSE +661 -0
- package/README.md +116 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +100 -0
- package/dist/automations/history-store.js +34 -0
- package/dist/automations/policy-store.js +43 -0
- package/dist/automations/runner.js +107 -0
- package/dist/automations/schedule.js +85 -0
- package/dist/automations/windows-task.js +53 -0
- package/dist/commands/advanced-cli.js +233 -0
- package/dist/commands/checkpoint-cli.js +45 -0
- package/dist/commands/config-cli.js +107 -0
- package/dist/commands/context.js +17 -0
- package/dist/commands/extensions-cli.js +207 -0
- package/dist/commands/project-cli.js +74 -0
- package/dist/commands/project-commands.js +143 -0
- package/dist/commands/provider-cli.js +244 -0
- package/dist/commands/services-cli.js +248 -0
- package/dist/commands/session-cli.js +171 -0
- package/dist/commands/system-cli.js +405 -0
- package/dist/config/commands.js +60 -0
- package/dist/config/load.js +382 -0
- package/dist/config/migrations.js +48 -0
- package/dist/config/types.js +11 -0
- package/dist/diagnostics/development-log.js +154 -0
- package/dist/diagnostics/doctor.js +104 -0
- package/dist/diagnostics/redact.js +80 -0
- package/dist/diagnostics/zip.js +52 -0
- package/dist/errors.js +156 -0
- package/dist/events/bridge.js +130 -0
- package/dist/extensions/installer.js +178 -0
- package/dist/extensions/package-schema.js +24 -0
- package/dist/headless/result.js +84 -0
- package/dist/headless/run.js +230 -0
- package/dist/i18n/en/approval.js +46 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +92 -0
- package/dist/i18n/en/errors.js +267 -0
- package/dist/i18n/en/firstrun.js +101 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +774 -0
- package/dist/i18n/en/tools.js +127 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/error-view.js +307 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/kernel-map.js +139 -0
- package/dist/io.js +45 -0
- package/dist/main.js +331 -0
- package/dist/paths.js +41 -0
- package/dist/protocol/v2/codec.js +9 -0
- package/dist/protocol/v2/events.js +555 -0
- package/dist/protocol/v2/index.js +4 -0
- package/dist/protocol/v2/replay.js +28 -0
- package/dist/protocol/v2/types.js +1 -0
- package/dist/provider/control-plane.js +135 -0
- package/dist/provider/environment-keys.js +275 -0
- package/dist/provider/health.js +110 -0
- package/dist/provider/missing-key.js +48 -0
- package/dist/provider/model-catalog.js +168 -0
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/provider/provider-text.js +13 -0
- package/dist/provider/store.js +95 -0
- package/dist/provider/system-keyring.js +214 -0
- package/dist/runtime/active-run.js +63 -0
- package/dist/runtime/agent-tree.js +74 -0
- package/dist/runtime/attachments.js +84 -0
- package/dist/runtime/brokered-executor.js +395 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/context-status.js +76 -0
- package/dist/runtime/create-runtime.js +111 -0
- package/dist/runtime/model-profile.js +361 -0
- package/dist/runtime/output-store.js +137 -0
- package/dist/runtime/provider-attempts.js +185 -0
- package/dist/runtime/replay-buffer.js +153 -0
- package/dist/runtime/repo.js +95 -0
- package/dist/runtime/session-facade.js +135 -0
- package/dist/runtime/session-summary.js +125 -0
- package/dist/runtime/supervisor.js +265 -0
- package/dist/runtime/talos-composition.js +1011 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/usage-snapshot.js +82 -0
- package/dist/security/auto-classifier.js +38 -0
- package/dist/security/credential-free-environment.js +54 -0
- package/dist/security/evaluate.js +243 -0
- package/dist/security/execution-backends.js +236 -0
- package/dist/security/execution-broker.js +102 -0
- package/dist/security/forge-scan.js +74 -0
- package/dist/security/from-approval.js +39 -0
- package/dist/security/mxc-execution-backend.js +281 -0
- package/dist/security/permission-engine.js +138 -0
- package/dist/security/permission-explanation.js +54 -0
- package/dist/security/persist.js +179 -0
- package/dist/security/plugin-guard.js +230 -0
- package/dist/security/process-tree-evidence-store.js +74 -0
- package/dist/security/process-tree-probe.js +554 -0
- package/dist/security/project-resource-inventory.js +186 -0
- package/dist/security/project-trust-gate.js +38 -0
- package/dist/security/project-trust.js +367 -0
- package/dist/security/rule-parser.js +201 -0
- package/dist/security/shell-segmentation.js +68 -0
- package/dist/security/trust-authority.js +324 -0
- package/dist/security/types.js +1 -0
- package/dist/security/workspace-identity.js +102 -0
- package/dist/services/automation-facade.js +59 -0
- package/dist/services/forge-facade.js +77 -0
- package/dist/services/hook-facade.js +240 -0
- package/dist/services/index.js +16 -0
- package/dist/services/library-facade.js +174 -0
- package/dist/services/mcp-facade.js +348 -0
- package/dist/services/memory-facade.js +126 -0
- package/dist/services/notes-facade.js +157 -0
- package/dist/services/plugin-facade.js +308 -0
- package/dist/services/research-facade.js +110 -0
- package/dist/services/task-board-facade.js +236 -0
- package/dist/services/workflow-catalog.js +246 -0
- package/dist/sessions/export-format.js +98 -0
- package/dist/sessions/metadata-store.js +160 -0
- package/dist/sessions/share.js +122 -0
- package/dist/sessions/transfer.js +16 -0
- package/dist/subcommands.js +62 -0
- package/dist/tui/agent-roster.js +36 -0
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +4098 -0
- package/dist/tui/approval.js +36 -0
- package/dist/tui/boot/boot-sequence.js +76 -0
- package/dist/tui/boot/cinematic.js +243 -0
- package/dist/tui/boot/desktop-logo.js +82 -0
- package/dist/tui/boot.js +3 -0
- package/dist/tui/busy-input.js +58 -0
- package/dist/tui/catalog-service.js +560 -0
- package/dist/tui/components/assistant-stream.js +1 -0
- package/dist/tui/components/command-menu.js +68 -0
- package/dist/tui/components/composer.js +219 -0
- package/dist/tui/components/diff.js +35 -0
- package/dist/tui/components/footer.js +48 -0
- package/dist/tui/components/header.js +6 -0
- package/dist/tui/components/markdown.js +305 -0
- package/dist/tui/components/status-indicator.js +38 -0
- package/dist/tui/components/terminal-shell.js +214 -0
- package/dist/tui/components/thinking-row.js +19 -0
- package/dist/tui/components/tool-row.js +97 -0
- package/dist/tui/components/transcript-virtualizer.js +165 -0
- package/dist/tui/components/transcript.js +43 -0
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/diff-model.js +77 -0
- package/dist/tui/editor-history.js +21 -0
- package/dist/tui/editor.js +210 -0
- package/dist/tui/event-adapter.js +436 -0
- package/dist/tui/exit-output.js +58 -0
- package/dist/tui/external-editor.js +53 -0
- package/dist/tui/file-completion.js +89 -0
- package/dist/tui/focus-manager.js +5 -0
- package/dist/tui/highlight.js +30 -0
- package/dist/tui/input-router.js +18 -0
- package/dist/tui/interrupt.js +99 -0
- package/dist/tui/keybindings.js +194 -0
- package/dist/tui/keymap-resolver.js +91 -0
- package/dist/tui/launch-state.js +148 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +56 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +43 -0
- package/dist/tui/overlays/approval-dialog.js +642 -0
- package/dist/tui/overlays/automation-center.js +51 -0
- package/dist/tui/overlays/checkpoint-picker.js +47 -0
- package/dist/tui/overlays/context-inspector.js +36 -0
- package/dist/tui/overlays/effort-line.js +110 -0
- package/dist/tui/overlays/forge-center.js +46 -0
- package/dist/tui/overlays/help-dialog.js +16 -0
- package/dist/tui/overlays/history-picker.js +37 -0
- package/dist/tui/overlays/hook-center.js +70 -0
- package/dist/tui/overlays/library-center.js +55 -0
- package/dist/tui/overlays/mcp-center.js +60 -0
- package/dist/tui/overlays/memory-center.js +63 -0
- package/dist/tui/overlays/model-picker.js +77 -0
- package/dist/tui/overlays/notes-center.js +53 -0
- package/dist/tui/overlays/overlay-host.js +8 -0
- package/dist/tui/overlays/plugin-center.js +76 -0
- package/dist/tui/overlays/provider-picker.js +171 -0
- package/dist/tui/overlays/queue-editor.js +32 -0
- package/dist/tui/overlays/research-center.js +59 -0
- package/dist/tui/overlays/scroll-window.js +234 -0
- package/dist/tui/overlays/session-picker.js +115 -0
- package/dist/tui/overlays/tasks-center.js +83 -0
- package/dist/tui/overlays/theme-picker.js +21 -0
- package/dist/tui/overlays/transcript-search.js +58 -0
- package/dist/tui/overlays/trust-center.js +29 -0
- package/dist/tui/overlays/workflow-center.js +46 -0
- package/dist/tui/project-file-index.js +214 -0
- package/dist/tui/project-references.js +85 -0
- package/dist/tui/project-trust-prompt.js +289 -0
- package/dist/tui/prompt-history-store.js +116 -0
- package/dist/tui/queue-store.js +110 -0
- package/dist/tui/regions/budget.js +59 -0
- package/dist/tui/regions/views.js +96 -0
- package/dist/tui/render-coordinator.js +148 -0
- package/dist/tui/render-scheduler.js +4 -0
- package/dist/tui/run.js +69 -0
- package/dist/tui/selection-list.js +29 -0
- package/dist/tui/session-controller.js +791 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/shell-input.js +35 -0
- package/dist/tui/shell-layout.js +48 -0
- package/dist/tui/shell-model.js +98 -0
- package/dist/tui/slash-commands.js +82 -0
- package/dist/tui/state.js +151 -0
- package/dist/tui/status-bar.js +125 -0
- package/dist/tui/status-view.js +40 -0
- package/dist/tui/terminal-capabilities.js +9 -0
- package/dist/tui/terminal-session.js +11 -0
- package/dist/tui/text-width.js +66 -0
- package/dist/tui/theme-catalog.js +27 -0
- package/dist/tui/theme-store.js +23 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/tool-display.js +135 -0
- package/dist/tui/tool-facts.js +1 -0
- package/dist/tui/tools/bash-renderer.js +32 -0
- package/dist/tui/tools/change.js +61 -0
- package/dist/tui/tools/edit-renderer.js +17 -0
- package/dist/tui/tools/generic-renderer.js +13 -0
- package/dist/tui/tools/list-renderer.js +14 -0
- package/dist/tui/tools/read-renderer.js +13 -0
- package/dist/tui/tools/registry.js +20 -0
- package/dist/tui/tools/row-format.js +187 -0
- package/dist/tui/tools/search-renderer.js +38 -0
- package/dist/tui/tools/shared.js +97 -0
- package/dist/tui/tools/write-renderer.js +15 -0
- package/dist/tui/transcript-model.js +441 -0
- package/dist/tui/ui-preferences.js +79 -0
- package/dist/tui/usage-view.js +77 -0
- package/dist/tui/vim-mode.js +99 -0
- package/dist/update/check.js +15 -0
- package/dist/update/npm.js +51 -0
- package/dist/update/run.js +129 -0
- package/dist/version.js +2 -0
- package/dist/workspace/checkpoint-store.js +210 -0
- package/dist/workspace/checkpoint.js +413 -0
- package/dist/workspace/restore.js +232 -0
- package/package.json +64 -5
- package/vendor/context-engine/package.json +11 -0
- package/vendor/context-engine/src/compaction-planner.mjs +57 -0
- package/vendor/context-engine/src/contracts.mjs +48 -0
- package/vendor/context-engine/src/engine.mjs +445 -0
- package/vendor/context-engine/src/node/context-export.mjs +164 -0
- package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
- package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
- package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
- package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
- package/vendor/context-engine/src/profiles.mjs +10 -0
- package/vendor/context-engine/src/retrieval.mjs +104 -0
- package/vendor/context-engine/src/summary.mjs +97 -0
- package/vendor/context-engine/src/usage.mjs +34 -0
- package/vendor/harness-ui/package.json +38 -0
- package/vendor/harness-ui/src/acp-agent.mjs +350 -0
- package/vendor/harness-ui/src/agent-service.mjs +2188 -0
- package/vendor/harness-ui/src/agui-events.mjs +452 -0
- package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
- package/vendor/harness-ui/src/artifact-store.mjs +39 -0
- package/vendor/harness-ui/src/assistenza.mjs +123 -0
- package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
- package/vendor/harness-ui/src/automation-store.mjs +145 -0
- package/vendor/harness-ui/src/browser-annota.mjs +639 -0
- package/vendor/harness-ui/src/browser-frame.mjs +211 -0
- package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
- package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
- package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
- package/vendor/harness-ui/src/browser-stream.mjs +445 -0
- package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
- package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
- package/vendor/harness-ui/src/config.mjs +697 -0
- package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
- package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
- package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
- package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
- package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
- package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
- package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
- package/vendor/harness-ui/src/context-runtime.mjs +118 -0
- package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
- package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
- package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
- package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
- package/vendor/harness-ui/src/custom-task.mjs +171 -0
- package/vendor/harness-ui/src/doctor.mjs +142 -0
- package/vendor/harness-ui/src/document-filename.mjs +97 -0
- package/vendor/harness-ui/src/document-generator.mjs +493 -0
- package/vendor/harness-ui/src/document-report.mjs +331 -0
- package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
- package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
- package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
- package/vendor/harness-ui/src/forge-contract.mjs +221 -0
- package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
- package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
- package/vendor/harness-ui/src/generation-idle.mjs +332 -0
- package/vendor/harness-ui/src/gguf-header.mjs +207 -0
- package/vendor/harness-ui/src/git-service.mjs +626 -0
- package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
- package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
- package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
- package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
- package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
- package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
- package/vendor/harness-ui/src/hook-registry.mjs +186 -0
- package/vendor/harness-ui/src/http-app.mjs +6259 -0
- package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
- package/vendor/harness-ui/src/id-archivio.mjs +27 -0
- package/vendor/harness-ui/src/image-generator.mjs +143 -0
- package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
- package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
- package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
- package/vendor/harness-ui/src/library-store.mjs +652 -0
- package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
- package/vendor/harness-ui/src/local-model-store.mjs +339 -0
- package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
- package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
- package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
- package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
- package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
- package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
- package/vendor/harness-ui/src/mcp-client.mjs +98 -0
- package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
- package/vendor/harness-ui/src/mcp-session.mjs +177 -0
- package/vendor/harness-ui/src/memory-store.mjs +227 -0
- package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
- package/vendor/harness-ui/src/model-catalog.mjs +129 -0
- package/vendor/harness-ui/src/model-destination.mjs +189 -0
- package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
- package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
- package/vendor/harness-ui/src/notes-store.mjs +250 -0
- package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
- package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
- package/vendor/harness-ui/src/path-policy.mjs +442 -0
- package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
- package/vendor/harness-ui/src/plugin-session.mjs +180 -0
- package/vendor/harness-ui/src/process-policy.mjs +345 -0
- package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
- package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
- package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
- package/vendor/harness-ui/src/provider-probe.mjs +582 -0
- package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
- package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
- package/vendor/harness-ui/src/public-problem.mjs +109 -0
- package/vendor/harness-ui/src/research/card.mjs +235 -0
- package/vendor/harness-ui/src/research/citations.mjs +142 -0
- package/vendor/harness-ui/src/research/collector.mjs +275 -0
- package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
- package/vendor/harness-ui/src/research/dossier.mjs +114 -0
- package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
- package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
- package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
- package/vendor/harness-ui/src/research/independence.mjs +159 -0
- package/vendor/harness-ui/src/research/ledger.mjs +166 -0
- package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
- package/vendor/harness-ui/src/research/narration.mjs +181 -0
- package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
- package/vendor/harness-ui/src/research/opposing.mjs +305 -0
- package/vendor/harness-ui/src/research/outline.mjs +111 -0
- package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
- package/vendor/harness-ui/src/research/pdf.mjs +291 -0
- package/vendor/harness-ui/src/research/plan.mjs +301 -0
- package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
- package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
- package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
- package/vendor/harness-ui/src/research/recheck.mjs +194 -0
- package/vendor/harness-ui/src/research/report.mjs +203 -0
- package/vendor/harness-ui/src/research/run.mjs +527 -0
- package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
- package/vendor/harness-ui/src/research/verification.mjs +572 -0
- package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
- package/vendor/harness-ui/src/research-store.mjs +1133 -0
- package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
- package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
- package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
- package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
- package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
- package/vendor/harness-ui/src/search-source-store.mjs +172 -0
- package/vendor/harness-ui/src/session-registry.mjs +6095 -0
- package/vendor/harness-ui/src/session-store.mjs +220 -0
- package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
- package/vendor/harness-ui/src/setup-stato.mjs +31 -0
- package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
- package/vendor/harness-ui/src/skill-registry.mjs +120 -0
- package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
- package/vendor/harness-ui/src/static-files.mjs +96 -0
- package/vendor/harness-ui/src/stream-partition.mjs +123 -0
- package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
- package/vendor/harness-ui/src/task-catalog.mjs +65 -0
- package/vendor/harness-ui/src/tasks-store.mjs +220 -0
- package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
- package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
- package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
- package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
- package/vendor/harness-ui/src/usage-cache.mjs +315 -0
- package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
- package/vendor/harness-ui/src/workspace-context.mjs +124 -0
- package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
- package/vendor/harness-ui/src/workspace-files.mjs +589 -0
- package/vendor/harness-ui/src/workspace-info.mjs +189 -0
- package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
- package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
- package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
- package/vendor/manifest.json +170 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-store.mjs — FASE L (30/8), piano `elegant-spinning-dongarra.md`.
|
|
3
|
+
* Owner: "ricerca web competitor" sulla domanda "un riavvio del
|
|
4
|
+
* processo perde una sessione in corso, è mai successo per davvero?".
|
|
5
|
+
*
|
|
6
|
+
* ⛔⛔⛔ La ricerca ha RIDEFINITO la fase, non solo risposto alla domanda.
|
|
7
|
+
* Verificato alla fonte (non presunto): `session-registry.mjs` dichiara
|
|
8
|
+
* fin dalla sua prima riga "solo in memoria, deliberato... non ancora
|
|
9
|
+
* aperto" — un riavvio del server perde OGNI sessione, non solo quelle
|
|
10
|
+
* in corso, e non è mai stato diversamente (`.sessions/` su disco che
|
|
11
|
+
* un piano precedente citava non esiste in questo file — verificato
|
|
12
|
+
* con una ricerca diretta, zero riscontri).
|
|
13
|
+
*
|
|
14
|
+
* Cosa fa davvero lo stato dell'arte in questo spazio: transcript JSONL
|
|
15
|
+
* append-only come fonte di verità per il replay
|
|
16
|
+
* ("rollout files"), un indice separato (SQLite, solo per liste
|
|
17
|
+
* veloci — non necessario alla scala di questo prodotto). ⭐⭐⭐ Onestamente
|
|
18
|
+
* ammesso altrove: "if a transport failure occurs early enough...
|
|
19
|
+
* no resumable artefacts may be written" — un ripristino a metà turno
|
|
20
|
+
* non è mai garantito, nemmeno lì. Il ripristino VERO che si offre in
|
|
21
|
+
* questi casi è
|
|
22
|
+
* "rilettura della trascrizione", non "il modello riprende da dove
|
|
23
|
+
* stava" — lo stesso confine onesto che questo modulo dichiara.
|
|
24
|
+
*
|
|
25
|
+
* ⛔ Trovato durante la ricerca, non ipotetico: riscrivere l'intero file
|
|
26
|
+
* invece di solo accodare è una classe di bug nota (perdita di dati su
|
|
27
|
+
* crash per una riscrittura non atomica della trascrizione) — la
|
|
28
|
+
* classe di errore che questo modulo evita per costruzione: MAI un
|
|
29
|
+
* `writeFile` che sostituisce il file intero, solo `appendFile`. Una
|
|
30
|
+
* riga JSONL è atomica sui filesystem POSIX: un crash a metà riga
|
|
31
|
+
* lascia al più UNA riga corrotta, mai le precedenti.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ Nessun fsync esplicito dopo ogni riga (differenza dichiarata dal
|
|
34
|
+
* pattern "production-hardened" trovato in ricerca): a differenza di
|
|
35
|
+
* un ledger finanziario, questo è uno strumento locale owner-only — la
|
|
36
|
+
* finestra di perdita è quella del buffer del sistema operativo (tipicamente
|
|
37
|
+
* pochi millisecondi), non l'intera sessione. Scelta esplicita, non
|
|
38
|
+
* un taglio silenzioso.
|
|
39
|
+
*/
|
|
40
|
+
import { promises as fsp, mkdirSync, appendFileSync } from 'node:fs';
|
|
41
|
+
import { join } from 'node:path';
|
|
42
|
+
|
|
43
|
+
export class SessionStoreError extends Error {
|
|
44
|
+
constructor(message, code = 'SESSION_STORE_FAILED') {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = 'SessionStoreError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ESTENSIONE = '.jsonl';
|
|
52
|
+
|
|
53
|
+
function percorsoDi(cartellaStore, sessionId) {
|
|
54
|
+
// ⛔ sessionId è sempre un randomUUID() generato da questo stesso processo (mai testo esterno) — nessuna sanificazione di percorso richiesta, a differenza di un nome file scelto dal modello (vedi library-store.mjs).
|
|
55
|
+
return join(cartellaStore, `${sessionId}${ESTENSIONE}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Accoda UNA riga — mai una riscrittura del file intero (la classe di
|
|
60
|
+
* bug vista in ricerca, vedi la testa del file). `record` è già serializzabile (un evento AG-UI,
|
|
61
|
+
* o l'intestazione, o il record `messaggiFinali`) — questo modulo non
|
|
62
|
+
* sa cosa contiene, solo che va in coda.
|
|
63
|
+
*/
|
|
64
|
+
/*
|
|
65
|
+
* ⭐⭐⭐ 04/9 — W0-07: UNA CODA PER FILE. `fsp.appendFile` non è atomica per
|
|
66
|
+
* un record più grande di una singola scrittura di sistema: il contenuto
|
|
67
|
+
* viene spezzato, e un secondo append concorrente sullo stesso file si
|
|
68
|
+
* infila in mezzo. Il risultato è una riga JSONL illeggibile — cioè una
|
|
69
|
+
* sessione che `ripristina()` scarta per sempre, in silenzio.
|
|
70
|
+
*
|
|
71
|
+
* ⛔ Non è teoria: nello store dell'owner il file `b7b1b7d2…` (31/08) aveva
|
|
72
|
+
* 4 righe rotte, la prima spezzata a **1.572.866 byte, esattamente 1,5
|
|
73
|
+
* MiB**, e le successive iniziavano a metà percorso pur finendo con
|
|
74
|
+
* `_sequenza` coerenti. Erano `WorkspaceChanged` da 1-1,5 MB l'uno (il
|
|
75
|
+
* workspace era `C:\` intero). Quella conversazione — vera, conclusa con
|
|
76
|
+
* successo — è stata invisibile per quattro giorni; l'ho recuperata a mano
|
|
77
|
+
* il 04/09 buttando solo le righe illeggibili.
|
|
78
|
+
*
|
|
79
|
+
* ⇒ Le scritture dello STESSO file si mettono in fila. File diversi non si
|
|
80
|
+
* aspettano fra loro (la chiave della coda è il percorso), quindi una
|
|
81
|
+
* sessione lenta non rallenta le altre. Un errore su una scrittura non
|
|
82
|
+
* blocca la coda: la successiva parte comunque.
|
|
83
|
+
*
|
|
84
|
+
* ⭐ Ricerca web del 04/09 (obbligo dell'owner, fatta PRIMA di considerare
|
|
85
|
+
* chiusa questa cura): `O_APPEND` è atomico **per singola chiamata di
|
|
86
|
+
* scrittura** — due scrittori non si sovrappongono finché ogni record entra
|
|
87
|
+
* in UNA chiamata; un payload grande viene però spezzato in più chiamate, ed
|
|
88
|
+
* è lì che si intrecciano. La forma raccomandata è esattamente questa: «le
|
|
89
|
+
* scritture concorrenti sullo stesso file si mettono in coda e si
|
|
90
|
+
* serializzano con le Promise, mentre file diversi restano in parallelo».
|
|
91
|
+
* Due cose che la ricerca aggiunge e che qui NON sono risolte, registrate
|
|
92
|
+
* come debito nel ledger: (a) per un log si consiglia uno stream persistente
|
|
93
|
+
* invece di aprire e chiudere il file a ogni evento; (b) una scrittura
|
|
94
|
+
* riuscita vive nella cache del kernel finché non c'è un `fsync`, quindi un
|
|
95
|
+
* crash può perdere gli ultimi eventi — l'invariante da tenere è non
|
|
96
|
+
* trattare mai come record dei byte di coda non validati, e `ripristina()`
|
|
97
|
+
* già distingue l'ultima riga spezzata (crash a metà append) da una riga
|
|
98
|
+
* rotta in mezzo (danno vero).
|
|
99
|
+
*/
|
|
100
|
+
const codeDiScrittura = new Map();
|
|
101
|
+
|
|
102
|
+
export async function registraRiga({ cartellaStore, sessionId, record, durable = false }, deps = {}) {
|
|
103
|
+
const mkdirFn = deps.mkdirFn ?? fsp.mkdir;
|
|
104
|
+
const appendFileFn = deps.appendFileFn ?? fsp.appendFile;
|
|
105
|
+
const percorso = percorsoDi(cartellaStore, sessionId);
|
|
106
|
+
// ⛔ Serializzato SUBITO, non dentro la coda: `record` potrebbe cambiare mentre questa scrittura aspetta il suo turno.
|
|
107
|
+
const riga = `${JSON.stringify(record)}\n`;
|
|
108
|
+
const precedente = codeDiScrittura.get(percorso) ?? Promise.resolve();
|
|
109
|
+
const corrente = precedente.catch(() => {}).then(async () => {
|
|
110
|
+
await mkdirFn(cartellaStore, { recursive: true });
|
|
111
|
+
await appendFileFn(percorso, riga, durable ? { encoding: 'utf8', flush: true } : 'utf8');
|
|
112
|
+
});
|
|
113
|
+
codeDiScrittura.set(percorso, corrente);
|
|
114
|
+
// La mappa non deve crescere per sempre: chi è l'ultimo della fila la ripulisce.
|
|
115
|
+
corrente.catch(() => {}).finally(() => {
|
|
116
|
+
if (codeDiScrittura.get(percorso) === corrente) codeDiScrittura.delete(percorso);
|
|
117
|
+
});
|
|
118
|
+
return corrente;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* ⭐⭐⭐ FASE L, trovato dalla verifica DAL VIVO (30/8) — non ipotizzato: un
|
|
123
|
+
* server VERO, una sessione VERA, uccisa (`child.kill()`, TerminateProcess
|
|
124
|
+
* su Windows — un crash vero, nessun handler di spegnimento gira) **6 ms**
|
|
125
|
+
* dopo aver ricevuto il sessionId dal client. `ripristina()` sul riavvio:
|
|
126
|
+
* "114/115" — la sessione appena creata non c'era proprio, perché la SUA
|
|
127
|
+
* riga di intestazione (`registraRiga`, fire-and-forget) non aveva ancora
|
|
128
|
+
* toccato il disco quando il processo è morto. Non un dato corrotto: la
|
|
129
|
+
* sessione non è mai esistita per `ripristina()`, un buco onesto ma
|
|
130
|
+
* evitabile — un crash nella manciata di millisecondi dopo la creazione
|
|
131
|
+
* perde l'intera sessione, in silenzio.
|
|
132
|
+
*
|
|
133
|
+
* ⛔ La cura NON è rendere `avvia()`/`avviaLibero()` asincrone (~110
|
|
134
|
+
* call-site di test, l'intero contratto sincrono di `session-registry.mjs`
|
|
135
|
+
* — sproporzionato per una finestra di pochi millisecondi, stessa strada
|
|
136
|
+
* già scartata per lo stesso motivo in FASE E). La cura è che la SOLA
|
|
137
|
+
* intestazione — una scrittura piccola, una volta per sessione, mai per
|
|
138
|
+
* ogni evento — usi l'API fs SINCRONA: `avvia()` può restare sincrona e
|
|
139
|
+
* il client non riceve MAI un sessionId la cui intestazione non sia già
|
|
140
|
+
* durevole sul disco. Gli eventi/messaggiFinali successivi restano
|
|
141
|
+
* fire-and-forget (`registraRiga` sopra, invariata) — quella finestra
|
|
142
|
+
* resta (dichiarata nel commento di testa del modulo, "pochi
|
|
143
|
+
* millisecondi... scelta esplicita"), ma non può più cancellare
|
|
144
|
+
* l'ESISTENZA stessa della sessione.
|
|
145
|
+
*/
|
|
146
|
+
export function registraRigaSync({ cartellaStore, sessionId, record }, deps = {}) {
|
|
147
|
+
const mkdirSyncFn = deps.mkdirSyncFn ?? mkdirSync;
|
|
148
|
+
const appendFileSyncFn = deps.appendFileSyncFn ?? appendFileSync;
|
|
149
|
+
mkdirSyncFn(cartellaStore, { recursive: true });
|
|
150
|
+
appendFileSyncFn(percorsoDi(cartellaStore, sessionId), `${JSON.stringify(record)}\n`, 'utf8');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Elenca gli id delle sessioni persistite — per la ricostruzione
|
|
155
|
+
* all'avvio del server. Cartella assente ⇒ `[]`, mai un errore (un
|
|
156
|
+
* primo avvio non ha ancora nessuna sessione salvata).
|
|
157
|
+
*/
|
|
158
|
+
export async function elencaSessioniPersistite({ cartellaStore }, deps = {}) {
|
|
159
|
+
const readdirFn = deps.readdirFn ?? fsp.readdir;
|
|
160
|
+
let voci;
|
|
161
|
+
try {
|
|
162
|
+
voci = await readdirFn(cartellaStore, { withFileTypes: true });
|
|
163
|
+
} catch (errore) {
|
|
164
|
+
if (errore?.code === 'ENOENT') return [];
|
|
165
|
+
throw new SessionStoreError(`Impossibile leggere ${cartellaStore}: ${errore.message}`, 'SESSION_STORE_READ_FAILED');
|
|
166
|
+
}
|
|
167
|
+
return voci
|
|
168
|
+
.filter((v) => v.isFile() && v.name.endsWith(ESTENSIONE))
|
|
169
|
+
.map((v) => v.name.slice(0, -ESTENSIONE.length));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* ⭐⭐⭐ 30/8, QA visiva (Task 14) — trovato dal vivo: nessun modo di
|
|
174
|
+
* eliminare una sessione, né qui né lato client. 144+ sessioni
|
|
175
|
+
* accumulate in un solo giro di QA senza possibilità di pulizia.
|
|
176
|
+
* Rimuove il file persistito — `ENOENT` è un esito onesto (già
|
|
177
|
+
* cancellato, o mai persistito perché senza `cartellaStore`), non un
|
|
178
|
+
* errore: stesso principio di `leggiRegistro` sopra.
|
|
179
|
+
*/
|
|
180
|
+
export async function eliminaSessionePersistita({ cartellaStore, sessionId }, deps = {}) {
|
|
181
|
+
const unlinkFn = deps.unlinkFn ?? fsp.unlink;
|
|
182
|
+
try {
|
|
183
|
+
await unlinkFn(percorsoDi(cartellaStore, sessionId));
|
|
184
|
+
} catch (errore) {
|
|
185
|
+
if (errore?.code === 'ENOENT') return;
|
|
186
|
+
throw new SessionStoreError(`Impossibile eliminare la sessione ${sessionId}: ${errore.message}`, 'SESSION_STORE_DELETE_FAILED');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Legge un registro per intero — una riga JSON per riga del file.
|
|
192
|
+
* ⭐⭐⭐ L'ULTIMA riga, se non è JSON valido, viene SCARTATA in silenzio
|
|
193
|
+
* (mai un errore che perde l'intero file): è esattamente il caso di
|
|
194
|
+
* un crash a metà scrittura, l'unica corruzione che l'append-only
|
|
195
|
+
* ammette per costruzione — "una riga rotta non invalida le
|
|
196
|
+
* precedenti", verificato in ricerca, non presunto. Una riga NON
|
|
197
|
+
* ultima malformata è invece un errore dichiarato: quello indica un
|
|
198
|
+
* file danneggiato in un altro modo, non un crash a metà append.
|
|
199
|
+
*/
|
|
200
|
+
export async function leggiRegistro({ cartellaStore, sessionId }, deps = {}) {
|
|
201
|
+
const readFileFn = deps.readFileFn ?? fsp.readFile;
|
|
202
|
+
let testo;
|
|
203
|
+
try {
|
|
204
|
+
testo = await readFileFn(percorsoDi(cartellaStore, sessionId), 'utf8');
|
|
205
|
+
} catch (errore) {
|
|
206
|
+
if (errore?.code === 'ENOENT') return null;
|
|
207
|
+
throw new SessionStoreError(`Impossibile leggere la sessione ${sessionId}: ${errore.message}`, 'SESSION_STORE_READ_FAILED');
|
|
208
|
+
}
|
|
209
|
+
const righe = testo.split('\n').filter((r) => r.trim() !== '');
|
|
210
|
+
const record = [];
|
|
211
|
+
for (let i = 0; i < righe.length; i++) {
|
|
212
|
+
try {
|
|
213
|
+
record.push(JSON.parse(righe[i]));
|
|
214
|
+
} catch {
|
|
215
|
+
if (i === righe.length - 1) break; // ultima riga, possibile crash a metà scrittura: scartata, non fatale
|
|
216
|
+
throw new SessionStoreError(`La sessione ${sessionId} ha una riga corrotta (non l'ultima): il file non è un crash a metà append, è danneggiato altrove.`, 'SESSION_STORE_CORRUPT');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return record;
|
|
220
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sessione-pronta.mjs — «questa sessione può partire con QUESTO modello?» (CLI-REQ-05, punto 1).
|
|
3
|
+
*
|
|
4
|
+
* Estratta da `server.mjs` il 17/09/2026 perché lì dentro era una chiusura che nessuna prova poteva
|
|
5
|
+
* chiamare: i revisori l'avevano misurata ricopiandone i byte. Una regola che decide se un giro parte
|
|
6
|
+
* merita un nome e delle prove sue.
|
|
7
|
+
*
|
|
8
|
+
* ⛔⛔⛔ 17/09/2026, difetto trovato dalla corsia della CLI DOPO la fusione (`ba420a95`) e riprodotto qui
|
|
9
|
+
* con lo store VERO prima di toccare una riga (`scratchpad/sonda-panchina.mjs`): una chiave messa in
|
|
10
|
+
* PANCHINA (rifiutata dal fornitore, o in attesa dopo un 429) rispondeva ancora «pronto».
|
|
11
|
+
* hasKey('deepseek') → true (conta OGNI chiave del pool, panchina compresa)
|
|
12
|
+
* getKey('deepseek') → null (passa da `scegliChiave`, che salta chi è in panchina)
|
|
13
|
+
* Il ramo OpenRouter usava già `getKey`, tutti gli altri `hasKey`: la sessione partiva, e il giro moriva
|
|
14
|
+
* subito dopo senza una chiave utilizzabile. L'owner l'ha incontrato nella CLI lo stesso giorno.
|
|
15
|
+
* ⇒ «Pronto» vuol dire UNA CHIAVE UTILIZZABILE ADESSO, per ogni fornitore. E quando le chiavi ci sono ma
|
|
16
|
+
* stanno tutte in panchina, il rifiuto lo DICE — con la causa in parole umane e fino a quando — invece
|
|
17
|
+
* di «Manca la chiave», che manderebbe la persona a incollare una chiave che ha già.
|
|
18
|
+
*
|
|
19
|
+
* ⛔ SINCRONA per contratto: `avviaESegui` nel registro non è `async` e non lo diventa (un solo tick fa
|
|
20
|
+
* cadere 148 prove, misurato il 10/09). Lo store risponde in memoria.
|
|
21
|
+
*/
|
|
22
|
+
import { REGISTRO_FORNITORI } from './provider-registry.mjs';
|
|
23
|
+
import { separaFonteModello } from './model-destination.mjs';
|
|
24
|
+
|
|
25
|
+
/** La causa della panchina, detta a una persona. Mai la classe tecnica a schermo. */
|
|
26
|
+
const CAUSA_UMANA = Object.freeze({
|
|
27
|
+
credenziale: 'il fornitore l\'ha rifiutata',
|
|
28
|
+
credito: 'il credito è esaurito',
|
|
29
|
+
traffico: 'il fornitore ha chiesto di aspettare per troppo traffico',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function fraseDellaPanchina(etichetta, pool, adesso) {
|
|
33
|
+
const ferme = pool.filter((v) => Number.isFinite(v?.inPanchinaFino) && v.inPanchinaFino > adesso);
|
|
34
|
+
if (ferme.length === 0) return null;
|
|
35
|
+
// La prima a tornare disponibile è quella che interessa: è quando si può riprovare.
|
|
36
|
+
const prima = ferme.reduce((a, b) => (a.inPanchinaFino <= b.inPanchinaFino ? a : b));
|
|
37
|
+
const causa = CAUSA_UMANA[prima.causa] ?? 'il fornitore non l\'ha accettata';
|
|
38
|
+
const minuti = Math.max(1, Math.ceil((prima.inPanchinaFino - adesso) / 60_000));
|
|
39
|
+
const quando = minuti >= 120 ? `fra circa ${Math.round(minuti / 60)} ore` : `fra circa ${minuti} min`;
|
|
40
|
+
const quante = ferme.length === 1 ? `La chiave di ${etichetta} è in pausa` : `Le ${ferme.length} chiavi di ${etichetta} sono in pausa`;
|
|
41
|
+
return `${quante}: ${causa}. Si riprova da sola ${quando}; per non aspettare, collega un'altra chiave da Fornitori e accessi.`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {{providerStore: {getKey: Function, elencaPool?: Function}, chiaveApi?: string|null, adessoFn?: () => number}} deps
|
|
46
|
+
* @returns {(modello: unknown) => {pronto: boolean, fornitore?: string, codice?: string, messaggio?: string}}
|
|
47
|
+
*/
|
|
48
|
+
export function creaProntoFn({ providerStore, chiaveApi = null, adessoFn = Date.now }) {
|
|
49
|
+
return function prontoFn(modello) {
|
|
50
|
+
/* ⛔ D2 (17/09): con un modello vuoto rispondeva «pronto» e la sessione partiva. Un modello che non si sa
|
|
51
|
+
leggere NON è pronto, e lo dice. */
|
|
52
|
+
if (typeof modello !== 'string' || modello.trim() === '') {
|
|
53
|
+
return { pronto: false, codice: 'CONFIG_INVALID', messaggio: 'Scegli un modello prima di avviare la sessione.' };
|
|
54
|
+
}
|
|
55
|
+
let fonte;
|
|
56
|
+
try { ({ fonte } = separaFonteModello(modello)); }
|
|
57
|
+
catch { return { pronto: false, codice: 'CONFIG_INVALID', messaggio: 'Questo modello non è riconosciuto: scegline uno dall\'elenco.' }; }
|
|
58
|
+
const record = REGISTRO_FORNITORI[fonte];
|
|
59
|
+
/* ⛔ `codice` e `messaggio` solo quando c'è qualcosa da dire: un «pronto» non porta un codice d'errore. */
|
|
60
|
+
if (!record || record.chiaveObbligatoria !== true) return { pronto: true, fornitore: record?.etichetta ?? fonte };
|
|
61
|
+
// UNA regola per tutti: una chiave utilizzabile ADESSO. La chiave d'ambiente di avvio vale solo per OpenRouter, com'era.
|
|
62
|
+
const utilizzabile = providerStore.getKey(fonte) ?? (fonte === 'openrouter' ? chiaveApi : null);
|
|
63
|
+
if (utilizzabile) return { pronto: true, fornitore: record.etichetta };
|
|
64
|
+
const pool = typeof providerStore.elencaPool === 'function' ? (providerStore.elencaPool(fonte) ?? []) : [];
|
|
65
|
+
const panchina = fraseDellaPanchina(record.etichetta, pool, adessoFn());
|
|
66
|
+
return {
|
|
67
|
+
pronto: false,
|
|
68
|
+
fornitore: record.etichetta,
|
|
69
|
+
codice: 'CONFIG_INVALID',
|
|
70
|
+
messaggio: panchina ?? `Manca la chiave di ${record.etichetta}: collegala da Fornitori e accessi.`,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⭐⭐⭐ 04/9 — R-02, intro al primo avvio (Fascia R, owner: «abbia un intro
|
|
3
|
+
* stile mobile»). Lo stato che dice al browser QUALI passi del primo avvio
|
|
4
|
+
* sono già fatti — letto dalla realtà, come sul telefono
|
|
5
|
+
* (`mobile/src/lib/onboarding/setupProgress.ts`: «a step is done when the
|
|
6
|
+
* thing it asks for EXISTS», non da un cursore salvato che può invecchiare).
|
|
7
|
+
*
|
|
8
|
+
* ⛔ Nessun segreto passa di qui: della chiave si dice solo per QUALI provider
|
|
9
|
+
* esiste (`conChiave`), mai il valore. È la stessa disciplina di
|
|
10
|
+
* `providerStore.listPublic()`, e il test lo prova con `JSON.stringify`.
|
|
11
|
+
*
|
|
12
|
+
* `TALOS_INTRO=0` è il rollback dichiarato nel ledger: l'intro non si apre,
|
|
13
|
+
* e il lanciatore torna al comportamento di R-01 (schermata Doctor se manca
|
|
14
|
+
* la chiave).
|
|
15
|
+
*/
|
|
16
|
+
export function statoPrimoAvvio({ providerStore = null, localeConfigurato = false, introDisattivato = false, cartelleProgetto = 0 } = {}) {
|
|
17
|
+
let conChiave = [];
|
|
18
|
+
if (providerStore && typeof providerStore.listPublic === 'function') {
|
|
19
|
+
try {
|
|
20
|
+
conChiave = providerStore.listPublic().filter((riga) => riga.keyConfigured === true).map((riga) => riga.id);
|
|
21
|
+
} catch {
|
|
22
|
+
conChiave = []; // un portachiavi rotto non è una chiave: si dichiara «non pronto», mai un'ipotesi
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const locale = localeConfigurato === true;
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
introDisattivato: introDisattivato === true,
|
|
28
|
+
provider: Object.freeze({ pronto: conChiave.length > 0 || locale, conChiave: Object.freeze(conChiave), localeConfigurato: locale }),
|
|
29
|
+
cartelleProgetto: Number.isInteger(cartelleProgetto) && cartelleProgetto >= 0 ? cartelleProgetto : 0,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// BC-48 A · lettore di sezioni Markdown e selezione deterministica, senza chiamate al modello.
|
|
2
|
+
// Riferimenti: CommonMark 0.31.2; node:path del runtime verificato v24.18.0.
|
|
3
|
+
import { posix, win32 } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const SOGLIA_RIGHE_INDICE = 60;
|
|
6
|
+
const byte = testo => Buffer.byteLength(testo, 'utf8');
|
|
7
|
+
const slash = testo => String(testo).replace(/\\/g, '/');
|
|
8
|
+
|
|
9
|
+
/** Conserva righe e terminatori originali: le coordinate appartengono al disco. */
|
|
10
|
+
function scandisci(contenuto) {
|
|
11
|
+
const righe = String(contenuto).match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
12
|
+
const commenti = [];
|
|
13
|
+
let recinto = null;
|
|
14
|
+
let commento = null;
|
|
15
|
+
const lette = righe.map((originale, indice) => {
|
|
16
|
+
const riga = originale.replace(/\r?\n$/, '');
|
|
17
|
+
const citazione = riga.match(/^(?: {0,3}>[ \t]?)+/)?.[0] ?? '';
|
|
18
|
+
const interna = riga.slice(citazione.length);
|
|
19
|
+
if (recinto?.citato && !citazione) recinto = null;
|
|
20
|
+
if (!commento && recinto) {
|
|
21
|
+
const chiusura = interna.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
|
|
22
|
+
if (chiusura && chiusura[1][0] === recinto.delimitatore[0] && chiusura[1].length >= recinto.delimitatore.length) recinto = null;
|
|
23
|
+
return { originale, pulita: originale, codice: true };
|
|
24
|
+
}
|
|
25
|
+
if (!commento) {
|
|
26
|
+
const apertura = interna.replace(/^ {0,3}(?:[-+*]|\d+[.)])[ \t]+/, '').match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
27
|
+
if (apertura && !(apertura[1][0] === '`' && apertura[2].includes('`'))) {
|
|
28
|
+
recinto = { delimitatore: apertura[1], citato: Boolean(citazione) };
|
|
29
|
+
return { originale, pulita: originale, codice: true };
|
|
30
|
+
}
|
|
31
|
+
if (/^( {4}|\t)/.test(interna)) return { originale, pulita: originale, codice: true };
|
|
32
|
+
}
|
|
33
|
+
let pulita = '';
|
|
34
|
+
for (let i = 0; i < originale.length;) {
|
|
35
|
+
if (commento) {
|
|
36
|
+
if (originale.startsWith('-->', i)) {
|
|
37
|
+
commenti.push(commento); commento = null; i += 3;
|
|
38
|
+
} else {
|
|
39
|
+
commento.testo += originale[i];
|
|
40
|
+
if (originale[i] === '\n' || originale[i] === '\r') pulita += originale[i];
|
|
41
|
+
i++;
|
|
42
|
+
}
|
|
43
|
+
} else if (originale.startsWith('<!--', i)) {
|
|
44
|
+
commento = { da: indice + 1, testo: '' }; i += 4;
|
|
45
|
+
} else if (originale[i] === '`') {
|
|
46
|
+
const delimitatore = originale.slice(i).match(/^`+/)[0];
|
|
47
|
+
const fine = originale.indexOf(delimitatore, i + delimitatore.length);
|
|
48
|
+
const dopo = fine < 0 ? i + delimitatore.length : fine + delimitatore.length;
|
|
49
|
+
pulita += originale.slice(i, dopo); i = dopo;
|
|
50
|
+
} else { pulita += originale[i]; i++; }
|
|
51
|
+
}
|
|
52
|
+
return { originale, pulita, codice: false };
|
|
53
|
+
});
|
|
54
|
+
return { righe: lette, commenti };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function rimuoviCommentiHtml(contenuto) {
|
|
58
|
+
return scandisci(contenuto).righe.map(r => r.pulita).join('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function primaFrase(testo) {
|
|
62
|
+
const righe = testo.split(/\r?\n/).slice(1);
|
|
63
|
+
const paragrafo = [];
|
|
64
|
+
for (const riga of righe) {
|
|
65
|
+
if (!riga.trim()) { if (paragrafo.length) break; else continue; }
|
|
66
|
+
if (paragrafo.length && /^\s*(?:[-*+] |\d+[.)] |#|```|~~~)/.test(riga)) break;
|
|
67
|
+
paragrafo.push(riga.trim().replace(/^(?:[-*+] |\d+[.)] )/, ''));
|
|
68
|
+
}
|
|
69
|
+
const frase = paragrafo.join(' ').replace(/\s+/g, ' ').trim();
|
|
70
|
+
return frase.match(/^.*?[.!?](?=\s|$)/)?.[0] ?? frase;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function analizzaSezioniIstruzioni(contenuto) {
|
|
74
|
+
const { righe, commenti } = scandisci(contenuto);
|
|
75
|
+
const inizi = righe.flatMap((r, i) => {
|
|
76
|
+
const titolo = !r.codice && r.pulita.match(/^ {0,3}##[ \t]+(.+?)[ \t]*(?:\r?\n)?$/);
|
|
77
|
+
return titolo ? [{ titolo: titolo[1].replace(/[ \t]+#+[ \t]*$/, '').trim(), da: i + 1 }] : [];
|
|
78
|
+
});
|
|
79
|
+
const sezioni = inizi.map((inizio, i) => {
|
|
80
|
+
const a = (inizi[i + 1]?.da ?? righe.length + 1) - 1;
|
|
81
|
+
const parte = righe.slice(inizio.da - 1, a);
|
|
82
|
+
const testo = parte.map(r => r.pulita).join('');
|
|
83
|
+
const metadati = commenti.filter(c => c.da > inizio.da && c.da <= a);
|
|
84
|
+
// Una lista separata da virgole; i gruppi {a,b} non vengono divisi.
|
|
85
|
+
const paths = metadati.flatMap(c => {
|
|
86
|
+
const m = c.testo.trim().match(/^talos:\s*paths:\s*([^\r\n]+)$/);
|
|
87
|
+
if (!m) return [];
|
|
88
|
+
return m[1].split(/,(?![^{}]*\})/).map(p => p.trim()).filter(Boolean);
|
|
89
|
+
});
|
|
90
|
+
return { ...inizio, a, byte: byte(parte.map(r => r.originale).join('')), testo, primaFrase: primaFrase(testo), paths,
|
|
91
|
+
sempre: (i === 0 && /^Non-Negotiable(?:\s|$)/i.test(inizio.titolo)) || metadati.some(c => /^talos:\s*sempre$/.test(c.testo.trim())) };
|
|
92
|
+
});
|
|
93
|
+
return { righe: righe.length, testo: righe.map(r => r.pulita).join(''),
|
|
94
|
+
premessa: righe.slice(0, (inizi[0]?.da ?? righe.length + 1) - 1).map(r => r.pulita).join(''), sezioni };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function rendiFileIstruzioni(file, { sogliaRighe = SOGLIA_RIGHE_INDICE } = {}) {
|
|
98
|
+
const analisi = analizzaSezioniIstruzioni(file.contenuto);
|
|
99
|
+
const indicizzato = analisi.sezioni.length > 0 && (!slash(file.etichetta).includes('/') || analisi.righe > sogliaRighe);
|
|
100
|
+
if (!indicizzato) return { ...analisi, indicizzato, sezioniSempre: [], testo: analisi.testo };
|
|
101
|
+
const sempre = analisi.sezioni.filter(s => s.sempre);
|
|
102
|
+
const indice = analisi.sezioni.filter(s => !s.sempre).map(s => `## ${s.titolo} · righe ${s.da}-${s.a} · ${s.byte} byte · ${s.primaFrase}`).join('\n');
|
|
103
|
+
const lettura = file.lettura ?? file.etichetta;
|
|
104
|
+
const guida = `Prima di lavorare su un'area, leggi la sua sezione con \`leggi\` su \`${lettura}\` alle righe indicate (righe del file su disco). Le sezioni con un percorso si aprono da sole quando tocchi quei file, se la gestione del contesto è attiva.\n`;
|
|
105
|
+
return { ...analisi, indicizzato, sezioniSempre: sempre.map(({ titolo, da, a, byte }) => ({ etichetta: file.etichetta, titolo, da, a, byte })),
|
|
106
|
+
testo: analisi.premessa + sempre.map(s => s.testo).join('') + guida + indice + (indice ? '\n' : '') };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function percorsoNelProgetto(percorso, cartella, radice) {
|
|
110
|
+
if (typeof percorso !== 'string' || !percorso.trim() || /[\r\n\0]/.test(percorso)) return null;
|
|
111
|
+
const api = /^[A-Za-z]:\/|^\/{2}/.test(slash(radice)) ? win32 : posix;
|
|
112
|
+
const pieno = api.resolve(cartella, slash(percorso));
|
|
113
|
+
const rel = slash(api.relative(radice, pieno));
|
|
114
|
+
if (rel === '..' || rel.startsWith('../') || api.isAbsolute(rel)) return null;
|
|
115
|
+
return rel || '.';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function percorsiToccati(messages, cartella, radice) {
|
|
119
|
+
const chiamate = new Map();
|
|
120
|
+
const pendenti = new Set();
|
|
121
|
+
const percorsi = new Set();
|
|
122
|
+
for (const messaggio of messages) {
|
|
123
|
+
if (messaggio?.role === 'assistant') for (const c of messaggio.tool_calls ?? []) {
|
|
124
|
+
if (typeof c?.id !== 'string') continue;
|
|
125
|
+
chiamate.set(c.id, c); pendenti.add(c.id);
|
|
126
|
+
}
|
|
127
|
+
if (messaggio?.role !== 'tool' || !pendenti.has(messaggio.tool_call_id)) continue;
|
|
128
|
+
pendenti.delete(messaggio.tool_call_id);
|
|
129
|
+
const chiamata = chiamate.get(messaggio.tool_call_id);
|
|
130
|
+
const nome = chiamata?.function?.name;
|
|
131
|
+
// ⭐ PO-12 (13/09/2026) — `file_edit` tocca un file per `percorso` esattamente come `scrivi`: senza questo nome le istruzioni di progetto della cartella toccata non si accendono su una MODIFICA, cioe' proprio sull'attrezzo con cui il modello lavora su codice che gia' esiste.
|
|
132
|
+
if (!['leggi', 'scrivi', 'file_edit', 'cerca', 'elenca'].includes(nome)) continue;
|
|
133
|
+
let argomenti;
|
|
134
|
+
try { argomenti = JSON.parse(chiamata.function.arguments); } catch { continue; }
|
|
135
|
+
if (!argomenti || typeof argomenti !== 'object' || Array.isArray(argomenti)) continue;
|
|
136
|
+
const candidati = [];
|
|
137
|
+
if (nome !== 'cerca') {
|
|
138
|
+
const percorso = ['percorso', 'path', 'file_path', 'filePath', 'file', 'filename'].map(k => argomenti[k]).find(v => typeof v === 'string' && v.trim());
|
|
139
|
+
if (percorso) candidati.push(percorso);
|
|
140
|
+
else if (nome === 'elenca') candidati.push('.');
|
|
141
|
+
}
|
|
142
|
+
// Questi due attrezzi restituiscono percorsi, uno per riga. Le code sono avvisi,
|
|
143
|
+
// non risultati: non leggere mai percorsi dal contenuto di leggi o di una shell.
|
|
144
|
+
if (['cerca', 'elenca'].includes(nome) && typeof messaggio.content === 'string') {
|
|
145
|
+
for (const riga of messaggio.content.split(/\r?\n/)) {
|
|
146
|
+
if (/^(?:⚠|…|no file matches\.|give at least|".*not a readable folder)/.test(riga)) break;
|
|
147
|
+
if (riga.trim()) candidati.push(riga);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const candidato of candidati) {
|
|
151
|
+
const percorso = percorsoNelProgetto(candidato, cartella, radice);
|
|
152
|
+
if (percorso !== null) percorsi.add(percorso);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { percorsi: [...percorsi], pendenti: pendenti.size };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function combacia(percorso, glob) {
|
|
159
|
+
const modello = slash(glob).replace(/^\.\//, '');
|
|
160
|
+
// Pattern confinati, espansione limitata: non permettere esplosioni di brace.
|
|
161
|
+
if (modello.length > 1024 || modello.startsWith('/') || /(^|\/)\.\.(\/|$)|^[A-Za-z]:/.test(modello) || (modello.match(/\{/g)?.length ?? 0) > 1) return false;
|
|
162
|
+
try { return posix.matchesGlob(percorso, modello); } catch { return false; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Accoda agli originali. Stato locale più marcatori nello storico: nessun registro globale. */
|
|
166
|
+
export function creaIniettoreSezioni({ file = [], cartella, radice, tetto = 24_000 } = {}) {
|
|
167
|
+
const catalogo = file.flatMap(f => {
|
|
168
|
+
const resa = rendiFileIstruzioni(f);
|
|
169
|
+
const occorrenze = new Map();
|
|
170
|
+
return resa.indicizzato ? resa.sezioni.map(s => {
|
|
171
|
+
const occorrenza = (occorrenze.get(s.titolo) ?? 0) + 1;
|
|
172
|
+
occorrenze.set(s.titolo, occorrenza);
|
|
173
|
+
return { ...s, occorrenza, etichetta: f.etichetta, lettura: f.lettura ?? f.etichetta };
|
|
174
|
+
}).filter(s => !s.sempre && s.paths.length) : [];
|
|
175
|
+
});
|
|
176
|
+
const viste = new Set();
|
|
177
|
+
return messages => {
|
|
178
|
+
if (!Array.isArray(messages) || !catalogo.length) return;
|
|
179
|
+
const { percorsi, pendenti } = percorsiToccati(messages, cartella, radice);
|
|
180
|
+
if (pendenti) return; // Preserva l'abbinamento anche nei batch di tool.
|
|
181
|
+
const aggiunte = [];
|
|
182
|
+
for (const sezione of catalogo) {
|
|
183
|
+
const marca = `Sezione di \`${sezione.etichetta}\` che vale per questa cartella: ${sezione.titolo} (${sezione.occorrenza}).\n`;
|
|
184
|
+
if (viste.has(marca)) continue;
|
|
185
|
+
if (messages.some(m => m?.role === 'user' && typeof m.content === 'string' && m.content.startsWith(marca))) { viste.add(marca); continue; }
|
|
186
|
+
if (!sezione.paths.some(glob => percorsi.some(p => combacia(p, glob)))) continue;
|
|
187
|
+
const intera = marca + `Righe ${sezione.da}-${sezione.a} del file su disco.\n` + sezione.testo;
|
|
188
|
+
const avviso = marca + `⚠ Tetto delle istruzioni (${tetto} byte): sezione omessa intera. Leggila con \`leggi\` su \`${sezione.lettura}\`, righe ${sezione.da}-${sezione.a}.`;
|
|
189
|
+
aggiunte.push({ marca, content: intera, avviso });
|
|
190
|
+
}
|
|
191
|
+
let totale = aggiunte.reduce((n, a) => n + byte(a.content), 0);
|
|
192
|
+
for (let i = aggiunte.length - 1; totale > tetto && i >= 0; i--) {
|
|
193
|
+
const a = aggiunte[i];
|
|
194
|
+
if (byte(a.avviso) >= byte(a.content)) continue;
|
|
195
|
+
totale += byte(a.avviso) - byte(a.content);
|
|
196
|
+
a.content = a.avviso;
|
|
197
|
+
}
|
|
198
|
+
if (totale > tetto) throw new RangeError('Il tetto delle istruzioni è insufficiente anche per gli avvisi delle sezioni.');
|
|
199
|
+
for (const { marca, content } of aggiunte) {
|
|
200
|
+
messages.push({ role: 'user', content });
|
|
201
|
+
viste.add(marca);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-registry.mjs — FASE F del piano `elegant-spinning-dongarra.md`
|
|
3
|
+
* (29/8): cartelle di istruzioni caricate on-demand, dichiarate per
|
|
4
|
+
* workspace. Stesso posto (`hook-registry.mjs`/`mcp-registry.mjs`
|
|
5
|
+
* letti come precedenti diretti) — `.harness-ui-skills/<nome>/SKILL.md`
|
|
6
|
+
* DENTRO il workspace, mai fuori: una skill è una proprietà del
|
|
7
|
+
* progetto, come `AGENTS.md` o `.harness-ui-hooks.json`.
|
|
8
|
+
*
|
|
9
|
+
* ⛔⛔ Nessun trust/hash gate, a differenza di hook (eseguono un
|
|
10
|
+
* comando) e server MCP (connettono un processo) — decisione
|
|
11
|
+
* deliberata: una skill è TESTO INERTE, la stessa categoria di rischio
|
|
12
|
+
* di `task.consegna` o di qualunque altro contenuto già nel system
|
|
13
|
+
* prompt, mai gated in questo progetto. Un cancello qui sarebbe un
|
|
14
|
+
* bluff di sicurezza, non una protezione vera — vedi la doc nel piano
|
|
15
|
+
* madre, FASE F, per il ragionamento completo.
|
|
16
|
+
*
|
|
17
|
+
* Formato verificato EMPIRICAMENTE il 29/8 leggendo quattro `SKILL.md`
|
|
18
|
+
* reali (`~/.claude/plugins/cache/.../SKILL.md`), non da una spec
|
|
19
|
+
* assunta: frontmatter YAML fra due righe `---`, tutte le istanze
|
|
20
|
+
* osservate a chiave singola per riga (`name: ...`, `description: ...`
|
|
21
|
+
* mai spezzato su più righe) — questo modulo parsa quella forma, non
|
|
22
|
+
* lo YAML generale (blocchi multi-riga, liste, nesting). Un file che
|
|
23
|
+
* usa una di quelle forme più ricche è un errore dichiarato
|
|
24
|
+
* (`SkillRegistryError`), mai un parsing silenziosamente sbagliato.
|
|
25
|
+
*/
|
|
26
|
+
import { promises as fsp } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
|
|
29
|
+
export class SkillRegistryError extends Error {
|
|
30
|
+
constructor(message, code = 'SKILL_INVALID') {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'SkillRegistryError';
|
|
33
|
+
this.code = code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const NOME_CARTELLA_SKILLS = '.harness-ui-skills';
|
|
38
|
+
const NOME_FILE_SKILL = 'SKILL.md';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parsa il frontmatter minimo (`chiave: valore`, una riga per coppia,
|
|
42
|
+
* fra due delimitatori `---`) di un SKILL.md. Torna
|
|
43
|
+
* `{campi, corpo}` — `campi` è un oggetto piatto stringa→stringa,
|
|
44
|
+
* `corpo` è tutto ciò che segue il secondo `---` (il markdown vero).
|
|
45
|
+
*/
|
|
46
|
+
function analizzaSkillMd(testo, skillId) {
|
|
47
|
+
const righe = testo.split('\n');
|
|
48
|
+
if (righe[0]?.trim() !== '---') {
|
|
49
|
+
throw new SkillRegistryError(`${skillId}/SKILL.md deve iniziare con un frontmatter "---"`, 'SKILL_MALFORMED');
|
|
50
|
+
}
|
|
51
|
+
const fineFrontmatter = righe.findIndex((r, i) => i > 0 && r.trim() === '---');
|
|
52
|
+
if (fineFrontmatter === -1) {
|
|
53
|
+
throw new SkillRegistryError(`${skillId}/SKILL.md ha un frontmatter mai chiuso (manca il secondo "---")`, 'SKILL_MALFORMED');
|
|
54
|
+
}
|
|
55
|
+
const campi = {};
|
|
56
|
+
for (const riga of righe.slice(1, fineFrontmatter)) {
|
|
57
|
+
if (riga.trim() === '') continue;
|
|
58
|
+
const indice = riga.indexOf(':');
|
|
59
|
+
if (indice === -1) {
|
|
60
|
+
throw new SkillRegistryError(`${skillId}/SKILL.md ha una riga di frontmatter non valida (attesa "chiave: valore"): "${riga}"`, 'SKILL_MALFORMED');
|
|
61
|
+
}
|
|
62
|
+
const chiave = riga.slice(0, indice).trim();
|
|
63
|
+
const valore = riga.slice(indice + 1).trim();
|
|
64
|
+
campi[chiave] = valore;
|
|
65
|
+
}
|
|
66
|
+
/*
|
|
67
|
+
* ⛔ `/^[\r\n]+/`, non `/^\n+/`: un file con terminatori CRLF (comune
|
|
68
|
+
* su Windows — stesso difetto già visto altrove in questo progetto,
|
|
69
|
+
* `scrivere-un-file-da-python-lo-converte-in-crlf.md` in memoria)
|
|
70
|
+
* lascerebbe un `\r` isolato davanti al corpo altrimenti — verificato
|
|
71
|
+
* dal vivo il 29/8 leggendo un vero SKILL.md con CRLF, non presunto.
|
|
72
|
+
*/
|
|
73
|
+
const corpo = righe.slice(fineFrontmatter + 1).join('\n').replace(/^[\r\n]+/, '');
|
|
74
|
+
return { campi, corpo };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Legge `<cartella>/.harness-ui-skills/*\/SKILL.md`. Un progetto senza
|
|
79
|
+
* skill dichiarate è uno stato valido — mai un errore, torna
|
|
80
|
+
* `{skills: []}`. Una singola skill malformata FERMA l'intero
|
|
81
|
+
* caricamento con un errore dichiarato (mai una skill fantasma
|
|
82
|
+
* ignorata in silenzio, stesso principio di caricaHooks/caricaServerMcp)
|
|
83
|
+
* — l'owner che ha sbagliato a scrivere un file deve saperlo.
|
|
84
|
+
*/
|
|
85
|
+
export async function caricaSkill({ cartella }, deps = {}) {
|
|
86
|
+
const readdirFn = deps.readdirFn ?? fsp.readdir;
|
|
87
|
+
const readFileFn = deps.readFileFn ?? fsp.readFile;
|
|
88
|
+
const cartellaSkills = join(cartella, NOME_CARTELLA_SKILLS);
|
|
89
|
+
let voci;
|
|
90
|
+
try {
|
|
91
|
+
voci = await readdirFn(cartellaSkills, { withFileTypes: true });
|
|
92
|
+
} catch (errore) {
|
|
93
|
+
if (errore?.code === 'ENOENT') return { skills: [] };
|
|
94
|
+
throw new SkillRegistryError(`Impossibile leggere ${NOME_CARTELLA_SKILLS}: ${errore.message}`, 'SKILL_READ_FAILED');
|
|
95
|
+
}
|
|
96
|
+
const skills = [];
|
|
97
|
+
for (const voce of voci) {
|
|
98
|
+
if (!voce.isDirectory()) continue; // un file sciolto dentro .harness-ui-skills/ non è una skill
|
|
99
|
+
const skillId = voce.name;
|
|
100
|
+
const percorso = join(cartellaSkills, skillId, NOME_FILE_SKILL);
|
|
101
|
+
let testo;
|
|
102
|
+
try {
|
|
103
|
+
testo = await readFileFn(percorso, 'utf8');
|
|
104
|
+
} catch (errore) {
|
|
105
|
+
if (errore?.code === 'ENOENT') continue; // una sottocartella senza SKILL.md non è una skill, non è un errore
|
|
106
|
+
throw new SkillRegistryError(`Impossibile leggere ${skillId}/${NOME_FILE_SKILL}: ${errore.message}`, 'SKILL_READ_FAILED');
|
|
107
|
+
}
|
|
108
|
+
const { campi, corpo } = analizzaSkillMd(testo, skillId);
|
|
109
|
+
if (typeof campi.name !== 'string' || campi.name.length === 0) {
|
|
110
|
+
throw new SkillRegistryError(`${skillId}/SKILL.md manca di "name" nel frontmatter`, 'SKILL_MALFORMED');
|
|
111
|
+
}
|
|
112
|
+
if (typeof campi.description !== 'string' || campi.description.length === 0) {
|
|
113
|
+
throw new SkillRegistryError(`${skillId}/SKILL.md manca di "description" nel frontmatter`, 'SKILL_MALFORMED');
|
|
114
|
+
}
|
|
115
|
+
skills.push({ id: skillId, name: campi.name, description: campi.description, corpo });
|
|
116
|
+
}
|
|
117
|
+
// ⭐ ordine deterministico — mai dipendere dall'ordine che il filesystem restituisce (varia per OS).
|
|
118
|
+
skills.sort((a, b) => a.id.localeCompare(b.id));
|
|
119
|
+
return { skills };
|
|
120
|
+
}
|
|
Binary file
|