nexakit 0.0.0-stage → 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/.claude-plugin/marketplace.json +28 -0
- package/LICENSE +26 -0
- package/README.md +79 -2
- package/agents/claude/docs-researcher.md +9 -0
- package/agents/claude/implementation-tutor.md +9 -0
- package/agents/claude/project-explorer.md +9 -0
- package/agents/claude/quality-reviewer.md +9 -0
- package/agents/claude/requirements-analyst.md +9 -0
- package/agents/claude/solution-architect.md +9 -0
- package/agents/codex/docs-researcher.toml +10 -0
- package/agents/codex/implementation-tutor.toml +10 -0
- package/agents/codex/project-explorer.toml +10 -0
- package/agents/codex/quality-reviewer.toml +10 -0
- package/agents/codex/requirements-analyst.toml +11 -0
- package/agents/codex/solution-architect.toml +10 -0
- package/assets/brand/README.md +38 -0
- package/assets/brand/agent-toolkit-mark.png +0 -0
- package/assets/brand/agent-toolkit-mark.svg +13 -0
- package/assets/brand/agent-toolkit-tui-hero.png +0 -0
- package/assets/brand/nexakit-readme-hero.png +0 -0
- package/assets/brand/nexakit-readme-hero.svg +21 -0
- package/assets/runtime/package-lock.json +1425 -0
- package/bin/agent-task.js +14 -0
- package/bin/agent-toolkit-gemini.js +13 -0
- package/bin/agent-toolkit-inbox.js +14 -0
- package/bin/agent-toolkit-mcp.js +17 -0
- package/bin/agent-toolkit.js +13 -0
- package/bin/nexakit.js +13 -0
- package/bin/project-memory-mcp.js +22 -0
- package/bin/project-memory.js +7 -0
- package/catalogs/skills/api-design-principles/LICENSE.txt +21 -0
- package/catalogs/skills/api-design-principles/SKILL.md +110 -0
- package/catalogs/skills/api-design-principles/assets/api-design-checklist.md +155 -0
- package/catalogs/skills/api-design-principles/assets/rest-api-template.py +185 -0
- package/catalogs/skills/api-design-principles/references/details.md +413 -0
- package/catalogs/skills/api-design-principles/references/graphql-schema-design.md +589 -0
- package/catalogs/skills/api-design-principles/references/rest-best-practices.md +408 -0
- package/catalogs/skills/nodejs-backend-patterns/LICENSE.txt +21 -0
- package/catalogs/skills/nodejs-backend-patterns/SKILL.md +45 -0
- package/catalogs/skills/nodejs-backend-patterns/references/advanced-patterns.md +430 -0
- package/catalogs/skills/nodejs-backend-patterns/references/details.md +599 -0
- package/catalogs/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
- package/catalogs/skills/supabase-postgres-best-practices/LICENSE.txt +21 -0
- package/catalogs/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/catalogs/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/catalogs/skills/vercel-composition-patterns/AGENTS.md +946 -0
- package/catalogs/skills/vercel-composition-patterns/LICENSE.txt +27 -0
- package/catalogs/skills/vercel-composition-patterns/README.md +60 -0
- package/catalogs/skills/vercel-composition-patterns/SKILL.md +89 -0
- package/catalogs/skills/vercel-composition-patterns/metadata.json +11 -0
- package/catalogs/skills/vercel-composition-patterns/rules/_sections.md +29 -0
- package/catalogs/skills/vercel-composition-patterns/rules/_template.md +24 -0
- package/catalogs/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +100 -0
- package/catalogs/skills/vercel-composition-patterns/rules/architecture-compound-components.md +112 -0
- package/catalogs/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md +87 -0
- package/catalogs/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md +100 -0
- package/catalogs/skills/vercel-composition-patterns/rules/react19-no-forwardref.md +42 -0
- package/catalogs/skills/vercel-composition-patterns/rules/state-context-interface.md +191 -0
- package/catalogs/skills/vercel-composition-patterns/rules/state-decouple-implementation.md +113 -0
- package/catalogs/skills/vercel-composition-patterns/rules/state-lift-state.md +125 -0
- package/catalogs/skills/vercel-react-best-practices/AGENTS.md +3810 -0
- package/catalogs/skills/vercel-react-best-practices/LICENSE.txt +27 -0
- package/catalogs/skills/vercel-react-best-practices/README.md +123 -0
- package/catalogs/skills/vercel-react-best-practices/SKILL.md +149 -0
- package/catalogs/skills/vercel-react-best-practices/metadata.json +15 -0
- package/catalogs/skills/vercel-react-best-practices/rules/_sections.md +46 -0
- package/catalogs/skills/vercel-react-best-practices/rules/_template.md +28 -0
- package/catalogs/skills/vercel-react-best-practices/rules/advanced-effect-event-deps.md +56 -0
- package/catalogs/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/catalogs/skills/vercel-react-best-practices/rules/advanced-init-once.md +42 -0
- package/catalogs/skills/vercel-react-best-practices/rules/advanced-use-latest.md +39 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-cheap-condition-before-await.md +37 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-defer-await.md +82 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-dependencies.md +51 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/catalogs/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-analyzable-paths.md +63 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +60 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/catalogs/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/catalogs/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/catalogs/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/catalogs/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/catalogs/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +107 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-flatmap-filter.md +60 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-request-idle-callback.md +105 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/catalogs/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +30 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-resource-hints.md +85 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md +68 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md +75 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +40 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md +38 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md +45 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md +82 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +35 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md +64 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md +59 -0
- package/catalogs/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +73 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-auth-actions.md +96 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-dedup-props.md +65 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-hoist-static-io.md +149 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-no-shared-module-state.md +50 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-parallel-nested-fetching.md +34 -0
- package/catalogs/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/catalogs/skills/webapp-testing/LICENSE.txt +202 -0
- package/catalogs/skills/webapp-testing/SKILL.md +96 -0
- package/catalogs/skills/webapp-testing/examples/console_logging.py +35 -0
- package/catalogs/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/catalogs/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/catalogs/skills/webapp-testing/scripts/with_server.py +106 -0
- package/catalogs/skills.json +1115 -0
- package/docs/skills/evidencias/2026-10-28-evaluacion-inicial-mcp-seguridad.md +118 -0
- package/docs/skills/evidencias/2026-10-28-incorporacion-mcp-seguridad.json +152 -0
- package/docs/skills/evidencias/2026-10-28-recomendador-preparacion.md +50 -0
- package/package.json +73 -4
- package/plugins/nexakit-modelo/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-modelo/hooks/hooks.json +1 -0
- package/plugins/nexakit-modelo/hooks/register.js +48 -0
- package/plugins/nexakit-modelo/hooks/status.js +8 -0
- package/plugins/nexakit-modelo/tests/register.test.ts +21 -0
- package/plugins/nexakit-panel/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-panel/README.md +20 -0
- package/plugins/nexakit-panel/hooks/hooks.json +1 -0
- package/plugins/nexakit-panel/hooks/lienzo.js +382 -0
- package/plugins/nexakit-panel/hooks/panel.js +279 -0
- package/plugins/nexakit-panel/hooks/register.tsx +248 -0
- package/plugins/nexakit-panel/tests/lienzo.test.ts +142 -0
- package/plugins/nexakit-panel/tests/panel.test.ts +240 -0
- package/plugins/nexakit-panel/tests/register.test.ts +167 -0
- package/plugins/nexakit-panel/types/index.d.ts +41 -0
- package/plugins/nexakit-sesion/.claude-plugin/plugin.json +1 -0
- package/plugins/nexakit-sesion/README.md +12 -0
- package/plugins/nexakit-sesion/hooks/aviso.js +38 -0
- package/plugins/nexakit-sesion/hooks/hooks.json +1 -0
- package/plugins/nexakit-sesion/hooks/register.js +28 -0
- package/plugins/nexakit-sesion/tests/register.test.ts +96 -0
- package/plugins/niveles/.claude-plugin/plugin.json +5 -0
- package/plugins/niveles/agents/nivel-alto.md +13 -0
- package/plugins/niveles/agents/nivel-exigente.md +13 -0
- package/plugins/niveles/agents/nivel-ligero.md +13 -0
- package/plugins/niveles/agents/nivel-normal.md +13 -0
- package/schemas/agent-client-adapter.schema.json +786 -0
- package/schemas/agent-client-auth-diagnostic.schema.json +801 -0
- package/schemas/agent-client-isolated-variant.schema.json +105 -0
- package/schemas/agent-client-isolated-verification.schema.json +99 -0
- package/schemas/agent-client-managed-preferences-invocation.schema.json +67 -0
- package/schemas/agent-client-managed-preferences.schema.json +63 -0
- package/schemas/agent-client-native-diagnostics.schema.json +85 -0
- package/schemas/agent-client-native-evaluation.schema.json +171 -0
- package/schemas/agent-client-native-preflight.schema.json +90 -0
- package/schemas/agent-client-output.schema.json +551 -0
- package/schemas/agent-client-runtime-fixture.schema.json +90 -0
- package/schemas/agent-client-runtime-local.schema.json +95 -0
- package/schemas/agent-client-runtime.schema.json +1288 -0
- package/schemas/agent-client-selective-config.schema.json +77 -0
- package/schemas/agent-client-supervised-local.schema.json +135 -0
- package/schemas/agent-client-supervised.schema.json +186 -0
- package/schemas/agent-model-preference.schema.json +573 -0
- package/schemas/agent-model-selection.schema.json +311 -0
- package/schemas/agent-task-approvals.schema.json +362 -0
- package/schemas/agent-task-prepare.schema.json +1324 -0
- package/schemas/agent-task-verify.schema.json +198 -0
- package/schemas/project-memory-update.schema.json +33 -0
- package/schemas/project-memory.schema.json +298 -0
- package/schemas/skill-catalog.schema.json +246 -0
- package/schemas/task-workflow-plan-output.schema.json +35 -0
- package/scripts/prepare-package.js +6 -0
- package/scripts/validate-skill-catalog.js +41 -0
- package/skills/cerrar-avisens/SKILL.md +12 -0
- package/skills/cerrar-memoria/SKILL.md +96 -0
- package/skills/cerrar-memoria/references/cierre.md +76 -0
- package/skills/decision-records/SKILL.md +27 -0
- package/skills/decision-records/agents/openai.yaml +7 -0
- package/skills/decision-records/references/adr-template.md +35 -0
- package/skills/differential-review/LICENSE.txt +427 -0
- package/skills/differential-review/SKILL.md +225 -0
- package/skills/differential-review/adversarial.md +203 -0
- package/skills/differential-review/agents/openai.yaml +6 -0
- package/skills/differential-review/assets/trail-of-bits-mark.svg +1 -0
- package/skills/differential-review/methodology.md +234 -0
- package/skills/differential-review/patterns.md +300 -0
- package/skills/differential-review/reporting.md +369 -0
- package/skills/elegir-modelo/SKILL.md +68 -0
- package/skills/elegir-modelo/modelos.mjs +108 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +71 -0
- package/skills/gemini/SKILL.md +52 -0
- package/skills/imagen/SKILL.md +12 -0
- package/skills/investigar/SKILL.md +11 -0
- package/skills/mcp-builder/LICENSE.txt +202 -0
- package/skills/mcp-builder/SKILL.md +236 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/ordenar-proyecto/SKILL.md +56 -0
- package/skills/project-handoff/SKILL.md +53 -0
- package/skills/project-handoff/agents/openai.yaml +7 -0
- package/skills/project-handoff/references/handoff-template.md +35 -0
- package/skills/quality-gate/LICENSE.txt +26 -0
- package/skills/quality-gate/SKILL.md +44 -0
- package/skills/quality-gate/agents/openai.yaml +7 -0
- package/skills/quality-gate/references/review-checklist.md +42 -0
- package/skills/revisar/SKILL.md +11 -0
- package/skills/revisar-skill/LICENSE-find-skills.txt +26 -0
- package/skills/revisar-skill/SKILL.md +94 -0
- package/skills/revision-cruzada/SKILL.md +59 -0
- package/skills/web-design-guidelines/AGENTS.md +156 -0
- package/skills/web-design-guidelines/LICENSE.txt +21 -0
- package/skills/web-design-guidelines/SKILL.md +29 -0
- package/src/agent-models.js +131 -0
- package/src/brand.js +117 -0
- package/src/claude-code-runner.js +642 -0
- package/src/cli-executable.js +88 -0
- package/src/cli.js +778 -0
- package/src/client-adapter-plan.js +271 -0
- package/src/client-auth-diagnostic-parser.js +211 -0
- package/src/client-auth-diagnostic.js +257 -0
- package/src/client-isolated-variant.js +106 -0
- package/src/client-isolated-verification.js +130 -0
- package/src/client-local-platform.js +14 -0
- package/src/client-managed-preferences-invocation.js +209 -0
- package/src/client-managed-preferences.js +78 -0
- package/src/client-native-access.js +134 -0
- package/src/client-native-diagnostics.js +253 -0
- package/src/client-native-evaluation.js +106 -0
- package/src/client-native-preflight-files.js +68 -0
- package/src/client-native-preflight.js +95 -0
- package/src/client-output-json.js +118 -0
- package/src/client-output-parser.js +278 -0
- package/src/client-runtime-admission.js +80 -0
- package/src/client-runtime-coordinator.js +70 -0
- package/src/client-runtime-core.js +92 -0
- package/src/client-runtime-fixture-permit.js +20 -0
- package/src/client-runtime-fixture-protocol.js +39 -0
- package/src/client-runtime-state.js +67 -0
- package/src/client-runtime-store-files.js +91 -0
- package/src/client-runtime-store.js +239 -0
- package/src/client-runtime-supervisor.js +146 -0
- package/src/client-selective-config.js +195 -0
- package/src/client-selective-files.js +126 -0
- package/src/client-supervised-codes.js +32 -0
- package/src/client-supervised-contract.js +52 -0
- package/src/client-supervised-coordinator.js +36 -0
- package/src/client-supervised-permit.js +20 -0
- package/src/client-supervised-protocol.js +37 -0
- package/src/client-supervised-store.js +167 -0
- package/src/client-supervised-supervisor.js +167 -0
- package/src/client-supervised.js +160 -0
- package/src/codex-cli-runner.js +358 -0
- package/src/codex-delivery.js +101 -0
- package/src/connect.js +703 -0
- package/src/doctor.js +795 -0
- package/src/gemini-cli.js +76 -0
- package/src/gemini-files.js +45 -0
- package/src/gemini-tools.js +249 -0
- package/src/init.js +473 -0
- package/src/install-lock.js +127 -0
- package/src/install-registry.js +61 -0
- package/src/legacy-repair.js +71 -0
- package/src/level-agents.js +111 -0
- package/src/link-states.js +172 -0
- package/src/memory-connection-artifacts.js +192 -0
- package/src/memory-connection-descriptor.js +158 -0
- package/src/memory-connection-diagnostic.js +177 -0
- package/src/memory-connection-files.js +191 -0
- package/src/memory-connection-manifest.js +118 -0
- package/src/memory-connection-package.js +176 -0
- package/src/memory-connection-plan.js +211 -0
- package/src/memory-connection-skill.js +211 -0
- package/src/memory-connection-toml.js +153 -0
- package/src/memory-locator.js +118 -0
- package/src/memory-mcp-policy.js +181 -0
- package/src/memory-mcp-server.js +166 -0
- package/src/memory-mcp-transport.js +59 -0
- package/src/missing-dependencies.js +21 -0
- package/src/model-command.js +147 -0
- package/src/model-preference.js +235 -0
- package/src/model-selection.js +368 -0
- package/src/native/managed-preferences.c +100 -0
- package/src/native/managed-preferences.h +17 -0
- package/src/package-lock-path.js +13 -0
- package/src/panel-cli.js +182 -0
- package/src/peer-handoff.js +27 -0
- package/src/peer-inbox-cli.js +211 -0
- package/src/peer-review-mcp.js +210 -0
- package/src/peer-review-store.js +532 -0
- package/src/platform.js +24 -0
- package/src/project-root.js +41 -0
- package/src/project-signals.js +339 -0
- package/src/recommend-report.js +77 -0
- package/src/review-instructions.js +211 -0
- package/src/review-policy.js +35 -0
- package/src/revision-adapters.js +157 -0
- package/src/revision-context.js +86 -0
- package/src/revision-isolation.js +222 -0
- package/src/revision-process.js +103 -0
- package/src/revision-provenance.js +89 -0
- package/src/revision-snapshot.js +120 -0
- package/src/revision.js +242 -0
- package/src/runtime-fixtures/runtime-supervisor/child.cjs +3 -0
- package/src/runtime-fixtures/runtime-supervisor/fixture.cjs +49 -0
- package/src/runtime-fixtures/supervised/child.cjs +3 -0
- package/src/runtime-fixtures/supervised/fixture.cjs +47 -0
- package/src/sensitive-data.js +21 -0
- package/src/setup-apply.js +231 -0
- package/src/setup-plan.js +231 -0
- package/src/setup-state.js +277 -0
- package/src/setup-tui.js +555 -0
- package/src/skill-catalog-add.js +91 -0
- package/src/skill-catalog-audit.js +68 -0
- package/src/skill-catalog.js +170 -0
- package/src/skill-files.js +39 -0
- package/src/skill-recommendations.js +1673 -0
- package/src/skill-scan-report.js +14 -0
- package/src/skill-scan.js +178 -0
- package/src/stable-command.js +53 -0
- package/src/stable-install.js +222 -0
- package/src/status.js +200 -0
- package/src/superpowers.js +133 -0
- package/src/sync-apply.js +227 -0
- package/src/sync-files.js +109 -0
- package/src/sync-inspect.js +394 -0
- package/src/sync-ledger.js +94 -0
- package/src/sync-markdown.js +151 -0
- package/src/sync-proposal.js +84 -0
- package/src/sync-record.js +459 -0
- package/src/sync.js +360 -0
- package/src/task-approvals.js +367 -0
- package/src/task-cli.js +79 -0
- package/src/task-external-verifier.js +584 -0
- package/src/task-prepare-files.js +115 -0
- package/src/task-prepare.js +180 -0
- package/src/task-verify-cli.js +67 -0
- package/src/task-verify-files.js +188 -0
- package/src/task-verify.js +118 -0
- package/src/task-workflow-mcp.js +544 -0
- package/src/task-workflow-store.js +254 -0
- package/src/terminal-text.js +68 -0
- package/src/toolkit-cli.js +543 -0
- package/src/toolkit-commands-guide.js +107 -0
- package/src/toolkit-setup.js +614 -0
- package/src/usage-metrics.js +43 -0
- package/templates/agent-team/01-encargo.md +158 -0
- package/templates/agent-team/02-propuesta.md +132 -0
- package/templates/agent-team/03-revision.md +150 -0
- package/templates/agent-team/README.md +213 -0
- package/templates/model-inventory/01-inventario.md +154 -0
- package/templates/model-inventory/README.md +115 -0
- package/templates/project-memory/README.md +67 -0
- package/templates/project-memory/project-memory.example.json +53 -0
- package/templates/project-memory/skill/project-id/SKILL.md +37 -0
- package/templates/project-memory/skill/project-id/references/project-profile.md +44 -0
- package/templates/project-memory/vault/00-INICIO/Estado actual.md +44 -0
- package/templates/project-memory/vault/00-INICIO/Fuentes de verdad.md +30 -0
- package/templates/project-memory/vault/00-INICIO/Inicio del proyecto.md +44 -0
- package/templates/project-memory/vault/00-INICIO/Mantenimiento de la memoria.md +26 -0
- package/templates/project-memory/vault/00-INICIO/Mapa de la memoria.md +33 -0
- package/templates/project-memory/vault/00-INICIO/Registro de sincronizaci/303/263n.md" +28 -0
- package/templates/project-memory/vault/01-PRODUCTO/Producto.md +16 -0
- package/templates/project-memory/vault/02-ARQUITECTURA/Arquitectura.md +16 -0
- package/templates/project-memory/vault/03-IMPLEMENTACION/Implementaci/303/263n.md +15 -0
- package/templates/project-memory/vault/04-DECISIONES/Decisiones.md +19 -0
- package/templates/project-memory/vault/05-ENTREGAS/Entregas.md +12 -0
- package/templates/project-memory/vault/06-OPERACION/Operaci/303/263n.md +15 -0
- package/templates/project-memory/vault/07-AGENTES/Protocolo de agentes.md +33 -0
- package/templates/project-memory/vault/08-RECURSOS/Recursos.md +12 -0
- package/templates/project-memory/vault/09-ARCHIVO/README.md +5 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
3
|
+
import { containsSensitiveData, isCredentialPath } from "./sensitive-data.js";
|
|
4
|
+
import { hashBytes, rejectFile, SYNC_LEDGER } from "./sync-files.js";
|
|
5
|
+
|
|
6
|
+
export const APPLY_LIMITS = Object.freeze({ proposal: 8 * 1024 * 1024, note: 1024 * 1024, notes: 20,
|
|
7
|
+
totalNotes: 5 * 1024 * 1024, manifest: 1024 * 1024, ledger: 1024 * 1024,
|
|
8
|
+
output: 1024 * 1024, virtualNotes: 5000, virtualBytes: 25 * 1024 * 1024, entries: 25_000 });
|
|
9
|
+
const schema = JSON.parse(fs.readFileSync(new URL("../schemas/project-memory-update.schema.json", import.meta.url), "utf8"));
|
|
10
|
+
const validate = new Ajv2020({ allErrors: false }).compile(schema);
|
|
11
|
+
export const portablePathKey = (value) => value.normalize("NFC").toLowerCase();
|
|
12
|
+
export function safeProposalPath(value, note = false) {
|
|
13
|
+
return typeof value === "string" && value.length > 0 && value.length <= 512 && value.trim() === value
|
|
14
|
+
&& !/[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069\\|`\[\]<>:*?]/.test(value)
|
|
15
|
+
&& !containsSensitiveData(value) && !isCredentialPath(value)
|
|
16
|
+
&& value.split("/").every((part) => part && ![".", ".."].includes(part) && (!note || !part.startsWith(".")))
|
|
17
|
+
&& (!note || value.endsWith(".md") && portablePathKey(value) !== portablePathKey(SYNC_LEDGER)
|
|
18
|
+
&& !value.split("/").some((part) => /^(?:skills?|config(?:uration)?|node_modules|credentials?|secrets?)$/i.test(part)));
|
|
19
|
+
}
|
|
20
|
+
export function parseProposal(text) {
|
|
21
|
+
let value;
|
|
22
|
+
try { checkJsonKeys(text); value = JSON.parse(text); }
|
|
23
|
+
catch { rejectFile("invalid-proposal", "La propuesta no es JSON estricto o contiene claves duplicadas.", "blocked"); }
|
|
24
|
+
if (!validate(value)) rejectFile("invalid-proposal", "La propuesta no cumple el contrato de actualización.", "blocked");
|
|
25
|
+
if (containsSensitiveData(text) || sensitiveDecodedValue(value)) rejectFile("sensitive-proposal", "La propuesta contiene patrones sensibles.", "blocked");
|
|
26
|
+
const seen = new Set(); let total = 0;
|
|
27
|
+
for (const note of value.notes) {
|
|
28
|
+
const key = portablePathKey(note.path);
|
|
29
|
+
if (!safeProposalPath(note.path, true) || seen.has(key) || !note.sourcePaths.every((p) => safeProposalPath(p))) rejectFile("proposal-path", "Una ruta de propuesta es insegura, duplicada o ambigua.", "blocked");
|
|
30
|
+
seen.add(key);
|
|
31
|
+
// Reject lone surrogates: Buffer.from must not silently replace JSON text.
|
|
32
|
+
if (/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/u.test(note.content)) rejectFile("invalid-text", "Una nota propuesta contiene Unicode inválido.", "blocked");
|
|
33
|
+
const bytes = Buffer.byteLength(note.content); total += bytes;
|
|
34
|
+
if (bytes > APPLY_LIMITS.note || total > APPLY_LIMITS.totalNotes) rejectFile("notes-limit", "Las notas propuestas superan el límite permitido.", "blocked");
|
|
35
|
+
}
|
|
36
|
+
return { ...value, notes: [...value.notes].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0) };
|
|
37
|
+
}
|
|
38
|
+
function sensitiveDecodedValue(value) {
|
|
39
|
+
if (typeof value === "string") return containsSensitiveData(value);
|
|
40
|
+
if (Array.isArray(value)) return value.some(sensitiveDecodedValue);
|
|
41
|
+
return value !== null && typeof value === "object" && Object.entries(value).some(([key, item]) => containsSensitiveData(key) || sensitiveDecodedValue(item));
|
|
42
|
+
}
|
|
43
|
+
export function proposalPlanId({ proposal, vault, proposalHash, indexHash, ledgerHash, acknowledgments }) {
|
|
44
|
+
return hashBytes(JSON.stringify(["sync-apply-v1", proposal.projectId, vault, proposal.manifestSha256,
|
|
45
|
+
proposalHash, proposal.memoryHead, indexHash, ledgerHash, proposal.repository, proposal.reviewBranch,
|
|
46
|
+
proposal.pr, proposal.mergeCommit, proposal.notes.map((note) => [note.path, note.beforeSha256, hashBytes(Buffer.from(note.content))]), acknowledgments]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A bounded syntax walk catches duplicate decoded keys before JSON.parse
|
|
50
|
+
// discards them. No JSON values are executed or resolved from external URLs.
|
|
51
|
+
function checkJsonKeys(text) {
|
|
52
|
+
let i = 0;
|
|
53
|
+
const whitespace = () => { while (/[ \t\r\n]/.test(text[i] ?? "x")) i++; };
|
|
54
|
+
const string = () => {
|
|
55
|
+
if (text[i] !== '"') throw new Error();
|
|
56
|
+
const start = i++;
|
|
57
|
+
while (i < text.length) {
|
|
58
|
+
if (text[i] === "\\") { i += 2; continue; }
|
|
59
|
+
if (text[i++] === '"') return JSON.parse(text.slice(start, i));
|
|
60
|
+
}
|
|
61
|
+
throw new Error();
|
|
62
|
+
};
|
|
63
|
+
const value = (depth) => {
|
|
64
|
+
if (depth > 64) throw new Error();
|
|
65
|
+
whitespace();
|
|
66
|
+
if (text[i] === "{") {
|
|
67
|
+
i++; whitespace(); const keys = new Set();
|
|
68
|
+
if (text[i] === "}") { i++; return; }
|
|
69
|
+
while (true) {
|
|
70
|
+
whitespace(); const key = string(); if (keys.has(key)) throw new Error(); keys.add(key);
|
|
71
|
+
whitespace(); if (text[i++] !== ":") throw new Error(); value(depth + 1); whitespace();
|
|
72
|
+
if (text[i] === "}") { i++; return; } if (text[i++] !== ",") throw new Error();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (text[i] === "[") {
|
|
76
|
+
i++; whitespace(); if (text[i] === "]") { i++; return; }
|
|
77
|
+
while (true) { value(depth + 1); whitespace(); if (text[i] === "]") { i++; return; } if (text[i++] !== ",") throw new Error(); }
|
|
78
|
+
}
|
|
79
|
+
if (text[i] === '"') { string(); return; }
|
|
80
|
+
const match = text.slice(i).match(/^(?:true|false|null|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)/);
|
|
81
|
+
if (!match) throw new Error(); i += match[0].length;
|
|
82
|
+
};
|
|
83
|
+
value(0); whitespace(); if (i !== text.length) throw new Error();
|
|
84
|
+
}
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { diagnoseMemory } from "./doctor.js";
|
|
7
|
+
import { inspectMemorySync } from "./sync-inspect.js";
|
|
8
|
+
import { parseGitHubRepository, validateAutomation } from "./sync.js";
|
|
9
|
+
import { appendLedgerRecord, parseWritableLedger } from "./sync-ledger.js";
|
|
10
|
+
import { containsSensitiveData, isCredentialPath } from "./sensitive-data.js";
|
|
11
|
+
import { sameGitRoot, relativeGitPath, sameSyncText, syncWriteLockPath } from "./sync-files.js";
|
|
12
|
+
|
|
13
|
+
export const RECORD_LIMITS = Object.freeze({ ledger: 1024 * 1024, note: 1024 * 1024,
|
|
14
|
+
notes: 20, totalNotes: 5 * 1024 * 1024, manifest: 1024 * 1024, git: 5 * 1024 * 1024 });
|
|
15
|
+
export const LEDGER_PATH = "00-INICIO/Registro de sincronización.md";
|
|
16
|
+
export const RECORD_CONCURRENCY_NOTICE = "Evita editar o sincronizar la memoria durante --apply: el bloqueo no controla editores externos.";
|
|
17
|
+
const ACK_CODES = new Set(["text-limit", "unsafe-reference"]);
|
|
18
|
+
const CANONICAL_DIRS = ["00-INICIO", "01-PRODUCTO", "02-ARQUITECTURA", "03-IMPLEMENTACION",
|
|
19
|
+
"04-DECISIONES", "05-ENTREGAS", "06-OPERACION", "07-AGENTES", "08-RECURSOS", "09-ARCHIVO"];
|
|
20
|
+
|
|
21
|
+
export class SyncRecordError extends Error {
|
|
22
|
+
constructor(code, message, status = "error") {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "SyncRecordError";
|
|
25
|
+
this.code = code;
|
|
26
|
+
this.status = status;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const fail = (code, message, status) => { throw new SyncRecordError(code, message, status); };
|
|
31
|
+
|
|
32
|
+
export function recordErrorReport(error) {
|
|
33
|
+
const safe = error instanceof SyncRecordError ? error
|
|
34
|
+
: new SyncRecordError("record-error", "No se pudo completar el registro de forma segura.");
|
|
35
|
+
return { command: "sync record", status: safe.status, code: safe.code,
|
|
36
|
+
details: [safe.message], wrote: false, concurrencyNotice: RECORD_CONCURRENCY_NOTICE };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function recordMemorySync(options = {}, context = {}) {
|
|
40
|
+
const input = validateOptions(options);
|
|
41
|
+
try {
|
|
42
|
+
return await prepareAndRecord(input, context);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof SyncRecordError) throw error;
|
|
45
|
+
// Provider output, filesystem messages and paths are deliberately not echoed.
|
|
46
|
+
fail("record-error", "No se pudo completar el registro; revisa permisos, configuración y estado de la memoria.");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function prepareAndRecord(input, context) {
|
|
51
|
+
const target = path.resolve(input.targetPath ?? process.cwd());
|
|
52
|
+
const explicitManifest = input.manifestPath ? path.resolve(input.manifestPath) : undefined;
|
|
53
|
+
const candidate = explicitManifest ?? (fs.lstatSync(target).isFile() ? target
|
|
54
|
+
: ["project-memory.json", "project-memory.local.json", "project-memory.example.json"]
|
|
55
|
+
.map((name) => path.join(target, name)).find((file) => fs.existsSync(file)));
|
|
56
|
+
if (!candidate) fail("manifest-missing", "No se encontró un manifiesto de memoria.", "blocked");
|
|
57
|
+
// Bound the manifest before doctor parses it; do not let a large JSON bypass
|
|
58
|
+
// the writer's preflight limits.
|
|
59
|
+
const checkedManifestPath = regularPath(candidate);
|
|
60
|
+
const preflightManifest = readRegular(checkedManifestPath, RECORD_LIMITS.manifest);
|
|
61
|
+
let doctor;
|
|
62
|
+
try {
|
|
63
|
+
doctor = await (context.diagnoseMemory ?? diagnoseMemory)(target, {
|
|
64
|
+
manifestPath: checkedManifestPath,
|
|
65
|
+
schemaPath: context.schemaPath
|
|
66
|
+
});
|
|
67
|
+
} catch { fail("doctor-error", "No se pudo diagnosticar la memoria."); }
|
|
68
|
+
if (!["ok", "warning"].includes(doctor.status) || !doctor.vaultPath || !doctor.manifestPath) {
|
|
69
|
+
fail("doctor-error", "La memoria no superó project-memory doctor.", "blocked");
|
|
70
|
+
}
|
|
71
|
+
if (fs.lstatSync(doctor.vaultPath).isSymbolicLink()) fail("unsafe-path", "La bóveda no puede ser un enlace simbólico.", "conflict");
|
|
72
|
+
const vault = fs.realpathSync(doctor.vaultPath);
|
|
73
|
+
const manifestPath = regularPath(doctor.manifestPath);
|
|
74
|
+
if (containsSensitiveData(vault) || /[\u0000-\u001f\u007f-\u009f]/.test(vault)) fail("unsafe-path", "La ruta de memoria no puede mostrarse de forma segura.");
|
|
75
|
+
const ledgerPath = safeJoin(vault, LEDGER_PATH);
|
|
76
|
+
const ledgerFile = readRegular(ledgerPath, RECORD_LIMITS.ledger);
|
|
77
|
+
const manifestFile = readRegular(manifestPath, RECORD_LIMITS.manifest);
|
|
78
|
+
if (manifestPath !== checkedManifestPath || manifestFile.hash !== preflightManifest.hash) fail("manifest-change", "El manifiesto cambió durante el diagnóstico.", "conflict");
|
|
79
|
+
const manifest = JSON.parse(manifestFile.text);
|
|
80
|
+
validateAutomation(manifest);
|
|
81
|
+
const repository = parseGitHubRepository(manifest.source?.remoteUrl);
|
|
82
|
+
if (containsSensitiveData(manifestFile.text) || /[\u0000-\u001f\u007f-\u009f]/.test(repository)) fail("unsafe-config", "El manifiesto contiene valores inseguros.");
|
|
83
|
+
let ledger;
|
|
84
|
+
try { ledger = parseWritableLedger(ledgerFile.text, repository); }
|
|
85
|
+
catch { fail("invalid-ledger", "La tabla del registro es ambigua, incompleta o incompatible.", "conflict"); }
|
|
86
|
+
const result = input.outcome === "no-documentation-change" ? "Sin cambios documentales"
|
|
87
|
+
: `${input.summary}; Notas: ${input.notes.join(", ")}`;
|
|
88
|
+
const existing = ledger.records.get(input.pr);
|
|
89
|
+
if (existing && (existing.state !== "Revisado" || existing.result !== result)) {
|
|
90
|
+
fail("record-conflict", "El PR ya tiene un registro diferente o incompleto; no se sobrescribirá.", "conflict");
|
|
91
|
+
}
|
|
92
|
+
const unchanged = Boolean(existing);
|
|
93
|
+
const noteFiles = input.notes.map((note) => {
|
|
94
|
+
validateNoteArea(note, manifest.memory?.layout);
|
|
95
|
+
const file = readRegular(safeJoin(vault, note), RECORD_LIMITS.note);
|
|
96
|
+
validateNote(file.text, vault, note);
|
|
97
|
+
return { note, file };
|
|
98
|
+
});
|
|
99
|
+
if (noteFiles.reduce((total, item) => total + item.file.bytes.length, 0) > RECORD_LIMITS.totalNotes) fail("notes-limit", "Las notas exceden el límite total permitido.", "blocked");
|
|
100
|
+
const git = gitState(vault);
|
|
101
|
+
requireTrackedLedger(vault);
|
|
102
|
+
validateTrackedFlags(vault);
|
|
103
|
+
if (!unchanged) {
|
|
104
|
+
const ledgerHead = gitRead(vault, ["show", `HEAD:${LEDGER_PATH}`]);
|
|
105
|
+
const ledgerIndex = gitRead(vault, ["show", `:${LEDGER_PATH}`]);
|
|
106
|
+
if (!sameSyncText(ledgerHead, ledgerFile.text)
|
|
107
|
+
|| ledgerIndex !== ledgerHead) fail("dirty-ledger", "El registro debe estar limpio tanto en HEAD como en staging.", "conflict");
|
|
108
|
+
}
|
|
109
|
+
validateChanges(git.changes, input, unchanged, vault, ledgerFile.text, existing, repository);
|
|
110
|
+
validateDiffs(vault, input.notes, git.changes);
|
|
111
|
+
// Discount only a diagnosed dirty-tree warning after exact-path validation.
|
|
112
|
+
const remainingChecks = (doctor.checks ?? []).filter((check) => !(check.level === "warning"
|
|
113
|
+
&& check.id === "git-cleanliness" && git.changes.length > 0));
|
|
114
|
+
const warnings = remainingChecks.filter((check) => check.level === "warning");
|
|
115
|
+
if (warnings.length || doctor.status === "warning" && !(doctor.checks?.some((c) => c.level === "warning"))) {
|
|
116
|
+
fail("doctor-warning", "Hay advertencias de diagnóstico ajenas a las notas autorizadas.", "blocked");
|
|
117
|
+
}
|
|
118
|
+
const cachedDoctor = { ...doctor, status: "ok", checks: remainingChecks };
|
|
119
|
+
const files = [ledgerPath, manifestPath, ...noteFiles.map((item) => item.file.path)];
|
|
120
|
+
const before = captureState(vault, files);
|
|
121
|
+
assertSnapshot(git, before.git);
|
|
122
|
+
const initialFiles = [ledgerFile, manifestFile, ...noteFiles.map((item) => item.file)];
|
|
123
|
+
for (const file of initialFiles) {
|
|
124
|
+
const captured = before.files.find((item) => item.path === file.path);
|
|
125
|
+
assertSnapshot({ hash: file.hash, identity: file.identity }, { hash: captured.hash, identity: captured.identity });
|
|
126
|
+
}
|
|
127
|
+
// One bounded inspector execution: identity validation always runs, including no-op.
|
|
128
|
+
let inspection;
|
|
129
|
+
try {
|
|
130
|
+
inspection = await inspectMemorySync({ targetPath: target, pr: input.pr,
|
|
131
|
+
manifestPath, maxFiles: 1000 }, { ...context, diagnoseMemory: async () => cachedDoctor });
|
|
132
|
+
} catch { fail("inspection-error", "No se pudo confirmar el PR integrado en GitHub.", "blocked"); }
|
|
133
|
+
if (inspection.repository !== repository || inspection.branch !== manifest.automation.reviewBranch) {
|
|
134
|
+
fail("inspection-conflict", "La identidad de la fuente cambió durante la revisión.", "conflict");
|
|
135
|
+
}
|
|
136
|
+
assertSnapshot(before, captureState(vault, files));
|
|
137
|
+
if (!unchanged) requireEvidence(inspection, input.ackWarnings);
|
|
138
|
+
const date = new Date((context.clock ?? Date.now)()).toISOString().slice(0, 10);
|
|
139
|
+
const report = { command: "sync record", status: unchanged ? "unchanged" : "preview",
|
|
140
|
+
repository, branch: inspection.branch, pr: input.pr, mergeCommit: inspection.pullRequest.mergeCommit,
|
|
141
|
+
ledgerPath, outcome: input.outcome, result, notes: input.notes,
|
|
142
|
+
date: existing?.date ?? date, acknowledgedWarnings: unchanged ? [] : input.ackWarnings,
|
|
143
|
+
wrote: false, concurrencyNotice: RECORD_CONCURRENCY_NOTICE };
|
|
144
|
+
if (unchanged) return report;
|
|
145
|
+
const proposal = appendLedgerRecord(ledger, { repository, pr: input.pr, date, result });
|
|
146
|
+
if (Buffer.byteLength(proposal) > RECORD_LIMITS.ledger) fail("ledger-limit", "La fila excedería el tamaño permitido del registro.", "blocked");
|
|
147
|
+
// Validate the complete document before touching the filesystem.
|
|
148
|
+
try { parseWritableLedger(proposal, repository); }
|
|
149
|
+
catch { fail("invalid-proposal", "La fila propuesta no produce un registro válido.", "blocked"); }
|
|
150
|
+
if (!input.apply) return report;
|
|
151
|
+
return applyRecord({ vault, files, before, ledgerFile, proposal, report }, context);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function validateOptions(options) {
|
|
155
|
+
const keys = new Set(["targetPath", "manifestPath", "pr", "outcome", "reviewed", "summary", "notes", "ackWarnings", "apply"]);
|
|
156
|
+
if (Object.keys(options).some((key) => !keys.has(key))) fail("invalid-option", "Opción desconocida de sync record.");
|
|
157
|
+
const number = typeof options.pr === "number" || typeof options.pr === "string" && /^[1-9]\d*$/.test(options.pr) ? Number(options.pr) : NaN;
|
|
158
|
+
if (!Number.isSafeInteger(number) || number < 1 || options.reviewed !== true
|
|
159
|
+
|| !["no-documentation-change", "documentation-updated"].includes(options.outcome)
|
|
160
|
+
|| options.apply !== undefined && typeof options.apply !== "boolean") {
|
|
161
|
+
fail("invalid-option", "Indica un PR positivo, un resultado válido y --reviewed; --apply debe ser explícito.");
|
|
162
|
+
}
|
|
163
|
+
for (const key of ["targetPath", "manifestPath"]) if (options[key] !== undefined
|
|
164
|
+
&& (typeof options[key] !== "string" || !options[key] || /[\u0000-\u001f]/.test(options[key]) || containsSensitiveData(options[key]))) {
|
|
165
|
+
fail("invalid-option", "La ruta de entrada no es válida.");
|
|
166
|
+
}
|
|
167
|
+
if (!Array.isArray(options.notes ?? []) || !Array.isArray(options.ackWarnings ?? [])
|
|
168
|
+
|| (options.notes?.length ?? 0) > RECORD_LIMITS.notes || (options.ackWarnings?.length ?? 0) > 2) fail("invalid-option", "Las listas superan los límites o no son válidas.");
|
|
169
|
+
const notes = [...new Set(options.notes ?? [])].sort();
|
|
170
|
+
for (const note of notes) if (!safeRelativeNote(note)) fail("invalid-option", "Una nota no es una ruta relativa Markdown segura.");
|
|
171
|
+
const ackWarnings = [...new Set(options.ackWarnings ?? [])].sort();
|
|
172
|
+
if (ackWarnings.some((code) => !ACK_CODES.has(code))) fail("invalid-option", "Solo se admiten text-limit y unsafe-reference como advertencias revisadas.");
|
|
173
|
+
if (options.outcome === "no-documentation-change" && (notes.length || options.summary !== undefined)) fail("invalid-option", "Sin cambios documentales no admite resumen ni notas.");
|
|
174
|
+
if (options.outcome === "documentation-updated" && (!notes.length || typeof options.summary !== "string"
|
|
175
|
+
|| options.summary.trim() !== options.summary || !options.summary.length || options.summary.length > 300 || !safeCell(options.summary))) {
|
|
176
|
+
fail("invalid-option", "Notas actualizadas exige rutas exactas y un resumen seguro de 1–300 caracteres.");
|
|
177
|
+
}
|
|
178
|
+
return { ...options, pr: number, notes, ackWarnings };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function safeCell(value) {
|
|
182
|
+
return !/[\u0000-\u001f\u007f-\u009f|`\[\]<>\\]/.test(value)
|
|
183
|
+
&& !/(?:https?:|data:|mailto:)/i.test(value) && !containsSensitiveData(value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function safeRelativeNote(note) {
|
|
187
|
+
return typeof note === "string" && note.length <= 512 && note.trim() === note && note.endsWith(".md")
|
|
188
|
+
&& !path.posix.isAbsolute(note) && !/[*?:]/.test(note) && safeCell(note)
|
|
189
|
+
&& note.split("/").every((part) => part && ![".", ".."].includes(part))
|
|
190
|
+
&& !note.split("/").some((part) => part.startsWith(".") || /^(?:skills?|config(?:uration)?|node_modules|credentials?|secrets?)$/i.test(part))
|
|
191
|
+
&& !isCredentialPath(note) && note !== LEDGER_PATH;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validateNoteArea(note, layout) {
|
|
195
|
+
const dirs = layout?.profile === "declared" ? layout.requiredDirectories : CANONICAL_DIRS;
|
|
196
|
+
if (!(dirs ?? []).some((dir) => note.startsWith(`${dir}/`))) fail("note-area", "Una nota queda fuera de las áreas declaradas.", "conflict");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function regularPath(input) {
|
|
200
|
+
if (fs.lstatSync(input).isSymbolicLink()) fail("unsafe-path", "No se admiten archivos enlazados.", "conflict");
|
|
201
|
+
const resolved = fs.realpathSync(input);
|
|
202
|
+
assertNoSymlinks(resolved);
|
|
203
|
+
return resolved;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function assertNoSymlinks(fullPath) {
|
|
207
|
+
const parsed = path.parse(fullPath);
|
|
208
|
+
let current = parsed.root;
|
|
209
|
+
const parts = fullPath.slice(parsed.root.length).split(path.sep).filter(Boolean);
|
|
210
|
+
for (let i = 0; i < parts.length; i++) {
|
|
211
|
+
current = path.join(current, parts[i]);
|
|
212
|
+
const stat = fs.lstatSync(current);
|
|
213
|
+
if (stat.isSymbolicLink() || i < parts.length - 1 && !stat.isDirectory()) fail("unsafe-path", "Una ruta contiene enlaces o componentes incompatibles.", "conflict");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function safeJoin(vault, relative) {
|
|
218
|
+
const full = path.resolve(vault, relative);
|
|
219
|
+
if (!full.startsWith(`${vault}${path.sep}`)) fail("unsafe-path", "La ruta escapa a la memoria.", "conflict");
|
|
220
|
+
assertNoSymlinks(full);
|
|
221
|
+
return full;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readRegular(fullPath, limit, binary = false) {
|
|
225
|
+
assertNoSymlinks(fullPath);
|
|
226
|
+
const before = fs.lstatSync(fullPath);
|
|
227
|
+
if (!before.isFile() || before.size > limit) fail("file-limit", "Un archivo no es regular o supera el límite permitido.", "blocked");
|
|
228
|
+
const fd = fs.openSync(fullPath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
229
|
+
let bytes;
|
|
230
|
+
try {
|
|
231
|
+
const opened = fs.fstatSync(fd);
|
|
232
|
+
if (opened.ino !== before.ino || opened.dev !== before.dev || !opened.isFile()
|
|
233
|
+
|| opened.size !== before.size || opened.mtimeMs !== before.mtimeMs || opened.ctimeMs !== before.ctimeMs) fail("changed-file", "Un archivo cambió mientras se leía.", "conflict");
|
|
234
|
+
const buffer = Buffer.alloc(Math.min(limit + 1, before.size + 1));
|
|
235
|
+
let offset = 0, received;
|
|
236
|
+
do { received = fs.readSync(fd, buffer, offset, buffer.length - offset, offset); offset += received; }
|
|
237
|
+
while (received && offset < buffer.length);
|
|
238
|
+
if (offset > limit) fail("file-limit", "Un archivo supera el límite permitido.", "blocked");
|
|
239
|
+
const after = fs.fstatSync(fd);
|
|
240
|
+
if (after.size !== offset || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs) fail("changed-file", "Un archivo cambió mientras se leía.", "conflict");
|
|
241
|
+
bytes = buffer.subarray(0, offset);
|
|
242
|
+
} finally { fs.closeSync(fd); }
|
|
243
|
+
let text;
|
|
244
|
+
if (!binary) {
|
|
245
|
+
try { text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes); }
|
|
246
|
+
catch { fail("invalid-text", "Un archivo no contiene texto UTF-8 válido.", "blocked"); }
|
|
247
|
+
}
|
|
248
|
+
return { path: fullPath, bytes, text, mode: before.mode & 0o777,
|
|
249
|
+
identity: [before.dev, before.ino, before.mode, before.size, before.mtimeMs, before.ctimeMs], hash: digest(bytes) };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function validateNote(text, vault, note) {
|
|
253
|
+
if (containsSensitiveData(text)) fail("sensitive-note", "Una nota contiene patrones sensibles; el registro se bloqueó.", "blocked");
|
|
254
|
+
if (!text.endsWith("\n") || /[ \t]+\r?$/m.test(text) || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/.test(text)) fail("note-format", "Una nota necesita corregir su formato.", "blocked");
|
|
255
|
+
const body = text.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, "").replace(/(`+)[^`\n]*?\1/g, "");
|
|
256
|
+
for (const match of body.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
257
|
+
const target = match[1].replace(/^<|>$/g, "");
|
|
258
|
+
if (/^(?:#|https?:|mailto:|data:|obsidian:)/i.test(target)) continue;
|
|
259
|
+
let decoded;
|
|
260
|
+
try { decoded = decodeURIComponent(target.split(/[?#]/)[0]); } catch { fail("note-links", "Una nota contiene un enlace no resolvible.", "blocked"); }
|
|
261
|
+
const relative = path.relative(vault, path.resolve(vault, path.dirname(note), decoded));
|
|
262
|
+
safeJoin(vault, relative);
|
|
263
|
+
}
|
|
264
|
+
// Doctor checks Wiki links for ordinary notes; oversized notes need the same
|
|
265
|
+
// check over their full content, without following linked directories.
|
|
266
|
+
const wiki = [...body.matchAll(/\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g)];
|
|
267
|
+
if (wiki.length) {
|
|
268
|
+
const names = new Map(), pending = [vault];
|
|
269
|
+
while (pending.length) {
|
|
270
|
+
const dir = pending.pop();
|
|
271
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
272
|
+
if (entry.isSymbolicLink() || entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
273
|
+
const full = path.join(dir, entry.name);
|
|
274
|
+
if (entry.isDirectory()) pending.push(full);
|
|
275
|
+
else if (entry.isFile() && entry.name.endsWith(".md")) names.set(path.basename(entry.name, ".md"), (names.get(path.basename(entry.name, ".md")) ?? 0) + 1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const match of wiki) {
|
|
279
|
+
const target = match[1].trim().replace(/\.md$/i, "");
|
|
280
|
+
if (target.includes("/")) safeJoin(vault, `${target}.md`);
|
|
281
|
+
else if (names.get(target) !== 1) fail("note-links", "Una nota tiene enlaces Wiki rotos o ambiguos.", "blocked");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function gitRead(vault, args) {
|
|
287
|
+
try {
|
|
288
|
+
const bytes = execFileSync("git", ["--no-optional-locks", "-c", "core.fsmonitor=false", "-C", vault, ...args],
|
|
289
|
+
{ timeout: 10_000, maxBuffer: RECORD_LIMITS.git, stdio: ["ignore", "pipe", "pipe"] });
|
|
290
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
291
|
+
} catch { fail("git-error", "No se pudo inspeccionar Git de forma segura.", "conflict"); }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function gitState(vault, ignored = []) {
|
|
295
|
+
const root = gitRead(vault, ["rev-parse", "--show-toplevel"]).trim();
|
|
296
|
+
if (!sameGitRoot(root, vault)) fail("git-root", "La bóveda debe tener su propio repositorio Git.", "conflict");
|
|
297
|
+
const head = gitRead(vault, ["rev-parse", "HEAD"]).trim();
|
|
298
|
+
const raw = gitRead(vault, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
|
|
299
|
+
const changes = [];
|
|
300
|
+
const items = raw.split("\0");
|
|
301
|
+
for (let index = 0; index < items.length; index++) {
|
|
302
|
+
const item = items[index];
|
|
303
|
+
if (!item) continue;
|
|
304
|
+
const state = item.slice(0, 2), name = item.slice(3);
|
|
305
|
+
if (/[RC]/.test(state)) index++;
|
|
306
|
+
if (!ignored.includes(name)) changes.push({ state, path: name });
|
|
307
|
+
}
|
|
308
|
+
const indexPath = path.resolve(vault, gitRead(vault, ["rev-parse", "--git-path", "index"]).trim());
|
|
309
|
+
const index = readRegular(indexPath, RECORD_LIMITS.git, true);
|
|
310
|
+
return { head, changes, index: { hash: index.hash, identity: index.identity } };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function requireTrackedLedger(vault) {
|
|
314
|
+
const entries = gitRead(vault, ["ls-files", "--stage", "-z", "--", LEDGER_PATH]).split("\0").filter(Boolean);
|
|
315
|
+
if (entries.length !== 1 || !/^100(?:644|755) [a-f0-9]+ 0\t/.test(entries[0])) fail("untracked-ledger", "El registro debe estar versionado y sin conflictos.", "conflict");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function validateTrackedFlags(vault) {
|
|
319
|
+
const entries = gitRead(vault, ["ls-files", "-v", "-z"]).split("\0").filter(Boolean);
|
|
320
|
+
if (entries.some((entry) => entry[0] === "S" || /[a-z]/.test(entry[0]))) {
|
|
321
|
+
fail("hidden-git-changes", "Hay archivos con assume-unchanged o skip-worktree; no se puede comprobar la limpieza.", "conflict");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function validateChanges(changes, input, unchanged, vault, content, existing, repository) {
|
|
326
|
+
for (const change of changes) {
|
|
327
|
+
if (unchanged && change.path === LEDGER_PATH && /^[ M]{2}$/.test(change.state)) {
|
|
328
|
+
const base = gitRead(vault, ["show", `HEAD:${LEDGER_PATH}`]);
|
|
329
|
+
let parsed;
|
|
330
|
+
try { parsed = parseWritableLedger(base, repository); } catch { fail("dirty-ledger", "El registro tiene cambios locales ajenos.", "conflict"); }
|
|
331
|
+
if (parsed.records.has(input.pr) || !sameSyncText(appendLedgerRecord(parsed, {
|
|
332
|
+
repository, pr: input.pr, date: existing.date, result: existing.result
|
|
333
|
+
}), content)) fail("dirty-ledger", "El registro tiene cambios locales ajenos.", "conflict");
|
|
334
|
+
const staged = gitRead(vault, ["show", `:${LEDGER_PATH}`]);
|
|
335
|
+
if (staged !== base && !sameSyncText(staged, content)) fail("dirty-ledger", "El registro tiene cambios ajenos en staging.", "conflict");
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (!input.notes.includes(change.path) || ![" M", "M ", "MM", "A ", "AM", "??"].includes(change.state)) {
|
|
339
|
+
fail("foreign-changes", "Hay cambios locales ajenos o incompatibles; no se registrará el PR.", "conflict");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (!unchanged && input.notes.some((note) => !changes.some((item) => item.path === note))) fail("unchanged-note", "Una nota declarada no tiene un cambio real.", "conflict");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function requireEvidence(report, acknowledged) {
|
|
346
|
+
const summary = report.checks.summary;
|
|
347
|
+
const codes = [...new Set(report.warnings.map((warning) => warning.code))];
|
|
348
|
+
if (report.status === "blocked" || !report.coverage.files.complete || !report.coverage.checks.complete
|
|
349
|
+
|| !report.checks.complete || summary.success < 1 || summary.adverse || summary.pending || summary.other
|
|
350
|
+
|| codes.some((code) => !ACK_CODES.has(code) || !acknowledged.includes(code))) {
|
|
351
|
+
fail("evidence-blocked", "La evidencia está incompleta, tiene fallos o necesita revisión explícita de advertencias admitidas.", "blocked");
|
|
352
|
+
}
|
|
353
|
+
if (acknowledged.some((code) => !codes.includes(code))) fail("invalid-option", "Se declaró una advertencia que no apareció en la inspección.");
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function validateDiffs(vault, notes, changes) {
|
|
357
|
+
for (const note of notes) {
|
|
358
|
+
const change = changes.find((item) => item.path === note);
|
|
359
|
+
if (!change || change.state === "??") continue;
|
|
360
|
+
for (const staged of [false, true]) {
|
|
361
|
+
const args = ["diff", "--no-ext-diff", "--no-textconv", ...(staged ? ["--cached"] : [])];
|
|
362
|
+
const diff = gitRead(vault, [...args, "--", note]);
|
|
363
|
+
if (containsSensitiveData(diff)) fail("sensitive-diff", "El diff de una nota contiene patrones sensibles.", "blocked");
|
|
364
|
+
try { gitRead(vault, [...args, "--check", "--", note]); }
|
|
365
|
+
catch { fail("note-format", "El diff de una nota contiene errores de formato.", "blocked"); }
|
|
366
|
+
}
|
|
367
|
+
if (change.state[0] !== " ") {
|
|
368
|
+
const entry = gitRead(vault, ["ls-files", "--stage", "-z", "--", note]).split("\0").filter(Boolean);
|
|
369
|
+
if (entry.length !== 1 || !/^100(?:644|755) [a-f0-9]+ 0\t/.test(entry[0])) fail("staged-type", "Una nota staged no es un archivo regular sin conflictos.", "conflict");
|
|
370
|
+
const staged = gitRead(vault, ["show", `:${note}`]);
|
|
371
|
+
validateNote(staged, vault, note);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function captureState(vault, files, ignored = []) {
|
|
377
|
+
assertNoSymlinks(vault);
|
|
378
|
+
const root = fs.lstatSync(vault);
|
|
379
|
+
return { root: [root.dev, root.ino], git: gitState(vault, ignored),
|
|
380
|
+
files: files.map((file) => { const item = readRegular(file, RECORD_LIMITS.git, true); return { path: file, hash: item.hash, identity: item.identity }; }) };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function assertSnapshot(before, after) {
|
|
384
|
+
if (JSON.stringify(before) !== JSON.stringify(after)) fail("concurrent-change", "La memoria, el manifiesto o Git cambiaron; vuelve a revisar el plan.", "conflict");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function applyRecord(plan, context) {
|
|
388
|
+
const { vault, files, before, ledgerFile, proposal, report } = plan;
|
|
389
|
+
const parent = path.dirname(ledgerFile.path);
|
|
390
|
+
const lockPath = syncWriteLockPath(vault);
|
|
391
|
+
const temporary = path.join(parent, `.project-memory-sync-record-${randomUUID()}.tmp`);
|
|
392
|
+
let lockFd, tempFd, lockIdentity, tempIdentity, wrote = false, primary, cleanupFailed = false;
|
|
393
|
+
const io = context.fileOperations ?? fs;
|
|
394
|
+
const ignored = [lockPath, temporary].map((file) => relativeGitPath(vault, file));
|
|
395
|
+
try {
|
|
396
|
+
assertNoSymlinks(parent);
|
|
397
|
+
try { lockFd = fs.openSync(lockPath, "wx", 0o600); lockIdentity = fs.fstatSync(lockFd); }
|
|
398
|
+
catch { fail("record-locked", "Otra ejecución o un bloqueo pendiente impide registrar; no se borrará.", "conflict"); }
|
|
399
|
+
fs.writeSync(lockFd, `${randomUUID()}\n`);
|
|
400
|
+
// The exclusive pathname is the cooperative lock. Closing its handle also
|
|
401
|
+
// lets Windows expose replacements so inode checks can preserve foreign locks.
|
|
402
|
+
fs.closeSync(lockFd); lockFd = undefined;
|
|
403
|
+
assertSnapshot(before, captureState(vault, files, ignored));
|
|
404
|
+
tempFd = fs.openSync(temporary, "wx", ledgerFile.mode);
|
|
405
|
+
tempIdentity = fs.fstatSync(tempFd);
|
|
406
|
+
io.writeFileSync(tempFd, proposal, "utf8");
|
|
407
|
+
fs.fchmodSync(tempFd, ledgerFile.mode);
|
|
408
|
+
fs.fsyncSync(tempFd);
|
|
409
|
+
fs.closeSync(tempFd); tempFd = undefined;
|
|
410
|
+
assertSnapshot(before, captureState(vault, files, ignored));
|
|
411
|
+
assertOwned(lockPath, lockIdentity);
|
|
412
|
+
assertOwned(temporary, tempIdentity);
|
|
413
|
+
if (readRegular(temporary, RECORD_LIMITS.ledger).hash !== digest(Buffer.from(proposal))) fail("artifact-conflict", "El archivo temporal cambió; no se aplicará.", "conflict");
|
|
414
|
+
// Narrow final read-to-rename window; independent editors must still pause.
|
|
415
|
+
io.renameSync(temporary, ledgerFile.path);
|
|
416
|
+
wrote = true;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
primary = error;
|
|
419
|
+
// An adapter can report an error after a completed rename. Do not claim
|
|
420
|
+
// that nothing was written or roll back a file already installed.
|
|
421
|
+
if (tempIdentity) {
|
|
422
|
+
try {
|
|
423
|
+
const current = fs.lstatSync(ledgerFile.path);
|
|
424
|
+
wrote = current.dev === tempIdentity.dev && current.ino === tempIdentity.ino
|
|
425
|
+
&& readRegular(ledgerFile.path, RECORD_LIMITS.ledger).hash === digest(Buffer.from(proposal));
|
|
426
|
+
} catch { /* Cannot establish an applied replacement. */ }
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
for (const fd of [tempFd, lockFd]) if (fd !== undefined) { try { fs.closeSync(fd); } catch { cleanupFailed = true; } }
|
|
431
|
+
for (const [file, identity] of [[temporary, tempIdentity], [lockPath, lockIdentity]]) {
|
|
432
|
+
if (!identity || wrote && file === temporary) continue;
|
|
433
|
+
try { assertNoSymlinks(parent); assertOwned(file, identity); io.unlinkSync(file); }
|
|
434
|
+
catch { cleanupFailed = true; }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (wrote) return { ...report, status: cleanupFailed || primary ? "error" : "recorded", wrote: true,
|
|
438
|
+
...(cleanupFailed || primary ? { code: cleanupFailed ? "cleanup-error" : "write-confirmation-error",
|
|
439
|
+
details: ["La fila se guardó, pero la confirmación o limpieza necesita revisión. No se revirtió."] } : {}) };
|
|
440
|
+
if (cleanupFailed) fail("cleanup-error", "No se guardó la fila y quedaron artefactos de esta ejecución; revisa el bloqueo.");
|
|
441
|
+
if (primary instanceof SyncRecordError) throw primary;
|
|
442
|
+
fail("write-error", "No se guardó el registro; el archivo original se conservó.");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function assertOwned(file, identity) {
|
|
446
|
+
const stat = fs.lstatSync(file);
|
|
447
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.dev !== identity.dev || stat.ino !== identity.ino) fail("artifact-conflict", "Un artefacto de la operación fue sustituido; no se tocará.", "conflict");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function digest(value) { return createHash("sha256").update(value).digest("hex"); }
|
|
451
|
+
|
|
452
|
+
export function formatSyncRecordReport(report) {
|
|
453
|
+
return [`Project Memory Sync Record: ${report.status.toUpperCase()}`,
|
|
454
|
+
...(report.repository ? [`Repositorio: ${report.repository}; rama: ${report.branch}`, `PR #${report.pr}; integración: ${report.mergeCommit}`,
|
|
455
|
+
`Registro: ${report.ledgerPath}`, `Resultado: ${report.result}`, `Fecha UTC: ${report.date}`] : []),
|
|
456
|
+
...(report.details ?? []), `Escritura: ${report.wrote ? "sí" : "no"}`,
|
|
457
|
+
...(report.status === "preview" ? ["Vista previa: --apply añade únicamente la fila del registro."] : []),
|
|
458
|
+
RECORD_CONCURRENCY_NOTICE, "La declaración --reviewed no certifica la comprensión ni la corrección del código. No se crean commits ni se hace push."].join("\n");
|
|
459
|
+
}
|