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,67 @@
|
|
|
1
|
+
import { createClientRuntimeAdmission } from "./client-runtime-admission.js";
|
|
2
|
+
import { validators, copyInertJson, ParseFailure, fail, hash, same, fresh, ledgerFacts, recalculate } from "./client-runtime-core.js";
|
|
3
|
+
|
|
4
|
+
/** Calculate CAS proposals only. No mutable store, locks, clock or processes. */
|
|
5
|
+
export function createClientRuntimeState(base, preference, adapter, runtime) {
|
|
6
|
+
const validate = validators(base, preference, adapter, runtime);
|
|
7
|
+
const admit = createClientRuntimeAdmission(base, preference, adapter, runtime);
|
|
8
|
+
return function transition(input) {
|
|
9
|
+
const r = { schemaVersion: "1.0.0", engineVersion: "1.0.0", state: "invalid", executed: false, runtimeReady: false,
|
|
10
|
+
verifiedAdmission: false, persisted: false, operation: null, nextLedger: null, compareAndSwap: null, reasons: [] };
|
|
11
|
+
function finish(state, code) {
|
|
12
|
+
r.state = state; r.reasons = [code];
|
|
13
|
+
if (state !== "proposed") { r.nextLedger = null; r.compareAndSwap = null; }
|
|
14
|
+
if (!validate.stateReport(r)) throw Error("Runtime state invariant failed"); return r;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const d = copyInertJson(input); if (!validate.stateInput(d)) fail("invalid-runtime-transition");
|
|
18
|
+
r.operation = d.operation; const beforeHash = hash(d.ledger);
|
|
19
|
+
if (d.expectedRevision !== d.ledger.revision || d.expectedLedgerSha256 !== beforeHash) return finish("blocked", "runtime-snapshot-conflict");
|
|
20
|
+
ledgerFacts(d.ledger, d.now);
|
|
21
|
+
if (d.ledger.revision >= 1_000_000) return finish("blocked", "runtime-revision-limit");
|
|
22
|
+
const next = copyInertJson(d.ledger); let a = next.attempts.find(a => a.attemptId === d.attemptId);
|
|
23
|
+
if (d.operation === "reserve") {
|
|
24
|
+
if (a) return finish("blocked", "runtime-attempt-already-exists");
|
|
25
|
+
if (!same(d.admissionInput.ledger, d.ledger) || d.admissionInput.now !== d.now) return finish("blocked", "runtime-admission-snapshot-conflict");
|
|
26
|
+
const admission = admit(d.admissionInput);
|
|
27
|
+
if (admission.state !== "eligible-for-reservation") return finish(admission.state === "invalid" ? "invalid" : "blocked", "runtime-reservation-not-eligible");
|
|
28
|
+
const v = admission.reservation;
|
|
29
|
+
a = { attemptId: d.attemptId, identity: v.identity, planSha256: v.planSha256, client: v.client,
|
|
30
|
+
profileId: v.profileId, model: v.model, effort: v.effort, state: "reserved", startObservation: "not-reported",
|
|
31
|
+
reservedCalls: v.calls, reservedSeconds: v.seconds, reservedAt: d.now, updatedAt: d.now,
|
|
32
|
+
actualCalls: null, actualSeconds: null, outcome: null, evidenceRef: null };
|
|
33
|
+
next.attempts.push(a);
|
|
34
|
+
} else {
|
|
35
|
+
if (!a) return finish("blocked", "runtime-attempt-not-found");
|
|
36
|
+
if (a.state === "settled") return finish("blocked", "runtime-attempt-already-settled");
|
|
37
|
+
if (Date.parse(d.now) < Date.parse(a.updatedAt)) return finish("blocked", "runtime-observation-before-attempt");
|
|
38
|
+
if (d.operation === "mark-started") {
|
|
39
|
+
if (a.state !== "reserved") return finish("blocked", "runtime-start-observation-conflict");
|
|
40
|
+
a.state = "started"; a.startObservation = "started";
|
|
41
|
+
} else if (d.operation === "interrupt") {
|
|
42
|
+
if (a.state === "reconciliation-required") return finish("blocked", "runtime-attempt-already-interrupted");
|
|
43
|
+
a.state = "reconciliation-required"; a.startObservation = a.startObservation === "started" ? "started" : "unknown"; a.outcome = "indeterminate";
|
|
44
|
+
} else {
|
|
45
|
+
if (d.reconciliation.status !== "approved" || d.reconciliation.revoked || !fresh(d.reconciliation, d.now, 3600) || !d.terminationConfirmed) return finish("blocked", "runtime-reconciliation-not-authorized-or-terminated");
|
|
46
|
+
if (d.reconciliation.projectId !== next.projectId || d.reconciliation.taskId !== next.taskId ||
|
|
47
|
+
d.reconciliation.attemptId !== a.attemptId || d.reconciliation.ledgerSha256 !== beforeHash) return finish("blocked", "runtime-reconciliation-binding-mismatch");
|
|
48
|
+
if (a.startObservation === "started" && d.startObservation !== "started") return finish("blocked", "runtime-start-observation-conflict");
|
|
49
|
+
if (d.startObservation === "not-started" && (d.actualCalls || d.actualSeconds || d.outcome !== "not-started")) return finish("blocked", "runtime-not-started-consumption-conflict");
|
|
50
|
+
if (d.startObservation === "started" && d.outcome === "not-started") return finish("blocked", "runtime-started-outcome-conflict");
|
|
51
|
+
Object.assign(a, { state: "settled", startObservation: d.startObservation, actualCalls: d.actualCalls,
|
|
52
|
+
actualSeconds: d.actualSeconds, outcome: d.outcome, evidenceRef: d.reconciliation.evidenceRef });
|
|
53
|
+
}
|
|
54
|
+
a.updatedAt = d.now;
|
|
55
|
+
}
|
|
56
|
+
next.revision++; recalculate(next);
|
|
57
|
+
if (!validate.ledger(next)) return finish("blocked", "runtime-ledger-counter-or-capacity-limit");
|
|
58
|
+
ledgerFacts(next, d.now);
|
|
59
|
+
r.nextLedger = next; r.compareAndSwap = { expectedRevision: d.expectedRevision, nextRevision: next.revision,
|
|
60
|
+
expectedSha256: beforeHash, nextSha256: hash(next) };
|
|
61
|
+
return finish("proposed", next.status === "reconciliation-required" ? "declared-transition-reconciliation-required" : "declared-transition-only");
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!(error instanceof ParseFailure)) throw error;
|
|
64
|
+
return finish("invalid", error.code);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { ParseFailure, fail } from "./client-output-json.js";
|
|
6
|
+
|
|
7
|
+
export const FILE_LIMIT = 1024 * 1024;
|
|
8
|
+
export const bytesHash = bytes => createHash("sha256").update(bytes).digest("hex");
|
|
9
|
+
const missing = e => e?.code === "ENOENT";
|
|
10
|
+
export { missing };
|
|
11
|
+
|
|
12
|
+
// Local cooperative writers only. These checks are not openat-style containment.
|
|
13
|
+
export function createStoreFiles(overrides = {}) {
|
|
14
|
+
const io = { ...fs, ...overrides };
|
|
15
|
+
const uid = process.getuid?.() ?? null;
|
|
16
|
+
// Directory link counts change when our own task directory is created.
|
|
17
|
+
// File link count is independently required to remain one by privateType.
|
|
18
|
+
const identity = s => ({ dev: s.dev, ino: s.ino, uid: s.uid, mode: s.mode });
|
|
19
|
+
const equal = (a, b) => JSON.stringify(identity(a)) === JSON.stringify(identity(b));
|
|
20
|
+
function privateType(s, directory) {
|
|
21
|
+
if (s.uid !== uid || (s.mode & 0o7777) !== (directory ? 0o700 : 0o600) ||
|
|
22
|
+
(directory ? !s.isDirectory() : !s.isFile() || s.nlink !== 1)) fail("store-unsafe-resource");
|
|
23
|
+
}
|
|
24
|
+
async function physical(full) {
|
|
25
|
+
let current = path.parse(full).root;
|
|
26
|
+
for (const part of full.slice(current.length).split(path.sep).filter(Boolean)) {
|
|
27
|
+
current = path.join(current, part); const s = await io.lstat(current);
|
|
28
|
+
if (s.isSymbolicLink()) fail("store-symlink-path");
|
|
29
|
+
if (current !== full && !s.isDirectory()) fail("store-invalid-parent");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function directory(full, expected) {
|
|
33
|
+
await physical(full); const s = await io.lstat(full); privateType(s, true);
|
|
34
|
+
if (expected && !equal(s, expected)) fail("store-directory-replaced");
|
|
35
|
+
return s;
|
|
36
|
+
}
|
|
37
|
+
async function syncDirectory(full, expected) {
|
|
38
|
+
await directory(full, expected);
|
|
39
|
+
const h = await io.open(full, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
40
|
+
try { const s = await h.stat(); privateType(s, true); if (!equal(s, expected)) fail("store-directory-replaced"); await h.sync(); }
|
|
41
|
+
finally { await h.close(); }
|
|
42
|
+
await directory(full, expected);
|
|
43
|
+
}
|
|
44
|
+
async function read(full, limit = FILE_LIMIT) {
|
|
45
|
+
await physical(full); const before = await io.lstat(full); privateType(before, false);
|
|
46
|
+
if (before.size > limit) fail("store-file-limit");
|
|
47
|
+
const h = await io.open(full, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
48
|
+
try {
|
|
49
|
+
const opened = await h.stat(); privateType(opened, false);
|
|
50
|
+
if (!equal(before, opened) || opened.size > limit) fail("store-resource-changed");
|
|
51
|
+
const buffer = Buffer.alloc(limit + 1); let offset = 0;
|
|
52
|
+
while (offset < buffer.length) { const { bytesRead } = await h.read(buffer, offset, buffer.length - offset, offset); if (!bytesRead) break; offset += bytesRead; }
|
|
53
|
+
if (offset > limit) fail("store-file-limit");
|
|
54
|
+
const after = await h.stat(); const named = await io.lstat(full);
|
|
55
|
+
privateType(after, false); privateType(named, false);
|
|
56
|
+
if (!equal(opened, after) || !equal(after, named) || after.size !== offset || opened.size !== after.size || opened.mtimeMs !== after.mtimeMs) fail("store-resource-changed");
|
|
57
|
+
return { bytes: buffer.subarray(0, offset), stat: after };
|
|
58
|
+
} finally { await h.close(); }
|
|
59
|
+
}
|
|
60
|
+
async function exclusive(full) {
|
|
61
|
+
let h;
|
|
62
|
+
try { h = await io.open(full, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600); }
|
|
63
|
+
catch (e) { if (e?.code === "EEXIST") fail("store-resource-exists"); throw e; }
|
|
64
|
+
try { const stat = await h.stat(); privateType(stat, false); return { handle: h, stat, full, digest: null }; }
|
|
65
|
+
catch { await h.close(); fail("store-created-resource-unusable"); }
|
|
66
|
+
}
|
|
67
|
+
async function write(resource, bytes) {
|
|
68
|
+
let offset = 0;
|
|
69
|
+
while (offset < bytes.length) {
|
|
70
|
+
const { bytesWritten } = await resource.handle.write(bytes, offset, bytes.length - offset, offset);
|
|
71
|
+
if (!bytesWritten) fail("store-short-write"); offset += bytesWritten;
|
|
72
|
+
}
|
|
73
|
+
resource.digest = bytesHash(bytes); await resource.handle.sync();
|
|
74
|
+
}
|
|
75
|
+
async function owned(resource) {
|
|
76
|
+
const r = await read(resource.full); if (!equal(r.stat, resource.stat)) fail("store-resource-replaced");
|
|
77
|
+
if (resource.digest !== null && bytesHash(r.bytes) !== resource.digest) fail("store-resource-replaced");
|
|
78
|
+
return r;
|
|
79
|
+
}
|
|
80
|
+
async function remove(resource, dir, expected) {
|
|
81
|
+
await directory(dir, expected); await owned(resource); await io.unlink(resource.full);
|
|
82
|
+
}
|
|
83
|
+
async function contents(dir, allowed) {
|
|
84
|
+
const h = await io.opendir(dir);
|
|
85
|
+
try { let n = 0; for (;;) { const e = await h.read(); if (!e) break; if (++n > allowed.length || !allowed.includes(e.name)) fail("store-residual-resource"); } }
|
|
86
|
+
finally { await h.close(); }
|
|
87
|
+
}
|
|
88
|
+
return { io, uid, equal, physical, directory, syncDirectory, read, exclusive, write, owned, remove, contents,
|
|
89
|
+
async exists(full) { try { return await io.lstat(full); } catch (e) { if (missing(e)) return null; throw e; } },
|
|
90
|
+
code(e) { return e instanceof ParseFailure ? e.code : "store-io-failed"; } };
|
|
91
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { supportsLocalPosix } from "./client-local-platform.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { createClientRuntimeState } from "./client-runtime-state.js";
|
|
8
|
+
import { copyInertJson, ParseFailure, fail, canonical, hash, same, fresh, ledgerFacts } from "./client-runtime-core.js";
|
|
9
|
+
import { decode, parseEventJson } from "./client-output-json.js";
|
|
10
|
+
import { createStoreFiles, bytesHash, FILE_LIMIT, missing } from "./client-runtime-store-files.js";
|
|
11
|
+
import { createFixturePermit, revokeFixturePermit } from "./client-runtime-fixture-permit.js";
|
|
12
|
+
|
|
13
|
+
export const LOCAL_STORE_ID = "https://agent-toolkit.local/schemas/agent-client-runtime-local.schema.json";
|
|
14
|
+
const toolkit = fileURLToPath(new URL("../", import.meta.url)).replace(/\/$/, "");
|
|
15
|
+
const contained = (child, parent) => child === parent || child.startsWith(parent + path.sep);
|
|
16
|
+
const validPath = p => path.isAbsolute(p) && path.normalize(p) === p && !p.includes("\0");
|
|
17
|
+
const utc = value => {
|
|
18
|
+
const t = Date.parse(value);
|
|
19
|
+
return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value) && Number.isFinite(t) && new Date(t).toISOString() === value.replace(/Z$/, ".000Z");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Trusted factory configuration/dependencies, inert JSON operations. No client launcher. */
|
|
23
|
+
export function createClientRuntimeStore(base, preference, adapter, runtime, local, configuration, dependencies = {}) {
|
|
24
|
+
const schemas = [base, preference, adapter, runtime, local].map(copyInertJson);
|
|
25
|
+
if (schemas[4].$id !== LOCAL_STORE_ID) throw Error("Unsupported local runtime schema");
|
|
26
|
+
const ajv = new Ajv2020({ strict: true, allErrors: true }); ajv.addFormat("date-time", { type: "string", validate: utc });
|
|
27
|
+
schemas.forEach(s => ajv.addSchema(s));
|
|
28
|
+
const check = Object.fromEntries(["configuration", "request", "document", "report"].map(n => [n, ajv.compile({ $ref: `${LOCAL_STORE_ID}#/$defs/${n}` })]));
|
|
29
|
+
const cfg = copyInertJson(configuration); if (!check.configuration(cfg)) throw Error("Invalid local runtime configuration");
|
|
30
|
+
const transition = createClientRuntimeState(...schemas.slice(0, 4));
|
|
31
|
+
const files = createStoreFiles(dependencies.io); const clock = dependencies.clock ?? Date.now;
|
|
32
|
+
const checkpoint = dependencies.checkpoint ?? (() => {});
|
|
33
|
+
|
|
34
|
+
async function execute(input, sessionConsumer, sessionGate) {
|
|
35
|
+
const r = { schemaVersion: "1.0.0", engineVersion: "1.0.0", scope: "cooperative-local-data-only", state: "blocked",
|
|
36
|
+
executed: false, nativeExecuted: false, runtimeReady: false, verifiedAdmission: false, writeObservation: "unchanged",
|
|
37
|
+
durability: "unconfirmed", cleanup: "complete", identity: null, snapshot: null, snapshotSha256: null, reasons: [] };
|
|
38
|
+
let root, rootStat, dir, dirStat, lock, temp, before = null, after = null, target, publishing = false, published = false;
|
|
39
|
+
let keep = false;
|
|
40
|
+
const sessionResources = [];
|
|
41
|
+
const phase = name => checkpoint(name);
|
|
42
|
+
function now() { const ms = clock(); if (typeof ms !== "number" || !Number.isFinite(ms)) fail("store-clock-unavailable"); const date = new Date(ms); if (!Number.isFinite(date.getTime())) fail("store-clock-unavailable"); return date.toISOString().replace(/\.\d{3}Z$/, "Z"); }
|
|
43
|
+
const document = ledger => ({ storageVersion: "1.0.0", identity: r.identity, ledgerSha256: hash(ledger), ledger });
|
|
44
|
+
function inspect(bytes, time) {
|
|
45
|
+
const d = copyInertJson(parseEventJson(decode(bytes)));
|
|
46
|
+
if (!check.document(d) || !same(d.identity, r.identity) || !same({ projectId: d.ledger.projectId, taskId: d.ledger.taskId }, r.identity) || d.ledgerSha256 !== hash(d.ledger)) fail("store-invalid-document");
|
|
47
|
+
ledgerFacts(d.ledger, time); return d;
|
|
48
|
+
}
|
|
49
|
+
async function roots() {
|
|
50
|
+
if (!validPath(cfg.root) || cfg.root === path.parse(cfg.root).root || !cfg.forbiddenRoots.every(validPath)) fail("store-invalid-root");
|
|
51
|
+
await files.physical(cfg.root); root = await files.io.realpath(cfg.root); rootStat = await files.directory(root);
|
|
52
|
+
const forbidden = [toolkit, ...cfg.forbiddenRoots];
|
|
53
|
+
for (const p of forbidden) {
|
|
54
|
+
let resolved = p; try { resolved = await files.io.realpath(p); } catch (e) { if (!missing(e)) throw e; }
|
|
55
|
+
if (contained(root, resolved) || contained(resolved, root)) fail("store-forbidden-root");
|
|
56
|
+
}
|
|
57
|
+
// Refuse roots within an existing repository or Obsidian vault without invoking Git.
|
|
58
|
+
for (let p = root; p !== path.parse(p).root; p = path.dirname(p)) {
|
|
59
|
+
if (await files.exists(path.join(p, ".git")) || await files.exists(path.join(p, ".obsidian"))) fail("store-forbidden-root");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function permission(d, time) {
|
|
63
|
+
const a = d.authorization;
|
|
64
|
+
if (a.status !== "approved" || a.revoked || !a.operations.includes(d.operation) || !fresh(a, time, 3600) ||
|
|
65
|
+
!same(a.identity, d.identity) || a.rootSha256 !== hash(root)) fail("store-not-authorized");
|
|
66
|
+
}
|
|
67
|
+
async function fence() {
|
|
68
|
+
await files.directory(root, rootStat); await files.directory(dir, dirStat); await files.owned(lock);
|
|
69
|
+
}
|
|
70
|
+
async function current(time) { const read = await files.read(target); return { ...read, data: inspect(read.bytes, time) }; }
|
|
71
|
+
async function unchanged(time) {
|
|
72
|
+
if (before === null) { if (await files.exists(target)) fail("store-snapshot-conflict"); }
|
|
73
|
+
else { const read = await current(time); if (!files.equal(before.stat, read.stat) || bytesHash(before.bytes) !== bytesHash(read.bytes)) fail("store-snapshot-conflict"); }
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const d = copyInertJson(input); if (!check.request(d)) fail("store-invalid-request"); r.identity = d.identity;
|
|
77
|
+
if (!supportsLocalPosix(process.platform) || cfg.evaluation.platform !== process.platform || cfg.evaluation.nodeVersion !== process.version ||
|
|
78
|
+
!constants.O_NOFOLLOW || !constants.O_DIRECTORY) fail("store-platform-not-evaluated");
|
|
79
|
+
let time = now(); await roots(); permission(d, time);
|
|
80
|
+
if (d.operation === "initialize") {
|
|
81
|
+
const imported = inspect(Buffer.from(canonical(document(d.ledger))), time);
|
|
82
|
+
const facts = ledgerFacts(imported.ledger, time);
|
|
83
|
+
if (d.importReferenceId !== imported.ledger.importEvidenceRef || imported.ledger.status !== "reconciled" || facts.active || facts.uncertain || facts.overrun) fail("store-import-not-reconciled");
|
|
84
|
+
} else if (d.operation === "apply" && !same({ projectId: d.transition.ledger.projectId, taskId: d.transition.ledger.taskId }, d.identity)) fail("store-transition-identity-mismatch");
|
|
85
|
+
dir = path.join(root, hash(d.identity)); target = path.join(dir, "ledger.json");
|
|
86
|
+
if (d.operation === "initialize") {
|
|
87
|
+
try { await files.io.mkdir(dir, { mode: 0o700 }); await files.syncDirectory(root, rootStat); }
|
|
88
|
+
catch (e) { if (e?.code !== "EEXIST") throw e; }
|
|
89
|
+
}
|
|
90
|
+
try { dirStat = await files.directory(dir); } catch (e) { if (missing(e)) fail("store-ledger-missing"); throw e; }
|
|
91
|
+
await files.contents(dir, ["ledger.json", "write.lock"]);
|
|
92
|
+
try { lock = await files.exclusive(path.join(dir, "write.lock")); }
|
|
93
|
+
catch (e) { if (e instanceof ParseFailure && e.code === "store-resource-exists") fail("store-locked"); throw e; }
|
|
94
|
+
await phase("lock-created"); await files.write(lock, Buffer.from(canonical({ protocolVersion: "1.0.0", ownerId: randomUUID() }) + "\n"));
|
|
95
|
+
await files.syncDirectory(dir, dirStat); await phase("lock-durable");
|
|
96
|
+
await fence(); time = now(); permission(d, time);
|
|
97
|
+
if (await files.exists(target)) before = await current(time);
|
|
98
|
+
await phase("snapshot-read");
|
|
99
|
+
if (d.operation === "initialize") {
|
|
100
|
+
if (before !== null) fail("store-already-initialized"); after = document(d.ledger);
|
|
101
|
+
} else {
|
|
102
|
+
if (before === null) fail("store-ledger-missing");
|
|
103
|
+
if (d.operation === "read") { r.state = "unchanged"; r.snapshot = before.data.ledger; r.snapshotSha256 = before.data.ledgerSha256; r.reasons = ["store-snapshot-read"]; }
|
|
104
|
+
else {
|
|
105
|
+
const t = copyInertJson(d.transition);
|
|
106
|
+
if (!same(t.ledger, before.data.ledger) || t.expectedRevision !== before.data.ledger.revision || t.expectedLedgerSha256 !== before.data.ledgerSha256) fail("store-snapshot-conflict");
|
|
107
|
+
t.now = time;
|
|
108
|
+
if (t.operation === "reserve") { t.admissionInput.now = time; t.admissionInput.ledger = copyInertJson(before.data.ledger); }
|
|
109
|
+
const proposal = transition(t); if (proposal.state !== "proposed") fail("store-transition-blocked");
|
|
110
|
+
after = document(proposal.nextLedger);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (after) {
|
|
114
|
+
const bytes = Buffer.from(canonical(after) + "\n"); if (bytes.length > FILE_LIMIT) fail("store-file-limit");
|
|
115
|
+
inspect(bytes, time);
|
|
116
|
+
temp = await files.exclusive(path.join(dir, `.ledger-${randomUUID()}.tmp`)); await phase("temp-created");
|
|
117
|
+
await files.write(temp, bytes); await phase("temp-synced");
|
|
118
|
+
const readback = await files.owned(temp); if (!same(inspect(readback.bytes, time), after)) fail("store-invalid-temporary");
|
|
119
|
+
await phase("before-publish"); time = now(); permission(d, time);
|
|
120
|
+
// Recheck temporal admission at publication, without refreshing evidence stamps.
|
|
121
|
+
if (d.operation === "apply") {
|
|
122
|
+
const t = copyInertJson(d.transition); t.now = time;
|
|
123
|
+
if (t.operation === "reserve") t.admissionInput.now = time;
|
|
124
|
+
const again = transition(t); if (again.state !== "proposed") fail("store-transition-blocked");
|
|
125
|
+
if (!same(again.nextLedger, after.ledger)) {
|
|
126
|
+
// Time-sensitive proposals must be regenerated, not silently installed stale.
|
|
127
|
+
fail("store-publication-time-changed");
|
|
128
|
+
}
|
|
129
|
+
} else inspect(bytes, time);
|
|
130
|
+
await fence(); await files.owned(temp); await unchanged(time);
|
|
131
|
+
publishing = true; await files.io.rename(temp.full, target); published = true; r.writeObservation = "replaced";
|
|
132
|
+
await phase("published"); await files.syncDirectory(dir, dirStat); r.durability = "confirmed";
|
|
133
|
+
await phase("directory-synced"); await fence(); const final = await current(now());
|
|
134
|
+
if (!same(final.data, after) || bytesHash(final.bytes) !== temp.digest) fail("store-publication-mismatch");
|
|
135
|
+
await phase("confirmed"); r.state = "committed"; r.snapshot = final.data.ledger; r.snapshotSha256 = final.data.ledgerSha256; r.reasons = ["store-transition-committed"];
|
|
136
|
+
if (sessionConsumer) {
|
|
137
|
+
// Internal session: lock remains held. A public committed JSON cannot enter here.
|
|
138
|
+
let active = true;
|
|
139
|
+
const pending = new Set();
|
|
140
|
+
const attemptId = d.transition.attemptId;
|
|
141
|
+
const getSnapshot = () => copyInertJson(r.snapshot);
|
|
142
|
+
const guard = async () => {
|
|
143
|
+
if (!active) fail("store-session-closed");
|
|
144
|
+
const time = now(); permission(d, time); await sessionGate(time);
|
|
145
|
+
await fence(); const observed = await current(time);
|
|
146
|
+
if (!same(observed.data.ledger, r.snapshot)) fail("store-snapshot-conflict");
|
|
147
|
+
};
|
|
148
|
+
const performSession = async value => {
|
|
149
|
+
await guard();
|
|
150
|
+
const t = copyInertJson(value);
|
|
151
|
+
if (!["mark-started", "interrupt", "settle"].includes(t.operation) || t.attemptId !== attemptId ||
|
|
152
|
+
!check.request({ ...d, transition: t }) || !same(t.ledger, r.snapshot) ||
|
|
153
|
+
t.expectedRevision !== r.snapshot.revision || t.expectedLedgerSha256 !== hash(r.snapshot)) fail("store-session-transition-invalid");
|
|
154
|
+
let time = now(); t.now = time;
|
|
155
|
+
const proposed = transition(t); if (proposed.state !== "proposed") fail("store-transition-blocked");
|
|
156
|
+
const previous = await current(time); after = document(proposed.nextLedger);
|
|
157
|
+
const bytes = Buffer.from(canonical(after) + "\n"); if (bytes.length > FILE_LIMIT) fail("store-file-limit"); inspect(bytes, time);
|
|
158
|
+
const next = await files.exclusive(path.join(dir, `.ledger-${randomUUID()}.tmp`)); sessionResources.push(next);
|
|
159
|
+
await files.write(next, bytes); await phase(`session-${t.operation}-before-publish`);
|
|
160
|
+
const readback = await files.owned(next); if (!same(inspect(readback.bytes, time), after)) fail("store-invalid-temporary");
|
|
161
|
+
time = now(); t.now = time; await guard();
|
|
162
|
+
const checked = transition(t);
|
|
163
|
+
if (checked.state !== "proposed" || !same(checked.nextLedger, after.ledger)) fail("store-publication-time-changed");
|
|
164
|
+
const old = await current(time);
|
|
165
|
+
if (!files.equal(previous.stat, old.stat) || bytesHash(previous.bytes) !== bytesHash(old.bytes)) fail("store-snapshot-conflict");
|
|
166
|
+
await files.owned(next); r.durability = "unconfirmed";
|
|
167
|
+
await files.io.rename(next.full, target); await phase(`session-${t.operation}-published`);
|
|
168
|
+
await files.syncDirectory(dir, dirStat); r.durability = "confirmed"; await fence();
|
|
169
|
+
const confirmed = await current(now());
|
|
170
|
+
if (!same(confirmed.data, after) || bytesHash(confirmed.bytes) !== next.digest) fail("store-publication-mismatch");
|
|
171
|
+
r.snapshot = confirmed.data.ledger; r.snapshotSha256 = confirmed.data.ledgerSha256;
|
|
172
|
+
await phase(`session-${t.operation}-confirmed`);
|
|
173
|
+
return getSnapshot();
|
|
174
|
+
};
|
|
175
|
+
const applySession = value => {
|
|
176
|
+
const operation = performSession(value); pending.add(operation);
|
|
177
|
+
operation.then(() => pending.delete(operation), () => pending.delete(operation));
|
|
178
|
+
return operation;
|
|
179
|
+
};
|
|
180
|
+
const bindingSha256 = await sessionGate(now());
|
|
181
|
+
const handle = createFixturePermit(async () => {
|
|
182
|
+
await guard();
|
|
183
|
+
if (r.snapshot.attempts.find(a => a.attemptId === attemptId)?.state !== "reserved") fail("store-session-not-reserved");
|
|
184
|
+
}, () => applySession({ schemaVersion: "1.0.0", operation: "mark-started", now: now(), ledger: getSnapshot(),
|
|
185
|
+
expectedRevision: r.snapshot.revision, expectedLedgerSha256: r.snapshotSha256, attemptId }), bindingSha256);
|
|
186
|
+
try {
|
|
187
|
+
await phase("session-reserved");
|
|
188
|
+
await sessionConsumer(Object.freeze({ handle, snapshot: getSnapshot, apply: applySession }));
|
|
189
|
+
if (r.snapshot.attempts.find(a => a.attemptId === attemptId)?.state !== "settled") fail("store-session-reconciliation-required");
|
|
190
|
+
await guard();
|
|
191
|
+
} finally {
|
|
192
|
+
active = false; revokeFixturePermit(handle);
|
|
193
|
+
// Do not close descriptors or return while a started-observation write is still in flight.
|
|
194
|
+
await Promise.allSettled([...pending]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
const primary = files.code(e); r.reasons = [primary]; r.state = "blocked"; r.snapshot = null; r.snapshotSha256 = null;
|
|
200
|
+
if (primary === "store-created-resource-unusable") { r.state = "indeterminate"; r.cleanup = "intervention-required"; }
|
|
201
|
+
if (publishing) {
|
|
202
|
+
keep = true; r.state = "indeterminate";
|
|
203
|
+
try {
|
|
204
|
+
const observed = await current(now());
|
|
205
|
+
if (after && same(observed.data, after)) { published = true; r.writeObservation = "replaced"; }
|
|
206
|
+
else if (before && bytesHash(observed.bytes) === bytesHash(before.bytes)) r.writeObservation = "unchanged";
|
|
207
|
+
else r.writeObservation = "unknown";
|
|
208
|
+
} catch { r.writeObservation = "unknown"; }
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
try {
|
|
212
|
+
if (lock) {
|
|
213
|
+
await phase("before-cleanup");
|
|
214
|
+
await files.directory(root, rootStat); await files.directory(dir, dirStat);
|
|
215
|
+
if (!keep) {
|
|
216
|
+
if (temp && !published) await files.remove(temp, dir, dirStat);
|
|
217
|
+
await files.remove(lock, dir, dirStat); await files.syncDirectory(dir, dirStat);
|
|
218
|
+
await phase("after-cleanup");
|
|
219
|
+
} else r.cleanup = "intervention-required";
|
|
220
|
+
}
|
|
221
|
+
} catch { r.cleanup = "intervention-required"; r.state = "indeterminate"; r.reasons.push("store-cleanup-required"); r.snapshot = null; r.snapshotSha256 = null; }
|
|
222
|
+
for (const resource of [...sessionResources, temp, lock]) if (resource) {
|
|
223
|
+
try { await resource.handle.close(); } catch { r.cleanup = "intervention-required"; r.state = "indeterminate"; r.reasons.push("store-close-unconfirmed"); }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (r.state === "indeterminate" || r.state === "blocked") { r.snapshot = null; r.snapshotSha256 = null; }
|
|
227
|
+
r.reasons = [...new Set(r.reasons)];
|
|
228
|
+
if (!check.report(r)) throw Error("Local runtime store invariant failed");
|
|
229
|
+
return r;
|
|
230
|
+
}
|
|
231
|
+
const store = input => execute(input, null, null);
|
|
232
|
+
// Trusted C2.2 coordinator only. JSON operation API and all C2.1 reports stay unchanged.
|
|
233
|
+
store.withFixtureSession = async (input, consumer, gate) => {
|
|
234
|
+
const d = copyInertJson(input);
|
|
235
|
+
if (d?.operation !== "apply" || d?.transition?.operation !== "reserve" || typeof consumer !== "function" || typeof gate !== "function") throw Error("Invalid trusted fixture session");
|
|
236
|
+
return execute(d, consumer, gate);
|
|
237
|
+
};
|
|
238
|
+
return store;
|
|
239
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { supportsLocalPosix } from "./client-local-platform.js";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawn as nativeSpawn } from "node:child_process";
|
|
7
|
+
import { claimFixturePermit } from "./client-runtime-fixture-permit.js";
|
|
8
|
+
import { createFixtureProtocol } from "./client-runtime-fixture-protocol.js";
|
|
9
|
+
import { copyBytes, ParseFailure } from "./client-output-json.js";
|
|
10
|
+
import { copyInertJson, hash } from "./client-runtime-core.js";
|
|
11
|
+
|
|
12
|
+
export const FIXTURES = Object.freeze(["success", "split-utf8", "silent", "blocked-stdin", "failure", "stdout-overflow", "stderr-overflow", "long-line", "event-overflow", "invalid-utf8", "forbidden-event", "duplicate-json", "after-terminal", "unterminated", "incomplete", "ignore-term", "parent-child", "child-after-parent", "stderr-invalid", "environment", "closed-stdin"]);
|
|
13
|
+
const script = fileURLToPath(new URL("./runtime-fixtures/runtime-supervisor/fixture.cjs", import.meta.url));
|
|
14
|
+
export const monotonicMs = () => Number(process.hrtime.bigint() / 1_000_000n);
|
|
15
|
+
const reason = e => e instanceof ParseFailure ? e.code : "fixture-local-error";
|
|
16
|
+
|
|
17
|
+
/** Trusted fixtures only. Internal dependencies cannot arrive in request JSON. */
|
|
18
|
+
export async function superviseFixture(handle, request, { signal, beganAt, checkpoint = () => {}, spawn = nativeSpawn,
|
|
19
|
+
kill = process.kill.bind(process), monotonic = monotonicMs } = {}) {
|
|
20
|
+
const r = { scope: "fixture-only", state: "not-started", fixtureExecuted: false, spawnAttempts: 0,
|
|
21
|
+
nativeExecuted: false, runtimeReady: false, verifiedAdmission: false, modelCalls: 0,
|
|
22
|
+
started: false, exitObserved: false, pipesClosed: false, groupObservation: "not-started",
|
|
23
|
+
terminationConfirmed: false, stdoutBytes: 0, stderrBytes: 0, events: 0, elapsedMs: 0, reasons: [] };
|
|
24
|
+
const start = beganAt ?? monotonic(); let l;
|
|
25
|
+
let cwd, owned, capability, spawnReturned = false;
|
|
26
|
+
try {
|
|
27
|
+
request = copyInertJson(request); l = request.limits;
|
|
28
|
+
if (!supportsLocalPosix(process.platform) || !FIXTURES.includes(request.fixtureId)) throw Error("fixture-not-admitted");
|
|
29
|
+
const input = copyBytes(request.inputText, l.maxInputBytes);
|
|
30
|
+
if (signal?.aborted) { r.reasons = ["fixture-canceled-before-start"]; r.terminationConfirmed = true; return r; }
|
|
31
|
+
cwd = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "agent-fixture-work-")));
|
|
32
|
+
await fs.chmod(cwd, 0o700); owned = await fs.lstat(cwd);
|
|
33
|
+
// Fixed packaged program and Node executable; never a path or recipe from JSON.
|
|
34
|
+
const source = await fs.lstat(script);
|
|
35
|
+
if (!source.isFile() || source.isSymbolicLink() || (source.mode & 0o022)) throw Error("fixture-source-unsafe");
|
|
36
|
+
if (monotonic() - start >= l.timeoutMs - l.graceMs - l.confirmationMs) throw Error("fixture-deadline-before-start");
|
|
37
|
+
await checkpoint("fixture-before-claim"); capability = await claimFixturePermit(handle, hash(request));
|
|
38
|
+
if (signal?.aborted || monotonic() - start >= l.timeoutMs - l.graceMs - l.confirmationMs) throw Error("fixture-stopped-before-start");
|
|
39
|
+
r.spawnAttempts = 1;
|
|
40
|
+
const child = spawn(process.execPath, [script, request.fixtureId], {
|
|
41
|
+
shell: false, detached: true, cwd, env: { LANG: "C", LC_ALL: "C" }, stdio: ["pipe", "pipe", "pipe"]
|
|
42
|
+
});
|
|
43
|
+
spawnReturned = true;
|
|
44
|
+
// No awaits between spawn and registration of all lifecycle/error handlers.
|
|
45
|
+
await new Promise(resolve => {
|
|
46
|
+
const protocol = createFixtureProtocol(l); const stderrDecoder = new TextDecoder("utf-8", { fatal: true });
|
|
47
|
+
let done = false, canceled = false, cancelAt = null, forced = false, terminalFailure = false;
|
|
48
|
+
let groupGone = false, spawnFailed = false, spawned = false, recordPending = false, recordFailed = false;
|
|
49
|
+
let stdinClosed = false, stdoutClosed = false, stderrClosed = false, closeObserved = false, exitCode = null;
|
|
50
|
+
let timer, hardTimer;
|
|
51
|
+
const add = code => { if (!r.reasons.includes(code)) r.reasons.push(code); };
|
|
52
|
+
const elapsed = () => Math.max(0, monotonic() - start);
|
|
53
|
+
const observeGroup = () => {
|
|
54
|
+
if (groupGone || spawnFailed) return false;
|
|
55
|
+
if (!spawned) return true;
|
|
56
|
+
try { kill(-child.pid, 0); return true; }
|
|
57
|
+
catch (e) { if (e?.code === "ESRCH") { groupGone = true; return false; } add("fixture-group-unconfirmed"); return true; }
|
|
58
|
+
};
|
|
59
|
+
const send = sig => {
|
|
60
|
+
if (!spawned || groupGone) return;
|
|
61
|
+
try { kill(-child.pid, sig); }
|
|
62
|
+
catch (e) { if (e?.code === "ESRCH") groupGone = true; else add("fixture-signal-unconfirmed"); }
|
|
63
|
+
};
|
|
64
|
+
const cancel = (code, state = "failed") => {
|
|
65
|
+
if (done) return;
|
|
66
|
+
add(code);
|
|
67
|
+
if (done || canceled) return;
|
|
68
|
+
canceled = true; terminalFailure = true; cancelAt = monotonic(); r.state = state;
|
|
69
|
+
child.stdin.destroy(); send("SIGTERM");
|
|
70
|
+
};
|
|
71
|
+
const finalize = uncertain => {
|
|
72
|
+
if (done) return; done = true; clearInterval(timer); clearTimeout(hardTimer); signal?.removeEventListener("abort", abort);
|
|
73
|
+
const counts = protocol.counts(); Object.assign(r, { stdoutBytes: counts.stdoutBytes, events: counts.events,
|
|
74
|
+
pipesClosed: stdinClosed && stdoutClosed && stderrClosed && closeObserved, elapsedMs: elapsed() });
|
|
75
|
+
r.groupObservation = spawnFailed ? "not-started" : groupGone ? "absent" : "unconfirmed";
|
|
76
|
+
// Process termination and uncertainty of storage/elapsed time are separate observations.
|
|
77
|
+
r.terminationConfirmed = spawnFailed || r.exitObserved && r.pipesClosed && groupGone;
|
|
78
|
+
if (uncertain || recordFailed || r.elapsedMs > l.timeoutMs) { r.state = "indeterminate"; add("fixture-reconciliation-required"); }
|
|
79
|
+
else if (spawnFailed) r.state = "not-started";
|
|
80
|
+
else if (!terminalFailure) r.state = "completed";
|
|
81
|
+
if (!r.reasons.length) add("fixture-completed");
|
|
82
|
+
// Never retain or print raw child data. Stop reads if closure is uncertain.
|
|
83
|
+
child.stdin.destroy(); child.stdout.destroy(); child.stderr.destroy(); resolve();
|
|
84
|
+
};
|
|
85
|
+
const poll = () => {
|
|
86
|
+
if (done) return;
|
|
87
|
+
const ms = elapsed();
|
|
88
|
+
if (ms >= l.timeoutMs - l.graceMs - l.confirmationMs && !canceled) cancel("fixture-deadline", "timed-out");
|
|
89
|
+
if (canceled && !forced && monotonic() - cancelAt >= l.graceMs) { forced = true; send("SIGKILL"); }
|
|
90
|
+
const alive = observeGroup();
|
|
91
|
+
if (r.exitObserved && closeObserved && stdinClosed && stdoutClosed && stderrClosed && !recordPending && !alive) {
|
|
92
|
+
if (!canceled && !spawnFailed) {
|
|
93
|
+
try { if (exitCode !== 0) throw Error("failed"); protocol.finish(); stderrDecoder.decode(); }
|
|
94
|
+
catch (e) { terminalFailure = true; r.state = "failed"; add(e instanceof ParseFailure ? e.code : "fixture-exit-or-stderr-invalid"); }
|
|
95
|
+
}
|
|
96
|
+
finalize(false);
|
|
97
|
+
} else if (r.exitObserved && alive && !canceled) cancel("fixture-descendants-live");
|
|
98
|
+
if (spawnFailed && closeObserved) finalize(false);
|
|
99
|
+
};
|
|
100
|
+
const abort = () => cancel("fixture-canceled", "canceled");
|
|
101
|
+
child.on("spawn", () => {
|
|
102
|
+
if (done) { spawned = true; send("SIGKILL"); return; }
|
|
103
|
+
spawned = true; r.started = true; r.fixtureExecuted = true; recordPending = true;
|
|
104
|
+
if (canceled) send("SIGTERM");
|
|
105
|
+
Promise.resolve().then(() => checkpoint("fixture-spawned")).then(() => capability.started())
|
|
106
|
+
.catch(() => { recordFailed = true; cancel("fixture-mark-started-unconfirmed"); })
|
|
107
|
+
.finally(() => { recordPending = false; poll(); });
|
|
108
|
+
});
|
|
109
|
+
child.on("error", () => { if (done) return; if (!spawned) { spawnFailed = true; add("fixture-spawn-failed"); } else cancel("fixture-process-error"); });
|
|
110
|
+
child.on("exit", code => { if (done) return; r.exitObserved = true; exitCode = code; poll(); });
|
|
111
|
+
child.on("close", () => { closeObserved = true; poll(); });
|
|
112
|
+
child.stdin.on("close", () => { stdinClosed = true; poll(); });
|
|
113
|
+
child.stdout.on("close", () => { stdoutClosed = true; poll(); });
|
|
114
|
+
child.stderr.on("close", () => { stderrClosed = true; poll(); });
|
|
115
|
+
child.stdin.on("error", () => cancel("fixture-stdin-error"));
|
|
116
|
+
child.stdout.on("error", () => cancel("fixture-stdout-error"));
|
|
117
|
+
child.stderr.on("error", () => cancel("fixture-stderr-error"));
|
|
118
|
+
child.stdout.on("data", chunk => {
|
|
119
|
+
if (done || canceled) return;
|
|
120
|
+
try { protocol.push(chunk); } catch (e) { cancel(reason(e)); }
|
|
121
|
+
});
|
|
122
|
+
child.stderr.on("data", chunk => {
|
|
123
|
+
if (done || canceled) return;
|
|
124
|
+
r.stderrBytes += chunk.length;
|
|
125
|
+
if (r.stderrBytes > l.maxStderrBytes) cancel("fixture-stderr-limit");
|
|
126
|
+
else try { stderrDecoder.decode(chunk, { stream: true }); } catch { cancel("fixture-stderr-invalid-utf8"); }
|
|
127
|
+
});
|
|
128
|
+
signal?.addEventListener("abort", abort, { once: true }); if (signal?.aborted) abort();
|
|
129
|
+
timer = setInterval(poll, 10);
|
|
130
|
+
hardTimer = setTimeout(() => { send("SIGKILL"); poll(); if (!done) finalize(true); }, Math.max(1, l.timeoutMs - elapsed()));
|
|
131
|
+
try { child.stdin.end(input, error => { if (error) cancel("fixture-stdin-error"); }); }
|
|
132
|
+
catch { cancel("fixture-stdin-error"); }
|
|
133
|
+
});
|
|
134
|
+
} catch (e) {
|
|
135
|
+
r.reasons = [e instanceof ParseFailure ? e.code : "fixture-start-blocked"];
|
|
136
|
+
r.terminationConfirmed = !spawnReturned; r.elapsedMs = Math.max(0, monotonic() - start);
|
|
137
|
+
// A synchronous spawn failure is known not-started; an unexpected post-spawn error is uncertain.
|
|
138
|
+
if (spawnReturned) r.state = "indeterminate";
|
|
139
|
+
} finally {
|
|
140
|
+
if (cwd && (!r.spawnAttempts || r.terminationConfirmed)) {
|
|
141
|
+
try { const s = await fs.lstat(cwd); if (s.dev !== owned.dev || s.ino !== owned.ino || !s.isDirectory()) throw Error("replaced"); await fs.rmdir(cwd); }
|
|
142
|
+
catch { r.state = "indeterminate"; r.reasons.push("fixture-workspace-cleanup-required"); }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return copyInertJson(r);
|
|
146
|
+
}
|