@hassangameryt/freeflow 0.7.3 → 0.9.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/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +1 -1
- package/NOTICE +14 -0
- package/README.md +101 -159
- package/capabilities/cognitive-routing/SKILL.md +47 -210
- package/capabilities/cognitive-routing/references/coordinator.md +163 -0
- package/capabilities/cognitive-routing/references/worker.md +38 -0
- package/capabilities/compaction/SKILL.md +49 -0
- package/capabilities/compaction/references/summary-format.md +250 -0
- package/capabilities/messaging/SKILL.md +49 -0
- package/capabilities/task-memory/SKILL.md +87 -0
- package/command-surface.json +8 -52
- package/gemini-extension.json +1 -1
- package/hooks/claude/hooks.json +13 -3
- package/hooks/codex/hooks.json +2 -1
- package/hooks/shared/runtime-context.mjs +73 -83
- package/licenses/Apache-2.0.txt +55 -0
- package/package.json +42 -12
- package/pi-extension/README.md +41 -0
- package/pi-extension/dist/ask-user/ask.js +108 -0
- package/pi-extension/dist/ask-user/form.js +250 -0
- package/pi-extension/dist/ask-user/questions.js +66 -0
- package/pi-extension/dist/ask-user/tool.js +124 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +417 -0
- package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +8 -6
- package/pi-extension/dist/cognitive-routing/economics.js +45 -0
- package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
- package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +107 -41
- package/pi-extension/dist/cognitive-routing/gate.js +256 -0
- package/pi-extension/dist/cognitive-routing/handlers.js +901 -0
- package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
- package/pi-extension/dist/cognitive-routing/model-control.js +476 -0
- package/pi-extension/dist/cognitive-routing/projection.js +520 -0
- package/pi-extension/dist/cognitive-routing/provenance.js +83 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +15 -15
- package/pi-extension/dist/cognitive-routing/runtime.js +398 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +47 -11
- package/pi-extension/dist/cognitive-routing/session.js +223 -0
- package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +126 -87
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +55 -386
- package/pi-extension/dist/cognitive-routing/status.js +81 -0
- package/pi-extension/dist/cognitive-routing/tools.js +74 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +14 -11
- package/pi-extension/dist/compaction/carry.js +205 -0
- package/pi-extension/dist/compaction/config.js +24 -0
- package/pi-extension/dist/compaction/controller.js +494 -0
- package/pi-extension/dist/compaction/harness.js +93 -0
- package/pi-extension/dist/compaction/results.js +159 -0
- package/pi-extension/dist/compaction/thresholds.js +35 -0
- package/pi-extension/dist/compaction/tool.js +57 -0
- package/pi-extension/dist/host/branch.js +13 -0
- package/pi-extension/dist/host/canonical.js +12 -0
- package/pi-extension/dist/host/catalog.js +62 -0
- package/pi-extension/dist/host/config-validation.js +233 -0
- package/pi-extension/dist/host/config.js +361 -0
- package/pi-extension/dist/host/inbound.js +142 -0
- package/pi-extension/dist/host/projection-tags.js +60 -0
- package/pi-extension/dist/host/prompts.js +93 -0
- package/pi-extension/dist/{session-sources → host}/read-only-session.js +117 -1
- package/pi-extension/dist/{runtime → host}/request-history.js +125 -39
- package/pi-extension/dist/host/rows.js +25 -0
- package/pi-extension/dist/host/runtime-state.js +204 -0
- package/pi-extension/dist/host/section-keeper.js +85 -0
- package/pi-extension/dist/host/settings/freeflow-command.js +2323 -0
- package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +205 -73
- package/pi-extension/dist/host/staging.js +28 -0
- package/pi-extension/dist/host/status.js +115 -0
- package/pi-extension/dist/index.js +1004 -188
- package/pi-extension/dist/messaging/config.js +27 -0
- package/pi-extension/dist/messaging/messaging.js +398 -0
- package/pi-extension/dist/messaging/registry.js +57 -0
- package/pi-extension/dist/messaging/render.js +90 -0
- package/pi-extension/dist/messaging/tools.js +109 -0
- package/pi-extension/dist/messaging/transport.js +101 -0
- package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
- package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
- package/pi-extension/dist/provider-support/cache/health.js +73 -0
- package/pi-extension/dist/provider-support/cache/index.js +58 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +222 -0
- package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
- package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
- package/pi-extension/dist/provider-support/effort.js +13 -0
- package/pi-extension/dist/provider-support/index.js +44 -3
- package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
- package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
- package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
- package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
- package/pi-extension/dist/provider-support/routes.js +17 -0
- package/pi-extension/dist/task-memory/config.js +49 -0
- package/pi-extension/dist/task-memory/registry.js +119 -0
- package/pi-extension/dist/task-memory/render.js +107 -0
- package/pi-extension/dist/task-memory/runtime.js +994 -0
- package/pi-extension/dist/task-memory/tool.js +233 -0
- package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
- package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
- package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
- package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
- package/pi-extension/dist/tool-execution/background.js +430 -0
- package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
- package/pi-extension/dist/tool-execution/config.js +33 -0
- package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
- package/pi-extension/dist/tool-execution/file-state.js +138 -0
- package/pi-extension/dist/tool-execution/file-tracking.js +163 -0
- package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
- package/pi-extension/dist/tool-execution/prompt.js +54 -0
- package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
- package/pi-extension/dist/tool-execution/tools.js +16 -0
- package/pi-extension/freeflow/README.md +9 -0
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +29 -72
- package/runtime/prompts/interaction-contract.md +18 -8
- package/runtime/prompts/tool-execution.md +25 -0
- package/runtime/prompts/working-method.md +25 -0
- package/skills/deliver-work/SKILL.md +76 -0
- package/skills/deliver-work/references/branch-closeout.md +134 -0
- package/skills/deliver-work/references/commit.md +83 -0
- package/skills/deliver-work/references/launch.md +94 -0
- package/skills/deliver-work/references/release.md +85 -0
- package/skills/deliver-work/scripts/commit-state.mjs +139 -0
- package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
- package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
- package/skills/deliver-work/scripts/push-route.mjs +122 -0
- package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
- package/skills/discuss-work/SKILL.md +148 -0
- package/skills/{discuss → discuss-work}/references/discussion-continuity.md +13 -50
- package/skills/discuss-work/references/learning-action.md +36 -0
- package/skills/discuss-work/scripts/choices.mjs +101 -0
- package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
- package/skills/evaluate-behavior/SKILL.md +107 -0
- package/skills/evaluate-behavior/references/authoring.md +381 -0
- package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
- package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
- package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
- package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
- package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
- package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
- package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
- package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
- package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
- package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
- package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
- package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
- package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
- package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
- package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
- package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
- package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
- package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
- package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
- package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
- package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
- package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
- package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
- package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
- package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
- package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
- package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
- package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
- package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +121 -14
- package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
- package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
- package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
- package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
- package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
- package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
- package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +239 -262
- package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
- package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
- package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
- package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +218 -11
- package/skills/evaluate-behavior/scripts/pi-command-tool.mjs +159 -0
- package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
- package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
- package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
- package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
- package/skills/execute-work/SKILL.md +96 -72
- package/skills/execute-work/references/code-practices.md +27 -49
- package/skills/execute-work/references/design.md +271 -0
- package/skills/execute-work/references/diagnose.md +117 -0
- package/skills/execute-work/references/migration.md +108 -0
- package/skills/execute-work/references/simplify.md +65 -0
- package/skills/execute-work/scripts/self-review.mjs +422 -0
- package/skills/execute-work/scripts/touch-map.mjs +290 -0
- package/skills/review-work/SKILL.md +94 -64
- package/skills/review-work/references/adjudication.md +94 -0
- package/skills/review-work/references/artifact.md +198 -0
- package/skills/review-work/references/evals-and-judges.md +66 -0
- package/skills/review-work/references/work.md +177 -0
- package/skills/review-work/scripts/check-brief.mjs +128 -0
- package/skills/review-work/scripts/review-packet.mjs +233 -0
- package/skills/setup-freeflow/SKILL.md +69 -16
- package/skills/track-work/SKILL.md +135 -151
- package/skills/track-work/references/handoff.md +190 -0
- package/skills/track-work/references/projects.md +51 -0
- package/skills/track-work/references/working-record-format.md +201 -344
- package/skills/track-work/scripts/lib/document.mjs +79 -19
- package/skills/track-work/scripts/lib/format.mjs +18 -1
- package/skills/track-work/scripts/lib/recover.mjs +254 -0
- package/skills/track-work/scripts/lib/registry.mjs +656 -0
- package/skills/track-work/scripts/lib/search.mjs +40 -0
- package/skills/track-work/scripts/lib/store.mjs +53 -21
- package/skills/track-work/scripts/lib/transitions.mjs +79 -4
- package/skills/track-work/scripts/lib/views.mjs +50 -1
- package/skills/track-work/scripts/working-record.mjs +263 -22
- package/skills/verify-work/SKILL.md +64 -47
- package/skills/verify-work/references/feature-map.md +120 -0
- package/skills/verify-work/references/runtime-evidence.md +203 -0
- package/skills/verify-work/references/test-design.md +27 -6
- package/skills/verify-work/scripts/test-changes.mjs +246 -0
- package/skills/write-docs/SKILL.md +171 -0
- package/skills/write-docs/references/documentation-shapes.md +184 -0
- package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
- package/skills/write-docs/scripts/artifact-check.mjs +148 -0
- package/skills/write-docs/scripts/docs-check.mjs +229 -0
- package/skills/write-docs/scripts/plan-lint.mjs +123 -0
- package/skills/write-skill/SKILL.md +165 -165
- package/skills/write-skill/references/activation-and-wording.md +143 -0
- package/skills/write-skill/references/placement-and-dependencies.md +154 -0
- package/skills/write-skill/scripts/lib/compare.mjs +701 -0
- package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
- package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
- package/skills/write-skill/scripts/skill-author.mjs +67 -8
- package/capabilities/context-virtualization/SKILL.md +0 -103
- package/capabilities/conversation-history/SKILL.md +0 -53
- package/hooks/tests/check-host-adapters.test.mjs +0 -82
- package/hooks/tests/check-runtime-context-hook.sh +0 -212
- package/pi-extension/dist/cognitive-routing-v2/projection.js +0 -330
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
- package/pi-extension/dist/cognitive-routing-v2/tools.js +0 -60
- package/pi-extension/dist/context-virtualization/commands.js +0 -89
- package/pi-extension/dist/context-virtualization/projector.js +0 -42
- package/pi-extension/dist/context-virtualization/runtime.js +0 -378
- package/pi-extension/dist/context-virtualization/state.js +0 -57
- package/pi-extension/dist/context-virtualization/types.js +0 -10
- package/pi-extension/dist/conversation-history/passages.js +0 -66
- package/pi-extension/dist/conversation-history/ranking.js +0 -105
- package/pi-extension/dist/conversation-history/runtime.js +0 -226
- package/pi-extension/dist/conversation-history/search.js +0 -56
- package/pi-extension/dist/conversation-history/types.js +0 -1
- package/pi-extension/dist/freeflow-context/resolver.js +0 -120
- package/pi-extension/dist/freeflow-context/runtime.js +0 -80
- package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
- package/pi-extension/dist/freeflow-context/tool.js +0 -314
- package/pi-extension/dist/freeflow-context/types.js +0 -12
- package/pi-extension/dist/freeflow-context/visibility.js +0 -28
- package/pi-extension/dist/integrations/mcp-config.js +0 -29
- package/pi-extension/dist/runtime/runtime-context.js +0 -754
- package/pi-extension/dist/runtime/runtime-identity.js +0 -5
- package/pi-extension/dist/session-sources/provenance.js +0 -32
- package/pi-extension/dist/settings/settings-ui.js +0 -1360
- package/pi-extension/dist/ui/text-component.js +0 -57
- package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
- package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
- package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
- package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
- package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
- package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
- package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
- package/pi-extension/src/context-virtualization/commands.ts +0 -111
- package/pi-extension/src/context-virtualization/projector.ts +0 -62
- package/pi-extension/src/context-virtualization/runtime.ts +0 -428
- package/pi-extension/src/context-virtualization/state.ts +0 -79
- package/pi-extension/src/context-virtualization/types.ts +0 -92
- package/pi-extension/src/conversation-history/passages.ts +0 -81
- package/pi-extension/src/conversation-history/ranking.ts +0 -155
- package/pi-extension/src/conversation-history/runtime.ts +0 -250
- package/pi-extension/src/conversation-history/search.ts +0 -79
- package/pi-extension/src/conversation-history/types.ts +0 -31
- package/pi-extension/src/freeflow-context/resolver.ts +0 -139
- package/pi-extension/src/freeflow-context/runtime.ts +0 -100
- package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
- package/pi-extension/src/freeflow-context/tool.ts +0 -366
- package/pi-extension/src/freeflow-context/types.ts +0 -70
- package/pi-extension/src/freeflow-context/visibility.ts +0 -53
- package/pi-extension/src/index.ts +0 -390
- package/pi-extension/src/integrations/mcp-config.ts +0 -32
- package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
- package/pi-extension/src/provider-support/astra/history.ts +0 -145
- package/pi-extension/src/provider-support/astra/index.ts +0 -12
- package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
- package/pi-extension/src/provider-support/index.ts +0 -5
- package/pi-extension/src/runtime/request-history.ts +0 -243
- package/pi-extension/src/runtime/runtime-context.ts +0 -860
- package/pi-extension/src/runtime/runtime-identity.ts +0 -8
- package/pi-extension/src/session-sources/events.ts +0 -230
- package/pi-extension/src/session-sources/provenance.ts +0 -39
- package/pi-extension/src/session-sources/read-only-session.ts +0 -170
- package/pi-extension/src/session-sources/sources.ts +0 -334
- package/pi-extension/src/settings/settings-tui.ts +0 -501
- package/pi-extension/src/settings/settings-ui.ts +0 -1652
- package/pi-extension/src/ui/text-component.ts +0 -66
- package/pi-extension/tests/cache-reuse/history.test.js +0 -117
- package/pi-extension/tests/cache-reuse/native.test.js +0 -91
- package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
- package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
- package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
- package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
- package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
- package/pi-extension/tests/conversation-history/search.test.js +0 -97
- package/pi-extension/tests/fixtures/pi-host.js +0 -4
- package/pi-extension/tests/fixtures/routing-native.js +0 -191
- package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
- package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
- package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
- package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
- package/pi-extension/tests/provider-support/astra.test.js +0 -306
- package/pi-extension/tests/provider-support/cli.test.js +0 -92
- package/pi-extension/tests/provider-support/native.test.js +0 -183
- package/pi-extension/tests/settings/settings-tui.test.js +0 -243
- package/pi-extension/tsconfig.json +0 -16
- package/plugin.json +0 -33
- package/runtime/prompts/context-virtualization.md +0 -3
- package/runtime/prompts/conversation-history.md +0 -3
- package/skills/action-selection/SKILL.md +0 -100
- package/skills/action-selection/references/trajectory-stalls.md +0 -71
- package/skills/bypass/SKILL.md +0 -153
- package/skills/commit-work/SKILL.md +0 -145
- package/skills/commit-work/references/git-state-edges.md +0 -94
- package/skills/commit-work/references/simple-push.md +0 -92
- package/skills/commit-work/references/staging-decisions.md +0 -106
- package/skills/decision-gate/SKILL.md +0 -106
- package/skills/design-for-depth/SKILL.md +0 -117
- package/skills/design-for-depth/references/design-pressure-signals.md +0 -182
- package/skills/design-for-depth/references/implementation-decisions.md +0 -76
- package/skills/design-for-depth/references/interface-design-loop.md +0 -136
- package/skills/design-for-depth/references/module-and-dependency-design.md +0 -147
- package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -136
- package/skills/diagnose-failure/SKILL.md +0 -125
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -48
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
- package/skills/discuss/SKILL.md +0 -116
- package/skills/evaluate-skill/SKILL.md +0 -219
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -385
- package/skills/evaluate-skill/references/evaluation-design.md +0 -112
- package/skills/evaluate-skill/references/execution-and-evidence.md +0 -90
- package/skills/evaluate-skill/references/review-and-revision.md +0 -92
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -407
- package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
- package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
- package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -368
- package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -164
- package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -102
- package/skills/execute-work/references/execute-work-edges.md +0 -68
- package/skills/finish-branch/SKILL.md +0 -117
- package/skills/finish-branch/references/integration-options.md +0 -80
- package/skills/handoff/SKILL.md +0 -82
- package/skills/handoff/references/handoff-templates.md +0 -150
- package/skills/launch-work/SKILL.md +0 -116
- package/skills/launch-work/references/launch-readiness.md +0 -85
- package/skills/migration-work/SKILL.md +0 -115
- package/skills/migration-work/references/migration-lifecycle.md +0 -82
- package/skills/release-work/SKILL.md +0 -118
- package/skills/release-work/references/release-evidence.md +0 -78
- package/skills/review-artifact/SKILL.md +0 -142
- package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
- package/skills/review-work/references/adjudicate-work-review.md +0 -97
- package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
- package/skills/review-work/references/security-risk-lens.md +0 -69
- package/skills/setup-freeflow/references/activation-contract.md +0 -109
- package/skills/setup-freeflow/references/host-setup.md +0 -67
- package/skills/simplify-code/SKILL.md +0 -85
- package/skills/simplify-code/references/simplification-patterns.md +0 -70
- package/skills/verify-work/references/browser-runtime-evidence.md +0 -55
- package/skills/verify-work/references/integration-evidence.md +0 -47
- package/skills/verify-work/references/performance-evidence.md +0 -57
- package/skills/workflow/SKILL.md +0 -127
- package/skills/workflow/references/domain-skill-composition.md +0 -43
- package/skills/write-plan/SKILL.md +0 -92
- package/skills/write-plan/references/plan-shapes.md +0 -156
- package/skills/write-skill/references/activation-boundaries.md +0 -104
- package/skills/write-skill/references/agent-first-instructions.md +0 -121
- package/skills/write-skill/references/progressive-disclosure.md +0 -127
- package/skills/write-skill/references/skill-dependency-graphs.md +0 -190
- package/skills/write-spec/SKILL.md +0 -92
- package/skills/write-spec/references/artifact-standards.md +0 -86
- package/skills/write-spec/references/decision-records.md +0 -78
- package/skills/write-spec/references/spec-shapes.md +0 -143
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const run = promisify(execFile);
|
|
8
|
+
const SNAPSHOT_SCRIPT = path.join("scripts", "freeflow-snapshot.mjs");
|
|
9
|
+
const ENTRY = path.join("pi-extension", "freeflow", "index.js");
|
|
10
|
+
const VALIDATOR = path.join("pi-extension", "dist", "host", "config-validation.js");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Builds the Freeflow package each arm declares, once per distinct commit, into the group's resources, and
|
|
14
|
+
* checks each arm's settings with the validator from its own build. Any failure refuses the group before a
|
|
15
|
+
* subject starts. The build uses the definition root's own snapshot tool with an explicit target, so the
|
|
16
|
+
* user's development snapshot is never touched.
|
|
17
|
+
*
|
|
18
|
+
* @param {{variants: Record<string, any>, root: string, groupDirectory: string}} options
|
|
19
|
+
* @returns {Promise<Record<string, any>>} each arm's built Freeflow, or null when it declares none
|
|
20
|
+
*/
|
|
21
|
+
export async function prepareFreeflow({ variants, root, groupDirectory }) {
|
|
22
|
+
const builds = new Map();
|
|
23
|
+
const prepared = {};
|
|
24
|
+
for (const [arm, variant] of Object.entries(variants)) {
|
|
25
|
+
const freeflow = variant.freeflow;
|
|
26
|
+
if (!freeflow) {
|
|
27
|
+
prepared[arm] = null;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!builds.has(freeflow.source.ref)) {
|
|
31
|
+
builds.set(freeflow.source.ref, await build(freeflow.source.ref, root, groupDirectory, builds.size));
|
|
32
|
+
}
|
|
33
|
+
const built = builds.get(freeflow.source.ref);
|
|
34
|
+
const validator = await import(pathToFileURL(path.join(built.packageRoot, VALIDATOR)).href);
|
|
35
|
+
const problems = [
|
|
36
|
+
["repository", validator.validateRepositoryConfig(freeflow.repository)],
|
|
37
|
+
["personal", validator.validateLocalConfig(freeflow.personal)],
|
|
38
|
+
].filter(([, problem]) => problem !== null);
|
|
39
|
+
if (problems.length > 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${arm} Freeflow settings are invalid for ${built.commit.slice(0, 12)}: ${problems.map(([scope, problem]) => `${scope}: ${problem}`).join("; ")}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
prepared[arm] = {
|
|
45
|
+
...built,
|
|
46
|
+
ref: freeflow.source.ref,
|
|
47
|
+
repository: freeflow.repository,
|
|
48
|
+
personal: freeflow.personal,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return prepared;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function build(ref, root, groupDirectory, index) {
|
|
55
|
+
const script = path.join(root, SNAPSHOT_SCRIPT);
|
|
56
|
+
if (!(await stat(script).catch(() => null))) {
|
|
57
|
+
throw new Error(`a Freeflow configuration needs ${SNAPSHOT_SCRIPT} in the definition root`);
|
|
58
|
+
}
|
|
59
|
+
const directory = path.join(groupDirectory, "resources", "freeflow", String(index));
|
|
60
|
+
await mkdir(directory, { recursive: true });
|
|
61
|
+
const packageRoot = path.join(directory, "package");
|
|
62
|
+
const metadataFile = path.join(directory, "snapshot.json");
|
|
63
|
+
try {
|
|
64
|
+
await run(
|
|
65
|
+
process.execPath,
|
|
66
|
+
[script, "--commit", ref, "--source-root", root, "--target", packageRoot, "--metadata", metadataFile],
|
|
67
|
+
{ cwd: root, timeout: 5 * 60 * 1000, maxBuffer: 16 * 1024 * 1024 },
|
|
68
|
+
);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const detail = error?.stderr?.trim() || (error instanceof Error ? error.message : String(error));
|
|
71
|
+
throw new Error(`building Freeflow at ${ref} failed: ${detail}`);
|
|
72
|
+
}
|
|
73
|
+
const metadata = JSON.parse(await readFile(metadataFile, "utf8"));
|
|
74
|
+
const entry = path.join(packageRoot, ENTRY);
|
|
75
|
+
if (!(await stat(entry).catch(() => null))) throw new Error(`the Freeflow build at ${ref} has no ${ENTRY}`);
|
|
76
|
+
if (typeof metadata.sourceCommit !== "string") throw new Error(`the Freeflow build at ${ref} recorded no commit`);
|
|
77
|
+
return {
|
|
78
|
+
commit: metadata.sourceCommit,
|
|
79
|
+
package: `${metadata.packageName}@${metadata.packageVersion}`,
|
|
80
|
+
tarballSha256: metadata.tarballSha256 ?? null,
|
|
81
|
+
packageRoot,
|
|
82
|
+
entry,
|
|
83
|
+
metadata: path.relative(groupDirectory, metadataFile),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Writes an arm's Freeflow settings into its workspace before the subject starts. A fixture that already
|
|
89
|
+
* carries either file is refused: the settings belong to the configuration, where the comparison declares them.
|
|
90
|
+
*/
|
|
91
|
+
export async function writeFreeflowSettings(workspace, freeflow) {
|
|
92
|
+
const directory = path.join(workspace, ".freeflow");
|
|
93
|
+
for (const file of ["config.json", "local.json"]) {
|
|
94
|
+
if (await stat(path.join(directory, file)).catch(() => null)) {
|
|
95
|
+
throw new Error(`the fixture already has .freeflow/${file}; declare Freeflow settings in configuration.freeflow`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await mkdir(directory, { recursive: true });
|
|
99
|
+
await writeFile(path.join(directory, "config.json"), `${JSON.stringify(freeflow.repository, null, 2)}\n`);
|
|
100
|
+
if (Object.keys(freeflow.personal).length > 0) {
|
|
101
|
+
await writeFile(path.join(directory, "local.json"), `${JSON.stringify(freeflow.personal, null, 2)}\n`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
// Command graders: a `command` expectation runs an author-declared argv, such as hidden tests or a linter, on a
|
|
2
|
+
// copy of the subject's final workspace with grader files the subject never saw. The command runs as a post-run
|
|
3
|
+
// hook, before run.json is written, and its record lands under `postRun.commands.<expectation id>`; the grader
|
|
4
|
+
// then reads that record like any other run evidence. The workspace itself is never touched, so file graders
|
|
5
|
+
// still verify its hashes.
|
|
6
|
+
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { cp, lstat, mkdir, readFile, realpath, rm, stat } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
|
|
11
|
+
import { comparisonArms } from "./arms.mjs";
|
|
12
|
+
import { registerAggregator, registerPostRunHook } from "./hooks.mjs";
|
|
13
|
+
import { processGroupExists, signalProcessTree, TERMINATION_GRACE_MS } from "./process.mjs";
|
|
14
|
+
import { completedCheck, isArm, isContained, isSafeRelativePath, unavailableCheck } from "./grade-support.mjs";
|
|
15
|
+
|
|
16
|
+
export const COMMAND_KIND = "command";
|
|
17
|
+
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
18
|
+
const MAX_TIMEOUT_SECONDS = 3600;
|
|
19
|
+
const TEST_FORMATS = new Set(["node-test", "tap", "junit"]);
|
|
20
|
+
// What a record keeps of each stream; the whole stdout (up to PARSE_LIMIT) is still parsed for test results.
|
|
21
|
+
const OUTPUT_TAIL_CHARACTERS = 20_000;
|
|
22
|
+
const PARSE_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Why a command expectation's own fields are malformed, or null. Load-time validation and the grader share it,
|
|
26
|
+
* so a definition that loads always grades.
|
|
27
|
+
*
|
|
28
|
+
* @returns {{ code: string, message: string } | null}
|
|
29
|
+
*/
|
|
30
|
+
export function commandExpectationProblem(expectation) {
|
|
31
|
+
const invalid = (message) => ({ code: "invalid-definition", message });
|
|
32
|
+
for (const field of ["turn", "expect"]) {
|
|
33
|
+
if (Object.hasOwn(expectation, field))
|
|
34
|
+
return invalid(`a command expectation grades the final work; ${field} is not supported`);
|
|
35
|
+
}
|
|
36
|
+
const { argv } = expectation;
|
|
37
|
+
if (!Array.isArray(argv) || argv.length === 0 || !argv.every((entry) => typeof entry === "string" && entry !== "")) {
|
|
38
|
+
return invalid("argv must be a non-empty array of non-empty strings");
|
|
39
|
+
}
|
|
40
|
+
if (Object.hasOwn(expectation, "files")) {
|
|
41
|
+
const { files } = expectation;
|
|
42
|
+
if (files === null || typeof files !== "object" || Array.isArray(files)) return invalid("files must be an object");
|
|
43
|
+
for (const [destination, source] of Object.entries(files)) {
|
|
44
|
+
if (!isSafeRelativePath(destination)) return invalid(`files destination must be a relative path: ${destination}`);
|
|
45
|
+
if (typeof source !== "string" || !isSafeRelativePath(source)) {
|
|
46
|
+
return { code: "unsafe-path", message: `files.${destination} must be a path inside the definition root` };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
Object.hasOwn(expectation, "timeout_seconds") &&
|
|
52
|
+
(!Number.isInteger(expectation.timeout_seconds) ||
|
|
53
|
+
expectation.timeout_seconds < 1 ||
|
|
54
|
+
expectation.timeout_seconds > MAX_TIMEOUT_SECONDS)
|
|
55
|
+
) {
|
|
56
|
+
return invalid(`timeout_seconds must be an integer from 1 to ${MAX_TIMEOUT_SECONDS}`);
|
|
57
|
+
}
|
|
58
|
+
return passRuleProblem(expectation.pass);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function passRuleProblem(pass) {
|
|
62
|
+
const invalid = (message) => ({ code: "invalid-definition", message });
|
|
63
|
+
if (pass === null || typeof pass !== "object" || Array.isArray(pass)) return invalid("pass must be an object");
|
|
64
|
+
const keys = Object.keys(pass);
|
|
65
|
+
if (keys.length !== 1 || !["exit_code", "tests"].includes(keys[0])) {
|
|
66
|
+
return invalid("pass must declare exactly one of exit_code or tests");
|
|
67
|
+
}
|
|
68
|
+
if (keys[0] === "exit_code") {
|
|
69
|
+
return Number.isInteger(pass.exit_code) && pass.exit_code >= 0 && pass.exit_code <= 255
|
|
70
|
+
? null
|
|
71
|
+
: invalid("pass.exit_code must be an integer from 0 to 255");
|
|
72
|
+
}
|
|
73
|
+
const tests = pass.tests;
|
|
74
|
+
if (tests === null || typeof tests !== "object" || Array.isArray(tests))
|
|
75
|
+
return invalid("pass.tests must be an object");
|
|
76
|
+
for (const key of Object.keys(tests)) {
|
|
77
|
+
if (!["format", "required", "report"].includes(key)) return invalid(`pass.tests contains unsupported field ${key}`);
|
|
78
|
+
}
|
|
79
|
+
if (!TEST_FORMATS.has(tests.format)) return invalid("pass.tests.format must be node-test, tap or junit");
|
|
80
|
+
const required = tests.required;
|
|
81
|
+
if (
|
|
82
|
+
!Array.isArray(required) ||
|
|
83
|
+
required.length === 0 ||
|
|
84
|
+
!required.every((name) => typeof name === "string" && name.trim() !== "") ||
|
|
85
|
+
new Set(required).size !== required.length
|
|
86
|
+
) {
|
|
87
|
+
return invalid("pass.tests.required must be a non-empty array of unique test names");
|
|
88
|
+
}
|
|
89
|
+
if (Object.hasOwn(tests, "report")) {
|
|
90
|
+
if (tests.format !== "junit") return invalid("pass.tests.report is only for the junit format");
|
|
91
|
+
if (typeof tests.report !== "string" || !isSafeRelativePath(tests.report)) {
|
|
92
|
+
return invalid("pass.tests.report must be a relative path in the command's workspace copy");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validates every command expectation of a group at load time and returns the absolute grader files it reads, so
|
|
100
|
+
* the run's committed-input check covers them. Sources resolve from the definition root, like the fixture, and
|
|
101
|
+
* may not lie inside the fixture, which the subject sees.
|
|
102
|
+
*
|
|
103
|
+
* @param {any} group
|
|
104
|
+
* @param {{ arms: string[], root: string, fail: (code: string, message: string) => never }} options
|
|
105
|
+
* @returns {string[]}
|
|
106
|
+
*/
|
|
107
|
+
export function validateCommandExpectations(group, { arms, root, fail }) {
|
|
108
|
+
const sources = [];
|
|
109
|
+
const fixture = group.fixture === null ? null : path.resolve(root, group.fixture);
|
|
110
|
+
for (const [index, expectation] of group.expectations.entries()) {
|
|
111
|
+
if (expectation.kind !== COMMAND_KIND) continue;
|
|
112
|
+
const label = `group.expectations[${index}]`;
|
|
113
|
+
if (group.type !== "body") fail("invalid-definition", `${label}: command expectations need a body group`);
|
|
114
|
+
if (!arms.includes(expectation.variant)) fail("invalid-definition", `${label}.variant must name one of group.arms`);
|
|
115
|
+
const problem = commandExpectationProblem(expectation);
|
|
116
|
+
if (problem !== null) fail(problem.code, `${label}: ${problem.message}`);
|
|
117
|
+
for (const source of Object.values(expectation.files ?? {})) {
|
|
118
|
+
const absolute = path.resolve(root, source);
|
|
119
|
+
if (!isContained(root, absolute))
|
|
120
|
+
fail("unsafe-path", `${label}.files source must stay inside the definition root`);
|
|
121
|
+
if (fixture !== null && isContained(fixture, absolute)) {
|
|
122
|
+
fail("invalid-definition", `${label}.files source ${source} is inside the fixture, which the subject sees`);
|
|
123
|
+
}
|
|
124
|
+
if (!sources.includes(absolute)) sources.push(absolute);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return sources;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The expected predicate a command check records; comparisons require it to be the same in every arm. */
|
|
131
|
+
function expectedOf(expectation) {
|
|
132
|
+
return {
|
|
133
|
+
argv: expectation.argv,
|
|
134
|
+
files: expectation.files ?? {},
|
|
135
|
+
timeout_seconds: expectation.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS,
|
|
136
|
+
pass: expectation.pass,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Grades a command expectation from the record its post-run hook stored in the run. */
|
|
141
|
+
export async function gradeCommand(group, runs, expectation) {
|
|
142
|
+
if (group.type !== "body" || !isArm(group, expectation.variant) || commandExpectationProblem(expectation) !== null) {
|
|
143
|
+
return { error: "invalid command expectation" };
|
|
144
|
+
}
|
|
145
|
+
const expected = expectedOf(expectation);
|
|
146
|
+
const run = runs[expectation.variant];
|
|
147
|
+
if (!run || run.state !== "complete") return unavailableCheck(expectation, expected);
|
|
148
|
+
const record = run.postRun?.commands?.[expectation.id];
|
|
149
|
+
if (!record || record.started !== true) {
|
|
150
|
+
return {
|
|
151
|
+
...unavailableCheck(expectation, expected),
|
|
152
|
+
observed: { reason: record?.error ?? "the command recorded no result" },
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const outcome = {
|
|
156
|
+
exitCode: record.exitCode,
|
|
157
|
+
signal: record.signal,
|
|
158
|
+
timedOut: record.timedOut,
|
|
159
|
+
durationMs: record.durationMs,
|
|
160
|
+
};
|
|
161
|
+
if (Object.hasOwn(expectation.pass, "exit_code")) {
|
|
162
|
+
const passed = record.timedOut !== true && record.exitCode === expectation.pass.exit_code;
|
|
163
|
+
return completedCheck(expectation, expected, outcome, passed);
|
|
164
|
+
}
|
|
165
|
+
const results = Array.isArray(record.tests?.results) ? record.tests.results : [];
|
|
166
|
+
const required = expectation.pass.tests.required.map((name) => ({ name, state: requiredState(results, name) }));
|
|
167
|
+
const passed = required.filter((entry) => entry.state === "pass").length;
|
|
168
|
+
return completedCheck(
|
|
169
|
+
expectation,
|
|
170
|
+
expected,
|
|
171
|
+
{
|
|
172
|
+
...outcome,
|
|
173
|
+
tests: { parsed: results.length, error: record.tests?.error ?? null, required },
|
|
174
|
+
score: passed / required.length,
|
|
175
|
+
},
|
|
176
|
+
passed === required.length,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** A required name matches a test by its full name or its own name; it passes only when every match passed. */
|
|
181
|
+
function requiredState(results, name) {
|
|
182
|
+
const matches = results.filter((result) => result.fullName === name || result.name === name);
|
|
183
|
+
if (matches.length === 0) return "missing";
|
|
184
|
+
return matches.every((result) => result.status === "pass") ? "pass" : "fail";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Post-run hook: runs each command expectation of this arm on a copy of the final workspace. Returns undefined when
|
|
189
|
+
* the arm has none, so its run.json gains nothing.
|
|
190
|
+
*/
|
|
191
|
+
export async function runCommandExpectations({ group, arm, run, root, directory, signal }) {
|
|
192
|
+
const expectations = group.expectations.filter(
|
|
193
|
+
(expectation) => expectation.kind === COMMAND_KIND && expectation.variant === arm,
|
|
194
|
+
);
|
|
195
|
+
if (expectations.length === 0) return undefined;
|
|
196
|
+
const records = {};
|
|
197
|
+
// A container environment runs its graders in the same image, on the graded copy.
|
|
198
|
+
const container = group.variants?.[arm]?.shell?.container;
|
|
199
|
+
for (const expectation of expectations) {
|
|
200
|
+
records[expectation.id] = await runCommandExpectation({ expectation, run, root, directory, signal, container });
|
|
201
|
+
}
|
|
202
|
+
return records;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Runs one command expectation and returns its record: `{ started: false, error }` when it could not start,
|
|
207
|
+
* otherwise exit code, signal, timeout, duration, output tails and parsed test results.
|
|
208
|
+
*/
|
|
209
|
+
export async function runCommandExpectation({ expectation, run, root, directory, signal, container }) {
|
|
210
|
+
const notStarted = (error) => ({ started: false, error });
|
|
211
|
+
if (run?.state !== "complete") return notStarted("the run did not complete");
|
|
212
|
+
if (signal?.aborted) return notStarted("cancelled");
|
|
213
|
+
if (typeof run.workspace !== "string" || !(await stat(run.workspace).catch(() => null))?.isDirectory()) {
|
|
214
|
+
return notStarted("the run recorded no workspace");
|
|
215
|
+
}
|
|
216
|
+
const commandDirectory = path.join(directory, "commands", expectation.id);
|
|
217
|
+
const copy = path.join(commandDirectory, "workspace");
|
|
218
|
+
try {
|
|
219
|
+
await rm(commandDirectory, { recursive: true, force: true });
|
|
220
|
+
await mkdir(commandDirectory, { recursive: true });
|
|
221
|
+
// The copy keeps symlinks as they are, so nothing outside the workspace is followed or copied.
|
|
222
|
+
await cp(run.workspace, copy, { recursive: true, verbatimSymlinks: true });
|
|
223
|
+
await copyGraderFiles(expectation.files ?? {}, root, copy);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return notStarted(`preparing the workspace copy failed: ${messageOf(error)}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const isolationRoot =
|
|
229
|
+
typeof run.isolation?.root === "string" ? run.isolation.root : path.join(directory, "isolation");
|
|
230
|
+
const environment = await commandEnvironment(isolationRoot, copy);
|
|
231
|
+
const timeoutMs = (expectation.timeout_seconds ?? DEFAULT_TIMEOUT_SECONDS) * 1000;
|
|
232
|
+
let argv = expectation.argv;
|
|
233
|
+
let containerName = null;
|
|
234
|
+
let clientEnvironment = null;
|
|
235
|
+
if (container) {
|
|
236
|
+
const { containerName: nameFor, dockerClientEnvironment, graderArgv } = await import("./container.mjs");
|
|
237
|
+
containerName = nameFor("grader");
|
|
238
|
+
argv = graderArgv({ container, copy, argv, name: containerName });
|
|
239
|
+
clientEnvironment = dockerClientEnvironment();
|
|
240
|
+
}
|
|
241
|
+
let result;
|
|
242
|
+
try {
|
|
243
|
+
// The docker CLI needs the user's own configuration to find its engine; the command itself runs in the image.
|
|
244
|
+
result = await runProcess(argv, {
|
|
245
|
+
cwd: copy,
|
|
246
|
+
environment: clientEnvironment ?? environment,
|
|
247
|
+
timeoutMs,
|
|
248
|
+
signal,
|
|
249
|
+
});
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return notStarted(`the command could not start: ${messageOf(error)}`);
|
|
252
|
+
} finally {
|
|
253
|
+
if (containerName !== null) await (await import("./container.mjs")).removeContainer(containerName);
|
|
254
|
+
}
|
|
255
|
+
const record = {
|
|
256
|
+
started: true,
|
|
257
|
+
argv: expectation.argv,
|
|
258
|
+
...(container ? { container: { image: container.image, workdir: container.workdir, name: containerName } } : {}),
|
|
259
|
+
directory: path.relative(directory, copy).split(path.sep).join("/"),
|
|
260
|
+
files: Object.keys(expectation.files ?? {}),
|
|
261
|
+
exitCode: result.exitCode,
|
|
262
|
+
signal: result.signal,
|
|
263
|
+
timedOut: result.timedOut,
|
|
264
|
+
cancelled: result.cancelled,
|
|
265
|
+
durationMs: result.durationMs,
|
|
266
|
+
stdout: result.stdout.summary(),
|
|
267
|
+
stderr: result.stderr.summary(),
|
|
268
|
+
tests: null,
|
|
269
|
+
};
|
|
270
|
+
const tests = expectation.pass.tests;
|
|
271
|
+
if (tests !== undefined) record.tests = await testResults(tests, result.stdout.text(), copy);
|
|
272
|
+
return record;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function copyGraderFiles(files, root, copy) {
|
|
276
|
+
const canonicalRoot = await realpath(root);
|
|
277
|
+
for (const [destination, source] of Object.entries(files)) {
|
|
278
|
+
const canonicalSource = await realpath(path.resolve(root, source));
|
|
279
|
+
if (!isContained(canonicalRoot, canonicalSource))
|
|
280
|
+
throw new Error(`grader file escapes the definition root: ${source}`);
|
|
281
|
+
// The subject controls the copy's contents: no existing component of the destination may be a symlink, so a
|
|
282
|
+
// grader file can only land inside the copy.
|
|
283
|
+
let current = copy;
|
|
284
|
+
for (const part of destination.split("/")) {
|
|
285
|
+
current = path.join(current, part);
|
|
286
|
+
const entry = await lstat(current).catch(() => null);
|
|
287
|
+
if (entry?.isSymbolicLink()) throw new Error(`grader file destination passes through a symlink: ${destination}`);
|
|
288
|
+
}
|
|
289
|
+
const target = path.join(copy, destination);
|
|
290
|
+
await rm(target, { recursive: true, force: true });
|
|
291
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
292
|
+
await cp(canonicalSource, target, { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The subject's base environment with HOME, temp, XDG and Pi directories inside the variant's isolation root. */
|
|
297
|
+
async function commandEnvironment(isolationRoot, cwd) {
|
|
298
|
+
const home = path.join(isolationRoot, "home");
|
|
299
|
+
const tmp = path.join(isolationRoot, "tmp");
|
|
300
|
+
const agent = path.join(isolationRoot, "agent");
|
|
301
|
+
for (const entry of [home, tmp, agent]) await mkdir(entry, { recursive: true });
|
|
302
|
+
// Loaded here so definitions.mjs, which validates command expectations, stays free of the host modules.
|
|
303
|
+
const { baseProcessEnvironment } = await import("./process.mjs");
|
|
304
|
+
return {
|
|
305
|
+
...baseProcessEnvironment(),
|
|
306
|
+
PWD: cwd,
|
|
307
|
+
HOME: home,
|
|
308
|
+
TMPDIR: tmp,
|
|
309
|
+
TMP: tmp,
|
|
310
|
+
TEMP: tmp,
|
|
311
|
+
XDG_CONFIG_HOME: path.join(home, ".config"),
|
|
312
|
+
XDG_DATA_HOME: path.join(home, ".local", "share"),
|
|
313
|
+
XDG_CACHE_HOME: path.join(home, ".cache"),
|
|
314
|
+
PI_CODING_AGENT_DIR: agent,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Runs argv without a shell in its own process group; a timeout or cancellation stops the whole tree. */
|
|
319
|
+
export function runProcess(argv, { cwd, environment, timeoutMs, signal }) {
|
|
320
|
+
return new Promise((resolve, reject) => {
|
|
321
|
+
const started = Date.now();
|
|
322
|
+
let child;
|
|
323
|
+
try {
|
|
324
|
+
child = spawn(argv[0], argv.slice(1), {
|
|
325
|
+
cwd,
|
|
326
|
+
env: environment,
|
|
327
|
+
shell: false,
|
|
328
|
+
detached: process.platform !== "win32",
|
|
329
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
330
|
+
});
|
|
331
|
+
} catch (error) {
|
|
332
|
+
reject(error);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const stdout = captureStream(child.stdout);
|
|
336
|
+
const stderr = captureStream(child.stderr);
|
|
337
|
+
let timedOut = false;
|
|
338
|
+
let cancelled = false;
|
|
339
|
+
let forced = null;
|
|
340
|
+
let settled = false;
|
|
341
|
+
const stop = () => {
|
|
342
|
+
signalProcessTree(child.pid, child, "SIGTERM");
|
|
343
|
+
forced ??= setTimeout(() => signalProcessTree(child.pid, child, "SIGKILL"), TERMINATION_GRACE_MS);
|
|
344
|
+
};
|
|
345
|
+
const timer = setTimeout(() => {
|
|
346
|
+
timedOut = true;
|
|
347
|
+
stop();
|
|
348
|
+
}, timeoutMs);
|
|
349
|
+
const onAbort = () => {
|
|
350
|
+
cancelled = true;
|
|
351
|
+
stop();
|
|
352
|
+
};
|
|
353
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
354
|
+
const cleanUp = () => {
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
clearTimeout(forced);
|
|
357
|
+
signal?.removeEventListener("abort", onAbort);
|
|
358
|
+
};
|
|
359
|
+
child.once("error", (error) => {
|
|
360
|
+
if (settled) return;
|
|
361
|
+
settled = true;
|
|
362
|
+
cleanUp();
|
|
363
|
+
reject(error);
|
|
364
|
+
});
|
|
365
|
+
child.once("close", (exitCode, exitSignal) => {
|
|
366
|
+
if (settled) return;
|
|
367
|
+
settled = true;
|
|
368
|
+
cleanUp();
|
|
369
|
+
// Anything the command left running in its process group is stopped too.
|
|
370
|
+
if (processGroupExists(child.pid)) signalProcessTree(child.pid, child, "SIGKILL");
|
|
371
|
+
resolve({
|
|
372
|
+
exitCode,
|
|
373
|
+
signal: exitSignal,
|
|
374
|
+
timedOut,
|
|
375
|
+
cancelled,
|
|
376
|
+
durationMs: Date.now() - started,
|
|
377
|
+
stdout,
|
|
378
|
+
stderr,
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Keeps the first PARSE_LIMIT_BYTES of a stream for parsing and its newest bytes for the recorded tail. */
|
|
385
|
+
function captureStream(stream) {
|
|
386
|
+
const head = [];
|
|
387
|
+
let headBytes = 0;
|
|
388
|
+
let tail = Buffer.alloc(0);
|
|
389
|
+
let bytes = 0;
|
|
390
|
+
const tailBytes = OUTPUT_TAIL_CHARACTERS * 4;
|
|
391
|
+
stream.on("data", (chunk) => {
|
|
392
|
+
bytes += chunk.length;
|
|
393
|
+
if (headBytes < PARSE_LIMIT_BYTES) {
|
|
394
|
+
const kept = chunk.subarray(0, PARSE_LIMIT_BYTES - headBytes);
|
|
395
|
+
head.push(kept);
|
|
396
|
+
headBytes += kept.length;
|
|
397
|
+
}
|
|
398
|
+
tail = Buffer.concat([tail, chunk]);
|
|
399
|
+
if (tail.length > tailBytes) tail = tail.subarray(tail.length - tailBytes);
|
|
400
|
+
});
|
|
401
|
+
return {
|
|
402
|
+
text: () => Buffer.concat(head).toString("utf8"),
|
|
403
|
+
summary() {
|
|
404
|
+
const text = tail.toString("utf8");
|
|
405
|
+
const kept = text.slice(-OUTPUT_TAIL_CHARACTERS);
|
|
406
|
+
return { tail: kept, bytes, truncated: kept.length < text.length || tail.length < bytes };
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function testResults(tests, stdout, copy) {
|
|
412
|
+
if (tests.format !== "junit") return { format: tests.format, results: parseTap(stdout), error: null };
|
|
413
|
+
if (tests.report === undefined) return { format: "junit", results: parseJunit(stdout), error: null };
|
|
414
|
+
const report = path.join(copy, tests.report);
|
|
415
|
+
try {
|
|
416
|
+
const canonical = await realpath(report);
|
|
417
|
+
if (!isContained(await realpath(copy), canonical)) throw new Error("the report is outside the workspace copy");
|
|
418
|
+
return {
|
|
419
|
+
format: "junit",
|
|
420
|
+
report: tests.report,
|
|
421
|
+
results: parseJunit(await readFile(canonical, "utf8")),
|
|
422
|
+
error: null,
|
|
423
|
+
};
|
|
424
|
+
} catch (error) {
|
|
425
|
+
return {
|
|
426
|
+
format: "junit",
|
|
427
|
+
report: tests.report,
|
|
428
|
+
results: [],
|
|
429
|
+
error: `cannot read the JUnit report: ${messageOf(error)}`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Test results from TAP, including Node's test runner output: nested subtests are indented, announced by
|
|
436
|
+
* `# Subtest: <name>` and reported after their children, so a nested test's full name joins its parents' with " > ".
|
|
437
|
+
*
|
|
438
|
+
* @returns {Array<{ name: string, fullName: string, status: "pass" | "fail" | "skip" | "todo" }>}
|
|
439
|
+
*/
|
|
440
|
+
export function parseTap(text) {
|
|
441
|
+
const results = [];
|
|
442
|
+
const names = [];
|
|
443
|
+
let yamlIndent = null;
|
|
444
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
445
|
+
const indent = line.length - line.trimStart().length;
|
|
446
|
+
const content = line.trim();
|
|
447
|
+
if (yamlIndent !== null) {
|
|
448
|
+
if (content === "..." && indent <= yamlIndent) yamlIndent = null;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (content === "---") {
|
|
452
|
+
yamlIndent = indent;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const depth = Math.floor(indent / 4);
|
|
456
|
+
const subtest = content.match(/^# Subtest: (.*)$/);
|
|
457
|
+
if (subtest) {
|
|
458
|
+
names.length = depth;
|
|
459
|
+
names[depth] = subtest[1];
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const point = content.match(/^(not ok|ok)\b(?:\s+\d+)?(?:\s+-)?\s*(.*)$/);
|
|
463
|
+
if (!point) continue;
|
|
464
|
+
const { description, directive } = splitDirective(point[2]);
|
|
465
|
+
const failed = point[1] === "not ok";
|
|
466
|
+
let status = failed ? "fail" : "pass";
|
|
467
|
+
if (/^skip\b/i.test(directive)) status = "skip";
|
|
468
|
+
else if (/^todo\b/i.test(directive)) status = "todo";
|
|
469
|
+
const parents = names.slice(0, depth).filter((name) => name !== undefined);
|
|
470
|
+
results.push({ name: description, fullName: [...parents, description].join(" > "), status });
|
|
471
|
+
names.length = depth;
|
|
472
|
+
}
|
|
473
|
+
return results;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Splits a TAP description at its first unescaped "#", and unescapes "\#" and "\\".
|
|
477
|
+
function splitDirective(value) {
|
|
478
|
+
let description = "";
|
|
479
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
480
|
+
const character = value[index];
|
|
481
|
+
if (character === "\\" && (value[index + 1] === "#" || value[index + 1] === "\\")) {
|
|
482
|
+
description += value[index + 1];
|
|
483
|
+
index += 1;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (character === "#") return { description: description.trim(), directive: value.slice(index + 1).trim() };
|
|
487
|
+
description += character;
|
|
488
|
+
}
|
|
489
|
+
return { description: description.trim(), directive: "" };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Test results from a JUnit XML report: each `<testcase>` passes unless it holds `<failure>` or `<error>`, and is
|
|
494
|
+
* skipped when it holds `<skipped>`. Its full name is `<classname> > <name>` when it has a class name.
|
|
495
|
+
*/
|
|
496
|
+
export function parseJunit(text) {
|
|
497
|
+
const results = [];
|
|
498
|
+
const testcase = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase\s*>)/g;
|
|
499
|
+
for (const match of String(text).matchAll(testcase)) {
|
|
500
|
+
const attributes = {};
|
|
501
|
+
for (const attribute of match[1].matchAll(/([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g)) {
|
|
502
|
+
attributes[attribute[1]] = decodeXml(attribute[2] ?? attribute[3]);
|
|
503
|
+
}
|
|
504
|
+
const name = attributes.name ?? "";
|
|
505
|
+
const body = match[2] ?? "";
|
|
506
|
+
let status = "pass";
|
|
507
|
+
if (/<(failure|error)\b/.test(body)) status = "fail";
|
|
508
|
+
else if (/<skipped\b/.test(body)) status = "skip";
|
|
509
|
+
const classname = attributes.classname ?? "";
|
|
510
|
+
results.push({ name, fullName: classname === "" ? name : `${classname} > ${name}`, status });
|
|
511
|
+
}
|
|
512
|
+
return results;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function decodeXml(value) {
|
|
516
|
+
return value.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (entity, code) => {
|
|
517
|
+
const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" }[code.toLowerCase()];
|
|
518
|
+
if (named !== undefined) return named;
|
|
519
|
+
const point =
|
|
520
|
+
code[1].toLowerCase() === "x" ? Number.parseInt(code.slice(2), 16) : Number.parseInt(code.slice(1), 10);
|
|
521
|
+
return Number.isFinite(point) ? String.fromCodePoint(point) : entity;
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function messageOf(error) {
|
|
526
|
+
return error instanceof Error ? error.message : String(error);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The `scores` aggregate section: each test-graded command check's score (passed required tests over required)
|
|
531
|
+
* per arm (count, mean, median over the trials that scored it) and per trial as the measure `score.<expectation id>`,
|
|
532
|
+
* and also as `score.<comparison id>` when the expectation belongs to a comparison, so a suite decision can name one
|
|
533
|
+
* measure that every arm reports. A missing or unavailable score is null. Nothing is recorded for a group without
|
|
534
|
+
* test-graded command checks.
|
|
535
|
+
*/
|
|
536
|
+
export function aggregateScores({ arms, trials }) {
|
|
537
|
+
const scored = new Map();
|
|
538
|
+
for (const trial of trials) {
|
|
539
|
+
for (const check of trial.grade?.checks ?? []) {
|
|
540
|
+
if (check.kind === COMMAND_KIND && check.expected?.pass?.tests !== undefined) scored.set(check.id, check.variant);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (scored.size === 0) return undefined;
|
|
544
|
+
const perTrial = [];
|
|
545
|
+
const byArm = Object.fromEntries(arms.map((arm) => [arm, {}]));
|
|
546
|
+
for (const trial of trials) {
|
|
547
|
+
const checks = new Map((trial.grade?.checks ?? []).map((check) => [check.id, check]));
|
|
548
|
+
const comparisonOf = new Map();
|
|
549
|
+
for (const comparison of trial.grade?.comparisons ?? []) {
|
|
550
|
+
for (const outcome of Object.values(comparisonArms(comparison).arms ?? {})) {
|
|
551
|
+
if (typeof outcome?.check === "string") comparisonOf.set(outcome.check, comparison.id);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
for (const arm of arms) {
|
|
555
|
+
const values = {};
|
|
556
|
+
for (const [id, variant] of scored) {
|
|
557
|
+
if (variant !== arm) continue;
|
|
558
|
+
// A run held for transcript flags no one has cleared scores nothing until it is cleared.
|
|
559
|
+
const score = trial.held?.includes(arm) ? null : checks.get(id)?.observed?.score;
|
|
560
|
+
const value = typeof score === "number" && Number.isFinite(score) ? score : null;
|
|
561
|
+
values[`score.${id}`] = value;
|
|
562
|
+
if (comparisonOf.has(id)) values[`score.${comparisonOf.get(id)}`] = value;
|
|
563
|
+
(byArm[arm][id] ??= []).push(value);
|
|
564
|
+
}
|
|
565
|
+
perTrial.push({ trial: trial.id, arm, values });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const summaries = Object.fromEntries(
|
|
569
|
+
Object.entries(byArm).map(([arm, ids]) => [
|
|
570
|
+
arm,
|
|
571
|
+
Object.fromEntries(Object.entries(ids).map(([id, values]) => [id, summarizeScores(values)])),
|
|
572
|
+
]),
|
|
573
|
+
);
|
|
574
|
+
return { arms: summaries, perTrial };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function summarizeScores(values) {
|
|
578
|
+
const measured = values.filter((value) => value !== null).sort((left, right) => left - right);
|
|
579
|
+
const count = measured.length;
|
|
580
|
+
if (count === 0) return { count: 0, unavailable: values.length, mean: null, median: null };
|
|
581
|
+
const middle = Math.floor(count / 2);
|
|
582
|
+
return {
|
|
583
|
+
count,
|
|
584
|
+
unavailable: values.length - count,
|
|
585
|
+
mean: measured.reduce((total, value) => total + value, 0) / count,
|
|
586
|
+
median: count % 2 === 1 ? measured[middle] : (measured[middle - 1] + measured[middle]) / 2,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
registerPostRunHook("commands", runCommandExpectations);
|
|
591
|
+
registerAggregator("scores", aggregateScores);
|