From 706570b823acd093f1176c423a4910e16cc719a0 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Fri, 14 Aug 2026 23:59:40 +0800 Subject: [PATCH 01/25] feat(harness): add production-ready harness DSL package Add a Langium-based Harness as Code compiler, versioned IR resolver, and Qoder/Pi SDK executors with honest materialization semantics and fail-closed host and runtime handling. Wire the package into the root workspace and cross-platform CI, and harden package verification. Implements docs/specs/2026-08-14-harness-as-code-v0.1.md. Validated with npm run check, focused documentation-link tests, npm audit, a cold package install, and a native Qoder SDK smoke. Co-authored-by: Codex (GPT 5.6 Sol) --- .github/workflows/ci.yml | 6 + docs/specs/2026-08-14-harness-as-code-v0.1.md | 126 + package-lock.json | 5743 +++++++++++++++-- package.json | 9 +- packages/harness/LICENSE | 21 + packages/harness/README.md | 116 + .../harness/examples/standard-coding.harness | 93 + packages/harness/langium-config.json | 11 + packages/harness/package.json | 78 + packages/harness/src/compiler/compile.ts | 356 + packages/harness/src/exec/executor.ts | 87 + packages/harness/src/exec/index.ts | 24 + packages/harness/src/exec/pi-sdk.ts | 210 + packages/harness/src/exec/qoder-sdk.ts | 152 + .../harness/src/highlight/harness-grammar.ts | 82 + packages/harness/src/highlight/shiki.ts | 45 + packages/harness/src/index.ts | 18 + packages/harness/src/ir/canonical.ts | 30 + packages/harness/src/ir/index.ts | 231 + .../harness/src/language/generated/ast.ts | 635 ++ .../harness/src/language/generated/grammar.ts | 1292 ++++ .../harness/src/language/generated/module.ts | 25 + .../harness/src/language/harness-module.ts | 37 + .../harness/src/language/harness-validator.ts | 57 + packages/harness/src/language/harness.langium | 112 + packages/harness/src/resolver/resolve.ts | 309 + packages/harness/test/compile.test.ts | 210 + packages/harness/test/exec.test.ts | 398 ++ packages/harness/test/highlight.test.ts | 62 + packages/harness/test/resolve.test.ts | 247 + packages/harness/tsconfig.json | 16 + packages/harness/vitest.config.ts | 10 + scripts/npm-package/verify-pack.mjs | 3 + 33 files changed, 10193 insertions(+), 658 deletions(-) create mode 100644 docs/specs/2026-08-14-harness-as-code-v0.1.md create mode 100644 packages/harness/LICENSE create mode 100644 packages/harness/README.md create mode 100644 packages/harness/examples/standard-coding.harness create mode 100644 packages/harness/langium-config.json create mode 100644 packages/harness/package.json create mode 100644 packages/harness/src/compiler/compile.ts create mode 100644 packages/harness/src/exec/executor.ts create mode 100644 packages/harness/src/exec/index.ts create mode 100644 packages/harness/src/exec/pi-sdk.ts create mode 100644 packages/harness/src/exec/qoder-sdk.ts create mode 100644 packages/harness/src/highlight/harness-grammar.ts create mode 100644 packages/harness/src/highlight/shiki.ts create mode 100644 packages/harness/src/index.ts create mode 100644 packages/harness/src/ir/canonical.ts create mode 100644 packages/harness/src/ir/index.ts create mode 100644 packages/harness/src/language/generated/ast.ts create mode 100644 packages/harness/src/language/generated/grammar.ts create mode 100644 packages/harness/src/language/generated/module.ts create mode 100644 packages/harness/src/language/harness-module.ts create mode 100644 packages/harness/src/language/harness-validator.ts create mode 100644 packages/harness/src/language/harness.langium create mode 100644 packages/harness/src/resolver/resolve.ts create mode 100644 packages/harness/test/compile.test.ts create mode 100644 packages/harness/test/exec.test.ts create mode 100644 packages/harness/test/highlight.test.ts create mode 100644 packages/harness/test/resolve.test.ts create mode 100644 packages/harness/tsconfig.json create mode 100644 packages/harness/vitest.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4675ee6..b05ffbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,12 @@ jobs: - run: npm ci - name: Run Vitest run: npm run test:ci + - name: Verify Harness DSL generated sources + run: npm run harness:generated + - name: Build Harness DSL package + run: npm run harness:build + - name: Test Harness DSL package + run: npm run harness:test - name: Upload JUnit test results if: ${{ always() }} uses: actions/upload-artifact@v4 diff --git a/docs/specs/2026-08-14-harness-as-code-v0.1.md b/docs/specs/2026-08-14-harness-as-code-v0.1.md new file mode 100644 index 0000000..6067b9f --- /dev/null +++ b/docs/specs/2026-08-14-harness-as-code-v0.1.md @@ -0,0 +1,126 @@ +# Make Harness as Code safe to install and execute + +## Traceability + +- Spec ID: harness-as-code-v0.1 +- Status: Implemented + +## Intent + +Ship the first production-ready `@qoder-ai/harness` package as a parseable, +lockable, and explainable assembly language for coding-agent harnesses. A +resolved revision must distinguish the strength declared by a host binding from +the strength the v0.1 executor can actually materialize, reject unsafe or +ambiguous assemblies before execution, and install, build, test, and pack from +the repository's normal dependency and CI workflow. + +The v0.1 executors materialize components as prompt instructions. They may +therefore realize at most `advisory` strength. Bindings may declare future +`wired` or `enforced` host mechanisms, but those declarations are reported as +degraded rather than presented as observed runtime guarantees. + +## Acceptance Scenarios + +- AC-1: One or more `.harness` sources compile into schema-valid versioned IR, + with stable diagnostics for parser, linker, semantic, and cross-file + declaration conflicts. +- AC-2: Duplicate component ids, composition ids, exact plugin versions, or + component/host bindings fail compilation even when the conflicting + declarations are split across source files. Invalid exact semantic versions, + duplicate plugin includes, and repeated component inputs/outputs also fail + before resolution. +- AC-3: Resolution records both declared and materialized strength. The v0.1 + runtime caps materialized strength at `advisory`, applies `minimum`, + `preferred`, and `on-degrade` to that effective strength, and never describes + an unmaterialized `wired` or `enforced` mechanism as realized. +- AC-4: An executor rejects a revision whose target host differs from the + executor host before starting an SDK session or child process. +- AC-5: Qoder execution uses the official Agent SDK with explicit authentication, + cwd, streaming output, and tool-authorization seams. Pi execution uses the + current non-deprecated SDK and `ModelRuntime` API, honors the task working + directory, disables native tools in the advisory-only v0.1 path, and reports + a missing optional runtime dependency with actionable guidance. Pi model + failures propagate through the run result and sessions are disposed after a + run. Qoder streams that terminate without a result message fail closed. +- AC-6: Pi package materialization emits `SKILL.md` only for advisory + skill-kind contracts, produces valid front matter for arbitrary DSL string + descriptions, stamps revision provenance, and fails closed when the target + directory already contains files. +- AC-7: The repository's root install and CI workflow installs this workspace, + regenerates the Langium sources, type-checks/builds the package, runs its + behavior tests on Windows, macOS, and Linux, and detects stale generated + artifacts. +- AC-8: `npm pack` contains the documented public exports, declarations, + example, README, and license without source-local dependencies, caches, or + absolute paths; a clean temporary install can import and exercise the public + API. +- AC-9: The package development guide describes supported Node and Pi SDK + versions, the declared-versus-materialized strength boundary, focused + commands, and current v0.1 limitations without claiming native host evidence. + +## Non-goals + +- Installing or managing native Qoder hooks or Pi extensions in v0.1. +- Claiming `wired` or `enforced` runtime strength without a future native + materialization and evidence-receipt contract. +- Adding another Coding Agent host adapter. +- Publishing the package to the npm registry in this change. +- Changing Better Harness release notes, versions, roadmaps, or changelog. +- Providing a full CLI, language server, editor extension, remote registry, or + dependency solver beyond the in-bundle semver selection API. + +## Plan and Tasks + +1. Tighten compiler semantic validation across the complete source set and + keep diagnostics source/line-qualified. +2. Extend realization IR with declared strength and make the v0.1 resolver + compute effective advisory materialization before applying degradation + policy. +3. Add shared executor host validation, integrate the official Qoder and Pi + SDKs, and make Pi skill materialization kind-safe and front-matter-safe. +4. Add the package as a root npm workspace, refresh the lockfile, expose root + build/test/generated-source gates, and wire them into the OS matrix CI. +5. Expand behavior tests for every repaired failure mode, public exports, + clean-pack import, and generated artifacts. +6. Validate a clean install, build, focused tests, root gates, package contents, + temporary consumer import, and locally available host surfaces. + +## Test and Review Evidence + +- AC-1/AC-2: package compiler tests use multi-source duplicate fixtures and + assert structured diagnostics; they pass as part of the 38-test package run. +- AC-3: resolver tests cover satisfied, degraded-report, degraded-fail, and + below-minimum paths against declared and materialized strengths. +- AC-4/AC-5: executor tests assert that mismatched hosts never invoke SDK + loaders, that Qoder uses explicit auth/tool options, and that Pi uses + `ModelRuntime.create` with the task cwd, selected model, and `noTools: "all"`. + Installed-package contract tests load both SDKs. A native Qoder SDK smoke + returned exactly `HARNESS_QODER_REVIEW_OK`; native Pi inference was not run + because this process had neither `DEEPSEEK_API_KEY` nor configured DeepSeek + authentication. +- AC-6: temporary-directory tests inspect the returned file set and parse-safe + generated front matter for skill and non-skill contracts. +- AC-7: `npm ci --ignore-scripts --registry=https://registry.npmjs.org`, an + explicit Qoder SDK rebuild to restore its intentionally skipped worker + postinstall, and `npm run check` passed. The final run covered 1,324 + repository tests, 38 package tests, generated-source verification, + TypeScript build, and pack verification. CI runs package gates on Windows, + macOS, and Linux with Node 22.20.0 plus Linux with Node 24. +- AC-8: `npm pack --workspace @qoder-ai/harness` ran the package prepack gate + and produced a 36-file, 27.2 kB tarball without the removed executor or + source-local artifacts. A clean temporary install audited 160 dependencies + with zero vulnerabilities and completed a Node ESM import/compile/resolve + smoke through the public API. +- AC-9: README review against the package manifest and observed validation + commands. +- Full regression: `npm run check` passed; root pack verification reported 523 + npm entries and 545 runtime-zip entries. The focused documentation link graph + passed all six assertions, and `npm audit --audit-level=high` reported zero + vulnerabilities after upgrading Pi to 0.84.2. +- Risk: generated Langium output can drift from its grammar. CI regenerates and + rejects a non-empty generated-source diff. +- Risk: adding a root workspace changes the lockfile and install graph. Review + the final lockfile diff and keep package dependencies scoped to the workspace. +- Risk: native host and model availability vary by machine. Deterministic tests + remain authoritative; native smoke is reported separately and never replaces + package or fixture evidence. diff --git a/package-lock.json b/package-lock.json index a1f8715..a3ab0ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "@qoder-ai/better-harness", "version": "0.6.3", "license": "MIT", + "workspaces": [ + "packages/harness" + ], "dependencies": { "@vscode/tree-sitter-wasm": "0.3.1", "esbuild-wasm": "0.28.1" @@ -23,949 +26,4978 @@ "npm": ">=10.9.3 <12.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" + "node_modules/@chevrotain/cst-dts-gen": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", + "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "12.0.0", + "@chevrotain/types": "12.0.0" + } }, - "node_modules/@oxc-project/types": { - "version": "0.144.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", - "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "node_modules/@chevrotain/gast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", + "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "12.0.0" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", - "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", - "cpu": [ - "arm64" - ], + "node_modules/@chevrotain/regexp-to-ast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", + "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", + "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", + "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.84.2.tgz", + "integrity": "sha512-l4E+B7hgXKWddRo8bC/eSue2aWZjEgJ9xIpf5p0Og+lq8a2TArCwJ0HCoCPCgaBP/tN4zbYH/wOwvx9pJpeLCA==", "dev": true, + "hasShrinkwrap": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@earendil-works/pi-agent-core": "^0.84.2", + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-client": "^0.84.2", + "@earendil-works/pi-protocol": "^0.84.2", + "@earendil-works/pi-tui": "^0.84.2", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "grok-mermaid": "0.2.2", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.3.7", + "undici": "8.9.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", - "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=16.0.0" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", - "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=16.0.0" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", - "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", - "cpu": [ - "arm" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", - "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", - "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", - "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", - "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", - "cpu": [ - "s390x" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", - "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", - "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", - "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", - "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", - "cpu": [ - "arm64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", - "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=20.0.0" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">= 14.0.0" } }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "tinyrainbow": "^3.1.0" + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.1.tgz", - "integrity": "sha512-RJFoomET6FajjG511fmQxeBQfU6M24a0aFZPqpid+ttIxanWf1VGytBG0UmsGjt07qmIPJS8U31D+aecuCucsQ==", - "license": "MIT" - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18.0.0" } }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=6.9.0" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.84.2.tgz", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.84.2", + "@earendil-works/pi-telemetry": "^0.84.2", + "diff": "8.0.4", + "ignore": "7.0.5", + "typebox": "1.3.7", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.84.2.tgz", "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.84.2", + "@google/genai": "1.52.0", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.40.0", + "partial-json": "0.1.7", + "typebox": "1.3.7" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, "engines": { - "node": ">=8" + "node": ">=22.19.0" } }, - "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-client": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-client/-/pi-client-0.84.2.tgz", "dev": true, - "license": "MIT" - }, - "node_modules/esbuild-wasm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.28.1.tgz", - "integrity": "sha512-p/GD4E8oYRjg3kjdKrnMb0s4PzXgJF42e0MF4H0+ACyK/kIlFRp3e0fzOleIG+wBBm6MM3XQrbpe7soEA+vJIA==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "@earendil-works/pi-protocol": "^0.84.2" }, "engines": { - "node": ">=18" + "node": ">=22.19.0" } }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-protocol": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-protocol/-/pi-protocol-0.84.2.tgz", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0" + "typebox": "1.3.7" + }, + "engines": { + "node": ">=22.19.0" } }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-telemetry": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.84.2.tgz", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">=22.19.0" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.84.2", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.84.2.tgz", "dev": true, "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, "engines": { - "node": ">=12.0.0" + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" }, "peerDependencies": { - "picomatch": "^3 || ^4" + "@modelcontextprotocol/sdk": "^1.25.2" }, "peerDependenciesMeta": { - "picomatch": { + "@modelcontextprotocol/sdk": { "optional": true } } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", - "cpu": [ - "x64" - ], + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", "cpu": [ "x64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ - "freebsd" + "darwin" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", "cpu": [ - "arm" + "arm64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">= 10" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", "cpu": [ "x64" ], "dev": true, - "license": "MPL-2.0", + "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "node": ">= 10" } }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", "dev": true, "funding": [ { "type": "github", - "url": "https://github.com/sponsors/ai" + "url": "https://github.com/sponsors/nodable" } ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=8.0.0" } }, - "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT", + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=12.20.0" + "node": ">=18.0.0" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", "dev": true, - "license": "MIT" + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/grok-mermaid": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/grok-mermaid/-/grok-mermaid-0.2.2.tgz", + "integrity": "sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", + "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.1.tgz", + "integrity": "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@qoder-ai/harness": { + "resolved": "packages/harness", + "link": true + }, + "node_modules/@qoder-ai/qoder-agent-sdk": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/@qoder-ai/qoder-agent-sdk/-/qoder-agent-sdk-1.0.21.tgz", + "integrity": "sha512-G+pLA5HYZRjbytpDF7nlumbEgLvntA+yL2IGGy7BO6OtRI0c9RfzjwGnVNqr3mHQeiF7mi9b4jmrBNtI8I1wAA==", + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.27.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.52", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", + "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", + "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "license": "ISC" + }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vscode/tree-sitter-wasm": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.1.tgz", + "integrity": "sha512-RJFoomET6FajjG511fmQxeBQfU6M24a0aFZPqpid+ttIxanWf1VGytBG0UmsGjt07qmIPJS8U31D+aecuCucsQ==", + "license": "MIT" + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chevrotain": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", + "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "12.0.0", + "@chevrotain/gast": "12.0.0", + "@chevrotain/regexp-to-ast": "12.0.0", + "@chevrotain/types": "12.0.0", + "@chevrotain/utils": "12.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.4.tgz", + "integrity": "sha512-HKjrnG6rGOkaxoOx8B4oKRYoD8Gdt6wCL5ItwAtZPgS5kI6rKyezUyWKCFVr6nLYQXT6/BLVVp5b00xBbnjakQ==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.18.1" + }, + "peerDependencies": { + "chevrotain": "^12.0.0" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.28.1.tgz", + "integrity": "sha512-p/GD4E8oYRjg3kjdKrnMb0s4PzXgJF42e0MF4H0+ACyK/kIlFRp3e0fzOleIG+wBBm6MM3XQrbpe7soEA+vJIA==", + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", + "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", + "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hono": { + "version": "4.13.2", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.2.tgz", + "integrity": "sha512-JydRilDRkYBQMt9qR9U92mXxmbGqsqSn/IKOrh4e7/gEbn+0zSr8igTu0obwJoNGN4sez28DIql7FBHWydoJpA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", + "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", + "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/langium": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.3.1.tgz", + "integrity": "sha512-4lmkLHytLXD5pvyL0PKUj1LCNpgzv9s8luH+GbWXHG45QLmO6PlFXrCUghDN5BfcFTlaQyXpXiaSl8A28Xep8w==", + "license": "MIT", + "dependencies": { + "@chevrotain/regexp-to-ast": "~12.0.0", + "chevrotain": "~12.0.0", + "chevrotain-allstar": "~0.4.3", + "vscode-languageserver": "~10.0.1", + "vscode-languageserver-protocol": "~3.18.1", + "vscode-languageserver-textdocument": "~1.0.13", + "vscode-languageserver-types": "~3.18.0", + "vscode-uri": "~3.1.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/langium-cli": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/langium-cli/-/langium-cli-4.3.0.tgz", + "integrity": "sha512-+TtgkhX8Z3exRBJvTFWV1semhWUCnWMhMP+ZKjpKIeyY6GgnQHehf3ZWyCvVL5T95uqgWZb77aoq9rhI2MPNTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "~5.6.2", + "commander": "~15.0.0", + "fs-extra": "~11.3.5", + "jsonschema": "~1.5.0", + "langium": "~4.3.0", + "langium-railroad": "~4.3.0", + "lodash": "~4.18.1" + }, + "bin": { + "langium": "bin/langium.js" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/langium-railroad": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/langium-railroad/-/langium-railroad-4.3.0.tgz", + "integrity": "sha512-dbuv149MQe8XcgM48FagH22JBaqN0MvN9RRJKBdRQWDeHUYAASliiOtLdMfgOl3kV+nLx7idSc+LLdcBSduvHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "langium": "~4.3.0", + "railroad-diagrams": "~1.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/railroad-diagrams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", + "integrity": "sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } }, - "node_modules/picocolors": { + "node_modules/side-channel": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/rolldown": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", - "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", - "dev": true, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.144.0", - "@rolldown/pluginutils": "^1.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" }, - "bin": { - "rolldown": "bin/cli.mjs" + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 0.4" }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.4", - "@rolldown/binding-darwin-arm64": "1.2.4", - "@rolldown/binding-darwin-x64": "1.2.4", - "@rolldown/binding-freebsd-x64": "1.2.4", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", - "@rolldown/binding-linux-arm64-gnu": "1.2.4", - "@rolldown/binding-linux-arm64-musl": "1.2.4", - "@rolldown/binding-linux-ppc64-gnu": "1.2.4", - "@rolldown/binding-linux-s390x-gnu": "1.2.4", - "@rolldown/binding-linux-x64-gnu": "1.2.4", - "@rolldown/binding-linux-x64-musl": "1.2.4", - "@rolldown/binding-openharmony-arm64": "1.2.4", - "@rolldown/binding-win32-arm64-msvc": "1.2.4", - "@rolldown/binding-win32-x64-msvc": "1.2.4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/siginfo": { @@ -985,6 +5017,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -992,6 +5034,15 @@ "dev": true, "license": "MIT" }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", @@ -999,6 +5050,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -1043,6 +5108,222 @@ "node": ">=14.0.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", @@ -1211,6 +5492,89 @@ } } }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.1.tgz", + "integrity": "sha512-rfuA6T75H6m5EkbhtEPzre9pT0HPcDI2MMy4+nPFIBks5J8JBAUHD4tRYSgaBOijIEC7SRkC1kKyXTLqbmh9jw==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-10.0.1.tgz", + "integrity": "sha512-hfERoZvpaHWzQa7t5mMIrHkEOhF7aTs1dpktem0TW/Xs4QZD7NtcHjIq/aE1CZJEj6Rb8LSGUkBhvTRDdpyGig==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.18.1" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.2.tgz", + "integrity": "sha512-XRyDbT0Pp3sSNti3JmxVEUMySWCSi1hhM+/KUlCy1hV1zmrqpM1OwO12EAki8blhmLuIMpaJrYbo0OzGVfK2Qg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.1", + "vscode-languageserver-types": "3.18.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.13.tgz", + "integrity": "sha512-nx0ZHwMGIsVkzFG3/VLeJYBLTaFBRuNdGDvevvjuoayU5EOS2fEYazOhtCM3PI9ClMMg5igc0uwXtAq4tJj+Dw==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.0.tgz", + "integrity": "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g==", + "license": "MIT" + }, + "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0.tgz", + "integrity": "sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.1.tgz", + "integrity": "sha512-RTiiVHdpxpYcJVI5sq6S5TLjQ4WDR/rBrIWru+kPXe6sGQ9PFQ3GamrTKLvPqbR4ylr1SoodhmcqbFII0WXVuw==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.0", + "vscode-languageserver-types": "3.18.0" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -1227,6 +5591,71 @@ "engines": { "node": ">=8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "packages/harness": { + "name": "@qoder-ai/harness", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@qoder-ai/qoder-agent-sdk": "1.0.21", + "@sinclair/typebox": "0.34.52", + "langium": "4.3.1", + "semver": "7.8.5", + "shiki": "4.4.3" + }, + "devDependencies": { + "@earendil-works/pi-coding-agent": "0.84.2", + "@types/node": "26.2.0", + "@types/semver": "7.8.0", + "langium-cli": "4.3.0", + "typescript": "7.0.2", + "vitest": "4.1.10" + }, + "engines": { + "node": ">=22.20.0 <25.0.0" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "^0.84.2" + }, + "peerDependenciesMeta": { + "@earendil-works/pi-coding-agent": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 93e2426..c32a521 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "pi-package" ], "type": "module", + "workspaces": [ + "packages/harness" + ], "pi": { "skills": [ "./skills" @@ -55,6 +58,7 @@ "!docs/scripts/", "!docs/build/", "!docs/.docusaurus/", + "!docs/node_modules/", "!docs/docusaurus.config.js", "!docs/sidebars.js", "!docs/package.json", @@ -74,7 +78,10 @@ }, "scripts": { "bundle:zip": "node scripts/npm-package/create-bundle.mjs", - "check": "npm test && npm run pack:verify", + "check": "npm test && npm run harness:generated && npm run harness:build && npm run harness:test && npm run pack:verify", + "harness:build": "npm run build -w @qoder-ai/harness", + "harness:generated": "npm run check:generated -w @qoder-ai/harness", + "harness:test": "npm test -w @qoder-ai/harness", "inspector": "node scripts/harness-inspector/cli.mjs render", "inspector:open": "node scripts/harness-inspector/cli.mjs render --open", "pack:verify": "node scripts/npm-package/verify-pack.mjs", diff --git a/packages/harness/LICENSE b/packages/harness/LICENSE new file mode 100644 index 0000000..e5b5c28 --- /dev/null +++ b/packages/harness/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Qoder + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/harness/README.md b/packages/harness/README.md new file mode 100644 index 0000000..121de03 --- /dev/null +++ b/packages/harness/README.md @@ -0,0 +1,116 @@ +# @qoder-ai/harness + +Harness as Code, v0.1: a parseable, lockable, explainable **assembly language** +for coding-agent harnesses. It is deliberately *not* a unified execution +language across agent runtimes — v0.1 answers one bounded question: + +> What did the composition request, what strength did its binding declare, and +> what did the selected v0.1 executor actually materialize? + +## Pipeline + +```text +.harness sources ──parse──▶ Langium AST ──lower──▶ versioned JSON IR (TypeBox) + │ + resolveComposition() + │ + ┌────────────────────┴───────────────────┐ + HarnessRevision ResolutionReport + (immutable run fact, hr_*) (requested vs realized strength) + │ + ┌──────────────┴──────────────┐ + PiSdkExecutor QoderSdkExecutor + (@earendil-works/pi-coding-agent) (@qoder-ai/qoder-agent-sdk) +``` + +## The v0.1 entities + +| Entity | Role | +| --- | --- | +| `ComponentContract` | What a capability is, needs, and produces — host-independent | +| `TargetBinding` | The strongest mechanism a host binding declares (`unsupported` / `advisory` / `wired` / `enforced`) | +| `PluginManifest` | Distribution boundary: contracts plus their bindings | +| `CompositionSpec` | Desired assembly for a run target, with strength floors and `on-degrade` policy | +| `HarnessRevision` | Immutable resolution fact: exact versions, hashes, declared and materialized strengths, and permissions | +| `ResolutionReport` | Requested, declared, and materialized strength for every requirement | + +All IR documents carry `irVersion` and a `kind` discriminator and are defined +as TypeBox schemas in +[`src/ir/index.ts`](https://github.com/QoderAI/better-harness/blob/main/packages/harness/src/ir/index.ts). + +## Quick start + +```ts +import { + compileHarness, + resolveComposition, + QoderSdkExecutor, + highlightHarness, +} from "@qoder-ai/harness"; + +const compiled = await compileHarness(source); +if (!compiled.bundle) { + throw new Error(compiled.diagnostics.map((item) => item.message).join("\n")); +} + +const { revision, report } = resolveComposition( + compiled.bundle, + "standard-coding-qoder", +); + +if (!revision) { + throw new Error(report.errors.join("\n")); +} + +// Defaults to the locally signed-in qodercli identity. Production and CI +// callers should inject a PAT or service-account auth factory instead. +const executor = new QoderSdkExecutor(); +const result = await executor.execute(revision, compiled.bundle, { + prompt: "Explain the repository in one sentence.", + cwd: process.cwd(), +}); + +const html = await highlightHarness(source); // Shiki, lang: "harness" +``` + +See [`examples/standard-coding.harness`](examples/standard-coding.harness) for +the full surface syntax. + +## Executor honesty rules + +v0.1 executors materialize components as prompt guidance, so effective strength +is capped at `advisory`. A binding can declare a future `wired` or `enforced` +mechanism, but resolution compares the composition's minimum and preferred +strength against the advisory materialization. Below-minimum assemblies fail +before execution; allowed degradation is visible in the report and run warnings. + +Executors reject revisions targeting another host. `materializePiPackage()` +emits an installable Pi package only for advisory, skill-kind components and +stamps every generated skill with revision provenance. + +The Qoder executor uses `@qoder-ai/qoder-agent-sdk` and defaults to +`qodercliAuth()` for local development. Automated environments should inject a +PAT or service-account auth factory without placing credentials in source or +logs. The Qoder SDK install script materializes its worker runtime; environments +that disable dependency scripts must approve that postinstall (or run +`npm rebuild @qoder-ai/qoder-agent-sdk`) before using `QoderSdkExecutor`. The Pi +SDK (`@earendil-works/pi-coding-agent`, supported `^0.84.2`) is an optional peer +dependency; its executor loads it lazily and reports install guidance when +missing. + +Native hooks and extensions are declaration-only in v0.1. Until a native +materialization receipt exists, they do not upgrade effective strength beyond +`advisory`. + +## Development + +```sh +npm install +npm run harness:generated # regenerate and reject stale Langium output +npm run harness:build +npm run harness:test +``` + +The package supports the repository's Node range (`>=22.20.0 <25`). See the +[v0.1 production-readiness spec](https://github.com/QoderAI/better-harness/blob/main/docs/specs/2026-08-14-harness-as-code-v0.1.md) +for acceptance scenarios, non-goals, and validation evidence. diff --git a/packages/harness/examples/standard-coding.harness b/packages/harness/examples/standard-coding.harness new file mode 100644 index 0000000..01553e5 --- /dev/null +++ b/packages/harness/examples/standard-coding.harness @@ -0,0 +1,93 @@ +// Example harness assembly: contracts, host bindings, plugins, and one +// composition targeting the Pi host. + +component impact-analysis { + kind skill + description "Before editing, map the blast radius of the change and list affected modules." + input { changed-files } + output { impact-report } + permissions { + workspace read + } +} + +component verification-before-complete { + kind policy + description "Do not declare the task complete until verification evidence (tests or diff review) exists." + input { verification-evidence } + permissions { + workspace read + network deny + } +} + +binding impact-analysis for pi { + mechanism skill-package + strength advisory +} + +binding impact-analysis for qoder { + mechanism skill-routing + strength advisory +} + +binding verification-before-complete for pi { + mechanism extension-event + strength enforced +} + +binding verification-before-complete for qoder { + mechanism system-instruction + strength advisory + notes "Qoder CLI print mode cannot intercept completion; the policy is prompt-injected." +} + +plugin repository-understanding { + version "1.4.2" + provides [ component.impact-analysis ] +} + +plugin change-verification { + version "2.1.3" + provides [ component.verification-before-complete ] +} + +composition standard-coding { + target pi + + include [ + plugin.repository-understanding@^1, + plugin.change-verification@^2 + ] + + require verification-before-complete { + preferred enforced + minimum advisory + on-degrade report + } + + configure { + shell.timeout = 60s + tool-call-budget = 80 + } +} + +composition standard-coding-qoder { + target qoder + + include [ + plugin.repository-understanding@^1, + plugin.change-verification@^2 + ] + + require verification-before-complete { + preferred enforced + minimum advisory + on-degrade report + } + + configure { + shell.timeout = 60s + tool-call-budget = 80 + } +} diff --git a/packages/harness/langium-config.json b/packages/harness/langium-config.json new file mode 100644 index 0000000..3a13658 --- /dev/null +++ b/packages/harness/langium-config.json @@ -0,0 +1,11 @@ +{ + "projectName": "Harness", + "languages": [ + { + "id": "harness", + "grammar": "src/language/harness.langium", + "fileExtensions": [".harness"] + } + ], + "out": "src/language/generated" +} diff --git a/packages/harness/package.json b/packages/harness/package.json new file mode 100644 index 0000000..2966adc --- /dev/null +++ b/packages/harness/package.json @@ -0,0 +1,78 @@ +{ + "name": "@qoder-ai/harness", + "version": "0.1.0", + "description": "Harness as Code: a parseable, lockable, explainable assembly language for coding-agent harnesses.", + "license": "MIT", + "type": "module", + "sideEffects": false, + "engines": { + "node": ">=22.20.0 <25.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/QoderAI/better-harness.git", + "directory": "packages/harness" + }, + "homepage": "https://github.com/QoderAI/better-harness/tree/main/packages/harness#readme", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./ir": { + "types": "./dist/ir/index.d.ts", + "import": "./dist/ir/index.js" + }, + "./exec": { + "types": "./dist/exec/index.d.ts", + "import": "./dist/exec/index.js" + }, + "./highlight": { + "types": "./dist/highlight/shiki.d.ts", + "import": "./dist/highlight/shiki.js" + } + }, + "files": [ + "dist/", + "examples/", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "npm run clean && npm run langium:generate && tsc", + "check:generated": "npm run langium:generate && git diff --exit-code -- src/language/generated", + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "langium:generate": "langium generate", + "prepack": "npm run build && npm test", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "peerDependencies": { + "@earendil-works/pi-coding-agent": "^0.84.2" + }, + "peerDependenciesMeta": { + "@earendil-works/pi-coding-agent": { + "optional": true + } + }, + "dependencies": { + "@qoder-ai/qoder-agent-sdk": "1.0.21", + "@sinclair/typebox": "0.34.52", + "langium": "4.3.1", + "semver": "7.8.5", + "shiki": "4.4.3" + }, + "devDependencies": { + "@earendil-works/pi-coding-agent": "0.84.2", + "@types/node": "26.2.0", + "@types/semver": "7.8.0", + "langium-cli": "4.3.0", + "typescript": "7.0.2", + "vitest": "4.1.10" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/harness/src/compiler/compile.ts b/packages/harness/src/compiler/compile.ts new file mode 100644 index 0000000..bb4b622 --- /dev/null +++ b/packages/harness/src/compiler/compile.ts @@ -0,0 +1,356 @@ +import { URI, type LangiumDocument } from "langium"; +import semver from "semver"; +import { createHarnessServices } from "../language/harness-module.js"; +import { + type ComponentContract, + type CompositionDeclaration, + type ConfigValue, + type HarnessDocument, + type PluginDeclaration, + type TargetBinding, + isComponentContract, + isCompositionDeclaration, + isPluginDeclaration, + isTargetBinding, +} from "../language/generated/ast.js"; +import { + type ComponentContractIr, + type CompositionSpecIr, + type ConfigValue as ConfigValueIr, + type HarnessIrBundle, + IR_VERSION, + type PluginManifestIr, + type Strength, + type TargetBindingIr, +} from "../ir/index.js"; + +export interface CompileDiagnostic { + severity: "error" | "warning"; + message: string; + source: string; + line?: number; +} + +export interface CompileResult { + bundle?: HarnessIrBundle; + diagnostics: CompileDiagnostic[]; +} + +export interface HarnessSource { + uri?: string; + text: string; +} + +const DURATION_FACTORS: Record = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 }; + +/** + * Parse one or more Harness DSL sources and lower them into the versioned + * JSON IR bundle. Cross-file references are resolved across all provided + * sources. Compilation fails (no bundle) when any parser, linking, or + * validation error is present. + */ +export async function compileHarness(input: string | HarnessSource[]): Promise { + const sources = typeof input === "string" ? [{ text: input }] : input; + const { shared } = createHarnessServices(); + const documents: LangiumDocument[] = sources.map((source, index) => + shared.workspace.LangiumDocumentFactory.fromString( + source.text, + URI.parse(source.uri ?? `memory://harness/${index}.harness`), + ), + ); + for (const document of documents) { + shared.workspace.LangiumDocuments.addDocument(document); + } + await shared.workspace.DocumentBuilder.build(documents, { validation: true }); + + const diagnostics: CompileDiagnostic[] = []; + for (const document of documents) { + for (const diagnostic of document.diagnostics ?? []) { + diagnostics.push({ + severity: diagnostic.severity === 1 ? "error" : "warning", + message: typeof diagnostic.message === "string" ? diagnostic.message : String(diagnostic.message), + source: document.uri.toString(), + line: diagnostic.range.start.line + 1, + }); + } + } + diagnostics.push(...collectBundleDiagnostics(documents)); + if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) { + return { diagnostics }; + } + + const components: ComponentContractIr[] = []; + const bindings: TargetBindingIr[] = []; + const plugins: PluginManifestIr[] = []; + const compositions: CompositionSpecIr[] = []; + for (const document of documents) { + const root = document.parseResult.value as HarnessDocument; + for (const element of root.elements) { + if (isComponentContract(element)) { + components.push(lowerComponent(element)); + } else if (isTargetBinding(element)) { + bindings.push(lowerBinding(element)); + } else if (isCompositionDeclaration(element)) { + compositions.push(lowerComposition(element)); + } + } + } + // Plugins are lowered last so provided components can be joined with their + // declared bindings into the plugin manifest. + for (const document of documents) { + const root = document.parseResult.value as HarnessDocument; + for (const element of root.elements) { + if (isPluginDeclaration(element)) { + plugins.push(lowerPlugin(element, bindings)); + } + } + } + + return { + bundle: { + irVersion: IR_VERSION, + kind: "harness-ir-bundle", + components, + bindings, + plugins, + compositions, + }, + diagnostics, + }; +} + +function collectBundleDiagnostics(documents: LangiumDocument[]): CompileDiagnostic[] { + const diagnostics: CompileDiagnostic[] = []; + const components = new Map(); + const compositions = new Map(); + const pluginVersions = new Map(); + const bindings = new Map(); + + for (const document of documents) { + const source = document.uri.toString(); + const root = document.parseResult.value as HarnessDocument; + for (const element of root.elements) { + if (isComponentContract(element)) { + recordUnique(components, element.name, "component", source, element.$cstNode?.range.start.line); + recordRepeatedValues(element.inputs, "component input", source, element.$cstNode?.range.start.line); + recordRepeatedValues(element.outputs, "component output", source, element.$cstNode?.range.start.line); + } else if (isCompositionDeclaration(element)) { + recordUnique(compositions, element.name, "composition", source, element.$cstNode?.range.start.line); + recordCompositionMembers(element, source); + } else if (isPluginDeclaration(element)) { + recordUnique( + pluginVersions, + `${element.name}@${element.version}`, + "plugin version", + source, + element.$cstNode?.range.start.line, + ); + recordRepeatedValues( + element.provides.map((provided) => provided.component.$refText), + "provided component", + source, + element.$cstNode?.range.start.line, + ); + if (semver.valid(element.version) === null) { + diagnostics.push( + semanticDiagnostic( + `Plugin '${element.name}' declares invalid semantic version '${element.version}'.`, + source, + element.$cstNode?.range.start.line, + ), + ); + } + } else if (isTargetBinding(element)) { + recordUnique( + bindings, + `${element.component.$refText}::${element.host}`, + "component/host binding", + source, + element.$cstNode?.range.start.line, + ); + } + } + } + return diagnostics; + + function recordCompositionMembers(composition: CompositionDeclaration, source: string): void { + const line = composition.$cstNode?.range.start.line; + recordRepeatedValues( + composition.includes.map((include) => include.plugin.$refText), + "included plugin", + source, + line, + ); + for (const include of composition.includes) { + const range = include.range.replace(/^@/, ""); + if (semver.validRange(range) === null) { + diagnostics.push( + semanticDiagnostic( + `Plugin '${include.plugin.$refText}' uses invalid semantic version range '${range}'.`, + source, + line, + ), + ); + } + } + recordRepeatedValues( + composition.requirements.map((requirement) => requirement.component.$refText), + "composition requirement", + source, + line, + ); + recordRepeatedValues( + composition.settings.map((setting) => setting.key), + "configuration key", + source, + line, + ); + } + + function recordRepeatedValues(values: string[], kind: string, source: string, zeroBasedLine?: number): void { + const seen = new Set(); + for (const value of values) { + if (seen.has(value)) { + diagnostics.push(duplicateDiagnostic(kind, value, source, zeroBasedLine)); + } + seen.add(value); + } + } + + function recordUnique( + seen: Map, + key: string, + kind: string, + source: string, + zeroBasedLine?: number, + ): void { + const firstSource = seen.get(key); + if (firstSource !== undefined) { + diagnostics.push({ + ...duplicateDiagnostic(kind, key, source, zeroBasedLine), + message: `Duplicate ${kind} '${key}'; first declared in ${firstSource}.`, + }); + return; + } + seen.set(key, source); + } +} + +function duplicateDiagnostic( + kind: string, + value: string, + source: string, + zeroBasedLine?: number, +): CompileDiagnostic { + return { + severity: "error", + message: `Duplicate ${kind} '${value}'.`, + source, + ...(zeroBasedLine !== undefined ? { line: zeroBasedLine + 1 } : {}), + }; +} + +function semanticDiagnostic( + message: string, + source: string, + zeroBasedLine?: number, +): CompileDiagnostic { + return { + severity: "error", + message, + source, + ...(zeroBasedLine !== undefined ? { line: zeroBasedLine + 1 } : {}), + }; +} + +function lowerComponent(contract: ComponentContract): ComponentContractIr { + return { + irVersion: IR_VERSION, + kind: "component-contract", + id: contract.name, + componentKind: contract.kind, + ...(contract.description !== undefined ? { description: contract.description } : {}), + inputs: [...contract.inputs], + outputs: [...contract.outputs], + permissions: contract.permissions.map((rule) => ({ domain: rule.domain, access: rule.access })), + }; +} + +function lowerBinding(binding: TargetBinding): TargetBindingIr { + return { + irVersion: IR_VERSION, + kind: "target-binding", + componentId: binding.component.$refText, + host: binding.host, + mechanism: binding.mechanism, + strength: binding.strength as Strength, + ...(binding.notes !== undefined ? { notes: binding.notes } : {}), + }; +} + +function lowerPlugin(plugin: PluginDeclaration, bindings: TargetBindingIr[]): PluginManifestIr { + const provides = plugin.provides.map((ref) => ref.component.$refText); + const provided = new Set(provides); + return { + irVersion: IR_VERSION, + kind: "plugin-manifest", + id: plugin.name, + version: plugin.version, + provides, + bindings: bindings + .filter((binding) => provided.has(binding.componentId)) + .map((binding) => ({ + componentId: binding.componentId, + host: binding.host, + mechanism: binding.mechanism, + strength: binding.strength, + })), + }; +} + +function lowerComposition(composition: CompositionDeclaration): CompositionSpecIr { + return { + irVersion: IR_VERSION, + kind: "composition-spec", + id: composition.name, + target: composition.target, + includes: composition.includes.map((include) => ({ + pluginId: include.plugin.$refText, + range: include.range.replace(/^@/, ""), + })), + requirements: composition.requirements.map((requirement) => ({ + componentId: requirement.component.$refText, + ...(requirement.preferred !== undefined + ? { preferred: requirement.preferred as Strength } + : {}), + minimum: requirement.minimum as Strength, + onDegrade: requirement.onDegrade ?? "report", + })), + settings: composition.settings.map((entry) => ({ + key: entry.key, + value: lowerConfigValue(entry.value), + })), + }; +} + +function lowerConfigValue(value: ConfigValue): ConfigValueIr { + switch (value.$type) { + case "IntLiteral": + return { type: "int", value: value.value }; + case "DurationLiteral": { + const match = /^([0-9]+)(ms|s|m|h)$/.exec(value.value); + if (!match) { + throw new Error(`Invalid duration literal: ${value.value}`); + } + return { + type: "duration", + value: value.value, + ms: Number(match[1]) * DURATION_FACTORS[match[2]], + }; + } + case "StringLiteral": + return { type: "string", value: value.value }; + case "BooleanLiteral": + return { type: "boolean", value: value.value }; + } +} diff --git a/packages/harness/src/exec/executor.ts b/packages/harness/src/exec/executor.ts new file mode 100644 index 0000000..16f2f3f --- /dev/null +++ b/packages/harness/src/exec/executor.ts @@ -0,0 +1,87 @@ +import type { HarnessIrBundle, HarnessRevision } from "../ir/index.js"; + +export interface HarnessRunTask { + prompt: string; + cwd?: string; +} + +export interface HarnessRunResult { + host: string; + revisionId: string; + exitCode: number; + output: string; + errorOutput: string; + /** Realizations the executor could not materialize on this host. */ + warnings: string[]; +} + +export interface HarnessExecutor { + readonly host: string; + execute(revision: HarnessRevision, bundle: HarnessIrBundle, task: HarnessRunTask): Promise; +} + +export interface RunPreamble { + preamble: string; + warnings: string[]; +} + +export class HarnessHostMismatchError extends Error { + constructor(revisionHost: string, executorHost: string) { + super(`Harness revision targets host '${revisionHost}', but executor host is '${executorHost}'.`); + this.name = "HarnessHostMismatchError"; + } +} + +export function assertRevisionHost(revision: HarnessRevision, executorHost: string): void { + if (revision.target.host !== executorHost) { + throw new HarnessHostMismatchError(revision.target.host, executorHost); + } +} + +/** + * Build the advisory portion of a run from a resolved revision. + * + * v0.1 executors materialize effective `advisory` realizations as prompt text. + * The resolver has already capped stronger declared bindings to this effective + * strength and applied the composition's degradation policy. + */ +export function buildRunPreamble(revision: HarnessRevision, bundle: HarnessIrBundle): RunPreamble { + const lines: string[] = []; + const warnings: string[] = []; + for (const realization of revision.realization) { + if (realization.action === "failed") { + continue; + } + if (realization.action === "degraded" && realization.reason) { + warnings.push(`Realization '${realization.componentId}' is degraded: ${realization.reason}.`); + } + if (realization.realized === "advisory") { + const contract = bundle.components.find( + (component) => component.id === realization.componentId, + ); + const text = contract?.description ?? `Apply the '${realization.componentId}' capability.`; + lines.push(`- [${realization.componentId}] ${text}`); + } + } + const preamble = + lines.length > 0 + ? [ + `You are running under harness revision ${revision.revisionId}.`, + "Follow these harness policies:", + ...lines, + ].join("\n") + : ""; + return { preamble, warnings }; +} + +export function buildRunPrompt( + revision: HarnessRevision, + bundle: HarnessIrBundle, + task: HarnessRunTask, +): { prompt: string; warnings: string[] } { + const { preamble, warnings } = buildRunPreamble(revision, bundle); + return { + prompt: preamble.length > 0 ? `${preamble}\n\n${task.prompt}` : task.prompt, + warnings, + }; +} diff --git a/packages/harness/src/exec/index.ts b/packages/harness/src/exec/index.ts new file mode 100644 index 0000000..32b9bab --- /dev/null +++ b/packages/harness/src/exec/index.ts @@ -0,0 +1,24 @@ +export { + buildRunPreamble, + buildRunPrompt, + assertRevisionHost, + HarnessHostMismatchError, + type HarnessExecutor, + type HarnessRunResult, + type HarnessRunTask, + type RunPreamble, +} from "./executor.js"; +export { + QoderSdkExecutor, + type QoderAuthFactory, + type QoderSdkExecutorOptions, + type QoderSdkLike, + type QoderSdkMessage, +} from "./qoder-sdk.js"; +export { + PiSdkExecutor, + materializePiPackage, + type PiSdkExecutorOptions, + type PiSdkLike, + type PiModelRuntimeLike, +} from "./pi-sdk.js"; diff --git a/packages/harness/src/exec/pi-sdk.ts b/packages/harness/src/exec/pi-sdk.ts new file mode 100644 index 0000000..62ab6a1 --- /dev/null +++ b/packages/harness/src/exec/pi-sdk.ts @@ -0,0 +1,210 @@ +import { mkdir, readdir, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { HarnessIrBundle, HarnessRevision } from "../ir/index.js"; +import { + assertRevisionHost, + buildRunPrompt, + type HarnessExecutor, + type HarnessRunResult, + type HarnessRunTask, +} from "./executor.js"; + +const PI_SDK_MODULE = "@earendil-works/pi-coding-agent"; + +/** + * Structural view of the Pi SDK surface this executor relies on + * (`createAgentSession`, `SessionManager.inMemory`, and `ModelRuntime.create`). + * Tests inject a stub through `loadSdk`. + */ +export interface PiSdkLike { + createAgentSession(config: { + cwd?: string; + sessionManager: unknown; + modelRuntime: unknown; + model?: unknown; + noTools?: "all"; + }): Promise<{ + session: { + prompt(text: string): Promise; + subscribe?(listener: (event: PiSdkEvent) => void): (() => void) | void; + dispose?(): void; + }; + }>; + SessionManager: { inMemory(): unknown }; + ModelRuntime: { create(): Promise }; +} + +export interface PiModelRuntimeLike { + setRuntimeApiKey?(providerId: string, apiKey: string): Promise; + getModels?(providerId?: string): readonly unknown[]; +} + +interface PiSdkEvent { + type?: string; + assistantMessageEvent?: { type?: string; delta?: string }; + message?: { + role?: string; + stopReason?: string; + errorMessage?: string; + }; +} + +export interface PiSdkExecutorOptions { + /** Injectable SDK loader; defaults to importing the optional peer dependency. */ + loadSdk?: () => Promise; + /** In-memory credential/runtime setup; callers must not log credential values. */ + configureModelRuntime?: (runtime: PiModelRuntimeLike) => Promise | void; + /** Optional explicit model selection after runtime configuration. */ + selectModel?: (runtime: PiModelRuntimeLike) => Promise | unknown; +} + +/** + * Executes a resolved revision through the Pi coding agent SDK + * (`@earendil-works/pi-coding-agent`, an optional peer dependency). + */ +export class PiSdkExecutor implements HarnessExecutor { + readonly host = "pi"; + private readonly loadSdk: () => Promise; + private readonly configureModelRuntime?: PiSdkExecutorOptions["configureModelRuntime"]; + private readonly selectModel?: PiSdkExecutorOptions["selectModel"]; + + constructor(options: PiSdkExecutorOptions = {}) { + this.loadSdk = () => loadPiSdk(options.loadSdk); + this.configureModelRuntime = options.configureModelRuntime; + this.selectModel = options.selectModel; + } + + async execute( + revision: HarnessRevision, + bundle: HarnessIrBundle, + task: HarnessRunTask, + ): Promise { + assertRevisionHost(revision, this.host); + const { prompt, warnings } = buildRunPrompt(revision, bundle, task); + const sdk = await this.loadSdk(); + const modelRuntime = await sdk.ModelRuntime.create(); + await this.configureModelRuntime?.(modelRuntime); + const model = await this.selectModel?.(modelRuntime); + const { session } = await sdk.createAgentSession({ + cwd: task.cwd, + sessionManager: sdk.SessionManager.inMemory(), + modelRuntime, + ...(model !== undefined ? { model } : {}), + noTools: "all", + }); + let streamedOutput = ""; + let errorOutput = ""; + const unsubscribe = session.subscribe?.((event) => { + if ( + event.type === "message_update" && + event.assistantMessageEvent?.type === "text_delta" && + typeof event.assistantMessageEvent.delta === "string" + ) { + streamedOutput += event.assistantMessageEvent.delta; + } + if ( + event.type === "message_end" && + event.message?.role === "assistant" && + (event.message.stopReason === "error" || event.message.stopReason === "aborted") + ) { + errorOutput = + event.message.errorMessage ?? `Pi SDK stopped with '${event.message.stopReason}'.`; + } + }); + try { + await session.prompt(prompt); + } finally { + unsubscribe?.(); + session.dispose?.(); + } + return { + host: this.host, + revisionId: revision.revisionId, + exitCode: errorOutput ? 1 : 0, + output: streamedOutput, + errorOutput, + warnings, + }; + } +} + +async function loadPiSdk(loader?: () => Promise): Promise { + try { + if (loader) { + return await loader(); + } + const moduleName = PI_SDK_MODULE; + const sdk = await import(moduleName); + return { + createAgentSession: sdk.createAgentSession, + SessionManager: sdk.SessionManager, + ModelRuntime: sdk.ModelRuntime, + } as PiSdkLike; + } catch (error) { + throw new Error( + `The Pi executor needs the optional peer dependency '${PI_SDK_MODULE}'. ` + + `Install it with: npm install ${PI_SDK_MODULE}`, + { cause: error }, + ); + } +} + +/** + * Materialize a revision as an installable Pi package directory: + * a `package.json` with the `pi.skills` contribution plus one + * `skills//SKILL.md` per advisory skill-shaped component. + * Returns the relative paths that were written. + */ +export async function materializePiPackage( + revision: HarnessRevision, + bundle: HarnessIrBundle, + directory: string, +): Promise { + assertRevisionHost(revision, "pi"); + const written: string[] = []; + const manifest = { + name: `harness-revision-${revision.revisionId.slice(3, 15)}`, + version: "0.0.0", + private: true, + description: `Materialized harness revision ${revision.revisionId} for host '${revision.target.host}'.`, + pi: { skills: ["./skills"] }, + }; + await mkdir(directory, { recursive: true }); + const existingEntries = await readdir(directory); + if (existingEntries.length > 0) { + throw new Error( + `Pi package destination must be empty: '${directory}' contains ${existingEntries.length} ` + + `existing ${existingEntries.length === 1 ? "entry" : "entries"}.`, + ); + } + await writeFile(join(directory, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); + written.push("package.json"); + + for (const realization of revision.realization) { + if (realization.action === "failed" || realization.realized !== "advisory") { + continue; + } + const contract = bundle.components.find((component) => component.id === realization.componentId); + if (!contract || contract.componentKind !== "skill") { + continue; + } + const skillDir = join(directory, "skills", contract.id); + await mkdir(skillDir, { recursive: true }); + const description = contract.description ?? `Harness component '${contract.id}'.`; + const body = [ + "---", + `name: ${contract.id}`, + `description: ${JSON.stringify(description)}`, + "---", + "", + description, + "", + `Provenance: harness revision ${revision.revisionId}, component hash ` + + `${revision.resolved.components.find((entry) => entry.id === contract.id)?.contentHash ?? "unknown"}.`, + "", + ].join("\n"); + await writeFile(join(skillDir, "SKILL.md"), body, "utf8"); + written.push(join("skills", contract.id, "SKILL.md")); + } + return written; +} diff --git a/packages/harness/src/exec/qoder-sdk.ts b/packages/harness/src/exec/qoder-sdk.ts new file mode 100644 index 0000000..bea5eaa --- /dev/null +++ b/packages/harness/src/exec/qoder-sdk.ts @@ -0,0 +1,152 @@ +import type { HarnessIrBundle, HarnessRevision } from "../ir/index.js"; +import { + assertRevisionHost, + buildRunPrompt, + type HarnessExecutor, + type HarnessRunResult, + type HarnessRunTask, +} from "./executor.js"; + +const QODER_SDK_MODULE = "@qoder-ai/qoder-agent-sdk"; + +export interface QoderSdkTextBlock { + type: string; + text?: string; +} + +export interface QoderSdkMessage { + type: string; + subtype?: string; + is_error?: boolean; + error?: string; + errors?: string[]; + result?: unknown; + message?: { content?: QoderSdkTextBlock[] }; +} + +export interface QoderSdkLike { + query(params: { + prompt: string; + options: { + auth: unknown; + cwd?: string; + allowedTools: string[]; + tools: string[]; + persistSession: boolean; + maxTurns: number; + }; + }): AsyncIterable; + qodercliAuth(): unknown; +} + +export type QoderAuthFactory = (sdk: QoderSdkLike) => unknown; + +export interface QoderSdkExecutorOptions { + /** Injectable SDK loader for deterministic tests. */ + loadSdk?: () => Promise; + /** Defaults to the locally signed-in qodercli session. */ + auth?: QoderAuthFactory; + /** Explicit SDK tool allowlist. v0.1 defaults to no pre-authorized tools. */ + allowedTools?: string[]; + /** Tools visible to the session. v0.1 defaults to none. */ + tools?: string[]; + /** Defaults to an ephemeral one-turn query. */ + persistSession?: boolean; + maxTurns?: number; +} + +/** Execute one resolved revision through the official Qoder Agent SDK. */ +export class QoderSdkExecutor implements HarnessExecutor { + readonly host = "qoder"; + private readonly loadSdk: () => Promise; + private readonly auth: QoderAuthFactory; + private readonly allowedTools: string[]; + private readonly tools: string[]; + private readonly persistSession: boolean; + private readonly maxTurns: number; + + constructor(options: QoderSdkExecutorOptions = {}) { + this.loadSdk = () => loadQoderSdk(options.loadSdk); + this.auth = options.auth ?? ((sdk) => sdk.qodercliAuth()); + this.allowedTools = [...(options.allowedTools ?? [])]; + this.tools = [...(options.tools ?? [])]; + this.persistSession = options.persistSession ?? false; + this.maxTurns = options.maxTurns ?? 1; + } + + async execute( + revision: HarnessRevision, + bundle: HarnessIrBundle, + task: HarnessRunTask, + ): Promise { + assertRevisionHost(revision, this.host); + const { prompt, warnings } = buildRunPrompt(revision, bundle, task); + const sdk = await this.loadSdk(); + const stream = sdk.query({ + prompt, + options: { + auth: this.auth(sdk), + cwd: task.cwd, + allowedTools: [...this.allowedTools], + tools: [...this.tools], + persistSession: this.persistSession, + maxTurns: this.maxTurns, + }, + }); + const output: string[] = []; + const errors: string[] = []; + let exitCode = 0; + let sawResult = false; + for await (const message of stream) { + if (message.type === "assistant") { + for (const block of message.message?.content ?? []) { + if (block.type === "text" && typeof block.text === "string") { + output.push(block.text); + } + } + } + if (message.type === "result") { + sawResult = true; + if (message.is_error === true || message.subtype !== "success") { + exitCode = 1; + errors.push(...(message.errors ?? [message.error ?? stringifyResult(message.result)])); + } + } + } + if (!sawResult) { + exitCode = 1; + errors.push("Qoder SDK query ended without a result message."); + } + return { + host: this.host, + revisionId: revision.revisionId, + exitCode, + output: output.join(""), + errorOutput: errors.filter(Boolean).join("\n"), + warnings, + }; + } +} + +async function loadQoderSdk(loader?: () => Promise): Promise { + try { + if (loader) { + return await loader(); + } + const moduleName = QODER_SDK_MODULE; + const sdk = await import(moduleName); + return { query: sdk.query, qodercliAuth: sdk.qodercliAuth } as QoderSdkLike; + } catch (error) { + throw new Error( + `The Qoder executor needs '${QODER_SDK_MODULE}'. Install workspace dependencies with: npm install`, + { cause: error }, + ); + } +} + +function stringifyResult(result: unknown): string { + if (typeof result === "string") { + return result; + } + return result === undefined ? "Qoder SDK query failed." : JSON.stringify(result); +} diff --git a/packages/harness/src/highlight/harness-grammar.ts b/packages/harness/src/highlight/harness-grammar.ts new file mode 100644 index 0000000..6b4712b --- /dev/null +++ b/packages/harness/src/highlight/harness-grammar.ts @@ -0,0 +1,82 @@ +import type { LanguageRegistration } from "shiki"; + +/** + * TextMate grammar for the Harness DSL, registered with Shiki as the + * `harness` language. + */ +export const harnessTextMateGrammar: LanguageRegistration = { + name: "harness", + scopeName: "source.harness", + displayName: "Harness", + patterns: [ + { include: "#comments" }, + { include: "#declarations" }, + { include: "#sections" }, + { include: "#constants" }, + { include: "#versions" }, + { include: "#durations" }, + { include: "#numbers" }, + { include: "#strings" }, + ], + repository: { + comments: { + patterns: [ + { name: "comment.line.double-slash.harness", match: "//[^\\n]*" }, + { name: "comment.block.harness", begin: "/\\*", end: "\\*/" }, + ], + }, + declarations: { + patterns: [ + { + match: "\\b(component|binding|plugin|composition)\\b\\s+([_a-zA-Z][\\w-]*)", + captures: { + "1": { name: "keyword.declaration.harness" }, + "2": { name: "entity.name.type.harness" }, + }, + }, + { name: "keyword.control.harness", match: "\\b(for|require|include|provides)\\b" }, + ], + }, + sections: { + patterns: [ + { + name: "keyword.other.section.harness", + match: + "\\b(kind|description|input|output|permissions|mechanism|strength|notes|version|target|preferred|minimum|on-degrade|configure)\\b", + }, + ], + }, + constants: { + patterns: [ + { + name: "constant.language.strength.harness", + match: "\\b(unsupported|advisory|wired|enforced)\\b", + }, + { + name: "constant.language.kind.harness", + match: "\\b(skill|tool|program|workflow|hook|policy|observer|ui)\\b", + }, + { + name: "constant.language.permission.harness", + match: "\\b(workspace|process|network|model|read|write|allow|deny)\\b", + }, + { name: "constant.language.degrade.harness", match: "\\b(fail|report)\\b" }, + { name: "constant.language.boolean.harness", match: "\\b(true|false)\\b" }, + ], + }, + versions: { + patterns: [ + { name: "constant.other.version.harness", match: "@[~^]?[0-9][0-9a-zA-Z.+*-]*" }, + ], + }, + durations: { + patterns: [{ name: "constant.numeric.duration.harness", match: "\\b[0-9]+(ms|s|m|h)\\b" }], + }, + numbers: { + patterns: [{ name: "constant.numeric.integer.harness", match: "\\b[0-9]+\\b" }], + }, + strings: { + patterns: [{ name: "string.quoted.double.harness", match: '"[^"]*"' }], + }, + }, +}; diff --git a/packages/harness/src/highlight/shiki.ts b/packages/harness/src/highlight/shiki.ts new file mode 100644 index 0000000..3b301d9 --- /dev/null +++ b/packages/harness/src/highlight/shiki.ts @@ -0,0 +1,45 @@ +import { + type BundledTheme, + type HighlighterGeneric, + type ThemedToken, + createHighlighter, +} from "shiki"; +import { harnessTextMateGrammar } from "./harness-grammar.js"; + +export { harnessTextMateGrammar }; + +export type HarnessHighlighter = HighlighterGeneric<"harness", BundledTheme>; + +let highlighterPromise: Promise | undefined; + +const DEFAULT_THEME: BundledTheme = "github-dark"; + +/** + * Lazily create (and cache) a Shiki highlighter with the Harness grammar + * registered as the `harness` language. + */ +export function getHarnessHighlighter(): Promise { + highlighterPromise ??= createHighlighter({ + themes: [DEFAULT_THEME], + langs: [harnessTextMateGrammar], + }) as unknown as Promise; + return highlighterPromise; +} + +/** Render Harness DSL source to themed HTML. */ +export async function highlightHarness( + code: string, + theme: BundledTheme = DEFAULT_THEME, +): Promise { + const highlighter = await getHarnessHighlighter(); + if (!highlighter.getLoadedThemes().includes(theme)) { + await highlighter.loadTheme(theme); + } + return highlighter.codeToHtml(code, { lang: "harness", theme }); +} + +/** Tokenize Harness DSL source; used to test grammar behaviour, not markup text. */ +export async function tokenizeHarness(code: string): Promise { + const highlighter = await getHarnessHighlighter(); + return highlighter.codeToTokensBase(code, { lang: "harness", theme: DEFAULT_THEME }); +} diff --git a/packages/harness/src/index.ts b/packages/harness/src/index.ts new file mode 100644 index 0000000..5078f6e --- /dev/null +++ b/packages/harness/src/index.ts @@ -0,0 +1,18 @@ +export * from "./ir/index.js"; +export { canonicalJson, contentHash, sha256Hex } from "./ir/canonical.js"; +export { + compileHarness, + type CompileDiagnostic, + type CompileResult, + type HarnessSource, +} from "./compiler/compile.js"; +export { mergePermissions, resolveComposition, type ResolveResult } from "./resolver/resolve.js"; +export { STRENGTH_ORDER, strengthIndex } from "./language/harness-validator.js"; +export { createHarnessServices, type HarnessServices } from "./language/harness-module.js"; +export * from "./exec/index.js"; +export { + getHarnessHighlighter, + harnessTextMateGrammar, + highlightHarness, + tokenizeHarness, +} from "./highlight/shiki.js"; diff --git a/packages/harness/src/ir/canonical.ts b/packages/harness/src/ir/canonical.ts new file mode 100644 index 0000000..ba6368f --- /dev/null +++ b/packages/harness/src/ir/canonical.ts @@ -0,0 +1,30 @@ +import { createHash } from "node:crypto"; + +/** + * Deterministic JSON serialization: object keys are sorted recursively so the + * same logical document always hashes to the same revision id. + */ +export function canonicalJson(value: unknown): string { + return JSON.stringify(sortValue(value)); +} + +export function sha256Hex(text: string): string { + return createHash("sha256").update(text, "utf8").digest("hex"); +} + +export function contentHash(value: unknown): string { + return `sha256:${sha256Hex(canonicalJson(value))}`; +} + +function sortValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(sortValue); + } + if (value !== null && typeof value === "object") { + const entries = Object.entries(value as Record) + .filter(([, entryValue]) => entryValue !== undefined) + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); + return Object.fromEntries(entries.map(([key, entryValue]) => [key, sortValue(entryValue)])); + } + return value; +} diff --git a/packages/harness/src/ir/index.ts b/packages/harness/src/ir/index.ts new file mode 100644 index 0000000..40d3e0d --- /dev/null +++ b/packages/harness/src/ir/index.ts @@ -0,0 +1,231 @@ +/** + * Versioned JSON intermediate representation for the Harness DSL. + * + * Every artifact carries `irVersion` plus a `kind` discriminator so stored + * documents stay self-describing. The five v0.1 entities are: + * CompositionSpec, ComponentContract, TargetBindingManifest (as part of a + * plugin manifest), HarnessRevision, and ResolutionReport. + */ +import { type Static, Type } from "@sinclair/typebox"; + +export const IR_VERSION = "0.1.0"; + +const IrVersion = Type.Literal(IR_VERSION); +const Identifier = Type.String({ pattern: "^[_a-zA-Z][\\w-]*$" }); + +export const StrengthSchema = Type.Union([ + Type.Literal("unsupported"), + Type.Literal("advisory"), + Type.Literal("wired"), + Type.Literal("enforced"), +]); +export type Strength = Static; + +export const ComponentKindSchema = Type.Union([ + Type.Literal("skill"), + Type.Literal("tool"), + Type.Literal("program"), + Type.Literal("workflow"), + Type.Literal("hook"), + Type.Literal("policy"), + Type.Literal("observer"), + Type.Literal("ui"), +]); +export type ComponentKind = Static; + +export const PermissionGrantSchema = Type.Object( + { + domain: Type.Union([ + Type.Literal("workspace"), + Type.Literal("process"), + Type.Literal("network"), + Type.Literal("model"), + ]), + access: Type.Union([ + Type.Literal("read"), + Type.Literal("write"), + Type.Literal("allow"), + Type.Literal("deny"), + ]), + }, + { additionalProperties: false }, +); +export type PermissionGrant = Static; + +export const ConfigValueSchema = Type.Union([ + Type.Object({ type: Type.Literal("int"), value: Type.Number() }, { additionalProperties: false }), + Type.Object( + { type: Type.Literal("duration"), value: Type.String(), ms: Type.Number() }, + { additionalProperties: false }, + ), + Type.Object({ type: Type.Literal("string"), value: Type.String() }, { additionalProperties: false }), + Type.Object({ type: Type.Literal("boolean"), value: Type.Boolean() }, { additionalProperties: false }), +]); +export type ConfigValue = Static; + +export const ConfigEntrySchema = Type.Object( + { key: Type.String(), value: ConfigValueSchema }, + { additionalProperties: false }, +); +export type ConfigEntryIr = Static; + +export const ComponentContractIrSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("component-contract"), + id: Identifier, + componentKind: ComponentKindSchema, + description: Type.Optional(Type.String()), + inputs: Type.Array(Identifier), + outputs: Type.Array(Identifier), + permissions: Type.Array(PermissionGrantSchema), + }, + { additionalProperties: false }, +); +export type ComponentContractIr = Static; + +export const TargetBindingIrSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("target-binding"), + componentId: Identifier, + host: Identifier, + mechanism: Identifier, + strength: StrengthSchema, + notes: Type.Optional(Type.String()), + }, + { additionalProperties: false }, +); +export type TargetBindingIr = Static; + +export const PluginManifestIrSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("plugin-manifest"), + id: Identifier, + version: Type.String(), + provides: Type.Array(Identifier), + bindings: Type.Array( + Type.Object( + { + componentId: Identifier, + host: Identifier, + mechanism: Identifier, + strength: StrengthSchema, + }, + { additionalProperties: false }, + ), + ), + }, + { additionalProperties: false }, +); +export type PluginManifestIr = Static; + +export const CapabilityRequirementIrSchema = Type.Object( + { + componentId: Identifier, + preferred: Type.Optional(StrengthSchema), + minimum: StrengthSchema, + onDegrade: Type.Union([Type.Literal("fail"), Type.Literal("report")]), + }, + { additionalProperties: false }, +); +export type CapabilityRequirementIr = Static; + +export const CompositionSpecIrSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("composition-spec"), + id: Identifier, + target: Identifier, + includes: Type.Array( + Type.Object({ pluginId: Identifier, range: Type.String() }, { additionalProperties: false }), + ), + requirements: Type.Array(CapabilityRequirementIrSchema), + settings: Type.Array(ConfigEntrySchema), + }, + { additionalProperties: false }, +); +export type CompositionSpecIr = Static; + +export const HarnessIrBundleSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("harness-ir-bundle"), + components: Type.Array(ComponentContractIrSchema), + bindings: Type.Array(TargetBindingIrSchema), + plugins: Type.Array(PluginManifestIrSchema), + compositions: Type.Array(CompositionSpecIrSchema), + }, + { additionalProperties: false }, +); +export type HarnessIrBundle = Static; + +export const RealizationSchema = Type.Object( + { + componentId: Identifier, + requestedMinimum: StrengthSchema, + requestedPreferred: Type.Optional(StrengthSchema), + declaredStrength: StrengthSchema, + declaredMechanism: Type.Union([Identifier, Type.Null()]), + realized: StrengthSchema, + materializedMechanism: Type.Union([Identifier, Type.Null()]), + action: Type.Union([ + Type.Literal("satisfied"), + Type.Literal("degraded"), + Type.Literal("failed"), + ]), + reason: Type.Optional(Type.String()), + }, + { additionalProperties: false }, +); +export type Realization = Static; + +export const HarnessRevisionSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("harness-revision"), + revisionId: Type.String({ pattern: "^hr_[0-9a-f]{32}$" }), + composition: Type.Object( + { id: Identifier, contentHash: Type.String() }, + { additionalProperties: false }, + ), + target: Type.Object({ host: Identifier }, { additionalProperties: false }), + resolved: Type.Object( + { + plugins: Type.Array( + Type.Object( + { id: Identifier, version: Type.String(), contentHash: Type.String() }, + { additionalProperties: false }, + ), + ), + components: Type.Array( + Type.Object( + { id: Identifier, contentHash: Type.String() }, + { additionalProperties: false }, + ), + ), + }, + { additionalProperties: false }, + ), + realization: Type.Array(RealizationSchema), + permissions: Type.Array(PermissionGrantSchema), + settings: Type.Array(ConfigEntrySchema), + }, + { additionalProperties: false }, +); +export type HarnessRevision = Static; + +export const ResolutionReportSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("resolution-report"), + compositionId: Identifier, + target: Identifier, + status: Type.Union([Type.Literal("resolved"), Type.Literal("failed")]), + realizations: Type.Array(RealizationSchema), + errors: Type.Array(Type.String()), + }, + { additionalProperties: false }, +); +export type ResolutionReport = Static; diff --git a/packages/harness/src/language/generated/ast.ts b/packages/harness/src/language/generated/ast.ts new file mode 100644 index 0000000..5b22338 --- /dev/null +++ b/packages/harness/src/language/generated/ast.ts @@ -0,0 +1,635 @@ +/****************************************************************************** + * This file was generated by langium-cli 4.3.0. + * DO NOT EDIT MANUALLY! + ******************************************************************************/ + +/* eslint-disable */ +import * as langium from 'langium'; + +export const HarnessTerminals = { + DURATION: /[0-9]+(ms|s|m|h)/, + INT: /[0-9]+/, + VERSION_RANGE: /@[~^]?[0-9][0-9a-zA-Z.+*-]*/, + ID: /[_a-zA-Z][\w-]*/, + STRING: /"[^"]*"/, + WS: /\s+/, + SL_COMMENT: /\/\/[^\n\r]*/, + ML_COMMENT: /\/\*[\s\S]*?\*\//, +}; + +export type HarnessTerminalNames = keyof typeof HarnessTerminals; + +export type HarnessKeywordNames = + | "," + | "." + | "=" + | "[" + | "]" + | "advisory" + | "allow" + | "binding" + | "component" + | "composition" + | "configure" + | "deny" + | "description" + | "enforced" + | "fail" + | "false" + | "for" + | "hook" + | "include" + | "input" + | "kind" + | "mechanism" + | "minimum" + | "model" + | "network" + | "notes" + | "observer" + | "on-degrade" + | "output" + | "permissions" + | "plugin" + | "policy" + | "preferred" + | "process" + | "program" + | "provides" + | "read" + | "report" + | "require" + | "skill" + | "strength" + | "target" + | "tool" + | "true" + | "ui" + | "unsupported" + | "version" + | "wired" + | "workflow" + | "workspace" + | "write" + | "{" + | "}"; + +export type HarnessTokenNames = HarnessTerminalNames | HarnessKeywordNames; + +export interface BooleanLiteral extends langium.AstNode { + readonly $container: ConfigEntry; + readonly $type: 'BooleanLiteral'; + value: boolean; +} + +export const BooleanLiteral = { + $type: 'BooleanLiteral', + value: 'value' +} as const; + +export function isBooleanLiteral(item: unknown): item is BooleanLiteral { + return reflection.isInstance(item, BooleanLiteral.$type); +} + +export interface CapabilityRequirement extends langium.AstNode { + readonly $container: CompositionDeclaration; + readonly $type: 'CapabilityRequirement'; + component: langium.Reference; + minimum: Strength; + onDegrade?: DegradePolicy; + preferred?: Strength; +} + +export const CapabilityRequirement = { + $type: 'CapabilityRequirement', + component: 'component', + minimum: 'minimum', + onDegrade: 'onDegrade', + preferred: 'preferred' +} as const; + +export function isCapabilityRequirement(item: unknown): item is CapabilityRequirement { + return reflection.isInstance(item, CapabilityRequirement.$type); +} + +export interface ComponentContract extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'ComponentContract'; + description?: string; + inputs: Array; + kind: ComponentKind; + name: string; + outputs: Array; + permissions: Array; +} + +export const ComponentContract = { + $type: 'ComponentContract', + description: 'description', + inputs: 'inputs', + kind: 'kind', + name: 'name', + outputs: 'outputs', + permissions: 'permissions' +} as const; + +export function isComponentContract(item: unknown): item is ComponentContract { + return reflection.isInstance(item, ComponentContract.$type); +} + +export type ComponentKind = 'hook' | 'observer' | 'policy' | 'program' | 'skill' | 'tool' | 'ui' | 'workflow'; + +export function isComponentKind(item: unknown): item is ComponentKind { + return item === 'skill' || item === 'tool' || item === 'program' || item === 'workflow' || item === 'hook' || item === 'policy' || item === 'observer' || item === 'ui'; +} + +export interface ComponentRef extends langium.AstNode { + readonly $container: PluginDeclaration; + readonly $type: 'ComponentRef'; + component: langium.Reference; +} + +export const ComponentRef = { + $type: 'ComponentRef', + component: 'component' +} as const; + +export function isComponentRef(item: unknown): item is ComponentRef { + return reflection.isInstance(item, ComponentRef.$type); +} + +export interface CompositionDeclaration extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'CompositionDeclaration'; + includes: Array; + name: string; + requirements: Array; + settings: Array; + target: string; +} + +export const CompositionDeclaration = { + $type: 'CompositionDeclaration', + includes: 'includes', + name: 'name', + requirements: 'requirements', + settings: 'settings', + target: 'target' +} as const; + +export function isCompositionDeclaration(item: unknown): item is CompositionDeclaration { + return reflection.isInstance(item, CompositionDeclaration.$type); +} + +export interface ConfigEntry extends langium.AstNode { + readonly $container: CompositionDeclaration; + readonly $type: 'ConfigEntry'; + key: DottedName; + value: ConfigValue; +} + +export const ConfigEntry = { + $type: 'ConfigEntry', + key: 'key', + value: 'value' +} as const; + +export function isConfigEntry(item: unknown): item is ConfigEntry { + return reflection.isInstance(item, ConfigEntry.$type); +} + +export type ConfigValue = BooleanLiteral | DurationLiteral | IntLiteral | StringLiteral; + +export const ConfigValue = { + $type: 'ConfigValue' +} as const; + +export function isConfigValue(item: unknown): item is ConfigValue { + return reflection.isInstance(item, ConfigValue.$type); +} + +export type Declaration = ComponentContract | CompositionDeclaration | PluginDeclaration | TargetBinding; + +export const Declaration = { + $type: 'Declaration' +} as const; + +export function isDeclaration(item: unknown): item is Declaration { + return reflection.isInstance(item, Declaration.$type); +} + +export type DegradePolicy = 'fail' | 'report'; + +export function isDegradePolicy(item: unknown): item is DegradePolicy { + return item === 'fail' || item === 'report'; +} + +export type DottedName = string; + +export function isDottedName(item: unknown): item is DottedName { + return typeof item === 'string'; +} + +export interface DurationLiteral extends langium.AstNode { + readonly $container: ConfigEntry; + readonly $type: 'DurationLiteral'; + value: string; +} + +export const DurationLiteral = { + $type: 'DurationLiteral', + value: 'value' +} as const; + +export function isDurationLiteral(item: unknown): item is DurationLiteral { + return reflection.isInstance(item, DurationLiteral.$type); +} + +export interface HarnessDocument extends langium.AstNode { + readonly $type: 'HarnessDocument'; + elements: Array; +} + +export const HarnessDocument = { + $type: 'HarnessDocument', + elements: 'elements' +} as const; + +export function isHarnessDocument(item: unknown): item is HarnessDocument { + return reflection.isInstance(item, HarnessDocument.$type); +} + +export interface IntLiteral extends langium.AstNode { + readonly $container: ConfigEntry; + readonly $type: 'IntLiteral'; + value: number; +} + +export const IntLiteral = { + $type: 'IntLiteral', + value: 'value' +} as const; + +export function isIntLiteral(item: unknown): item is IntLiteral { + return reflection.isInstance(item, IntLiteral.$type); +} + +export type PermissionAccess = 'allow' | 'deny' | 'read' | 'write'; + +export function isPermissionAccess(item: unknown): item is PermissionAccess { + return item === 'read' || item === 'write' || item === 'allow' || item === 'deny'; +} + +export type PermissionDomain = 'model' | 'network' | 'process' | 'workspace'; + +export function isPermissionDomain(item: unknown): item is PermissionDomain { + return item === 'workspace' || item === 'process' || item === 'network' || item === 'model'; +} + +export interface PermissionRule extends langium.AstNode { + readonly $container: ComponentContract; + readonly $type: 'PermissionRule'; + access: PermissionAccess; + domain: PermissionDomain; +} + +export const PermissionRule = { + $type: 'PermissionRule', + access: 'access', + domain: 'domain' +} as const; + +export function isPermissionRule(item: unknown): item is PermissionRule { + return reflection.isInstance(item, PermissionRule.$type); +} + +export interface PluginDeclaration extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'PluginDeclaration'; + name: string; + provides: Array; + version: string; +} + +export const PluginDeclaration = { + $type: 'PluginDeclaration', + name: 'name', + provides: 'provides', + version: 'version' +} as const; + +export function isPluginDeclaration(item: unknown): item is PluginDeclaration { + return reflection.isInstance(item, PluginDeclaration.$type); +} + +export interface PluginRequirement extends langium.AstNode { + readonly $container: CompositionDeclaration; + readonly $type: 'PluginRequirement'; + plugin: langium.Reference; + range: string; +} + +export const PluginRequirement = { + $type: 'PluginRequirement', + plugin: 'plugin', + range: 'range' +} as const; + +export function isPluginRequirement(item: unknown): item is PluginRequirement { + return reflection.isInstance(item, PluginRequirement.$type); +} + +export type Strength = 'advisory' | 'enforced' | 'unsupported' | 'wired'; + +export function isStrength(item: unknown): item is Strength { + return item === 'unsupported' || item === 'advisory' || item === 'wired' || item === 'enforced'; +} + +export interface StringLiteral extends langium.AstNode { + readonly $container: ConfigEntry; + readonly $type: 'StringLiteral'; + value: string; +} + +export const StringLiteral = { + $type: 'StringLiteral', + value: 'value' +} as const; + +export function isStringLiteral(item: unknown): item is StringLiteral { + return reflection.isInstance(item, StringLiteral.$type); +} + +export interface TargetBinding extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'TargetBinding'; + component: langium.Reference; + host: string; + mechanism: string; + notes?: string; + strength: Strength; +} + +export const TargetBinding = { + $type: 'TargetBinding', + component: 'component', + host: 'host', + mechanism: 'mechanism', + notes: 'notes', + strength: 'strength' +} as const; + +export function isTargetBinding(item: unknown): item is TargetBinding { + return reflection.isInstance(item, TargetBinding.$type); +} + +export type HarnessAstType = { + BooleanLiteral: BooleanLiteral + CapabilityRequirement: CapabilityRequirement + ComponentContract: ComponentContract + ComponentRef: ComponentRef + CompositionDeclaration: CompositionDeclaration + ConfigEntry: ConfigEntry + ConfigValue: ConfigValue + Declaration: Declaration + DurationLiteral: DurationLiteral + HarnessDocument: HarnessDocument + IntLiteral: IntLiteral + PermissionRule: PermissionRule + PluginDeclaration: PluginDeclaration + PluginRequirement: PluginRequirement + StringLiteral: StringLiteral + TargetBinding: TargetBinding +} + +export class HarnessAstReflection extends langium.AbstractAstReflection { + override readonly types = { + BooleanLiteral: { + name: BooleanLiteral.$type, + properties: { + value: { + name: BooleanLiteral.value, + defaultValue: false, + optional: true + } + }, + superTypes: [ConfigValue.$type] + }, + CapabilityRequirement: { + name: CapabilityRequirement.$type, + properties: { + component: { + name: CapabilityRequirement.component, + referenceType: ComponentContract.$type + }, + minimum: { + name: CapabilityRequirement.minimum + }, + onDegrade: { + name: CapabilityRequirement.onDegrade, + optional: true + }, + preferred: { + name: CapabilityRequirement.preferred, + optional: true + } + }, + superTypes: [] + }, + ComponentContract: { + name: ComponentContract.$type, + properties: { + description: { + name: ComponentContract.description, + optional: true + }, + inputs: { + name: ComponentContract.inputs, + defaultValue: [], + optional: true + }, + kind: { + name: ComponentContract.kind + }, + name: { + name: ComponentContract.name + }, + outputs: { + name: ComponentContract.outputs, + defaultValue: [], + optional: true + }, + permissions: { + name: ComponentContract.permissions, + defaultValue: [], + optional: true + } + }, + superTypes: [Declaration.$type] + }, + ComponentRef: { + name: ComponentRef.$type, + properties: { + component: { + name: ComponentRef.component, + referenceType: ComponentContract.$type + } + }, + superTypes: [] + }, + CompositionDeclaration: { + name: CompositionDeclaration.$type, + properties: { + includes: { + name: CompositionDeclaration.includes, + defaultValue: [] + }, + name: { + name: CompositionDeclaration.name + }, + requirements: { + name: CompositionDeclaration.requirements, + defaultValue: [], + optional: true + }, + settings: { + name: CompositionDeclaration.settings, + defaultValue: [], + optional: true + }, + target: { + name: CompositionDeclaration.target + } + }, + superTypes: [Declaration.$type] + }, + ConfigEntry: { + name: ConfigEntry.$type, + properties: { + key: { + name: ConfigEntry.key + }, + value: { + name: ConfigEntry.value + } + }, + superTypes: [] + }, + ConfigValue: { + name: ConfigValue.$type, + properties: { + }, + superTypes: [] + }, + Declaration: { + name: Declaration.$type, + properties: { + }, + superTypes: [] + }, + DurationLiteral: { + name: DurationLiteral.$type, + properties: { + value: { + name: DurationLiteral.value + } + }, + superTypes: [ConfigValue.$type] + }, + HarnessDocument: { + name: HarnessDocument.$type, + properties: { + elements: { + name: HarnessDocument.elements, + defaultValue: [], + optional: true + } + }, + superTypes: [] + }, + IntLiteral: { + name: IntLiteral.$type, + properties: { + value: { + name: IntLiteral.value + } + }, + superTypes: [ConfigValue.$type] + }, + PermissionRule: { + name: PermissionRule.$type, + properties: { + access: { + name: PermissionRule.access + }, + domain: { + name: PermissionRule.domain + } + }, + superTypes: [] + }, + PluginDeclaration: { + name: PluginDeclaration.$type, + properties: { + name: { + name: PluginDeclaration.name + }, + provides: { + name: PluginDeclaration.provides, + defaultValue: [] + }, + version: { + name: PluginDeclaration.version + } + }, + superTypes: [Declaration.$type] + }, + PluginRequirement: { + name: PluginRequirement.$type, + properties: { + plugin: { + name: PluginRequirement.plugin, + referenceType: PluginDeclaration.$type + }, + range: { + name: PluginRequirement.range + } + }, + superTypes: [] + }, + StringLiteral: { + name: StringLiteral.$type, + properties: { + value: { + name: StringLiteral.value + } + }, + superTypes: [ConfigValue.$type] + }, + TargetBinding: { + name: TargetBinding.$type, + properties: { + component: { + name: TargetBinding.component, + referenceType: ComponentContract.$type + }, + host: { + name: TargetBinding.host + }, + mechanism: { + name: TargetBinding.mechanism + }, + notes: { + name: TargetBinding.notes, + optional: true + }, + strength: { + name: TargetBinding.strength + } + }, + superTypes: [Declaration.$type] + } + } as const satisfies langium.AstMetaData +} + +export const reflection = new HarnessAstReflection(); diff --git a/packages/harness/src/language/generated/grammar.ts b/packages/harness/src/language/generated/grammar.ts new file mode 100644 index 0000000..5ac1ae5 --- /dev/null +++ b/packages/harness/src/language/generated/grammar.ts @@ -0,0 +1,1292 @@ +/****************************************************************************** + * This file was generated by langium-cli 4.3.0. + * DO NOT EDIT MANUALLY! + ******************************************************************************/ + +import type { Grammar } from 'langium'; +import { loadGrammarFromJson } from 'langium'; + +let loadedHarnessGrammar: Grammar | undefined; +export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarnessGrammar = loadGrammarFromJson(`{ + "$type": "Grammar", + "isDeclared": true, + "name": "Harness", + "rules": [ + { + "$type": "ParserRule", + "entry": true, + "name": "HarnessDocument", + "definition": { + "$type": "Assignment", + "feature": "elements", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@1" + }, + "arguments": [] + }, + "cardinality": "*" + }, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "Declaration", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@2" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@7" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@11" + }, + "arguments": [] + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "ComponentContract", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "component" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Keyword", + "value": "kind" + }, + { + "$type": "Assignment", + "feature": "kind", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@26" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "input" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "inputs", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "output" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "outputs", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "permissions" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "permissions", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "ComponentKind", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "skill" + }, + { + "$type": "Keyword", + "value": "tool" + }, + { + "$type": "Keyword", + "value": "program" + }, + { + "$type": "Keyword", + "value": "workflow" + }, + { + "$type": "Keyword", + "value": "hook" + }, + { + "$type": "Keyword", + "value": "policy" + }, + { + "$type": "Keyword", + "value": "observer" + }, + { + "$type": "Keyword", + "value": "ui" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "PermissionRule", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "domain", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@5" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "access", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@6" + }, + "arguments": [] + } + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "PermissionDomain", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "workspace" + }, + { + "$type": "Keyword", + "value": "process" + }, + { + "$type": "Keyword", + "value": "network" + }, + { + "$type": "Keyword", + "value": "model" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "PermissionAccess", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "read" + }, + { + "$type": "Keyword", + "value": "write" + }, + { + "$type": "Keyword", + "value": "allow" + }, + { + "$type": "Keyword", + "value": "deny" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "TargetBinding", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "binding" + }, + { + "$type": "Assignment", + "feature": "component", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@2" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + }, + "deprecatedSyntax": false, + "isMulti": false + } + }, + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Assignment", + "feature": "host", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Keyword", + "value": "mechanism" + }, + { + "$type": "Assignment", + "feature": "mechanism", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "strength" + }, + { + "$type": "Assignment", + "feature": "strength", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "notes" + }, + { + "$type": "Assignment", + "feature": "notes", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@26" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "Strength", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "unsupported" + }, + { + "$type": "Keyword", + "value": "advisory" + }, + { + "$type": "Keyword", + "value": "wired" + }, + { + "$type": "Keyword", + "value": "enforced" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "PluginDeclaration", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "plugin" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Keyword", + "value": "version" + }, + { + "$type": "Assignment", + "feature": "version", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@26" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "provides" + }, + { + "$type": "Keyword", + "value": "[" + }, + { + "$type": "Assignment", + "feature": "provides", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "," + }, + { + "$type": "Assignment", + "feature": "provides", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "]" + }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "ComponentRef", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "component" + }, + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "Assignment", + "feature": "component", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@2" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + }, + "deprecatedSyntax": false, + "isMulti": false + } + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "CompositionDeclaration", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "composition" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Keyword", + "value": "target" + }, + { + "$type": "Assignment", + "feature": "target", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "include" + }, + { + "$type": "Keyword", + "value": "[" + }, + { + "$type": "Assignment", + "feature": "includes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "," + }, + { + "$type": "Assignment", + "feature": "includes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "]" + }, + { + "$type": "Assignment", + "feature": "requirements", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@13" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "configure" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "settings", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@15" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "PluginRequirement", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "plugin" + }, + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "Assignment", + "feature": "plugin", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@9" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + }, + "deprecatedSyntax": false, + "isMulti": false + } + }, + { + "$type": "Assignment", + "feature": "range", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@24" + }, + "arguments": [] + } + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "CapabilityRequirement", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "require" + }, + { + "$type": "Assignment", + "feature": "component", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@2" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + }, + "deprecatedSyntax": false, + "isMulti": false + } + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "preferred" + }, + { + "$type": "Assignment", + "feature": "preferred", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "minimum" + }, + { + "$type": "Assignment", + "feature": "minimum", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "on-degrade" + }, + { + "$type": "Assignment", + "feature": "onDegrade", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@14" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "DegradePolicy", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "fail" + }, + { + "$type": "Keyword", + "value": "report" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "ConfigEntry", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "key", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@16" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "=" + }, + { + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@17" + }, + "arguments": [] + } + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "DottedName", + "dataType": "string", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + ], + "cardinality": "*" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "ConfigValue", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@18" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@20" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@21" + }, + "arguments": [] + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "IntLiteral", + "definition": { + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + } + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "DurationLiteral", + "definition": { + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "StringLiteral", + "definition": { + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@26" + }, + "arguments": [] + } + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "BooleanLiteral", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "BooleanLiteral" + } + }, + { + "$type": "Alternatives", + "elements": [ + { + "$type": "Assignment", + "feature": "value", + "operator": "?=", + "terminal": { + "$type": "Keyword", + "value": "true" + } + }, + { + "$type": "Keyword", + "value": "false" + } + ] + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "TerminalRule", + "name": "DURATION", + "definition": { + "$type": "RegexToken", + "regex": "/[0-9]+(ms|s|m|h)/", + "parenthesized": false + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "INT", + "type": { + "$type": "ReturnType", + "name": "number" + }, + "definition": { + "$type": "RegexToken", + "regex": "/[0-9]+/", + "parenthesized": false + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "VERSION_RANGE", + "definition": { + "$type": "RegexToken", + "regex": "/@[~^]?[0-9][0-9a-zA-Z.+*-]*/", + "parenthesized": false + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "ID", + "definition": { + "$type": "RegexToken", + "regex": "/[_a-zA-Z][\\\\w-]*/", + "parenthesized": false + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "name": "STRING", + "type": { + "$type": "ReturnType", + "name": "string" + }, + "definition": { + "$type": "RegexToken", + "regex": "/\\"[^\\"]*\\"/", + "parenthesized": false + }, + "fragment": false, + "hidden": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "WS", + "definition": { + "$type": "RegexToken", + "regex": "/\\\\s+/", + "parenthesized": false + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "SL_COMMENT", + "definition": { + "$type": "RegexToken", + "regex": "/\\\\/\\\\/[^\\\\n\\\\r]*/", + "parenthesized": false + }, + "fragment": false + }, + { + "$type": "TerminalRule", + "hidden": true, + "name": "ML_COMMENT", + "definition": { + "$type": "RegexToken", + "regex": "/\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\//", + "parenthesized": false + }, + "fragment": false + } + ], + "imports": [], + "interfaces": [], + "types": [] +}`)); diff --git a/packages/harness/src/language/generated/module.ts b/packages/harness/src/language/generated/module.ts new file mode 100644 index 0000000..b8d4fe6 --- /dev/null +++ b/packages/harness/src/language/generated/module.ts @@ -0,0 +1,25 @@ +/****************************************************************************** + * This file was generated by langium-cli 4.3.0. + * DO NOT EDIT MANUALLY! + ******************************************************************************/ + +import type { LangiumSharedCoreServices, LangiumCoreServices, LangiumGeneratedCoreServices, LangiumGeneratedSharedCoreServices, LanguageMetaData, Module } from 'langium'; +import { HarnessAstReflection } from './ast.js'; +import { HarnessGrammar } from './grammar.js'; + +export const HarnessLanguageMetaData = { + languageId: 'harness', + fileExtensions: ['.harness'], + caseInsensitive: false, + mode: 'development' +} as const satisfies LanguageMetaData; + +export const HarnessGeneratedSharedModule: Module = { + AstReflection: () => new HarnessAstReflection() +}; + +export const HarnessGeneratedModule: Module = { + Grammar: () => HarnessGrammar(), + LanguageMetaData: () => HarnessLanguageMetaData, + parser: {} +}; diff --git a/packages/harness/src/language/harness-module.ts b/packages/harness/src/language/harness-module.ts new file mode 100644 index 0000000..637605e --- /dev/null +++ b/packages/harness/src/language/harness-module.ts @@ -0,0 +1,37 @@ +import { + type DefaultSharedCoreModuleContext, + EmptyFileSystem, + type LangiumCoreServices, + type LangiumSharedCoreServices, + type Module, + type PartialLangiumCoreServices, + createDefaultCoreModule, + createDefaultSharedCoreModule, + inject, +} from "langium"; +import { HarnessGeneratedModule, HarnessGeneratedSharedModule } from "./generated/module.js"; +import { HarnessValidator, registerValidationChecks } from "./harness-validator.js"; + +export type HarnessAddedServices = { + validation: { + HarnessValidator: HarnessValidator; + }; +}; + +export type HarnessServices = LangiumCoreServices & HarnessAddedServices; + +export const HarnessModule: Module = { + validation: { + HarnessValidator: () => new HarnessValidator(), + }, +}; + +export function createHarnessServices( + context: DefaultSharedCoreModuleContext = EmptyFileSystem, +): { shared: LangiumSharedCoreServices; Harness: HarnessServices } { + const shared = inject(createDefaultSharedCoreModule(context), HarnessGeneratedSharedModule); + const Harness = inject(createDefaultCoreModule({ shared }), HarnessGeneratedModule, HarnessModule); + shared.ServiceRegistry.register(Harness); + registerValidationChecks(Harness); + return { shared, Harness }; +} diff --git a/packages/harness/src/language/harness-validator.ts b/packages/harness/src/language/harness-validator.ts new file mode 100644 index 0000000..544e8c4 --- /dev/null +++ b/packages/harness/src/language/harness-validator.ts @@ -0,0 +1,57 @@ +import type { ValidationAcceptor, ValidationChecks } from "langium"; +import { + type CapabilityRequirement, + type HarnessAstType, + type HarnessDocument, + isTargetBinding, +} from "./generated/ast.js"; +import type { HarnessServices } from "./harness-module.js"; + +export const STRENGTH_ORDER = ["unsupported", "advisory", "wired", "enforced"] as const; + +export function strengthIndex(strength: string): number { + return STRENGTH_ORDER.indexOf(strength as (typeof STRENGTH_ORDER)[number]); +} + +export function registerValidationChecks(services: HarnessServices): void { + const registry = services.validation.ValidationRegistry; + const validator = services.validation.HarnessValidator; + const checks: ValidationChecks = { + CapabilityRequirement: validator.checkPreferredNotBelowMinimum, + HarnessDocument: validator.checkUniqueBindingPerHost, + }; + registry.register(checks, validator); +} + +export class HarnessValidator { + checkPreferredNotBelowMinimum(requirement: CapabilityRequirement, accept: ValidationAcceptor): void { + if (requirement.preferred === undefined || requirement.minimum === undefined) { + return; + } + if (strengthIndex(requirement.preferred) < strengthIndex(requirement.minimum)) { + accept( + "error", + `Preferred strength '${requirement.preferred}' is below minimum '${requirement.minimum}'.`, + { node: requirement, property: "preferred" }, + ); + } + } + + checkUniqueBindingPerHost(document: HarnessDocument, accept: ValidationAcceptor): void { + const seen = new Set(); + for (const element of document.elements) { + if (!isTargetBinding(element)) { + continue; + } + const key = `${element.component.$refText}::${element.host}`; + if (seen.has(key)) { + accept( + "error", + `Duplicate binding for component '${element.component.$refText}' on host '${element.host}'.`, + { node: element, property: "host" }, + ); + } + seen.add(key); + } + } +} diff --git a/packages/harness/src/language/harness.langium b/packages/harness/src/language/harness.langium new file mode 100644 index 0000000..fe12aaa --- /dev/null +++ b/packages/harness/src/language/harness.langium @@ -0,0 +1,112 @@ +// Harness assembly language, v0.1 surface. +// +// The DSL declares what a harness is assembled from (contracts, bindings, +// plugins, compositions). Revisions and resolution reports are resolver +// outputs, not authored syntax. +grammar Harness + +entry HarnessDocument: + (elements+=Declaration)*; + +Declaration: + ComponentContract | TargetBinding | PluginDeclaration | CompositionDeclaration; + +// A target-independent capability contract: what a capability is, +// what it consumes and produces, and which permissions it asks for. +ComponentContract: + 'component' name=ID '{' + 'kind' kind=ComponentKind + ('description' description=STRING)? + ('input' '{' (inputs+=ID)* '}')? + ('output' '{' (outputs+=ID)* '}')? + ('permissions' '{' (permissions+=PermissionRule)* '}')? + '}'; + +ComponentKind returns string: + 'skill' | 'tool' | 'program' | 'workflow' | 'hook' | 'policy' | 'observer' | 'ui'; + +PermissionRule: + domain=PermissionDomain access=PermissionAccess; + +PermissionDomain returns string: + 'workspace' | 'process' | 'network' | 'model'; + +PermissionAccess returns string: + 'read' | 'write' | 'allow' | 'deny'; + +// How a component is realized on one concrete host, and how strong +// the realized guarantee is. +TargetBinding: + 'binding' component=[ComponentContract:ID] 'for' host=ID '{' + 'mechanism' mechanism=ID + 'strength' strength=Strength + ('notes' notes=STRING)? + '}'; + +Strength returns string: + 'unsupported' | 'advisory' | 'wired' | 'enforced'; + +// Distribution boundary: which contracts a package provides. +// Bindings for provided components are attached by the compiler. +PluginDeclaration: + 'plugin' name=ID '{' + 'version' version=STRING + 'provides' '[' provides+=ComponentRef (',' provides+=ComponentRef)* ']' + '}'; + +ComponentRef: + 'component' '.' component=[ComponentContract:ID]; + +// Desired assembly for one run target. Resolving a composition against +// a registry produces an immutable revision plus a resolution report. +CompositionDeclaration: + 'composition' name=ID '{' + 'target' target=ID + 'include' '[' includes+=PluginRequirement (',' includes+=PluginRequirement)* ']' + (requirements+=CapabilityRequirement)* + ('configure' '{' (settings+=ConfigEntry)* '}')? + '}'; + +PluginRequirement: + 'plugin' '.' plugin=[PluginDeclaration:ID] range=VERSION_RANGE; + +CapabilityRequirement: + 'require' component=[ComponentContract:ID] '{' + ('preferred' preferred=Strength)? + 'minimum' minimum=Strength + ('on-degrade' onDegrade=DegradePolicy)? + '}'; + +DegradePolicy returns string: + 'fail' | 'report'; + +ConfigEntry: + key=DottedName '=' value=ConfigValue; + +DottedName returns string: + ID ('.' ID)*; + +ConfigValue: + IntLiteral | DurationLiteral | StringLiteral | BooleanLiteral; + +IntLiteral: + value=INT; + +DurationLiteral: + value=DURATION; + +StringLiteral: + value=STRING; + +BooleanLiteral: + {infer BooleanLiteral} (value?='true' | 'false'); + +terminal DURATION: /[0-9]+(ms|s|m|h)/; +terminal INT returns number: /[0-9]+/; +terminal VERSION_RANGE: /@[~^]?[0-9][0-9a-zA-Z.+*-]*/; +terminal ID: /[_a-zA-Z][\w-]*/; +terminal STRING returns string: /"[^"]*"/; + +hidden terminal WS: /\s+/; +hidden terminal SL_COMMENT: /\/\/[^\n\r]*/; +hidden terminal ML_COMMENT: /\/\*[\s\S]*?\*\//; diff --git a/packages/harness/src/resolver/resolve.ts b/packages/harness/src/resolver/resolve.ts new file mode 100644 index 0000000..6abd386 --- /dev/null +++ b/packages/harness/src/resolver/resolve.ts @@ -0,0 +1,309 @@ +import semver from "semver"; +import { contentHash, sha256Hex, canonicalJson } from "../ir/canonical.js"; +import { STRENGTH_ORDER, strengthIndex } from "../language/harness-validator.js"; +import { + type CapabilityRequirementIr, + type ComponentContractIr, + type HarnessIrBundle, + type HarnessRevision, + IR_VERSION, + type PermissionGrant, + type PluginManifestIr, + type Realization, + type ResolutionReport, + type TargetBindingIr, +} from "../ir/index.js"; + +export interface ResolveResult { + /** Immutable run fact. Only present when the report status is `resolved`. */ + revision?: HarnessRevision; + /** Always present: requested vs realized strength for every requirement. */ + report: ResolutionReport; +} + +/** + * Resolve a composition against the plugins declared in the bundle. + * + * Degradation semantics: + * A binding declares the strongest mechanism a host can eventually provide. + * The v0.1 executors materialize every supported binding as prompt guidance, + * so effective strength is capped at `advisory` until a native artifact and + * evidence-receipt contract lands. + * + * - realized >= preferred (or no preferred): satisfied + * - minimum <= realized < preferred: degraded; `on-degrade fail` turns the + * degradation into a resolution failure instead of a silent prompt + * - realized < minimum: always a resolution failure + */ +export function resolveComposition(bundle: HarnessIrBundle, compositionId: string): ResolveResult { + const composition = bundle.compositions.find((candidate) => candidate.id === compositionId); + if (!composition) { + return { + report: failedReport(compositionId, "unknown", [], [ + `Composition '${compositionId}' is not defined in the bundle.`, + ]), + }; + } + + const errors: string[] = []; + const resolvedPlugins: PluginManifestIr[] = []; + for (const include of composition.includes) { + const candidates = bundle.plugins.filter((plugin) => plugin.id === include.pluginId); + if (candidates.length === 0) { + errors.push(`Plugin '${include.pluginId}' is not available in the registry.`); + continue; + } + const matching = candidates + .filter((plugin) => satisfiesRange(plugin.version, include.range)) + .sort((a, b) => semver.rcompare(normalizeVersion(a.version), normalizeVersion(b.version))); + if (matching.length === 0) { + errors.push( + `Plugin '${include.pluginId}' has no version satisfying '${include.range}' ` + + `(available: ${candidates.map((plugin) => plugin.version).join(", ")}).`, + ); + continue; + } + resolvedPlugins.push(matching[0]); + } + + const enabledComponentIds = new Set(resolvedPlugins.flatMap((plugin) => plugin.provides)); + const enabledComponents = bundle.components.filter((component) => + enabledComponentIds.has(component.id), + ); + + const realizations: Realization[] = []; + for (const requirement of composition.requirements) { + if (!enabledComponentIds.has(requirement.componentId)) { + errors.push( + `Required component '${requirement.componentId}' is not provided by any included plugin.`, + ); + realizations.push( + realize( + requirement, + undefined, + materializeV01(undefined), + "failed", + "component not provided", + ), + ); + continue; + } + const binding = bundle.bindings.find( + (candidate) => + candidate.componentId === requirement.componentId && candidate.host === composition.target, + ); + const materialization = materializeV01(binding); + const realized = materialization.strength; + const minimumIndex = strengthIndex(requirement.minimum); + const realizedIndex = strengthIndex(realized); + const preferredIndex = + requirement.preferred !== undefined ? strengthIndex(requirement.preferred) : minimumIndex; + + if (realizedIndex < minimumIndex) { + errors.push( + `Component '${requirement.componentId}' realizes '${realized}' on host ` + + `'${composition.target}', below the required minimum '${requirement.minimum}'.`, + ); + realizations.push( + realize(requirement, binding, materialization, "failed", degradeReason(binding, composition.target)), + ); + continue; + } + if (realizedIndex < preferredIndex) { + const reason = degradeReason(binding, composition.target); + if (requirement.onDegrade === "fail") { + errors.push( + `Component '${requirement.componentId}' realizes '${realized}' on host ` + + `'${composition.target}', below the preferred '${requirement.preferred}' ` + + `and the requirement declares 'on-degrade fail'.`, + ); + realizations.push(realize(requirement, binding, materialization, "failed", reason)); + } else { + realizations.push(realize(requirement, binding, materialization, "degraded", reason)); + } + continue; + } + realizations.push(realize(requirement, binding, materialization, "satisfied")); + } + + // Components enabled by resolved plugins without an explicit requirement + // still belong to the realized surface of the run: record an implicit + // realization so executors and checkpoints see the full assembly. + const required = new Set(composition.requirements.map((requirement) => requirement.componentId)); + for (const component of enabledComponents) { + if (required.has(component.id)) { + continue; + } + const binding = bundle.bindings.find( + (candidate) => candidate.componentId === component.id && candidate.host === composition.target, + ); + const materialization = materializeV01(binding); + const isDegraded = + !binding || + materialization.strength === "unsupported" || + strengthIndex(binding.strength) > strengthIndex(materialization.strength); + realizations.push({ + componentId: component.id, + requestedMinimum: "unsupported", + declaredStrength: binding?.strength ?? "unsupported", + declaredMechanism: binding?.mechanism ?? null, + realized: materialization.strength, + materializedMechanism: materialization.mechanism, + action: isDegraded ? "degraded" : "satisfied", + reason: + !binding + ? `implicitly enabled; host '${composition.target}' has no binding for this component` + : binding.strength === "unsupported" + ? `implicitly enabled; host '${composition.target}' declares this component unsupported ` + + `via '${binding.mechanism}'` + : isDegraded + ? `implicitly enabled; declared '${binding.strength}' via '${binding.mechanism}', ` + + `materialized as '${materialization.strength}' via '${materialization.mechanism}'` + : "implicitly enabled by an included plugin", + }); + } + realizations.sort((a, b) => compareByKey(a.componentId, b.componentId)); + + if (errors.length > 0) { + return { report: failedReport(composition.id, composition.target, realizations, errors) }; + } + + const revisionBody: Omit = { + irVersion: IR_VERSION, + kind: "harness-revision" as const, + composition: { id: composition.id, contentHash: contentHash(composition) }, + target: { host: composition.target }, + resolved: { + plugins: resolvedPlugins + .map((plugin) => ({ id: plugin.id, version: plugin.version, contentHash: contentHash(plugin) })) + .sort((a, b) => compareByKey(a.id, b.id)), + components: enabledComponents + .map((component) => ({ id: component.id, contentHash: contentHash(component) })) + .sort((a, b) => compareByKey(a.id, b.id)), + }, + realization: realizations, + permissions: mergePermissions(enabledComponents), + settings: composition.settings, + }; + const revision: HarnessRevision = { + ...revisionBody, + revisionId: `hr_${sha256Hex(canonicalJson(revisionBody)).slice(0, 32)}`, + }; + + return { + revision, + report: { + irVersion: IR_VERSION, + kind: "resolution-report", + compositionId: composition.id, + target: composition.target, + status: "resolved", + realizations, + errors: [], + }, + }; +} + +/** + * Merge component permission grants into the revision permission surface. + * The merge is monotonically tightening: an explicit `deny` on a domain + * always wins over any grant, regardless of declaration order. + */ +export function mergePermissions(components: ComponentContractIr[]): PermissionGrant[] { + const byDomain = new Map>(); + for (const component of components) { + for (const grant of component.permissions) { + const accesses = byDomain.get(grant.domain) ?? new Set(); + accesses.add(grant.access); + byDomain.set(grant.domain, accesses); + } + } + const merged: PermissionGrant[] = []; + for (const [domain, accesses] of byDomain) { + if (accesses.has("deny")) { + merged.push({ domain, access: "deny" }); + continue; + } + for (const access of accesses) { + merged.push({ domain, access }); + } + } + return merged.sort( + (a, b) => compareByKey(a.domain, b.domain) || compareByKey(a.access, b.access), + ); +} + +function realize( + requirement: CapabilityRequirementIr, + binding: TargetBindingIr | undefined, + materialization: V01Materialization, + action: Realization["action"], + reason?: string, +): Realization { + return { + componentId: requirement.componentId, + requestedMinimum: requirement.minimum, + ...(requirement.preferred !== undefined ? { requestedPreferred: requirement.preferred } : {}), + declaredStrength: binding?.strength ?? "unsupported", + declaredMechanism: binding?.mechanism ?? null, + realized: materialization.strength, + materializedMechanism: materialization.mechanism, + action, + ...(reason !== undefined ? { reason } : {}), + }; +} + +function degradeReason(binding: TargetBindingIr | undefined, host: string): string { + if (!binding) { + return `host '${host}' has no binding for this component`; + } + const detail = + `host '${host}' declares '${binding.strength}' via '${binding.mechanism}', ` + + "but the v0.1 executor materializes prompt guidance at 'advisory' strength"; + return binding.notes ? `${detail}: ${binding.notes}` : detail; +} + +interface V01Materialization { + strength: Realization["realized"]; + mechanism: string | null; +} + +function materializeV01(binding: TargetBindingIr | undefined): V01Materialization { + if (!binding || binding.strength === "unsupported") { + return { strength: "unsupported", mechanism: null }; + } + return { strength: "advisory", mechanism: "prompt-preamble" }; +} + +function failedReport( + compositionId: string, + target: string, + realizations: Realization[], + errors: string[], +): ResolutionReport { + return { + irVersion: IR_VERSION, + kind: "resolution-report", + compositionId, + target, + status: "failed", + realizations, + errors, + }; +} + +function satisfiesRange(version: string, range: string): boolean { + const normalized = normalizeVersion(version); + const validRange = semver.validRange(range); + return validRange !== null && semver.satisfies(normalized, validRange); +} + +function normalizeVersion(version: string): string { + return semver.coerce(version)?.version ?? version; +} + +function compareByKey(a: string, b: string): number { + return a < b ? -1 : a > b ? 1 : 0; +} + +export { STRENGTH_ORDER }; diff --git a/packages/harness/test/compile.test.ts b/packages/harness/test/compile.test.ts new file mode 100644 index 0000000..c54c98e --- /dev/null +++ b/packages/harness/test/compile.test.ts @@ -0,0 +1,210 @@ +import { readFile } from "node:fs/promises"; +import { Value } from "@sinclair/typebox/value"; +import { describe, expect, it } from "vitest"; +import { compileHarness } from "../src/compiler/compile.js"; +import { HarnessIrBundleSchema, IR_VERSION } from "../src/ir/index.js"; + +const EXAMPLE_URL = new URL("../examples/standard-coding.harness", import.meta.url); + +describe("compileHarness", () => { + it("lowers the example document into a schema-valid IR bundle", async () => { + const source = await readFile(EXAMPLE_URL, "utf8"); + const result = await compileHarness(source); + + expect(result.diagnostics.filter((d) => d.severity === "error")).toEqual([]); + expect(result.bundle).toBeDefined(); + const bundle = result.bundle!; + expect(Value.Check(HarnessIrBundleSchema, bundle)).toBe(true); + expect(bundle.irVersion).toBe(IR_VERSION); + expect(bundle.components.map((component) => component.id)).toEqual([ + "impact-analysis", + "verification-before-complete", + ]); + expect(bundle.compositions.map((composition) => composition.id)).toEqual([ + "standard-coding", + "standard-coding-qoder", + ]); + }); + + it("joins declared bindings into the plugin manifest of provided components", async () => { + const source = await readFile(EXAMPLE_URL, "utf8"); + const { bundle } = await compileHarness(source); + + const plugin = bundle!.plugins.find((candidate) => candidate.id === "change-verification")!; + expect(plugin.provides).toEqual(["verification-before-complete"]); + expect(plugin.bindings).toEqual([ + expect.objectContaining({ host: "pi", mechanism: "extension-event", strength: "enforced" }), + expect.objectContaining({ host: "qoder", mechanism: "system-instruction", strength: "advisory" }), + ]); + }); + + it("normalizes version ranges and duration settings", async () => { + const source = await readFile(EXAMPLE_URL, "utf8"); + const { bundle } = await compileHarness(source); + + const composition = bundle!.compositions.find((candidate) => candidate.id === "standard-coding")!; + expect(composition.includes).toEqual([ + { pluginId: "repository-understanding", range: "^1" }, + { pluginId: "change-verification", range: "^2" }, + ]); + expect(composition.settings).toEqual([ + { key: "shell.timeout", value: { type: "duration", value: "60s", ms: 60_000 } }, + { key: "tool-call-budget", value: { type: "int", value: 80 } }, + ]); + }); + + it("resolves cross-file references across multiple sources", async () => { + const contracts = ` + component impact-analysis { + kind skill + } + `; + const assembly = ` + binding impact-analysis for qoder { + mechanism skill-routing + strength advisory + } + plugin repo-tools { + version "1.0.0" + provides [ component.impact-analysis ] + } + `; + const result = await compileHarness([{ text: contracts }, { text: assembly }]); + + expect(result.diagnostics.filter((d) => d.severity === "error")).toEqual([]); + expect(result.bundle!.plugins[0].bindings).toEqual([ + expect.objectContaining({ componentId: "impact-analysis", host: "qoder" }), + ]); + }); + + it("fails compilation when preferred strength is below minimum", async () => { + const source = ` + component gate { kind policy } + binding gate for pi { mechanism completion-hook strength enforced } + plugin p { version "1.0.0" provides [ component.gate ] } + composition c { + target pi + include [ plugin.p@1 ] + require gate { + preferred advisory + minimum enforced + } + } + `; + const result = await compileHarness(source); + + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true); + }); + + it("fails compilation on unresolved component references", async () => { + const result = await compileHarness(` + binding does-not-exist for pi { + mechanism completion-hook + strength advisory + } + `); + + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true); + }); + + it("fails compilation on duplicate bindings for the same component and host", async () => { + const result = await compileHarness(` + component gate { kind policy } + binding gate for pi { mechanism completion-hook strength advisory } + binding gate for pi { mechanism other strength enforced } + `); + + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true); + }); + + it("fails compilation on duplicate bindings split across source files", async () => { + const result = await compileHarness([ + { + uri: "memory://harness/first.harness", + text: ` + component gate { kind policy } + binding gate for pi { mechanism prompt strength advisory } + `, + }, + { + uri: "memory://harness/second.harness", + text: "binding gate for pi { mechanism extension-event strength enforced }", + }, + ]); + + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics).toContainEqual( + expect.objectContaining({ + severity: "error", + source: "memory://harness/second.harness", + message: expect.stringContaining("Duplicate component/host binding 'gate::pi'"), + }), + ); + }); + + it("rejects duplicate globally named declarations and repeated composition members", async () => { + const result = await compileHarness(` + component gate { kind policy } + component gate { kind skill } + plugin p { version "1.0.0" provides [ component.gate, component.gate ] } + composition run { + target pi + include [ plugin.p@1, plugin.p@^1 ] + require gate { minimum advisory } + require gate { minimum advisory } + configure { shell.timeout = 1s shell.timeout = 2s } + } + composition run { target pi include [ plugin.p@1 ] } + `); + + expect(result.bundle).toBeUndefined(); + const messages = result.diagnostics.map((diagnostic) => diagnostic.message); + expect(messages).toEqual( + expect.arrayContaining([ + expect.stringContaining("Duplicate component 'gate'"), + expect.stringContaining("Duplicate provided component 'gate'"), + expect.stringContaining("Duplicate included plugin 'p'"), + expect.stringContaining("Duplicate composition requirement 'gate'"), + expect.stringContaining("Duplicate configuration key 'shell.timeout'"), + expect.stringContaining("Duplicate composition 'run'"), + ]), + ); + }); + + it("rejects invalid exact plugin versions before resolution", async () => { + const result = await compileHarness(` + component gate { kind policy } + plugin verification { version "not-semver" provides [ component.gate ] } + composition run { target pi include [ plugin.verification@1 ] } + `); + + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics).toContainEqual( + expect.objectContaining({ + severity: "error", + message: "Plugin 'verification' declares invalid semantic version 'not-semver'.", + }), + ); + }); + + it("rejects repeated component inputs and outputs", async () => { + const result = await compileHarness(` + component analyzer { + kind skill + input { source source } + output { result result } + } + `); + + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics.map((diagnostic) => diagnostic.message)).toEqual( + expect.arrayContaining([ + "Duplicate component input 'source'.", + "Duplicate component output 'result'.", + ]), + ); + }); +}); diff --git a/packages/harness/test/exec.test.ts b/packages/harness/test/exec.test.ts new file mode 100644 index 0000000..5726299 --- /dev/null +++ b/packages/harness/test/exec.test.ts @@ -0,0 +1,398 @@ +import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { compileHarness } from "../src/compiler/compile.js"; +import type { HarnessIrBundle, HarnessRevision } from "../src/ir/index.js"; +import { resolveComposition } from "../src/resolver/resolve.js"; +import { PiSdkExecutor, materializePiPackage, type PiSdkLike } from "../src/exec/pi-sdk.js"; +import { QoderSdkExecutor, type QoderSdkLike } from "../src/exec/qoder-sdk.js"; + +const SOURCE = ` + component impact-analysis { + kind skill + description "Impact analysis: map the blast radius before editing." + } + component verification-before-complete { + kind policy + description "Do not complete without verification evidence." + } + binding impact-analysis for qoder { + mechanism skill-routing + strength advisory + } + binding verification-before-complete for qoder { + mechanism stop-hook + strength enforced + } + binding impact-analysis for pi { + mechanism skill-package + strength advisory + } + binding verification-before-complete for pi { + mechanism extension-event + strength enforced + } + plugin all { + version "1.0.0" + provides [ component.impact-analysis, component.verification-before-complete ] + } + composition on-qoder { + target qoder + include [ plugin.all@1 ] + require verification-before-complete { + preferred enforced + minimum advisory + on-degrade report + } + } + composition on-pi { + target pi + include [ plugin.all@1 ] + require verification-before-complete { + preferred enforced + minimum advisory + on-degrade report + } + } +`; + +async function resolveFor(compositionId: string): Promise<{ bundle: HarnessIrBundle; revision: HarnessRevision }> { + const { bundle } = await compileHarness(SOURCE); + const { revision } = resolveComposition(bundle!, compositionId); + return { bundle: bundle!, revision: revision! }; +} + +describe("QoderSdkExecutor", () => { + it("streams one Qoder SDK query with explicit auth, cwd, and tool authorization", async () => { + const { bundle, revision } = await resolveFor("on-qoder"); + const queries: Parameters[0][] = []; + const auth = { kind: "test-auth" }; + const sdk: QoderSdkLike = { + qodercliAuth: () => auth, + query: async function* (params) { + queries.push(params); + yield { + type: "assistant", + message: { content: [{ type: "text", text: "done" }] }, + }; + yield { type: "result", subtype: "success" }; + }, + }; + const executor = new QoderSdkExecutor({ loadSdk: async () => sdk, allowedTools: ["Read"] }); + + const result = await executor.execute(revision, bundle, { prompt: "Fix the bug", cwd: "/tmp" }); + + expect(queries).toHaveLength(1); + expect(queries[0].options).toEqual({ + auth, + cwd: "/tmp", + allowedTools: ["Read"], + tools: [], + persistSession: false, + maxTurns: 1, + }); + const prompt = queries[0].prompt; + expect(prompt.endsWith("Fix the bug")).toBe(true); + expect(prompt).toContain(revision.revisionId); + expect(prompt).toContain("Impact analysis: map the blast radius before editing."); + expect(result).toMatchObject({ host: "qoder", exitCode: 0, output: "done" }); + }); + + it("reports declared native strength as an advisory degradation", async () => { + const { bundle, revision } = await resolveFor("on-qoder"); + const sdk: QoderSdkLike = { + qodercliAuth: () => ({}), + query: async function* () { + yield { type: "result", subtype: "success" }; + }, + }; + const executor = new QoderSdkExecutor({ + loadSdk: async () => sdk, + }); + + const result = await executor.execute(revision, bundle, { prompt: "Fix the bug" }); + + expect(result.warnings).toHaveLength(1); + expect(result.warnings[0]).toContain("verification-before-complete"); + expect(result.warnings[0]).toContain("stop-hook"); + }); + + it("rejects a revision targeting another host before loading the Qoder SDK", async () => { + const { bundle, revision } = await resolveFor("on-pi"); + let loaded = false; + const executor = new QoderSdkExecutor({ + loadSdk: async () => { + loaded = true; + throw new Error("should not load"); + }, + }); + + await expect(executor.execute(revision, bundle, { prompt: "Fix the bug" })).rejects.toThrow( + /targets host 'pi'.*executor host is 'qoder'/, + ); + expect(loaded).toBe(false); + }); + + it("reports an SDK query failure without leaking non-error messages", async () => { + const { bundle, revision } = await resolveFor("on-qoder"); + const sdk: QoderSdkLike = { + qodercliAuth: () => ({}), + query: async function* () { + yield { type: "assistant", message: { content: [{ type: "text", text: "partial" }] } }; + yield { + type: "result", + subtype: "error_during_execution", + is_error: true, + errors: ["auth failed"], + }; + }, + }; + + const result = await new QoderSdkExecutor({ loadSdk: async () => sdk }).execute( + revision, + bundle, + { prompt: "Fix the bug" }, + ); + + expect(result).toMatchObject({ exitCode: 1, output: "partial", errorOutput: "auth failed" }); + }); + + it("fails closed when the Qoder SDK stream ends without a result message", async () => { + const { bundle, revision } = await resolveFor("on-qoder"); + const sdk: QoderSdkLike = { + qodercliAuth: () => ({}), + query: async function* () { + yield { type: "assistant", message: { content: [{ type: "text", text: "partial" }] } }; + }, + }; + + const result = await new QoderSdkExecutor({ loadSdk: async () => sdk }).execute( + revision, + bundle, + { prompt: "Fix the bug" }, + ); + + expect(result).toMatchObject({ + exitCode: 1, + output: "partial", + errorOutput: "Qoder SDK query ended without a result message.", + }); + }); +}); + +describe("PiSdkExecutor", () => { + it("drives the Pi SDK session with the composed prompt", async () => { + const { bundle, revision } = await resolveFor("on-pi"); + const prompts: string[] = []; + const workingDirectories: Array = []; + const selectedModel = { provider: "deepseek", id: "deepseek-chat" }; + const runtime = { marker: "runtime" }; + const configuredRuntimes: unknown[] = []; + const sessionConfigs: Parameters[0][] = []; + let disposed = false; + let listener: ((event: { type?: string; assistantMessageEvent?: { type?: string; delta?: string } }) => void) | undefined; + const stubSdk: PiSdkLike = { + createAgentSession: async (config) => { + sessionConfigs.push(config); + workingDirectories.push(config.cwd); + return { + session: { + prompt: async (text: string) => { + prompts.push(text); + listener?.({ + type: "message_update", + assistantMessageEvent: { type: "text_delta", delta: "pi-response" }, + }); + return undefined; + }, + subscribe: (nextListener) => { + listener = nextListener; + return () => { + listener = undefined; + }; + }, + dispose: () => { + disposed = true; + }, + }, + }; + }, + SessionManager: { inMemory: () => ({}) }, + ModelRuntime: { create: async () => runtime }, + }; + const executor = new PiSdkExecutor({ + loadSdk: async () => stubSdk, + configureModelRuntime: async (modelRuntime) => { + configuredRuntimes.push(modelRuntime); + }, + selectModel: (modelRuntime) => { + expect(modelRuntime).toBe(runtime); + return selectedModel; + }, + }); + + const result = await executor.execute(revision, bundle, { prompt: "Fix the bug", cwd: "/tmp" }); + + expect(prompts).toHaveLength(1); + expect(configuredRuntimes).toEqual([runtime]); + expect(workingDirectories).toEqual(["/tmp"]); + expect(sessionConfigs[0]).toMatchObject({ + modelRuntime: runtime, + model: selectedModel, + noTools: "all", + }); + expect(disposed).toBe(true); + expect(prompts[0].endsWith("Fix the bug")).toBe(true); + expect(prompts[0]).toContain(revision.revisionId); + expect(result).toMatchObject({ host: "pi", exitCode: 0, output: "pi-response" }); + }); + + it("surfaces a model failure encoded in the final Pi assistant message", async () => { + const { bundle, revision } = await resolveFor("on-pi"); + let listener: + | ((event: { + type?: string; + message?: { role?: string; stopReason?: string; errorMessage?: string }; + }) => void) + | undefined; + let disposed = false; + const stubSdk: PiSdkLike = { + createAgentSession: async () => ({ + session: { + prompt: async () => { + listener?.({ + type: "message_end", + message: { + role: "assistant", + stopReason: "error", + errorMessage: "provider rejected request", + }, + }); + }, + subscribe: (nextListener) => { + listener = nextListener; + return () => { + listener = undefined; + }; + }, + dispose: () => { + disposed = true; + }, + }, + }), + SessionManager: { inMemory: () => ({}) }, + ModelRuntime: { create: async () => ({}) }, + }; + + const result = await new PiSdkExecutor({ loadSdk: async () => stubSdk }).execute( + revision, + bundle, + { prompt: "Fix the bug" }, + ); + + expect(result).toMatchObject({ + exitCode: 1, + output: "", + errorOutput: "provider rejected request", + }); + expect(disposed).toBe(true); + }); + + it("rejects a revision targeting another host before loading the Pi SDK", async () => { + const { bundle, revision } = await resolveFor("on-qoder"); + let loaded = false; + const executor = new PiSdkExecutor({ + loadSdk: async () => { + loaded = true; + throw new Error("should not load"); + }, + }); + + await expect(executor.execute(revision, bundle, { prompt: "Fix the bug" })).rejects.toThrow( + /targets host 'qoder'.*executor host is 'pi'/, + ); + expect(loaded).toBe(false); + }); + + it("reports the missing optional peer dependency with install guidance", async () => { + const { bundle, revision } = await resolveFor("on-pi"); + const executor = new PiSdkExecutor({ + loadSdk: async () => { + throw new Error("simulated missing optional peer"); + }, + }); + + await expect(executor.execute(revision, bundle, { prompt: "Fix the bug" })).rejects.toThrow( + /@earendil-works\/pi-coding-agent/, + ); + }); +}); + +describe("installed SDK contracts", () => { + it("exposes the Qoder SDK entry points used by the executor", async () => { + const sdk = await import("@qoder-ai/qoder-agent-sdk"); + + expect(sdk.query).toBeTypeOf("function"); + expect(sdk.qodercliAuth).toBeTypeOf("function"); + }); + + it("exposes the current Pi SDK entry points used by the executor", async () => { + const sdk = await import("@earendil-works/pi-coding-agent"); + + expect(sdk.createAgentSession).toBeTypeOf("function"); + expect(sdk.SessionManager.inMemory).toBeTypeOf("function"); + expect(sdk.ModelRuntime.create).toBeTypeOf("function"); + }); +}); + +describe("materializePiPackage", () => { + let directory: string | undefined; + + afterEach(async () => { + if (directory) { + await rm(directory, { recursive: true, force: true }); + directory = undefined; + } + }); + + it("writes an installable Pi package with skills for advisory components", async () => { + const { bundle, revision } = await resolveFor("on-pi"); + directory = await mkdtemp(join(tmpdir(), "harness-pi-")); + + const written = await materializePiPackage(revision, bundle, directory); + + expect(written).toContain("package.json"); + const manifest = JSON.parse(await readFile(join(directory, "package.json"), "utf8")); + expect(manifest.pi.skills).toEqual(["./skills"]); + const skill = await readFile(join(directory, "skills", "impact-analysis", "SKILL.md"), "utf8"); + expect(skill).toContain("name: impact-analysis"); + expect(skill).toContain('description: "Impact analysis: map the blast radius before editing."'); + expect(skill).toContain(revision.revisionId); + // Policies remain prompt guidance and must not be mislabeled as Pi skills. + expect(written.some((path) => path.includes("verification-before-complete"))).toBe(false); + await expect( + access(join(directory, "skills", "verification-before-complete", "SKILL.md")), + ).rejects.toThrow(); + }); + + it("rejects materialization for a revision targeting another host", async () => { + const { bundle, revision } = await resolveFor("on-qoder"); + directory = await mkdtemp(join(tmpdir(), "harness-pi-")); + + await expect(materializePiPackage(revision, bundle, directory)).rejects.toThrow( + /targets host 'qoder'.*executor host is 'pi'/, + ); + }); + + it("fails closed instead of mixing a revision with pre-existing files", async () => { + const { bundle, revision } = await resolveFor("on-pi"); + directory = await mkdtemp(join(tmpdir(), "harness-pi-")); + await writeFile(join(directory, "keep.txt"), "user-owned\n", "utf8"); + + await expect(materializePiPackage(revision, bundle, directory)).rejects.toThrow( + /destination must be empty/, + ); + expect(await readFile(join(directory, "keep.txt"), "utf8")).toBe("user-owned\n"); + }); +}); diff --git a/packages/harness/test/highlight.test.ts b/packages/harness/test/highlight.test.ts new file mode 100644 index 0000000..a0596be --- /dev/null +++ b/packages/harness/test/highlight.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, it } from "vitest"; +import { highlightHarness, tokenizeHarness } from "../src/highlight/shiki.js"; + +const SAMPLE = `// assemble +composition standard-coding { + target pi + include [ plugin.change-verification@^2 ] + require verification-before-complete { + minimum wired + } + configure { + shell.timeout = 60s + } +} +`; + +function findToken(tokens: Awaited>, content: string) { + for (const line of tokens) { + for (const token of line) { + if (token.content.trim() === content) { + return token; + } + } + } + return undefined; +} + +describe("harness syntax highlighting", () => { + it("renders themed HTML for the registered harness language", async () => { + const html = await highlightHarness(SAMPLE); + + expect(html.startsWith(" { + const tokens = await tokenizeHarness(SAMPLE); + + const keyword = findToken(tokens, "composition"); + const strength = findToken(tokens, "wired"); + const name = findToken(tokens, "standard-coding"); + const comment = findToken(tokens, "// assemble"); + + expect(keyword).toBeDefined(); + expect(strength).toBeDefined(); + expect(name).toBeDefined(); + expect(comment).toBeDefined(); + // Behavioural contract of the grammar: these categories must not fall + // back to one identical style. + expect(keyword!.color).not.toBe(name!.color); + expect(strength!.color).not.toBe(name!.color); + expect(comment!.color).not.toBe(keyword!.color); + }); + + it("splits declaration keyword and declared name into separate tokens", async () => { + const tokens = await tokenizeHarness("composition standard-coding {\n}\n"); + const firstLine = tokens[0].map((token) => token.content.trim()).filter(Boolean); + + expect(firstLine[0]).toBe("composition"); + expect(firstLine).toContain("standard-coding"); + }); +}); diff --git a/packages/harness/test/resolve.test.ts b/packages/harness/test/resolve.test.ts new file mode 100644 index 0000000..6594141 --- /dev/null +++ b/packages/harness/test/resolve.test.ts @@ -0,0 +1,247 @@ +import { readFile } from "node:fs/promises"; +import { Value } from "@sinclair/typebox/value"; +import { describe, expect, it } from "vitest"; +import { compileHarness } from "../src/compiler/compile.js"; +import { HarnessRevisionSchema, ResolutionReportSchema, type HarnessIrBundle } from "../src/ir/index.js"; +import { resolveComposition } from "../src/resolver/resolve.js"; + +const EXAMPLE_URL = new URL("../examples/standard-coding.harness", import.meta.url); + +async function compileExample(): Promise { + const source = await readFile(EXAMPLE_URL, "utf8"); + const { bundle } = await compileHarness(source); + return bundle!; +} + +async function compileSource(source: string): Promise { + const result = await compileHarness(source); + expect(result.diagnostics.filter((d) => d.severity === "error")).toEqual([]); + return result.bundle!; +} + +describe("resolveComposition", () => { + it("produces a schema-valid revision and reports declared versus materialized strength", async () => { + const bundle = await compileExample(); + const { revision, report } = resolveComposition(bundle, "standard-coding"); + + expect(report.status).toBe("resolved"); + expect(Value.Check(ResolutionReportSchema, report)).toBe(true); + expect(revision).toBeDefined(); + expect(Value.Check(HarnessRevisionSchema, revision)).toBe(true); + expect(revision!.target.host).toBe("pi"); + expect(revision!.resolved.plugins.map((plugin) => `${plugin.id}@${plugin.version}`)).toEqual([ + "change-verification@2.1.3", + "repository-understanding@1.4.2", + ]); + expect(report.realizations).toEqual([ + expect.objectContaining({ + componentId: "impact-analysis", + declaredStrength: "advisory", + realized: "advisory", + materializedMechanism: "prompt-preamble", + action: "satisfied", + reason: "implicitly enabled by an included plugin", + }), + expect.objectContaining({ + componentId: "verification-before-complete", + declaredStrength: "enforced", + declaredMechanism: "extension-event", + realized: "advisory", + materializedMechanism: "prompt-preamble", + action: "degraded", + }), + ]); + }); + + it("is deterministic: the same bundle resolves to the same revision id", async () => { + const first = resolveComposition(await compileExample(), "standard-coding"); + const second = resolveComposition(await compileExample(), "standard-coding"); + + expect(first.revision!.revisionId).toBe(second.revision!.revisionId); + expect(first.revision!.revisionId).toMatch(/^hr_[0-9a-f]{32}$/); + }); + + const degradedSource = (onDegrade: string) => ` + component gate { + kind policy + description "Verify before completing." + } + binding gate for codex { + mechanism system-instruction + strength advisory + } + plugin verification { + version "2.0.0" + provides [ component.gate ] + } + composition run-on-codex { + target codex + include [ plugin.verification@^2 ] + require gate { + preferred enforced + minimum advisory + on-degrade ${onDegrade} + } + } + `; + + it("fails resolution when a degradation below preferred hits 'on-degrade fail'", async () => { + const bundle = await compileSource(degradedSource("fail")); + const { revision, report } = resolveComposition(bundle, "run-on-codex"); + + expect(revision).toBeUndefined(); + expect(report.status).toBe("failed"); + expect(report.errors.length).toBeGreaterThan(0); + expect(report.realizations[0]).toMatchObject({ + componentId: "gate", + requestedPreferred: "enforced", + declaredStrength: "advisory", + realized: "advisory", + action: "failed", + }); + }); + + it("records the degradation and still resolves under 'on-degrade report'", async () => { + const bundle = await compileSource(degradedSource("report")); + const { revision, report } = resolveComposition(bundle, "run-on-codex"); + + expect(report.status).toBe("resolved"); + expect(revision).toBeDefined(); + expect(revision!.realization[0]).toMatchObject({ + requestedPreferred: "enforced", + declaredStrength: "advisory", + realized: "advisory", + declaredMechanism: "system-instruction", + materializedMechanism: "prompt-preamble", + action: "degraded", + }); + expect(revision!.realization[0].reason).toBeDefined(); + }); + + it("always fails when the realized strength is below minimum", async () => { + const bundle = await compileSource(` + component gate { kind policy } + binding gate for codex { mechanism system-instruction strength advisory } + plugin verification { version "2.0.0" provides [ component.gate ] } + composition strict { + target codex + include [ plugin.verification@^2 ] + require gate { + minimum wired + on-degrade report + } + } + `); + const { revision, report } = resolveComposition(bundle, "strict"); + + expect(revision).toBeUndefined(); + expect(report.status).toBe("failed"); + }); + + it("does not treat a declared enforced binding as materialized enforcement", async () => { + const bundle = await compileSource(` + component gate { kind policy } + binding gate for pi { mechanism completion-hook strength enforced } + plugin verification { version "1.0.0" provides [ component.gate ] } + composition strict { + target pi + include [ plugin.verification@1 ] + require gate { minimum enforced } + } + `); + + const { revision, report } = resolveComposition(bundle, "strict"); + + expect(revision).toBeUndefined(); + expect(report.status).toBe("failed"); + expect(report.realizations[0]).toMatchObject({ + declaredStrength: "enforced", + declaredMechanism: "completion-hook", + realized: "advisory", + materializedMechanism: "prompt-preamble", + action: "failed", + }); + }); + + it("fails when no plugin version satisfies the requested range", async () => { + const bundle = await compileSource(` + component gate { kind policy } + binding gate for pi { mechanism completion-hook strength enforced } + plugin verification { version "1.0.0" provides [ component.gate ] } + composition wants-v2 { + target pi + include [ plugin.verification@^2 ] + require gate { minimum advisory } + } + `); + const { revision, report } = resolveComposition(bundle, "wants-v2"); + + expect(revision).toBeUndefined(); + expect(report.status).toBe("failed"); + expect(report.errors.some((error) => error.includes("verification"))).toBe(true); + }); + + it("merges permissions monotonically: an explicit deny wins over allow", async () => { + const bundle = await compileSource(` + component fetcher { + kind tool + permissions { network allow } + } + component gate { + kind policy + permissions { network deny workspace read } + } + binding fetcher for pi { mechanism tool-adapter strength wired } + binding gate for pi { mechanism completion-hook strength enforced } + plugin all { version "1.0.0" provides [ component.fetcher, component.gate ] } + composition run { + target pi + include [ plugin.all@1 ] + } + `); + const { revision } = resolveComposition(bundle, "run"); + + expect(revision!.permissions).toEqual([ + { domain: "network", access: "deny" }, + { domain: "workspace", access: "read" }, + ]); + }); + + it("marks an implicitly enabled component without a host binding as degraded", async () => { + const bundle = await compileSource(` + component optional-observer { kind observer } + plugin diagnostics { version "1.0.0" provides [ component.optional-observer ] } + composition run { target pi include [ plugin.diagnostics@1 ] } + `); + + const { revision, report } = resolveComposition(bundle, "run"); + + expect(report.status).toBe("resolved"); + expect(revision!.realization[0]).toMatchObject({ + componentId: "optional-observer", + declaredStrength: "unsupported", + realized: "unsupported", + action: "degraded", + }); + expect(revision!.realization[0].reason).toContain("has no binding"); + }); + + it("marks an implicitly enabled component with an unsupported binding as degraded", async () => { + const bundle = await compileSource(` + component optional-observer { kind observer } + binding optional-observer for pi { mechanism unavailable strength unsupported } + plugin diagnostics { version "1.0.0" provides [ component.optional-observer ] } + composition run { target pi include [ plugin.diagnostics@1 ] } + `); + + const { revision } = resolveComposition(bundle, "run"); + + expect(revision!.realization[0]).toMatchObject({ + declaredStrength: "unsupported", + declaredMechanism: "unavailable", + realized: "unsupported", + action: "degraded", + }); + expect(revision!.realization[0].reason).toContain("declares this component unsupported"); + }); +}); diff --git a/packages/harness/tsconfig.json b/packages/harness/tsconfig.json new file mode 100644 index 0000000..8a00e90 --- /dev/null +++ b/packages/harness/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "declaration": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/harness/vitest.config.ts b/packages/harness/vitest.config.ts new file mode 100644 index 0000000..8dd72f9 --- /dev/null +++ b/packages/harness/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["test/**/*.test.ts"], + pool: "forks", + testTimeout: 60_000, + }, +}); diff --git a/scripts/npm-package/verify-pack.mjs b/scripts/npm-package/verify-pack.mjs index e6916e5..25bc362 100644 --- a/scripts/npm-package/verify-pack.mjs +++ b/scripts/npm-package/verify-pack.mjs @@ -20,6 +20,9 @@ function run(command, args) { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], + // `npm pack --json` includes one record per packaged file. The repository's + // intentionally large docs/reference surface exceeds Node's 1 MiB default. + maxBuffer: 64 * 1024 * 1024, }); if (result.status !== 0) { fail(`${command} ${args.join(" ")} exited ${result.status}\n${result.error?.message ?? result.stderr}`); From e2262411fb995c70ac66261f442213f7e3cc6601 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 15 Aug 2026 08:34:39 +0800 Subject: [PATCH 02/25] feat(harness): teach agents to generate DSL Add a package-local generation skill, compact language contract, and deterministic compiler/resolver validator, and include them in the @qoder-ai/harness tarball. Cover valid, invalid, missing-composition, and honest degradation behavior. Implements docs/specs/2026-08-15-harness-dsl-generation-skill.md. Validated with the 42-test package suite, skill validation, agent asset lint, documentation-link checks, npm pack inspection, a cold package install, and an isolated fresh-agent forward test. Co-authored-by: Codex (GPT 5.6 Sol) --- ...2026-08-15-harness-dsl-generation-skill.md | 102 ++++++++++++++ packages/harness/README.md | 12 ++ packages/harness/package.json | 1 + .../skills/generate-harness-dsl/SKILL.md | 67 +++++++++ .../generate-harness-dsl/agents/openai.yaml | 4 + .../references/dsl-contract.md | 129 ++++++++++++++++++ .../generate-harness-dsl/scripts/validate.mjs | 88 ++++++++++++ packages/harness/test/skill.test.ts | 109 +++++++++++++++ 8 files changed, 512 insertions(+) create mode 100644 docs/specs/2026-08-15-harness-dsl-generation-skill.md create mode 100644 packages/harness/skills/generate-harness-dsl/SKILL.md create mode 100644 packages/harness/skills/generate-harness-dsl/agents/openai.yaml create mode 100644 packages/harness/skills/generate-harness-dsl/references/dsl-contract.md create mode 100644 packages/harness/skills/generate-harness-dsl/scripts/validate.mjs create mode 100644 packages/harness/test/skill.test.ts diff --git a/docs/specs/2026-08-15-harness-dsl-generation-skill.md b/docs/specs/2026-08-15-harness-dsl-generation-skill.md new file mode 100644 index 0000000..b90890a --- /dev/null +++ b/docs/specs/2026-08-15-harness-dsl-generation-skill.md @@ -0,0 +1,102 @@ +# Teach agents to generate valid Harness DSL + +## Traceability + +- Spec ID: harness-dsl-generation-skill +- Status: Implemented + +## Intent + +Ship a package-local skill that teaches coding agents to author complete, +valid Harness as Code v0.1 documents. The skill must route agents to the +canonical language contract, require deterministic compiler and resolver +validation, and preserve the runtime's declared-versus-materialized strength +boundary. + +The skill belongs to `packages/harness/skills/` so it evolves with the DSL and +is included in the `@qoder-ai/harness` tarball. It is generation guidance, not +an executor: it must not start Qoder or Pi sessions, handle model credentials, +or claim native enforcement without runtime evidence. + +## Acceptance Scenarios + +- AC-1: Invoking `$generate-harness-dsl` gives an agent a concise workflow for + eliciting a target, declaring components, bindings, plugins, and + compositions, and returning a complete `.harness` document. +- AC-2: The routed reference documents the v0.1 grammar, semantic constraints, + version and identifier rules, configuration value types, and the + declared-versus-materialized strength boundary without duplicating runtime + ownership. +- AC-3: A portable Node script compiles a supplied `.harness` file with the + package public API, resolves all or selected compositions, emits structured + JSON diagnostics, and exits non-zero for invalid or unresolved input. +- AC-4: Behavior tests exercise the validator with valid and invalid files; + the skill package passes the system skill validator and repository agent + asset lint. +- AC-5: The npm package manifest includes the package-local skill, and an npm + pack inspection confirms `SKILL.md`, its reference, validator, and generated + UI metadata are present without secrets or source-local dependencies. +- AC-6: A fresh AI forward test, run outside the repository worktree, follows + the skill to produce DSL that passes the deterministic validator and reports + any resolution degradation honestly. +- AC-7: Relative Markdown links remain valid and the repository documentation + link graph stays green. + +## Non-goals + +- Changing the Harness DSL grammar, IR, resolver, or executor behavior. +- Running Qoder or Pi SDK inference as part of DSL generation or validation. +- Storing API keys, authentication material, or other secrets in the skill or + generated DSL. +- Installing the skill globally or adding a new Coding Agent host adapter. +- Claiming `wired` or `enforced` materialization from declarations alone. +- Publishing a package, changing versions, release notes, or the changelog. + +## Plan and Tasks + +1. Initialize the package-local skill with the standard skill scaffolder and + generate its UI metadata. +2. Write a compact `SKILL.md`, a routed v0.1 contract reference, and a portable + compiler/resolver validation script. +3. Include the skill directory in the package tarball and add behavior tests + for the validator's success and failure contracts. +4. Run skill, package, documentation, and pack validation, then forward-test + the instructions with a fresh AI agent from a neutral directory. +5. Record observed evidence and review the final staged/unstaged boundary. + +## Test and Review Evidence + +- AC-1/AC-2: `SKILL.md` routes to one focused v0.1 contract reference and the + package example. System `quick_validate.py` accepted the skill structure and + metadata; `agent-lint --profile agent-assets-review` reported one skill with + zero findings, errors, warnings, or advisories. +- AC-3/AC-4: `npm run harness:build` and the 42-test package suite passed. The + behavior tests execute the shipped Node validator against both the package + example and invalid temporary input, asserting parsed JSON, exit status, and + declared-versus-realized strength rather than source text. +- AC-5: package `npm pack` prepack gates passed and produced a 40-entry tarball + containing `SKILL.md`, `agents/openai.yaml`, `references/dsl-contract.md`, and + `scripts/validate.mjs`. A clean temporary install of that tarball ran the + installed validator successfully against the installed Qoder example. +- AC-6: an isolated fresh agent used the skill to create + `safe-repository-change` outside the repository. Independent validation + compiled and resolved it with no diagnostics, satisfying repository impact + at advisory strength and reporting verification honestly degraded from + declared `enforced` to materialized `advisory`. An attempted Qoder CLI run + stopped before inference because the isolated config was not logged in; it + is not counted as forward-test evidence. +- AC-7: the focused documentation link graph passed all six assertions, and + root pack verification passed with 524 npm entries and 546 runtime-zip + entries. Generated Langium source verification also passed. +- Full regression: the root test run passed 1,322 of 1,324 tests. Two unrelated + governance tests timed out at their 120-second limit; a focused rerun passed + 52 of 54 but timed out in two different test cases, indicating an unstable + baseline timing issue rather than a deterministic assertion or changed DSL + package behavior. Package, generated-source, documentation, validator, and + pack gates all completed successfully when run separately. +- Risk: the validator imports `dist/` relative to the packaged skill. The clean + tarball install smoke proves this layout; source-tree contributors must build + before invoking it, and the failure message gives that command. +- Risk: generated DSL can overstate a host binding. The reference, workflow, + behavior test, and fresh-agent output all preserve the v0.1 advisory ceiling + and expose degradation in the resolution report. diff --git a/packages/harness/README.md b/packages/harness/README.md index 121de03..9596669 100644 --- a/packages/harness/README.md +++ b/packages/harness/README.md @@ -76,6 +76,18 @@ const html = await highlightHarness(source); // Shiki, lang: "harness" See [`examples/standard-coding.harness`](examples/standard-coding.harness) for the full surface syntax. +## AI authoring skill + +The package includes +[`skills/generate-harness-dsl/SKILL.md`](skills/generate-harness-dsl/SKILL.md), +which teaches compatible coding agents to generate complete v0.1 documents and +validate every composition with the same compiler and resolver exported by this +package. Its validator is also available directly after the package is built: + +```sh +node skills/generate-harness-dsl/scripts/validate.mjs workflow.harness +``` + ## Executor honesty rules v0.1 executors materialize components as prompt guidance, so effective strength diff --git a/packages/harness/package.json b/packages/harness/package.json index 2966adc..6354efb 100644 --- a/packages/harness/package.json +++ b/packages/harness/package.json @@ -37,6 +37,7 @@ "files": [ "dist/", "examples/", + "skills/", "README.md", "LICENSE" ], diff --git a/packages/harness/skills/generate-harness-dsl/SKILL.md b/packages/harness/skills/generate-harness-dsl/SKILL.md new file mode 100644 index 0000000..7ee1d23 --- /dev/null +++ b/packages/harness/skills/generate-harness-dsl/SKILL.md @@ -0,0 +1,67 @@ +--- +name: generate-harness-dsl +description: Generate, revise, or review complete Harness as Code `.harness` files for coding-agent compositions. Use when a user asks to describe components, host bindings, plugins, composition requirements, permissions, degradation policy, or typed settings in the `@qoder-ai/harness` DSL, or when existing Harness DSL must be made compiler-valid and resolvable. +--- + +# Generate Harness DSL + +Create a complete, standalone Harness as Code v0.1 document and prove it with +the package compiler and resolver. Keep declared host capability separate from +what the v0.1 runtime actually materializes. + +## Workflow + +1. Establish the requested coding-agent host, capabilities, permissions, + minimum and preferred strengths, degradation behavior, and configuration. + Infer low-risk details from context; ask only when a missing choice changes + the safety or meaning of the composition. +2. Read [the DSL contract](references/dsl-contract.md) before authoring. Open + [the complete example](../../examples/standard-coding.harness) only when a + full syntax example is useful. +3. Generate one self-contained `.harness` document unless the user explicitly + requests a fragment. Declare each referenced component and plugin in the + same document. +4. Validate the result with `scripts/validate.mjs`. Fix every compiler or + resolution error, rerun validation, and inspect degraded realizations before + presenting the result. +5. Return the DSL or saved file plus a short summary of the target, + composition id, included plugins, resolution status, and any degradation. + +## Authoring Rules + +- Use descriptive, stable identifiers and exact semantic versions for plugin + declarations. Use a compatible version range in each composition include. +- Declare every component referenced by a binding, plugin, or requirement. + Do not duplicate ids, bindings, plugin includes, requirements, inputs, + outputs, or configuration keys. +- Request only permissions the component needs. Never place credentials, + tokens, private keys, or secret values in DSL source. +- Treat `wired` and `enforced` as binding declarations, not observed runtime + guarantees. Harness v0.1 materializes supported bindings as + `prompt-preamble` at no more than `advisory` strength. +- Use `on-degrade fail` when falling below the preferred strength must stop + resolution. Use `report` only when the weaker materialization is acceptable + and make that degradation visible to the user. +- Do not invoke Qoder or Pi SDKs, install host integrations, or claim native + enforcement while generating or validating DSL. + +## Validate + +Run from this skill directory, substituting the output file and optionally one +or more composition ids: + +```sh +node scripts/validate.mjs /path/to/workflow.harness [composition-id ...] +``` + +The command prints JSON and exits non-zero when compilation or any selected +composition resolution fails. If no composition id is supplied, it resolves +every composition in the file. A successful exit is required before calling +generated DSL valid. + +When editing this skill itself, build the package first so `dist/` reflects the +current compiler: + +```sh +npm run harness:build +``` diff --git a/packages/harness/skills/generate-harness-dsl/agents/openai.yaml b/packages/harness/skills/generate-harness-dsl/agents/openai.yaml new file mode 100644 index 0000000..9fc0a93 --- /dev/null +++ b/packages/harness/skills/generate-harness-dsl/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Generate Harness DSL" + short_description: "Generate and validate Harness as Code DSL" + default_prompt: "Use $generate-harness-dsl to create a validated Harness as Code composition for my coding-agent workflow." diff --git a/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md b/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md new file mode 100644 index 0000000..7eed865 --- /dev/null +++ b/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md @@ -0,0 +1,129 @@ +# Harness as Code v0.1 contract + +Use this reference as the authoring contract. The grammar and runtime remain +owned by `packages/harness/src/`; this file is a compact generation guide. + +- [Document model](#document-model) +- [Component](#component) +- [Host binding](#host-binding) +- [Plugin](#plugin) +- [Composition](#composition) +- [Semantic checklist](#semantic-checklist) + +## Document model + +A complete document normally declares, in dependency order: + +1. `component`: a host-independent capability contract. +2. `binding`: how one host declares that it can realize a component. +3. `plugin`: an exact-version distribution that provides components. +4. `composition`: the target host, plugin ranges, requirements, and settings. + +Comments use `//` or `/* ... */`. Whitespace is insignificant. Identifiers must +match `[_a-zA-Z][\w-]*`; prefer descriptive kebab-case identifiers and avoid +language keywords such as `component`, `binding`, `plugin`, `composition`, +`report`, `fail`, `true`, and `false`. + +Strings use double quotes and do not support embedded escaped double quotes. +Rephrase descriptions that would otherwise require one. + +## Component + +```harness +component repository-analysis { + kind skill + description "Map affected modules before editing." + input { changed-files } + output { impact-summary } + permissions { + workspace read + network deny + } +} +``` + +Kinds are `skill`, `tool`, `program`, `workflow`, `hook`, `policy`, `observer`, +or `ui`. Permission domains are `workspace`, `process`, `network`, and `model`; +access values are `read`, `write`, `allow`, and `deny`. Do not use commas inside +`input`, `output`, or `permissions` blocks. + +## Host binding + +```harness +binding repository-analysis for qoder { + mechanism skill-routing + strength advisory + notes "Injected as prompt guidance in v0.1." +} +``` + +The host and mechanism are identifiers. Strength is `unsupported`, `advisory`, +`wired`, or `enforced`. Declare at most one binding per component and host. + +A binding strength states the strongest host capability being modeled. It does +not prove that the current executor materialized that capability. In v0.1, a +supported binding resolves through `prompt-preamble` at `advisory`; a missing or +`unsupported` binding realizes as `unsupported`. + +## Plugin + +```harness +plugin repository-tools { + version "1.2.0" + provides [ component.repository-analysis ] +} +``` + +Use a full, valid semantic version such as `1.2.0`. Separate multiple provided +components with commas. Every provided component must be declared. + +## Composition + +```harness +composition repository-change { + target qoder + + include [ plugin.repository-tools@^1 ] + + require repository-analysis { + preferred advisory + minimum advisory + on-degrade fail + } + + configure { + shell.timeout = 60s + tool-call-budget = 80 + explain-degradation = true + review-label = "repository change" + } +} +``` + +Include ranges accept a numeric version optionally prefixed by `^` or `~`, for +example `@1`, `@^1`, `@~1.2`, or `@1.2.0`. Do not include the same plugin twice. + +Each requirement needs `minimum`. `preferred` and `on-degrade` are optional; +the degradation policy defaults to `report`. Resolution behaves as follows: + +- realized below minimum: fail; +- realized below preferred with `on-degrade fail`: fail; +- realized below preferred with `on-degrade report`: resolve as degraded; +- realized at or above preferred: satisfy. + +Configuration keys are dotted or hyphenated identifiers. Values may be +non-negative integers, booleans, strings, or integer durations ending in +`ms`, `s`, `m`, or `h`. + +## Semantic checklist + +- Declare every referenced component and plugin. +- Give every plugin declaration an exact semantic version. +- Keep declaration ids, bindings, includes, requirements, inputs, outputs, and + configuration keys unique within their applicable scope. +- Include a plugin that provides each required component. +- Bind required components for the composition target. +- Set minimum and degradation policy against v0.1's actual advisory ceiling. +- Keep secrets out of source and descriptions. +- Run `scripts/validate.mjs` and inspect every realization, not only the exit + code. diff --git a/packages/harness/skills/generate-harness-dsl/scripts/validate.mjs b/packages/harness/skills/generate-harness-dsl/scripts/validate.mjs new file mode 100644 index 0000000..6431f59 --- /dev/null +++ b/packages/harness/skills/generate-harness-dsl/scripts/validate.mjs @@ -0,0 +1,88 @@ +#!/usr/bin/env node + +import { readFile } from "node:fs/promises"; +import { basename, resolve } from "node:path"; + +function print(value) { + process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); +} + +function usage() { + process.stderr.write( + "Usage: node scripts/validate.mjs [composition-id ...]\n", + ); +} + +async function loadHarnessApi() { + try { + return await import(new URL("../../../dist/index.js", import.meta.url)); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Cannot load the built @qoder-ai/harness package. Run npm run harness:build first. ${message}`); + } +} + +async function main() { + const [input, ...requestedIds] = process.argv.slice(2); + if (!input || input === "--help" || input === "-h") { + usage(); + process.exitCode = input ? 0 : 2; + return; + } + + const inputPath = resolve(input); + const text = await readFile(inputPath, "utf8"); + const { compileHarness, resolveComposition } = await loadHarnessApi(); + const source = `harness://input/${encodeURIComponent(basename(inputPath))}`; + const compiled = await compileHarness([{ uri: source, text }]); + + if (!compiled.bundle) { + print({ + valid: false, + file: input, + diagnostics: compiled.diagnostics, + compositions: [], + }); + process.exitCode = 1; + return; + } + + const compositionIds = requestedIds.length > 0 + ? requestedIds + : compiled.bundle.compositions.map((composition) => composition.id); + const reports = compositionIds.map((compositionId) => { + const { revision, report } = resolveComposition(compiled.bundle, compositionId); + return { + compositionId, + target: report.target, + status: report.status, + revisionId: revision?.revisionId ?? null, + errors: report.errors, + realizations: report.realizations, + }; + }); + const structuralErrors = compositionIds.length === 0 + ? [{ + severity: "error", + message: "No composition is declared; generated DSL must be independently resolvable.", + source, + }] + : []; + const valid = structuralErrors.length === 0 && reports.every((report) => report.status === "resolved"); + + print({ + valid, + file: input, + diagnostics: [...compiled.diagnostics, ...structuralErrors], + compositions: reports, + }); + if (!valid) { + process.exitCode = 1; + } +} + +main().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + print({ valid: false, diagnostics: [{ severity: "error", message }], compositions: [] }); + process.exitCode = 1; +}); diff --git a/packages/harness/test/skill.test.ts b/packages/harness/test/skill.test.ts new file mode 100644 index 0000000..4ee2dfb --- /dev/null +++ b/packages/harness/test/skill.test.ts @@ -0,0 +1,109 @@ +import { spawnSync } from "node:child_process"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; + +const VALIDATOR_PATH = fileURLToPath( + new URL("../skills/generate-harness-dsl/scripts/validate.mjs", import.meta.url), +); +const EXAMPLE_PATH = fileURLToPath( + new URL("../examples/standard-coding.harness", import.meta.url), +); +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +function runValidator(file: string, ...compositionIds: string[]) { + return spawnSync(process.execPath, [VALIDATOR_PATH, file, ...compositionIds], { + encoding: "utf8", + }); +} + +describe("generate-harness-dsl validator", () => { + it("compiles and resolves the published example through the built public API", () => { + const result = runValidator(EXAMPLE_PATH); + + expect(result.status).toBe(0); + const output = JSON.parse(result.stdout); + expect(output.valid).toBe(true); + expect(output.diagnostics).toEqual([]); + expect(output.compositions).toEqual([ + expect.objectContaining({ compositionId: "standard-coding", status: "resolved" }), + expect.objectContaining({ compositionId: "standard-coding-qoder", status: "resolved" }), + ]); + expect(output.compositions[0].realizations).toContainEqual( + expect.objectContaining({ + componentId: "verification-before-complete", + declaredStrength: "enforced", + realized: "advisory", + action: "degraded", + }), + ); + }); + + it("returns structured diagnostics and a non-zero status for invalid DSL", async () => { + const directory = await mkdtemp(join(tmpdir(), "harness-skill-test-")); + temporaryDirectories.push(directory); + const file = join(directory, "invalid.harness"); + await writeFile( + file, + "composition broken { target qoder include [ plugin.missing@1 ] }", + "utf8", + ); + + const result = runValidator(file); + + expect(result.status).toBe(1); + const output = JSON.parse(result.stdout); + expect(output.valid).toBe(false); + expect(output.compositions).toEqual([]); + expect(output.diagnostics).toContainEqual(expect.objectContaining({ severity: "error" })); + }); + + it("fails when a requested composition does not exist", () => { + const result = runValidator(EXAMPLE_PATH, "missing-composition"); + + expect(result.status).toBe(1); + const output = JSON.parse(result.stdout); + expect(output.valid).toBe(false); + expect(output.compositions).toEqual([ + expect.objectContaining({ + compositionId: "missing-composition", + status: "failed", + errors: ["Composition 'missing-composition' is not defined in the bundle."], + }), + ]); + }); + + it("requires generated files to declare an independently resolvable composition", async () => { + const directory = await mkdtemp(join(tmpdir(), "harness-skill-test-")); + temporaryDirectories.push(directory); + const file = join(directory, "library-only.harness"); + await writeFile( + file, + "component repository-analysis { kind skill permissions { workspace read } }", + "utf8", + ); + + const result = runValidator(file); + + expect(result.status).toBe(1); + const output = JSON.parse(result.stdout); + expect(output.valid).toBe(false); + expect(output.compositions).toEqual([]); + expect(output.diagnostics).toContainEqual( + expect.objectContaining({ + severity: "error", + message: "No composition is declared; generated DSL must be independently resolvable.", + }), + ); + }); +}); From e519865fb9bcc1682ae863a885bf0b526f7d8fc6 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 15 Aug 2026 10:20:41 +0800 Subject: [PATCH 03/25] feat(harness): compare real coding outcomes Implements docs/specs/2026-08-15-harness-coding-compare.md with a full-surface DSL fixture and a frozen Qoder README comparison runner. Trials use isolated repositories, bounded tool permissions, deterministic grading, redacted traces, runtime receipts, and durable verdict artifacts. Validated with npm run check, a native Qoder H0/H1 run where both real README outcomes scored 100, a permission-model regrade, and a cold tarball install/import smoke. Co-authored-by: Codex (GPT 5.6 Sol) --- .../2026-08-15-harness-coding-compare.md | 126 ++++ packages/harness/README.md | 39 ++ .../harness/examples/full-surface.harness | 93 +++ .../examples/readme-compare/experiment.json | 34 + .../readme-compare/fixture/docs/archive-v0.md | 5 + .../readme-compare/fixture/examples/basic.mjs | 10 + .../readme-compare/fixture/package.json | 12 + .../readme-compare/fixture/src/index.mjs | 1 + .../readme-compare/fixture/src/retry.mjs | 39 ++ .../fixture/test/retry.test.mjs | 35 ++ .../readme-compare/grader-contract.json | 9 + .../harness/examples/readme-compare/prompt.md | 17 + .../readme-compare/readme-compare.harness | 65 ++ packages/harness/package.json | 7 + packages/harness/src/compare/cli.ts | 66 ++ packages/harness/src/compare/grader.ts | 330 ++++++++++ packages/harness/src/compare/index.ts | 22 + packages/harness/src/compare/manifest.ts | 165 +++++ packages/harness/src/compare/permissions.ts | 96 +++ packages/harness/src/compare/process.ts | 55 ++ packages/harness/src/compare/runner.ts | 582 ++++++++++++++++++ packages/harness/src/exec/executor.ts | 33 + packages/harness/src/exec/index.ts | 6 + packages/harness/src/exec/qoder-sdk.ts | 140 ++++- packages/harness/src/index.ts | 1 + packages/harness/test/compare-cli.test.ts | 22 + packages/harness/test/compare.test.ts | 187 ++++++ packages/harness/test/compile.test.ts | 43 ++ packages/harness/test/exec.test.ts | 102 ++- 29 files changed, 2330 insertions(+), 12 deletions(-) create mode 100644 docs/specs/2026-08-15-harness-coding-compare.md create mode 100644 packages/harness/examples/full-surface.harness create mode 100644 packages/harness/examples/readme-compare/experiment.json create mode 100644 packages/harness/examples/readme-compare/fixture/docs/archive-v0.md create mode 100644 packages/harness/examples/readme-compare/fixture/examples/basic.mjs create mode 100644 packages/harness/examples/readme-compare/fixture/package.json create mode 100644 packages/harness/examples/readme-compare/fixture/src/index.mjs create mode 100644 packages/harness/examples/readme-compare/fixture/src/retry.mjs create mode 100644 packages/harness/examples/readme-compare/fixture/test/retry.test.mjs create mode 100644 packages/harness/examples/readme-compare/grader-contract.json create mode 100644 packages/harness/examples/readme-compare/prompt.md create mode 100644 packages/harness/examples/readme-compare/readme-compare.harness create mode 100644 packages/harness/src/compare/cli.ts create mode 100644 packages/harness/src/compare/grader.ts create mode 100644 packages/harness/src/compare/index.ts create mode 100644 packages/harness/src/compare/manifest.ts create mode 100644 packages/harness/src/compare/permissions.ts create mode 100644 packages/harness/src/compare/process.ts create mode 100644 packages/harness/src/compare/runner.ts create mode 100644 packages/harness/test/compare-cli.test.ts create mode 100644 packages/harness/test/compare.test.ts diff --git a/docs/specs/2026-08-15-harness-coding-compare.md b/docs/specs/2026-08-15-harness-coding-compare.md new file mode 100644 index 0000000..b059d3a --- /dev/null +++ b/docs/specs/2026-08-15-harness-coding-compare.md @@ -0,0 +1,126 @@ +# Compare harnesses with real coding outcomes + +## Traceability + +- Spec ID: harness-coding-compare +- Status: Implemented + +## Intent + +Add a reproducible Qoder-backed comparison workflow to `@qoder-ai/harness` so +maintainers can compare two DSL compositions by letting each one modify the +same isolated repository fixture and grading the resulting files. The first +benchmark creates a repository-grounded `README.md`; success is based on the +file diff and deterministic validation, not on whether an SDK returned text. + +Keep the `.harness` language responsible for agent-harness assembly. A separate +versioned experiment manifest owns tasks, trials, runtime budgets, graders, and +artifact locations so evaluation policy does not become authored composition +syntax. + +## Acceptance Scenarios + +- AC-1: A full-surface `.harness` fixture exercises every component kind, + permission domain/access, binding strength, degradation policy, and + configuration value type. Tests assert compiled IR and resolution behavior, + while focused invalid fixtures assert structured diagnostics. +- AC-2: A `harness-compare.v1` manifest parser rejects unknown fields, unsafe + paths, unsupported hosts, invalid trial counts, missing compositions, and + runtime settings that do not provide a bounded Qoder coding tool policy. +- AC-3: A comparison run copies the frozen task repository into a fresh + directory for every variant and trial, resolves each composition, invokes + the Qoder SDK, captures before/after file state, runs deterministic graders, + and emits parser-safe JSON artifacts without changing the source fixture. +- AC-4: Qoder execution can receive explicit visible tools, auto-approved tools, + disallowed tools, permission mode, model, turn limit, and a permission + callback. Its result records the exact non-secret runtime receipt, SDK event + trace, duration, turns, token usage, cost, session id, and terminal reason. +- AC-5: The README benchmark requires only `README.md` to change, parses its + structure, resolves local links, executes its JavaScript example against the + fixture package, checks documented public API and behavior against executable + package contracts, rejects stale or invented capabilities, and runs the + package's existing tests. +- AC-6: A deterministic fake-SDK end-to-end test proves the compare runner + creates and grades real files. A native Qoder smoke, when local authentication + is available, runs the same public compare command and retains its artifacts; + unavailable authentication is reported separately from product correctness. +- AC-7: Package documentation explains the assembly/evaluation boundary, safe + tool policy, CLI command, artifact layout, limitations, and validation + commands. The package build, tests, generated-source check, document-link + graph, and pack surface pass before commit. + +## Non-goals + +- Enabling Pi coding tools; the Pi v0.1 executor remains advisory and + `noTools: "all"`. +- Treating one README task as proof of general coding-agent improvement. +- Adding a new Coding Agent host adapter or changing existing support claims. +- Allowing arbitrary shell commands, network access, destructive operations, + or writes outside an isolated trial directory. +- Publishing a package, changing package versions, or editing release metadata. +- Using an LLM grader as the primary correctness decision. + +## Plan and Tasks + +1. Add a full-surface language fixture and behavior-focused compiler/resolver + coverage. +2. Extend the Qoder executor with explicit coding options, a bounded + permission callback seam, trace capture, usage metrics, and runtime receipt. +3. Add the versioned compare manifest contract, isolated trial runner, + filesystem snapshot/diff evidence, command grader, aggregation, and CLI. +4. Add the frozen README fixture and a deterministic Markdown/package grader. +5. Test manifest failure modes, source-fixture immutability, variant isolation, + real file creation, grader failures, artifacts, and safe tool decisions. +6. Document and run the focused package gates, native Qoder smoke when + available, full repository gates, link graph regeneration, and pack checks. + +The compare implementation remains under `packages/harness/src/compare/` and +uses only public compiler, resolver, and executor surfaces. It uses Node and Git +argument arrays for cross-platform execution and never invokes a shell string. + +## Test and Review Evidence + +- AC-1: `examples/full-surface.harness` compiles into schema-valid IR and + resolves the satisfied and degraded requirement paths. The package suite + asserts all eight component kinds, four strengths, permission alternatives, + degradation policies, and configuration value types. +- AC-2/AC-3/AC-5: `test/compare.test.ts` exercises manifest policy failures, + bounded tool decisions, real isolated file creation, standard Git patches, + source-fixture immutability, deterministic README grading, redacted paths, + and rejection of generated examples that request host capabilities. +- AC-4: `test/exec.test.ts` passes explicit Qoder SDK options through the + injectable SDK seam and asserts runtime receipts, duration, turns, dollar + cost, credits, usage, session/termination data, and recursive credential + redaction. +- AC-6: A native SDK run used the frozen `performance` model for one H0 and one + H1 trial. Both changed only `README.md`, both executable Quick Starts passed, + both package test runs passed 3/3, and both deterministic scores were 100. + H0 used 6 turns, 519,923 ms, and 82.219797 credits; H1 used 5 turns, + 63,633 ms, and 10.768145 credits. The result correctly remained + `need_more_work` because a one-trial tie does not establish improvement. + Evidence hashes were manifest + `a2fc04c095c71152441f2095d731d0c4662f005d1b2970871a09cc364c1e66b5`, + fixture `d70ee5ff320be019be3590e78792d262ed70e76139cf93cf88c1b048472453ba`, + and harness + `d522506d80f2bad6c6dfd4ffdcd0780193825ce461bf64c707d336940b6432df`. + The H1 native patch was then replayed through the final Node permission-model + grader and again scored 100 with its example and package tests passing. +- AC-7: `npm run check` passed 1,324 root tests, 52 package tests, generated + source verification, package build, and pack verification (525 npm entries, + 547 runtime-zip entries). The focused documentation link graph passed six + assertions. The final tarball contained 65 files (51.9 kB, shasum + `82401bc9ca61f5df0ba872b2b5d485f224d10302`), installed into a + clean temporary consumer, exposed a working `harness-compare` npm bin, and + compiled/resolved the shipped full-surface DSL plus loaded the shipped + `harness-compare.v1` manifest. +- Risk: Agent nondeterminism can create misleading one-shot wins. Preserve each + trial independently and require multiple trials before an improvement claim. +- Risk: Tool approval mistakes can escape the fixture. Resolve every path + against the trial root, allow only a narrow command vocabulary, deny network + tools, and record every permission decision. +- Risk: Authentication, model availability, SDK workers, and machine resources + can fail independently. Classify these as infrastructure failures and retain + stderr/trace evidence rather than scoring them as task failures. +- Risk: SDK messages may contain sensitive data. Persist structured protocol + events only after recursively redacting token/key/authorization fields; never + store injected credentials in manifests, receipts, traces, or fixtures. diff --git a/packages/harness/README.md b/packages/harness/README.md index 9596669..54f8295 100644 --- a/packages/harness/README.md +++ b/packages/harness/README.md @@ -76,6 +76,45 @@ const html = await highlightHarness(source); // Shiki, lang: "harness" See [`examples/standard-coding.harness`](examples/standard-coding.harness) for the full surface syntax. +[`examples/full-surface.harness`](examples/full-surface.harness) is the compiler +conformance fixture: it deliberately exercises every v0.1 component kind, +permission, strength, degradation policy, and configuration value type. + +## Compare real coding outcomes + +The DSL describes an assembled agent harness; it does not make a successful SDK +response equivalent to a successful coding task. The companion +`harness-compare.v1` manifest freezes the task repository, two composition ids, +runtime policy, trial count, and deterministic grader. Each variant/trial runs +in a separate temporary Git repository and retains the resulting patch, +redacted SDK trace, runtime receipt, permission decisions, validation details, +metrics, and aggregate verdict. + +The included benchmark asks both compositions to create a repository-grounded +`README.md` and validates the changed-file scope, Markdown structure, local +links, package exports, documented behavior, executable ESM Quick Start, stale +or invented claims, and the package's existing tests: + +```sh +npm run harness:build +node packages/harness/dist/compare/cli.js run packages/harness/examples/readme-compare/experiment.json --out ./harness-readme-compare-evidence --trials 1 +``` + +The command uses the official Qoder Agent SDK and the locally signed-in +`qodercli` identity. `--trials` may reduce the frozen maximum for a development +smoke; use all five trials before treating the result as comparative evidence. +The README task is one benchmark, not proof of general coding performance. + +Coding trials expose only `Read`, `Glob`, `Grep`, `Edit`, `Write`, and `Bash`. +No tool is auto-approved: a permission callback confines file operations to the +isolated trial, restricts writes to the manifest's expected files, and permits +only a small validation-command allowlist. Web tools, +network commands, command chaining, repository escapes, and unknown tools fail +closed. Credentials are supplied by the SDK authentication adapter and are not +written to manifests, receipts, traces, or fixtures. Generated Quick Start code +is screened for host capabilities, receives a secret-free environment, and runs +in a separate Node permission-model process with read-only fixture access. + ## AI authoring skill The package includes diff --git a/packages/harness/examples/full-surface.harness b/packages/harness/examples/full-surface.harness new file mode 100644 index 0000000..14c6a03 --- /dev/null +++ b/packages/harness/examples/full-surface.harness @@ -0,0 +1,93 @@ +// Compiler conformance fixture: every v0.1 authored syntax branch appears here. + +component repository-skill { + kind skill + description "Read repository evidence before changing files." + input { repository task } + output { evidence } + permissions { workspace read workspace write } +} + +component file-tool { + kind tool + permissions { process allow } +} + +component test-program { + kind program + permissions { process deny } +} + +component coding-workflow { + kind workflow + permissions { network allow } +} + +component completion-hook { + kind hook + permissions { network deny } +} + +component evidence-policy { + kind policy + permissions { model allow } +} + +component trace-observer { + kind observer + permissions { model deny } +} + +component result-ui { + kind ui + permissions { workspace allow } +} + +binding repository-skill for qoder { + mechanism prompt-preamble + strength advisory + notes "Prompt guidance is the v0.1 materialization boundary." +} +binding file-tool for qoder { mechanism unavailable strength unsupported } +binding test-program for qoder { mechanism sdk-tool strength wired } +binding coding-workflow for qoder { mechanism native-workflow strength enforced } +binding completion-hook for qoder { mechanism prompt-preamble strength advisory } +binding evidence-policy for qoder { mechanism sdk-policy strength wired } +binding trace-observer for qoder { mechanism unavailable strength unsupported } +binding result-ui for qoder { mechanism native-view strength enforced } + +plugin full-surface { + version "1.2.3" + provides [ + component.repository-skill, + component.file-tool, + component.test-program, + component.coding-workflow, + component.completion-hook, + component.evidence-policy, + component.trace-observer, + component.result-ui + ] +} + +composition full-surface-qoder { + target qoder + include [ plugin.full-surface@^1.0.0 ] + require repository-skill { + preferred advisory + minimum advisory + on-degrade fail + } + require test-program { + preferred wired + minimum advisory + on-degrade report + } + configure { + runtime.max-turns = 24 + runtime.timeout = 10m + runtime.label = "full-surface" + runtime.checkpoints = true + runtime.network-enabled = false + } +} diff --git a/packages/harness/examples/readme-compare/experiment.json b/packages/harness/examples/readme-compare/experiment.json new file mode 100644 index 0000000..6995fa6 --- /dev/null +++ b/packages/harness/examples/readme-compare/experiment.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": "harness-compare.v1", + "harness": "./readme-compare.harness", + "variants": { + "baseline": "readme-baseline", + "candidate": "readme-grounded" + }, + "task": { + "fixture": "./fixture", + "prompt": "./prompt.md", + "expectedFiles": ["README.md"], + "grader": { + "kind": "readme-package-v1", + "contract": "./grader-contract.json" + } + }, + "runtime": { + "host": "qoder", + "model": "performance", + "tools": ["Read", "Glob", "Grep", "Edit", "Write", "Bash"], + "allowedTools": [], + "disallowedTools": ["WebFetch", "WebSearch", "Task"], + "permissionMode": "default", + "maxTurns": 24, + "timeoutMs": 600000, + "network": "deny", + "enableFileCheckpointing": true + }, + "trials": { + "count": 5, + "seed": 20260815, + "order": "randomized" + } +} diff --git a/packages/harness/examples/readme-compare/fixture/docs/archive-v0.md b/packages/harness/examples/readme-compare/fixture/docs/archive-v0.md new file mode 100644 index 0000000..181d3cb --- /dev/null +++ b/packages/harness/examples/readme-compare/fixture/docs/archive-v0.md @@ -0,0 +1,5 @@ +# Archived v0 notes + +This document is intentionally stale and must not be used as current package +evidence. The removed `retry-kit` CLI used five attempts by default and exposed +a CommonJS `require()` entrypoint. Neither capability exists in version 1.0. diff --git a/packages/harness/examples/readme-compare/fixture/examples/basic.mjs b/packages/harness/examples/readme-compare/fixture/examples/basic.mjs new file mode 100644 index 0000000..aec43f7 --- /dev/null +++ b/packages/harness/examples/readme-compare/fixture/examples/basic.mjs @@ -0,0 +1,10 @@ +import { retry } from "@fixture/retry-kit"; + +let attempts = 0; +const value = await retry(async () => { + attempts += 1; + if (attempts < 2) throw new Error("transient"); + return "ready"; +}); + +console.log(value); diff --git a/packages/harness/examples/readme-compare/fixture/package.json b/packages/harness/examples/readme-compare/fixture/package.json new file mode 100644 index 0000000..0b9be1d --- /dev/null +++ b/packages/harness/examples/readme-compare/fixture/package.json @@ -0,0 +1,12 @@ +{ + "name": "@fixture/retry-kit", + "version": "1.0.0", + "description": "Small ESM retry helper with abort support.", + "type": "module", + "engines": { "node": ">=20" }, + "exports": "./src/index.mjs", + "scripts": { + "test": "node --test", + "example": "node examples/basic.mjs" + } +} diff --git a/packages/harness/examples/readme-compare/fixture/src/index.mjs b/packages/harness/examples/readme-compare/fixture/src/index.mjs new file mode 100644 index 0000000..c23dc74 --- /dev/null +++ b/packages/harness/examples/readme-compare/fixture/src/index.mjs @@ -0,0 +1 @@ +export { DEFAULT_MAX_ATTEMPTS, RetryExhaustedError, retry } from "./retry.mjs"; diff --git a/packages/harness/examples/readme-compare/fixture/src/retry.mjs b/packages/harness/examples/readme-compare/fixture/src/retry.mjs new file mode 100644 index 0000000..4d9620e --- /dev/null +++ b/packages/harness/examples/readme-compare/fixture/src/retry.mjs @@ -0,0 +1,39 @@ +export const DEFAULT_MAX_ATTEMPTS = 3; + +export class RetryExhaustedError extends Error { + constructor(attempts, cause) { + super(`Operation failed after ${attempts} attempts`, { cause }); + this.name = "RetryExhaustedError"; + this.attempts = attempts; + } +} + +export async function retry(operation, options = {}) { + const { + maxAttempts = DEFAULT_MAX_ATTEMPTS, + backoffMs = 0, + signal, + } = options; + if (!Number.isInteger(maxAttempts) || maxAttempts < 1) { + throw new TypeError("maxAttempts must be a positive integer"); + } + let lastError; + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + signal?.throwIfAborted(); + try { + return await operation(attempt); + } catch (error) { + lastError = error; + if (attempt < maxAttempts && backoffMs > 0) { + await new Promise((resolve, reject) => { + const timer = setTimeout(resolve, backoffMs); + signal?.addEventListener("abort", () => { + clearTimeout(timer); + reject(signal.reason); + }, { once: true }); + }); + } + } + } + throw new RetryExhaustedError(maxAttempts, lastError); +} diff --git a/packages/harness/examples/readme-compare/fixture/test/retry.test.mjs b/packages/harness/examples/readme-compare/fixture/test/retry.test.mjs new file mode 100644 index 0000000..ab58225 --- /dev/null +++ b/packages/harness/examples/readme-compare/fixture/test/retry.test.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + DEFAULT_MAX_ATTEMPTS, + RetryExhaustedError, + retry, +} from "@fixture/retry-kit"; + +test("retries three times by default and reports exhaustion", async () => { + let attempts = 0; + await assert.rejects( + retry(async () => { + attempts += 1; + throw new Error("still failing"); + }), + (error) => error instanceof RetryExhaustedError && error.attempts === DEFAULT_MAX_ATTEMPTS, + ); + assert.equal(attempts, 3); +}); + +test("returns the first successful value", async () => { + const value = await retry(async (attempt) => { + if (attempt < 2) throw new Error("transient"); + return "ready"; + }); + assert.equal(value, "ready"); +}); + +test("honors an already-aborted signal before invoking the operation", async () => { + const controller = new AbortController(); + controller.abort(new Error("cancelled")); + let called = false; + await assert.rejects(retry(async () => { called = true; }, { signal: controller.signal }), /cancelled/); + assert.equal(called, false); +}); diff --git a/packages/harness/examples/readme-compare/grader-contract.json b/packages/harness/examples/readme-compare/grader-contract.json new file mode 100644 index 0000000..b4a22db --- /dev/null +++ b/packages/harness/examples/readme-compare/grader-contract.json @@ -0,0 +1,9 @@ +{ + "requiredHeadings": ["Purpose", "Installation", "Quick Start", "API", "Behavior", "Verification"], + "publicApi": ["retry", "RetryExhaustedError", "DEFAULT_MAX_ATTEMPTS"], + "requiredCodeTokens": ["@fixture/retry-kit", "maxAttempts", "3", "AbortSignal"], + "requiredAnyCodeTokens": [["npm test", "node --test"]], + "forbiddenClaims": ["supports CommonJS", "CommonJS support", "require(\"@fixture/retry-kit\")", "command-line interface", "retry-kit CLI", "npm install -g", "https://retry-kit.example"], + "exampleLanguages": ["js", "javascript", "mjs"], + "forbidRemoteLinks": true +} diff --git a/packages/harness/examples/readme-compare/prompt.md b/packages/harness/examples/readme-compare/prompt.md new file mode 100644 index 0000000..544254e --- /dev/null +++ b/packages/harness/examples/readme-compare/prompt.md @@ -0,0 +1,17 @@ +Create `README.md` for this package and change no other repository file. + +Ground every claim in the current `package.json`, public source, tests, and +working example. Include Purpose, Installation, Quick Start, API, Behavior, and +Verification sections. The Quick Start must be an executable ESM JavaScript +example. The Installation section must show the consumer command +`npm install @fixture/retry-kit`. Document retry defaults and abort behavior +precisely. + +Do not copy claims from explicitly archived documentation. Do not invent a CLI, +CommonJS support, badges, remote URLs, network behavior, or unimplemented API. +Run the repository's allowed validation before completing. + +The bounded Bash policy accepts these validation commands exactly: `npm test`, +`npm run example`, `node --test`, `node examples/basic.mjs`, +`node ./examples/basic.mjs`, `git status --short`, and `git diff --check`. +Use the dedicated Read, Glob, and Grep tools for repository inspection. diff --git a/packages/harness/examples/readme-compare/readme-compare.harness b/packages/harness/examples/readme-compare/readme-compare.harness new file mode 100644 index 0000000..57e825f --- /dev/null +++ b/packages/harness/examples/readme-compare/readme-compare.harness @@ -0,0 +1,65 @@ +component coding-loop { + kind workflow + description "Inspect the repository, make the requested scoped change, run relevant validation, and report concrete evidence." + input { task repository } + output { patch validation } + permissions { workspace read workspace write process allow network deny } +} + +component repository-grounding { + kind skill + description "Before writing documentation, derive claims from package metadata, public source, executable examples, and current tests; treat archived documentation as stale evidence." + input { package-metadata public-source tests examples } + output { fact-map } + permissions { workspace read network deny } +} + +component readme-only-scope { + kind policy + description "Change only README.md. Do not invent commands, module formats, badges, URLs, defaults, or capabilities that current repository evidence does not support." + input { fact-map } + output { scoped-readme } + permissions { workspace write network deny } +} + +component executable-readme-verification { + kind workflow + description "Run the package tests and execute the README quick-start example before completing; repair documentation until both pass." + input { scoped-readme } + output { verification } + permissions { process allow workspace read network deny } +} + +binding coding-loop for qoder { mechanism prompt-preamble strength advisory } +binding repository-grounding for qoder { mechanism prompt-preamble strength advisory } +binding readme-only-scope for qoder { mechanism prompt-preamble strength advisory } +binding executable-readme-verification for qoder { mechanism prompt-preamble strength advisory } + +plugin coding-core { + version "1.0.0" + provides [ component.coding-loop ] +} + +plugin grounded-readme { + version "1.0.0" + provides [ + component.repository-grounding, + component.readme-only-scope, + component.executable-readme-verification + ] +} + +composition readme-baseline { + target qoder + include [ plugin.coding-core@1 ] + require coding-loop { minimum advisory } +} + +composition readme-grounded { + target qoder + include [ plugin.coding-core@1, plugin.grounded-readme@1 ] + require coding-loop { minimum advisory } + require repository-grounding { minimum advisory } + require readme-only-scope { minimum advisory } + require executable-readme-verification { minimum advisory } +} diff --git a/packages/harness/package.json b/packages/harness/package.json index 6354efb..4b8076b 100644 --- a/packages/harness/package.json +++ b/packages/harness/package.json @@ -16,6 +16,9 @@ "homepage": "https://github.com/QoderAI/better-harness/tree/main/packages/harness#readme", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "bin": { + "harness-compare": "./dist/compare/cli.js" + }, "exports": { ".": { "types": "./dist/index.d.ts", @@ -29,6 +32,10 @@ "types": "./dist/exec/index.d.ts", "import": "./dist/exec/index.js" }, + "./compare": { + "types": "./dist/compare/index.d.ts", + "import": "./dist/compare/index.js" + }, "./highlight": { "types": "./dist/highlight/shiki.d.ts", "import": "./dist/highlight/shiki.js" diff --git a/packages/harness/src/compare/cli.ts b/packages/harness/src/compare/cli.ts new file mode 100644 index 0000000..8026aa0 --- /dev/null +++ b/packages/harness/src/compare/cli.ts @@ -0,0 +1,66 @@ +#!/usr/bin/env node +import { realpathSync } from "node:fs"; +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { runHarnessComparison } from "./runner.js"; + +export async function main(argv: string[]): Promise { + if (argv.includes("--help") || argv.includes("-h")) { + process.stdout.write(help()); + return 0; + } + const [command, manifestPath, ...rest] = argv; + if (command !== "run" || !manifestPath) { + process.stderr.write("Expected: harness-compare run --out \n"); + return 2; + } + let outputDirectory: string | undefined; + let trialCount: number | undefined; + let json = false; + for (let index = 0; index < rest.length; index += 1) { + const flag = rest[index]; + if (flag === "--out") outputDirectory = rest[++index]; + else if (flag === "--trials") trialCount = Number(rest[++index]); + else if (flag === "--json") json = true; + else { + process.stderr.write(`Unknown option: ${flag}\n`); + return 2; + } + } + if (!outputDirectory) { + process.stderr.write("--out is required so evidence has an explicit owner.\n"); + return 2; + } + try { + const verdict = await runHarnessComparison({ + manifestPath, + outputDirectory, + ...(trialCount !== undefined ? { trialCount } : {}), + }); + if (json) process.stdout.write(`${JSON.stringify(verdict)}\n`); + else { + process.stdout.write( + `${verdict.status}: ${verdict.reason}\n` + + `H0 ${verdict.baseline.passedTrials}/${verdict.baseline.completedTrials}, score ${verdict.baseline.meanScore}\n` + + `H1 ${verdict.candidate.passedTrials}/${verdict.candidate.completedTrials}, score ${verdict.candidate.meanScore}\n` + + `Evidence: ${resolve(outputDirectory)}\n`, + ); + } + return verdict.status === "reject" || verdict.status === "infrastructure_error" ? 1 : 0; + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + return 1; + } +} + +function help(): string { + return `Harness composition comparison\n\n` + + `Usage:\n harness-compare run --out [--trials ] [--json]\n\n` + + `The command creates an isolated repository per composition/trial, invokes Qoder through\n` + + `the official SDK, grades the resulting files, and writes immutable comparison evidence.\n`; +} + +const invokedPath = process.argv[1] ? pathToFileURL(realpathSync(resolve(process.argv[1]))).href : ""; +if (import.meta.url === invokedPath) { + process.exitCode = await main(process.argv.slice(2)); +} diff --git a/packages/harness/src/compare/grader.ts b/packages/harness/src/compare/grader.ts new file mode 100644 index 0000000..aee4722 --- /dev/null +++ b/packages/harness/src/compare/grader.ts @@ -0,0 +1,330 @@ +import { access, readFile, realpath, unlink, writeFile } from "node:fs/promises"; +import { dirname, isAbsolute, relative, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { Type, type Static } from "@sinclair/typebox"; +import { Value } from "@sinclair/typebox/value"; +import { runCommand, type CommandResult } from "./process.js"; + +const ReadmeGraderContractSchema = Type.Object( + { + requiredHeadings: Type.Array(Type.String(), { minItems: 1, uniqueItems: true }), + publicApi: Type.Array(Type.String(), { minItems: 1, uniqueItems: true }), + requiredCodeTokens: Type.Array(Type.String(), { minItems: 1, uniqueItems: true }), + requiredAnyCodeTokens: Type.Array( + Type.Array(Type.String(), { minItems: 1, uniqueItems: true }), + { uniqueItems: true }, + ), + forbiddenClaims: Type.Array(Type.String(), { uniqueItems: true }), + exampleLanguages: Type.Array(Type.String(), { minItems: 1, uniqueItems: true }), + forbidRemoteLinks: Type.Boolean(), + }, + { additionalProperties: false }, +); + +type ReadmeGraderContract = Static; + +export interface GraderCheck { + id: string; + passed: boolean; + hard: boolean; + weight: number; + detail: string; + command?: CommandResult; +} + +export interface ReadmeGrade { + kind: "readme-package-v1"; + passed: boolean; + score: number; + checks: GraderCheck[]; +} + +interface MarkdownDocument { + headings: string[]; + codeSpans: string[]; + codeBlocks: Array<{ language: string; section?: string; code: string }>; + links: string[]; +} + +export async function gradeReadmePackage(options: { + trialRoot: string; + contractPath: string; + changedFiles: string[]; + expectedFiles: string[]; +}): Promise { + const contract = await loadContract(options.contractPath); + const readmePath = resolve(options.trialRoot, "README.md"); + const checks: GraderCheck[] = []; + const changed = [...options.changedFiles].sort(); + const expected = [...options.expectedFiles].sort(); + checks.push(check( + "scope", + arraysEqual(changed, expected), + 15, + `changed=${JSON.stringify(changed)} expected=${JSON.stringify(expected)}`, + )); + + let source = ""; + try { + source = await readFile(readmePath, "utf8"); + } catch { + checks.push(check("readme-exists", false, 85, "README.md was not created.")); + return summarize(checks); + } + const document = parseMarkdown(source); + const normalizedHeadings = new Set(document.headings.map(normalizeText)); + const missingHeadings = contract.requiredHeadings.filter( + (heading) => !normalizedHeadings.has(normalizeText(heading)), + ); + checks.push(check( + "structure", + missingHeadings.length === 0, + 10, + missingHeadings.length === 0 ? "All required sections are present." : `Missing headings: ${missingHeadings.join(", ")}`, + )); + + const linkFailures = await validateLinks(document.links, readmePath, options.trialRoot, contract.forbidRemoteLinks); + checks.push(check( + "links", + linkFailures.length === 0, + 5, + linkFailures.length === 0 ? "All local links resolve and no remote links are claimed." : linkFailures.join("; "), + )); + + const allCode = [...document.codeSpans, ...document.codeBlocks.map((block) => block.code)].join("\n"); + const packageJson = JSON.parse(await readFile(resolve(options.trialRoot, "package.json"), "utf8")) as { + name?: string; + }; + const installNeedle = `npm install ${packageJson.name ?? ""}`; + checks.push(check( + "installation", + Boolean(packageJson.name) && allCode.includes(installNeedle), + 10, + `Expected an installation command containing '${installNeedle}'.`, + )); + + const moduleUrl = `${pathToFileURL(resolve(options.trialRoot, "src/index.mjs")).href}?grade=${Date.now()}`; + const publicModule = await import(moduleUrl) as Record; + const missingExports = contract.publicApi.filter((name) => !(name in publicModule)); + const undocumentedExports = contract.publicApi.filter((name) => !allCode.includes(name)); + checks.push(check( + "public-api", + missingExports.length === 0 && undocumentedExports.length === 0, + 15, + `missing exports=${JSON.stringify(missingExports)} undocumented=${JSON.stringify(undocumentedExports)}`, + )); + + const missingTokens = contract.requiredCodeTokens.filter((token) => !allCode.includes(token)); + const missingAlternatives = contract.requiredAnyCodeTokens.filter( + (alternatives) => !alternatives.some((token) => allCode.includes(token)), + ); + checks.push(check( + "behavior-evidence", + missingTokens.length === 0 && missingAlternatives.length === 0, + 15, + missingTokens.length === 0 && missingAlternatives.length === 0 + ? "Required behavior tokens are documented." + : `Missing code evidence: ${[ + ...missingTokens, + ...missingAlternatives.map((alternatives) => `one of (${alternatives.join(" | ")})`), + ].join(", ")}`, + )); + + const quickStartBlocks = document.codeBlocks.filter( + (block) => normalizeText(block.section ?? "") === "quick start" && contract.exampleLanguages.includes(block.language), + ); + const exampleResults: CommandResult[] = []; + const realTrialRoot = await realpath(options.trialRoot); + for (let index = 0; index < quickStartBlocks.length; index += 1) { + const temporaryExample = resolve(options.trialRoot, `.harness-readme-example-${index}.mjs`); + try { + const safetyError = validateExampleSafety(quickStartBlocks[index].code, packageJson.name ?? ""); + if (safetyError) { + exampleResults.push({ + command: ["node", ""], + exitCode: 1, + stdout: "", + stderr: safetyError, + timedOut: false, + durationMs: 0, + }); + } else { + await writeFile(temporaryExample, quickStartBlocks[index].code, "utf8"); + const realTemporaryExample = await realpath(temporaryExample); + exampleResults.push(await runCommand(process.execPath, [ + "--permission", + `--allow-fs-read=${realTrialRoot}`, + realTemporaryExample, + ], { + cwd: options.trialRoot, + timeoutMs: 30_000, + env: safeExampleEnvironment(), + })); + } + } finally { + await unlink(temporaryExample).catch(() => undefined); + } + } + const examplesPass = quickStartBlocks.length > 0 && exampleResults.every((result) => result.exitCode === 0 && !result.timedOut); + checks.push({ + ...check( + "quick-start", + examplesPass, + 15, + quickStartBlocks.length === 0 + ? "Quick Start has no executable ESM JavaScript fence." + : `Executed ${quickStartBlocks.length} Quick Start example(s).`, + ), + ...(exampleResults[0] ? { command: redactCommandResult(exampleResults[0], options.trialRoot, realTrialRoot) } : {}), + }); + + const testResult = await runCommand("npm", ["test"], { cwd: options.trialRoot, timeoutMs: 60_000 }); + checks.push({ + ...check("package-tests", testResult.exitCode === 0 && !testResult.timedOut, 10, "Ran the fixture's existing test contract."), + command: redactCommandResult(testResult, options.trialRoot), + }); + + const lowered = source.toLocaleLowerCase("en-US"); + const forbidden = contract.forbiddenClaims.filter((claim) => lowered.includes(claim.toLocaleLowerCase("en-US"))); + checks.push(check( + "no-invented-capabilities", + forbidden.length === 0, + 5, + forbidden.length === 0 ? "No frozen forbidden claim was found." : `Forbidden claims: ${forbidden.join(", ")}`, + )); + return summarize(checks); +} + +async function loadContract(path: string): Promise { + const value = JSON.parse(await readFile(path, "utf8")) as unknown; + if (!Value.Check(ReadmeGraderContractSchema, value)) { + const detail = [...Value.Errors(ReadmeGraderContractSchema, value)] + .map((error) => `${error.path || "/"}: ${error.message}`) + .join("; "); + throw new Error(`Invalid readme-package-v1 grader contract: ${detail}`); + } + return value; +} + +function parseMarkdown(source: string): MarkdownDocument { + const headings: string[] = []; + const codeSpans: string[] = []; + const codeBlocks: MarkdownDocument["codeBlocks"] = []; + const links: string[] = []; + let currentSection: string | undefined; + let fence: { marker: string; language: string; section?: string; lines: string[] } | undefined; + for (const line of source.split(/\r?\n/)) { + const fenceMatch = line.match(/^ {0,3}(```+|~~~+)\s*([\w+-]*)\s*$/); + if (fenceMatch) { + if (!fence) { + fence = { marker: fenceMatch[1][0], language: fenceMatch[2].toLowerCase(), section: currentSection, lines: [] }; + } else if (fence.marker === fenceMatch[1][0]) { + codeBlocks.push({ language: fence.language, section: fence.section, code: fence.lines.join("\n") }); + fence = undefined; + } else { + fence.lines.push(line); + } + continue; + } + if (fence) { + fence.lines.push(line); + continue; + } + const heading = line.match(/^ {0,3}#{1,6}\s+(.+?)\s*#*\s*$/); + if (heading) { + currentSection = stripInlineMarkup(heading[1]); + headings.push(currentSection); + } + for (const match of line.matchAll(/`([^`\n]+)`/g)) codeSpans.push(match[1]); + for (const match of line.matchAll(/!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g)) links.push(match[1]); + } + return { headings, codeSpans, codeBlocks, links }; +} + +async function validateLinks( + links: string[], + readmePath: string, + trialRoot: string, + forbidRemote: boolean, +): Promise { + const failures: string[] = []; + for (const link of links) { + if (/^(?:https?:)?\/\//i.test(link)) { + if (forbidRemote) failures.push(`Remote link is not allowed: ${link}`); + continue; + } + if (/^(?:mailto|data):/i.test(link) || link.startsWith("#")) continue; + const pathPart = decodeURIComponent(link.split("#", 1)[0]); + if (!pathPart) continue; + const target = resolve(dirname(readmePath), pathPart); + const fromRoot = relative(trialRoot, target); + if (isAbsolute(fromRoot) || fromRoot.startsWith("..")) { + failures.push(`Link escapes the repository: ${link}`); + continue; + } + await access(target).catch(() => failures.push(`Missing local link target: ${link}`)); + } + return failures; +} + +function check(id: string, passed: boolean, weight: number, detail: string): GraderCheck { + return { id, passed, hard: true, weight, detail }; +} + +function summarize(checks: GraderCheck[]): ReadmeGrade { + const total = checks.reduce((sum, item) => sum + item.weight, 0); + const earned = checks.reduce((sum, item) => sum + (item.passed ? item.weight : 0), 0); + return { + kind: "readme-package-v1", + passed: checks.every((item) => !item.hard || item.passed), + score: total === 0 ? 0 : Math.round((earned / total) * 100), + checks, + }; +} + +function redactCommandResult(result: CommandResult, root: string, realRoot = root): CommandResult { + const replaceRoot = (value: string): string => + value.replaceAll(realRoot, "").replaceAll(root, ""); + return { + ...result, + command: result.command.map((value, index) => index === 0 && value === process.execPath ? "node" : replaceRoot(value)), + stdout: replaceRoot(result.stdout), + stderr: replaceRoot(result.stderr), + }; +} + +function stripInlineMarkup(value: string): string { + return value.replace(/[*_`]/g, "").trim(); +} + +function normalizeText(value: string): string { + return stripInlineMarkup(value).toLocaleLowerCase("en-US"); +} + +function arraysEqual(a: string[], b: string[]): boolean { + return a.length === b.length && a.every((value, index) => value === b[index]); +} + +const UNSAFE_EXAMPLE_TOKEN = /\b(?:process|globalThis|fetch|WebSocket|require|eval|Function|child_process|worker_threads|Deno|Bun)\b|\bimport\s*\(|(?:node|https?|data|file):|__proto__|\.constructor\b/; + +function validateExampleSafety(source: string, packageName: string): string | undefined { + if (UNSAFE_EXAMPLE_TOKEN.test(source)) { + return "Quick Start uses a capability outside the isolated example policy."; + } + for (const match of source.matchAll(/\b(?:import|export)\b[^;\n]*?\bfrom\s*["']([^"']+)["']/g)) { + if (match[1] !== packageName) { + return `Quick Start imports '${match[1]}', but only '${packageName}' is allowed.`; + } + } + if (/\bimport\s*["']/.test(source)) { + return "Quick Start side-effect imports are not allowed."; + } + return undefined; +} + +function safeExampleEnvironment(): NodeJS.ProcessEnv { + const keys = ["PATH", "SystemRoot", "ComSpec", "PATHEXT", "TEMP", "TMP", "TMPDIR"]; + return Object.fromEntries( + keys.flatMap((key) => process.env[key] === undefined ? [] : [[key, process.env[key]]]), + ); +} diff --git a/packages/harness/src/compare/index.ts b/packages/harness/src/compare/index.ts new file mode 100644 index 0000000..4371825 --- /dev/null +++ b/packages/harness/src/compare/index.ts @@ -0,0 +1,22 @@ +export { + HarnessCompareManifestSchema, + loadHarnessCompareManifest, + type HarnessCompareManifest, + type LoadedHarnessCompareManifest, +} from "./manifest.js"; +export { gradeReadmePackage, type GraderCheck, type ReadmeGrade } from "./grader.js"; +export { + createBoundedQoderPermissionCallback, + type ToolPermissionDecision, +} from "./permissions.js"; +export { + runHarnessComparison, + type CompareExecutorContext, + type CompareExecutorFactory, + type CompareTrialResult, + type CompareVariant, + type FileEvidence, + type HarnessCompareVerdict, + type TrialClassification, + type VariantAggregate, +} from "./runner.js"; diff --git a/packages/harness/src/compare/manifest.ts b/packages/harness/src/compare/manifest.ts new file mode 100644 index 0000000..7ffafd6 --- /dev/null +++ b/packages/harness/src/compare/manifest.ts @@ -0,0 +1,165 @@ +import { readFile } from "node:fs/promises"; +import { isAbsolute, resolve, sep } from "node:path"; +import { Type, type Static } from "@sinclair/typebox"; +import { Value } from "@sinclair/typebox/value"; + +const RelativePathSchema = Type.String({ minLength: 1, maxLength: 512 }); +const IdentifierSchema = Type.String({ pattern: "^[_a-zA-Z][\\w-]*$" }); + +export const HarnessCompareManifestSchema = Type.Object( + { + schemaVersion: Type.Literal("harness-compare.v1"), + harness: RelativePathSchema, + variants: Type.Object( + { baseline: IdentifierSchema, candidate: IdentifierSchema }, + { additionalProperties: false }, + ), + task: Type.Object( + { + fixture: RelativePathSchema, + prompt: RelativePathSchema, + expectedFiles: Type.Array(RelativePathSchema, { minItems: 1, maxItems: 32, uniqueItems: true }), + grader: Type.Object( + { kind: Type.Literal("readme-package-v1"), contract: RelativePathSchema }, + { additionalProperties: false }, + ), + }, + { additionalProperties: false }, + ), + runtime: Type.Object( + { + host: Type.Literal("qoder"), + model: Type.String({ minLength: 1, maxLength: 128 }), + tools: Type.Array(Type.String(), { minItems: 1, maxItems: 32, uniqueItems: true }), + allowedTools: Type.Array(Type.String(), { maxItems: 32, uniqueItems: true }), + disallowedTools: Type.Array(Type.String(), { maxItems: 32, uniqueItems: true }), + permissionMode: Type.Literal("default"), + maxTurns: Type.Integer({ minimum: 2, maximum: 50 }), + timeoutMs: Type.Integer({ minimum: 1_000, maximum: 1_800_000 }), + network: Type.Literal("deny"), + enableFileCheckpointing: Type.Boolean(), + }, + { additionalProperties: false }, + ), + trials: Type.Object( + { + count: Type.Integer({ minimum: 1, maximum: 20 }), + seed: Type.Integer({ minimum: 0, maximum: 4_294_967_295 }), + order: Type.Literal("randomized"), + }, + { additionalProperties: false }, + ), + }, + { additionalProperties: false }, +); + +export type HarnessCompareManifest = Static; + +export interface LoadedHarnessCompareManifest { + path: string; + directory: string; + value: HarnessCompareManifest; + resolved: { + harness: string; + fixture: string; + prompt: string; + graderContract: string; + }; +} + +export async function loadHarnessCompareManifest(path: string): Promise { + const manifestPath = resolve(path); + let value: unknown; + try { + value = JSON.parse(await readFile(manifestPath, "utf8")); + } catch (error) { + throw new Error(`Cannot read harness compare manifest '${path}': ${errorMessage(error)}`); + } + if (!Value.Check(HarnessCompareManifestSchema, value)) { + const details = [...Value.Errors(HarnessCompareManifestSchema, value)] + .slice(0, 8) + .map((error) => `${error.path || "/"}: ${error.message}`) + .join("; "); + throw new Error(`Invalid harness-compare.v1 manifest: ${details}`); + } + validateManifestPolicy(value); + const directory = resolve(manifestPath, ".."); + return { + path: manifestPath, + directory, + value, + resolved: { + harness: resolveOwnedPath(directory, value.harness, "harness"), + fixture: resolveOwnedPath(directory, value.task.fixture, "task.fixture"), + prompt: resolveOwnedPath(directory, value.task.prompt, "task.prompt"), + graderContract: resolveOwnedPath(directory, value.task.grader.contract, "task.grader.contract"), + }, + }; +} + +function validateManifestPolicy(manifest: HarnessCompareManifest): void { + if (manifest.variants.baseline === manifest.variants.candidate) { + throw new Error("Invalid harness-compare.v1 manifest: baseline and candidate must differ."); + } + const requiredTools = ["Read", "Edit", "Write", "Bash"]; + const supportedTools = new Set(["Read", "Glob", "Grep", "Edit", "Write", "Bash"]); + const unsupportedTools = manifest.runtime.tools.filter((tool) => !supportedTools.has(tool)); + if (unsupportedTools.length > 0) { + throw new Error( + `Invalid harness-compare.v1 manifest: unsupported visible tools: ${unsupportedTools.join(", ")}.`, + ); + } + for (const tool of requiredTools) { + if (!manifest.runtime.tools.includes(tool)) { + throw new Error(`Invalid harness-compare.v1 manifest: runtime.tools must include '${tool}'.`); + } + } + if (manifest.runtime.allowedTools.length > 0) { + throw new Error("Invalid harness-compare.v1 manifest: allowedTools must be empty; every tool use requires the bounded permission callback."); + } + for (const tool of ["WebFetch", "WebSearch"]) { + if (!manifest.runtime.disallowedTools.includes(tool)) { + throw new Error(`Invalid harness-compare.v1 manifest: disallowedTools must include '${tool}'.`); + } + } + const unavailableRequiredTools = requiredTools.filter((tool) => manifest.runtime.disallowedTools.includes(tool)); + if (unavailableRequiredTools.length > 0) { + throw new Error( + `Invalid harness-compare.v1 manifest: required tools are also disallowed: ${unavailableRequiredTools.join(", ")}.`, + ); + } + for (const path of [ + manifest.harness, + manifest.task.fixture, + manifest.task.prompt, + manifest.task.grader.contract, + ...manifest.task.expectedFiles, + ]) { + assertPortableRelativePath(path); + } +} + +function resolveOwnedPath(base: string, relativePath: string, label: string): string { + assertPortableRelativePath(relativePath); + const result = resolve(base, relativePath); + const prefix = base.endsWith(sep) ? base : `${base}${sep}`; + if (result !== base && !result.startsWith(prefix)) { + throw new Error(`Invalid harness-compare.v1 manifest: ${label} escapes the manifest directory.`); + } + return result; +} + +function assertPortableRelativePath(value: string): void { + if ( + isAbsolute(value) || + /^[A-Za-z]:[\\/]/.test(value) || + value.includes("\\") || + value.split("/").some((segment) => segment === ".." || segment === "") + ) { + throw new Error(`Invalid harness-compare.v1 manifest path '${value}'; use a portable relative path.`); + } +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/packages/harness/src/compare/permissions.ts b/packages/harness/src/compare/permissions.ts new file mode 100644 index 0000000..ef65f40 --- /dev/null +++ b/packages/harness/src/compare/permissions.ts @@ -0,0 +1,96 @@ +import { isAbsolute, relative, resolve } from "node:path"; +import type { + QoderToolPermissionCallback, + QoderToolPermissionResult, +} from "../exec/qoder-sdk.js"; + +export interface ToolPermissionDecision { + toolName: string; + behavior: "allow" | "deny"; + reason: string; +} + +const FILE_TOOLS = new Set(["Read", "Edit", "Write", "Glob", "Grep"]); +const MUTATING_FILE_TOOLS = new Set(["Edit", "Write"]); +const SAFE_COMMANDS = new Set([ + "npm test", + "npm run test", + "npm run build", + "npm run example", + "node --test", + "node --version", + "node examples/basic.mjs", + "node ./examples/basic.mjs", + "git diff --check", + "git status --short", + "git --no-pager status --short", +]); +const SHELL_META = /[\n\r;&|`$<>]/; + +export function createBoundedQoderPermissionCallback( + trialRoot: string, + decisions: ToolPermissionDecision[], + writablePaths: string[], +): QoderToolPermissionCallback { + const root = resolve(trialRoot); + return async (toolName, input): Promise => { + const decision = decideToolUse(root, new Set(writablePaths.map((path) => portable(path))), toolName, input); + decisions.push({ toolName, behavior: decision.behavior, reason: decision.reason }); + return decision.behavior === "allow" + ? { behavior: "allow" } + : { behavior: "deny", message: decision.reason }; + }; +} + +function decideToolUse( + root: string, + writablePaths: Set, + toolName: string, + input: Record, +): { behavior: "allow" | "deny"; reason: string } { + if (toolName === "Bash") { + const command = typeof input.command === "string" ? normalizeWhitespace(input.command) : ""; + if (!command || SHELL_META.test(command) || !SAFE_COMMANDS.has(command)) { + return { behavior: "deny", reason: "Command is outside the frozen validation allowlist." }; + } + return { behavior: "allow", reason: `Allowed validation command: ${command}` }; + } + if (!FILE_TOOLS.has(toolName)) { + return { behavior: "deny", reason: `Tool '${toolName}' is not part of the coding benchmark policy.` }; + } + const pathValue = firstString(input, ["file_path", "path", "directory"]); + if (pathValue === undefined) { + if (MUTATING_FILE_TOOLS.has(toolName) || toolName === "Read") { + return { behavior: "deny", reason: `Tool '${toolName}' did not provide a file path.` }; + } + return { behavior: "allow", reason: `Tool '${toolName}' defaults to the isolated trial root.` }; + } + const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(root, pathValue); + const fromRoot = relative(root, candidate); + if (fromRoot === "" || (!fromRoot.startsWith("..") && !isAbsolute(fromRoot))) { + const ownedPath = portable(fromRoot || "."); + if (ownedPath === ".git" || ownedPath.startsWith(".git/")) { + return { behavior: "deny", reason: "Git metadata is not part of the task write surface." }; + } + if (MUTATING_FILE_TOOLS.has(toolName) && !writablePaths.has(ownedPath)) { + return { behavior: "deny", reason: `Task does not authorize writing '${ownedPath}'.` }; + } + return { behavior: "allow", reason: `Path is inside the isolated trial root: ${ownedPath}` }; + } + return { behavior: "deny", reason: `Path escapes the isolated trial root: ${portable(pathValue)}` }; +} + +function firstString(input: Record, keys: string[]): string | undefined { + for (const key of keys) { + if (typeof input[key] === "string") return input[key]; + } + return undefined; +} + +function normalizeWhitespace(value: string): string { + return value.trim().replace(/\s+/g, " "); +} + +function portable(value: string): string { + return value.replaceAll("\\", "/"); +} diff --git a/packages/harness/src/compare/process.ts b/packages/harness/src/compare/process.ts new file mode 100644 index 0000000..dd05395 --- /dev/null +++ b/packages/harness/src/compare/process.ts @@ -0,0 +1,55 @@ +import { spawn } from "node:child_process"; + +export interface CommandResult { + command: string[]; + exitCode: number; + stdout: string; + stderr: string; + timedOut: boolean; + durationMs: number; +} + +const MAX_CAPTURE_BYTES = 1_000_000; + +export async function runCommand( + command: string, + args: string[], + options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }, +): Promise { + const started = Date.now(); + return await new Promise((resolveResult, reject) => { + const child = spawn(command, args, { + cwd: options.cwd, + env: options.env ?? process.env, + stdio: ["ignore", "pipe", "pipe"], + shell: false, + windowsHide: true, + }); + let stdout = ""; + let stderr = ""; + let timedOut = false; + const append = (current: string, chunk: Buffer): string => { + if (Buffer.byteLength(current) >= MAX_CAPTURE_BYTES) return current; + const remaining = MAX_CAPTURE_BYTES - Buffer.byteLength(current); + return current + chunk.subarray(0, remaining).toString("utf8"); + }; + child.stdout.on("data", (chunk: Buffer) => { stdout = append(stdout, chunk); }); + child.stderr.on("data", (chunk: Buffer) => { stderr = append(stderr, chunk); }); + child.once("error", reject); + const timer = setTimeout(() => { + timedOut = true; + child.kill(); + }, options.timeoutMs); + child.once("close", (code) => { + clearTimeout(timer); + resolveResult({ + command: [command, ...args], + exitCode: code ?? 1, + stdout, + stderr, + timedOut, + durationMs: Date.now() - started, + }); + }); + }); +} diff --git a/packages/harness/src/compare/runner.ts b/packages/harness/src/compare/runner.ts new file mode 100644 index 0000000..cfec3b6 --- /dev/null +++ b/packages/harness/src/compare/runner.ts @@ -0,0 +1,582 @@ +import { createHash } from "node:crypto"; +import { + cp, + lstat, + mkdir, + mkdtemp, + readdir, + readFile, + readlink, + rm, + writeFile, +} from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { basename, isAbsolute, join, relative, resolve } from "node:path"; +import { compileHarness } from "../compiler/compile.js"; +import type { HarnessExecutor, HarnessRunResult } from "../exec/executor.js"; +import { QoderSdkExecutor } from "../exec/qoder-sdk.js"; +import { canonicalJson, sha256Hex } from "../ir/canonical.js"; +import type { HarnessIrBundle, HarnessRevision } from "../ir/index.js"; +import { resolveComposition } from "../resolver/resolve.js"; +import { gradeReadmePackage, type ReadmeGrade } from "./grader.js"; +import { + loadHarnessCompareManifest, + type HarnessCompareManifest, + type LoadedHarnessCompareManifest, +} from "./manifest.js"; +import { + createBoundedQoderPermissionCallback, + type ToolPermissionDecision, +} from "./permissions.js"; +import { runCommand } from "./process.js"; + +export type CompareVariant = "baseline" | "candidate"; +export type TrialClassification = "passed" | "failed" | "infrastructure_error"; + +export interface CompareExecutorContext { + runtime: HarnessCompareManifest["runtime"]; + trialRoot: string; + abortController: AbortController; + permissionDecisions: ToolPermissionDecision[]; + expectedFiles: string[]; + traceEvents: unknown[]; +} + +export type CompareExecutorFactory = (context: CompareExecutorContext) => HarnessExecutor; + +export interface FileEvidence { + path: string; + kind: "file" | "symlink"; + bytes: number; + sha256: string; +} + +export interface CompareTrialResult { + variant: CompareVariant; + compositionId: string; + trial: number; + classification: TrialClassification; + changedFiles: string[]; + grade: ReadmeGrade; + executorExitCode: number; + executorError: string; + revisionId: string; + durationMs: number; + artifactDirectory: string; + metrics?: HarnessRunResult["metrics"]; +} + +export interface VariantAggregate { + trials: number; + completedTrials: number; + infrastructureErrors: number; + passedTrials: number; + passRate: number; + meanScore: number; + totalCostUsd: number; + totalCredits: number; +} + +export interface HarnessCompareVerdict { + schemaVersion: "harness-compare-result.v1"; + status: "accept" | "need_more_work" | "reject" | "infrastructure_error"; + reason: string; + manifestHash: string; + fixtureHash: string; + harnessHash: string; + baseline: VariantAggregate; + candidate: VariantAggregate; + trials: CompareTrialResult[]; +} + +export async function runHarnessComparison(options: { + manifestPath: string; + outputDirectory: string; + trialCount?: number; + executorFactory?: CompareExecutorFactory; +}): Promise { + const loaded = await loadHarnessCompareManifest(options.manifestPath); + const trialCount = options.trialCount ?? loaded.value.trials.count; + if (!Number.isInteger(trialCount) || trialCount < 1 || trialCount > loaded.value.trials.count) { + throw new Error(`trialCount must be between 1 and the frozen manifest count (${loaded.value.trials.count}).`); + } + const outputDirectory = resolve(options.outputDirectory); + assertOutputSeparated(outputDirectory, loaded.resolved.fixture); + await prepareEmptyOutput(outputDirectory); + const harnessSource = await readFile(loaded.resolved.harness, "utf8"); + const prompt = await readFile(loaded.resolved.prompt, "utf8"); + const { bundle, revisions } = await compileAndResolveVariants(loaded, harnessSource); + const fixtureBefore = await snapshotDirectory(loaded.resolved.fixture); + if (fixtureBefore.some((item) => item.kind === "symlink")) { + throw new Error("Frozen compare fixtures may not contain symbolic links."); + } + const fixtureHash = sha256Hex(canonicalJson(fixtureBefore)); + const effectiveManifest = { + ...loaded.value, + trials: { ...loaded.value.trials, count: trialCount }, + }; + const manifestHash = sha256Hex(canonicalJson(effectiveManifest)); + const harnessHash = sha256Hex(harnessSource); + const jobs = randomizedJobs(trialCount, loaded.value.trials.seed); + await writeJson(join(outputDirectory, "manifest.json"), { + ...effectiveManifest, + evidence: { manifestHash, harnessHash, fixtureHash, executionOrder: jobs }, + }); + await writeJson(join(outputDirectory, "frozen-task.json"), { + schemaVersion: "harness-frozen-task.v1", + prompt, + promptHash: sha256Hex(prompt), + fixtureHash, + fixtureFiles: fixtureBefore, + expectedFiles: loaded.value.task.expectedFiles, + graderContractHash: sha256Hex(await readFile(loaded.resolved.graderContract, "utf8")), + }); + + const results: CompareTrialResult[] = []; + for (const job of jobs) { + const compositionId = loaded.value.variants[job.variant]; + const result = await runTrial({ + loaded, + outputDirectory, + prompt, + bundle, + revision: revisions[job.variant], + compositionId, + variant: job.variant, + trial: job.trial, + executorFactory: options.executorFactory ?? defaultExecutorFactory, + }); + results.push(result); + } + + const fixtureAfter = await snapshotDirectory(loaded.resolved.fixture); + if (canonicalJson(fixtureAfter) !== canonicalJson(fixtureBefore)) { + throw new Error("Source fixture changed during comparison; refusing to produce a verdict."); + } + results.sort((a, b) => compareVariant(a.variant, b.variant) || a.trial - b.trial); + const baseline = aggregate(results.filter((result) => result.variant === "baseline")); + const candidate = aggregate(results.filter((result) => result.variant === "candidate")); + const decision = decideVerdict(baseline, candidate); + const verdict: HarnessCompareVerdict = { + schemaVersion: "harness-compare-result.v1", + ...decision, + manifestHash, + fixtureHash, + harnessHash, + baseline, + candidate, + trials: results, + }; + await writeJson(join(outputDirectory, "verdict.json"), verdict); + await writeFile(join(outputDirectory, "verdict.html"), renderVerdictHtml(verdict), "utf8"); + return verdict; +} + +async function compileAndResolveVariants( + loaded: LoadedHarnessCompareManifest, + harnessSource: string, +): Promise<{ + bundle: HarnessIrBundle; + revisions: Record; +}> { + const compiled = await compileHarness([{ uri: pathToMemoryUri(loaded.resolved.harness), text: harnessSource }]); + if (!compiled.bundle) { + const diagnostics = compiled.diagnostics.map((item) => `${item.source}:${item.line ?? 1}: ${item.message}`).join("\n"); + throw new Error(`Harness compilation failed:\n${diagnostics}`); + } + const revisions = {} as Record; + for (const variant of ["baseline", "candidate"] as const) { + const compositionId = loaded.value.variants[variant]; + const resolved = resolveComposition(compiled.bundle, compositionId); + if (!resolved.revision) { + throw new Error(`Cannot resolve ${variant} composition '${compositionId}': ${resolved.report.errors.join("; ")}`); + } + if (resolved.revision.target.host !== loaded.value.runtime.host) { + throw new Error( + `${variant} composition targets '${resolved.revision.target.host}', expected '${loaded.value.runtime.host}'.`, + ); + } + revisions[variant] = resolved.revision; + } + return { bundle: compiled.bundle, revisions }; +} + +async function runTrial(options: { + loaded: LoadedHarnessCompareManifest; + outputDirectory: string; + prompt: string; + bundle: HarnessIrBundle; + revision: HarnessRevision; + compositionId: string; + variant: CompareVariant; + trial: number; + executorFactory: CompareExecutorFactory; +}): Promise { + const variantDirectory = options.variant === "baseline" ? "H0" : "H1"; + const artifactDirectory = join( + options.outputDirectory, + variantDirectory, + `trial-${String(options.trial).padStart(3, "0")}`, + ); + await mkdir(artifactDirectory, { recursive: true }); + const temporaryParent = await mkdtemp(join(tmpdir(), "harness-compare-")); + const trialRoot = join(temporaryParent, "repo"); + const permissionDecisions: ToolPermissionDecision[] = []; + const traceEvents: unknown[] = []; + const started = Date.now(); + let execution: HarnessRunResult = { + host: "qoder", + revisionId: options.revision.revisionId, + exitCode: 1, + output: "", + errorOutput: "Executor did not start.", + warnings: [], + }; + let infrastructureError: string | undefined; + try { + await cp(options.loaded.resolved.fixture, trialRoot, { recursive: true, errorOnExist: true, force: false }); + const before = await snapshotDirectory(trialRoot); + await initializeGitRepository(trialRoot, temporaryParent); + const abortController = new AbortController(); + const executor = options.executorFactory({ + runtime: options.loaded.value.runtime, + trialRoot, + abortController, + permissionDecisions, + expectedFiles: options.loaded.value.task.expectedFiles, + traceEvents, + }); + try { + execution = await withTimeout( + executor.execute(options.revision, options.bundle, { prompt: options.prompt, cwd: trialRoot }), + options.loaded.value.runtime.timeoutMs, + abortController, + ); + } catch (error) { + infrastructureError = errorMessage(error); + execution = { + host: "qoder", + revisionId: options.revision.revisionId, + exitCode: 1, + output: "", + errorOutput: infrastructureError, + warnings: [], + }; + } + const after = await snapshotDirectory(trialRoot); + const changedFiles = diffSnapshots(before, after); + const patch = await capturePatch(trialRoot); + await writeFile(join(artifactDirectory, "prompt.txt"), options.prompt, "utf8"); + await writeFile(join(artifactDirectory, "response.txt"), execution.output, "utf8"); + await writeFile(join(artifactDirectory, "stderr.txt"), redactRoot(execution.errorOutput, trialRoot), "utf8"); + await writeFile(join(artifactDirectory, "patch.diff"), redactRoot(patch, trialRoot), "utf8"); + await writeTrace( + join(artifactDirectory, "trace.jsonl"), + execution.trace && execution.trace.length > 0 ? execution.trace : traceEvents, + trialRoot, + ); + await writeJson(join(artifactDirectory, "permission-decisions.json"), permissionDecisions); + const grade = await gradeReadmePackage({ + trialRoot, + contractPath: options.loaded.resolved.graderContract, + changedFiles, + expectedFiles: options.loaded.value.task.expectedFiles, + }); + await writeJson(join(artifactDirectory, "validation.json"), grade); + const classification = classifyTrial(execution, grade, infrastructureError); + const result: CompareTrialResult = { + variant: options.variant, + compositionId: options.compositionId, + trial: options.trial, + classification, + changedFiles, + grade, + executorExitCode: execution.exitCode, + executorError: redactRoot(execution.errorOutput, trialRoot), + revisionId: options.revision.revisionId, + durationMs: Date.now() - started, + artifactDirectory: `${variantDirectory}/trial-${String(options.trial).padStart(3, "0")}`, + ...(execution.metrics ? { metrics: execution.metrics } : {}), + }; + await writeJson(join(artifactDirectory, "runtime-receipt.json"), execution.runtimeReceipt ?? { + executor: "injected-test-executor", + permissionCallback: "none", + }); + await writeJson(join(artifactDirectory, "metrics.json"), result); + return result; + } finally { + await rm(temporaryParent, { recursive: true, force: true }); + } +} + +function defaultExecutorFactory(context: CompareExecutorContext): HarnessExecutor { + return new QoderSdkExecutor({ + tools: context.runtime.tools, + allowedTools: context.runtime.allowedTools, + disallowedTools: context.runtime.disallowedTools, + permissionMode: context.runtime.permissionMode, + canUseTool: createBoundedQoderPermissionCallback( + context.trialRoot, + context.permissionDecisions, + context.expectedFiles, + ), + maxTurns: context.runtime.maxTurns, + persistSession: false, + ...(context.runtime.model ? { model: context.runtime.model } : {}), + enableFileCheckpointing: context.runtime.enableFileCheckpointing, + abortController: context.abortController, + onTraceEvent: (event) => context.traceEvents.push(event), + }); +} + +async function prepareEmptyOutput(path: string): Promise { + try { + const entries = await readdir(path); + if (entries.length > 0) throw new Error(`Output directory is not empty: ${path}`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; + } + await mkdir(path, { recursive: true }); +} + +function assertOutputSeparated(outputDirectory: string, fixtureDirectory: string): void { + const outputFromFixture = relative(fixtureDirectory, outputDirectory); + const fixtureFromOutput = relative(outputDirectory, fixtureDirectory); + const isOwned = (value: string): boolean => value === "" || (!value.startsWith("..") && !isAbsolute(value)); + if (isOwned(outputFromFixture) || isOwned(fixtureFromOutput)) { + throw new Error("Output directory and frozen fixture must not contain one another."); + } +} + +async function initializeGitRepository(root: string, temporaryParent: string): Promise { + const hooks = join(temporaryParent, "empty-hooks"); + await mkdir(hooks); + const commands: string[][] = [ + ["init", "--quiet"], + ["-c", "core.autocrlf=false", "add", "--all"], + [ + "-c", `core.hooksPath=${hooks}`, + "-c", "user.name=Harness Compare", + "-c", "user.email=harness-compare@example.invalid", + "commit", "--quiet", "--no-gpg-sign", "-m", "frozen fixture", + ], + ]; + for (const args of commands) { + const result = await runCommand("git", args, { cwd: root, timeoutMs: 30_000 }); + if (result.exitCode !== 0) { + throw new Error(`Cannot initialize isolated Git fixture: ${result.stderr || result.stdout}`); + } + } +} + +async function capturePatch(root: string): Promise { + const untracked = await runCommand("git", ["ls-files", "--others", "--exclude-standard", "-z"], { + cwd: root, + timeoutMs: 30_000, + }); + if (untracked.exitCode !== 0) throw new Error(`Cannot inspect untracked trial files: ${untracked.stderr}`); + for (const path of untracked.stdout.split("\0").filter(Boolean).sort()) { + const intent = await runCommand("git", ["add", "--intent-to-add", "--", path], { + cwd: root, + timeoutMs: 30_000, + }); + if (intent.exitCode !== 0) throw new Error(`Cannot mark untracked file '${path}' for diff: ${intent.stderr}`); + } + const result = await runCommand("git", ["diff", "--binary", "--no-ext-diff", "--", "."], { + cwd: root, + timeoutMs: 30_000, + }); + if (result.exitCode !== 0) throw new Error(`Cannot capture trial patch: ${result.stderr}`); + return result.stdout; +} + +async function snapshotDirectory(root: string): Promise { + const evidence: FileEvidence[] = []; + await visit(root, ""); + return evidence.sort((a, b) => a.path.localeCompare(b.path)); + + async function visit(directory: string, relativeDirectory: string): Promise { + const entries = await readdir(directory, { withFileTypes: true }); + entries.sort((a, b) => a.name.localeCompare(b.name)); + for (const entry of entries) { + if (relativeDirectory === "" && entry.name === ".git") continue; + const relativePath = portable(join(relativeDirectory, entry.name)); + const absolutePath = join(directory, entry.name); + const metadata = await lstat(absolutePath); + if (metadata.isDirectory()) { + await visit(absolutePath, relativePath); + } else if (metadata.isSymbolicLink()) { + const target = await readlink(absolutePath); + evidence.push({ path: relativePath, kind: "symlink", bytes: Buffer.byteLength(target), sha256: sha256Hex(target) }); + } else if (metadata.isFile()) { + if (metadata.size > 5_000_000) { + evidence.push({ path: relativePath, kind: "file", bytes: metadata.size, sha256: "oversize" }); + } else { + const bytes = await readFile(absolutePath); + evidence.push({ path: relativePath, kind: "file", bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") }); + } + } + } + } +} + +function diffSnapshots(before: FileEvidence[], after: FileEvidence[]): string[] { + const beforeByPath = new Map(before.map((item) => [item.path, item])); + const afterByPath = new Map(after.map((item) => [item.path, item])); + return [...new Set([...beforeByPath.keys(), ...afterByPath.keys()])] + .filter((path) => canonicalJson(beforeByPath.get(path)) !== canonicalJson(afterByPath.get(path))) + .sort(); +} + +function randomizedJobs(count: number, seed: number): Array<{ variant: CompareVariant; trial: number }> { + const jobs: Array<{ variant: CompareVariant; trial: number }> = []; + for (let trial = 1; trial <= count; trial += 1) { + jobs.push({ variant: "baseline", trial }, { variant: "candidate", trial }); + } + const random = mulberry32(seed); + for (let index = jobs.length - 1; index > 0; index -= 1) { + const other = Math.floor(random() * (index + 1)); + [jobs[index], jobs[other]] = [jobs[other], jobs[index]]; + } + return jobs; +} + +function mulberry32(seed: number): () => number { + let value = seed >>> 0; + return () => { + value += 0x6d2b79f5; + let result = value; + result = Math.imul(result ^ (result >>> 15), result | 1); + result ^= result + Math.imul(result ^ (result >>> 7), result | 61); + return ((result ^ (result >>> 14)) >>> 0) / 4_294_967_296; + }; +} + +function classifyTrial( + execution: HarnessRunResult, + grade: ReadmeGrade, + thrownError?: string, +): TrialClassification { + if (thrownError || (execution.exitCode !== 0 && /auth|credential|worker|runtime|spawn|ECONN|ENOTFOUND|timed out/i.test(execution.errorOutput))) { + return "infrastructure_error"; + } + return execution.exitCode === 0 && grade.passed ? "passed" : "failed"; +} + +function aggregate(results: CompareTrialResult[]): VariantAggregate { + const completed = results.filter((result) => result.classification !== "infrastructure_error"); + const passed = completed.filter((result) => result.classification === "passed"); + return { + trials: results.length, + completedTrials: completed.length, + infrastructureErrors: results.length - completed.length, + passedTrials: passed.length, + passRate: completed.length === 0 ? 0 : passed.length / completed.length, + meanScore: completed.length === 0 + ? 0 + : Math.round((completed.reduce((sum, result) => sum + result.grade.score, 0) / completed.length) * 100) / 100, + totalCostUsd: Math.round(results.reduce((sum, result) => sum + (result.metrics?.costUsd ?? 0), 0) * 1_000_000) / 1_000_000, + totalCredits: Math.round(results.reduce((sum, result) => sum + (result.metrics?.credits ?? 0), 0) * 1_000_000) / 1_000_000, + }; +} + +function decideVerdict( + baseline: VariantAggregate, + candidate: VariantAggregate, +): Pick { + if (baseline.completedTrials === 0 || candidate.completedTrials === 0) { + return { status: "infrastructure_error", reason: "At least one variant has no completed trial." }; + } + if (candidate.passRate < baseline.passRate || candidate.meanScore + 5 < baseline.meanScore) { + return { status: "reject", reason: "Candidate regressed pass rate or deterministic score." }; + } + const costWithinLimit = + (baseline.totalCostUsd === 0 || candidate.totalCostUsd <= baseline.totalCostUsd * 1.25) && + (baseline.totalCredits === 0 || candidate.totalCredits <= baseline.totalCredits * 1.25); + if ((candidate.passRate > baseline.passRate || candidate.meanScore >= baseline.meanScore + 5) && costWithinLimit) { + return { status: "accept", reason: "Candidate improved deterministic outcomes without exceeding the 1.25x cost guardrail." }; + } + return { status: "need_more_work", reason: "No regression, but the candidate did not clear the improvement threshold." }; +} + +async function withTimeout(promise: Promise, timeoutMs: number, controller: AbortController): Promise { + let timer: NodeJS.Timeout | undefined; + let timedOut = false; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => { + timedOut = true; + controller.abort(); + reject(new Error(`Qoder SDK trial timed out after ${timeoutMs}ms.`)); + }, timeoutMs); + }), + ]); + } catch (error) { + if (timedOut) { + await Promise.race([ + promise.catch(() => undefined), + new Promise((resolveDelay) => setTimeout(resolveDelay, 5_000)), + ]); + } + throw error; + } finally { + if (timer) clearTimeout(timer); + } +} + +async function writeTrace(path: string, events: unknown[], trialRoot: string): Promise { + const content = events.map((event) => JSON.stringify(redactEvidenceValue(event, trialRoot))).join("\n"); + await writeFile(path, content ? `${content}\n` : "", "utf8"); +} + +function redactEvidenceValue(value: unknown, trialRoot: string): unknown { + if (typeof value === "string") return redactRoot(value, trialRoot); + if (Array.isArray(value)) return value.map((item) => redactEvidenceValue(item, trialRoot)); + if (value && typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, redactEvidenceValue(item, trialRoot)]), + ); + } + return value; +} + +async function writeJson(path: string, value: unknown): Promise { + await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function renderVerdictHtml(verdict: HarnessCompareVerdict): string { + const row = (name: string, aggregate: VariantAggregate): string => + `${name}${aggregate.passedTrials}/${aggregate.completedTrials}${aggregate.meanScore}${aggregate.infrastructureErrors}$${aggregate.totalCostUsd.toFixed(4)}${aggregate.totalCredits.toFixed(3)}`; + return `\nHarness compare verdict` + + `` + + `

Harness compare verdict

${escapeHtml(verdict.status)}: ${escapeHtml(verdict.reason)}

` + + `` + + row("H0 baseline", verdict.baseline) + row("H1 candidate", verdict.candidate) + + `
VariantPassedMean scoreInfra errorsCostCredits

Manifest ${verdict.manifestHash}

\n`; +} + +function escapeHtml(value: string): string { + return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); +} + +function redactRoot(value: string, root: string): string { + return value.replaceAll(root, ""); +} + +function portable(value: string): string { + return value.replaceAll("\\", "/"); +} + +function compareVariant(a: CompareVariant, b: CompareVariant): number { + return a === b ? 0 : a === "baseline" ? -1 : 1; +} + +function pathToMemoryUri(path: string): string { + return `memory://harness/${encodeURIComponent(basename(path))}`; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/packages/harness/src/exec/executor.ts b/packages/harness/src/exec/executor.ts index 16f2f3f..e7dc911 100644 --- a/packages/harness/src/exec/executor.ts +++ b/packages/harness/src/exec/executor.ts @@ -13,6 +13,39 @@ export interface HarnessRunResult { errorOutput: string; /** Realizations the executor could not materialize on this host. */ warnings: string[]; + /** Redacted protocol events retained as run evidence when the host exposes them. */ + trace?: unknown[]; + /** Non-secret options the executor actually passed to the host runtime. */ + runtimeReceipt?: HarnessRuntimeReceipt; + /** Host-reported consumption and termination evidence. */ + metrics?: HarnessRunMetrics; +} + +export interface HarnessRuntimeReceipt { + executor: string; + tools: string[]; + allowedTools: string[]; + disallowedTools: string[]; + permissionMode?: string; + maxTurns?: number; + persistSession?: boolean; + model?: string; + fileCheckpointing?: boolean; + permissionCallback: "configured" | "none"; +} + +export interface HarnessRunMetrics { + durationMs?: number; + durationApiMs?: number; + turns?: number; + costUsd?: number; + credits?: number; + usage?: Record; + modelUsage?: Record; + permissionDenials?: unknown[]; + sessionId?: string; + stopReason?: string; + terminalReason?: string; } export interface HarnessExecutor { diff --git a/packages/harness/src/exec/index.ts b/packages/harness/src/exec/index.ts index 32b9bab..816ec74 100644 --- a/packages/harness/src/exec/index.ts +++ b/packages/harness/src/exec/index.ts @@ -5,7 +5,9 @@ export { HarnessHostMismatchError, type HarnessExecutor, type HarnessRunResult, + type HarnessRunMetrics, type HarnessRunTask, + type HarnessRuntimeReceipt, type RunPreamble, } from "./executor.js"; export { @@ -14,6 +16,10 @@ export { type QoderSdkExecutorOptions, type QoderSdkLike, type QoderSdkMessage, + type QoderPermissionMode, + type QoderToolPermissionCallback, + type QoderToolPermissionResult, + redactTraceValue, } from "./qoder-sdk.js"; export { PiSdkExecutor, diff --git a/packages/harness/src/exec/qoder-sdk.ts b/packages/harness/src/exec/qoder-sdk.ts index bea5eaa..cc2660d 100644 --- a/packages/harness/src/exec/qoder-sdk.ts +++ b/packages/harness/src/exec/qoder-sdk.ts @@ -15,6 +15,7 @@ export interface QoderSdkTextBlock { } export interface QoderSdkMessage { + [key: string]: unknown; type: string; subtype?: string; is_error?: boolean; @@ -22,8 +23,38 @@ export interface QoderSdkMessage { errors?: string[]; result?: unknown; message?: { content?: QoderSdkTextBlock[] }; + duration_ms?: number; + duration_api_ms?: number; + num_turns?: number; + total_cost_usd?: number; + total_credits?: number; + usage?: Record; + modelUsage?: Record; + permission_denials?: unknown[]; + session_id?: string; + stop_reason?: string | null; + terminal_reason?: string | null; } +export type QoderPermissionMode = + | "default" + | "acceptEdits" + | "bypassPermissions" + | "yolo" + | "plan" + | "dontAsk" + | "auto"; + +export type QoderToolPermissionResult = + | { behavior: "allow"; updatedInput?: Record } + | { behavior: "deny"; message: string; interrupt?: boolean }; + +export type QoderToolPermissionCallback = ( + toolName: string, + input: Record, + options: { signal: AbortSignal; toolUseID: string; [key: string]: unknown }, +) => Promise; + export interface QoderSdkLike { query(params: { prompt: string; @@ -32,8 +63,14 @@ export interface QoderSdkLike { cwd?: string; allowedTools: string[]; tools: string[]; + disallowedTools?: string[]; + permissionMode?: QoderPermissionMode; + canUseTool?: QoderToolPermissionCallback; persistSession: boolean; maxTurns: number; + model?: string; + enableFileCheckpointing?: boolean; + abortController?: AbortController; }; }): AsyncIterable; qodercliAuth(): unknown; @@ -50,9 +87,18 @@ export interface QoderSdkExecutorOptions { allowedTools?: string[]; /** Tools visible to the session. v0.1 defaults to none. */ tools?: string[]; + /** Tools removed from the session even when the runtime would normally expose them. */ + disallowedTools?: string[]; + permissionMode?: QoderPermissionMode; + canUseTool?: QoderToolPermissionCallback; /** Defaults to an ephemeral one-turn query. */ persistSession?: boolean; maxTurns?: number; + model?: string; + enableFileCheckpointing?: boolean; + abortController?: AbortController; + /** Receives each redacted SDK event as it arrives, including before a timeout. */ + onTraceEvent?: (event: unknown) => void; } /** Execute one resolved revision through the official Qoder Agent SDK. */ @@ -62,16 +108,30 @@ export class QoderSdkExecutor implements HarnessExecutor { private readonly auth: QoderAuthFactory; private readonly allowedTools: string[]; private readonly tools: string[]; + private readonly disallowedTools: string[]; + private readonly permissionMode?: QoderPermissionMode; + private readonly canUseTool?: QoderToolPermissionCallback; private readonly persistSession: boolean; private readonly maxTurns: number; + private readonly model?: string; + private readonly enableFileCheckpointing: boolean; + private readonly abortController?: AbortController; + private readonly onTraceEvent?: (event: unknown) => void; constructor(options: QoderSdkExecutorOptions = {}) { this.loadSdk = () => loadQoderSdk(options.loadSdk); this.auth = options.auth ?? ((sdk) => sdk.qodercliAuth()); this.allowedTools = [...(options.allowedTools ?? [])]; this.tools = [...(options.tools ?? [])]; + this.disallowedTools = [...(options.disallowedTools ?? [])]; + this.permissionMode = options.permissionMode; + this.canUseTool = options.canUseTool; this.persistSession = options.persistSession ?? false; this.maxTurns = options.maxTurns ?? 1; + this.model = options.model; + this.enableFileCheckpointing = options.enableFileCheckpointing ?? false; + this.abortController = options.abortController; + this.onTraceEvent = options.onTraceEvent; } async execute( @@ -82,22 +142,34 @@ export class QoderSdkExecutor implements HarnessExecutor { assertRevisionHost(revision, this.host); const { prompt, warnings } = buildRunPrompt(revision, bundle, task); const sdk = await this.loadSdk(); + const sdkOptions: Parameters[0]["options"] = { + auth: this.auth(sdk), + cwd: task.cwd, + allowedTools: [...this.allowedTools], + tools: [...this.tools], + disallowedTools: [...this.disallowedTools], + persistSession: this.persistSession, + maxTurns: this.maxTurns, + enableFileCheckpointing: this.enableFileCheckpointing, + ...(this.permissionMode !== undefined ? { permissionMode: this.permissionMode } : {}), + ...(this.canUseTool !== undefined ? { canUseTool: this.canUseTool } : {}), + ...(this.model !== undefined ? { model: this.model } : {}), + ...(this.abortController !== undefined ? { abortController: this.abortController } : {}), + }; const stream = sdk.query({ prompt, - options: { - auth: this.auth(sdk), - cwd: task.cwd, - allowedTools: [...this.allowedTools], - tools: [...this.tools], - persistSession: this.persistSession, - maxTurns: this.maxTurns, - }, + options: sdkOptions, }); const output: string[] = []; const errors: string[] = []; + const trace: unknown[] = []; + let metrics: HarnessRunResult["metrics"]; let exitCode = 0; let sawResult = false; for await (const message of stream) { + const traceEvent = redactTraceValue(message); + trace.push(traceEvent); + this.onTraceEvent?.(traceEvent); if (message.type === "assistant") { for (const block of message.message?.content ?? []) { if (block.type === "text" && typeof block.text === "string") { @@ -107,6 +179,23 @@ export class QoderSdkExecutor implements HarnessExecutor { } if (message.type === "result") { sawResult = true; + metrics = { + ...(message.duration_ms !== undefined ? { durationMs: message.duration_ms } : {}), + ...(message.duration_api_ms !== undefined ? { durationApiMs: message.duration_api_ms } : {}), + ...(message.num_turns !== undefined ? { turns: message.num_turns } : {}), + ...(message.total_cost_usd !== undefined ? { costUsd: message.total_cost_usd } : {}), + ...(message.total_credits !== undefined ? { credits: message.total_credits } : {}), + ...(message.usage !== undefined ? { usage: redactTraceValue(message.usage) as Record } : {}), + ...(message.modelUsage !== undefined + ? { modelUsage: redactTraceValue(message.modelUsage) as Record } + : {}), + ...(message.permission_denials !== undefined + ? { permissionDenials: redactTraceValue(message.permission_denials) as unknown[] } + : {}), + ...(message.session_id !== undefined ? { sessionId: message.session_id } : {}), + ...(message.stop_reason ? { stopReason: message.stop_reason } : {}), + ...(message.terminal_reason ? { terminalReason: message.terminal_reason } : {}), + }; if (message.is_error === true || message.subtype !== "success") { exitCode = 1; errors.push(...(message.errors ?? [message.error ?? stringifyResult(message.result)])); @@ -124,10 +213,45 @@ export class QoderSdkExecutor implements HarnessExecutor { output: output.join(""), errorOutput: errors.filter(Boolean).join("\n"), warnings, + trace, + runtimeReceipt: { + executor: "@qoder-ai/qoder-agent-sdk", + tools: [...this.tools], + allowedTools: [...this.allowedTools], + disallowedTools: [...this.disallowedTools], + ...(this.permissionMode !== undefined ? { permissionMode: this.permissionMode } : {}), + maxTurns: this.maxTurns, + persistSession: this.persistSession, + ...(this.model !== undefined ? { model: this.model } : {}), + fileCheckpointing: this.enableFileCheckpointing, + permissionCallback: this.canUseTool ? "configured" : "none", + }, + ...(metrics !== undefined ? { metrics } : {}), }; } } +const SECRET_FIELD = /(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|service[_-]?account[_-]?key|secret|credential)/i; + +/** Recursively remove credential-shaped fields before persisting SDK events. */ +export function redactTraceValue(value: unknown, seen = new WeakSet()): unknown { + if (value === null || typeof value !== "object") { + return value; + } + if (seen.has(value)) { + return "[Circular]"; + } + seen.add(value); + if (Array.isArray(value)) { + return value.map((item) => redactTraceValue(item, seen)); + } + const redacted: Record = {}; + for (const [key, item] of Object.entries(value)) { + redacted[key] = SECRET_FIELD.test(key) ? "[REDACTED]" : redactTraceValue(item, seen); + } + return redacted; +} + async function loadQoderSdk(loader?: () => Promise): Promise { try { if (loader) { diff --git a/packages/harness/src/index.ts b/packages/harness/src/index.ts index 5078f6e..31c4e3f 100644 --- a/packages/harness/src/index.ts +++ b/packages/harness/src/index.ts @@ -10,6 +10,7 @@ export { mergePermissions, resolveComposition, type ResolveResult } from "./reso export { STRENGTH_ORDER, strengthIndex } from "./language/harness-validator.js"; export { createHarnessServices, type HarnessServices } from "./language/harness-module.js"; export * from "./exec/index.js"; +export * from "./compare/index.js"; export { getHarnessHighlighter, harnessTextMateGrammar, diff --git a/packages/harness/test/compare-cli.test.ts b/packages/harness/test/compare-cli.test.ts new file mode 100644 index 0000000..fc2c7a2 --- /dev/null +++ b/packages/harness/test/compare-cli.test.ts @@ -0,0 +1,22 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { main } from "../src/compare/cli.js"; + +afterEach(() => vi.restoreAllMocks()); + +describe("harness-compare CLI", () => { + it("prints discoverable help without running a comparison", async () => { + const stdout = vi.spyOn(process.stdout, "write").mockImplementation(() => true); + + await expect(main(["--help"])).resolves.toBe(0); + + expect(stdout).toHaveBeenCalledWith(expect.stringContaining("harness-compare run ")); + }); + + it("rejects unknown commands on stderr", async () => { + const stderr = vi.spyOn(process.stderr, "write").mockImplementation(() => true); + + await expect(main(["compare"])).resolves.toBe(2); + + expect(stderr).toHaveBeenCalledWith(expect.stringContaining("Expected: harness-compare run")); + }); +}); diff --git a/packages/harness/test/compare.test.ts b/packages/harness/test/compare.test.ts new file mode 100644 index 0000000..47c5af7 --- /dev/null +++ b/packages/harness/test/compare.test.ts @@ -0,0 +1,187 @@ +import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import type { HarnessExecutor } from "../src/exec/executor.js"; +import { gradeReadmePackage } from "../src/compare/grader.js"; +import { loadHarnessCompareManifest } from "../src/compare/manifest.js"; +import { createBoundedQoderPermissionCallback, type ToolPermissionDecision } from "../src/compare/permissions.js"; +import { runHarnessComparison } from "../src/compare/runner.js"; + +const EXPERIMENT_URL = new URL("../examples/readme-compare/experiment.json", import.meta.url); +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all(temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true }))); +}); + +describe("harness compare manifest", () => { + it("loads the frozen README experiment with owned relative paths", async () => { + const loaded = await loadHarnessCompareManifest(EXPERIMENT_URL.pathname); + + expect(loaded.value).toMatchObject({ + schemaVersion: "harness-compare.v1", + variants: { baseline: "readme-baseline", candidate: "readme-grounded" }, + runtime: { host: "qoder", permissionMode: "default", network: "deny" }, + }); + expect(loaded.resolved.fixture.endsWith("examples/readme-compare/fixture")).toBe(true); + }); + + it("rejects every auto-approved tool surface", async () => { + const directory = await makeTemporaryDirectory(); + const manifest = JSON.parse(await readFile(EXPERIMENT_URL, "utf8")) as { + runtime: { allowedTools: string[] }; + }; + manifest.runtime.allowedTools = ["Bash"]; + const path = join(directory, "experiment.json"); + await writeFile(path, JSON.stringify(manifest), "utf8"); + + await expect(loadHarnessCompareManifest(path)).rejects.toThrow(/allowedTools must be empty/); + }); +}); + +describe("bounded Qoder permissions", () => { + it("allows owned file operations and frozen validation commands", async () => { + const directory = await makeTemporaryDirectory(); + const decisions: ToolPermissionDecision[] = []; + const callback = createBoundedQoderPermissionCallback(directory, decisions, ["README.md"]); + + await expect(callback("Write", { file_path: "README.md" }, toolOptions())).resolves.toEqual({ behavior: "allow" }); + await expect(callback("Bash", { command: "npm test" }, toolOptions())).resolves.toEqual({ behavior: "allow" }); + expect(decisions.map((decision) => decision.behavior)).toEqual(["allow", "allow"]); + }); + + it("denies repository escapes, command chaining, network tools, and unknown commands", async () => { + const directory = await makeTemporaryDirectory(); + const callback = createBoundedQoderPermissionCallback(directory, [], ["README.md"]); + + await expect(callback("Write", { file_path: "../outside.md" }, toolOptions())).resolves.toMatchObject({ behavior: "deny" }); + await expect(callback("Write", { file_path: "package.json" }, toolOptions())).resolves.toMatchObject({ behavior: "deny" }); + await expect(callback("Write", { file_path: ".git/config" }, toolOptions())).resolves.toMatchObject({ behavior: "deny" }); + await expect(callback("Bash", { command: "npm test && curl example.com" }, toolOptions())).resolves.toMatchObject({ behavior: "deny" }); + await expect(callback("WebFetch", { url: "https://example.com" }, toolOptions())).resolves.toMatchObject({ behavior: "deny" }); + await expect(callback("Bash", { command: "pwd" }, toolOptions())).resolves.toMatchObject({ behavior: "deny" }); + }); +}); + +describe("README coding comparison", () => { + it("rejects generated examples that request host capabilities", async () => { + const directory = await makeTemporaryDirectory(); + const fixture = new URL("../examples/readme-compare/fixture", import.meta.url); + await cp(fixture, directory, { recursive: true, force: true }); + await writeFile( + join(directory, "README.md"), + VALID_README.replace("console.log(value);", "console.log(process.env);"), + "utf8", + ); + + const grade = await gradeReadmePackage({ + trialRoot: directory, + contractPath: new URL("../examples/readme-compare/grader-contract.json", import.meta.url).pathname, + changedFiles: ["README.md"], + expectedFiles: ["README.md"], + }); + + expect(grade.checks.find((item) => item.id === "quick-start")).toMatchObject({ + passed: false, + command: { stderr: expect.stringContaining("outside the isolated example policy") }, + }); + }); + + it("creates and grades real README files in isolated variant trials", async () => { + const directory = await makeTemporaryDirectory(); + const output = join(directory, "evidence"); + const fixtureReadme = new URL("../examples/readme-compare/fixture/README.md", import.meta.url); + await expect(readFile(fixtureReadme, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + + const verdict = await runHarnessComparison({ + manifestPath: EXPERIMENT_URL.pathname, + outputDirectory: output, + trialCount: 1, + executorFactory: ({ trialRoot }): HarnessExecutor => ({ + host: "qoder", + execute: async (revision) => { + if (revision.composition.id === "readme-grounded") { + await writeFile(join(trialRoot, "README.md"), VALID_README, "utf8"); + } + return { + host: "qoder", + revisionId: revision.revisionId, + exitCode: 0, + output: "completed", + errorOutput: "", + warnings: [], + trace: [{ type: "result", subtype: "success", file_path: join(trialRoot, "README.md") }], + metrics: { durationMs: 10, turns: 1, costUsd: 0.001 }, + }; + }, + }), + }); + + const candidateTrial = verdict.trials.find((trial) => trial.variant === "candidate"); + const quickStart = candidateTrial?.grade.checks.find((item) => item.id === "quick-start"); + expect(quickStart?.command?.stderr).toBe(""); + expect(quickStart).toMatchObject({ passed: true }); + expect(verdict.status).toBe("accept"); + expect(verdict.baseline).toMatchObject({ passedTrials: 0, meanScore: 0 }); + expect(verdict.candidate).toMatchObject({ passedTrials: 1, meanScore: 100 }); + expect(candidateTrial).toMatchObject({ + classification: "passed", + changedFiles: ["README.md"], + grade: { passed: true, score: 100 }, + }); + expect(await readFile(join(output, "H1/trial-001/patch.diff"), "utf8")).toContain("README.md"); + expect(await readFile(join(output, "H1/trial-001/patch.diff"), "utf8")).toContain("+# Retry Kit"); + expect(await readFile(join(output, "H1/trial-001/trace.jsonl"), "utf8")).toContain("/README.md"); + expect(JSON.parse(await readFile(join(output, "H1/trial-001/validation.json"), "utf8"))).toMatchObject({ passed: true }); + expect(JSON.parse(await readFile(join(output, "verdict.json"), "utf8"))).toMatchObject({ status: "accept" }); + expect(await readFile(join(output, "verdict.html"), "utf8")).toContain("Harness compare verdict"); + await expect(readFile(fixtureReadme, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + }); +}); + +async function makeTemporaryDirectory(): Promise { + const directory = await mkdtemp(join(tmpdir(), "harness-compare-test-")); + temporaryDirectories.push(directory); + return directory; +} + +function toolOptions(): { signal: AbortSignal; toolUseID: string } { + return { signal: new AbortController().signal, toolUseID: "tool-use-1" }; +} + +const VALID_README = `# Retry Kit + +## Purpose + +\`@fixture/retry-kit\` retries asynchronous operations with abort support. + +## Installation + +\`\`\`sh +npm install @fixture/retry-kit +\`\`\` + +## Quick Start + +\`\`\`js +import { retry } from "@fixture/retry-kit"; + +const value = await retry(async () => "ready", { maxAttempts: 3 }); +console.log(value); +\`\`\` + +## API + +- \`retry\` runs an asynchronous operation until it succeeds or exhausts its attempts. +- \`RetryExhaustedError\` reports the final failure. +- \`DEFAULT_MAX_ATTEMPTS\` exposes the default. + +## Behavior + +The default \`maxAttempts\` is \`3\`. An \`AbortSignal\` is checked before every attempt and while waiting for backoff. + +## Verification + +Run \`npm test\`. +`; diff --git a/packages/harness/test/compile.test.ts b/packages/harness/test/compile.test.ts index c54c98e..24fe392 100644 --- a/packages/harness/test/compile.test.ts +++ b/packages/harness/test/compile.test.ts @@ -3,10 +3,53 @@ import { Value } from "@sinclair/typebox/value"; import { describe, expect, it } from "vitest"; import { compileHarness } from "../src/compiler/compile.js"; import { HarnessIrBundleSchema, IR_VERSION } from "../src/ir/index.js"; +import { resolveComposition } from "../src/resolver/resolve.js"; const EXAMPLE_URL = new URL("../examples/standard-coding.harness", import.meta.url); +const FULL_SURFACE_URL = new URL("../examples/full-surface.harness", import.meta.url); describe("compileHarness", () => { + it("compiles every authored v0.1 syntax branch into typed IR", async () => { + const source = await readFile(FULL_SURFACE_URL, "utf8"); + const result = await compileHarness(source); + + expect(result.diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]); + expect(Value.Check(HarnessIrBundleSchema, result.bundle)).toBe(true); + expect(result.bundle!.components.map((component) => component.componentKind)).toEqual([ + "skill", "tool", "program", "workflow", "hook", "policy", "observer", "ui", + ]); + expect(new Set(result.bundle!.bindings.map((binding) => binding.strength))).toEqual( + new Set(["unsupported", "advisory", "wired", "enforced"]), + ); + expect(result.bundle!.components.flatMap((component) => component.permissions)).toEqual( + expect.arrayContaining([ + { domain: "workspace", access: "read" }, + { domain: "workspace", access: "write" }, + { domain: "process", access: "allow" }, + { domain: "process", access: "deny" }, + { domain: "network", access: "allow" }, + { domain: "network", access: "deny" }, + { domain: "model", access: "allow" }, + { domain: "model", access: "deny" }, + ]), + ); + expect(result.bundle!.compositions[0].settings).toEqual([ + { key: "runtime.max-turns", value: { type: "int", value: 24 } }, + { key: "runtime.timeout", value: { type: "duration", value: "10m", ms: 600_000 } }, + { key: "runtime.label", value: { type: "string", value: "full-surface" } }, + { key: "runtime.checkpoints", value: { type: "boolean", value: true } }, + { key: "runtime.network-enabled", value: { type: "boolean", value: false } }, + ]); + const resolved = resolveComposition(result.bundle!, "full-surface-qoder"); + expect(resolved.report.status).toBe("resolved"); + expect(resolved.report.realizations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ componentId: "repository-skill", action: "satisfied" }), + expect.objectContaining({ componentId: "test-program", action: "degraded" }), + ]), + ); + }); + it("lowers the example document into a schema-valid IR bundle", async () => { const source = await readFile(EXAMPLE_URL, "utf8"); const result = await compileHarness(source); diff --git a/packages/harness/test/exec.test.ts b/packages/harness/test/exec.test.ts index 5726299..3f4bfab 100644 --- a/packages/harness/test/exec.test.ts +++ b/packages/harness/test/exec.test.ts @@ -79,7 +79,22 @@ describe("QoderSdkExecutor", () => { yield { type: "result", subtype: "success" }; }, }; - const executor = new QoderSdkExecutor({ loadSdk: async () => sdk, allowedTools: ["Read"] }); + const abortController = new AbortController(); + const canUseTool = async () => ({ behavior: "allow" as const }); + const streamedTrace: unknown[] = []; + const executor = new QoderSdkExecutor({ + loadSdk: async () => sdk, + allowedTools: ["Read"], + tools: ["Read", "Bash"], + disallowedTools: ["WebFetch"], + permissionMode: "default", + canUseTool, + model: "test-model", + enableFileCheckpointing: true, + abortController, + onTraceEvent: (event) => streamedTrace.push(event), + maxTurns: 12, + }); const result = await executor.execute(revision, bundle, { prompt: "Fix the bug", cwd: "/tmp" }); @@ -88,15 +103,94 @@ describe("QoderSdkExecutor", () => { auth, cwd: "/tmp", allowedTools: ["Read"], - tools: [], + tools: ["Read", "Bash"], + disallowedTools: ["WebFetch"], + permissionMode: "default", + canUseTool, persistSession: false, - maxTurns: 1, + maxTurns: 12, + model: "test-model", + enableFileCheckpointing: true, + abortController, }); const prompt = queries[0].prompt; expect(prompt.endsWith("Fix the bug")).toBe(true); expect(prompt).toContain(revision.revisionId); expect(prompt).toContain("Impact analysis: map the blast radius before editing."); - expect(result).toMatchObject({ host: "qoder", exitCode: 0, output: "done" }); + expect(result).toMatchObject({ + host: "qoder", + exitCode: 0, + output: "done", + runtimeReceipt: { + executor: "@qoder-ai/qoder-agent-sdk", + tools: ["Read", "Bash"], + allowedTools: ["Read"], + disallowedTools: ["WebFetch"], + permissionMode: "default", + maxTurns: 12, + model: "test-model", + fileCheckpointing: true, + permissionCallback: "configured", + }, + }); + expect(streamedTrace).toEqual(result.trace); + }); + + it("retains usage evidence while redacting credential-shaped trace fields", async () => { + const { bundle, revision } = await resolveFor("on-qoder"); + const sdk: QoderSdkLike = { + qodercliAuth: () => ({}), + query: async function* () { + yield { + type: "system", + subtype: "init", + access_token: "must-not-leak", + nested: { serviceAccountKey: "also-must-not-leak" }, + }; + yield { + type: "result", + subtype: "success", + duration_ms: 120, + duration_api_ms: 90, + num_turns: 2, + total_cost_usd: 0.01, + total_credits: 1.25, + usage: { input_tokens: 10, output_tokens: 5 }, + modelUsage: { model: { inputTokens: 10 } }, + permission_denials: [], + session_id: "session-1", + stop_reason: "end_turn", + terminal_reason: "completed", + }; + }, + }; + + const result = await new QoderSdkExecutor({ loadSdk: async () => sdk }).execute( + revision, + bundle, + { prompt: "Inspect" }, + ); + + expect(result.trace).toContainEqual( + expect.objectContaining({ access_token: "[REDACTED]" }), + ); + expect(JSON.stringify(result.trace)).not.toContain("must-not-leak"); + expect(result.trace).toContainEqual( + expect.objectContaining({ nested: { serviceAccountKey: "[REDACTED]" } }), + ); + expect(result.metrics).toEqual({ + durationMs: 120, + durationApiMs: 90, + turns: 2, + costUsd: 0.01, + credits: 1.25, + usage: { input_tokens: 10, output_tokens: 5 }, + modelUsage: { model: { inputTokens: 10 } }, + permissionDenials: [], + sessionId: "session-1", + stopReason: "end_turn", + terminalReason: "completed", + }); }); it("reports declared native strength as an advisory degradation", async () => { From e5b118a4a62f946ecdc3d7c37d398b81451dd22e Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 15 Aug 2026 11:22:45 +0800 Subject: [PATCH 04/25] feat(harness): add minimal qoder runtime profile Add a named qoder-minimal-v1 executor contract and variant-level compare profiles so runtime effects can be measured independently of harness composition. The profile fixes a four-tool surface, isolates optional SDK context, and records non-secret runtime evidence. Implements docs/specs/2026-08-15-qoder-minimal-runtime-profile.md. Validation passed in an isolated staged checkout with npm run harness:generated, npm run harness:build, npm run harness:test (56 tests), and the documentation link graph (6 checks). Co-authored-by: Codex (GPT 5.6 Sol) --- ...026-08-15-qoder-minimal-runtime-profile.md | 104 +++++++++++++++ packages/harness/README.md | 22 ++++ .../minimal-profile-experiment.json | 38 ++++++ .../readme-compare/minimal-profile-prompt.md | 18 +++ packages/harness/src/compare/index.ts | 3 + packages/harness/src/compare/manifest.ts | 81 +++++++++--- packages/harness/src/compare/runner.ts | 13 +- packages/harness/src/exec/executor.ts | 8 ++ packages/harness/src/exec/index.ts | 1 + packages/harness/src/exec/qoder-sdk.ts | 123 +++++++++++++++++- packages/harness/test/compare.test.ts | 77 ++++++++++- packages/harness/test/exec.test.ts | 86 ++++++++++++ 12 files changed, 546 insertions(+), 28 deletions(-) create mode 100644 docs/specs/2026-08-15-qoder-minimal-runtime-profile.md create mode 100644 packages/harness/examples/readme-compare/minimal-profile-experiment.json create mode 100644 packages/harness/examples/readme-compare/minimal-profile-prompt.md diff --git a/docs/specs/2026-08-15-qoder-minimal-runtime-profile.md b/docs/specs/2026-08-15-qoder-minimal-runtime-profile.md new file mode 100644 index 0000000..6f9761d --- /dev/null +++ b/docs/specs/2026-08-15-qoder-minimal-runtime-profile.md @@ -0,0 +1,104 @@ +# Compare a minimal Qoder coding runtime + +## Traceability + +- Spec ID: qoder-minimal-runtime-profile +- Status: Implemented + +## Intent + +Provide a named, evidence-bearing Qoder runtime profile that presents the model +with the same four fundamental coding tools as Pi while suppressing optional SDK +context sources. Let one harness comparison hold the task and composition fixed +while assigning different named runtime profiles to the baseline and candidate, +so a successful README artifact can test the profile rather than merely prove +that the SDK returned a result. + +## Acceptance Scenarios + +- AC-1: `QoderSdkExecutor` accepts `qoder-minimal-v1` and sends exactly `Read`, + `Write`, `Edit`, and `Bash`, an explicit minimal system prompt, no filesystem + setting sources, empty SDK selections for skills/plugins/extensions, no + configured MCP servers, strict MCP isolation, an ephemeral session, and the + existing bounded permission callback to the official Qoder Agent SDK. +- AC-2: The runtime receipt names the selected profile and records non-secret + context-isolation facts without persisting the system-prompt body or any + credential. Contradictory options that would weaken the named minimal profile + fail before the SDK is loaded. +- AC-3: A `harness-compare.v1` manifest may explicitly assign a named Qoder + profile to each variant. The comparison resolves the effective runtime per + trial, records the profile in trial evidence, and permits both variants to use + the same composition only when the profile assignment differs. +- AC-4: Existing manifests with no per-variant profile assignment retain their + current tool set, runtime behavior, and validation policy. Their existing + evidence fields remain intact, with `qoder-default-v1` added explicitly. +- AC-5: Deterministic tests prove profile forwarding, fail-closed validation, + variant routing, and backward compatibility. A real SDK smoke runs the frozen + README task once under each profile and grades the resulting file and changed + file scope. + +## Non-goals + +- Replacing Qoder's qodercli/worker agent loop with a bare model loop. +- Implementing host-owned `read`, `patch`, or `run` MCP tools in this change. +- Claiming that one README trial establishes general coding quality or a + statistically significant performance improvement. +- Changing the `.harness` grammar, adding a host adapter, publishing a package, + or editing release metadata. + +## Plan and Tasks + +1. Add a public Qoder runtime-profile type and a frozen `qoder-minimal-v1` + contract to the executor, including conflict validation and evidence fields. +2. Extend the additive `harness-compare.v1` manifest surface with explicit + baseline/candidate profile assignment and resolve it per trial. +3. Add an isolated profile-comparison example that holds the grounded + composition, fixture, prompt, grader, model, permissions, and trial order + constant. +4. Add behavior tests and documentation for profile semantics and comparison + limits. +5. Run focused package checks, repository gates, and one real SDK trial per + profile; retain generated evidence outside the source tree. + +## Test and Review Evidence + +- AC-1/AC-2: the isolated staged checkout passed 56 package behavior tests. The executor test + observes the exact four-tool SDK options, empty optional context sources, + strict MCP configuration, cwd-bearing system prompt, prompt-free receipt, + and fail-closed conflict validation. +- AC-3/AC-4: manifest and runner tests prove that an unchanged v1 manifest + keeps `qoder-default-v1`, while the isolated profile experiment routes the + same composition through six-tool default and four-tool minimal runtimes and + records each profile in trial evidence. +- AC-5: a real Qoder SDK trial under each profile changed only `README.md` and + passed all nine deterministic grader checks with score 100. After repairing + missing cwd context found by the first smoke, both final trials completed in + five turns with no permission denial. The observed credits were 12.449909 for + default and 9.785649 for minimal; one trial is insufficient to claim a stable + efficiency improvement, so the aggregate verdict remains `need_more_work`. + The minimal init event exposed exactly `Bash`, `Edit`, `Read`, and `Write` as + tools. It still inventoried installed skill metadata, which is not treated as + proof that the files were absent; `skills: []` and the missing `Skill` tool + are the bounded model-context/invocation evidence. + Trial artifacts were retained outside the repository for local review and + were not added to the package or source tree. +- Isolated staged-checkout verification: `harness:generated` and + `harness:build` passed; all seven package test files / 56 tests passed; and + the documentation link graph passed six checks. Earlier whole-tree gates also + passed 94 root test files / 1,324 tests and root pack verification with 526 npm + and 548 runtime-zip entries; those whole-tree counts include concurrent local + work and are supporting evidence rather than the staged-slice proof. +- Verification qualification: the composite `npm run check` cannot report a + zero exit while a concurrent, uncommitted DSL grammar change legitimately + differs from `HEAD`, because `check:generated` rejects every generated diff. + Its root-test stage passed, and all remaining stages were run individually. +- Risk: four tools may increase discovery attempts because `Glob` and `Grep` + are hidden. The first smoke confirmed this risk when missing cwd context led + to five denied path guesses; the repaired profile made the working directory + explicit and the final smoke had no denials. +- Risk: filesystem settings and optional SDK resources can silently expand + context. The executor sends explicit empty collections and strict MCP config, + and the runtime receipt records those values. +- Risk: the working tree contains concurrent DSL sugar and compare-hardening + changes. Final review must preserve them and distinguish their ownership from + this profile slice rather than staging the complete dirty tree. diff --git a/packages/harness/README.md b/packages/harness/README.md index 54f8295..8ba0f64 100644 --- a/packages/harness/README.md +++ b/packages/harness/README.md @@ -105,6 +105,26 @@ The command uses the official Qoder Agent SDK and the locally signed-in smoke; use all five trials before treating the result as comparative evidence. The README task is one benchmark, not proof of general coding performance. +To isolate runtime-tool effects from harness-policy effects, the companion +profile experiment holds the grounded composition and task constant. Its +baseline uses the manifest's six bounded coding tools; its candidate uses the +named `qoder-minimal-v1` profile: + +```sh +node packages/harness/dist/compare/cli.js run packages/harness/examples/readme-compare/minimal-profile-experiment.json --out ./harness-qoder-profile-evidence --trials 1 +``` + +`qoder-minimal-v1` exposes exactly `Read`, `Write`, `Edit`, and `Bash`, replaces +the runtime system prompt with a short coding contract, skips filesystem setting +sources, passes empty SDK selections for skills/plugins/extensions, configures +no MCP servers, enables strict MCP isolation, and keeps the session ephemeral. +The runtime may still report installed skill metadata during initialization; +the model-visible tool list is the stronger observation and contains only the +four named tools. The receipt records the profile and non-secret isolation +options without storing the prompt body. +One trial is a runtime smoke only; use the frozen five trials, and additional +coding tasks, before drawing comparative quality or cost conclusions. + Coding trials expose only `Read`, `Glob`, `Grep`, `Edit`, `Write`, and `Bash`. No tool is auto-approved: a permission callback confines file operations to the isolated trial, restricts writes to the manifest's expected files, and permits @@ -165,3 +185,5 @@ npm run harness:test The package supports the repository's Node range (`>=22.20.0 <25`). See the [v0.1 production-readiness spec](https://github.com/QoderAI/better-harness/blob/main/docs/specs/2026-08-14-harness-as-code-v0.1.md) for acceptance scenarios, non-goals, and validation evidence. +The named minimal profile and its isolated comparison are specified in +[`docs/specs/2026-08-15-qoder-minimal-runtime-profile.md`](../../docs/specs/2026-08-15-qoder-minimal-runtime-profile.md). diff --git a/packages/harness/examples/readme-compare/minimal-profile-experiment.json b/packages/harness/examples/readme-compare/minimal-profile-experiment.json new file mode 100644 index 0000000..f9ecf2d --- /dev/null +++ b/packages/harness/examples/readme-compare/minimal-profile-experiment.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": "harness-compare.v1", + "harness": "./readme-compare.harness", + "variants": { + "baseline": "readme-grounded", + "candidate": "readme-grounded" + }, + "runtimeProfiles": { + "baseline": "qoder-default-v1", + "candidate": "qoder-minimal-v1" + }, + "task": { + "fixture": "./fixture", + "prompt": "./minimal-profile-prompt.md", + "expectedFiles": ["README.md"], + "grader": { + "kind": "readme-package-v1", + "contract": "./grader-contract.json" + } + }, + "runtime": { + "host": "qoder", + "model": "performance", + "tools": ["Read", "Glob", "Grep", "Edit", "Write", "Bash"], + "allowedTools": [], + "disallowedTools": ["WebFetch", "WebSearch", "Task"], + "permissionMode": "default", + "maxTurns": 24, + "timeoutMs": 600000, + "network": "deny", + "enableFileCheckpointing": true + }, + "trials": { + "count": 5, + "seed": 20260815, + "order": "randomized" + } +} diff --git a/packages/harness/examples/readme-compare/minimal-profile-prompt.md b/packages/harness/examples/readme-compare/minimal-profile-prompt.md new file mode 100644 index 0000000..e535b1e --- /dev/null +++ b/packages/harness/examples/readme-compare/minimal-profile-prompt.md @@ -0,0 +1,18 @@ +Create `README.md` for this package and change no other repository file. + +Ground every claim in `package.json`, `src/index.mjs`, `src/retry.mjs`, +`test/retry.test.mjs`, and `examples/basic.mjs`. Include Purpose, Installation, +Quick Start, API, Behavior, and Verification sections. The Quick Start must be +an executable ESM JavaScript example. The Installation section must show the +consumer command `npm install @fixture/retry-kit`. Document retry defaults and +abort behavior precisely. + +Do not copy claims from `docs/archive-v0.md`. Do not invent a CLI, CommonJS +support, badges, remote URLs, network behavior, or unimplemented API. Run the +repository's allowed validation before completing. + +The bounded Bash policy accepts these validation commands exactly: `npm test`, +`npm run example`, `node --test`, `node examples/basic.mjs`, +`node ./examples/basic.mjs`, `git status --short`, and `git diff --check`. +Use the repository-inspection tools available in the current runtime profile; +do not probe for hidden tools or unsupported shell commands. diff --git a/packages/harness/src/compare/index.ts b/packages/harness/src/compare/index.ts index 4371825..2851865 100644 --- a/packages/harness/src/compare/index.ts +++ b/packages/harness/src/compare/index.ts @@ -1,8 +1,11 @@ export { HarnessCompareManifestSchema, loadHarnessCompareManifest, + resolveHarnessCompareRuntime, + type HarnessCompareVariant, type HarnessCompareManifest, type LoadedHarnessCompareManifest, + type ResolvedHarnessCompareRuntime, } from "./manifest.js"; export { gradeReadmePackage, type GraderCheck, type ReadmeGrade } from "./grader.js"; export { diff --git a/packages/harness/src/compare/manifest.ts b/packages/harness/src/compare/manifest.ts index 7ffafd6..ee0497c 100644 --- a/packages/harness/src/compare/manifest.ts +++ b/packages/harness/src/compare/manifest.ts @@ -2,9 +2,14 @@ import { readFile } from "node:fs/promises"; import { isAbsolute, resolve, sep } from "node:path"; import { Type, type Static } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; +import type { QoderRuntimeProfile } from "../exec/qoder-sdk.js"; const RelativePathSchema = Type.String({ minLength: 1, maxLength: 512 }); const IdentifierSchema = Type.String({ pattern: "^[_a-zA-Z][\\w-]*$" }); +const QoderRuntimeProfileSchema = Type.Union([ + Type.Literal("qoder-default-v1"), + Type.Literal("qoder-minimal-v1"), +]); export const HarnessCompareManifestSchema = Type.Object( { @@ -14,6 +19,13 @@ export const HarnessCompareManifestSchema = Type.Object( { baseline: IdentifierSchema, candidate: IdentifierSchema }, { additionalProperties: false }, ), + runtimeProfiles: Type.Optional(Type.Object( + { + baseline: QoderRuntimeProfileSchema, + candidate: QoderRuntimeProfileSchema, + }, + { additionalProperties: false }, + )), task: Type.Object( { fixture: RelativePathSchema, @@ -54,6 +66,10 @@ export const HarnessCompareManifestSchema = Type.Object( ); export type HarnessCompareManifest = Static; +export type HarnessCompareVariant = "baseline" | "candidate"; +export type ResolvedHarnessCompareRuntime = HarnessCompareManifest["runtime"] & { + profile: QoderRuntimeProfile; +}; export interface LoadedHarnessCompareManifest { path: string; @@ -97,46 +113,75 @@ export async function loadHarnessCompareManifest(path: string): Promise !supportedTools.has(tool)); + const unsupportedTools = runtime.tools.filter((tool) => !supportedTools.has(tool)); if (unsupportedTools.length > 0) { throw new Error( - `Invalid harness-compare.v1 manifest: unsupported visible tools: ${unsupportedTools.join(", ")}.`, + `Invalid harness-compare.v1 manifest: ${label} has unsupported visible tools: ${unsupportedTools.join(", ")}.`, ); } for (const tool of requiredTools) { - if (!manifest.runtime.tools.includes(tool)) { - throw new Error(`Invalid harness-compare.v1 manifest: runtime.tools must include '${tool}'.`); + if (!runtime.tools.includes(tool)) { + throw new Error(`Invalid harness-compare.v1 manifest: ${label}.tools must include '${tool}'.`); } } - if (manifest.runtime.allowedTools.length > 0) { + if (runtime.allowedTools.length > 0) { throw new Error("Invalid harness-compare.v1 manifest: allowedTools must be empty; every tool use requires the bounded permission callback."); } for (const tool of ["WebFetch", "WebSearch"]) { - if (!manifest.runtime.disallowedTools.includes(tool)) { + if (!runtime.disallowedTools.includes(tool)) { throw new Error(`Invalid harness-compare.v1 manifest: disallowedTools must include '${tool}'.`); } } - const unavailableRequiredTools = requiredTools.filter((tool) => manifest.runtime.disallowedTools.includes(tool)); + const unavailableRequiredTools = requiredTools.filter((tool) => runtime.disallowedTools.includes(tool)); if (unavailableRequiredTools.length > 0) { throw new Error( `Invalid harness-compare.v1 manifest: required tools are also disallowed: ${unavailableRequiredTools.join(", ")}.`, ); } - for (const path of [ - manifest.harness, - manifest.task.fixture, - manifest.task.prompt, - manifest.task.grader.contract, - ...manifest.task.expectedFiles, - ]) { - assertPortableRelativePath(path); - } } function resolveOwnedPath(base: string, relativePath: string, label: string): string { diff --git a/packages/harness/src/compare/runner.ts b/packages/harness/src/compare/runner.ts index cfec3b6..ace0aab 100644 --- a/packages/harness/src/compare/runner.ts +++ b/packages/harness/src/compare/runner.ts @@ -21,8 +21,9 @@ import { resolveComposition } from "../resolver/resolve.js"; import { gradeReadmePackage, type ReadmeGrade } from "./grader.js"; import { loadHarnessCompareManifest, - type HarnessCompareManifest, + resolveHarnessCompareRuntime, type LoadedHarnessCompareManifest, + type ResolvedHarnessCompareRuntime, } from "./manifest.js"; import { createBoundedQoderPermissionCallback, @@ -34,7 +35,7 @@ export type CompareVariant = "baseline" | "candidate"; export type TrialClassification = "passed" | "failed" | "infrastructure_error"; export interface CompareExecutorContext { - runtime: HarnessCompareManifest["runtime"]; + runtime: ResolvedHarnessCompareRuntime; trialRoot: string; abortController: AbortController; permissionDecisions: ToolPermissionDecision[]; @@ -54,6 +55,7 @@ export interface FileEvidence { export interface CompareTrialResult { variant: CompareVariant; compositionId: string; + runtimeProfile: string; trial: number; classification: TrialClassification; changedFiles: string[]; @@ -212,6 +214,7 @@ async function runTrial(options: { trial: number; executorFactory: CompareExecutorFactory; }): Promise { + const runtime = resolveHarnessCompareRuntime(options.loaded.value, options.variant); const variantDirectory = options.variant === "baseline" ? "H0" : "H1"; const artifactDirectory = join( options.outputDirectory, @@ -239,7 +242,7 @@ async function runTrial(options: { await initializeGitRepository(trialRoot, temporaryParent); const abortController = new AbortController(); const executor = options.executorFactory({ - runtime: options.loaded.value.runtime, + runtime, trialRoot, abortController, permissionDecisions, @@ -249,7 +252,7 @@ async function runTrial(options: { try { execution = await withTimeout( executor.execute(options.revision, options.bundle, { prompt: options.prompt, cwd: trialRoot }), - options.loaded.value.runtime.timeoutMs, + runtime.timeoutMs, abortController, ); } catch (error) { @@ -287,6 +290,7 @@ async function runTrial(options: { const result: CompareTrialResult = { variant: options.variant, compositionId: options.compositionId, + runtimeProfile: runtime.profile, trial: options.trial, classification, changedFiles, @@ -311,6 +315,7 @@ async function runTrial(options: { function defaultExecutorFactory(context: CompareExecutorContext): HarnessExecutor { return new QoderSdkExecutor({ + profile: context.runtime.profile, tools: context.runtime.tools, allowedTools: context.runtime.allowedTools, disallowedTools: context.runtime.disallowedTools, diff --git a/packages/harness/src/exec/executor.ts b/packages/harness/src/exec/executor.ts index e7dc911..27cf687 100644 --- a/packages/harness/src/exec/executor.ts +++ b/packages/harness/src/exec/executor.ts @@ -23,6 +23,7 @@ export interface HarnessRunResult { export interface HarnessRuntimeReceipt { executor: string; + runtimeProfile?: string; tools: string[]; allowedTools: string[]; disallowedTools: string[]; @@ -32,6 +33,13 @@ export interface HarnessRuntimeReceipt { model?: string; fileCheckpointing?: boolean; permissionCallback: "configured" | "none"; + systemPromptSource?: "runtime-default" | "executor-profile"; + settingSources?: string[] | "runtime-default"; + skills?: string[] | "all" | "runtime-default"; + extensionCount?: number; + pluginCount?: number; + mcpServerNames?: string[]; + strictMcpConfig?: boolean; } export interface HarnessRunMetrics { diff --git a/packages/harness/src/exec/index.ts b/packages/harness/src/exec/index.ts index 816ec74..5e50b3b 100644 --- a/packages/harness/src/exec/index.ts +++ b/packages/harness/src/exec/index.ts @@ -17,6 +17,7 @@ export { type QoderSdkLike, type QoderSdkMessage, type QoderPermissionMode, + type QoderRuntimeProfile, type QoderToolPermissionCallback, type QoderToolPermissionResult, redactTraceValue, diff --git a/packages/harness/src/exec/qoder-sdk.ts b/packages/harness/src/exec/qoder-sdk.ts index cc2660d..6096224 100644 --- a/packages/harness/src/exec/qoder-sdk.ts +++ b/packages/harness/src/exec/qoder-sdk.ts @@ -9,6 +9,17 @@ import { const QODER_SDK_MODULE = "@qoder-ai/qoder-agent-sdk"; +export type QoderRuntimeProfile = "qoder-default-v1" | "qoder-minimal-v1"; + +const QODER_MINIMAL_TOOLS = ["Read", "Write", "Edit", "Bash"] as const; +const QODER_MINIMAL_DENIED_TOOLS = ["WebFetch", "WebSearch", "Agent", "Task"] as const; +type QoderSettingSource = "user" | "project" | "local"; +type QoderSystemPrompt = string | { + type: "preset"; + preset: "qodercli"; + append?: string; +}; + export interface QoderSdkTextBlock { type: string; text?: string; @@ -71,6 +82,13 @@ export interface QoderSdkLike { model?: string; enableFileCheckpointing?: boolean; abortController?: AbortController; + settingSources?: QoderSettingSource[]; + skills?: string[] | "all"; + extensions?: string[]; + plugins?: unknown[]; + mcpServers?: Record; + strictMcpConfig?: boolean; + systemPrompt?: QoderSystemPrompt; }; }): AsyncIterable; qodercliAuth(): unknown; @@ -99,6 +117,8 @@ export interface QoderSdkExecutorOptions { abortController?: AbortController; /** Receives each redacted SDK event as it arrives, including before a timeout. */ onTraceEvent?: (event: unknown) => void; + /** A frozen executor-owned runtime contract. Defaults to the SDK-compatible v1 behavior. */ + profile?: QoderRuntimeProfile; } /** Execute one resolved revision through the official Qoder Agent SDK. */ @@ -117,21 +137,45 @@ export class QoderSdkExecutor implements HarnessExecutor { private readonly enableFileCheckpointing: boolean; private readonly abortController?: AbortController; private readonly onTraceEvent?: (event: unknown) => void; + private readonly profile: QoderRuntimeProfile; + private readonly settingSources?: QoderSettingSource[]; + private readonly skills?: string[] | "all"; + private readonly extensions?: string[]; + private readonly plugins?: unknown[]; + private readonly mcpServers?: Record; + private readonly strictMcpConfig?: boolean; constructor(options: QoderSdkExecutorOptions = {}) { + this.profile = options.profile ?? "qoder-default-v1"; + assertSupportedProfile(this.profile); + if (this.profile === "qoder-minimal-v1") { + assertMinimalProfileOptions(options); + } this.loadSdk = () => loadQoderSdk(options.loadSdk); this.auth = options.auth ?? ((sdk) => sdk.qodercliAuth()); - this.allowedTools = [...(options.allowedTools ?? [])]; - this.tools = [...(options.tools ?? [])]; - this.disallowedTools = [...(options.disallowedTools ?? [])]; - this.permissionMode = options.permissionMode; + this.allowedTools = this.profile === "qoder-minimal-v1" ? [] : [...(options.allowedTools ?? [])]; + this.tools = this.profile === "qoder-minimal-v1" + ? [...QODER_MINIMAL_TOOLS] + : [...(options.tools ?? [])]; + this.disallowedTools = this.profile === "qoder-minimal-v1" + ? unique([...QODER_MINIMAL_DENIED_TOOLS, ...(options.disallowedTools ?? [])]) + : [...(options.disallowedTools ?? [])]; + this.permissionMode = this.profile === "qoder-minimal-v1" ? "default" : options.permissionMode; this.canUseTool = options.canUseTool; - this.persistSession = options.persistSession ?? false; + this.persistSession = this.profile === "qoder-minimal-v1" ? false : (options.persistSession ?? false); this.maxTurns = options.maxTurns ?? 1; this.model = options.model; this.enableFileCheckpointing = options.enableFileCheckpointing ?? false; this.abortController = options.abortController; this.onTraceEvent = options.onTraceEvent; + if (this.profile === "qoder-minimal-v1") { + this.settingSources = []; + this.skills = []; + this.extensions = []; + this.plugins = []; + this.mcpServers = {}; + this.strictMcpConfig = true; + } } async execute( @@ -142,6 +186,9 @@ export class QoderSdkExecutor implements HarnessExecutor { assertRevisionHost(revision, this.host); const { prompt, warnings } = buildRunPrompt(revision, bundle, task); const sdk = await this.loadSdk(); + const systemPrompt = this.profile === "qoder-minimal-v1" + ? buildQoderMinimalSystemPrompt(task.cwd) + : undefined; const sdkOptions: Parameters[0]["options"] = { auth: this.auth(sdk), cwd: task.cwd, @@ -155,6 +202,13 @@ export class QoderSdkExecutor implements HarnessExecutor { ...(this.canUseTool !== undefined ? { canUseTool: this.canUseTool } : {}), ...(this.model !== undefined ? { model: this.model } : {}), ...(this.abortController !== undefined ? { abortController: this.abortController } : {}), + ...(this.settingSources !== undefined ? { settingSources: [...this.settingSources] } : {}), + ...(this.skills !== undefined ? { skills: Array.isArray(this.skills) ? [...this.skills] : this.skills } : {}), + ...(this.extensions !== undefined ? { extensions: [...this.extensions] } : {}), + ...(this.plugins !== undefined ? { plugins: [...this.plugins] } : {}), + ...(this.mcpServers !== undefined ? { mcpServers: { ...this.mcpServers } } : {}), + ...(this.strictMcpConfig !== undefined ? { strictMcpConfig: this.strictMcpConfig } : {}), + ...(systemPrompt !== undefined ? { systemPrompt } : {}), }; const stream = sdk.query({ prompt, @@ -216,6 +270,7 @@ export class QoderSdkExecutor implements HarnessExecutor { trace, runtimeReceipt: { executor: "@qoder-ai/qoder-agent-sdk", + runtimeProfile: this.profile, tools: [...this.tools], allowedTools: [...this.allowedTools], disallowedTools: [...this.disallowedTools], @@ -225,12 +280,70 @@ export class QoderSdkExecutor implements HarnessExecutor { ...(this.model !== undefined ? { model: this.model } : {}), fileCheckpointing: this.enableFileCheckpointing, permissionCallback: this.canUseTool ? "configured" : "none", + systemPromptSource: systemPrompt === undefined ? "runtime-default" : "executor-profile", + settingSources: this.settingSources === undefined ? "runtime-default" : [...this.settingSources], + skills: this.skills === undefined + ? "runtime-default" + : Array.isArray(this.skills) ? [...this.skills] : this.skills, + ...(this.extensions !== undefined ? { extensionCount: this.extensions.length } : {}), + ...(this.plugins !== undefined ? { pluginCount: this.plugins.length } : {}), + ...(this.mcpServers !== undefined ? { mcpServerNames: Object.keys(this.mcpServers).sort() } : {}), + ...(this.strictMcpConfig !== undefined ? { strictMcpConfig: this.strictMcpConfig } : {}), }, ...(metrics !== undefined ? { metrics } : {}), }; } } +function assertSupportedProfile(profile: QoderRuntimeProfile): void { + if (profile !== "qoder-default-v1" && profile !== "qoder-minimal-v1") { + throw new Error(`Unsupported Qoder runtime profile '${String(profile)}'.`); + } +} + +function assertMinimalProfileOptions(options: QoderSdkExecutorOptions): void { + if (options.tools !== undefined && !sameToolSet(options.tools, QODER_MINIMAL_TOOLS)) { + throw new Error(`qoder-minimal-v1 fixes the visible tools to: ${QODER_MINIMAL_TOOLS.join(", ")}.`); + } + if ((options.allowedTools?.length ?? 0) > 0) { + throw new Error("qoder-minimal-v1 does not permit auto-approved tools; use canUseTool for bounded decisions."); + } + const minimalTools = new Set(QODER_MINIMAL_TOOLS); + const deniedRequired = options.disallowedTools?.filter((tool) => minimalTools.has(tool)) ?? []; + if (deniedRequired.length > 0) { + throw new Error(`qoder-minimal-v1 cannot disallow required tools: ${deniedRequired.join(", ")}.`); + } + if (options.permissionMode !== undefined && options.permissionMode !== "default") { + throw new Error("qoder-minimal-v1 fixes permissionMode to 'default'."); + } + if (options.persistSession === true) { + throw new Error("qoder-minimal-v1 requires an ephemeral session."); + } + if (options.canUseTool === undefined) { + throw new Error("qoder-minimal-v1 requires canUseTool so every tool call has a host-owned decision."); + } +} + +function sameToolSet(actual: string[], expected: readonly string[]): boolean { + return actual.length === expected.length && expected.every((tool) => actual.includes(tool)); +} + +function unique(values: readonly string[]): string[] { + return [...new Set(values)]; +} + +function buildQoderMinimalSystemPrompt(cwd?: string): string { + const workingDirectory = cwd ?? process.cwd(); + return [ + "You are a focused coding agent operating inside the host-supplied working directory.", + `The working directory is ${JSON.stringify(workingDirectory)}. Use relative paths such as package.json; do not guess other absolute roots.`, + "Use only the Read, Write, Edit, and Bash tools exposed by the host.", + "Inspect repository evidence before editing, keep changes inside the requested scope, and verify the result.", + "Use Bash only for validation commands explicitly allowed by the task or host policy.", + "Do not access the network, invent repository facts, or report success without concrete validation evidence.", + ].join("\n"); +} + const SECRET_FIELD = /(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|service[_-]?account[_-]?key|secret|credential)/i; /** Recursively remove credential-shaped fields before persisting SDK events. */ diff --git a/packages/harness/test/compare.test.ts b/packages/harness/test/compare.test.ts index 47c5af7..039a992 100644 --- a/packages/harness/test/compare.test.ts +++ b/packages/harness/test/compare.test.ts @@ -4,11 +4,15 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import type { HarnessExecutor } from "../src/exec/executor.js"; import { gradeReadmePackage } from "../src/compare/grader.js"; -import { loadHarnessCompareManifest } from "../src/compare/manifest.js"; +import { loadHarnessCompareManifest, resolveHarnessCompareRuntime } from "../src/compare/manifest.js"; import { createBoundedQoderPermissionCallback, type ToolPermissionDecision } from "../src/compare/permissions.js"; import { runHarnessComparison } from "../src/compare/runner.js"; const EXPERIMENT_URL = new URL("../examples/readme-compare/experiment.json", import.meta.url); +const MINIMAL_EXPERIMENT_URL = new URL( + "../examples/readme-compare/minimal-profile-experiment.json", + import.meta.url, +); const temporaryDirectories: string[] = []; afterEach(async () => { @@ -38,6 +42,23 @@ describe("harness compare manifest", () => { await expect(loadHarnessCompareManifest(path)).rejects.toThrow(/allowedTools must be empty/); }); + + it("accepts an isolated same-composition comparison when runtime profiles differ", async () => { + const loaded = await loadHarnessCompareManifest(MINIMAL_EXPERIMENT_URL.pathname); + + expect(loaded.value.variants).toEqual({ + baseline: "readme-grounded", + candidate: "readme-grounded", + }); + expect(resolveHarnessCompareRuntime(loaded.value, "baseline")).toMatchObject({ + profile: "qoder-default-v1", + tools: ["Read", "Glob", "Grep", "Edit", "Write", "Bash"], + }); + expect(resolveHarnessCompareRuntime(loaded.value, "candidate")).toMatchObject({ + profile: "qoder-minimal-v1", + tools: ["Read", "Write", "Edit", "Bash"], + }); + }); }); describe("bounded Qoder permissions", () => { @@ -138,6 +159,60 @@ describe("README coding comparison", () => { expect(await readFile(join(output, "verdict.html"), "utf8")).toContain("Harness compare verdict"); await expect(readFile(fixtureReadme, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); }); + + it("routes each variant through its resolved Qoder runtime profile", async () => { + const directory = await makeTemporaryDirectory(); + const output = join(directory, "evidence"); + const observed: Array<{ profile: string; tools: string[] }> = []; + + const verdict = await runHarnessComparison({ + manifestPath: MINIMAL_EXPERIMENT_URL.pathname, + outputDirectory: output, + trialCount: 1, + executorFactory: (context): HarnessExecutor => { + observed.push({ profile: context.runtime.profile, tools: [...context.runtime.tools] }); + return { + host: "qoder", + execute: async (revision) => { + await writeFile(join(context.trialRoot, "README.md"), VALID_README, "utf8"); + return { + host: "qoder", + revisionId: revision.revisionId, + exitCode: 0, + output: "completed", + errorOutput: "", + warnings: [], + runtimeReceipt: { + executor: "injected-test-executor", + runtimeProfile: context.runtime.profile, + tools: [...context.runtime.tools], + allowedTools: [], + disallowedTools: [...context.runtime.disallowedTools], + permissionCallback: "configured", + }, + }; + }, + }; + }, + }); + + expect(observed).toEqual(expect.arrayContaining([ + { + profile: "qoder-default-v1", + tools: ["Read", "Glob", "Grep", "Edit", "Write", "Bash"], + }, + { + profile: "qoder-minimal-v1", + tools: ["Read", "Write", "Edit", "Bash"], + }, + ])); + expect(verdict.trials).toEqual(expect.arrayContaining([ + expect.objectContaining({ variant: "baseline", runtimeProfile: "qoder-default-v1" }), + expect.objectContaining({ variant: "candidate", runtimeProfile: "qoder-minimal-v1" }), + ])); + expect(JSON.parse(await readFile(join(output, "H1/trial-001/runtime-receipt.json"), "utf8"))) + .toMatchObject({ runtimeProfile: "qoder-minimal-v1", tools: ["Read", "Write", "Edit", "Bash"] }); + }); }); async function makeTemporaryDirectory(): Promise { diff --git a/packages/harness/test/exec.test.ts b/packages/harness/test/exec.test.ts index 3f4bfab..8859f50 100644 --- a/packages/harness/test/exec.test.ts +++ b/packages/harness/test/exec.test.ts @@ -193,6 +193,92 @@ describe("QoderSdkExecutor", () => { }); }); + it("materializes the frozen qoder-minimal-v1 SDK surface and receipt", async () => { + const { bundle, revision } = await resolveFor("on-qoder"); + const queries: Parameters[0][] = []; + const canUseTool = async () => ({ behavior: "allow" as const }); + const sdk: QoderSdkLike = { + qodercliAuth: () => ({ kind: "test-auth" }), + query: async function* (params) { + queries.push(params); + yield { type: "result", subtype: "success" }; + }, + }; + const executor = new QoderSdkExecutor({ + profile: "qoder-minimal-v1", + loadSdk: async () => sdk, + tools: ["Bash", "Edit", "Read", "Write"], + allowedTools: [], + disallowedTools: ["WebFetch", "WebSearch", "Task"], + permissionMode: "default", + canUseTool, + maxTurns: 8, + }); + + const result = await executor.execute(revision, bundle, { prompt: "Create README.md", cwd: "/tmp" }); + + expect(queries).toHaveLength(1); + expect(queries[0].options).toMatchObject({ + tools: ["Read", "Write", "Edit", "Bash"], + allowedTools: [], + disallowedTools: ["WebFetch", "WebSearch", "Agent", "Task"], + permissionMode: "default", + persistSession: false, + settingSources: [], + skills: [], + extensions: [], + plugins: [], + mcpServers: {}, + strictMcpConfig: true, + systemPrompt: expect.stringContaining("focused coding agent"), + }); + expect(queries[0].options.systemPrompt).toEqual(expect.stringContaining('working directory is "/tmp"')); + expect(queries[0].options.systemPrompt).toEqual(expect.stringContaining("Use relative paths such as package.json")); + expect(result.runtimeReceipt).toEqual(expect.objectContaining({ + runtimeProfile: "qoder-minimal-v1", + tools: ["Read", "Write", "Edit", "Bash"], + allowedTools: [], + systemPromptSource: "executor-profile", + settingSources: [], + skills: [], + extensionCount: 0, + pluginCount: 0, + mcpServerNames: [], + strictMcpConfig: true, + permissionCallback: "configured", + })); + expect(JSON.stringify(result.runtimeReceipt)).not.toContain("focused coding agent"); + }); + + it("rejects options that weaken qoder-minimal-v1 before loading the SDK", async () => { + let loaded = false; + const loadSdk = async (): Promise => { + loaded = true; + throw new Error("must not load"); + }; + + expect(() => new QoderSdkExecutor({ + profile: "qoder-minimal-v1", + loadSdk, + allowedTools: ["Read"], + })).toThrow(/does not permit auto-approved tools/); + expect(() => new QoderSdkExecutor({ + profile: "qoder-minimal-v1", + loadSdk, + tools: ["Read", "Glob", "Grep", "Edit", "Write", "Bash"], + })).toThrow(/fixes the visible tools/); + expect(() => new QoderSdkExecutor({ + profile: "qoder-minimal-v1", + loadSdk, + persistSession: true, + })).toThrow(/requires an ephemeral session/); + expect(() => new QoderSdkExecutor({ + profile: "qoder-minimal-v1", + loadSdk, + })).toThrow(/requires canUseTool/); + expect(loaded).toBe(false); + }); + it("reports declared native strength as an advisory degradation", async () => { const { bundle, revision } = await resolveFor("on-qoder"); const sdk: QoderSdkLike = { From b8b80b5d83e23017463c562d82926fbe3c6671d4 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 15 Aug 2026 11:40:14 +0800 Subject: [PATCH 05/25] feat(harness): add progressive-disclosure DSL sugar and fix comparison defects Make the smallest useful .harness document five lines: a bare materializes a component as advisory prompt guidance, mechanism and strength are optional, one binding can name several hosts, and a composition can extend another. The README now teaches that minimal form first and routes to the full surface only when an author needs it. --- .gitignore | 2 + .../2026-08-15-harness-coding-compare.md | 50 +++ packages/harness/README.md | 49 ++- packages/harness/examples/minimal.harness | 14 + .../harness/examples/readme-compare/prompt.md | 22 +- .../readme-compare/readme-compare.harness | 6 +- .../harness/examples/standard-coding.harness | 20 +- .../skills/generate-harness-dsl/SKILL.md | 12 +- .../references/dsl-contract.md | 68 +++- packages/harness/src/compare/grader.ts | 71 +++- packages/harness/src/compare/manifest.ts | 13 +- packages/harness/src/compare/process.ts | 49 ++- packages/harness/src/compare/runner.ts | 205 ++++++---- packages/harness/src/compiler/compile.ts | 153 +++++++- packages/harness/src/exec/qoder-sdk.ts | 31 +- .../harness/src/highlight/harness-grammar.ts | 5 +- .../harness/src/language/generated/ast.ts | 40 +- .../harness/src/language/generated/grammar.ts | 364 ++++++++++++------ .../harness/src/language/harness-validator.ts | 18 +- packages/harness/src/language/harness.langium | 37 +- packages/harness/src/resolver/resolve.ts | 46 ++- packages/harness/test/compare.test.ts | 100 ++++- packages/harness/test/sugar.test.ts | 142 +++++++ 23 files changed, 1189 insertions(+), 328 deletions(-) create mode 100644 packages/harness/examples/minimal.harness create mode 100644 packages/harness/test/sugar.test.ts diff --git a/.gitignore b/.gitignore index 2258779..c387213 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ skills/**/.plugin-eval/ assets/wasm dev/terminal-demo/*.cast /.codex/better-harness/ +/harness-readme-compare-evidence/ +/harness-qoder-profile-evidence/ design-qa.md diff --git a/docs/specs/2026-08-15-harness-coding-compare.md b/docs/specs/2026-08-15-harness-coding-compare.md index b059d3a..7ba7a30 100644 --- a/docs/specs/2026-08-15-harness-coding-compare.md +++ b/docs/specs/2026-08-15-harness-coding-compare.md @@ -124,3 +124,53 @@ argument arrays for cross-platform execution and never invokes a shell string. - Risk: SDK messages may contain sensitive data. Persist structured protocol events only after recursively redacting token/key/authorization fields; never store injected credentials in manifests, receipts, traces, or fixtures. + +## Post-Review Corrections + +A review of the merged change found eight defects. All are fixed, and each +behavioral fix is covered by a test in the package suite. + +1. Validation commands spawned `npm` without a shell, which cannot start the + Windows `npm.cmd` shim and aborted the whole comparison. `npmInvocation()` + now resolves an `npm-cli.js` entry point, and a test runs it on the host. +2. A timed-out command killed only the direct child. Commands now run in their + own process group and are stopped together with their children. +3. The shared task prompt stated the required README sections, grounding rules, + install command, and validation duty, so the baseline received the candidate + composition's content and the native AC-6 run tied at score 100. The prompt + now states only the goal and the runtime tool policy; the documentation + standard moved into the candidate composition's components. The runtime + profile experiment keeps its explicit prompt because both of its arms share + one composition. +4. Tests derived filesystem paths from `URL.pathname`, which is not a valid path + on Windows. They now use `fileURLToPath()`. +5. Trace redaction shared one cycle set across sibling values, so an object + referenced twice was recorded as `[Circular]`. Only the ancestor chain is + tracked now. +6. The grader imported the agent-modified package entry point into its own + process. Exports are now read by a separate `--permission` Node process, and + a test proves a tampered entry point is graded without executing inside the + grader. +7. Fixture copy, Git setup, patch capture, and grader breakage propagated out of + a trial and discarded the whole run. Such failures are now recorded as that + trial's `infrastructure_error` with retained evidence. +8. `runtime.network` was validated but never used, and `metrics.json` duplicated + the full check list. Network denial now drives the web-tool requirement, and + `metrics.json` keeps the grade summary while `validation.json` keeps checks. + +The AC-6 native manifest, harness, and fixture hashes above belong to the +pre-correction prompt and harness files. They remain the honest record of that +run, but they no longer describe the current experiment: a fresh native run is +required before any comparative claim, and the baseline is now expected to score +lower than the candidate rather than tie. + +Re-verified after the corrections: `npm run harness:generated`, +`npm run harness:build`, `npm run harness:test` (60 package tests), +`npx vitest run test/skills-docs/doc-link-graph.test.mjs` (6 assertions), and +root `npm test` (1,322 of 1,324; `test/plugins/host-support.test.mjs` and +`test/reporting/report-source-review.test.mjs` hit the 120 s limit under full +suite load and both pass in isolation). `npm run harness:generated` and +`npm pack --dry-run` were not re-established after these corrections: a separate +uncommitted change to the grammar, resolver, and `test/sugar.test.ts` is in +flight in the same worktree, so the generated-source diff is dirty and one sugar +test is red. Re-run both gates once that change is complete. diff --git a/packages/harness/README.md b/packages/harness/README.md index 8ba0f64..b1c90b6 100644 --- a/packages/harness/README.md +++ b/packages/harness/README.md @@ -7,6 +7,40 @@ language across agent runtimes — v0.1 answers one bounded question: > What did the composition request, what strength did its binding declare, and > what did the selected v0.1 executor actually materialize? +## The five-line harness + +You do not need the full assembly model to start. The smallest useful document +is one capability plus a composition that requires it — no plugin, binding, or +strength boilerplate ([`examples/minimal.harness`](examples/minimal.harness)): + +```harness +component require-tests { + kind policy + description "Do not report the task complete until tests or a diff review prove it." +} + +composition my-agent { + target qoder + require require-tests +} +``` + +A bare `require` materializes the component as advisory prompt guidance — the +v0.1 floor — so this resolves and runs as-is. The rest of the language is +*progressive disclosure*: reach for it only when you need more. + +| When you need… | Add… | +| --- | --- | +| A stronger declared mechanism, or an explicit `unsupported` | a `binding` (its `mechanism`/`strength` default to advisory prompt guidance) | +| The same binding on several hosts | `binding x for [pi, qoder] { … }` | +| A distribution boundary / versioned bundle | a `plugin` with `include [ plugin.x@^1 ]` | +| A stricter floor or fatal degradation | a `require x { minimum … on-degrade fail }` block | +| The same assembly on another host | `composition y extends x { target … }` | + +Authors who want the whole surface at once should read +[`examples/standard-coding.harness`](examples/standard-coding.harness); the +sections below document the resolved IR those forms lower into. + ## Pipeline ```text @@ -105,6 +139,16 @@ The command uses the official Qoder Agent SDK and the locally signed-in smoke; use all five trials before treating the result as comparative evidence. The README task is one benchmark, not proof of general coding performance. +The shared task prompt states only the goal and the runtime tool policy. What a +good README contains — required sections, grounding rules, the consumer install +command, the executable Quick Start — lives in the candidate composition's +components, which the executor materializes as harness guidance. Keeping that +expectation out of the prompt is what makes the measured difference attributable +to the composition, so the baseline is expected to score lower. The runtime +profile experiment below is the opposite case: its two arms share one +composition, so its prompt states the task in full and only the runtime profile +varies. + To isolate runtime-tool effects from harness-policy effects, the companion profile experiment holds the grounded composition and task constant. Its baseline uses the manifest's six bounded coding tools; its candidate uses the @@ -133,7 +177,10 @@ network commands, command chaining, repository escapes, and unknown tools fail closed. Credentials are supplied by the SDK authentication adapter and are not written to manifests, receipts, traces, or fixtures. Generated Quick Start code is screened for host capabilities, receives a secret-free environment, and runs -in a separate Node permission-model process with read-only fixture access. +in a separate Node permission-model process with read-only fixture access. The +graded package's own entry point is read the same way, so agent-modified code +never loads inside the grader. Validation commands run without a shell, and a +timed-out command is stopped together with the processes it started. ## AI authoring skill diff --git a/packages/harness/examples/minimal.harness b/packages/harness/examples/minimal.harness new file mode 100644 index 0000000..1b6cab4 --- /dev/null +++ b/packages/harness/examples/minimal.harness @@ -0,0 +1,14 @@ +// The smallest useful harness. One policy, applied as advisory prompt +// guidance — no plugin, binding, or strength boilerplate. A bare `require` +// asks for the component at the v0.1 floor; the omitted binding is exactly +// what a hand-written `binding … { strength advisory }` would declare. + +component require-tests { + kind policy + description "Do not report the task complete until tests or a diff review prove it." +} + +composition my-agent { + target qoder + require require-tests +} diff --git a/packages/harness/examples/readme-compare/prompt.md b/packages/harness/examples/readme-compare/prompt.md index 544254e..acdc2ea 100644 --- a/packages/harness/examples/readme-compare/prompt.md +++ b/packages/harness/examples/readme-compare/prompt.md @@ -1,17 +1,11 @@ -Create `README.md` for this package and change no other repository file. +Create `README.md` for this package. Change no other repository file. -Ground every claim in the current `package.json`, public source, tests, and -working example. Include Purpose, Installation, Quick Start, API, Behavior, and -Verification sections. The Quick Start must be an executable ESM JavaScript -example. The Installation section must show the consumer command -`npm install @fixture/retry-kit`. Document retry defaults and abort behavior -precisely. - -Do not copy claims from explicitly archived documentation. Do not invent a CLI, -CommonJS support, badges, remote URLs, network behavior, or unimplemented API. -Run the repository's allowed validation before completing. - -The bounded Bash policy accepts these validation commands exactly: `npm test`, +Runtime policy for this environment, identical for every composition: the +bounded Bash policy accepts these validation commands exactly: `npm test`, `npm run example`, `node --test`, `node examples/basic.mjs`, `node ./examples/basic.mjs`, `git status --short`, and `git diff --check`. -Use the dedicated Read, Glob, and Grep tools for repository inspection. +Every other command is denied. Use the dedicated Read, Glob, and Grep tools for +repository inspection. + +What a good README must contain is not stated here: that expectation belongs to +the harness under comparison, not to the shared task. diff --git a/packages/harness/examples/readme-compare/readme-compare.harness b/packages/harness/examples/readme-compare/readme-compare.harness index 57e825f..5f9a6ff 100644 --- a/packages/harness/examples/readme-compare/readme-compare.harness +++ b/packages/harness/examples/readme-compare/readme-compare.harness @@ -8,7 +8,7 @@ component coding-loop { component repository-grounding { kind skill - description "Before writing documentation, derive claims from package metadata, public source, executable examples, and current tests; treat archived documentation as stale evidence." + description "Before writing documentation, derive every claim from package metadata, public source, executable examples, and current tests; treat archived documentation as stale evidence; state the real retry defaults and abort behavior precisely instead of approximating them." input { package-metadata public-source tests examples } output { fact-map } permissions { workspace read network deny } @@ -16,7 +16,7 @@ component repository-grounding { component readme-only-scope { kind policy - description "Change only README.md. Do not invent commands, module formats, badges, URLs, defaults, or capabilities that current repository evidence does not support." + description "Change only README.md, and give it exactly the sections a consumer needs in this order: Purpose, Installation, Quick Start, API, Behavior, Verification. Installation shows the consumer command `npm install `. Do not invent commands, module formats, badges, URLs, defaults, or capabilities that current repository evidence does not support." input { fact-map } output { scoped-readme } permissions { workspace write network deny } @@ -24,7 +24,7 @@ component readme-only-scope { component executable-readme-verification { kind workflow - description "Run the package tests and execute the README quick-start example before completing; repair documentation until both pass." + description "Write Quick Start as an executable ESM JavaScript example that imports the package by name, then run the package tests and that example before completing; repair the documentation until both pass." input { scoped-readme } output { verification } permissions { process allow workspace read network deny } diff --git a/packages/harness/examples/standard-coding.harness b/packages/harness/examples/standard-coding.harness index 01553e5..fc69dc6 100644 --- a/packages/harness/examples/standard-coding.harness +++ b/packages/harness/examples/standard-coding.harness @@ -72,22 +72,8 @@ composition standard-coding { } } -composition standard-coding-qoder { +// Same assembly, different host. `extends` inherits the includes, +// requirements, and configuration above; only the target changes. +composition standard-coding-qoder extends standard-coding { target qoder - - include [ - plugin.repository-understanding@^1, - plugin.change-verification@^2 - ] - - require verification-before-complete { - preferred enforced - minimum advisory - on-degrade report - } - - configure { - shell.timeout = 60s - tool-call-budget = 80 - } } diff --git a/packages/harness/skills/generate-harness-dsl/SKILL.md b/packages/harness/skills/generate-harness-dsl/SKILL.md index 7ee1d23..3f51ad8 100644 --- a/packages/harness/skills/generate-harness-dsl/SKILL.md +++ b/packages/harness/skills/generate-harness-dsl/SKILL.md @@ -15,9 +15,12 @@ what the v0.1 runtime actually materializes. minimum and preferred strengths, degradation behavior, and configuration. Infer low-risk details from context; ask only when a missing choice changes the safety or meaning of the composition. -2. Read [the DSL contract](references/dsl-contract.md) before authoring. Open - [the complete example](../../examples/standard-coding.harness) only when a - full syntax example is useful. +2. Read [the DSL contract](references/dsl-contract.md) before authoring. Start + from the smallest form that expresses the intent — often a component plus a + composition with a bare `require`, as in + [the minimal example](../../examples/minimal.harness). Open + [the complete example](../../examples/standard-coding.harness) only when + plugins, explicit bindings, or degradation policy are actually needed. 3. Generate one self-contained `.harness` document unless the user explicitly requests a fragment. Declare each referenced component and plugin in the same document. @@ -29,6 +32,9 @@ what the v0.1 runtime actually materializes. ## Authoring Rules +- Prefer the least syntax that expresses the intent. Add a `binding`, `plugin`, + or a `require` block only when the default advisory floor is insufficient; + reuse an assembly across hosts with `extends` rather than copying it. - Use descriptive, stable identifiers and exact semantic versions for plugin declarations. Use a compatible version range in each composition include. - Declare every component referenced by a binding, plugin, or requirement. diff --git a/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md b/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md index 7eed865..3bfeca5 100644 --- a/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md +++ b/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md @@ -12,12 +12,32 @@ owned by `packages/harness/src/`; this file is a compact generation guide. ## Document model -A complete document normally declares, in dependency order: +The smallest useful document is a component plus a composition that requires it: + +```harness +component require-tests { + kind policy + description "Do not report the task complete until tests prove it." +} + +composition my-agent { + target qoder + require require-tests +} +``` + +A bare `require` with no plugin or binding materializes the component as +advisory prompt guidance — the v0.1 floor. Reach for the fuller model below only +when you need distribution boundaries or a stronger declared mechanism: 1. `component`: a host-independent capability contract. -2. `binding`: how one host declares that it can realize a component. -3. `plugin`: an exact-version distribution that provides components. -4. `composition`: the target host, plugin ranges, requirements, and settings. +2. `binding` (optional): how one host declares that it can realize a component. + Omit it to accept the advisory floor; write it to declare a stronger future + mechanism or an explicit `unsupported`. +3. `plugin` (optional): an exact-version distribution that provides components. + Omit it when a composition `require`s the component directly. +4. `composition`: the target host, optional plugin ranges, requirements, and + settings. Comments use `//` or `/* ... */`. Whitespace is insignificant. Identifiers must match `[_a-zA-Z][\w-]*`; prefer descriptive kebab-case identifiers and avoid @@ -60,6 +80,14 @@ binding repository-analysis for qoder { The host and mechanism are identifiers. Strength is `unsupported`, `advisory`, `wired`, or `enforced`. Declare at most one binding per component and host. +`mechanism` and `strength` are optional and default to `prompt-preamble` at +`advisory` — an empty `binding x for pi {}` is the advisory floor. Bind several +hosts with the same declaration using a list: + +```harness +binding repository-analysis for [pi, qoder] { strength advisory } +``` + A binding strength states the strongest host capability being modeled. It does not prove that the current executor materialized that capability. In v0.1, a supported binding resolves through `prompt-preamble` at `advisory`; a missing or @@ -103,8 +131,14 @@ composition repository-change { Include ranges accept a numeric version optionally prefixed by `^` or `~`, for example `@1`, `@^1`, `@~1.2`, or `@1.2.0`. Do not include the same plugin twice. -Each requirement needs `minimum`. `preferred` and `on-degrade` are optional; -the degradation policy defaults to `report`. Resolution behaves as follows: +`include` is optional: a composition may `require` a component declared in the +document directly, with no plugin wrapper. `target` is required unless inherited +through `extends`. + +A requirement block is optional. A bare `require component` asks for the +component at `minimum advisory` with `on-degrade report`. Add the block to raise +`minimum`, state a `preferred` ceiling, or set `on-degrade fail`. Resolution +behaves as follows: - realized below minimum: fail; - realized below preferred with `on-degrade fail`: fail; @@ -115,14 +149,30 @@ Configuration keys are dotted or hyphenated identifiers. Values may be non-negative integers, booleans, strings, or integer durations ending in `ms`, `s`, `m`, or `h`. +Reuse one assembly across hosts with `extends` instead of copying it. The child +inherits the base target, includes, requirements, and settings, and overrides +`target` (and any member sharing a plugin id, component id, or setting key): + +```harness +composition on-qoder extends repository-change { + target qoder +} +``` + +The `extends` chain must be acyclic, and every composition must end up with a +`target`, its own or inherited. + ## Semantic checklist -- Declare every referenced component and plugin. +- Declare every referenced component. Declare a plugin only when you need a + distribution boundary; a directly `require`d component needs none. - Give every plugin declaration an exact semantic version. - Keep declaration ids, bindings, includes, requirements, inputs, outputs, and configuration keys unique within their applicable scope. -- Include a plugin that provides each required component. -- Bind required components for the composition target. +- Bind required components for the composition target only to declare a stronger + mechanism or an explicit `unsupported`; the advisory floor needs no binding. +- Give every composition a `target`, directly or through `extends`, with no + `extends` cycle. - Set minimum and degradation policy against v0.1's actual advisory ceiling. - Keep secrets out of source and descriptions. - Run `scripts/validate.mjs` and inspect every realization, not only the exit diff --git a/packages/harness/src/compare/grader.ts b/packages/harness/src/compare/grader.ts index aee4722..500390f 100644 --- a/packages/harness/src/compare/grader.ts +++ b/packages/harness/src/compare/grader.ts @@ -3,7 +3,7 @@ import { dirname, isAbsolute, relative, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { Type, type Static } from "@sinclair/typebox"; import { Value } from "@sinclair/typebox/value"; -import { runCommand, type CommandResult } from "./process.js"; +import { npmInvocation, runCommand, type CommandResult } from "./process.js"; const ReadmeGraderContractSchema = Type.Object( { @@ -103,16 +103,23 @@ export async function gradeReadmePackage(options: { `Expected an installation command containing '${installNeedle}'.`, )); - const moduleUrl = `${pathToFileURL(resolve(options.trialRoot, "src/index.mjs")).href}?grade=${Date.now()}`; - const publicModule = await import(moduleUrl) as Record; - const missingExports = contract.publicApi.filter((name) => !(name in publicModule)); + const realTrialRoot = await realpath(options.trialRoot); + const publicExports = await readModuleExports(options.trialRoot, realTrialRoot); + const missingExports = contract.publicApi.filter((name) => !publicExports.names.includes(name)); const undocumentedExports = contract.publicApi.filter((name) => !allCode.includes(name)); - checks.push(check( - "public-api", - missingExports.length === 0 && undocumentedExports.length === 0, - 15, - `missing exports=${JSON.stringify(missingExports)} undocumented=${JSON.stringify(undocumentedExports)}`, - )); + checks.push({ + ...check( + "public-api", + missingExports.length === 0 && undocumentedExports.length === 0, + 15, + publicExports.result.exitCode === 0 + ? `missing exports=${JSON.stringify(missingExports)} undocumented=${JSON.stringify(undocumentedExports)}` + : "The package entry point could not be loaded in the restricted export probe.", + ), + ...(publicExports.result.exitCode !== 0 + ? { command: redactCommandResult(publicExports.result, options.trialRoot, realTrialRoot) } + : {}), + }); const missingTokens = contract.requiredCodeTokens.filter((token) => !allCode.includes(token)); const missingAlternatives = contract.requiredAnyCodeTokens.filter( @@ -134,7 +141,6 @@ export async function gradeReadmePackage(options: { (block) => normalizeText(block.section ?? "") === "quick start" && contract.exampleLanguages.includes(block.language), ); const exampleResults: CommandResult[] = []; - const realTrialRoot = await realpath(options.trialRoot); for (let index = 0; index < quickStartBlocks.length; index += 1) { const temporaryExample = resolve(options.trialRoot, `.harness-readme-example-${index}.mjs`); try { @@ -178,7 +184,11 @@ export async function gradeReadmePackage(options: { ...(exampleResults[0] ? { command: redactCommandResult(exampleResults[0], options.trialRoot, realTrialRoot) } : {}), }); - const testResult = await runCommand("npm", ["test"], { cwd: options.trialRoot, timeoutMs: 60_000 }); + const npmTest = npmInvocation(["test"]); + const testResult = await runCommand(npmTest.command, npmTest.args, { + cwd: options.trialRoot, + timeoutMs: 60_000, + }); checks.push({ ...check("package-tests", testResult.exitCode === 0 && !testResult.timedOut, 10, "Ran the fixture's existing test contract."), command: redactCommandResult(testResult, options.trialRoot), @@ -195,6 +205,39 @@ export async function gradeReadmePackage(options: { return summarize(checks); } +/** + * Read the package's real exports from a separate permission-restricted process. + * + * The graded repository is agent-modified, so its module must never be imported + * into the grader itself. + */ +async function readModuleExports( + trialRoot: string, + realTrialRoot: string, +): Promise<{ names: string[]; result: CommandResult }> { + const moduleUrl = pathToFileURL(resolve(realTrialRoot, "src/index.mjs")).href; + const probe = + `const module = await import(${JSON.stringify(moduleUrl)});\n` + + `process.stdout.write(JSON.stringify(Object.keys(module)));\n`; + const result = await runCommand( + process.execPath, + ["--permission", `--allow-fs-read=${realTrialRoot}`, "--input-type=module", "--eval", probe], + { cwd: trialRoot, timeoutMs: 30_000, env: safeExampleEnvironment() }, + ); + if (result.exitCode !== 0) { + return { names: [], result }; + } + try { + const parsed = JSON.parse(result.stdout) as unknown; + return { + names: Array.isArray(parsed) ? parsed.filter((name): name is string => typeof name === "string") : [], + result, + }; + } catch { + return { names: [], result }; + } +} + async function loadContract(path: string): Promise { const value = JSON.parse(await readFile(path, "utf8")) as unknown; if (!Value.Check(ReadmeGraderContractSchema, value)) { @@ -287,7 +330,9 @@ function redactCommandResult(result: CommandResult, root: string, realRoot = roo value.replaceAll(realRoot, "").replaceAll(root, ""); return { ...result, - command: result.command.map((value, index) => index === 0 && value === process.execPath ? "node" : replaceRoot(value)), + command: result.command.map((value, index) => + index === 0 && value === process.execPath ? "node" : replaceRoot(value), + ), stdout: replaceRoot(result.stdout), stderr: replaceRoot(result.stderr), }; diff --git a/packages/harness/src/compare/manifest.ts b/packages/harness/src/compare/manifest.ts index ee0497c..8aa7839 100644 --- a/packages/harness/src/compare/manifest.ts +++ b/packages/harness/src/compare/manifest.ts @@ -11,6 +11,9 @@ const QoderRuntimeProfileSchema = Type.Union([ Type.Literal("qoder-minimal-v1"), ]); +/** Host tools that reach the network and must be unavailable under a denied network. */ +const NETWORK_TOOLS = ["WebFetch", "WebSearch"]; + export const HarnessCompareManifestSchema = Type.Object( { schemaVersion: Type.Literal("harness-compare.v1"), @@ -171,9 +174,13 @@ function validateRuntimePolicy(runtime: HarnessCompareManifest["runtime"], label if (runtime.allowedTools.length > 0) { throw new Error("Invalid harness-compare.v1 manifest: allowedTools must be empty; every tool use requires the bounded permission callback."); } - for (const tool of ["WebFetch", "WebSearch"]) { - if (!runtime.disallowedTools.includes(tool)) { - throw new Error(`Invalid harness-compare.v1 manifest: disallowedTools must include '${tool}'.`); + if (runtime.network === "deny") { + for (const tool of NETWORK_TOOLS) { + if (!runtime.disallowedTools.includes(tool)) { + throw new Error( + `Invalid harness-compare.v1 manifest: network 'deny' requires disallowedTools to include '${tool}'.`, + ); + } } } const unavailableRequiredTools = requiredTools.filter((tool) => runtime.disallowedTools.includes(tool)); diff --git a/packages/harness/src/compare/process.ts b/packages/harness/src/compare/process.ts index dd05395..4b1d623 100644 --- a/packages/harness/src/compare/process.ts +++ b/packages/harness/src/compare/process.ts @@ -1,4 +1,6 @@ -import { spawn } from "node:child_process"; +import { spawn, type ChildProcess } from "node:child_process"; +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; export interface CommandResult { command: string[]; @@ -10,6 +12,21 @@ export interface CommandResult { } const MAX_CAPTURE_BYTES = 1_000_000; +const KILL_ESCALATION_MS = 2_000; + +/** + * Resolve `npm` without a shell so validation commands also run on Windows, + * where the `npm` entry point is a `.cmd` shim that `spawn` cannot execute. + */ +export function npmInvocation(args: string[]): { command: string; args: string[] } { + const candidates = [ + process.env.npm_execpath, + join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"), + join(dirname(process.execPath), "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"), + ]; + const cli = candidates.find((candidate) => candidate && candidate.endsWith(".js") && existsSync(candidate)); + return cli ? { command: process.execPath, args: [cli, ...args] } : { command: "npm", args }; +} export async function runCommand( command: string, @@ -23,6 +40,8 @@ export async function runCommand( env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"], shell: false, + // A process group lets a timeout stop the spawned tool and everything it started. + detached: process.platform !== "win32", windowsHide: true, }); let stdout = ""; @@ -38,7 +57,7 @@ export async function runCommand( child.once("error", reject); const timer = setTimeout(() => { timedOut = true; - child.kill(); + killProcessTree(child); }, options.timeoutMs); child.once("close", (code) => { clearTimeout(timer); @@ -53,3 +72,29 @@ export async function runCommand( }); }); } + +/** Stop a timed-out tool together with the children it spawned. */ +function killProcessTree(child: ChildProcess): void { + const pid = child.pid; + if (pid === undefined) return; + if (process.platform === "win32") { + const killer = spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { + stdio: "ignore", + windowsHide: true, + }); + killer.once("error", () => child.kill("SIGKILL")); + return; + } + signalGroup(pid, child, "SIGTERM"); + setTimeout(() => { + if (child.exitCode === null && child.signalCode === null) signalGroup(pid, child, "SIGKILL"); + }, KILL_ESCALATION_MS).unref(); +} + +function signalGroup(pid: number, child: ChildProcess, signal: NodeJS.Signals): void { + try { + process.kill(-pid, signal); + } catch { + child.kill(signal); + } +} diff --git a/packages/harness/src/compare/runner.ts b/packages/harness/src/compare/runner.ts index ace0aab..9d5c7b2 100644 --- a/packages/harness/src/compare/runner.ts +++ b/packages/harness/src/compare/runner.ts @@ -22,6 +22,7 @@ import { gradeReadmePackage, type ReadmeGrade } from "./grader.js"; import { loadHarnessCompareManifest, resolveHarnessCompareRuntime, + type HarnessCompareManifest, type LoadedHarnessCompareManifest, type ResolvedHarnessCompareRuntime, } from "./manifest.js"; @@ -237,82 +238,156 @@ async function runTrial(options: { }; let infrastructureError: string | undefined; try { - await cp(options.loaded.resolved.fixture, trialRoot, { recursive: true, errorOnExist: true, force: false }); - const before = await snapshotDirectory(trialRoot); - await initializeGitRepository(trialRoot, temporaryParent); - const abortController = new AbortController(); - const executor = options.executorFactory({ - runtime, - trialRoot, - abortController, - permissionDecisions, - expectedFiles: options.loaded.value.task.expectedFiles, - traceEvents, - }); try { - execution = await withTimeout( - executor.execute(options.revision, options.bundle, { prompt: options.prompt, cwd: trialRoot }), - runtime.timeoutMs, + await cp(options.loaded.resolved.fixture, trialRoot, { recursive: true, errorOnExist: true, force: false }); + const before = await snapshotDirectory(trialRoot); + await initializeGitRepository(trialRoot, temporaryParent); + const abortController = new AbortController(); + const executor = options.executorFactory({ + runtime, + trialRoot, abortController, + permissionDecisions, + expectedFiles: options.loaded.value.task.expectedFiles, + traceEvents, + }); + try { + execution = await withTimeout( + executor.execute(options.revision, options.bundle, { prompt: options.prompt, cwd: trialRoot }), + runtime.timeoutMs, + abortController, + ); + } catch (error) { + infrastructureError = errorMessage(error); + execution = { + host: "qoder", + revisionId: options.revision.revisionId, + exitCode: 1, + output: "", + errorOutput: infrastructureError, + warnings: [], + }; + } + const after = await snapshotDirectory(trialRoot); + const changedFiles = diffSnapshots(before, after); + const patch = await capturePatch(trialRoot); + await writeFile(join(artifactDirectory, "prompt.txt"), options.prompt, "utf8"); + await writeFile(join(artifactDirectory, "response.txt"), execution.output, "utf8"); + await writeFile(join(artifactDirectory, "stderr.txt"), redactRoot(execution.errorOutput, trialRoot), "utf8"); + await writeFile(join(artifactDirectory, "patch.diff"), redactRoot(patch, trialRoot), "utf8"); + await writeTrace( + join(artifactDirectory, "trace.jsonl"), + execution.trace && execution.trace.length > 0 ? execution.trace : traceEvents, + trialRoot, ); - } catch (error) { - infrastructureError = errorMessage(error); - execution = { - host: "qoder", + await writeJson(join(artifactDirectory, "permission-decisions.json"), permissionDecisions); + const grade = await gradeReadmePackage({ + trialRoot, + contractPath: options.loaded.resolved.graderContract, + changedFiles, + expectedFiles: options.loaded.value.task.expectedFiles, + }); + await writeJson(join(artifactDirectory, "validation.json"), grade); + const classification = classifyTrial(execution, grade, infrastructureError); + const result: CompareTrialResult = { + variant: options.variant, + compositionId: options.compositionId, + runtimeProfile: runtime.profile, + trial: options.trial, + classification, + changedFiles, + grade, + executorExitCode: execution.exitCode, + executorError: redactRoot(execution.errorOutput, trialRoot), revisionId: options.revision.revisionId, - exitCode: 1, - output: "", - errorOutput: infrastructureError, - warnings: [], + durationMs: Date.now() - started, + artifactDirectory: trialArtifactPath(variantDirectory, options.trial), + ...(execution.metrics ? { metrics: execution.metrics } : {}), }; + await writeJson(join(artifactDirectory, "runtime-receipt.json"), execution.runtimeReceipt ?? { + executor: "injected-test-executor", + permissionCallback: "none", + }); + await writeJson(join(artifactDirectory, "metrics.json"), trialSummary(result)); + return result; + } catch (error) { + // Harness, Git, filesystem, and grader breakage is an infrastructure failure + // for this trial, not a coding outcome, and must not discard the other trials. + return await recordInfrastructureFailure({ + artifactDirectory, + variantDirectory, + compositionId: options.compositionId, + runtimeProfile: runtime.profile, + variant: options.variant, + trial: options.trial, + revisionId: options.revision.revisionId, + durationMs: Date.now() - started, + detail: redactRoot(errorMessage(error), trialRoot), + permissionDecisions, + }); } - const after = await snapshotDirectory(trialRoot); - const changedFiles = diffSnapshots(before, after); - const patch = await capturePatch(trialRoot); - await writeFile(join(artifactDirectory, "prompt.txt"), options.prompt, "utf8"); - await writeFile(join(artifactDirectory, "response.txt"), execution.output, "utf8"); - await writeFile(join(artifactDirectory, "stderr.txt"), redactRoot(execution.errorOutput, trialRoot), "utf8"); - await writeFile(join(artifactDirectory, "patch.diff"), redactRoot(patch, trialRoot), "utf8"); - await writeTrace( - join(artifactDirectory, "trace.jsonl"), - execution.trace && execution.trace.length > 0 ? execution.trace : traceEvents, - trialRoot, - ); - await writeJson(join(artifactDirectory, "permission-decisions.json"), permissionDecisions); - const grade = await gradeReadmePackage({ - trialRoot, - contractPath: options.loaded.resolved.graderContract, - changedFiles, - expectedFiles: options.loaded.value.task.expectedFiles, - }); - await writeJson(join(artifactDirectory, "validation.json"), grade); - const classification = classifyTrial(execution, grade, infrastructureError); - const result: CompareTrialResult = { - variant: options.variant, - compositionId: options.compositionId, - runtimeProfile: runtime.profile, - trial: options.trial, - classification, - changedFiles, - grade, - executorExitCode: execution.exitCode, - executorError: redactRoot(execution.errorOutput, trialRoot), - revisionId: options.revision.revisionId, - durationMs: Date.now() - started, - artifactDirectory: `${variantDirectory}/trial-${String(options.trial).padStart(3, "0")}`, - ...(execution.metrics ? { metrics: execution.metrics } : {}), - }; - await writeJson(join(artifactDirectory, "runtime-receipt.json"), execution.runtimeReceipt ?? { - executor: "injected-test-executor", - permissionCallback: "none", - }); - await writeJson(join(artifactDirectory, "metrics.json"), result); - return result; } finally { await rm(temporaryParent, { recursive: true, force: true }); } } +async function recordInfrastructureFailure(options: { + artifactDirectory: string; + variantDirectory: string; + compositionId: string; + runtimeProfile: string; + variant: CompareVariant; + trial: number; + revisionId: string; + durationMs: number; + detail: string; + permissionDecisions: ToolPermissionDecision[]; +}): Promise { + const result: CompareTrialResult = { + variant: options.variant, + compositionId: options.compositionId, + runtimeProfile: options.runtimeProfile, + trial: options.trial, + classification: "infrastructure_error", + changedFiles: [], + grade: { + kind: "readme-package-v1", + passed: false, + score: 0, + checks: [{ + id: "infrastructure", + passed: false, + hard: true, + weight: 100, + detail: options.detail, + }], + }, + executorExitCode: 1, + executorError: options.detail, + revisionId: options.revisionId, + durationMs: options.durationMs, + artifactDirectory: trialArtifactPath(options.variantDirectory, options.trial), + }; + await writeFile(join(options.artifactDirectory, "stderr.txt"), `${options.detail}\n`, "utf8").catch(() => undefined); + await writeJson(join(options.artifactDirectory, "validation.json"), result.grade).catch(() => undefined); + await writeJson(join(options.artifactDirectory, "permission-decisions.json"), options.permissionDecisions) + .catch(() => undefined); + await writeJson(join(options.artifactDirectory, "metrics.json"), trialSummary(result)).catch(() => undefined); + return result; +} + +function trialArtifactPath(variantDirectory: string, trial: number): string { + return `${variantDirectory}/trial-${String(trial).padStart(3, "0")}`; +} + +/** Keep per-trial metrics readable; the full check list stays in validation.json. */ +function trialSummary(result: CompareTrialResult): Record { + return { + ...result, + grade: { kind: result.grade.kind, passed: result.grade.passed, score: result.grade.score }, + }; +} + function defaultExecutorFactory(context: CompareExecutorContext): HarnessExecutor { return new QoderSdkExecutor({ profile: context.runtime.profile, diff --git a/packages/harness/src/compiler/compile.ts b/packages/harness/src/compiler/compile.ts index bb4b622..1f66c41 100644 --- a/packages/harness/src/compiler/compile.ts +++ b/packages/harness/src/compiler/compile.ts @@ -2,11 +2,14 @@ import { URI, type LangiumDocument } from "langium"; import semver from "semver"; import { createHarnessServices } from "../language/harness-module.js"; import { + type CapabilityRequirement, type ComponentContract, type CompositionDeclaration, + type ConfigEntry, type ConfigValue, type HarnessDocument, type PluginDeclaration, + type PluginRequirement, type TargetBinding, isComponentContract, isCompositionDeclaration, @@ -43,6 +46,81 @@ export interface HarnessSource { const DURATION_FACTORS: Record = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 }; +/** Default mechanism for a binding that omits one: prompt guidance. */ +const DEFAULT_MECHANISM = "prompt-preamble"; +/** Default strength for a binding or requirement that omits one. */ +const DEFAULT_STRENGTH: Strength = "advisory"; + +/** + * Flattened, `extends`-resolved view of a composition. The child overrides the + * base's `target` and, per member key, its includes/requirements/settings. + */ +interface MergedComposition { + name: string; + target: string | undefined; + includes: PluginRequirement[]; + requirements: CapabilityRequirement[]; + settings: ConfigEntry[]; +} + +/** Whether following `extends` from this composition revisits a node. */ +function hasExtendsCycle(composition: CompositionDeclaration): boolean { + const seen = new Set(); + let current: CompositionDeclaration | undefined = composition; + while (current) { + if (seen.has(current)) { + return true; + } + seen.add(current); + current = current.base?.ref; + } + return false; +} + +/** + * Walk the `extends` chain base-first and merge members. Later (more derived) + * declarations override earlier ones by key: plugin id, required component id, + * and configuration key. A cycle is broken defensively and reported separately + * by {@link collectBundleDiagnostics}. + */ +function mergeExtends(composition: CompositionDeclaration): MergedComposition { + const chain: CompositionDeclaration[] = []; + const seen = new Set(); + let current: CompositionDeclaration | undefined = composition; + while (current && !seen.has(current)) { + seen.add(current); + chain.push(current); + current = current.base?.ref; + } + chain.reverse(); + + const includes = new Map(); + const requirements = new Map(); + const settings = new Map(); + let target: string | undefined; + for (const node of chain) { + if (node.target !== undefined) { + target = node.target; + } + for (const include of node.includes) { + includes.set(include.plugin.$refText, include); + } + for (const requirement of node.requirements) { + requirements.set(requirement.component.$refText, requirement); + } + for (const setting of node.settings) { + settings.set(setting.key, setting); + } + } + return { + name: composition.name, + target, + includes: [...includes.values()], + requirements: [...requirements.values()], + settings: [...settings.values()], + }; +} + /** * Parse one or more Harness DSL sources and lower them into the versioned * JSON IR bundle. Cross-file references are resolved across all provided @@ -89,7 +167,7 @@ export async function compileHarness(input: string | HarnessSource[]): Promise ({ irVersion: IR_VERSION, kind: "target-binding", componentId: binding.component.$refText, - host: binding.host, - mechanism: binding.mechanism, - strength: binding.strength as Strength, + host, + mechanism: binding.mechanism ?? DEFAULT_MECHANISM, + strength: (binding.strength ?? DEFAULT_STRENGTH) as Strength, ...(binding.notes !== undefined ? { notes: binding.notes } : {}), - }; + })); } function lowerPlugin(plugin: PluginDeclaration, bindings: TargetBindingIr[]): PluginManifestIr { @@ -309,24 +422,26 @@ function lowerPlugin(plugin: PluginDeclaration, bindings: TargetBindingIr[]): Pl } function lowerComposition(composition: CompositionDeclaration): CompositionSpecIr { + const merged = mergeExtends(composition); return { irVersion: IR_VERSION, kind: "composition-spec", - id: composition.name, - target: composition.target, - includes: composition.includes.map((include) => ({ + id: merged.name, + // A missing target is rejected by collectBundleDiagnostics before lowering. + target: merged.target ?? "", + includes: merged.includes.map((include) => ({ pluginId: include.plugin.$refText, range: include.range.replace(/^@/, ""), })), - requirements: composition.requirements.map((requirement) => ({ + requirements: merged.requirements.map((requirement) => ({ componentId: requirement.component.$refText, ...(requirement.preferred !== undefined ? { preferred: requirement.preferred as Strength } : {}), - minimum: requirement.minimum as Strength, + minimum: (requirement.minimum ?? DEFAULT_STRENGTH) as Strength, onDegrade: requirement.onDegrade ?? "report", })), - settings: composition.settings.map((entry) => ({ + settings: merged.settings.map((entry) => ({ key: entry.key, value: lowerConfigValue(entry.value), })), diff --git a/packages/harness/src/exec/qoder-sdk.ts b/packages/harness/src/exec/qoder-sdk.ts index 6096224..b2bc0d1 100644 --- a/packages/harness/src/exec/qoder-sdk.ts +++ b/packages/harness/src/exec/qoder-sdk.ts @@ -346,23 +346,32 @@ function buildQoderMinimalSystemPrompt(cwd?: string): string { const SECRET_FIELD = /(?:authorization|api[_-]?key|access[_-]?token|refresh[_-]?token|service[_-]?account[_-]?key|secret|credential)/i; -/** Recursively remove credential-shaped fields before persisting SDK events. */ -export function redactTraceValue(value: unknown, seen = new WeakSet()): unknown { +/** + * Recursively remove credential-shaped fields before persisting SDK events. + * + * Only the current ancestor chain is tracked, so a value referenced twice in + * sibling positions is still recorded instead of being reported as a cycle. + */ +export function redactTraceValue(value: unknown, ancestors = new WeakSet()): unknown { if (value === null || typeof value !== "object") { return value; } - if (seen.has(value)) { + if (ancestors.has(value)) { return "[Circular]"; } - seen.add(value); - if (Array.isArray(value)) { - return value.map((item) => redactTraceValue(item, seen)); - } - const redacted: Record = {}; - for (const [key, item] of Object.entries(value)) { - redacted[key] = SECRET_FIELD.test(key) ? "[REDACTED]" : redactTraceValue(item, seen); + ancestors.add(value); + try { + if (Array.isArray(value)) { + return value.map((item) => redactTraceValue(item, ancestors)); + } + const redacted: Record = {}; + for (const [key, item] of Object.entries(value)) { + redacted[key] = SECRET_FIELD.test(key) ? "[REDACTED]" : redactTraceValue(item, ancestors); + } + return redacted; + } finally { + ancestors.delete(value); } - return redacted; } async function loadQoderSdk(loader?: () => Promise): Promise { diff --git a/packages/harness/src/highlight/harness-grammar.ts b/packages/harness/src/highlight/harness-grammar.ts index 6b4712b..2e052f5 100644 --- a/packages/harness/src/highlight/harness-grammar.ts +++ b/packages/harness/src/highlight/harness-grammar.ts @@ -34,7 +34,10 @@ export const harnessTextMateGrammar: LanguageRegistration = { "2": { name: "entity.name.type.harness" }, }, }, - { name: "keyword.control.harness", match: "\\b(for|require|include|provides)\\b" }, + { + name: "keyword.control.harness", + match: "\\b(for|require|include|provides|extends)\\b", + }, ], }, sections: { diff --git a/packages/harness/src/language/generated/ast.ts b/packages/harness/src/language/generated/ast.ts index 5b22338..c4d6886 100644 --- a/packages/harness/src/language/generated/ast.ts +++ b/packages/harness/src/language/generated/ast.ts @@ -34,6 +34,7 @@ export type HarnessKeywordNames = | "deny" | "description" | "enforced" + | "extends" | "fail" | "false" | "for" @@ -95,7 +96,7 @@ export interface CapabilityRequirement extends langium.AstNode { readonly $container: CompositionDeclaration; readonly $type: 'CapabilityRequirement'; component: langium.Reference; - minimum: Strength; + minimum?: Strength; onDegrade?: DegradePolicy; preferred?: Strength; } @@ -161,15 +162,17 @@ export function isComponentRef(item: unknown): item is ComponentRef { export interface CompositionDeclaration extends langium.AstNode { readonly $container: HarnessDocument; readonly $type: 'CompositionDeclaration'; + base?: langium.Reference; includes: Array; name: string; requirements: Array; settings: Array; - target: string; + target?: string; } export const CompositionDeclaration = { $type: 'CompositionDeclaration', + base: 'base', includes: 'includes', name: 'name', requirements: 'requirements', @@ -364,16 +367,16 @@ export interface TargetBinding extends langium.AstNode { readonly $container: HarnessDocument; readonly $type: 'TargetBinding'; component: langium.Reference; - host: string; - mechanism: string; + hosts: Array; + mechanism?: string; notes?: string; - strength: Strength; + strength?: Strength; } export const TargetBinding = { $type: 'TargetBinding', component: 'component', - host: 'host', + hosts: 'hosts', mechanism: 'mechanism', notes: 'notes', strength: 'strength' @@ -423,7 +426,8 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { referenceType: ComponentContract.$type }, minimum: { - name: CapabilityRequirement.minimum + name: CapabilityRequirement.minimum, + optional: true }, onDegrade: { name: CapabilityRequirement.onDegrade, @@ -480,9 +484,15 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { CompositionDeclaration: { name: CompositionDeclaration.$type, properties: { + base: { + name: CompositionDeclaration.base, + referenceType: CompositionDeclaration.$type, + optional: true + }, includes: { name: CompositionDeclaration.includes, - defaultValue: [] + defaultValue: [], + optional: true }, name: { name: CompositionDeclaration.name @@ -498,7 +508,8 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { optional: true }, target: { - name: CompositionDeclaration.target + name: CompositionDeclaration.target, + optional: true } }, superTypes: [Declaration.$type] @@ -613,18 +624,21 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { name: TargetBinding.component, referenceType: ComponentContract.$type }, - host: { - name: TargetBinding.host + hosts: { + name: TargetBinding.hosts, + defaultValue: [] }, mechanism: { - name: TargetBinding.mechanism + name: TargetBinding.mechanism, + optional: true }, notes: { name: TargetBinding.notes, optional: true }, strength: { - name: TargetBinding.strength + name: TargetBinding.strength, + optional: true } }, superTypes: [Declaration.$type] diff --git a/packages/harness/src/language/generated/grammar.ts b/packages/harness/src/language/generated/grammar.ts index 5ac1ae5..f6de10b 100644 --- a/packages/harness/src/language/generated/grammar.ts +++ b/packages/harness/src/language/generated/grammar.ts @@ -413,52 +413,116 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "value": "for" }, { - "$type": "Assignment", - "feature": "host", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" + "$type": "Alternatives", + "elements": [ + { + "$type": "Assignment", + "feature": "hosts", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } }, - "arguments": [] - } + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "[" + }, + { + "$type": "Assignment", + "feature": "hosts", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "," + }, + { + "$type": "Assignment", + "feature": "hosts", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "]" + } + ] + } + ] }, { "$type": "Keyword", "value": "{" }, { - "$type": "Keyword", - "value": "mechanism" - }, - { - "$type": "Assignment", - "feature": "mechanism", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "mechanism" }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "strength" + { + "$type": "Assignment", + "feature": "mechanism", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + } + ], + "cardinality": "?" }, { - "$type": "Assignment", - "feature": "strength", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "strength" }, - "arguments": [] - } + { + "$type": "Assignment", + "feature": "strength", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ], + "cardinality": "?" }, { "$type": "Group", @@ -682,51 +746,71 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn } }, { - "$type": "Keyword", - "value": "{" - }, - { - "$type": "Keyword", - "value": "target" - }, - { - "$type": "Assignment", - "feature": "target", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "extends" }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "include" + { + "$type": "Assignment", + "feature": "base", + "operator": "=", + "terminal": { + "$type": "CrossReference", + "type": { + "$ref": "#/rules@11" + }, + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + }, + "deprecatedSyntax": false, + "isMulti": false + } + } + ], + "cardinality": "?" }, { "$type": "Keyword", - "value": "[" + "value": "{" }, { - "$type": "Assignment", - "feature": "includes", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@12" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "target" }, - "arguments": [] - } + { + "$type": "Assignment", + "feature": "target", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" + }, + "arguments": [] + } + } + ], + "cardinality": "?" }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "," + "value": "include" + }, + { + "$type": "Keyword", + "value": "[" }, { "$type": "Assignment", @@ -739,13 +823,35 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, "arguments": [] } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "," + }, + { + "$type": "Assignment", + "feature": "includes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "]" } ], - "cardinality": "*" - }, - { - "$type": "Keyword", - "value": "]" + "cardinality": "?" }, { "$type": "Assignment", @@ -883,73 +989,85 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "isMulti": false } }, - { - "$type": "Keyword", - "value": "{" - }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "preferred" + "value": "{" }, { - "$type": "Assignment", - "feature": "preferred", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "preferred" }, - "arguments": [] - } - } - ], - "cardinality": "?" - }, - { - "$type": "Keyword", - "value": "minimum" - }, - { - "$type": "Assignment", - "feature": "minimum", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" + { + "$type": "Assignment", + "feature": "preferred", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ], + "cardinality": "?" }, - "arguments": [] - } - }, - { - "$type": "Group", - "elements": [ { - "$type": "Keyword", - "value": "on-degrade" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "minimum" + }, + { + "$type": "Assignment", + "feature": "minimum", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + } + } + ], + "cardinality": "?" }, { - "$type": "Assignment", - "feature": "onDegrade", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@14" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "on-degrade" }, - "arguments": [] - } + { + "$type": "Assignment", + "feature": "onDegrade", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@14" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "}" } ], "cardinality": "?" - }, - { - "$type": "Keyword", - "value": "}" } ] }, diff --git a/packages/harness/src/language/harness-validator.ts b/packages/harness/src/language/harness-validator.ts index 544e8c4..e609d01 100644 --- a/packages/harness/src/language/harness-validator.ts +++ b/packages/harness/src/language/harness-validator.ts @@ -43,15 +43,17 @@ export class HarnessValidator { if (!isTargetBinding(element)) { continue; } - const key = `${element.component.$refText}::${element.host}`; - if (seen.has(key)) { - accept( - "error", - `Duplicate binding for component '${element.component.$refText}' on host '${element.host}'.`, - { node: element, property: "host" }, - ); + for (const host of element.hosts) { + const key = `${element.component.$refText}::${host}`; + if (seen.has(key)) { + accept( + "error", + `Duplicate binding for component '${element.component.$refText}' on host '${host}'.`, + { node: element, property: "hosts" }, + ); + } + seen.add(key); } - seen.add(key); } } } diff --git a/packages/harness/src/language/harness.langium b/packages/harness/src/language/harness.langium index fe12aaa..cea1dcc 100644 --- a/packages/harness/src/language/harness.langium +++ b/packages/harness/src/language/harness.langium @@ -34,12 +34,17 @@ PermissionDomain returns string: PermissionAccess returns string: 'read' | 'write' | 'allow' | 'deny'; -// How a component is realized on one concrete host, and how strong -// the realized guarantee is. +// How a component is realized on one or more concrete hosts, and how +// strong the realized guarantee is. `for [a, b]` declares the same +// mechanism/strength on several hosts at once. `mechanism` and `strength` +// are optional: an omitted binding is prompt guidance at `advisory`, the +// v0.1 materialization floor, so authors only spell out a binding to +// declare a stronger future mechanism or an explicit `unsupported`. TargetBinding: - 'binding' component=[ComponentContract:ID] 'for' host=ID '{' - 'mechanism' mechanism=ID - 'strength' strength=Strength + 'binding' component=[ComponentContract:ID] 'for' + ( hosts+=ID | '[' hosts+=ID (',' hosts+=ID)* ']' ) '{' + ('mechanism' mechanism=ID)? + ('strength' strength=Strength)? ('notes' notes=STRING)? '}'; @@ -59,10 +64,16 @@ ComponentRef: // Desired assembly for one run target. Resolving a composition against // a registry produces an immutable revision plus a resolution report. +// +// `extends` inherits the base composition's target, includes, requirements, +// and configuration; the child overrides `target` and appends its own +// members. `include` is optional: a composition may `require` components +// directly, without wrapping them in a plugin, in which case they +// materialize as advisory prompt guidance. CompositionDeclaration: - 'composition' name=ID '{' - 'target' target=ID - 'include' '[' includes+=PluginRequirement (',' includes+=PluginRequirement)* ']' + 'composition' name=ID ('extends' base=[CompositionDeclaration:ID])? '{' + ('target' target=ID)? + ('include' '[' includes+=PluginRequirement (',' includes+=PluginRequirement)* ']')? (requirements+=CapabilityRequirement)* ('configure' '{' (settings+=ConfigEntry)* '}')? '}'; @@ -70,12 +81,16 @@ CompositionDeclaration: PluginRequirement: 'plugin' '.' plugin=[PluginDeclaration:ID] range=VERSION_RANGE; +// A bare `require component` is the common case: it asks for the component +// at the `advisory` floor with `on-degrade report`. The optional block +// raises the floor (`minimum`), states a `preferred` ceiling, or makes +// degradation fatal (`on-degrade fail`). CapabilityRequirement: - 'require' component=[ComponentContract:ID] '{' + 'require' component=[ComponentContract:ID] ('{' ('preferred' preferred=Strength)? - 'minimum' minimum=Strength + ('minimum' minimum=Strength)? ('on-degrade' onDegrade=DegradePolicy)? - '}'; + '}')?; DegradePolicy returns string: 'fail' | 'report'; diff --git a/packages/harness/src/resolver/resolve.ts b/packages/harness/src/resolver/resolve.ts index 6abd386..bdf2bae 100644 --- a/packages/harness/src/resolver/resolve.ts +++ b/packages/harness/src/resolver/resolve.ts @@ -66,7 +66,18 @@ export function resolveComposition(bundle: HarnessIrBundle, compositionId: strin resolvedPlugins.push(matching[0]); } - const enabledComponentIds = new Set(resolvedPlugins.flatMap((plugin) => plugin.provides)); + // Components reach a composition two ways: bundled inside an included plugin, + // or `require`d directly. A directly-required component needs no plugin + // wrapper and, absent an explicit binding, materializes as advisory prompt + // guidance — the v0.1 floor — so a bare `require` just works. + const pluginComponentIds = new Set(resolvedPlugins.flatMap((plugin) => plugin.provides)); + const bundleComponentIds = new Set(bundle.components.map((component) => component.id)); + const directComponentIds = new Set( + composition.requirements + .map((requirement) => requirement.componentId) + .filter((id) => !pluginComponentIds.has(id) && bundleComponentIds.has(id)), + ); + const enabledComponentIds = new Set([...pluginComponentIds, ...directComponentIds]); const enabledComponents = bundle.components.filter((component) => enabledComponentIds.has(component.id), ); @@ -88,10 +99,11 @@ export function resolveComposition(bundle: HarnessIrBundle, compositionId: strin ); continue; } - const binding = bundle.bindings.find( - (candidate) => - candidate.componentId === requirement.componentId && candidate.host === composition.target, - ); + const binding = + bundle.bindings.find( + (candidate) => + candidate.componentId === requirement.componentId && candidate.host === composition.target, + ) ?? defaultBindingFor(requirement.componentId, composition.target, directComponentIds); const materialization = materializeV01(binding); const realized = materialization.strength; const minimumIndex = strengthIndex(requirement.minimum); @@ -263,6 +275,30 @@ function degradeReason(binding: TargetBindingIr | undefined, host: string): stri return binding.notes ? `${detail}: ${binding.notes}` : detail; } +/** + * A directly-`require`d component without an explicit binding is treated as an + * advisory prompt-guidance binding: the same floor a written binding would + * declare, so authoring the binding is optional rather than required. Plugin + * components keep their explicit-binding-or-`unsupported` semantics. + */ +function defaultBindingFor( + componentId: string, + host: string, + directComponentIds: Set, +): TargetBindingIr | undefined { + if (!directComponentIds.has(componentId)) { + return undefined; + } + return { + irVersion: IR_VERSION, + kind: "target-binding", + componentId, + host, + mechanism: "prompt-preamble", + strength: "advisory", + }; +} + interface V01Materialization { strength: Realization["realized"]; mechanism: string | null; diff --git a/packages/harness/test/compare.test.ts b/packages/harness/test/compare.test.ts index 039a992..2b1645a 100644 --- a/packages/harness/test/compare.test.ts +++ b/packages/harness/test/compare.test.ts @@ -1,11 +1,13 @@ import { cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import type { HarnessExecutor } from "../src/exec/executor.js"; import { gradeReadmePackage } from "../src/compare/grader.js"; import { loadHarnessCompareManifest, resolveHarnessCompareRuntime } from "../src/compare/manifest.js"; import { createBoundedQoderPermissionCallback, type ToolPermissionDecision } from "../src/compare/permissions.js"; +import { npmInvocation, runCommand } from "../src/compare/process.js"; import { runHarnessComparison } from "../src/compare/runner.js"; const EXPERIMENT_URL = new URL("../examples/readme-compare/experiment.json", import.meta.url); @@ -13,6 +15,10 @@ const MINIMAL_EXPERIMENT_URL = new URL( "../examples/readme-compare/minimal-profile-experiment.json", import.meta.url, ); +const EXPERIMENT_PATH = fileURLToPath(EXPERIMENT_URL); +const MINIMAL_EXPERIMENT_PATH = fileURLToPath(MINIMAL_EXPERIMENT_URL); +const CONTRACT_PATH = fileURLToPath(new URL("../examples/readme-compare/grader-contract.json", import.meta.url)); +const FIXTURE_URL = new URL("../examples/readme-compare/fixture", import.meta.url); const temporaryDirectories: string[] = []; afterEach(async () => { @@ -21,7 +27,7 @@ afterEach(async () => { describe("harness compare manifest", () => { it("loads the frozen README experiment with owned relative paths", async () => { - const loaded = await loadHarnessCompareManifest(EXPERIMENT_URL.pathname); + const loaded = await loadHarnessCompareManifest(EXPERIMENT_PATH); expect(loaded.value).toMatchObject({ schemaVersion: "harness-compare.v1", @@ -44,7 +50,7 @@ describe("harness compare manifest", () => { }); it("accepts an isolated same-composition comparison when runtime profiles differ", async () => { - const loaded = await loadHarnessCompareManifest(MINIMAL_EXPERIMENT_URL.pathname); + const loaded = await loadHarnessCompareManifest(MINIMAL_EXPERIMENT_PATH); expect(loaded.value.variants).toEqual({ baseline: "readme-grounded", @@ -59,6 +65,19 @@ describe("harness compare manifest", () => { tools: ["Read", "Write", "Edit", "Bash"], }); }); + + it("rejects a denied network that leaves a web tool reachable", async () => { + const directory = await makeTemporaryDirectory(); + const manifest = JSON.parse(await readFile(EXPERIMENT_URL, "utf8")) as { + runtime: { network: string; disallowedTools: string[] }; + }; + expect(manifest.runtime.network).toBe("deny"); + manifest.runtime.disallowedTools = manifest.runtime.disallowedTools.filter((tool) => tool !== "WebFetch"); + const path = join(directory, "experiment.json"); + await writeFile(path, JSON.stringify(manifest), "utf8"); + + await expect(loadHarnessCompareManifest(path)).rejects.toThrow(/network 'deny' requires disallowedTools/); + }); }); describe("bounded Qoder permissions", () => { @@ -85,11 +104,25 @@ describe("bounded Qoder permissions", () => { }); }); +describe("validation command execution", () => { + it("runs npm without a shell on this host", async () => { + const directory = await makeTemporaryDirectory(); + const invocation = npmInvocation(["--version"]); + + const result = await runCommand(invocation.command, invocation.args, { + cwd: directory, + timeoutMs: 60_000, + }); + + expect(result.exitCode).toBe(0); + expect(result.stdout.trim()).toMatch(/^\d+\.\d+\.\d+/); + }); +}); + describe("README coding comparison", () => { it("rejects generated examples that request host capabilities", async () => { const directory = await makeTemporaryDirectory(); - const fixture = new URL("../examples/readme-compare/fixture", import.meta.url); - await cp(fixture, directory, { recursive: true, force: true }); + await cp(FIXTURE_URL, directory, { recursive: true, force: true }); await writeFile( join(directory, "README.md"), VALID_README.replace("console.log(value);", "console.log(process.env);"), @@ -98,7 +131,7 @@ describe("README coding comparison", () => { const grade = await gradeReadmePackage({ trialRoot: directory, - contractPath: new URL("../examples/readme-compare/grader-contract.json", import.meta.url).pathname, + contractPath: CONTRACT_PATH, changedFiles: ["README.md"], expectedFiles: ["README.md"], }); @@ -109,6 +142,32 @@ describe("README coding comparison", () => { }); }); + it("grades a tampered package entry point without loading it into the grader", async () => { + const directory = await makeTemporaryDirectory(); + await cp(FIXTURE_URL, directory, { recursive: true, force: true }); + await writeFile(join(directory, "README.md"), VALID_README, "utf8"); + await writeFile( + join(directory, "src/index.mjs"), + "globalThis.harnessGraderTampered = true;\nthrow new Error('tampered entry point');\n", + "utf8", + ); + + const grade = await gradeReadmePackage({ + trialRoot: directory, + contractPath: CONTRACT_PATH, + changedFiles: ["README.md", "src/index.mjs"], + expectedFiles: ["README.md"], + }); + + expect("harnessGraderTampered" in globalThis).toBe(false); + expect(grade.passed).toBe(false); + expect(grade.checks.find((item) => item.id === "public-api")).toMatchObject({ + passed: false, + command: { stderr: expect.stringContaining("tampered entry point") }, + }); + expect(grade.checks.find((item) => item.id === "scope")).toMatchObject({ passed: false }); + }); + it("creates and grades real README files in isolated variant trials", async () => { const directory = await makeTemporaryDirectory(); const output = join(directory, "evidence"); @@ -116,7 +175,7 @@ describe("README coding comparison", () => { await expect(readFile(fixtureReadme, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); const verdict = await runHarnessComparison({ - manifestPath: EXPERIMENT_URL.pathname, + manifestPath: EXPERIMENT_PATH, outputDirectory: output, trialCount: 1, executorFactory: ({ trialRoot }): HarnessExecutor => ({ @@ -160,13 +219,40 @@ describe("README coding comparison", () => { await expect(readFile(fixtureReadme, "utf8")).rejects.toMatchObject({ code: "ENOENT" }); }); + it("reports harness setup breakage as an infrastructure error instead of a coding outcome", async () => { + const directory = await makeTemporaryDirectory(); + const output = join(directory, "evidence"); + + const verdict = await runHarnessComparison({ + manifestPath: EXPERIMENT_PATH, + outputDirectory: output, + trialCount: 1, + executorFactory: (): HarnessExecutor => { + throw new Error("SDK worker could not start"); + }, + }); + + expect(verdict.status).toBe("infrastructure_error"); + expect(verdict.trials).toHaveLength(2); + expect(verdict.trials.every((trial) => trial.classification === "infrastructure_error")).toBe(true); + expect(verdict.baseline).toMatchObject({ trials: 1, completedTrials: 0, infrastructureErrors: 1, passRate: 0 }); + expect(verdict.trials[0].grade.checks[0]).toMatchObject({ + id: "infrastructure", + detail: expect.stringContaining("SDK worker could not start"), + }); + expect(JSON.parse(await readFile(join(output, "H0/trial-001/metrics.json"), "utf8"))).toMatchObject({ + classification: "infrastructure_error", + grade: { passed: false, score: 0 }, + }); + }); + it("routes each variant through its resolved Qoder runtime profile", async () => { const directory = await makeTemporaryDirectory(); const output = join(directory, "evidence"); const observed: Array<{ profile: string; tools: string[] }> = []; const verdict = await runHarnessComparison({ - manifestPath: MINIMAL_EXPERIMENT_URL.pathname, + manifestPath: MINIMAL_EXPERIMENT_PATH, outputDirectory: output, trialCount: 1, executorFactory: (context): HarnessExecutor => { diff --git a/packages/harness/test/sugar.test.ts b/packages/harness/test/sugar.test.ts new file mode 100644 index 0000000..d8242fa --- /dev/null +++ b/packages/harness/test/sugar.test.ts @@ -0,0 +1,142 @@ +import { readFile } from "node:fs/promises"; +import { Value } from "@sinclair/typebox/value"; +import { describe, expect, it } from "vitest"; +import { compileHarness } from "../src/compiler/compile.js"; +import { HarnessIrBundleSchema, type HarnessIrBundle } from "../src/ir/index.js"; +import { resolveComposition } from "../src/resolver/resolve.js"; + +const MINIMAL_URL = new URL("../examples/minimal.harness", import.meta.url); + +async function compile(source: string): Promise { + const result = await compileHarness(source); + expect(result.diagnostics.filter((d) => d.severity === "error")).toEqual([]); + return result.bundle!; +} + +describe("progressive-disclosure sugar", () => { + it("resolves a minimal file: a bare require with no plugin or binding", async () => { + const bundle = await compile(await readFile(MINIMAL_URL, "utf8")); + expect(Value.Check(HarnessIrBundleSchema, bundle)).toBe(true); + + const { revision, report } = resolveComposition(bundle, "my-agent"); + expect(report.status).toBe("resolved"); + expect(revision!.resolved.components.map((component) => component.id)).toEqual([ + "require-tests", + ]); + expect(report.realizations).toEqual([ + expect.objectContaining({ + componentId: "require-tests", + declaredStrength: "advisory", + realized: "advisory", + materializedMechanism: "prompt-preamble", + action: "satisfied", + }), + ]); + }); + + it("a bare require defaults to minimum advisory with on-degrade report", async () => { + const bundle = await compile(` + component gate { kind policy } + composition run { target pi require gate } + `); + expect(bundle.compositions[0].requirements).toEqual([ + { componentId: "gate", minimum: "advisory", onDegrade: "report" }, + ]); + }); + + it("expands a for [a, b] binding into one binding per host", async () => { + const bundle = await compile(` + component gate { kind policy } + binding gate for [pi, qoder] { mechanism completion-hook strength enforced } + `); + expect(bundle.bindings).toEqual([ + expect.objectContaining({ host: "pi", mechanism: "completion-hook", strength: "enforced" }), + expect.objectContaining({ host: "qoder", mechanism: "completion-hook", strength: "enforced" }), + ]); + }); + + it("defaults an omitted binding mechanism and strength to advisory prompt guidance", async () => { + const bundle = await compile(` + component gate { kind policy } + binding gate for pi {} + `); + expect(bundle.bindings[0]).toMatchObject({ + host: "pi", + mechanism: "prompt-preamble", + strength: "advisory", + }); + }); + + it("inherits target, includes, requirements, and settings through extends", async () => { + const bundle = await compile(` + component gate { kind policy } + binding gate for [pi, qoder] { strength advisory } + plugin p { version "1.0.0" provides [ component.gate ] } + composition base { + target pi + include [ plugin.p@1 ] + require gate { minimum advisory } + configure { shell.timeout = 30s } + } + composition derived extends base { + target qoder + } + `); + const derived = bundle.compositions.find((composition) => composition.id === "derived")!; + expect(derived.target).toBe("qoder"); + expect(derived.includes).toEqual([{ pluginId: "p", range: "1" }]); + expect(derived.requirements).toEqual([ + { componentId: "gate", minimum: "advisory", onDegrade: "report" }, + ]); + expect(derived.settings).toEqual([ + { key: "shell.timeout", value: { type: "duration", value: "30s", ms: 30_000 } }, + ]); + expect(resolveComposition(bundle, "derived").report.status).toBe("resolved"); + }); + + it("lets a derived composition override an inherited setting by key", async () => { + const bundle = await compile(` + component gate { kind policy } + composition base { target pi require gate configure { shell.timeout = 30s } } + composition derived extends base { configure { shell.timeout = 90s } } + `); + const derived = bundle.compositions.find((composition) => composition.id === "derived")!; + expect(derived.settings).toEqual([ + { key: "shell.timeout", value: { type: "duration", value: "90s", ms: 90_000 } }, + ]); + }); + + it("fails compilation when a composition has neither target nor an extends target", async () => { + const result = await compileHarness(` + component gate { kind policy } + composition run { require gate } + `); + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics).toContainEqual( + expect.objectContaining({ + severity: "error", + message: expect.stringContaining("has no target"), + }), + ); + }); + + it("fails compilation on a circular extends chain", async () => { + const result = await compileHarness(` + composition a extends b { target pi } + composition b extends a { target qoder } + `); + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics.some((d) => d.message.includes("circular 'extends'"))).toBe(true); + }); + + it("still reports duplicate hosts within a single multi-host binding", async () => { + const result = await compileHarness(` + component gate { kind policy } + binding gate for [pi, pi] { strength advisory } + `); + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics.some((d) => d.message.includes("Duplicate component/host binding"))).toBe( + true, + ); + }); +}); From 7324ded4586c2bc884e523d80c1702f56d1be7b5 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 15 Aug 2026 13:10:08 +0800 Subject: [PATCH 06/25] feat(harness): converge the DSL on a standard resource model Replace the v0.1 authored surface (component / binding / plugin / composition) with host-neutral core semantics: a harness assembles one workflow, its logical agent roles, and their capability requirements; skill, tool, and mcp are the three standard capability kinds; runtime names a concrete host with its adapter and execution style; target is a deployment statement; binding is the adapter-layer mapping. The core DSL no longer defines a generic plugin. Host plugin and extension concepts stay in the host's own namespace as dotted binding mechanisms (qoder.plugin, pi.extension, deepseek.plugin, prime.python-skill) behind the runtime's adapter, so DeepSeek's Cordis plugin runtime and Pi's package/extension split remain isolated implementation details. Workflow implementation and capability exposure become independent dimensions: a workflow is either a declarative graph (edges, event routes, stop conditions) or a programmatic controller, and a runtime exposes capabilities through tool calling or programmatic calling in a host language. Resolution enforces deployability instead of pretending a Deno workflow can be translated into Prime's Python, and points at a matching runtime or external ACP driving. The proven v0.1 machinery is kept: the strength ladder, preferred / minimum / on-degrade policy, the declared-versus-materialized boundary with its advisory cap, deny-wins permission merging (now including MCP transport grants), canonical hashing, and the immutable revision. Realizations are recorded per agent and capability, and IR_VERSION moves to 0.2.0. Validated with npm run build, npm run typecheck, the 78 package tests covering every new rejection and resolution path, check:generated, the repository documentation link graph, and the authoring skill validator against the rewritten examples. Co-authored-by: QoderAI (Qwen 3.8 Max) --- ...6-08-15-harness-dsl-v0.2-resource-model.md | 127 ++ packages/harness/README.md | 167 +- .../harness/examples/full-surface.harness | 159 +- packages/harness/examples/minimal.harness | 28 +- .../harness/examples/readme-compare/prompt.md | 2 +- .../readme-compare/readme-compare.harness | 76 +- .../harness/examples/standard-coding.harness | 95 +- .../skills/generate-harness-dsl/SKILL.md | 70 +- .../generate-harness-dsl/agents/openai.yaml | 2 +- .../references/dsl-contract.md | 278 ++- .../generate-harness-dsl/scripts/validate.mjs | 36 +- packages/harness/src/compare/cli.ts | 4 +- packages/harness/src/compare/manifest.ts | 2 +- packages/harness/src/compare/runner.ts | 28 +- packages/harness/src/compiler/compile.ts | 684 +++--- packages/harness/src/exec/executor.ts | 67 +- packages/harness/src/exec/pi-sdk.ts | 35 +- .../harness/src/highlight/harness-grammar.ts | 15 +- packages/harness/src/index.ts | 2 +- packages/harness/src/ir/index.ts | 268 ++- .../harness/src/language/generated/ast.ts | 944 ++++++--- .../harness/src/language/generated/grammar.ts | 1856 ++++++++++++----- .../harness/src/language/harness-validator.ts | 22 +- packages/harness/src/language/harness.langium | 230 +- packages/harness/src/resolver/resolve.ts | 415 ++-- packages/harness/test/compare.test.ts | 4 +- packages/harness/test/compile.test.ts | 384 +++- packages/harness/test/exec.test.ts | 98 +- packages/harness/test/highlight.test.ts | 27 +- packages/harness/test/resolve.test.ts | 302 ++- packages/harness/test/skill.test.ts | 38 +- packages/harness/test/sugar.test.ts | 156 +- 32 files changed, 4457 insertions(+), 2164 deletions(-) create mode 100644 docs/specs/2026-08-15-harness-dsl-v0.2-resource-model.md diff --git a/docs/specs/2026-08-15-harness-dsl-v0.2-resource-model.md b/docs/specs/2026-08-15-harness-dsl-v0.2-resource-model.md new file mode 100644 index 0000000..c102712 --- /dev/null +++ b/docs/specs/2026-08-15-harness-dsl-v0.2-resource-model.md @@ -0,0 +1,127 @@ +# Converge the Harness DSL on a standard resource model + +## Traceability + +- Spec ID: harness-dsl-v0.2-resource-model +- Status: Implemented + +## Intent + +Replace the v0.1 authored surface (`component` / `binding` / `plugin` / +`composition`) with a resource model whose core semantics are host-neutral: + +- `harness` — the complete assembly of how an agent run works: workflow, + agent roles, capability requirements, and configuration. +- `workflow` — control flow, part of a harness, not the harness itself. + Either a declarative graph (`author -> verifier`, `on verifier.failed -> + author`, `stop when verifier.passed`) or a programmatic controller + (`program deno "./flows/coding-loop.ts"`). +- `agent` — a logical role inside a harness (`author`, `reviewer`). Qoder + and Pi are not agents; they are runtimes. +- `runtime` — a concrete host (Qoder, Pi, DeepSeek Harness, Prime Agent) + with an adapter package and an execution style: `tool-calling` or + `programmatic.` (Prime's persistent IPython, DeepSeek's + TypeScript plugin runtime). +- `skill` / `tool` / `mcp` — the three standard capability kinds. A skill is + progressive knowledge (Agent Skills standard, `source` directory) or + inline guidance (`description` only). A tool is an atomic callable with + input/output identifiers. An MCP entry is a connection (transport + + endpoint), not a tool itself. +- `target` — deployment statement selecting a runtime (`target qoder uses + adapter.qoder`). +- `binding` — adapter-layer mapping of one capability onto one runtime, + carrying `mechanism` (now a dotted name so host-native assets such as + `qoder.plugin`, `pi.extension`, `deepseek.plugin`, `prime.python-skill` + are expressible), `strength`, and `notes`. + +The core DSL no longer defines a generic `plugin`. Host plugin/extension +concepts live behind adapters as binding mechanisms in a host namespace. +"Everything is a Plugin" (DeepSeek/Cordis) and "Package vs Extension" (Pi) +remain host implementation details, isolated behind the adapter boundary. + +What is deliberately kept from v0.1: the strength ladder +(`unsupported < advisory < wired < enforced`), requirement degradation +policy (`preferred` / `minimum` / `on-degrade`), the declared-versus- +materialized strength boundary (v0.x executors still materialize prompt +guidance capped at `advisory`), permission merging with deny-wins, canonical +hashing, and the immutable resolved revision. + +## Acceptance Scenarios + +- AC-1: `.harness` sources using the v0.2 surface (`harness`, `workflow`, + `agent`, `skill`, `tool`, `mcp`, `runtime`, `target`, `binding`) compile + into schema-valid versioned IR (`irVersion` 0.2.0); the tokens `plugin` + and `composition` are no longer part of the grammar and fail to parse. +- AC-2: Compilation rejects, with source/line-qualified diagnostics: + duplicate capability ids across skills/tools/mcps, duplicate + harness/workflow/runtime ids, duplicate agent names in a harness, + duplicate requirements per agent, duplicate capability/runtime bindings, + verb–kind mismatches (`use skill` on a tool, `require tool` on an MCP, + `connect mcp` on a skill), workflow statements naming agents a using + harness does not declare, workflows mixing `program` with graph + statements, empty declarative workflows, skills with neither `source` nor + `description`, and `target ... uses adapter.X` conflicting with a declared + runtime's adapter. +- AC-3: Resolution of a harness for a runtime records declared and + materialized strength per (agent, capability). The v0.2 executors still + cap materialization at `advisory`; `minimum`, `preferred`, and + `on-degrade` apply to the effective strength exactly as in v0.1. +- AC-4: A programmatic workflow only resolves against a runtime whose + execution is `programmatic` in the same language; the resolution error + names external driving (ACP) or a matching runtime as the fix. + Declarative workflows resolve against any runtime. +- AC-5: The revision captures the harness content hash, the target runtime + (id, adapter, execution style), the workflow mode and hash, each agent's + resolved capability list, per-(agent, capability) realizations, merged + permissions (deny wins; MCP transports contribute implicit `network` / + `process` grants), and settings. Revision ids remain `hr_`. +- AC-6: Executors, Pi skill materialization, and the compare runner operate + on the new bundle/revision shapes: host checks compare + `revision.target.runtime`, the run preamble derives from agent roles, + workflow guidance, and capability text, and compare manifests name + harness ids in `variants`. +- AC-7: Examples, the `generate-harness-dsl` skill contract, the TextMate + highlight grammar, and the package README describe only the v0.2 surface; + `npm run build` and the package test suite pass with regenerated Langium + sources. + +## Non-goals + +- Implementing real adapter packages (`@harness/adapter-qoder` etc.) or + executing programmatic workflows; v0.2 records execution style and + enforces deployability only. +- Wiring native MCP connections, host plugins, or extensions; materialized + strength stays capped at `advisory` until an evidence-receipt contract. +- A migration tool for v0.1 sources; v0.1 documents fail to parse and are + rewritten by hand (the surface shipped in one prior spec cycle). +- Registry, versioned capability distribution, or a dependency solver + (removed together with `plugin`; distribution returns via `bundle` later). +- Changing root-level scripts (`harness-component-snapshot` uses an + unrelated "component" notion) or release metadata. + +## Plan and Tasks + +1. Rewrite `harness.langium` to the v0.2 declarations and regenerate. +2. Replace the IR entities and bump `IR_VERSION` to 0.2.0. +3. Rewrite compiler lowering and cross-file semantic diagnostics. +4. Update Langium validator checks (preferred≥minimum, verb–kind match, + unique bindings per runtime). +5. Rewrite the resolver as `resolveHarness(bundle, harnessId, runtime?)` + with the execution-model deployability check. +6. Update executors, Pi skill materialization, compare runner/manifest, + highlight grammar, examples, skill contract, and README. +7. Rewrite package tests against the new behavior with equivalent coverage. + +## Test and Review Evidence + +- AC-1/AC-2: `test/compile.test.ts` compiles v0.2 fixtures to schema-valid + IR and asserts each rejection listed above via structured diagnostics. +- AC-3/AC-4/AC-5: `test/resolve.test.ts` covers satisfied, degraded-report, + degraded-fail, below-minimum, programmatic-mismatch, runtime synthesis + from `target`, permission merging with MCP transport grants, and revision + shape/id stability. +- AC-6: `test/exec.test.ts` and `test/skill.test.ts` assert host checks and + preamble/skill output from the new shapes; `test/compare.test.ts` runs the + compare pipeline over a rewritten two-harness experiment. +- AC-7: `npm run build && npm test` in `packages/harness`, plus + `npm run check:generated`. diff --git a/packages/harness/README.md b/packages/harness/README.md index b1c90b6..1e4a921 100644 --- a/packages/harness/README.md +++ b/packages/harness/README.md @@ -1,41 +1,77 @@ # @qoder-ai/harness -Harness as Code, v0.1: a parseable, lockable, explainable **assembly language** +Harness as Code, v0.2: a parseable, lockable, explainable **assembly language** for coding-agent harnesses. It is deliberately *not* a unified execution -language across agent runtimes — v0.1 answers one bounded question: +language across agent runtimes — v0.2 answers one bounded question: -> What did the composition request, what strength did its binding declare, and -> what did the selected v0.1 executor actually materialize? +> What working style did the harness assemble — workflow, agent roles, and +> capability requirements — what strength did each adapter binding declare, +> and what did the selected v0.2 executor actually materialize? -## The five-line harness +## The core resource model -You do not need the full assembly model to start. The smallest useful document -is one capability plus a composition that requires it — no plugin, binding, or -strength boilerplate ([`examples/minimal.harness`](examples/minimal.harness)): +The core DSL has host-neutral semantics only: + +- `harness` — the complete assembly: workflow, agent roles, capability + requirements, and configuration. +- `workflow` — control flow, part of a harness, not the harness itself. +- `agent` — a logical role (`author`, `verifier`). Qoder and Pi are not + agents; they are runtimes. +- `skill` / `tool` / `mcp` — the three capability kinds: progressive + knowledge ([Agent Skills](https://agentskills.io/specification)), atomic + callables, and capability connections. +- `runtime` — a concrete host (Qoder, Pi, DeepSeek Harness, Prime Agent) + with an adapter and an execution style (`tool-calling` or + `programmatic.`). +- `target` — deployment statement selecting a runtime. +- `binding` — the adapter layer mapping one capability onto one runtime. + +There is **no generic `plugin`** in the core DSL. Host plugin and extension +concepts stay in the host's own namespace as binding mechanisms — +`qoder.plugin`, `pi.extension`, `deepseek.plugin`, `prime.python-skill` — +behind the runtime's adapter. + +## The smallest harness + +You do not need the full model to start. The smallest useful document is one +inline skill, one workflow, one agent, and one target — no runtime, binding, +or strength boilerplate ([`examples/minimal.harness`](examples/minimal.harness)): ```harness -component require-tests { - kind policy +skill require-tests { description "Do not report the task complete until tests or a diff review prove it." } -composition my-agent { - target qoder - require require-tests +workflow single-pass { + stop when coder.done } + +harness my-agent { + workflow single-pass + + agent coder { + use skill require-tests + } +} + +target qoder ``` -A bare `require` materializes the component as advisory prompt guidance — the -v0.1 floor — so this resolves and runs as-is. The rest of the language is +A bare `use skill` materializes as advisory prompt guidance — the v0.2 +floor — and a bare `target` synthesizes a tool-calling runtime with the +conventional `@harness/adapter-` package. The rest of the language is *progressive disclosure*: reach for it only when you need more. | When you need… | Add… | | --- | --- | -| A stronger declared mechanism, or an explicit `unsupported` | a `binding` (its `mechanism`/`strength` default to advisory prompt guidance) | -| The same binding on several hosts | `binding x for [pi, qoder] { … }` | -| A distribution boundary / versioned bundle | a `plugin` with `include [ plugin.x@^1 ]` | -| A stricter floor or fatal degradation | a `require x { minimum … on-degrade fail }` block | -| The same assembly on another host | `composition y extends x { target … }` | +| Multi-role control flow | `workflow` edges: `author -> verifier`, `on verifier.failed -> author`, `stop when verifier.passed` | +| A programmatic controller | `workflow x { program deno "./flows/loop.ts" }` plus a runtime with matching `execution programmatic.` | +| Atomic callables | `require tool workspace.read` (undeclared tools become implicit contracts) | +| External capability servers | `mcp registry { transport http url env.REGISTRY_MCP }` + `connect mcp registry` | +| A stronger declared mechanism, or an explicit `unsupported` | a `binding` with a host-namespaced mechanism (`qoder.plugin`, `pi.extension`) | +| The same binding on several runtimes | `binding x for [pi, qoder] { … }` | +| A stricter floor or fatal degradation | `use skill x { minimum … on-degrade fail }` | +| The same assembly on another runtime | another `target` — deployment is a target choice, not a harness rewrite | Authors who want the whole surface at once should read [`examples/standard-coding.harness`](examples/standard-coding.harness); the @@ -46,7 +82,7 @@ sections below document the resolved IR those forms lower into. ```text .harness sources ──parse──▶ Langium AST ──lower──▶ versioned JSON IR (TypeBox) │ - resolveComposition() + resolveHarness(bundle, id, runtime) │ ┌────────────────────┴───────────────────┐ HarnessRevision ResolutionReport @@ -57,15 +93,17 @@ sections below document the resolved IR those forms lower into. (@earendil-works/pi-coding-agent) (@qoder-ai/qoder-agent-sdk) ``` -## The v0.1 entities +## The v0.2 entities | Entity | Role | | --- | --- | -| `ComponentContract` | What a capability is, needs, and produces — host-independent | -| `TargetBinding` | The strongest mechanism a host binding declares (`unsupported` / `advisory` / `wired` / `enforced`) | -| `PluginManifest` | Distribution boundary: contracts plus their bindings | -| `CompositionSpec` | Desired assembly for a run target, with strength floors and `on-degrade` policy | -| `HarnessRevision` | Immutable resolution fact: exact versions, hashes, declared and materialized strengths, and permissions | +| `SkillIr` / `ToolIr` / `McpIr` | The three capability kinds — host-independent contracts | +| `WorkflowIr` | Declarative graph (edges, events, stops) or programmatic controller (`program`) | +| `RuntimeIr` | A concrete host: adapter package plus `tool-calling` or `programmatic.` execution | +| `HarnessSpecIr` | The assembly: workflow reference, agent roles with capability requirements, settings | +| `TargetIr` | Deployment statement selecting a runtime (adapter synthesized when omitted) | +| `CapabilityBindingIr` | Adapter mapping: capability × runtime × mechanism × declared strength | +| `HarnessRevision` | Immutable resolution fact: hashes, runtime execution, per-(agent, capability) strengths, permissions | | `ResolutionReport` | Requested, declared, and materialized strength for every requirement | All IR documents carry `irVersion` and a `kind` discriminator and are defined @@ -77,7 +115,7 @@ as TypeBox schemas in ```ts import { compileHarness, - resolveComposition, + resolveHarness, QoderSdkExecutor, highlightHarness, } from "@qoder-ai/harness"; @@ -87,9 +125,10 @@ if (!compiled.bundle) { throw new Error(compiled.diagnostics.map((item) => item.message).join("\n")); } -const { revision, report } = resolveComposition( +const { revision, report } = resolveHarness( compiled.bundle, - "standard-coding-qoder", + "standard-coding", + "qoder", ); if (!revision) { @@ -111,20 +150,43 @@ See [`examples/standard-coding.harness`](examples/standard-coding.harness) for the full surface syntax. [`examples/full-surface.harness`](examples/full-surface.harness) is the compiler -conformance fixture: it deliberately exercises every v0.1 component kind, -permission, strength, degradation policy, and configuration value type. +conformance fixture: it deliberately exercises every v0.2 capability kind, +transport, execution style, permission, strength, degradation policy, and +configuration value type. + +## Workflow and execution are independent dimensions + +How a workflow is *implemented* (declarative graph vs `program`) is +independent of how a runtime *exposes capabilities* (tool calling vs +programmatic calling). Declarative workflows deploy to any runtime. A +programmatic workflow only resolves against a runtime whose execution is +`programmatic` in the same language: + +```harness +runtime prime { + adapter "@harness/adapter-prime" + execution programmatic.python { + repl persistent + } +} +``` + +An arbitrary Deno workflow is never silently translated into Prime's Python; +resolution fails with instructions to author in the runtime's native language, +restrict deployment targets, or drive the runtime externally over +[ACP](https://agentclientprotocol.com/get-started/introduction). ## Compare real coding outcomes The DSL describes an assembled agent harness; it does not make a successful SDK response equivalent to a successful coding task. The companion -`harness-compare.v1` manifest freezes the task repository, two composition ids, +`harness-compare.v1` manifest freezes the task repository, two harness ids, runtime policy, trial count, and deterministic grader. Each variant/trial runs in a separate temporary Git repository and retains the resulting patch, redacted SDK trace, runtime receipt, permission decisions, validation details, metrics, and aggregate verdict. -The included benchmark asks both compositions to create a repository-grounded +The included benchmark asks both harnesses to create a repository-grounded `README.md` and validates the changed-file scope, Markdown structure, local links, package exports, documented behavior, executable ESM Quick Start, stale or invented claims, and the package's existing tests: @@ -141,16 +203,16 @@ The README task is one benchmark, not proof of general coding performance. The shared task prompt states only the goal and the runtime tool policy. What a good README contains — required sections, grounding rules, the consumer install -command, the executable Quick Start — lives in the candidate composition's -components, which the executor materializes as harness guidance. Keeping that +command, the executable Quick Start — lives in the candidate harness's +skills, which the executor materializes as harness guidance. Keeping that expectation out of the prompt is what makes the measured difference attributable -to the composition, so the baseline is expected to score lower. The runtime +to the harness, so the baseline is expected to score lower. The runtime profile experiment below is the opposite case: its two arms share one -composition, so its prompt states the task in full and only the runtime profile +harness, so its prompt states the task in full and only the runtime profile varies. To isolate runtime-tool effects from harness-policy effects, the companion -profile experiment holds the grounded composition and task constant. Its +profile experiment holds the grounded harness and task constant. Its baseline uses the manifest's six bounded coding tools; its candidate uses the named `qoder-minimal-v1` profile: @@ -186,8 +248,8 @@ timed-out command is stopped together with the processes it started. The package includes [`skills/generate-harness-dsl/SKILL.md`](skills/generate-harness-dsl/SKILL.md), -which teaches compatible coding agents to generate complete v0.1 documents and -validate every composition with the same compiler and resolver exported by this +which teaches compatible coding agents to generate complete v0.2 documents and +validate every harness with the same compiler and resolver exported by this package. Its validator is also available directly after the package is built: ```sh @@ -196,14 +258,15 @@ node skills/generate-harness-dsl/scripts/validate.mjs workflow.harness ## Executor honesty rules -v0.1 executors materialize components as prompt guidance, so effective strength -is capped at `advisory`. A binding can declare a future `wired` or `enforced` -mechanism, but resolution compares the composition's minimum and preferred -strength against the advisory materialization. Below-minimum assemblies fail -before execution; allowed degradation is visible in the report and run warnings. +v0.2 executors materialize capabilities as prompt guidance, so effective +strength is capped at `advisory`. A binding can declare a future `wired` or +`enforced` host-native mechanism (`qoder.plugin`, `pi.extension`), but +resolution compares the requirement's minimum and preferred strength against +the advisory materialization. Below-minimum assemblies fail before execution; +allowed degradation is visible in the report and run warnings. -Executors reject revisions targeting another host. `materializePiPackage()` -emits an installable Pi package only for advisory, skill-kind components and +Executors reject revisions targeting another runtime. `materializePiPackage()` +emits an installable Pi package only for advisory skill capabilities and stamps every generated skill with revision provenance. The Qoder executor uses `@qoder-ai/qoder-agent-sdk` and defaults to @@ -216,9 +279,9 @@ SDK (`@earendil-works/pi-coding-agent`, supported `^0.84.2`) is an optional peer dependency; its executor loads it lazily and reports install guidance when missing. -Native hooks and extensions are declaration-only in v0.1. Until a native -materialization receipt exists, they do not upgrade effective strength beyond -`advisory`. +Native plugins, extensions, and MCP connections are declaration-only in v0.2. +Until a native materialization receipt exists, they do not upgrade effective +strength beyond `advisory`. ## Development @@ -230,6 +293,8 @@ npm run harness:test ``` The package supports the repository's Node range (`>=22.20.0 <25`). See the +[v0.2 resource-model spec](https://github.com/QoderAI/better-harness/blob/main/docs/specs/2026-08-15-harness-dsl-v0.2-resource-model.md) +and the [v0.1 production-readiness spec](https://github.com/QoderAI/better-harness/blob/main/docs/specs/2026-08-14-harness-as-code-v0.1.md) for acceptance scenarios, non-goals, and validation evidence. The named minimal profile and its isolated comparison are specified in diff --git a/packages/harness/examples/full-surface.harness b/packages/harness/examples/full-surface.harness index 14c6a03..fd492df 100644 --- a/packages/harness/examples/full-surface.harness +++ b/packages/harness/examples/full-surface.harness @@ -1,88 +1,74 @@ -// Compiler conformance fixture: every v0.1 authored syntax branch appears here. +// Compiler conformance fixture: every v0.2 authored syntax branch appears here. -component repository-skill { - kind skill +skill repository-grounding { + source "./skills/repository-grounding" description "Read repository evidence before changing files." - input { repository task } - output { evidence } permissions { workspace read workspace write } } -component file-tool { - kind tool - permissions { process allow } +skill inline-guidance { + description "Prefer small, verifiable steps." } -component test-program { - kind program - permissions { process deny } +tool workspace.read { + description "Read files inside the workspace." + input { path } + output { contents } + permissions { workspace read } } -component coding-workflow { - kind workflow - permissions { network allow } +tool process.exec { + description "Run a workspace-scoped process." + permissions { process allow } } -component completion-hook { - kind hook - permissions { network deny } +mcp package-registry { + transport http + url env.PACKAGE_REGISTRY_MCP } -component evidence-policy { - kind policy - permissions { model allow } +mcp docs-search { + transport sse + url "https://docs.example.test/mcp" } -component trace-observer { - kind observer - permissions { model deny } +mcp local-indexer { + transport stdio + command "indexer --stdio" } -component result-ui { - kind ui - permissions { workspace allow } +workflow coding-loop { + author -> verifier + on verifier.failed -> author + stop when verifier.passed } -binding repository-skill for qoder { - mechanism prompt-preamble - strength advisory - notes "Prompt guidance is the v0.1 materialization boundary." -} -binding file-tool for qoder { mechanism unavailable strength unsupported } -binding test-program for qoder { mechanism sdk-tool strength wired } -binding coding-workflow for qoder { mechanism native-workflow strength enforced } -binding completion-hook for qoder { mechanism prompt-preamble strength advisory } -binding evidence-policy for qoder { mechanism sdk-policy strength wired } -binding trace-observer for qoder { mechanism unavailable strength unsupported } -binding result-ui for qoder { mechanism native-view strength enforced } - -plugin full-surface { - version "1.2.3" - provides [ - component.repository-skill, - component.file-tool, - component.test-program, - component.coding-workflow, - component.completion-hook, - component.evidence-policy, - component.trace-observer, - component.result-ui - ] -} - -composition full-surface-qoder { - target qoder - include [ plugin.full-surface@^1.0.0 ] - require repository-skill { - preferred advisory - minimum advisory - on-degrade fail +workflow scripted-loop { + program deno "./flows/coding-loop.ts" +} + +harness full-surface { + workflow coding-loop + + agent author { + use skill repository-grounding + use skill inline-guidance + require tool workspace.read + // Implicit tool: never declared, synthesized as an atomic contract. + require tool workspace.write + connect mcp package-registry + connect mcp local-indexer } - require test-program { - preferred wired - minimum advisory - on-degrade report + + agent verifier { + require tool process.exec { + preferred wired + minimum advisory + on-degrade report + } + connect mcp docs-search } + configure { runtime.max-turns = 24 runtime.timeout = 10m @@ -91,3 +77,48 @@ composition full-surface-qoder { runtime.network-enabled = false } } + +harness scripted-surface { + workflow scripted-loop + + agent driver { + require tool process.exec + } +} + +runtime prime { + adapter "@harness/adapter-prime" + execution programmatic.python { + repl persistent + } +} + +runtime deepseek { + adapter "@harness/adapter-deepseek" + execution programmatic.typescript { + mode code + } +} + +runtime pi { + adapter "@harness/adapter-pi" + execution tool-calling +} + +target qoder uses adapter.qoder +target pi + +// Host-native assets stay in the host's own namespace behind the adapter. +binding repository-grounding for qoder { + mechanism prompt-preamble + strength advisory + notes "Prompt guidance is the v0.2 materialization boundary." +} +binding repository-grounding for [pi, prime] { + mechanism pi.skill-package + strength advisory +} +binding workspace.read for qoder { mechanism qoder.plugin strength wired } +binding process.exec for deepseek { mechanism deepseek.plugin strength enforced } +binding package-registry for pi { mechanism pi.extension strength wired } +binding inline-guidance for qoder { mechanism unavailable strength unsupported } diff --git a/packages/harness/examples/minimal.harness b/packages/harness/examples/minimal.harness index 1b6cab4..58fc16a 100644 --- a/packages/harness/examples/minimal.harness +++ b/packages/harness/examples/minimal.harness @@ -1,14 +1,24 @@ -// The smallest useful harness. One policy, applied as advisory prompt -// guidance — no plugin, binding, or strength boilerplate. A bare `require` -// asks for the component at the v0.1 floor; the omitted binding is exactly -// what a hand-written `binding … { strength advisory }` would declare. +// The smallest useful harness. One agent role, one inline skill, applied as +// advisory prompt guidance — no runtime, binding, or strength boilerplate. +// A bare `use skill` asks for the v0.2 floor; the omitted binding is exactly +// what a hand-written `binding … { strength advisory }` would declare, and +// the bare `target` synthesizes a tool-calling runtime with a conventional +// adapter package. -component require-tests { - kind policy +skill require-tests { description "Do not report the task complete until tests or a diff review prove it." } -composition my-agent { - target qoder - require require-tests +workflow single-pass { + stop when coder.done } + +harness my-agent { + workflow single-pass + + agent coder { + use skill require-tests + } +} + +target qoder diff --git a/packages/harness/examples/readme-compare/prompt.md b/packages/harness/examples/readme-compare/prompt.md index acdc2ea..70f2023 100644 --- a/packages/harness/examples/readme-compare/prompt.md +++ b/packages/harness/examples/readme-compare/prompt.md @@ -1,6 +1,6 @@ Create `README.md` for this package. Change no other repository file. -Runtime policy for this environment, identical for every composition: the +Runtime policy for this environment, identical for every harness: the bounded Bash policy accepts these validation commands exactly: `npm test`, `npm run example`, `node --test`, `node examples/basic.mjs`, `node ./examples/basic.mjs`, `git status --short`, and `git diff --check`. diff --git a/packages/harness/examples/readme-compare/readme-compare.harness b/packages/harness/examples/readme-compare/readme-compare.harness index 5f9a6ff..8a9e780 100644 --- a/packages/harness/examples/readme-compare/readme-compare.harness +++ b/packages/harness/examples/readme-compare/readme-compare.harness @@ -1,65 +1,55 @@ -component coding-loop { - kind workflow +// README-writing comparison: a baseline coding loop against the same loop +// grounded in repository evidence. Both harnesses share one workflow and +// deploy to the Qoder runtime; the compare manifest picks one harness per +// variant. + +skill coding-loop-discipline { description "Inspect the repository, make the requested scoped change, run relevant validation, and report concrete evidence." - input { task repository } - output { patch validation } permissions { workspace read workspace write process allow network deny } } -component repository-grounding { - kind skill +skill repository-grounding { description "Before writing documentation, derive every claim from package metadata, public source, executable examples, and current tests; treat archived documentation as stale evidence; state the real retry defaults and abort behavior precisely instead of approximating them." - input { package-metadata public-source tests examples } - output { fact-map } permissions { workspace read network deny } } -component readme-only-scope { - kind policy +skill readme-only-scope { description "Change only README.md, and give it exactly the sections a consumer needs in this order: Purpose, Installation, Quick Start, API, Behavior, Verification. Installation shows the consumer command `npm install `. Do not invent commands, module formats, badges, URLs, defaults, or capabilities that current repository evidence does not support." - input { fact-map } - output { scoped-readme } permissions { workspace write network deny } } -component executable-readme-verification { - kind workflow +skill executable-readme-verification { description "Write Quick Start as an executable ESM JavaScript example that imports the package by name, then run the package tests and that example before completing; repair the documentation until both pass." - input { scoped-readme } - output { verification } permissions { process allow workspace read network deny } } -binding coding-loop for qoder { mechanism prompt-preamble strength advisory } -binding repository-grounding for qoder { mechanism prompt-preamble strength advisory } -binding readme-only-scope for qoder { mechanism prompt-preamble strength advisory } -binding executable-readme-verification for qoder { mechanism prompt-preamble strength advisory } - -plugin coding-core { - version "1.0.0" - provides [ component.coding-loop ] +workflow readme-loop { + writer -> writer + stop when writer.done } -plugin grounded-readme { - version "1.0.0" - provides [ - component.repository-grounding, - component.readme-only-scope, - component.executable-readme-verification - ] -} +harness readme-baseline { + workflow readme-loop -composition readme-baseline { - target qoder - include [ plugin.coding-core@1 ] - require coding-loop { minimum advisory } + agent writer { + use skill coding-loop-discipline { minimum advisory } + } } -composition readme-grounded { - target qoder - include [ plugin.coding-core@1, plugin.grounded-readme@1 ] - require coding-loop { minimum advisory } - require repository-grounding { minimum advisory } - require readme-only-scope { minimum advisory } - require executable-readme-verification { minimum advisory } +harness readme-grounded { + workflow readme-loop + + agent writer { + use skill coding-loop-discipline { minimum advisory } + use skill repository-grounding { minimum advisory } + use skill readme-only-scope { minimum advisory } + use skill executable-readme-verification { minimum advisory } + } } + +binding coding-loop-discipline for qoder { mechanism prompt-preamble strength advisory } +binding repository-grounding for qoder { mechanism prompt-preamble strength advisory } +binding readme-only-scope for qoder { mechanism prompt-preamble strength advisory } +binding executable-readme-verification for qoder { mechanism prompt-preamble strength advisory } + +target qoder uses adapter.qoder diff --git a/packages/harness/examples/standard-coding.harness b/packages/harness/examples/standard-coding.harness index fc69dc6..6a4193a 100644 --- a/packages/harness/examples/standard-coding.harness +++ b/packages/harness/examples/standard-coding.harness @@ -1,79 +1,72 @@ -// Example harness assembly: contracts, host bindings, plugins, and one -// composition targeting the Pi host. +// Example assembly: one coding loop with author/verifier roles, capability +// requirements with the degradation ladder, adapter bindings onto host-native +// assets, and deployment targets for the Pi and Qoder runtimes. -component impact-analysis { - kind skill +skill impact-analysis { description "Before editing, map the blast radius of the change and list affected modules." - input { changed-files } - output { impact-report } permissions { workspace read } } -component verification-before-complete { - kind policy +skill verification-before-complete { description "Do not declare the task complete until verification evidence (tests or diff review) exists." - input { verification-evidence } permissions { workspace read network deny } } -binding impact-analysis for pi { - mechanism skill-package - strength advisory +workflow coding-loop { + author -> verifier + on verifier.failed -> author + stop when verifier.passed +} + +harness standard-coding { + workflow coding-loop + + agent author { + use skill impact-analysis + require tool workspace.read + require tool workspace.write + } + + agent verifier { + use skill verification-before-complete { + preferred enforced + minimum advisory + on-degrade report + } + require tool process.exec + } + + configure { + shell.timeout = 60s + tool-call-budget = 80 + } } -binding impact-analysis for qoder { - mechanism skill-routing +// Adapter layer: how each runtime realizes the capabilities, in the host's +// own namespace. The core DSL has no generic plugin; `pi.extension` and +// `qoder.system-instruction` are host-native assets behind the adapter. +binding impact-analysis for [pi, qoder] { + mechanism prompt-preamble strength advisory } binding verification-before-complete for pi { - mechanism extension-event + mechanism pi.extension strength enforced } binding verification-before-complete for qoder { - mechanism system-instruction + mechanism qoder.system-instruction strength advisory notes "Qoder CLI print mode cannot intercept completion; the policy is prompt-injected." } -plugin repository-understanding { - version "1.4.2" - provides [ component.impact-analysis ] -} - -plugin change-verification { - version "2.1.3" - provides [ component.verification-before-complete ] -} - -composition standard-coding { - target pi - - include [ - plugin.repository-understanding@^1, - plugin.change-verification@^2 - ] - - require verification-before-complete { - preferred enforced - minimum advisory - on-degrade report - } - - configure { - shell.timeout = 60s - tool-call-budget = 80 - } -} - -// Same assembly, different host. `extends` inherits the includes, -// requirements, and configuration above; only the target changes. -composition standard-coding-qoder extends standard-coding { - target qoder -} +// Same assembly, different runtime: deployment is a target choice, not a +// harness rewrite. +target pi uses adapter.pi +target qoder uses adapter.qoder diff --git a/packages/harness/skills/generate-harness-dsl/SKILL.md b/packages/harness/skills/generate-harness-dsl/SKILL.md index 3f51ad8..c62c635 100644 --- a/packages/harness/skills/generate-harness-dsl/SKILL.md +++ b/packages/harness/skills/generate-harness-dsl/SKILL.md @@ -1,49 +1,57 @@ --- name: generate-harness-dsl -description: Generate, revise, or review complete Harness as Code `.harness` files for coding-agent compositions. Use when a user asks to describe components, host bindings, plugins, composition requirements, permissions, degradation policy, or typed settings in the `@qoder-ai/harness` DSL, or when existing Harness DSL must be made compiler-valid and resolvable. +description: Generate, revise, or review complete Harness as Code `.harness` files for coding-agent harnesses. Use when a user asks to describe workflows, agent roles, skills, tools, MCP connections, runtimes, adapter bindings, permissions, degradation policy, or typed settings in the `@qoder-ai/harness` DSL, or when existing Harness DSL must be made compiler-valid and resolvable. --- # Generate Harness DSL -Create a complete, standalone Harness as Code v0.1 document and prove it with -the package compiler and resolver. Keep declared host capability separate from -what the v0.1 runtime actually materializes. +Create a complete, standalone Harness as Code v0.2 document and prove it with +the package compiler and resolver. Keep declared adapter capability separate +from what the v0.2 runtime actually materializes. ## Workflow -1. Establish the requested coding-agent host, capabilities, permissions, - minimum and preferred strengths, degradation behavior, and configuration. - Infer low-risk details from context; ask only when a missing choice changes - the safety or meaning of the composition. +1. Establish the requested runtimes, workflow shape, agent roles, capabilities + (skills, tools, MCP connections), permissions, minimum and preferred + strengths, degradation behavior, and configuration. Infer low-risk details + from context; ask only when a missing choice changes the safety or meaning + of the harness. 2. Read [the DSL contract](references/dsl-contract.md) before authoring. Start - from the smallest form that expresses the intent — often a component plus a - composition with a bare `require`, as in + from the smallest form that expresses the intent — often one inline skill, + a single-agent workflow, and a bare `target`, as in [the minimal example](../../examples/minimal.harness). Open [the complete example](../../examples/standard-coding.harness) only when - plugins, explicit bindings, or degradation policy are actually needed. + multi-role workflows, explicit bindings, or degradation policy are actually + needed. 3. Generate one self-contained `.harness` document unless the user explicitly - requests a fragment. Declare each referenced component and plugin in the - same document. + requests a fragment. Declare each referenced skill, MCP entry, workflow, + and runtime in the same document; only tools may stay implicit. 4. Validate the result with `scripts/validate.mjs`. Fix every compiler or resolution error, rerun validation, and inspect degraded realizations before presenting the result. -5. Return the DSL or saved file plus a short summary of the target, - composition id, included plugins, resolution status, and any degradation. +5. Return the DSL or saved file plus a short summary of the harness id, target + runtimes, resolution status, and any degradation. ## Authoring Rules -- Prefer the least syntax that expresses the intent. Add a `binding`, `plugin`, - or a `require` block only when the default advisory floor is insufficient; - reuse an assembly across hosts with `extends` rather than copying it. -- Use descriptive, stable identifiers and exact semantic versions for plugin - declarations. Use a compatible version range in each composition include. -- Declare every component referenced by a binding, plugin, or requirement. - Do not duplicate ids, bindings, plugin includes, requirements, inputs, - outputs, or configuration keys. -- Request only permissions the component needs. Never place credentials, - tokens, private keys, or secret values in DSL source. +- Prefer the least syntax that expresses the intent. Add a `binding`, + `runtime`, or a requirement block only when the default advisory floor is + insufficient; deploy an assembly to another host with a second `target` + rather than copying the harness. +- Never author a generic plugin: the core DSL has none. Host-native assets + belong in binding mechanisms under the host's namespace, such as + `qoder.plugin`, `pi.extension`, `deepseek.plugin`, or `prime.python-skill`. +- Match the requirement verb to the capability kind: `use skill`, + `require tool`, `connect mcp`. Declare every skill and MCP entry; an + undeclared `require tool` synthesizes an implicit atomic contract. +- Keep workflow and execution independent: a declarative graph deploys to any + runtime, while a `program ` workflow resolves only against a + runtime declaring `execution programmatic.`. +- Request only permissions the capability needs. Never place credentials, + tokens, private keys, or secret values in DSL source. Reference MCP + endpoints through `env.VARIABLE` rather than embedding URLs with secrets. - Treat `wired` and `enforced` as binding declarations, not observed runtime - guarantees. Harness v0.1 materializes supported bindings as + guarantees. Harness v0.2 materializes supported bindings as `prompt-preamble` at no more than `advisory` strength. - Use `on-degrade fail` when falling below the preferred strength must stop resolution. Use `report` only when the weaker materialization is acceptable @@ -54,16 +62,16 @@ what the v0.1 runtime actually materializes. ## Validate Run from this skill directory, substituting the output file and optionally one -or more composition ids: +or more harness ids: ```sh -node scripts/validate.mjs /path/to/workflow.harness [composition-id ...] +node scripts/validate.mjs /path/to/workflow.harness [harness-id ...] ``` The command prints JSON and exits non-zero when compilation or any selected -composition resolution fails. If no composition id is supplied, it resolves -every composition in the file. A successful exit is required before calling -generated DSL valid. +harness resolution fails. If no harness id is supplied, it resolves every +harness in the file against every declared target (or runtime). A successful +exit is required before calling generated DSL valid. When editing this skill itself, build the package first so `dist/` reflects the current compiler: diff --git a/packages/harness/skills/generate-harness-dsl/agents/openai.yaml b/packages/harness/skills/generate-harness-dsl/agents/openai.yaml index 9fc0a93..51ac34c 100644 --- a/packages/harness/skills/generate-harness-dsl/agents/openai.yaml +++ b/packages/harness/skills/generate-harness-dsl/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Generate Harness DSL" short_description: "Generate and validate Harness as Code DSL" - default_prompt: "Use $generate-harness-dsl to create a validated Harness as Code composition for my coding-agent workflow." + default_prompt: "Use $generate-harness-dsl to create a validated Harness as Code harness for my coding-agent workflow." diff --git a/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md b/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md index 3bfeca5..a183045 100644 --- a/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md +++ b/packages/harness/skills/generate-harness-dsl/references/dsl-contract.md @@ -1,179 +1,231 @@ -# Harness as Code v0.1 contract +# Harness as Code v0.2 contract Use this reference as the authoring contract. The grammar and runtime remain owned by `packages/harness/src/`; this file is a compact generation guide. - [Document model](#document-model) -- [Component](#component) -- [Host binding](#host-binding) -- [Plugin](#plugin) -- [Composition](#composition) +- [Harness and agents](#harness-and-agents) +- [Workflow](#workflow) +- [Capabilities: skill, tool, mcp](#capabilities-skill-tool-mcp) +- [Runtime and target](#runtime-and-target) +- [Adapter binding](#adapter-binding) - [Semantic checklist](#semantic-checklist) ## Document model -The smallest useful document is a component plus a composition that requires it: +The smallest useful document is one inline skill, a single-agent workflow, a +harness, and a target: ```harness -component require-tests { - kind policy +skill require-tests { description "Do not report the task complete until tests prove it." } -composition my-agent { - target qoder - require require-tests +workflow single-pass { + stop when coder.done } + +harness my-agent { + workflow single-pass + + agent coder { + use skill require-tests + } +} + +target qoder ``` -A bare `require` with no plugin or binding materializes the component as -advisory prompt guidance — the v0.1 floor. Reach for the fuller model below only -when you need distribution boundaries or a stronger declared mechanism: +A bare `use skill` with no binding materializes as advisory prompt guidance — +the v0.2 floor — and a bare `target` synthesizes a tool-calling runtime with +the conventional `@harness/adapter-` package. Reach for the fuller model +below only when you need multi-role flow, host-native mechanisms, or +programmatic execution. -1. `component`: a host-independent capability contract. -2. `binding` (optional): how one host declares that it can realize a component. - Omit it to accept the advisory floor; write it to declare a stronger future - mechanism or an explicit `unsupported`. -3. `plugin` (optional): an exact-version distribution that provides components. - Omit it when a composition `require`s the component directly. -4. `composition`: the target host, optional plugin ranges, requirements, and - settings. +The core DSL defines **no generic plugin**. Host plugin and extension concepts +appear only as binding mechanisms in the host's own namespace +(`qoder.plugin`, `pi.extension`, `deepseek.plugin`, `prime.python-skill`). -Comments use `//` or `/* ... */`. Whitespace is insignificant. Identifiers must -match `[_a-zA-Z][\w-]*`; prefer descriptive kebab-case identifiers and avoid -language keywords such as `component`, `binding`, `plugin`, `composition`, -`report`, `fail`, `true`, and `false`. +Comments use `//` or `/* ... */`. Whitespace is insignificant. Identifiers +match `[_a-zA-Z][\w-]*`; tool ids, binding mechanisms, and configuration keys +may be dotted (`workspace.read`, `qoder.plugin`, `runtime.timeout`). Prefer +descriptive kebab-case identifiers. Strings use double quotes and do not support embedded escaped double quotes. Rephrase descriptions that would otherwise require one. -## Component +## Harness and agents ```harness -component repository-analysis { - kind skill - description "Map affected modules before editing." - input { changed-files } - output { impact-summary } - permissions { - workspace read - network deny +harness standard-coding { + workflow coding-loop + + agent author { + use skill impact-analysis + require tool workspace.read + connect mcp package-registry + } + + agent verifier { + require tool process.exec { + preferred enforced + minimum advisory + on-degrade report + } + } + + configure { + shell.timeout = 60s + tool-call-budget = 80 } } ``` -Kinds are `skill`, `tool`, `program`, `workflow`, `hook`, `policy`, `observer`, -or `ui`. Permission domains are `workspace`, `process`, `network`, and `model`; -access values are `read`, `write`, `allow`, and `deny`. Do not use commas inside -`input`, `output`, or `permissions` blocks. +A harness names one workflow, declares its logical agent roles, and holds run +configuration. Agents are roles (`author`, `verifier`) — hosts like Qoder or +Pi are runtimes, never agents. The requirement verb states the capability +kind: `use skill`, `require tool`, `connect mcp`; the verb must match the +declared kind. -## Host binding +A requirement block is optional. A bare requirement asks for the capability at +`minimum advisory` with `on-degrade report`. Add the block to raise `minimum`, +state a `preferred` ceiling, or set `on-degrade fail`. Resolution behaves as +follows: -```harness -binding repository-analysis for qoder { - mechanism skill-routing - strength advisory - notes "Injected as prompt guidance in v0.1." -} -``` +- realized below minimum: fail; +- realized below preferred with `on-degrade fail`: fail; +- realized below preferred with `on-degrade report`: resolve as degraded; +- realized at or above preferred: satisfy. -The host and mechanism are identifiers. Strength is `unsupported`, `advisory`, -`wired`, or `enforced`. Declare at most one binding per component and host. +Configuration keys are dotted or hyphenated identifiers. Values may be +non-negative integers, booleans, strings, or integer durations ending in +`ms`, `s`, `m`, or `h`. -`mechanism` and `strength` are optional and default to `prompt-preamble` at -`advisory` — an empty `binding x for pi {}` is the advisory floor. Bind several -hosts with the same declaration using a list: +## Workflow + +Declarative graph — agent-to-agent edges, event routes, and stop conditions: ```harness -binding repository-analysis for [pi, qoder] { strength advisory } +workflow coding-loop { + author -> verifier + on verifier.failed -> author + stop when verifier.passed +} ``` -A binding strength states the strongest host capability being modeled. It does -not prove that the current executor materialized that capability. In v0.1, a -supported binding resolves through `prompt-preamble` at `advisory`; a missing or -`unsupported` binding realizes as `unsupported`. - -## Plugin +Programmatic controller — control flow lives in a program: ```harness -plugin repository-tools { - version "1.2.0" - provides [ component.repository-analysis ] +workflow scripted-loop { + program deno "./flows/coding-loop.ts" } ``` -Use a full, valid semantic version such as `1.2.0`. Separate multiple provided -components with commas. Every provided component must be declared. +A workflow declares exactly one of the two forms. Every agent a workflow +references must be declared by each harness that uses it. A programmatic +workflow resolves only against a runtime whose execution is +`programmatic.`; declarative workflows deploy anywhere. -## Composition +## Capabilities: skill, tool, mcp ```harness -composition repository-change { - target qoder - - include [ plugin.repository-tools@^1 ] - - require repository-analysis { - preferred advisory - minimum advisory - on-degrade fail +skill repository-analysis { + source "./skills/repository-analysis" + description "Map affected modules before editing." + permissions { + workspace read + network deny } +} - configure { - shell.timeout = 60s - tool-call-budget = 80 - explain-degradation = true - review-label = "repository change" - } +tool workspace.read { + description "Read files inside the workspace." + input { path } + output { contents } + permissions { workspace read } +} + +mcp package-registry { + transport http + url env.PACKAGE_REGISTRY_MCP } ``` -Include ranges accept a numeric version optionally prefixed by `^` or `~`, for -example `@1`, `@^1`, `@~1.2`, or `@1.2.0`. Do not include the same plugin twice. +A skill needs `source`, `description`, or both. Tools referenced by +`require tool` without a declaration become implicit atomic contracts. +An MCP entry is a connection, not a tool: `transport` is `stdio` (requires +`command`), `http`, or `sse` (both require `url`, either a string or +`env.VARIABLE`). Permission domains are `workspace`, `process`, `network`, +and `model`; access values are `read`, `write`, `allow`, and `deny`. Do not +use commas inside `input`, `output`, or `permissions` blocks. MCP transports +imply their connection grant at resolution (`stdio` → process, `http`/`sse` +→ network). -`include` is optional: a composition may `require` a component declared in the -document directly, with no plugin wrapper. `target` is required unless inherited -through `extends`. +## Runtime and target -A requirement block is optional. A bare `require component` asks for the -component at `minimum advisory` with `on-degrade report`. Add the block to raise -`minimum`, state a `preferred` ceiling, or set `on-degrade fail`. Resolution -behaves as follows: +```harness +runtime prime { + adapter "@harness/adapter-prime" + execution programmatic.python { + repl persistent + } +} -- realized below minimum: fail; -- realized below preferred with `on-degrade fail`: fail; -- realized below preferred with `on-degrade report`: resolve as degraded; -- realized at or above preferred: satisfy. +target prime +target qoder uses adapter.qoder +``` -Configuration keys are dotted or hyphenated identifiers. Values may be -non-negative integers, booleans, strings, or integer durations ending in -`ms`, `s`, `m`, or `h`. +A runtime is a concrete host with its adapter package and capability-calling +style: `tool-calling` (default) or `programmatic.` with optional +key/value options. A `target` deploys the document's harnesses to a runtime; +without a runtime block it synthesizes a tool-calling runtime whose adapter is +`@harness/adapter-` (using the `uses adapter.` shorthand when given). +Do not add `uses adapter` to a target whose runtime block already declares an +adapter. -Reuse one assembly across hosts with `extends` instead of copying it. The child -inherits the base target, includes, requirements, and settings, and overrides -`target` (and any member sharing a plugin id, component id, or setting key): +## Adapter binding ```harness -composition on-qoder extends repository-change { - target qoder +binding repository-analysis for qoder { + mechanism qoder.plugin + strength wired + notes "Realized natively once the adapter ships." } ``` -The `extends` chain must be acyclic, and every composition must end up with a -`target`, its own or inherited. +The runtime is an identifier; the mechanism is a dotted name so host-native +assets stay in the host's namespace. Strength is `unsupported`, `advisory`, +`wired`, or `enforced`. Declare at most one binding per capability and +runtime. + +`mechanism` and `strength` are optional and default to `prompt-preamble` at +`advisory` — an empty `binding x for pi {}` is the advisory floor. Bind +several runtimes with one declaration using a list: + +```harness +binding repository-analysis for [pi, qoder] { strength advisory } +``` + +A binding strength states the strongest adapter capability being modeled. It +does not prove that the current executor materialized that capability. In +v0.2, a supported binding resolves through `prompt-preamble` at `advisory`; an +`unsupported` binding realizes as `unsupported` and fails any requirement. ## Semantic checklist -- Declare every referenced component. Declare a plugin only when you need a - distribution boundary; a directly `require`d component needs none. -- Give every plugin declaration an exact semantic version. -- Keep declaration ids, bindings, includes, requirements, inputs, outputs, and - configuration keys unique within their applicable scope. -- Bind required components for the composition target only to declare a stronger +- Declare every referenced skill, MCP entry, workflow, and runtime; only + tools may stay implicit. Never declare a generic plugin. +- Match requirement verbs to capability kinds (`use skill`, `require tool`, + `connect mcp`). +- Keep declaration ids, bindings, targets, agents, requirements, inputs, + outputs, and configuration keys unique within their applicable scope. +- Ensure every agent named by a workflow exists in each harness using it. +- Pair programmatic workflows with a runtime declaring the same + `programmatic.` execution, or keep the workflow declarative. +- Bind capabilities for a runtime only to declare a stronger host-native mechanism or an explicit `unsupported`; the advisory floor needs no binding. -- Give every composition a `target`, directly or through `extends`, with no - `extends` cycle. -- Set minimum and degradation policy against v0.1's actual advisory ceiling. -- Keep secrets out of source and descriptions. +- Set minimum and degradation policy against v0.2's actual advisory ceiling. +- Keep secrets out of source and descriptions; reference MCP endpoints via + `env.VARIABLE`. - Run `scripts/validate.mjs` and inspect every realization, not only the exit code. diff --git a/packages/harness/skills/generate-harness-dsl/scripts/validate.mjs b/packages/harness/skills/generate-harness-dsl/scripts/validate.mjs index 6431f59..1aeb526 100644 --- a/packages/harness/skills/generate-harness-dsl/scripts/validate.mjs +++ b/packages/harness/skills/generate-harness-dsl/scripts/validate.mjs @@ -9,7 +9,7 @@ function print(value) { function usage() { process.stderr.write( - "Usage: node scripts/validate.mjs [composition-id ...]\n", + "Usage: node scripts/validate.mjs [harness-id ...]\n", ); } @@ -32,7 +32,7 @@ async function main() { const inputPath = resolve(input); const text = await readFile(inputPath, "utf8"); - const { compileHarness, resolveComposition } = await loadHarnessApi(); + const { compileHarness, resolveHarness } = await loadHarnessApi(); const source = `harness://input/${encodeURIComponent(basename(inputPath))}`; const compiled = await compileHarness([{ uri: source, text }]); @@ -41,30 +41,38 @@ async function main() { valid: false, file: input, diagnostics: compiled.diagnostics, - compositions: [], + harnesses: [], }); process.exitCode = 1; return; } - const compositionIds = requestedIds.length > 0 + const harnessIds = requestedIds.length > 0 ? requestedIds - : compiled.bundle.compositions.map((composition) => composition.id); - const reports = compositionIds.map((compositionId) => { - const { revision, report } = resolveComposition(compiled.bundle, compositionId); + : compiled.bundle.harnesses.map((harness) => harness.id); + // Resolve each harness against every deployable runtime: declared targets + // first, declared runtimes otherwise. With neither, a single unqualified + // resolution surfaces the resolver's own runtime-selection error. + const runtimeIds = compiled.bundle.targets.length > 0 + ? compiled.bundle.targets.map((target) => target.runtime) + : compiled.bundle.runtimes.length > 0 + ? compiled.bundle.runtimes.map((runtime) => runtime.id) + : [undefined]; + const reports = harnessIds.flatMap((harnessId) => runtimeIds.map((runtimeId) => { + const { revision, report } = resolveHarness(compiled.bundle, harnessId, runtimeId); return { - compositionId, - target: report.target, + harnessId, + runtime: report.runtime, status: report.status, revisionId: revision?.revisionId ?? null, errors: report.errors, realizations: report.realizations, }; - }); - const structuralErrors = compositionIds.length === 0 + })); + const structuralErrors = harnessIds.length === 0 ? [{ severity: "error", - message: "No composition is declared; generated DSL must be independently resolvable.", + message: "No harness is declared; generated DSL must be independently resolvable.", source, }] : []; @@ -74,7 +82,7 @@ async function main() { valid, file: input, diagnostics: [...compiled.diagnostics, ...structuralErrors], - compositions: reports, + harnesses: reports, }); if (!valid) { process.exitCode = 1; @@ -83,6 +91,6 @@ async function main() { main().catch((error) => { const message = error instanceof Error ? error.message : String(error); - print({ valid: false, diagnostics: [{ severity: "error", message }], compositions: [] }); + print({ valid: false, diagnostics: [{ severity: "error", message }], harnesses: [] }); process.exitCode = 1; }); diff --git a/packages/harness/src/compare/cli.ts b/packages/harness/src/compare/cli.ts index 8026aa0..535c9f9 100644 --- a/packages/harness/src/compare/cli.ts +++ b/packages/harness/src/compare/cli.ts @@ -54,9 +54,9 @@ export async function main(argv: string[]): Promise { } function help(): string { - return `Harness composition comparison\n\n` + + return `Harness comparison\n\n` + `Usage:\n harness-compare run --out [--trials ] [--json]\n\n` + - `The command creates an isolated repository per composition/trial, invokes Qoder through\n` + + `The command creates an isolated repository per harness/trial, invokes Qoder through\n` + `the official SDK, grades the resulting files, and writes immutable comparison evidence.\n`; } diff --git a/packages/harness/src/compare/manifest.ts b/packages/harness/src/compare/manifest.ts index 8aa7839..e5a929a 100644 --- a/packages/harness/src/compare/manifest.ts +++ b/packages/harness/src/compare/manifest.ts @@ -139,7 +139,7 @@ function validateManifestPolicy(manifest: HarnessCompareManifest): void { ) ) { throw new Error( - "Invalid harness-compare.v1 manifest: baseline and candidate must differ by composition or runtime profile.", + "Invalid harness-compare.v1 manifest: baseline and candidate must differ by harness or runtime profile.", ); } validateRuntimePolicy(manifest.runtime, "runtime"); diff --git a/packages/harness/src/compare/runner.ts b/packages/harness/src/compare/runner.ts index 9d5c7b2..a14b515 100644 --- a/packages/harness/src/compare/runner.ts +++ b/packages/harness/src/compare/runner.ts @@ -17,7 +17,7 @@ import type { HarnessExecutor, HarnessRunResult } from "../exec/executor.js"; import { QoderSdkExecutor } from "../exec/qoder-sdk.js"; import { canonicalJson, sha256Hex } from "../ir/canonical.js"; import type { HarnessIrBundle, HarnessRevision } from "../ir/index.js"; -import { resolveComposition } from "../resolver/resolve.js"; +import { resolveHarness } from "../resolver/resolve.js"; import { gradeReadmePackage, type ReadmeGrade } from "./grader.js"; import { loadHarnessCompareManifest, @@ -55,7 +55,7 @@ export interface FileEvidence { export interface CompareTrialResult { variant: CompareVariant; - compositionId: string; + harnessId: string; runtimeProfile: string; trial: number; classification: TrialClassification; @@ -137,14 +137,14 @@ export async function runHarnessComparison(options: { const results: CompareTrialResult[] = []; for (const job of jobs) { - const compositionId = loaded.value.variants[job.variant]; + const harnessId = loaded.value.variants[job.variant]; const result = await runTrial({ loaded, outputDirectory, prompt, bundle, revision: revisions[job.variant], - compositionId, + harnessId, variant: job.variant, trial: job.trial, executorFactory: options.executorFactory ?? defaultExecutorFactory, @@ -189,14 +189,14 @@ async function compileAndResolveVariants( } const revisions = {} as Record; for (const variant of ["baseline", "candidate"] as const) { - const compositionId = loaded.value.variants[variant]; - const resolved = resolveComposition(compiled.bundle, compositionId); + const harnessId = loaded.value.variants[variant]; + const resolved = resolveHarness(compiled.bundle, harnessId, loaded.value.runtime.host); if (!resolved.revision) { - throw new Error(`Cannot resolve ${variant} composition '${compositionId}': ${resolved.report.errors.join("; ")}`); + throw new Error(`Cannot resolve ${variant} harness '${harnessId}': ${resolved.report.errors.join("; ")}`); } - if (resolved.revision.target.host !== loaded.value.runtime.host) { + if (resolved.revision.target.runtime !== loaded.value.runtime.host) { throw new Error( - `${variant} composition targets '${resolved.revision.target.host}', expected '${loaded.value.runtime.host}'.`, + `${variant} harness targets '${resolved.revision.target.runtime}', expected '${loaded.value.runtime.host}'.`, ); } revisions[variant] = resolved.revision; @@ -210,7 +210,7 @@ async function runTrial(options: { prompt: string; bundle: HarnessIrBundle; revision: HarnessRevision; - compositionId: string; + harnessId: string; variant: CompareVariant; trial: number; executorFactory: CompareExecutorFactory; @@ -291,7 +291,7 @@ async function runTrial(options: { const classification = classifyTrial(execution, grade, infrastructureError); const result: CompareTrialResult = { variant: options.variant, - compositionId: options.compositionId, + harnessId: options.harnessId, runtimeProfile: runtime.profile, trial: options.trial, classification, @@ -316,7 +316,7 @@ async function runTrial(options: { return await recordInfrastructureFailure({ artifactDirectory, variantDirectory, - compositionId: options.compositionId, + harnessId: options.harnessId, runtimeProfile: runtime.profile, variant: options.variant, trial: options.trial, @@ -334,7 +334,7 @@ async function runTrial(options: { async function recordInfrastructureFailure(options: { artifactDirectory: string; variantDirectory: string; - compositionId: string; + harnessId: string; runtimeProfile: string; variant: CompareVariant; trial: number; @@ -345,7 +345,7 @@ async function recordInfrastructureFailure(options: { }): Promise { const result: CompareTrialResult = { variant: options.variant, - compositionId: options.compositionId, + harnessId: options.harnessId, runtimeProfile: options.runtimeProfile, trial: options.trial, classification: "infrastructure_error", diff --git a/packages/harness/src/compiler/compile.ts b/packages/harness/src/compiler/compile.ts index 1f66c41..b69d9d2 100644 --- a/packages/harness/src/compiler/compile.ts +++ b/packages/harness/src/compiler/compile.ts @@ -1,30 +1,48 @@ import { URI, type LangiumDocument } from "langium"; -import semver from "semver"; import { createHarnessServices } from "../language/harness-module.js"; import { - type CapabilityRequirement, - type ComponentContract, - type CompositionDeclaration, - type ConfigEntry, + type AgentDeclaration, + type CapabilityBinding, + type CapabilityUse, type ConfigValue, + type ExecutionSpec, + type HarnessDeclaration, type HarnessDocument, - type PluginDeclaration, - type PluginRequirement, - type TargetBinding, - isComponentContract, - isCompositionDeclaration, - isPluginDeclaration, - isTargetBinding, + type McpDeclaration, + type RuntimeDeclaration, + type SkillDeclaration, + type TargetDeclaration, + type ToolDeclaration, + type WorkflowDeclaration, + isEdgeStatement, + isEnvEndpoint, + isEventStatement, + isHarnessDeclaration, + isMcpDeclaration, + isProgrammaticExecution, + isRuntimeDeclaration, + isSkillDeclaration, + isStopStatement, + isTargetDeclaration, + isToolDeclaration, + isWorkflowDeclaration, + isCapabilityBinding, } from "../language/generated/ast.js"; import { - type ComponentContractIr, - type CompositionSpecIr, + type CapabilityBindingIr, + type CapabilityKind, type ConfigValue as ConfigValueIr, + type ExecutionIr, type HarnessIrBundle, + type HarnessSpecIr, IR_VERSION, - type PluginManifestIr, + type McpIr, + type RuntimeIr, + type SkillIr, type Strength, - type TargetBindingIr, + type TargetIr, + type ToolIr, + type WorkflowIr, } from "../ir/index.js"; export interface CompileDiagnostic { @@ -46,80 +64,17 @@ export interface HarnessSource { const DURATION_FACTORS: Record = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000 }; -/** Default mechanism for a binding that omits one: prompt guidance. */ +/** Default mechanism for a capability without a binding: prompt guidance. */ const DEFAULT_MECHANISM = "prompt-preamble"; /** Default strength for a binding or requirement that omits one. */ const DEFAULT_STRENGTH: Strength = "advisory"; -/** - * Flattened, `extends`-resolved view of a composition. The child overrides the - * base's `target` and, per member key, its includes/requirements/settings. - */ -interface MergedComposition { - name: string; - target: string | undefined; - includes: PluginRequirement[]; - requirements: CapabilityRequirement[]; - settings: ConfigEntry[]; -} - -/** Whether following `extends` from this composition revisits a node. */ -function hasExtendsCycle(composition: CompositionDeclaration): boolean { - const seen = new Set(); - let current: CompositionDeclaration | undefined = composition; - while (current) { - if (seen.has(current)) { - return true; - } - seen.add(current); - current = current.base?.ref; - } - return false; -} - -/** - * Walk the `extends` chain base-first and merge members. Later (more derived) - * declarations override earlier ones by key: plugin id, required component id, - * and configuration key. A cycle is broken defensively and reported separately - * by {@link collectBundleDiagnostics}. - */ -function mergeExtends(composition: CompositionDeclaration): MergedComposition { - const chain: CompositionDeclaration[] = []; - const seen = new Set(); - let current: CompositionDeclaration | undefined = composition; - while (current && !seen.has(current)) { - seen.add(current); - chain.push(current); - current = current.base?.ref; - } - chain.reverse(); - - const includes = new Map(); - const requirements = new Map(); - const settings = new Map(); - let target: string | undefined; - for (const node of chain) { - if (node.target !== undefined) { - target = node.target; - } - for (const include of node.includes) { - includes.set(include.plugin.$refText, include); - } - for (const requirement of node.requirements) { - requirements.set(requirement.component.$refText, requirement); - } - for (const setting of node.settings) { - settings.set(setting.key, setting); - } - } - return { - name: composition.name, - target, - includes: [...includes.values()], - requirements: [...requirements.values()], - settings: [...settings.values()], - }; -} +/** The capability kind each requirement verb asks for. */ +const VERB_KIND: Record = { + use: "skill", + require: "tool", + connect: "mcp", +}; /** * Parse one or more Harness DSL sources and lower them into the versioned @@ -152,175 +107,308 @@ export async function compileHarness(input: string | HarnessSource[]): Promise diagnostic.severity === "error")) { return { diagnostics }; } - const components: ComponentContractIr[] = []; - const bindings: TargetBindingIr[] = []; - const plugins: PluginManifestIr[] = []; - const compositions: CompositionSpecIr[] = []; + return { bundle: lowerBundle(index), diagnostics }; +} + +/** All declarations across the source set, in declaration order. */ +interface DeclarationIndex { + skills: SkillDeclaration[]; + tools: ToolDeclaration[]; + mcps: McpDeclaration[]; + workflows: WorkflowDeclaration[]; + runtimes: RuntimeDeclaration[]; + harnesses: HarnessDeclaration[]; + targets: TargetDeclaration[]; + bindings: CapabilityBinding[]; + /** Capability id -> declared kind, across the shared capability namespace. */ + capabilityKinds: Map; + /** Tool ids named by `require tool` but never declared. */ + implicitTools: Set; +} + +function indexDeclarations(documents: LangiumDocument[]): DeclarationIndex { + const index: DeclarationIndex = { + skills: [], + tools: [], + mcps: [], + workflows: [], + runtimes: [], + harnesses: [], + targets: [], + bindings: [], + capabilityKinds: new Map(), + implicitTools: new Set(), + }; for (const document of documents) { const root = document.parseResult.value as HarnessDocument; for (const element of root.elements) { - if (isComponentContract(element)) { - components.push(lowerComponent(element)); - } else if (isTargetBinding(element)) { - bindings.push(...lowerBinding(element)); - } else if (isCompositionDeclaration(element)) { - compositions.push(lowerComposition(element)); + if (isSkillDeclaration(element)) { + index.skills.push(element); + index.capabilityKinds.set(element.name, "skill"); + } else if (isToolDeclaration(element)) { + index.tools.push(element); + index.capabilityKinds.set(element.name, "tool"); + } else if (isMcpDeclaration(element)) { + index.mcps.push(element); + index.capabilityKinds.set(element.name, "mcp"); + } else if (isWorkflowDeclaration(element)) { + index.workflows.push(element); + } else if (isRuntimeDeclaration(element)) { + index.runtimes.push(element); + } else if (isHarnessDeclaration(element)) { + index.harnesses.push(element); + } else if (isTargetDeclaration(element)) { + index.targets.push(element); + } else if (isCapabilityBinding(element)) { + index.bindings.push(element); } } } - // Plugins are lowered last so provided components can be joined with their - // declared bindings into the plugin manifest. - for (const document of documents) { - const root = document.parseResult.value as HarnessDocument; - for (const element of root.elements) { - if (isPluginDeclaration(element)) { - plugins.push(lowerPlugin(element, bindings)); + for (const harness of index.harnesses) { + for (const agent of harness.agents) { + for (const requirement of agent.requirements) { + if (requirement.verb === "require" && !index.capabilityKinds.has(requirement.capability)) { + index.implicitTools.add(requirement.capability); + } } } } - - return { - bundle: { - irVersion: IR_VERSION, - kind: "harness-ir-bundle", - components, - bindings, - plugins, - compositions, - }, - diagnostics, - }; + return index; } -function collectBundleDiagnostics(documents: LangiumDocument[]): CompileDiagnostic[] { +function collectBundleDiagnostics( + documents: LangiumDocument[], + index: DeclarationIndex, +): CompileDiagnostic[] { const diagnostics: CompileDiagnostic[] = []; - const components = new Map(); - const compositions = new Map(); - const pluginVersions = new Map(); + const capabilities = new Map(); + const workflows = new Map(); + const runtimes = new Map(); + const harnesses = new Map(); + const targets = new Map(); const bindings = new Map(); for (const document of documents) { const source = document.uri.toString(); const root = document.parseResult.value as HarnessDocument; for (const element of root.elements) { - if (isComponentContract(element)) { - recordUnique(components, element.name, "component", source, element.$cstNode?.range.start.line); - recordRepeatedValues(element.inputs, "component input", source, element.$cstNode?.range.start.line); - recordRepeatedValues(element.outputs, "component output", source, element.$cstNode?.range.start.line); - } else if (isCompositionDeclaration(element)) { - recordUnique(compositions, element.name, "composition", source, element.$cstNode?.range.start.line); - recordCompositionMembers(element, source); - } else if (isPluginDeclaration(element)) { - recordUnique( - pluginVersions, - `${element.name}@${element.version}`, - "plugin version", - source, - element.$cstNode?.range.start.line, - ); - recordRepeatedValues( - element.provides.map((provided) => provided.component.$refText), - "provided component", - source, - element.$cstNode?.range.start.line, - ); - if (semver.valid(element.version) === null) { + const line = element.$cstNode?.range.start.line; + if (isSkillDeclaration(element)) { + recordUnique(capabilities, element.name, "capability", source, line); + if (element.source === undefined && element.description === undefined) { + diagnostics.push( + semanticDiagnostic( + `Skill '${element.name}' declares neither 'source' nor 'description'; ` + + "a skill needs a source directory, inline guidance, or both.", + source, + line, + ), + ); + } + } else if (isToolDeclaration(element)) { + recordUnique(capabilities, element.name, "capability", source, line); + recordRepeatedValues(element.inputs, "tool input", source, line); + recordRepeatedValues(element.outputs, "tool output", source, line); + } else if (isMcpDeclaration(element)) { + recordUnique(capabilities, element.name, "capability", source, line); + if (element.transport === "stdio" && element.command === undefined) { + diagnostics.push( + semanticDiagnostic( + `MCP '${element.name}' uses 'stdio' transport but declares no 'command'.`, + source, + line, + ), + ); + } + if (element.transport !== "stdio" && element.url === undefined) { + diagnostics.push( + semanticDiagnostic( + `MCP '${element.name}' uses '${element.transport}' transport but declares no 'url'.`, + source, + line, + ), + ); + } + } else if (isWorkflowDeclaration(element)) { + recordUnique(workflows, element.name, "workflow", source, line); + if (element.program !== undefined && element.statements.length > 0) { + diagnostics.push( + semanticDiagnostic( + `Workflow '${element.name}' mixes 'program' with graph statements; ` + + "declare either a programmatic controller or a declarative graph.", + source, + line, + ), + ); + } + if (element.program === undefined && element.statements.length === 0) { diagnostics.push( semanticDiagnostic( - `Plugin '${element.name}' declares invalid semantic version '${element.version}'.`, + `Workflow '${element.name}' is empty; declare edges or a 'program'.`, source, - element.$cstNode?.range.start.line, + line, ), ); } - } else if (isTargetBinding(element)) { - for (const host of element.hosts) { + } else if (isRuntimeDeclaration(element)) { + recordUnique(runtimes, element.name, "runtime", source, line); + if (element.execution !== undefined && isProgrammaticExecution(element.execution)) { + recordRepeatedValues( + element.execution.options.map((option) => option.key), + "execution option", + source, + line, + ); + } + } else if (isHarnessDeclaration(element)) { + recordUnique(harnesses, element.name, "harness", source, line); + recordHarnessMembers(element, source, line); + } else if (isTargetDeclaration(element)) { + recordUnique(targets, element.runtime, "target runtime", source, line); + const runtime = index.runtimes.find((candidate) => candidate.name === element.runtime); + if (runtime !== undefined && element.adapter !== undefined) { + diagnostics.push( + semanticDiagnostic( + `Target '${element.runtime}' declares 'uses adapter.${element.adapter}', but runtime ` + + `'${runtime.name}' already owns adapter '${runtime.adapter}'; drop the target adapter.`, + source, + line, + ), + ); + } + } else if (isCapabilityBinding(element)) { + for (const runtime of element.runtimes) { recordUnique( bindings, - `${element.component.$refText}::${host}`, - "component/host binding", + `${element.capability}::${runtime}`, + "capability/runtime binding", source, - element.$cstNode?.range.start.line, + line, + ); + } + if ( + !index.capabilityKinds.has(element.capability) && + !index.implicitTools.has(element.capability) + ) { + diagnostics.push( + semanticDiagnostic( + `Binding references unknown capability '${element.capability}'; declare it as a ` + + "skill, tool, or mcp, or require it from an agent.", + source, + line, + ), ); } } } } - // `extends` is resolved after the parser links references, so cycles and - // missing targets are semantic errors detected here rather than by the - // grammar. + // Workflow statements name agents by role. Every harness that uses a + // workflow must declare each referenced role. for (const document of documents) { const source = document.uri.toString(); const root = document.parseResult.value as HarnessDocument; for (const element of root.elements) { - if (!isCompositionDeclaration(element)) { + if (!isHarnessDeclaration(element)) { continue; } - const line = element.$cstNode?.range.start.line; - if (hasExtendsCycle(element)) { - diagnostics.push( - semanticDiagnostic( - `Composition '${element.name}' has a circular 'extends' chain.`, - source, - line, - ), - ); - continue; + const workflow = element.workflow.ref; + if (workflow === undefined) { + continue; // Linking already reported the missing workflow. } - if (mergeExtends(element).target === undefined) { - diagnostics.push( - semanticDiagnostic( - `Composition '${element.name}' has no target; declare 'target' or 'extends' a composition that does.`, - source, - line, - ), - ); + const roles = new Set(element.agents.map((agent) => agent.name)); + const line = element.$cstNode?.range.start.line; + for (const referenced of workflowAgentNames(workflow)) { + if (!roles.has(referenced)) { + diagnostics.push( + semanticDiagnostic( + `Workflow '${workflow.name}' references agent '${referenced}', which harness ` + + `'${element.name}' does not declare.`, + source, + line, + ), + ); + } } } } return diagnostics; - function recordCompositionMembers(composition: CompositionDeclaration, source: string): void { - const line = composition.$cstNode?.range.start.line; - recordRepeatedValues( - composition.includes.map((include) => include.plugin.$refText), - "included plugin", - source, - line, - ); - for (const include of composition.includes) { - const range = include.range.replace(/^@/, ""); - if (semver.validRange(range) === null) { - diagnostics.push( - semanticDiagnostic( - `Plugin '${include.plugin.$refText}' uses invalid semantic version range '${range}'.`, - source, - line, - ), - ); - } - } + function recordHarnessMembers( + harness: HarnessDeclaration, + source: string, + line: number | undefined, + ): void { recordRepeatedValues( - composition.requirements.map((requirement) => requirement.component.$refText), - "composition requirement", + harness.agents.map((agent) => agent.name), + "agent", source, line, ); recordRepeatedValues( - composition.settings.map((setting) => setting.key), + harness.settings.map((setting) => setting.key), "configuration key", source, line, ); + for (const agent of harness.agents) { + const agentLine = agent.$cstNode?.range.start.line ?? line; + recordRepeatedValues( + agent.requirements.map((requirement) => requirement.capability), + `agent '${agent.name}' requirement`, + source, + agentLine, + ); + for (const requirement of agent.requirements) { + diagnoseRequirement(requirement, source, agentLine); + } + } } - function recordRepeatedValues(values: string[], kind: string, source: string, zeroBasedLine?: number): void { + function diagnoseRequirement( + requirement: CapabilityUse, + source: string, + line: number | undefined, + ): void { + const expected = VERB_KIND[requirement.verb]; + const declared = index.capabilityKinds.get(requirement.capability); + if (declared !== undefined && declared !== expected) { + diagnostics.push( + semanticDiagnostic( + `'${requirement.verb}' expects a ${expected}, but '${requirement.capability}' ` + + `is declared as a ${declared}.`, + source, + line, + ), + ); + return; + } + // Tools may stay implicit; skills carry sources and MCP entries carry + // connection facts, so both must be declared. + if (declared === undefined && expected !== "tool") { + diagnostics.push( + semanticDiagnostic( + `Agent requirement references unknown ${expected} '${requirement.capability}'; ` + + `declare '${expected} ${requirement.capability} { ... }'.`, + source, + line, + ), + ); + } + } + + function recordRepeatedValues( + values: string[], + kind: string, + source: string, + zeroBasedLine?: number, + ): void { const seen = new Set(); for (const value of values) { if (seen.has(value)) { @@ -349,6 +437,23 @@ function collectBundleDiagnostics(documents: LangiumDocument[]): CompileDiagnost } } +/** Agent role names referenced by a workflow's declarative statements. */ +function workflowAgentNames(workflow: WorkflowDeclaration): Set { + const names = new Set(); + for (const statement of workflow.statements) { + if (isEdgeStatement(statement)) { + names.add(statement.from); + names.add(statement.to); + } else if (isEventStatement(statement)) { + names.add(statement.agent); + names.add(statement.to); + } else if (isStopStatement(statement)) { + names.add(statement.agent); + } + } + return names; +} + function duplicateDiagnostic( kind: string, value: string, @@ -376,78 +481,173 @@ function semanticDiagnostic( }; } -function lowerComponent(contract: ComponentContract): ComponentContractIr { +function lowerBundle(index: DeclarationIndex): HarnessIrBundle { return { irVersion: IR_VERSION, - kind: "component-contract", - id: contract.name, - componentKind: contract.kind, - ...(contract.description !== undefined ? { description: contract.description } : {}), - inputs: [...contract.inputs], - outputs: [...contract.outputs], - permissions: contract.permissions.map((rule) => ({ domain: rule.domain, access: rule.access })), + kind: "harness-ir-bundle", + skills: index.skills.map(lowerSkill), + tools: [ + ...index.tools.map((tool) => lowerTool(tool)), + ...[...index.implicitTools].sort().map(implicitTool), + ], + mcps: index.mcps.map(lowerMcp), + workflows: index.workflows.map(lowerWorkflow), + runtimes: index.runtimes.map(lowerRuntime), + harnesses: index.harnesses.map(lowerHarness), + targets: index.targets.map(lowerTarget), + bindings: index.bindings.flatMap(lowerBinding), }; } -function lowerBinding(binding: TargetBinding): TargetBindingIr[] { - return binding.hosts.map((host) => ({ +function lowerSkill(skill: SkillDeclaration): SkillIr { + return { irVersion: IR_VERSION, - kind: "target-binding", - componentId: binding.component.$refText, - host, - mechanism: binding.mechanism ?? DEFAULT_MECHANISM, - strength: (binding.strength ?? DEFAULT_STRENGTH) as Strength, - ...(binding.notes !== undefined ? { notes: binding.notes } : {}), - })); + kind: "skill", + id: skill.name, + ...(skill.source !== undefined ? { source: skill.source } : {}), + ...(skill.description !== undefined ? { description: skill.description } : {}), + permissions: skill.permissions.map((rule) => ({ domain: rule.domain, access: rule.access })), + }; +} + +function lowerTool(tool: ToolDeclaration): ToolIr { + return { + irVersion: IR_VERSION, + kind: "tool", + id: tool.name, + ...(tool.description !== undefined ? { description: tool.description } : {}), + inputs: [...tool.inputs], + outputs: [...tool.outputs], + permissions: tool.permissions.map((rule) => ({ domain: rule.domain, access: rule.access })), + implicit: false, + }; } -function lowerPlugin(plugin: PluginDeclaration, bindings: TargetBindingIr[]): PluginManifestIr { - const provides = plugin.provides.map((ref) => ref.component.$refText); - const provided = new Set(provides); +function implicitTool(id: string): ToolIr { return { irVersion: IR_VERSION, - kind: "plugin-manifest", - id: plugin.name, - version: plugin.version, - provides, - bindings: bindings - .filter((binding) => provided.has(binding.componentId)) - .map((binding) => ({ - componentId: binding.componentId, - host: binding.host, - mechanism: binding.mechanism, - strength: binding.strength, - })), + kind: "tool", + id, + inputs: [], + outputs: [], + permissions: [], + implicit: true, }; } -function lowerComposition(composition: CompositionDeclaration): CompositionSpecIr { - const merged = mergeExtends(composition); +function lowerMcp(mcp: McpDeclaration): McpIr { return { irVersion: IR_VERSION, - kind: "composition-spec", - id: merged.name, - // A missing target is rejected by collectBundleDiagnostics before lowering. - target: merged.target ?? "", - includes: merged.includes.map((include) => ({ - pluginId: include.plugin.$refText, - range: include.range.replace(/^@/, ""), + kind: "mcp", + id: mcp.name, + transport: mcp.transport, + ...(mcp.url !== undefined + ? { + url: isEnvEndpoint(mcp.url) + ? { type: "env" as const, variable: mcp.url.variable } + : { type: "literal" as const, value: mcp.url.value }, + } + : {}), + ...(mcp.command !== undefined ? { command: mcp.command } : {}), + }; +} + +function lowerWorkflow(workflow: WorkflowDeclaration): WorkflowIr { + const edges: WorkflowIr["edges"] = []; + const events: WorkflowIr["events"] = []; + const stops: WorkflowIr["stops"] = []; + for (const statement of workflow.statements) { + if (isEdgeStatement(statement)) { + edges.push({ from: statement.from, to: statement.to }); + } else if (isEventStatement(statement)) { + events.push({ agent: statement.agent, outcome: statement.outcome, to: statement.to }); + } else if (isStopStatement(statement)) { + stops.push({ agent: statement.agent, outcome: statement.outcome }); + } + } + return { + irVersion: IR_VERSION, + kind: "workflow", + id: workflow.name, + mode: workflow.program !== undefined ? "programmatic" : "declarative", + edges, + events, + stops, + ...(workflow.program !== undefined + ? { program: { language: workflow.program.language, entry: workflow.program.entry } } + : {}), + }; +} + +function lowerExecution(execution: ExecutionSpec | undefined): ExecutionIr { + if (execution === undefined || !isProgrammaticExecution(execution)) { + return { style: "tool-calling" }; + } + return { + style: "programmatic", + language: execution.language, + options: execution.options.map((option) => ({ key: option.key, value: option.value })), + }; +} + +function lowerRuntime(runtime: RuntimeDeclaration): RuntimeIr { + return { + irVersion: IR_VERSION, + kind: "runtime", + id: runtime.name, + adapter: runtime.adapter, + execution: lowerExecution(runtime.execution), + }; +} + +function lowerHarness(harness: HarnessDeclaration): HarnessSpecIr { + return { + irVersion: IR_VERSION, + kind: "harness-spec", + id: harness.name, + workflow: harness.workflow.$refText, + agents: harness.agents.map(lowerAgent), + settings: harness.settings.map((entry) => ({ + key: entry.key, + value: lowerConfigValue(entry.value), })), - requirements: merged.requirements.map((requirement) => ({ - componentId: requirement.component.$refText, + }; +} + +function lowerAgent(agent: AgentDeclaration): HarnessSpecIr["agents"][number] { + return { + id: agent.name, + requirements: agent.requirements.map((requirement) => ({ + capabilityId: requirement.capability, + capabilityKind: VERB_KIND[requirement.verb], ...(requirement.preferred !== undefined ? { preferred: requirement.preferred as Strength } : {}), minimum: (requirement.minimum ?? DEFAULT_STRENGTH) as Strength, onDegrade: requirement.onDegrade ?? "report", })), - settings: merged.settings.map((entry) => ({ - key: entry.key, - value: lowerConfigValue(entry.value), - })), }; } +function lowerTarget(target: TargetDeclaration): TargetIr { + return { + runtime: target.runtime, + ...(target.adapter !== undefined ? { adapter: target.adapter } : {}), + }; +} + +function lowerBinding(binding: CapabilityBinding): CapabilityBindingIr[] { + return binding.runtimes.map((runtime) => ({ + irVersion: IR_VERSION, + kind: "capability-binding", + capabilityId: binding.capability, + runtime, + mechanism: binding.mechanism ?? DEFAULT_MECHANISM, + strength: (binding.strength ?? DEFAULT_STRENGTH) as Strength, + ...(binding.notes !== undefined ? { notes: binding.notes } : {}), + })); +} + function lowerConfigValue(value: ConfigValue): ConfigValueIr { switch (value.$type) { case "IntLiteral": diff --git a/packages/harness/src/exec/executor.ts b/packages/harness/src/exec/executor.ts index 27cf687..891a279 100644 --- a/packages/harness/src/exec/executor.ts +++ b/packages/harness/src/exec/executor.ts @@ -1,4 +1,4 @@ -import type { HarnessIrBundle, HarnessRevision } from "../ir/index.js"; +import { findCapability, type CapabilityIr, type HarnessIrBundle, type HarnessRevision, type WorkflowIr } from "../ir/index.js"; export interface HarnessRunTask { prompt: string; @@ -67,24 +67,58 @@ export interface RunPreamble { } export class HarnessHostMismatchError extends Error { - constructor(revisionHost: string, executorHost: string) { - super(`Harness revision targets host '${revisionHost}', but executor host is '${executorHost}'.`); + constructor(revisionRuntime: string, executorHost: string) { + super( + `Harness revision targets runtime '${revisionRuntime}', but executor host is '${executorHost}'.`, + ); this.name = "HarnessHostMismatchError"; } } export function assertRevisionHost(revision: HarnessRevision, executorHost: string): void { - if (revision.target.host !== executorHost) { - throw new HarnessHostMismatchError(revision.target.host, executorHost); + if (revision.target.runtime !== executorHost) { + throw new HarnessHostMismatchError(revision.target.runtime, executorHost); } } +/** One prompt line per advisory capability, in the voice of the agent role. */ +function capabilityGuidance(capability: CapabilityIr | undefined, capabilityId: string): string { + if (capability === undefined) { + return `Apply the '${capabilityId}' capability.`; + } + switch (capability.kind) { + case "skill": + return ( + capability.description ?? + `Apply the '${capability.id}' skill from '${capability.source ?? "its source"}'.` + ); + case "tool": + return capability.description ?? `Use the '${capability.id}' tool when applicable.`; + case "mcp": + return `Connect to the '${capability.id}' MCP server over ${capability.transport}.`; + } +} + +/** Render a declarative workflow graph as one prompt guidance line. */ +function workflowGuidance(workflow: WorkflowIr | undefined): string | undefined { + if (workflow === undefined || workflow.mode !== "declarative") { + return undefined; + } + const parts = [ + ...workflow.edges.map((edge) => `${edge.from} -> ${edge.to}`), + ...workflow.events.map((event) => `on ${event.agent}.${event.outcome} -> ${event.to}`), + ...workflow.stops.map((stop) => `stop when ${stop.agent}.${stop.outcome}`), + ]; + return parts.length > 0 ? `Workflow '${workflow.id}': ${parts.join("; ")}.` : undefined; +} + /** * Build the advisory portion of a run from a resolved revision. * - * v0.1 executors materialize effective `advisory` realizations as prompt text. - * The resolver has already capped stronger declared bindings to this effective - * strength and applied the composition's degradation policy. + * v0.2 executors materialize effective `advisory` realizations as prompt text: + * the workflow graph plus each agent role's capabilities. The resolver has + * already capped stronger declared bindings to this effective strength and + * applied the harness's degradation policy. */ export function buildRunPreamble(revision: HarnessRevision, bundle: HarnessIrBundle): RunPreamble { const lines: string[] = []; @@ -94,20 +128,27 @@ export function buildRunPreamble(revision: HarnessRevision, bundle: HarnessIrBun continue; } if (realization.action === "degraded" && realization.reason) { - warnings.push(`Realization '${realization.componentId}' is degraded: ${realization.reason}.`); + warnings.push( + `Realization '${realization.capabilityId}' for agent '${realization.agentId}' ` + + `is degraded: ${realization.reason}.`, + ); } if (realization.realized === "advisory") { - const contract = bundle.components.find( - (component) => component.id === realization.componentId, + const capability = findCapability(bundle, realization.capabilityId); + lines.push( + `- [${realization.agentId}/${realization.capabilityId}] ` + + capabilityGuidance(capability, realization.capabilityId), ); - const text = contract?.description ?? `Apply the '${realization.componentId}' capability.`; - lines.push(`- [${realization.componentId}] ${text}`); } } + const flow = workflowGuidance( + bundle.workflows.find((workflow) => workflow.id === revision.workflow.id), + ); const preamble = lines.length > 0 ? [ `You are running under harness revision ${revision.revisionId}.`, + ...(flow !== undefined ? [flow] : []), "Follow these harness policies:", ...lines, ].join("\n") diff --git a/packages/harness/src/exec/pi-sdk.ts b/packages/harness/src/exec/pi-sdk.ts index 62ab6a1..81af59f 100644 --- a/packages/harness/src/exec/pi-sdk.ts +++ b/packages/harness/src/exec/pi-sdk.ts @@ -152,7 +152,7 @@ async function loadPiSdk(loader?: () => Promise): Promise /** * Materialize a revision as an installable Pi package directory: * a `package.json` with the `pi.skills` contribution plus one - * `skills//SKILL.md` per advisory skill-shaped component. + * `skills//SKILL.md` per advisory skill capability. * Returns the relative paths that were written. */ export async function materializePiPackage( @@ -166,7 +166,7 @@ export async function materializePiPackage( name: `harness-revision-${revision.revisionId.slice(3, 15)}`, version: "0.0.0", private: true, - description: `Materialized harness revision ${revision.revisionId} for host '${revision.target.host}'.`, + description: `Materialized harness revision ${revision.revisionId} for runtime '${revision.target.runtime}'.`, pi: { skills: ["./skills"] }, }; await mkdir(directory, { recursive: true }); @@ -180,31 +180,44 @@ export async function materializePiPackage( await writeFile(join(directory, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); written.push("package.json"); + // Realizations are per (agent, capability): the same skill used by several + // agent roles materializes once. + const writtenSkillIds = new Set(); for (const realization of revision.realization) { - if (realization.action === "failed" || realization.realized !== "advisory") { + if ( + realization.action === "failed" || + realization.realized !== "advisory" || + realization.capabilityKind !== "skill" || + writtenSkillIds.has(realization.capabilityId) + ) { continue; } - const contract = bundle.components.find((component) => component.id === realization.componentId); - if (!contract || contract.componentKind !== "skill") { + const skill = bundle.skills.find((candidate) => candidate.id === realization.capabilityId); + if (!skill) { continue; } - const skillDir = join(directory, "skills", contract.id); + writtenSkillIds.add(skill.id); + const skillDir = join(directory, "skills", skill.id); await mkdir(skillDir, { recursive: true }); - const description = contract.description ?? `Harness component '${contract.id}'.`; + const description = + skill.description ?? + (skill.source !== undefined + ? `Harness skill '${skill.id}' sourced from '${skill.source}'.` + : `Harness skill '${skill.id}'.`); const body = [ "---", - `name: ${contract.id}`, + `name: ${skill.id}`, `description: ${JSON.stringify(description)}`, "---", "", description, "", - `Provenance: harness revision ${revision.revisionId}, component hash ` + - `${revision.resolved.components.find((entry) => entry.id === contract.id)?.contentHash ?? "unknown"}.`, + `Provenance: harness revision ${revision.revisionId}, capability hash ` + + `${revision.resolved.capabilities.find((entry) => entry.id === skill.id)?.contentHash ?? "unknown"}.`, "", ].join("\n"); await writeFile(join(skillDir, "SKILL.md"), body, "utf8"); - written.push(join("skills", contract.id, "SKILL.md")); + written.push(join("skills", skill.id, "SKILL.md")); } return written; } diff --git a/packages/harness/src/highlight/harness-grammar.ts b/packages/harness/src/highlight/harness-grammar.ts index 2e052f5..4217376 100644 --- a/packages/harness/src/highlight/harness-grammar.ts +++ b/packages/harness/src/highlight/harness-grammar.ts @@ -28,7 +28,8 @@ export const harnessTextMateGrammar: LanguageRegistration = { declarations: { patterns: [ { - match: "\\b(component|binding|plugin|composition)\\b\\s+([_a-zA-Z][\\w-]*)", + match: + "\\b(harness|workflow|agent|skill|tool|mcp|runtime|target|binding)\\b\\s+([_a-zA-Z][\\w-]*(?:\\.[_a-zA-Z][\\w-]*)*)", captures: { "1": { name: "keyword.declaration.harness" }, "2": { name: "entity.name.type.harness" }, @@ -36,7 +37,7 @@ export const harnessTextMateGrammar: LanguageRegistration = { }, { name: "keyword.control.harness", - match: "\\b(for|require|include|provides|extends)\\b", + match: "\\b(for|use|require|connect|uses|program|on|stop|when)\\b|->", }, ], }, @@ -45,7 +46,7 @@ export const harnessTextMateGrammar: LanguageRegistration = { { name: "keyword.other.section.harness", match: - "\\b(kind|description|input|output|permissions|mechanism|strength|notes|version|target|preferred|minimum|on-degrade|configure)\\b", + "\\b(source|description|input|output|permissions|mechanism|strength|notes|adapter|execution|transport|url|command|env|preferred|minimum|on-degrade|configure)\\b", }, ], }, @@ -56,8 +57,12 @@ export const harnessTextMateGrammar: LanguageRegistration = { match: "\\b(unsupported|advisory|wired|enforced)\\b", }, { - name: "constant.language.kind.harness", - match: "\\b(skill|tool|program|workflow|hook|policy|observer|ui)\\b", + name: "constant.language.execution.harness", + match: "\\b(tool-calling|programmatic)\\b", + }, + { + name: "constant.language.transport.harness", + match: "\\b(stdio|http|sse)\\b", }, { name: "constant.language.permission.harness", diff --git a/packages/harness/src/index.ts b/packages/harness/src/index.ts index 31c4e3f..cab1b2d 100644 --- a/packages/harness/src/index.ts +++ b/packages/harness/src/index.ts @@ -6,7 +6,7 @@ export { type CompileResult, type HarnessSource, } from "./compiler/compile.js"; -export { mergePermissions, resolveComposition, type ResolveResult } from "./resolver/resolve.js"; +export { mergePermissions, resolveHarness, type ResolveResult } from "./resolver/resolve.js"; export { STRENGTH_ORDER, strengthIndex } from "./language/harness-validator.js"; export { createHarnessServices, type HarnessServices } from "./language/harness-module.js"; export * from "./exec/index.js"; diff --git a/packages/harness/src/ir/index.ts b/packages/harness/src/ir/index.ts index 40d3e0d..9f97f14 100644 --- a/packages/harness/src/ir/index.ts +++ b/packages/harness/src/ir/index.ts @@ -2,16 +2,20 @@ * Versioned JSON intermediate representation for the Harness DSL. * * Every artifact carries `irVersion` plus a `kind` discriminator so stored - * documents stay self-describing. The five v0.1 entities are: - * CompositionSpec, ComponentContract, TargetBindingManifest (as part of a - * plugin manifest), HarnessRevision, and ResolutionReport. + * documents stay self-describing. The v0.2 entities follow the standard + * resource model: capabilities (skill / tool / mcp), workflows, runtimes, + * harnesses with agent roles, deployment targets, capability bindings, + * HarnessRevision, and ResolutionReport. There is no generic plugin entity: + * host-native assets appear only as binding mechanisms in a host namespace + * (`qoder.plugin`, `pi.extension`). */ import { type Static, Type } from "@sinclair/typebox"; -export const IR_VERSION = "0.1.0"; +export const IR_VERSION = "0.2.0"; const IrVersion = Type.Literal(IR_VERSION); const Identifier = Type.String({ pattern: "^[_a-zA-Z][\\w-]*$" }); +const QualifiedIdentifier = Type.String({ pattern: "^[_a-zA-Z][\\w-]*(\\.[_a-zA-Z][\\w-]*)*$" }); export const StrengthSchema = Type.Union([ Type.Literal("unsupported"), @@ -21,17 +25,12 @@ export const StrengthSchema = Type.Union([ ]); export type Strength = Static; -export const ComponentKindSchema = Type.Union([ +export const CapabilityKindSchema = Type.Union([ Type.Literal("skill"), Type.Literal("tool"), - Type.Literal("program"), - Type.Literal("workflow"), - Type.Literal("hook"), - Type.Literal("policy"), - Type.Literal("observer"), - Type.Literal("ui"), + Type.Literal("mcp"), ]); -export type ComponentKind = Static; +export type CapabilityKind = Static; export const PermissionGrantSchema = Type.Object( { @@ -69,61 +68,143 @@ export const ConfigEntrySchema = Type.Object( ); export type ConfigEntryIr = Static; -export const ComponentContractIrSchema = Type.Object( +/** Progressive knowledge: a source directory, inline guidance, or both. */ +export const SkillIrSchema = Type.Object( { irVersion: IrVersion, - kind: Type.Literal("component-contract"), + kind: Type.Literal("skill"), id: Identifier, - componentKind: ComponentKindSchema, + source: Type.Optional(Type.String()), + description: Type.Optional(Type.String()), + permissions: Type.Array(PermissionGrantSchema), + }, + { additionalProperties: false }, +); +export type SkillIr = Static; + +/** + * An atomic callable capability. `implicit` marks tools that were only ever + * named by a `require tool` and never declared: a contract synthesized from + * its dotted name, with no permissions of its own. + */ +export const ToolIrSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("tool"), + id: QualifiedIdentifier, description: Type.Optional(Type.String()), inputs: Type.Array(Identifier), outputs: Type.Array(Identifier), permissions: Type.Array(PermissionGrantSchema), + implicit: Type.Boolean(), }, { additionalProperties: false }, ); -export type ComponentContractIr = Static; +export type ToolIr = Static; -export const TargetBindingIrSchema = Type.Object( +export const McpEndpointIrSchema = Type.Union([ + Type.Object( + { type: Type.Literal("env"), variable: Identifier }, + { additionalProperties: false }, + ), + Type.Object( + { type: Type.Literal("literal"), value: Type.String() }, + { additionalProperties: false }, + ), +]); +export type McpEndpointIr = Static; + +/** A capability connection (transport + endpoint), not a tool itself. */ +export const McpIrSchema = Type.Object( { irVersion: IrVersion, - kind: Type.Literal("target-binding"), - componentId: Identifier, - host: Identifier, - mechanism: Identifier, - strength: StrengthSchema, - notes: Type.Optional(Type.String()), + kind: Type.Literal("mcp"), + id: Identifier, + transport: Type.Union([Type.Literal("stdio"), Type.Literal("http"), Type.Literal("sse")]), + url: Type.Optional(McpEndpointIrSchema), + command: Type.Optional(Type.String()), }, { additionalProperties: false }, ); -export type TargetBindingIr = Static; +export type McpIr = Static; -export const PluginManifestIrSchema = Type.Object( +export type CapabilityIr = SkillIr | ToolIr | McpIr; + +export const WorkflowEdgeSchema = Type.Object( + { from: Identifier, to: Identifier }, + { additionalProperties: false }, +); +export type WorkflowEdge = Static; + +export const WorkflowEventSchema = Type.Object( + { agent: Identifier, outcome: Identifier, to: Identifier }, + { additionalProperties: false }, +); +export type WorkflowEvent = Static; + +export const WorkflowStopSchema = Type.Object( + { agent: Identifier, outcome: Identifier }, + { additionalProperties: false }, +); +export type WorkflowStop = Static; + +/** + * Control flow: a declarative graph of edges/events/stops, or a programmatic + * controller in a named language. The two dimensions are independent of how + * a runtime exposes capabilities (tool calling vs programmatic calling). + */ +export const WorkflowIrSchema = Type.Object( { irVersion: IrVersion, - kind: Type.Literal("plugin-manifest"), + kind: Type.Literal("workflow"), id: Identifier, - version: Type.String(), - provides: Type.Array(Identifier), - bindings: Type.Array( + mode: Type.Union([Type.Literal("declarative"), Type.Literal("programmatic")]), + edges: Type.Array(WorkflowEdgeSchema), + events: Type.Array(WorkflowEventSchema), + stops: Type.Array(WorkflowStopSchema), + program: Type.Optional( Type.Object( - { - componentId: Identifier, - host: Identifier, - mechanism: Identifier, - strength: StrengthSchema, - }, + { language: Identifier, entry: Type.String() }, { additionalProperties: false }, ), ), }, { additionalProperties: false }, ); -export type PluginManifestIr = Static; +export type WorkflowIr = Static; + +export const ExecutionIrSchema = Type.Union([ + Type.Object({ style: Type.Literal("tool-calling") }, { additionalProperties: false }), + Type.Object( + { + style: Type.Literal("programmatic"), + language: Identifier, + options: Type.Array( + Type.Object({ key: Identifier, value: Identifier }, { additionalProperties: false }), + ), + }, + { additionalProperties: false }, + ), +]); +export type ExecutionIr = Static; + +/** A concrete host with its adapter package and capability-calling style. */ +export const RuntimeIrSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("runtime"), + id: Identifier, + adapter: Type.String(), + execution: ExecutionIrSchema, + }, + { additionalProperties: false }, +); +export type RuntimeIr = Static; export const CapabilityRequirementIrSchema = Type.Object( { - componentId: Identifier, + capabilityId: QualifiedIdentifier, + capabilityKind: CapabilityKindSchema, preferred: Type.Optional(StrengthSchema), minimum: StrengthSchema, onDegrade: Type.Union([Type.Literal("fail"), Type.Literal("report")]), @@ -132,30 +213,64 @@ export const CapabilityRequirementIrSchema = Type.Object( ); export type CapabilityRequirementIr = Static; -export const CompositionSpecIrSchema = Type.Object( +export const AgentIrSchema = Type.Object( { - irVersion: IrVersion, - kind: Type.Literal("composition-spec"), id: Identifier, - target: Identifier, - includes: Type.Array( - Type.Object({ pluginId: Identifier, range: Type.String() }, { additionalProperties: false }), - ), requirements: Type.Array(CapabilityRequirementIrSchema), + }, + { additionalProperties: false }, +); +export type AgentIr = Static; + +export const HarnessSpecIrSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("harness-spec"), + id: Identifier, + workflow: Identifier, + agents: Type.Array(AgentIrSchema), settings: Type.Array(ConfigEntrySchema), }, { additionalProperties: false }, ); -export type CompositionSpecIr = Static; +export type HarnessSpecIr = Static; + +/** Deployment statement: harnesses in the bundle deploy to this runtime. */ +export const TargetIrSchema = Type.Object( + { + runtime: Identifier, + adapter: Type.Optional(Identifier), + }, + { additionalProperties: false }, +); +export type TargetIr = Static; + +export const CapabilityBindingIrSchema = Type.Object( + { + irVersion: IrVersion, + kind: Type.Literal("capability-binding"), + capabilityId: QualifiedIdentifier, + runtime: Identifier, + mechanism: QualifiedIdentifier, + strength: StrengthSchema, + notes: Type.Optional(Type.String()), + }, + { additionalProperties: false }, +); +export type CapabilityBindingIr = Static; export const HarnessIrBundleSchema = Type.Object( { irVersion: IrVersion, kind: Type.Literal("harness-ir-bundle"), - components: Type.Array(ComponentContractIrSchema), - bindings: Type.Array(TargetBindingIrSchema), - plugins: Type.Array(PluginManifestIrSchema), - compositions: Type.Array(CompositionSpecIrSchema), + skills: Type.Array(SkillIrSchema), + tools: Type.Array(ToolIrSchema), + mcps: Type.Array(McpIrSchema), + workflows: Type.Array(WorkflowIrSchema), + runtimes: Type.Array(RuntimeIrSchema), + harnesses: Type.Array(HarnessSpecIrSchema), + targets: Type.Array(TargetIrSchema), + bindings: Type.Array(CapabilityBindingIrSchema), }, { additionalProperties: false }, ); @@ -163,13 +278,15 @@ export type HarnessIrBundle = Static; export const RealizationSchema = Type.Object( { - componentId: Identifier, + agentId: Identifier, + capabilityId: QualifiedIdentifier, + capabilityKind: CapabilityKindSchema, requestedMinimum: StrengthSchema, requestedPreferred: Type.Optional(StrengthSchema), declaredStrength: StrengthSchema, - declaredMechanism: Type.Union([Identifier, Type.Null()]), + declaredMechanism: Type.Union([QualifiedIdentifier, Type.Null()]), realized: StrengthSchema, - materializedMechanism: Type.Union([Identifier, Type.Null()]), + materializedMechanism: Type.Union([QualifiedIdentifier, Type.Null()]), action: Type.Union([ Type.Literal("satisfied"), Type.Literal("degraded"), @@ -186,28 +303,39 @@ export const HarnessRevisionSchema = Type.Object( irVersion: IrVersion, kind: Type.Literal("harness-revision"), revisionId: Type.String({ pattern: "^hr_[0-9a-f]{32}$" }), - composition: Type.Object( + harness: Type.Object( { id: Identifier, contentHash: Type.String() }, { additionalProperties: false }, ), - target: Type.Object({ host: Identifier }, { additionalProperties: false }), + target: Type.Object( + { runtime: Identifier, adapter: Type.String(), execution: ExecutionIrSchema }, + { additionalProperties: false }, + ), + workflow: Type.Object( + { + id: Identifier, + mode: Type.Union([Type.Literal("declarative"), Type.Literal("programmatic")]), + contentHash: Type.String(), + }, + { additionalProperties: false }, + ), resolved: Type.Object( { - plugins: Type.Array( - Type.Object( - { id: Identifier, version: Type.String(), contentHash: Type.String() }, - { additionalProperties: false }, - ), - ), - components: Type.Array( + capabilities: Type.Array( Type.Object( - { id: Identifier, contentHash: Type.String() }, + { id: QualifiedIdentifier, kind: CapabilityKindSchema, contentHash: Type.String() }, { additionalProperties: false }, ), ), }, { additionalProperties: false }, ), + agents: Type.Array( + Type.Object( + { id: Identifier, capabilities: Type.Array(QualifiedIdentifier) }, + { additionalProperties: false }, + ), + ), realization: Type.Array(RealizationSchema), permissions: Type.Array(PermissionGrantSchema), settings: Type.Array(ConfigEntrySchema), @@ -220,8 +348,8 @@ export const ResolutionReportSchema = Type.Object( { irVersion: IrVersion, kind: Type.Literal("resolution-report"), - compositionId: Identifier, - target: Identifier, + harnessId: Identifier, + runtime: Identifier, status: Type.Union([Type.Literal("resolved"), Type.Literal("failed")]), realizations: Type.Array(RealizationSchema), errors: Type.Array(Type.String()), @@ -229,3 +357,15 @@ export const ResolutionReportSchema = Type.Object( { additionalProperties: false }, ); export type ResolutionReport = Static; + +/** Look up one capability across the bundle's skill/tool/mcp arrays. */ +export function findCapability( + bundle: HarnessIrBundle, + capabilityId: string, +): CapabilityIr | undefined { + return ( + bundle.skills.find((skill) => skill.id === capabilityId) ?? + bundle.tools.find((tool) => tool.id === capabilityId) ?? + bundle.mcps.find((mcp) => mcp.id === capabilityId) + ); +} diff --git a/packages/harness/src/language/generated/ast.ts b/packages/harness/src/language/generated/ast.ts index c4d6886..6d26277 100644 --- a/packages/harness/src/language/generated/ast.ts +++ b/packages/harness/src/language/generated/ast.ts @@ -9,7 +9,6 @@ import * as langium from 'langium'; export const HarnessTerminals = { DURATION: /[0-9]+(ms|s|m|h)/, INT: /[0-9]+/, - VERSION_RANGE: /@[~^]?[0-9][0-9a-zA-Z.+*-]*/, ID: /[_a-zA-Z][\w-]*/, STRING: /"[^"]*"/, WS: /\s+/, @@ -21,53 +20,64 @@ export type HarnessTerminalNames = keyof typeof HarnessTerminals; export type HarnessKeywordNames = | "," + | "->" | "." | "=" | "[" | "]" + | "adapter" | "advisory" + | "agent" | "allow" | "binding" - | "component" - | "composition" + | "command" | "configure" + | "connect" | "deny" | "description" | "enforced" - | "extends" + | "env" + | "execution" | "fail" | "false" | "for" - | "hook" - | "include" + | "harness" + | "http" | "input" - | "kind" + | "mcp" | "mechanism" | "minimum" | "model" | "network" | "notes" - | "observer" + | "on" | "on-degrade" | "output" | "permissions" - | "plugin" - | "policy" | "preferred" | "process" | "program" - | "provides" + | "programmatic" | "read" | "report" | "require" + | "runtime" | "skill" + | "source" + | "sse" + | "stdio" + | "stop" | "strength" | "target" | "tool" + | "tool-calling" + | "transport" | "true" - | "ui" | "unsupported" - | "version" + | "url" + | "use" + | "uses" + | "when" | "wired" | "workflow" | "workspace" @@ -77,6 +87,23 @@ export type HarnessKeywordNames = export type HarnessTokenNames = HarnessTerminalNames | HarnessKeywordNames; +export interface AgentDeclaration extends langium.AstNode { + readonly $container: HarnessDeclaration; + readonly $type: 'AgentDeclaration'; + name: string; + requirements: Array; +} + +export const AgentDeclaration = { + $type: 'AgentDeclaration', + name: 'name', + requirements: 'requirements' +} as const; + +export function isAgentDeclaration(item: unknown): item is AgentDeclaration { + return reflection.isInstance(item, AgentDeclaration.$type); +} + export interface BooleanLiteral extends langium.AstNode { readonly $container: ConfigEntry; readonly $type: 'BooleanLiteral'; @@ -92,102 +119,66 @@ export function isBooleanLiteral(item: unknown): item is BooleanLiteral { return reflection.isInstance(item, BooleanLiteral.$type); } -export interface CapabilityRequirement extends langium.AstNode { - readonly $container: CompositionDeclaration; - readonly $type: 'CapabilityRequirement'; - component: langium.Reference; - minimum?: Strength; - onDegrade?: DegradePolicy; - preferred?: Strength; -} - -export const CapabilityRequirement = { - $type: 'CapabilityRequirement', - component: 'component', - minimum: 'minimum', - onDegrade: 'onDegrade', - preferred: 'preferred' -} as const; - -export function isCapabilityRequirement(item: unknown): item is CapabilityRequirement { - return reflection.isInstance(item, CapabilityRequirement.$type); -} - -export interface ComponentContract extends langium.AstNode { +export interface CapabilityBinding extends langium.AstNode { readonly $container: HarnessDocument; - readonly $type: 'ComponentContract'; - description?: string; - inputs: Array; - kind: ComponentKind; - name: string; - outputs: Array; - permissions: Array; + readonly $type: 'CapabilityBinding'; + capability: QualifiedName; + mechanism?: QualifiedName; + notes?: string; + runtimes: Array; + strength?: Strength; } -export const ComponentContract = { - $type: 'ComponentContract', - description: 'description', - inputs: 'inputs', - kind: 'kind', - name: 'name', - outputs: 'outputs', - permissions: 'permissions' +export const CapabilityBinding = { + $type: 'CapabilityBinding', + capability: 'capability', + mechanism: 'mechanism', + notes: 'notes', + runtimes: 'runtimes', + strength: 'strength' } as const; -export function isComponentContract(item: unknown): item is ComponentContract { - return reflection.isInstance(item, ComponentContract.$type); +export function isCapabilityBinding(item: unknown): item is CapabilityBinding { + return reflection.isInstance(item, CapabilityBinding.$type); } -export type ComponentKind = 'hook' | 'observer' | 'policy' | 'program' | 'skill' | 'tool' | 'ui' | 'workflow'; - -export function isComponentKind(item: unknown): item is ComponentKind { - return item === 'skill' || item === 'tool' || item === 'program' || item === 'workflow' || item === 'hook' || item === 'policy' || item === 'observer' || item === 'ui'; -} +export type CapabilityDeclaration = McpDeclaration | SkillDeclaration | ToolDeclaration; -export interface ComponentRef extends langium.AstNode { - readonly $container: PluginDeclaration; - readonly $type: 'ComponentRef'; - component: langium.Reference; -} - -export const ComponentRef = { - $type: 'ComponentRef', - component: 'component' +export const CapabilityDeclaration = { + $type: 'CapabilityDeclaration' } as const; -export function isComponentRef(item: unknown): item is ComponentRef { - return reflection.isInstance(item, ComponentRef.$type); +export function isCapabilityDeclaration(item: unknown): item is CapabilityDeclaration { + return reflection.isInstance(item, CapabilityDeclaration.$type); } -export interface CompositionDeclaration extends langium.AstNode { - readonly $container: HarnessDocument; - readonly $type: 'CompositionDeclaration'; - base?: langium.Reference; - includes: Array; - name: string; - requirements: Array; - settings: Array; - target?: string; +export interface CapabilityUse extends langium.AstNode { + readonly $container: AgentDeclaration; + readonly $type: 'CapabilityUse'; + capability: QualifiedName; + minimum?: Strength; + onDegrade?: DegradePolicy; + preferred?: Strength; + verb: 'connect' | 'require' | 'use'; } -export const CompositionDeclaration = { - $type: 'CompositionDeclaration', - base: 'base', - includes: 'includes', - name: 'name', - requirements: 'requirements', - settings: 'settings', - target: 'target' +export const CapabilityUse = { + $type: 'CapabilityUse', + capability: 'capability', + minimum: 'minimum', + onDegrade: 'onDegrade', + preferred: 'preferred', + verb: 'verb' } as const; -export function isCompositionDeclaration(item: unknown): item is CompositionDeclaration { - return reflection.isInstance(item, CompositionDeclaration.$type); +export function isCapabilityUse(item: unknown): item is CapabilityUse { + return reflection.isInstance(item, CapabilityUse.$type); } export interface ConfigEntry extends langium.AstNode { - readonly $container: CompositionDeclaration; + readonly $container: HarnessDeclaration; readonly $type: 'ConfigEntry'; - key: DottedName; + key: QualifiedName; value: ConfigValue; } @@ -211,7 +202,7 @@ export function isConfigValue(item: unknown): item is ConfigValue { return reflection.isInstance(item, ConfigValue.$type); } -export type Declaration = ComponentContract | CompositionDeclaration | PluginDeclaration | TargetBinding; +export type Declaration = CapabilityBinding | CapabilityDeclaration | HarnessDeclaration | RuntimeDeclaration | TargetDeclaration | WorkflowDeclaration; export const Declaration = { $type: 'Declaration' @@ -227,12 +218,6 @@ export function isDegradePolicy(item: unknown): item is DegradePolicy { return item === 'fail' || item === 'report'; } -export type DottedName = string; - -export function isDottedName(item: unknown): item is DottedName { - return typeof item === 'string'; -} - export interface DurationLiteral extends langium.AstNode { readonly $container: ConfigEntry; readonly $type: 'DurationLiteral'; @@ -248,6 +233,105 @@ export function isDurationLiteral(item: unknown): item is DurationLiteral { return reflection.isInstance(item, DurationLiteral.$type); } +export interface EdgeStatement extends langium.AstNode { + readonly $container: WorkflowDeclaration; + readonly $type: 'EdgeStatement'; + from: string; + to: string; +} + +export const EdgeStatement = { + $type: 'EdgeStatement', + from: 'from', + to: 'to' +} as const; + +export function isEdgeStatement(item: unknown): item is EdgeStatement { + return reflection.isInstance(item, EdgeStatement.$type); +} + +export interface EnvEndpoint extends langium.AstNode { + readonly $container: McpDeclaration; + readonly $type: 'EnvEndpoint'; + variable: string; +} + +export const EnvEndpoint = { + $type: 'EnvEndpoint', + variable: 'variable' +} as const; + +export function isEnvEndpoint(item: unknown): item is EnvEndpoint { + return reflection.isInstance(item, EnvEndpoint.$type); +} + +export interface EventStatement extends langium.AstNode { + readonly $container: WorkflowDeclaration; + readonly $type: 'EventStatement'; + agent: string; + outcome: string; + to: string; +} + +export const EventStatement = { + $type: 'EventStatement', + agent: 'agent', + outcome: 'outcome', + to: 'to' +} as const; + +export function isEventStatement(item: unknown): item is EventStatement { + return reflection.isInstance(item, EventStatement.$type); +} + +export interface ExecutionOption extends langium.AstNode { + readonly $container: ProgrammaticExecution; + readonly $type: 'ExecutionOption'; + key: string; + value: string; +} + +export const ExecutionOption = { + $type: 'ExecutionOption', + key: 'key', + value: 'value' +} as const; + +export function isExecutionOption(item: unknown): item is ExecutionOption { + return reflection.isInstance(item, ExecutionOption.$type); +} + +export type ExecutionSpec = ProgrammaticExecution | ToolCallingExecution; + +export const ExecutionSpec = { + $type: 'ExecutionSpec' +} as const; + +export function isExecutionSpec(item: unknown): item is ExecutionSpec { + return reflection.isInstance(item, ExecutionSpec.$type); +} + +export interface HarnessDeclaration extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'HarnessDeclaration'; + agents: Array; + name: string; + settings: Array; + workflow: langium.Reference; +} + +export const HarnessDeclaration = { + $type: 'HarnessDeclaration', + agents: 'agents', + name: 'name', + settings: 'settings', + workflow: 'workflow' +} as const; + +export function isHarnessDeclaration(item: unknown): item is HarnessDeclaration { + return reflection.isInstance(item, HarnessDeclaration.$type); +} + export interface HarnessDocument extends langium.AstNode { readonly $type: 'HarnessDocument'; elements: Array; @@ -277,6 +361,58 @@ export function isIntLiteral(item: unknown): item is IntLiteral { return reflection.isInstance(item, IntLiteral.$type); } +export interface LiteralEndpoint extends langium.AstNode { + readonly $container: McpDeclaration; + readonly $type: 'LiteralEndpoint'; + value: string; +} + +export const LiteralEndpoint = { + $type: 'LiteralEndpoint', + value: 'value' +} as const; + +export function isLiteralEndpoint(item: unknown): item is LiteralEndpoint { + return reflection.isInstance(item, LiteralEndpoint.$type); +} + +export interface McpDeclaration extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'McpDeclaration'; + command?: string; + name: string; + transport: Transport; + url?: McpEndpoint; +} + +export const McpDeclaration = { + $type: 'McpDeclaration', + command: 'command', + name: 'name', + transport: 'transport', + url: 'url' +} as const; + +export function isMcpDeclaration(item: unknown): item is McpDeclaration { + return reflection.isInstance(item, McpDeclaration.$type); +} + +export type McpEndpoint = EnvEndpoint | LiteralEndpoint; + +export const McpEndpoint = { + $type: 'McpEndpoint' +} as const; + +export function isMcpEndpoint(item: unknown): item is McpEndpoint { + return reflection.isInstance(item, McpEndpoint.$type); +} + +export type NameSegment = 'adapter' | 'agent' | 'allow' | 'command' | 'configure' | 'deny' | 'description' | 'env' | 'execution' | 'harness' | 'http' | 'input' | 'mcp' | 'mechanism' | 'model' | 'network' | 'notes' | 'output' | 'process' | 'program' | 'read' | 'runtime' | 'skill' | 'source' | 'sse' | 'stdio' | 'strength' | 'target' | 'tool' | 'transport' | 'url' | 'workflow' | 'workspace' | 'write' | string; + +export function isNameSegment(item: unknown): item is NameSegment { + return item === 'workspace' || item === 'process' || item === 'network' || item === 'model' || item === 'read' || item === 'write' || item === 'allow' || item === 'deny' || item === 'env' || item === 'url' || item === 'command' || item === 'source' || item === 'description' || item === 'transport' || item === 'adapter' || item === 'execution' || item === 'program' || item === 'runtime' || item === 'target' || item === 'agent' || item === 'skill' || item === 'tool' || item === 'mcp' || item === 'workflow' || item === 'harness' || item === 'input' || item === 'output' || item === 'mechanism' || item === 'strength' || item === 'notes' || item === 'configure' || item === 'stdio' || item === 'http' || item === 'sse' || (typeof item === 'string' && (/[_a-zA-Z][\w-]*/.test(item))); +} + export type PermissionAccess = 'allow' | 'deny' | 'read' | 'write'; export function isPermissionAccess(item: unknown): item is PermissionAccess { @@ -290,7 +426,7 @@ export function isPermissionDomain(item: unknown): item is PermissionDomain { } export interface PermissionRule extends langium.AstNode { - readonly $container: ComponentContract; + readonly $container: SkillDeclaration | ToolDeclaration; readonly $type: 'PermissionRule'; access: PermissionAccess; domain: PermissionDomain; @@ -306,40 +442,101 @@ export function isPermissionRule(item: unknown): item is PermissionRule { return reflection.isInstance(item, PermissionRule.$type); } -export interface PluginDeclaration extends langium.AstNode { +export interface ProgramBody extends langium.AstNode { + readonly $container: WorkflowDeclaration; + readonly $type: 'ProgramBody'; + entry: string; + language: string; +} + +export const ProgramBody = { + $type: 'ProgramBody', + entry: 'entry', + language: 'language' +} as const; + +export function isProgramBody(item: unknown): item is ProgramBody { + return reflection.isInstance(item, ProgramBody.$type); +} + +export interface ProgrammaticExecution extends langium.AstNode { + readonly $container: RuntimeDeclaration; + readonly $type: 'ProgrammaticExecution'; + language: string; + options: Array; +} + +export const ProgrammaticExecution = { + $type: 'ProgrammaticExecution', + language: 'language', + options: 'options' +} as const; + +export function isProgrammaticExecution(item: unknown): item is ProgrammaticExecution { + return reflection.isInstance(item, ProgrammaticExecution.$type); +} + +export type QualifiedName = string; + +export function isQualifiedName(item: unknown): item is QualifiedName { + return typeof item === 'string'; +} + +export interface RuntimeDeclaration extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'RuntimeDeclaration'; + adapter: string; + execution?: ExecutionSpec; + name: string; +} + +export const RuntimeDeclaration = { + $type: 'RuntimeDeclaration', + adapter: 'adapter', + execution: 'execution', + name: 'name' +} as const; + +export function isRuntimeDeclaration(item: unknown): item is RuntimeDeclaration { + return reflection.isInstance(item, RuntimeDeclaration.$type); +} + +export interface SkillDeclaration extends langium.AstNode { readonly $container: HarnessDocument; - readonly $type: 'PluginDeclaration'; + readonly $type: 'SkillDeclaration'; + description?: string; name: string; - provides: Array; - version: string; + permissions: Array; + source?: string; } -export const PluginDeclaration = { - $type: 'PluginDeclaration', +export const SkillDeclaration = { + $type: 'SkillDeclaration', + description: 'description', name: 'name', - provides: 'provides', - version: 'version' + permissions: 'permissions', + source: 'source' } as const; -export function isPluginDeclaration(item: unknown): item is PluginDeclaration { - return reflection.isInstance(item, PluginDeclaration.$type); +export function isSkillDeclaration(item: unknown): item is SkillDeclaration { + return reflection.isInstance(item, SkillDeclaration.$type); } -export interface PluginRequirement extends langium.AstNode { - readonly $container: CompositionDeclaration; - readonly $type: 'PluginRequirement'; - plugin: langium.Reference; - range: string; +export interface StopStatement extends langium.AstNode { + readonly $container: WorkflowDeclaration; + readonly $type: 'StopStatement'; + agent: string; + outcome: string; } -export const PluginRequirement = { - $type: 'PluginRequirement', - plugin: 'plugin', - range: 'range' +export const StopStatement = { + $type: 'StopStatement', + agent: 'agent', + outcome: 'outcome' } as const; -export function isPluginRequirement(item: unknown): item is PluginRequirement { - return reflection.isInstance(item, PluginRequirement.$type); +export function isStopStatement(item: unknown): item is StopStatement { + return reflection.isInstance(item, StopStatement.$type); } export type Strength = 'advisory' | 'enforced' | 'unsupported' | 'wired'; @@ -363,50 +560,147 @@ export function isStringLiteral(item: unknown): item is StringLiteral { return reflection.isInstance(item, StringLiteral.$type); } -export interface TargetBinding extends langium.AstNode { +export interface TargetDeclaration extends langium.AstNode { readonly $container: HarnessDocument; - readonly $type: 'TargetBinding'; - component: langium.Reference; - hosts: Array; - mechanism?: string; - notes?: string; - strength?: Strength; + readonly $type: 'TargetDeclaration'; + adapter?: string; + runtime: string; } -export const TargetBinding = { - $type: 'TargetBinding', - component: 'component', - hosts: 'hosts', - mechanism: 'mechanism', - notes: 'notes', - strength: 'strength' +export const TargetDeclaration = { + $type: 'TargetDeclaration', + adapter: 'adapter', + runtime: 'runtime' +} as const; + +export function isTargetDeclaration(item: unknown): item is TargetDeclaration { + return reflection.isInstance(item, TargetDeclaration.$type); +} + +export interface ToolCallingExecution extends langium.AstNode { + readonly $container: RuntimeDeclaration; + readonly $type: 'ToolCallingExecution'; + style: 'tool-calling'; +} + +export const ToolCallingExecution = { + $type: 'ToolCallingExecution', + style: 'style' +} as const; + +export function isToolCallingExecution(item: unknown): item is ToolCallingExecution { + return reflection.isInstance(item, ToolCallingExecution.$type); +} + +export interface ToolDeclaration extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'ToolDeclaration'; + description?: string; + inputs: Array; + name: QualifiedName; + outputs: Array; + permissions: Array; +} + +export const ToolDeclaration = { + $type: 'ToolDeclaration', + description: 'description', + inputs: 'inputs', + name: 'name', + outputs: 'outputs', + permissions: 'permissions' +} as const; + +export function isToolDeclaration(item: unknown): item is ToolDeclaration { + return reflection.isInstance(item, ToolDeclaration.$type); +} + +export type Transport = 'http' | 'sse' | 'stdio'; + +export function isTransport(item: unknown): item is Transport { + return item === 'stdio' || item === 'http' || item === 'sse'; +} + +export interface WorkflowDeclaration extends langium.AstNode { + readonly $container: HarnessDocument; + readonly $type: 'WorkflowDeclaration'; + name: string; + program?: ProgramBody; + statements: Array; +} + +export const WorkflowDeclaration = { + $type: 'WorkflowDeclaration', + name: 'name', + program: 'program', + statements: 'statements' +} as const; + +export function isWorkflowDeclaration(item: unknown): item is WorkflowDeclaration { + return reflection.isInstance(item, WorkflowDeclaration.$type); +} + +export type WorkflowStatement = EdgeStatement | EventStatement | StopStatement; + +export const WorkflowStatement = { + $type: 'WorkflowStatement' } as const; -export function isTargetBinding(item: unknown): item is TargetBinding { - return reflection.isInstance(item, TargetBinding.$type); +export function isWorkflowStatement(item: unknown): item is WorkflowStatement { + return reflection.isInstance(item, WorkflowStatement.$type); } export type HarnessAstType = { + AgentDeclaration: AgentDeclaration BooleanLiteral: BooleanLiteral - CapabilityRequirement: CapabilityRequirement - ComponentContract: ComponentContract - ComponentRef: ComponentRef - CompositionDeclaration: CompositionDeclaration + CapabilityBinding: CapabilityBinding + CapabilityDeclaration: CapabilityDeclaration + CapabilityUse: CapabilityUse ConfigEntry: ConfigEntry ConfigValue: ConfigValue Declaration: Declaration DurationLiteral: DurationLiteral + EdgeStatement: EdgeStatement + EnvEndpoint: EnvEndpoint + EventStatement: EventStatement + ExecutionOption: ExecutionOption + ExecutionSpec: ExecutionSpec + HarnessDeclaration: HarnessDeclaration HarnessDocument: HarnessDocument IntLiteral: IntLiteral + LiteralEndpoint: LiteralEndpoint + McpDeclaration: McpDeclaration + McpEndpoint: McpEndpoint PermissionRule: PermissionRule - PluginDeclaration: PluginDeclaration - PluginRequirement: PluginRequirement + ProgramBody: ProgramBody + ProgrammaticExecution: ProgrammaticExecution + RuntimeDeclaration: RuntimeDeclaration + SkillDeclaration: SkillDeclaration + StopStatement: StopStatement StringLiteral: StringLiteral - TargetBinding: TargetBinding + TargetDeclaration: TargetDeclaration + ToolCallingExecution: ToolCallingExecution + ToolDeclaration: ToolDeclaration + WorkflowDeclaration: WorkflowDeclaration + WorkflowStatement: WorkflowStatement } export class HarnessAstReflection extends langium.AbstractAstReflection { override readonly types = { + AgentDeclaration: { + name: AgentDeclaration.$type, + properties: { + name: { + name: AgentDeclaration.name + }, + requirements: { + name: AgentDeclaration.requirements, + defaultValue: [], + optional: true + } + }, + superTypes: [] + }, BooleanLiteral: { name: BooleanLiteral.$type, properties: { @@ -418,101 +712,60 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { }, superTypes: [ConfigValue.$type] }, - CapabilityRequirement: { - name: CapabilityRequirement.$type, + CapabilityBinding: { + name: CapabilityBinding.$type, properties: { - component: { - name: CapabilityRequirement.component, - referenceType: ComponentContract.$type - }, - minimum: { - name: CapabilityRequirement.minimum, - optional: true - }, - onDegrade: { - name: CapabilityRequirement.onDegrade, - optional: true + capability: { + name: CapabilityBinding.capability }, - preferred: { - name: CapabilityRequirement.preferred, - optional: true - } - }, - superTypes: [] - }, - ComponentContract: { - name: ComponentContract.$type, - properties: { - description: { - name: ComponentContract.description, + mechanism: { + name: CapabilityBinding.mechanism, optional: true }, - inputs: { - name: ComponentContract.inputs, - defaultValue: [], + notes: { + name: CapabilityBinding.notes, optional: true }, - kind: { - name: ComponentContract.kind - }, - name: { - name: ComponentContract.name - }, - outputs: { - name: ComponentContract.outputs, - defaultValue: [], - optional: true + runtimes: { + name: CapabilityBinding.runtimes, + defaultValue: [] }, - permissions: { - name: ComponentContract.permissions, - defaultValue: [], + strength: { + name: CapabilityBinding.strength, optional: true } }, superTypes: [Declaration.$type] }, - ComponentRef: { - name: ComponentRef.$type, + CapabilityDeclaration: { + name: CapabilityDeclaration.$type, properties: { - component: { - name: ComponentRef.component, - referenceType: ComponentContract.$type - } }, - superTypes: [] + superTypes: [Declaration.$type] }, - CompositionDeclaration: { - name: CompositionDeclaration.$type, + CapabilityUse: { + name: CapabilityUse.$type, properties: { - base: { - name: CompositionDeclaration.base, - referenceType: CompositionDeclaration.$type, - optional: true + capability: { + name: CapabilityUse.capability }, - includes: { - name: CompositionDeclaration.includes, - defaultValue: [], + minimum: { + name: CapabilityUse.minimum, optional: true }, - name: { - name: CompositionDeclaration.name - }, - requirements: { - name: CompositionDeclaration.requirements, - defaultValue: [], + onDegrade: { + name: CapabilityUse.onDegrade, optional: true }, - settings: { - name: CompositionDeclaration.settings, - defaultValue: [], + preferred: { + name: CapabilityUse.preferred, optional: true }, - target: { - name: CompositionDeclaration.target, - optional: true + verb: { + name: CapabilityUse.verb } }, - superTypes: [Declaration.$type] + superTypes: [] }, ConfigEntry: { name: ConfigEntry.$type, @@ -547,6 +800,83 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { }, superTypes: [ConfigValue.$type] }, + EdgeStatement: { + name: EdgeStatement.$type, + properties: { + from: { + name: EdgeStatement.from + }, + to: { + name: EdgeStatement.to + } + }, + superTypes: [WorkflowStatement.$type] + }, + EnvEndpoint: { + name: EnvEndpoint.$type, + properties: { + variable: { + name: EnvEndpoint.variable + } + }, + superTypes: [McpEndpoint.$type] + }, + EventStatement: { + name: EventStatement.$type, + properties: { + agent: { + name: EventStatement.agent + }, + outcome: { + name: EventStatement.outcome + }, + to: { + name: EventStatement.to + } + }, + superTypes: [WorkflowStatement.$type] + }, + ExecutionOption: { + name: ExecutionOption.$type, + properties: { + key: { + name: ExecutionOption.key + }, + value: { + name: ExecutionOption.value + } + }, + superTypes: [] + }, + ExecutionSpec: { + name: ExecutionSpec.$type, + properties: { + }, + superTypes: [] + }, + HarnessDeclaration: { + name: HarnessDeclaration.$type, + properties: { + agents: { + name: HarnessDeclaration.agents, + defaultValue: [], + optional: true + }, + name: { + name: HarnessDeclaration.name + }, + settings: { + name: HarnessDeclaration.settings, + defaultValue: [], + optional: true + }, + workflow: { + name: HarnessDeclaration.workflow, + referenceType: WorkflowDeclaration.$type + } + }, + superTypes: [Declaration.$type] + }, HarnessDocument: { name: HarnessDocument.$type, properties: { @@ -567,6 +897,41 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { }, superTypes: [ConfigValue.$type] }, + LiteralEndpoint: { + name: LiteralEndpoint.$type, + properties: { + value: { + name: LiteralEndpoint.value + } + }, + superTypes: [McpEndpoint.$type] + }, + McpDeclaration: { + name: McpDeclaration.$type, + properties: { + command: { + name: McpDeclaration.command, + optional: true + }, + name: { + name: McpDeclaration.name + }, + transport: { + name: McpDeclaration.transport + }, + url: { + name: McpDeclaration.url, + optional: true + } + }, + superTypes: [CapabilityDeclaration.$type] + }, + McpEndpoint: { + name: McpEndpoint.$type, + properties: { + }, + superTypes: [] + }, PermissionRule: { name: PermissionRule.$type, properties: { @@ -579,34 +944,81 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { }, superTypes: [] }, - PluginDeclaration: { - name: PluginDeclaration.$type, + ProgramBody: { + name: ProgramBody.$type, properties: { - name: { - name: PluginDeclaration.name + entry: { + name: ProgramBody.entry }, - provides: { - name: PluginDeclaration.provides, - defaultValue: [] + language: { + name: ProgramBody.language + } + }, + superTypes: [] + }, + ProgrammaticExecution: { + name: ProgrammaticExecution.$type, + properties: { + language: { + name: ProgrammaticExecution.language }, - version: { - name: PluginDeclaration.version + options: { + name: ProgrammaticExecution.options, + defaultValue: [], + optional: true + } + }, + superTypes: [ExecutionSpec.$type] + }, + RuntimeDeclaration: { + name: RuntimeDeclaration.$type, + properties: { + adapter: { + name: RuntimeDeclaration.adapter + }, + execution: { + name: RuntimeDeclaration.execution, + optional: true + }, + name: { + name: RuntimeDeclaration.name } }, superTypes: [Declaration.$type] }, - PluginRequirement: { - name: PluginRequirement.$type, + SkillDeclaration: { + name: SkillDeclaration.$type, + properties: { + description: { + name: SkillDeclaration.description, + optional: true + }, + name: { + name: SkillDeclaration.name + }, + permissions: { + name: SkillDeclaration.permissions, + defaultValue: [], + optional: true + }, + source: { + name: SkillDeclaration.source, + optional: true + } + }, + superTypes: [CapabilityDeclaration.$type] + }, + StopStatement: { + name: StopStatement.$type, properties: { - plugin: { - name: PluginRequirement.plugin, - referenceType: PluginDeclaration.$type + agent: { + name: StopStatement.agent }, - range: { - name: PluginRequirement.range + outcome: { + name: StopStatement.outcome } }, - superTypes: [] + superTypes: [WorkflowStatement.$type] }, StringLiteral: { name: StringLiteral.$type, @@ -617,31 +1029,79 @@ export class HarnessAstReflection extends langium.AbstractAstReflection { }, superTypes: [ConfigValue.$type] }, - TargetBinding: { - name: TargetBinding.$type, + TargetDeclaration: { + name: TargetDeclaration.$type, properties: { - component: { - name: TargetBinding.component, - referenceType: ComponentContract.$type + adapter: { + name: TargetDeclaration.adapter, + optional: true }, - hosts: { - name: TargetBinding.hosts, - defaultValue: [] + runtime: { + name: TargetDeclaration.runtime + } + }, + superTypes: [Declaration.$type] + }, + ToolCallingExecution: { + name: ToolCallingExecution.$type, + properties: { + style: { + name: ToolCallingExecution.style + } + }, + superTypes: [ExecutionSpec.$type] + }, + ToolDeclaration: { + name: ToolDeclaration.$type, + properties: { + description: { + name: ToolDeclaration.description, + optional: true }, - mechanism: { - name: TargetBinding.mechanism, + inputs: { + name: ToolDeclaration.inputs, + defaultValue: [], optional: true }, - notes: { - name: TargetBinding.notes, + name: { + name: ToolDeclaration.name + }, + outputs: { + name: ToolDeclaration.outputs, + defaultValue: [], optional: true }, - strength: { - name: TargetBinding.strength, + permissions: { + name: ToolDeclaration.permissions, + defaultValue: [], + optional: true + } + }, + superTypes: [CapabilityDeclaration.$type] + }, + WorkflowDeclaration: { + name: WorkflowDeclaration.$type, + properties: { + name: { + name: WorkflowDeclaration.name + }, + program: { + name: WorkflowDeclaration.program, + optional: true + }, + statements: { + name: WorkflowDeclaration.statements, + defaultValue: [], optional: true } }, superTypes: [Declaration.$type] + }, + WorkflowStatement: { + name: WorkflowStatement.$type, + properties: { + }, + superTypes: [] } } as const satisfies langium.AstMetaData } diff --git a/packages/harness/src/language/generated/grammar.ts b/packages/harness/src/language/generated/grammar.ts index f6de10b..520f1a7 100644 --- a/packages/harness/src/language/generated/grammar.ts +++ b/packages/harness/src/language/generated/grammar.ts @@ -48,21 +48,35 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn { "$type": "RuleCall", "rule": { - "$ref": "#/rules@7" + "$ref": "#/rules@5" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@9" + "$ref": "#/rules@11" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@11" + "$ref": "#/rules@17" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@20" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@21" }, "arguments": [] } @@ -74,13 +88,13 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "ComponentContract", + "name": "HarnessDeclaration", "definition": { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "component" + "value": "harness" }, { "$type": "Assignment", @@ -89,7 +103,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@37" }, "arguments": [] } @@ -100,110 +114,47 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "Keyword", - "value": "kind" + "value": "workflow" }, { "$type": "Assignment", - "feature": "kind", + "feature": "workflow", "operator": "=", "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@3" - }, - "arguments": [] - } - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "description" - }, - { - "$type": "Assignment", - "feature": "description", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@26" - }, - "arguments": [] - } - } - ], - "cardinality": "?" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "input" - }, - { - "$type": "Keyword", - "value": "{" + "$type": "CrossReference", + "type": { + "$ref": "#/rules@5" }, - { - "$type": "Assignment", - "feature": "inputs", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" }, - "cardinality": "*" + "arguments": [] }, - { - "$type": "Keyword", - "value": "}" - } - ], - "cardinality": "?" + "deprecatedSyntax": false, + "isMulti": false + } }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "output" - }, - { - "$type": "Keyword", - "value": "{" - }, - { - "$type": "Assignment", - "feature": "outputs", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] - }, - "cardinality": "*" + "$type": "Assignment", + "feature": "agents", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@3" }, - { - "$type": "Keyword", - "value": "}" - } - ], - "cardinality": "?" + "arguments": [] + }, + "cardinality": "*" }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "permissions" + "value": "configure" }, { "$type": "Keyword", @@ -211,12 +162,12 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "Assignment", - "feature": "permissions", + "feature": "settings", "operator": "+=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@4" + "$ref": "#/rules@27" }, "arguments": [] }, @@ -241,42 +192,46 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "ComponentKind", - "dataType": "string", + "name": "AgentDeclaration", "definition": { - "$type": "Alternatives", + "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "skill" - }, - { - "$type": "Keyword", - "value": "tool" - }, - { - "$type": "Keyword", - "value": "program" - }, - { - "$type": "Keyword", - "value": "workflow" + "value": "agent" }, { - "$type": "Keyword", - "value": "hook" + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } }, { "$type": "Keyword", - "value": "policy" + "value": "{" }, { - "$type": "Keyword", - "value": "observer" + "$type": "Assignment", + "feature": "requirements", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@4" + }, + "arguments": [] + }, + "cardinality": "*" }, { "$type": "Keyword", - "value": "ui" + "value": "}" } ] }, @@ -286,91 +241,476 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "PermissionRule", + "name": "CapabilityUse", "definition": { "$type": "Group", "elements": [ { - "$type": "Assignment", - "feature": "domain", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@5" + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "verb", + "operator": "=", + "terminal": { + "$type": "Keyword", + "value": "use" + } + }, + { + "$type": "Keyword", + "value": "skill" + } + ] }, - "arguments": [] - } + { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "verb", + "operator": "=", + "terminal": { + "$type": "Keyword", + "value": "require" + } + }, + { + "$type": "Keyword", + "value": "tool" + } + ] + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "verb", + "operator": "=", + "terminal": { + "$type": "Keyword", + "value": "connect" + } + }, + { + "$type": "Keyword", + "value": "mcp" + } + ] + } + ] }, { "$type": "Assignment", - "feature": "access", + "feature": "capability", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@6" + "$ref": "#/rules@28" }, "arguments": [] } - } - ] - }, - "entry": false, - "fragment": false, - "parameters": [] - }, - { - "$type": "ParserRule", - "name": "PermissionDomain", - "dataType": "string", - "definition": { - "$type": "Alternatives", - "elements": [ - { - "$type": "Keyword", - "value": "workspace" - }, - { - "$type": "Keyword", - "value": "process" - }, - { - "$type": "Keyword", - "value": "network" }, { - "$type": "Keyword", - "value": "model" - } - ] - }, - "entry": false, - "fragment": false, - "parameters": [] - }, - { - "$type": "ParserRule", - "name": "PermissionAccess", - "dataType": "string", + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "preferred" + }, + { + "$type": "Assignment", + "feature": "preferred", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "minimum" + }, + { + "$type": "Assignment", + "feature": "minimum", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@22" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "on-degrade" + }, + { + "$type": "Assignment", + "feature": "onDegrade", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@23" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "}" + } + ], + "cardinality": "?" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "WorkflowDeclaration", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "workflow" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "program", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@6" + }, + "arguments": [] + }, + "cardinality": "?" + }, + { + "$type": "Assignment", + "feature": "statements", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@7" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "ProgramBody", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "program" + }, + { + "$type": "Assignment", + "feature": "language", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + }, + { + "$type": "Assignment", + "feature": "entry", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@38" + }, + "arguments": [] + } + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "WorkflowStatement", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@8" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@9" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@10" + }, + "arguments": [] + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "EdgeStatement", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "from", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "->" + }, + { + "$type": "Assignment", + "feature": "to", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "EventStatement", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "on" + }, + { + "$type": "Assignment", + "feature": "agent", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "Assignment", + "feature": "outcome", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "->" + }, + { + "$type": "Assignment", + "feature": "to", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "StopStatement", "definition": { - "$type": "Alternatives", + "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "read" + "value": "stop" }, { "$type": "Keyword", - "value": "write" + "value": "when" }, { - "$type": "Keyword", - "value": "allow" + "$type": "Assignment", + "feature": "agent", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } }, { "$type": "Keyword", - "value": "deny" + "value": "." + }, + { + "$type": "Assignment", + "feature": "outcome", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "CapabilityDeclaration", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@12" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@13" + }, + "arguments": [] + }, + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@14" + }, + "arguments": [] } ] }, @@ -380,143 +720,323 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "TargetBinding", + "name": "SkillDeclaration", "definition": { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "binding" + "value": "skill" }, { "$type": "Assignment", - "feature": "component", + "feature": "name", "operator": "=", "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@2" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" }, - "deprecatedSyntax": false, - "isMulti": false + "arguments": [] } }, { "$type": "Keyword", - "value": "for" + "value": "{" }, { - "$type": "Alternatives", + "$type": "Group", "elements": [ + { + "$type": "Keyword", + "value": "source" + }, { "$type": "Assignment", - "feature": "hosts", - "operator": "+=", + "feature": "source", + "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@38" }, "arguments": [] } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "[" + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@38" }, - { - "$type": "Assignment", - "feature": "hosts", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] - } + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "permissions" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "permissions", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@24" }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "," - }, - { - "$type": "Assignment", - "feature": "hosts", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] - } - } - ], - "cardinality": "*" + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" + } + ], + "cardinality": "?" + }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "ToolDeclaration", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "tool" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@28" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Assignment", + "feature": "description", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@38" }, - { - "$type": "Keyword", - "value": "]" - } - ] + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "input" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "inputs", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" } - ] + ], + "cardinality": "?" }, { - "$type": "Keyword", - "value": "{" + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "output" + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "outputs", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" + } + ], + "cardinality": "?" }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "mechanism" + "value": "permissions" + }, + { + "$type": "Keyword", + "value": "{" }, { "$type": "Assignment", - "feature": "mechanism", - "operator": "=", + "feature": "permissions", + "operator": "+=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@24" }, "arguments": [] - } + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" } ], "cardinality": "?" }, + { + "$type": "Keyword", + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "McpDeclaration", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "mcp" + }, + { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Keyword", + "value": "transport" + }, + { + "$type": "Assignment", + "feature": "transport", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@15" + }, + "arguments": [] + } + }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "strength" + "value": "url" }, { "$type": "Assignment", - "feature": "strength", + "feature": "url", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@8" + "$ref": "#/rules@16" }, "arguments": [] } @@ -529,16 +1049,16 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "elements": [ { "$type": "Keyword", - "value": "notes" + "value": "command" }, { "$type": "Assignment", - "feature": "notes", + "feature": "command", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@38" }, "arguments": [] } @@ -558,26 +1078,90 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "Strength", + "name": "Transport", "dataType": "string", "definition": { "$type": "Alternatives", "elements": [ { "$type": "Keyword", - "value": "unsupported" + "value": "stdio" }, { "$type": "Keyword", - "value": "advisory" + "value": "http" }, { "$type": "Keyword", - "value": "wired" + "value": "sse" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "McpEndpoint", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "EnvEndpoint" + } + }, + { + "$type": "Keyword", + "value": "env" + }, + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "Assignment", + "feature": "variable", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + } + ] }, { - "$type": "Keyword", - "value": "enforced" + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "LiteralEndpoint" + } + }, + { + "$type": "Assignment", + "feature": "value", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@38" + }, + "arguments": [] + } + } + ] } ] }, @@ -587,13 +1171,13 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "PluginDeclaration", + "name": "RuntimeDeclaration", "definition": { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "plugin" + "value": "runtime" }, { "$type": "Assignment", @@ -602,7 +1186,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@37" }, "arguments": [] } @@ -613,36 +1197,16 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "Keyword", - "value": "version" + "value": "adapter" }, { "$type": "Assignment", - "feature": "version", + "feature": "adapter", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" - }, - "arguments": [] - } - }, - { - "$type": "Keyword", - "value": "provides" - }, - { - "$type": "Keyword", - "value": "[" - }, - { - "$type": "Assignment", - "feature": "provides", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@38" }, "arguments": [] } @@ -652,30 +1216,118 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "elements": [ { "$type": "Keyword", - "value": "," + "value": "execution" }, { "$type": "Assignment", - "feature": "provides", - "operator": "+=", + "feature": "execution", + "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@10" + "$ref": "#/rules@18" }, "arguments": [] } } ], - "cardinality": "*" + "cardinality": "?" }, { "$type": "Keyword", - "value": "]" + "value": "}" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "ExecutionSpec", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "ToolCallingExecution" + } + }, + { + "$type": "Assignment", + "feature": "style", + "operator": "=", + "terminal": { + "$type": "Keyword", + "value": "tool-calling" + } + } + ] }, { - "$type": "Keyword", - "value": "}" + "$type": "Group", + "elements": [ + { + "$type": "Action", + "inferredType": { + "$type": "InferredType", + "name": "ProgrammaticExecution" + } + }, + { + "$type": "Keyword", + "value": "programmatic" + }, + { + "$type": "Keyword", + "value": "." + }, + { + "$type": "Assignment", + "feature": "language", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "{" + }, + { + "$type": "Assignment", + "feature": "options", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@19" + }, + "arguments": [] + }, + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "}" + } + ], + "cardinality": "?" + } + ] } ] }, @@ -685,36 +1337,32 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "ComponentRef", + "name": "ExecutionOption", "definition": { "$type": "Group", "elements": [ { - "$type": "Keyword", - "value": "component" - }, - { - "$type": "Keyword", - "value": "." + "$type": "Assignment", + "feature": "key", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } }, { "$type": "Assignment", - "feature": "component", + "feature": "value", "operator": "=", "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@2" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" }, - "deprecatedSyntax": false, - "isMulti": false + "arguments": [] } } ] @@ -725,22 +1373,22 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "CompositionDeclaration", + "name": "TargetDeclaration", "definition": { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "composition" + "value": "target" }, { "$type": "Assignment", - "feature": "name", + "feature": "runtime", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@37" }, "arguments": [] } @@ -750,76 +1398,74 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "elements": [ { "$type": "Keyword", - "value": "extends" + "value": "uses" }, { - "$type": "Assignment", - "feature": "base", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@11" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] - }, - "deprecatedSyntax": false, - "isMulti": false - } - } - ], - "cardinality": "?" - }, - { - "$type": "Keyword", - "value": "{" - }, - { - "$type": "Group", - "elements": [ + "$type": "Keyword", + "value": "adapter" + }, { "$type": "Keyword", - "value": "target" + "value": "." }, { "$type": "Assignment", - "feature": "target", + "feature": "adapter", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@37" }, "arguments": [] } } ], "cardinality": "?" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "CapabilityBinding", + "definition": { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "binding" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "include" - }, - { - "$type": "Keyword", - "value": "[" + "$type": "Assignment", + "feature": "capability", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@28" }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "for" + }, + { + "$type": "Alternatives", + "elements": [ { "$type": "Assignment", - "feature": "includes", + "feature": "runtimes", "operator": "+=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@37" }, "arguments": [] } @@ -829,70 +1475,116 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "elements": [ { "$type": "Keyword", - "value": "," + "value": "[" }, { "$type": "Assignment", - "feature": "includes", + "feature": "runtimes", "operator": "+=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@12" + "$ref": "#/rules@37" }, "arguments": [] } + }, + { + "$type": "Group", + "elements": [ + { + "$type": "Keyword", + "value": "," + }, + { + "$type": "Assignment", + "feature": "runtimes", + "operator": "+=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + } + } + ], + "cardinality": "*" + }, + { + "$type": "Keyword", + "value": "]" } - ], - "cardinality": "*" - }, - { - "$type": "Keyword", - "value": "]" + ] } - ], - "cardinality": "?" + ] }, { - "$type": "Assignment", - "feature": "requirements", - "operator": "+=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@13" - }, - "arguments": [] - }, - "cardinality": "*" + "$type": "Keyword", + "value": "{" }, { "$type": "Group", "elements": [ { "$type": "Keyword", - "value": "configure" + "value": "mechanism" }, + { + "$type": "Assignment", + "feature": "mechanism", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@28" + }, + "arguments": [] + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ { "$type": "Keyword", - "value": "{" + "value": "strength" }, { "$type": "Assignment", - "feature": "settings", - "operator": "+=", + "feature": "strength", + "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@15" + "$ref": "#/rules@22" }, "arguments": [] - }, - "cardinality": "*" - }, + } + } + ], + "cardinality": "?" + }, + { + "$type": "Group", + "elements": [ { "$type": "Keyword", - "value": "}" + "value": "notes" + }, + { + "$type": "Assignment", + "feature": "notes", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@38" + }, + "arguments": [] + } } ], "cardinality": "?" @@ -909,46 +1601,80 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "PluginRequirement", + "name": "Strength", + "dataType": "string", "definition": { - "$type": "Group", + "$type": "Alternatives", "elements": [ { "$type": "Keyword", - "value": "plugin" + "value": "unsupported" + }, + { + "$type": "Keyword", + "value": "advisory" }, { "$type": "Keyword", - "value": "." + "value": "wired" + }, + { + "$type": "Keyword", + "value": "enforced" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "DegradePolicy", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "Keyword", + "value": "fail" }, + { + "$type": "Keyword", + "value": "report" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, + { + "$type": "ParserRule", + "name": "PermissionRule", + "definition": { + "$type": "Group", + "elements": [ { "$type": "Assignment", - "feature": "plugin", + "feature": "domain", "operator": "=", "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@9" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@25" }, - "deprecatedSyntax": false, - "isMulti": false + "arguments": [] } }, { "$type": "Assignment", - "feature": "range", + "feature": "access", "operator": "=", "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@24" + "$ref": "#/rules@26" }, "arguments": [] } @@ -961,113 +1687,26 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "CapabilityRequirement", + "name": "PermissionDomain", + "dataType": "string", "definition": { - "$type": "Group", + "$type": "Alternatives", "elements": [ { "$type": "Keyword", - "value": "require" + "value": "workspace" }, { - "$type": "Assignment", - "feature": "component", - "operator": "=", - "terminal": { - "$type": "CrossReference", - "type": { - "$ref": "#/rules@2" - }, - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@25" - }, - "arguments": [] - }, - "deprecatedSyntax": false, - "isMulti": false - } + "$type": "Keyword", + "value": "process" }, { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "{" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "preferred" - }, - { - "$type": "Assignment", - "feature": "preferred", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" - }, - "arguments": [] - } - } - ], - "cardinality": "?" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "minimum" - }, - { - "$type": "Assignment", - "feature": "minimum", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@8" - }, - "arguments": [] - } - } - ], - "cardinality": "?" - }, - { - "$type": "Group", - "elements": [ - { - "$type": "Keyword", - "value": "on-degrade" - }, - { - "$type": "Assignment", - "feature": "onDegrade", - "operator": "=", - "terminal": { - "$type": "RuleCall", - "rule": { - "$ref": "#/rules@14" - }, - "arguments": [] - } - } - ], - "cardinality": "?" - }, - { - "$type": "Keyword", - "value": "}" - } - ], - "cardinality": "?" + "$type": "Keyword", + "value": "network" + }, + { + "$type": "Keyword", + "value": "model" } ] }, @@ -1077,18 +1716,26 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "DegradePolicy", + "name": "PermissionAccess", "dataType": "string", "definition": { "$type": "Alternatives", "elements": [ { "$type": "Keyword", - "value": "fail" + "value": "read" }, { "$type": "Keyword", - "value": "report" + "value": "write" + }, + { + "$type": "Keyword", + "value": "allow" + }, + { + "$type": "Keyword", + "value": "deny" } ] }, @@ -1109,7 +1756,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@16" + "$ref": "#/rules@28" }, "arguments": [] } @@ -1125,7 +1772,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@17" + "$ref": "#/rules@30" }, "arguments": [] } @@ -1138,7 +1785,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn }, { "$type": "ParserRule", - "name": "DottedName", + "name": "QualifiedName", "dataType": "string", "definition": { "$type": "Group", @@ -1146,7 +1793,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@29" }, "arguments": [] }, @@ -1160,7 +1807,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn { "$type": "RuleCall", "rule": { - "$ref": "#/rules@25" + "$ref": "#/rules@29" }, "arguments": [] } @@ -1173,6 +1820,162 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "fragment": false, "parameters": [] }, + { + "$type": "ParserRule", + "name": "NameSegment", + "dataType": "string", + "definition": { + "$type": "Alternatives", + "elements": [ + { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@37" + }, + "arguments": [] + }, + { + "$type": "Keyword", + "value": "workspace" + }, + { + "$type": "Keyword", + "value": "process" + }, + { + "$type": "Keyword", + "value": "network" + }, + { + "$type": "Keyword", + "value": "model" + }, + { + "$type": "Keyword", + "value": "read" + }, + { + "$type": "Keyword", + "value": "write" + }, + { + "$type": "Keyword", + "value": "allow" + }, + { + "$type": "Keyword", + "value": "deny" + }, + { + "$type": "Keyword", + "value": "env" + }, + { + "$type": "Keyword", + "value": "url" + }, + { + "$type": "Keyword", + "value": "command" + }, + { + "$type": "Keyword", + "value": "source" + }, + { + "$type": "Keyword", + "value": "description" + }, + { + "$type": "Keyword", + "value": "transport" + }, + { + "$type": "Keyword", + "value": "adapter" + }, + { + "$type": "Keyword", + "value": "execution" + }, + { + "$type": "Keyword", + "value": "program" + }, + { + "$type": "Keyword", + "value": "runtime" + }, + { + "$type": "Keyword", + "value": "target" + }, + { + "$type": "Keyword", + "value": "agent" + }, + { + "$type": "Keyword", + "value": "skill" + }, + { + "$type": "Keyword", + "value": "tool" + }, + { + "$type": "Keyword", + "value": "mcp" + }, + { + "$type": "Keyword", + "value": "workflow" + }, + { + "$type": "Keyword", + "value": "harness" + }, + { + "$type": "Keyword", + "value": "input" + }, + { + "$type": "Keyword", + "value": "output" + }, + { + "$type": "Keyword", + "value": "mechanism" + }, + { + "$type": "Keyword", + "value": "strength" + }, + { + "$type": "Keyword", + "value": "notes" + }, + { + "$type": "Keyword", + "value": "configure" + }, + { + "$type": "Keyword", + "value": "stdio" + }, + { + "$type": "Keyword", + "value": "http" + }, + { + "$type": "Keyword", + "value": "sse" + } + ] + }, + "entry": false, + "fragment": false, + "parameters": [] + }, { "$type": "ParserRule", "name": "ConfigValue", @@ -1182,28 +1985,28 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn { "$type": "RuleCall", "rule": { - "$ref": "#/rules@18" + "$ref": "#/rules@31" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@19" + "$ref": "#/rules@32" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@20" + "$ref": "#/rules@33" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@21" + "$ref": "#/rules@34" }, "arguments": [] } @@ -1223,7 +2026,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@23" + "$ref": "#/rules@36" }, "arguments": [] } @@ -1242,7 +2045,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@22" + "$ref": "#/rules@35" }, "arguments": [] } @@ -1261,7 +2064,7 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@26" + "$ref": "#/rules@38" }, "arguments": [] } @@ -1333,17 +2136,6 @@ export const HarnessGrammar = (): Grammar => loadedHarnessGrammar ?? (loadedHarn "fragment": false, "hidden": false }, - { - "$type": "TerminalRule", - "name": "VERSION_RANGE", - "definition": { - "$type": "RegexToken", - "regex": "/@[~^]?[0-9][0-9a-zA-Z.+*-]*/", - "parenthesized": false - }, - "fragment": false, - "hidden": false - }, { "$type": "TerminalRule", "name": "ID", diff --git a/packages/harness/src/language/harness-validator.ts b/packages/harness/src/language/harness-validator.ts index e609d01..0cfde23 100644 --- a/packages/harness/src/language/harness-validator.ts +++ b/packages/harness/src/language/harness-validator.ts @@ -1,9 +1,9 @@ import type { ValidationAcceptor, ValidationChecks } from "langium"; import { - type CapabilityRequirement, + type CapabilityUse, type HarnessAstType, type HarnessDocument, - isTargetBinding, + isCapabilityBinding, } from "./generated/ast.js"; import type { HarnessServices } from "./harness-module.js"; @@ -17,14 +17,14 @@ export function registerValidationChecks(services: HarnessServices): void { const registry = services.validation.ValidationRegistry; const validator = services.validation.HarnessValidator; const checks: ValidationChecks = { - CapabilityRequirement: validator.checkPreferredNotBelowMinimum, - HarnessDocument: validator.checkUniqueBindingPerHost, + CapabilityUse: validator.checkPreferredNotBelowMinimum, + HarnessDocument: validator.checkUniqueBindingPerRuntime, }; registry.register(checks, validator); } export class HarnessValidator { - checkPreferredNotBelowMinimum(requirement: CapabilityRequirement, accept: ValidationAcceptor): void { + checkPreferredNotBelowMinimum(requirement: CapabilityUse, accept: ValidationAcceptor): void { if (requirement.preferred === undefined || requirement.minimum === undefined) { return; } @@ -37,19 +37,19 @@ export class HarnessValidator { } } - checkUniqueBindingPerHost(document: HarnessDocument, accept: ValidationAcceptor): void { + checkUniqueBindingPerRuntime(document: HarnessDocument, accept: ValidationAcceptor): void { const seen = new Set(); for (const element of document.elements) { - if (!isTargetBinding(element)) { + if (!isCapabilityBinding(element)) { continue; } - for (const host of element.hosts) { - const key = `${element.component.$refText}::${host}`; + for (const runtime of element.runtimes) { + const key = `${element.capability}::${runtime}`; if (seen.has(key)) { accept( "error", - `Duplicate binding for component '${element.component.$refText}' on host '${host}'.`, - { node: element, property: "hosts" }, + `Duplicate binding for capability '${element.capability}' on runtime '${runtime}'.`, + { node: element, property: "runtimes" }, ); } seen.add(key); diff --git a/packages/harness/src/language/harness.langium b/packages/harness/src/language/harness.langium index cea1dcc..2ebd34b 100644 --- a/packages/harness/src/language/harness.langium +++ b/packages/harness/src/language/harness.langium @@ -1,49 +1,149 @@ -// Harness assembly language, v0.1 surface. +// Harness assembly language, v0.2 surface. // -// The DSL declares what a harness is assembled from (contracts, bindings, -// plugins, compositions). Revisions and resolution reports are resolver -// outputs, not authored syntax. +// The DSL describes how an agent run works, host-neutrally: which harness +// assembles which workflow and agent roles, which capabilities (skills, +// tools, MCP connections) each role needs, and which runtimes the harness +// deploys to. Adapters map capabilities onto host-native assets via +// `binding` mechanisms in a host namespace (`qoder.plugin`, `pi.extension`); +// the core language defines no generic plugin. Revisions and resolution +// reports are resolver outputs, not authored syntax. grammar Harness entry HarnessDocument: (elements+=Declaration)*; Declaration: - ComponentContract | TargetBinding | PluginDeclaration | CompositionDeclaration; + HarnessDeclaration | WorkflowDeclaration | CapabilityDeclaration + | RuntimeDeclaration | TargetDeclaration | CapabilityBinding; + +// A complete assembly: the workflow that drives the run, the logical agent +// roles that participate, and run configuration. Runtimes are chosen at +// deployment via `target`, never inside the harness itself. +HarnessDeclaration: + 'harness' name=ID '{' + 'workflow' workflow=[WorkflowDeclaration:ID] + (agents+=AgentDeclaration)* + ('configure' '{' (settings+=ConfigEntry)* '}')? + '}'; + +// A logical role inside a harness (`author`, `verifier`). Hosts such as +// Qoder or Pi are runtimes, not agents. +AgentDeclaration: + 'agent' name=ID '{' + (requirements+=CapabilityUse)* + '}'; + +// One capability requirement of an agent. The verb states the capability +// kind: `use skill`, `require tool`, `connect mcp`. Tools referenced without +// a declaration become implicit atomic contracts; skills and MCP entries +// must be declared because they carry source or connection facts. The +// optional block keeps the v0.1 degradation ladder: a bare requirement asks +// for the `advisory` floor with `on-degrade report`. +CapabilityUse: + ( verb='use' 'skill' | verb='require' 'tool' | verb='connect' 'mcp' ) + capability=QualifiedName + ('{' + ('preferred' preferred=Strength)? + ('minimum' minimum=Strength)? + ('on-degrade' onDegrade=DegradePolicy)? + '}')?; + +// Control flow. Declarative graphs list agent-to-agent edges, event routes, +// and stop conditions; programmatic workflows delegate control to a program +// in a named language. A workflow declares exactly one of the two forms +// (enforced semantically for precise diagnostics). +WorkflowDeclaration: + 'workflow' name=ID '{' + (program=ProgramBody)? + (statements+=WorkflowStatement)* + '}'; + +ProgramBody: + 'program' language=ID entry=STRING; + +WorkflowStatement: + EdgeStatement | EventStatement | StopStatement; -// A target-independent capability contract: what a capability is, -// what it consumes and produces, and which permissions it asks for. -ComponentContract: - 'component' name=ID '{' - 'kind' kind=ComponentKind +EdgeStatement: + from=ID '->' to=ID; + +EventStatement: + 'on' agent=ID '.' outcome=ID '->' to=ID; + +StopStatement: + 'stop' 'when' agent=ID '.' outcome=ID; + +CapabilityDeclaration: + SkillDeclaration | ToolDeclaration | McpDeclaration; + +// Progressive knowledge per the Agent Skills standard: either a `source` +// directory, or inline `description` guidance, or both. +SkillDeclaration: + 'skill' name=ID '{' + ('source' source=STRING)? + ('description' description=STRING)? + ('permissions' '{' (permissions+=PermissionRule)* '}')? + '}'; + +// An atomic callable capability with input/output identifiers. Dotted names +// (`workspace.read`) namespace tools by capability surface. +ToolDeclaration: + 'tool' name=QualifiedName '{' ('description' description=STRING)? ('input' '{' (inputs+=ID)* '}')? ('output' '{' (outputs+=ID)* '}')? ('permissions' '{' (permissions+=PermissionRule)* '}')? '}'; -ComponentKind returns string: - 'skill' | 'tool' | 'program' | 'workflow' | 'hook' | 'policy' | 'observer' | 'ui'; - -PermissionRule: - domain=PermissionDomain access=PermissionAccess; - -PermissionDomain returns string: - 'workspace' | 'process' | 'network' | 'model'; +// A capability connection, not a tool: what the MCP server is reached over. +// Which tools/resources/prompts it provides is discovered at runtime. +McpDeclaration: + 'mcp' name=ID '{' + 'transport' transport=Transport + ('url' url=McpEndpoint)? + ('command' command=STRING)? + '}'; -PermissionAccess returns string: - 'read' | 'write' | 'allow' | 'deny'; +Transport returns string: + 'stdio' | 'http' | 'sse'; + +McpEndpoint: + {infer EnvEndpoint} 'env' '.' variable=ID + | {infer LiteralEndpoint} value=STRING; + +// A concrete host (Qoder, Pi, DeepSeek Harness, Prime Agent): its adapter +// package and how it exposes capabilities to the model — classic tool +// calling, or programmatic calling in a host language (Prime's persistent +// IPython, DeepSeek's TypeScript plugin runtime). +RuntimeDeclaration: + 'runtime' name=ID '{' + 'adapter' adapter=STRING + ('execution' execution=ExecutionSpec)? + '}'; -// How a component is realized on one or more concrete hosts, and how -// strong the realized guarantee is. `for [a, b]` declares the same -// mechanism/strength on several hosts at once. `mechanism` and `strength` -// are optional: an omitted binding is prompt guidance at `advisory`, the -// v0.1 materialization floor, so authors only spell out a binding to -// declare a stronger future mechanism or an explicit `unsupported`. -TargetBinding: - 'binding' component=[ComponentContract:ID] 'for' - ( hosts+=ID | '[' hosts+=ID (',' hosts+=ID)* ']' ) '{' - ('mechanism' mechanism=ID)? +ExecutionSpec: + {infer ToolCallingExecution} style='tool-calling' + | {infer ProgrammaticExecution} 'programmatic' '.' language=ID + ('{' (options+=ExecutionOption)* '}')?; + +ExecutionOption: + key=ID value=ID; + +// Deployment statement: this document's harnesses deploy to the named +// runtime. `uses adapter.` is shorthand when no runtime block exists; +// a declared runtime already owns its adapter. +TargetDeclaration: + 'target' runtime=ID ('uses' 'adapter' '.' adapter=ID)?; + +// Adapter-layer mapping of one capability onto one or more runtimes. The +// mechanism is a dotted name so host-native assets stay in the host's own +// namespace (`qoder.plugin`, `pi.extension`, `deepseek.plugin`, +// `prime.python-skill`). An omitted binding is prompt guidance at +// `advisory`, the v0.2 materialization floor. +CapabilityBinding: + 'binding' capability=QualifiedName 'for' + ( runtimes+=ID | '[' runtimes+=ID (',' runtimes+=ID)* ']' ) '{' + ('mechanism' mechanism=QualifiedName)? ('strength' strength=Strength)? ('notes' notes=STRING)? '}'; @@ -51,55 +151,36 @@ TargetBinding: Strength returns string: 'unsupported' | 'advisory' | 'wired' | 'enforced'; -// Distribution boundary: which contracts a package provides. -// Bindings for provided components are attached by the compiler. -PluginDeclaration: - 'plugin' name=ID '{' - 'version' version=STRING - 'provides' '[' provides+=ComponentRef (',' provides+=ComponentRef)* ']' - '}'; - -ComponentRef: - 'component' '.' component=[ComponentContract:ID]; - -// Desired assembly for one run target. Resolving a composition against -// a registry produces an immutable revision plus a resolution report. -// -// `extends` inherits the base composition's target, includes, requirements, -// and configuration; the child overrides `target` and appends its own -// members. `include` is optional: a composition may `require` components -// directly, without wrapping them in a plugin, in which case they -// materialize as advisory prompt guidance. -CompositionDeclaration: - 'composition' name=ID ('extends' base=[CompositionDeclaration:ID])? '{' - ('target' target=ID)? - ('include' '[' includes+=PluginRequirement (',' includes+=PluginRequirement)* ']')? - (requirements+=CapabilityRequirement)* - ('configure' '{' (settings+=ConfigEntry)* '}')? - '}'; +DegradePolicy returns string: + 'fail' | 'report'; -PluginRequirement: - 'plugin' '.' plugin=[PluginDeclaration:ID] range=VERSION_RANGE; +PermissionRule: + domain=PermissionDomain access=PermissionAccess; -// A bare `require component` is the common case: it asks for the component -// at the `advisory` floor with `on-degrade report`. The optional block -// raises the floor (`minimum`), states a `preferred` ceiling, or makes -// degradation fatal (`on-degrade fail`). -CapabilityRequirement: - 'require' component=[ComponentContract:ID] ('{' - ('preferred' preferred=Strength)? - ('minimum' minimum=Strength)? - ('on-degrade' onDegrade=DegradePolicy)? - '}')?; +PermissionDomain returns string: + 'workspace' | 'process' | 'network' | 'model'; -DegradePolicy returns string: - 'fail' | 'report'; +PermissionAccess returns string: + 'read' | 'write' | 'allow' | 'deny'; ConfigEntry: - key=DottedName '=' value=ConfigValue; - -DottedName returns string: - ID ('.' ID)*; + key=QualifiedName '=' value=ConfigValue; + +// Dotted names appear as tool ids, binding mechanisms, and configuration +// keys. Their segments accept the language's non-structural keywords as soft +// keywords, so `workspace.read`, `process.exec`, or `runtime.timeout` stay +// legal even though `workspace`, `process`, and `runtime` are keywords +// elsewhere in the grammar. +QualifiedName returns string: + NameSegment ('.' NameSegment)*; + +NameSegment returns string: + ID | 'workspace' | 'process' | 'network' | 'model' | 'read' | 'write' + | 'allow' | 'deny' | 'env' | 'url' | 'command' | 'source' | 'description' + | 'transport' | 'adapter' | 'execution' | 'program' | 'runtime' | 'target' + | 'agent' | 'skill' | 'tool' | 'mcp' | 'workflow' | 'harness' | 'input' + | 'output' | 'mechanism' | 'strength' | 'notes' | 'configure' | 'stdio' + | 'http' | 'sse'; ConfigValue: IntLiteral | DurationLiteral | StringLiteral | BooleanLiteral; @@ -118,7 +199,6 @@ BooleanLiteral: terminal DURATION: /[0-9]+(ms|s|m|h)/; terminal INT returns number: /[0-9]+/; -terminal VERSION_RANGE: /@[~^]?[0-9][0-9a-zA-Z.+*-]*/; terminal ID: /[_a-zA-Z][\w-]*/; terminal STRING returns string: /"[^"]*"/; diff --git a/packages/harness/src/resolver/resolve.ts b/packages/harness/src/resolver/resolve.ts index bdf2bae..d6d86f5 100644 --- a/packages/harness/src/resolver/resolve.ts +++ b/packages/harness/src/resolver/resolve.ts @@ -1,17 +1,18 @@ -import semver from "semver"; import { contentHash, sha256Hex, canonicalJson } from "../ir/canonical.js"; import { STRENGTH_ORDER, strengthIndex } from "../language/harness-validator.js"; import { + type AgentIr, + type CapabilityBindingIr, + type CapabilityIr, type CapabilityRequirementIr, - type ComponentContractIr, type HarnessIrBundle, type HarnessRevision, IR_VERSION, type PermissionGrant, - type PluginManifestIr, type Realization, type ResolutionReport, - type TargetBindingIr, + type RuntimeIr, + findCapability, } from "../ir/index.js"; export interface ResolveResult { @@ -22,180 +23,127 @@ export interface ResolveResult { } /** - * Resolve a composition against the plugins declared in the bundle. + * Resolve a harness against a target runtime. * - * Degradation semantics: - * A binding declares the strongest mechanism a host can eventually provide. - * The v0.1 executors materialize every supported binding as prompt guidance, - * so effective strength is capped at `advisory` until a native artifact and - * evidence-receipt contract lands. + * Runtime selection: an explicit `runtimeId` wins; otherwise a single + * `target` statement in the bundle, or a single declared `runtime`, selects + * itself. A target without a runtime block synthesizes a tool-calling + * runtime whose adapter is `@harness/adapter-` — adapters are compile + * backends that harness authors should not have to spell out. + * + * Degradation semantics (unchanged from v0.1): + * A binding declares the strongest mechanism an adapter can eventually + * provide on a runtime. The v0.2 executors materialize every supported + * binding as prompt guidance, so effective strength is capped at `advisory` + * until a native artifact and evidence-receipt contract lands. * * - realized >= preferred (or no preferred): satisfied * - minimum <= realized < preferred: degraded; `on-degrade fail` turns the * degradation into a resolution failure instead of a silent prompt * - realized < minimum: always a resolution failure + * + * Deployability: a programmatic workflow only resolves against a runtime + * whose execution is `programmatic` in the same language. Declarative + * workflows deploy to any runtime. */ -export function resolveComposition(bundle: HarnessIrBundle, compositionId: string): ResolveResult { - const composition = bundle.compositions.find((candidate) => candidate.id === compositionId); - if (!composition) { +export function resolveHarness( + bundle: HarnessIrBundle, + harnessId: string, + runtimeId?: string, +): ResolveResult { + const harness = bundle.harnesses.find((candidate) => candidate.id === harnessId); + if (!harness) { return { - report: failedReport(compositionId, "unknown", [], [ - `Composition '${compositionId}' is not defined in the bundle.`, + report: failedReport(harnessId, runtimeId ?? "unknown", [], [ + `Harness '${harnessId}' is not defined in the bundle.`, ]), }; } const errors: string[] = []; - const resolvedPlugins: PluginManifestIr[] = []; - for (const include of composition.includes) { - const candidates = bundle.plugins.filter((plugin) => plugin.id === include.pluginId); - if (candidates.length === 0) { - errors.push(`Plugin '${include.pluginId}' is not available in the registry.`); - continue; - } - const matching = candidates - .filter((plugin) => satisfiesRange(plugin.version, include.range)) - .sort((a, b) => semver.rcompare(normalizeVersion(a.version), normalizeVersion(b.version))); - if (matching.length === 0) { - errors.push( - `Plugin '${include.pluginId}' has no version satisfying '${include.range}' ` + - `(available: ${candidates.map((plugin) => plugin.version).join(", ")}).`, - ); - continue; - } - resolvedPlugins.push(matching[0]); + const runtime = selectRuntime(bundle, runtimeId, errors); + if (!runtime) { + return { report: failedReport(harness.id, runtimeId ?? "unknown", [], errors) }; } - // Components reach a composition two ways: bundled inside an included plugin, - // or `require`d directly. A directly-required component needs no plugin - // wrapper and, absent an explicit binding, materializes as advisory prompt - // guidance — the v0.1 floor — so a bare `require` just works. - const pluginComponentIds = new Set(resolvedPlugins.flatMap((plugin) => plugin.provides)); - const bundleComponentIds = new Set(bundle.components.map((component) => component.id)); - const directComponentIds = new Set( - composition.requirements - .map((requirement) => requirement.componentId) - .filter((id) => !pluginComponentIds.has(id) && bundleComponentIds.has(id)), - ); - const enabledComponentIds = new Set([...pluginComponentIds, ...directComponentIds]); - const enabledComponents = bundle.components.filter((component) => - enabledComponentIds.has(component.id), - ); + const workflow = bundle.workflows.find((candidate) => candidate.id === harness.workflow); + if (!workflow) { + return { + report: failedReport(harness.id, runtime.id, [], [ + `Workflow '${harness.workflow}' is not defined in the bundle.`, + ]), + }; + } - const realizations: Realization[] = []; - for (const requirement of composition.requirements) { - if (!enabledComponentIds.has(requirement.componentId)) { + if (workflow.mode === "programmatic") { + const language = workflow.program?.language ?? "unknown"; + if (runtime.execution.style !== "programmatic") { errors.push( - `Required component '${requirement.componentId}' is not provided by any included plugin.`, - ); - realizations.push( - realize( - requirement, - undefined, - materializeV01(undefined), - "failed", - "component not provided", - ), + `Workflow '${workflow.id}' is programmatic (${language}), but runtime '${runtime.id}' ` + + `exposes tool-calling execution. Declare a runtime with 'execution ` + + `programmatic.${language}', or drive the workflow externally over ACP.`, ); - continue; - } - const binding = - bundle.bindings.find( - (candidate) => - candidate.componentId === requirement.componentId && candidate.host === composition.target, - ) ?? defaultBindingFor(requirement.componentId, composition.target, directComponentIds); - const materialization = materializeV01(binding); - const realized = materialization.strength; - const minimumIndex = strengthIndex(requirement.minimum); - const realizedIndex = strengthIndex(realized); - const preferredIndex = - requirement.preferred !== undefined ? strengthIndex(requirement.preferred) : minimumIndex; - - if (realizedIndex < minimumIndex) { + } else if (runtime.execution.language !== language) { errors.push( - `Component '${requirement.componentId}' realizes '${realized}' on host ` + - `'${composition.target}', below the required minimum '${requirement.minimum}'.`, + `Workflow '${workflow.id}' is programmatic (${language}), but runtime '${runtime.id}' ` + + `executes programmatic.${runtime.execution.language}. Author the workflow in the ` + + `runtime's native language or restrict deployment targets.`, ); - realizations.push( - realize(requirement, binding, materialization, "failed", degradeReason(binding, composition.target)), - ); - continue; } - if (realizedIndex < preferredIndex) { - const reason = degradeReason(binding, composition.target); - if (requirement.onDegrade === "fail") { + } + + const realizations: Realization[] = []; + const enabledCapabilityIds = new Set(); + for (const agent of harness.agents) { + for (const requirement of agent.requirements) { + const capability = findCapability(bundle, requirement.capabilityId); + if (!capability) { errors.push( - `Component '${requirement.componentId}' realizes '${realized}' on host ` + - `'${composition.target}', below the preferred '${requirement.preferred}' ` + - `and the requirement declares 'on-degrade fail'.`, + `Agent '${agent.id}' requires unknown capability '${requirement.capabilityId}'.`, ); - realizations.push(realize(requirement, binding, materialization, "failed", reason)); - } else { - realizations.push(realize(requirement, binding, materialization, "degraded", reason)); + continue; } - continue; - } - realizations.push(realize(requirement, binding, materialization, "satisfied")); - } - - // Components enabled by resolved plugins without an explicit requirement - // still belong to the realized surface of the run: record an implicit - // realization so executors and checkpoints see the full assembly. - const required = new Set(composition.requirements.map((requirement) => requirement.componentId)); - for (const component of enabledComponents) { - if (required.has(component.id)) { - continue; + enabledCapabilityIds.add(capability.id); + realizations.push( + ...realizeRequirement(bundle, agent, requirement, runtime.id, errors), + ); } - const binding = bundle.bindings.find( - (candidate) => candidate.componentId === component.id && candidate.host === composition.target, - ); - const materialization = materializeV01(binding); - const isDegraded = - !binding || - materialization.strength === "unsupported" || - strengthIndex(binding.strength) > strengthIndex(materialization.strength); - realizations.push({ - componentId: component.id, - requestedMinimum: "unsupported", - declaredStrength: binding?.strength ?? "unsupported", - declaredMechanism: binding?.mechanism ?? null, - realized: materialization.strength, - materializedMechanism: materialization.mechanism, - action: isDegraded ? "degraded" : "satisfied", - reason: - !binding - ? `implicitly enabled; host '${composition.target}' has no binding for this component` - : binding.strength === "unsupported" - ? `implicitly enabled; host '${composition.target}' declares this component unsupported ` + - `via '${binding.mechanism}'` - : isDegraded - ? `implicitly enabled; declared '${binding.strength}' via '${binding.mechanism}', ` + - `materialized as '${materialization.strength}' via '${materialization.mechanism}'` - : "implicitly enabled by an included plugin", - }); } - realizations.sort((a, b) => compareByKey(a.componentId, b.componentId)); + realizations.sort( + (a, b) => compareByKey(a.agentId, b.agentId) || compareByKey(a.capabilityId, b.capabilityId), + ); if (errors.length > 0) { - return { report: failedReport(composition.id, composition.target, realizations, errors) }; + return { report: failedReport(harness.id, runtime.id, realizations, errors) }; } + const enabledCapabilities = [...enabledCapabilityIds] + .sort(compareByKey) + .map((id) => findCapability(bundle, id)) + .filter((capability): capability is CapabilityIr => capability !== undefined); + const revisionBody: Omit = { irVersion: IR_VERSION, kind: "harness-revision" as const, - composition: { id: composition.id, contentHash: contentHash(composition) }, - target: { host: composition.target }, + harness: { id: harness.id, contentHash: contentHash(harness) }, + target: { runtime: runtime.id, adapter: runtime.adapter, execution: runtime.execution }, + workflow: { id: workflow.id, mode: workflow.mode, contentHash: contentHash(workflow) }, resolved: { - plugins: resolvedPlugins - .map((plugin) => ({ id: plugin.id, version: plugin.version, contentHash: contentHash(plugin) })) - .sort((a, b) => compareByKey(a.id, b.id)), - components: enabledComponents - .map((component) => ({ id: component.id, contentHash: contentHash(component) })) - .sort((a, b) => compareByKey(a.id, b.id)), + capabilities: enabledCapabilities.map((capability) => ({ + id: capability.id, + kind: capability.kind, + contentHash: contentHash(capability), + })), }, + agents: harness.agents.map((agent) => ({ + id: agent.id, + capabilities: agent.requirements + .map((requirement) => requirement.capabilityId) + .sort(compareByKey), + })), realization: realizations, - permissions: mergePermissions(enabledComponents), - settings: composition.settings, + permissions: mergePermissions(enabledCapabilities), + settings: harness.settings, }; const revision: HarnessRevision = { ...revisionBody, @@ -207,8 +155,8 @@ export function resolveComposition(bundle: HarnessIrBundle, compositionId: strin report: { irVersion: IR_VERSION, kind: "resolution-report", - compositionId: composition.id, - target: composition.target, + harnessId: harness.id, + runtime: runtime.id, status: "resolved", realizations, errors: [], @@ -216,18 +164,112 @@ export function resolveComposition(bundle: HarnessIrBundle, compositionId: strin }; } +function selectRuntime( + bundle: HarnessIrBundle, + runtimeId: string | undefined, + errors: string[], +): RuntimeIr | undefined { + const id = + runtimeId ?? + (bundle.targets.length === 1 + ? bundle.targets[0].runtime + : bundle.targets.length === 0 && bundle.runtimes.length === 1 + ? bundle.runtimes[0].id + : undefined); + if (id === undefined) { + errors.push( + bundle.targets.length === 0 && bundle.runtimes.length === 0 + ? "No runtime is available: declare a 'target' or 'runtime', or pass a runtime id." + : "Multiple runtimes are available; pass the runtime id to resolve against.", + ); + return undefined; + } + const declared = bundle.runtimes.find((runtime) => runtime.id === id); + if (declared) { + return declared; + } + const target = bundle.targets.find((candidate) => candidate.runtime === id); + if (!target && runtimeId !== undefined) { + errors.push(`Runtime '${id}' is neither declared as a runtime nor listed as a target.`); + return undefined; + } + // Deployment shorthand: `target qoder uses adapter.qoder` without a runtime + // block means a tool-calling runtime with a conventional adapter package. + return { + irVersion: IR_VERSION, + kind: "runtime", + id, + adapter: `@harness/adapter-${target?.adapter ?? id}`, + execution: { style: "tool-calling" }, + }; +} + +function realizeRequirement( + bundle: HarnessIrBundle, + agent: AgentIr, + requirement: CapabilityRequirementIr, + runtimeId: string, + errors: string[], +): Realization[] { + const binding = + bundle.bindings.find( + (candidate) => + candidate.capabilityId === requirement.capabilityId && candidate.runtime === runtimeId, + ) ?? defaultBindingFor(requirement.capabilityId, runtimeId); + const materialization = materializeV02(binding); + const realized = materialization.strength; + const minimumIndex = strengthIndex(requirement.minimum); + const realizedIndex = strengthIndex(realized); + const preferredIndex = + requirement.preferred !== undefined ? strengthIndex(requirement.preferred) : minimumIndex; + + if (realizedIndex < minimumIndex) { + errors.push( + `Capability '${requirement.capabilityId}' for agent '${agent.id}' realizes '${realized}' ` + + `on runtime '${runtimeId}', below the required minimum '${requirement.minimum}'.`, + ); + return [realize(agent, requirement, binding, materialization, "failed", degradeReason(binding, runtimeId))]; + } + if (realizedIndex < preferredIndex) { + const reason = degradeReason(binding, runtimeId); + if (requirement.onDegrade === "fail") { + errors.push( + `Capability '${requirement.capabilityId}' for agent '${agent.id}' realizes '${realized}' ` + + `on runtime '${runtimeId}', below the preferred '${requirement.preferred}' ` + + `and the requirement declares 'on-degrade fail'.`, + ); + return [realize(agent, requirement, binding, materialization, "failed", reason)]; + } + return [realize(agent, requirement, binding, materialization, "degraded", reason)]; + } + return [realize(agent, requirement, binding, materialization, "satisfied")]; +} + /** - * Merge component permission grants into the revision permission surface. + * Merge capability permission grants into the revision permission surface. * The merge is monotonically tightening: an explicit `deny` on a domain - * always wins over any grant, regardless of declaration order. + * always wins over any grant, regardless of declaration order. MCP entries + * carry no permission block; their transport implies the connection grant + * (`stdio` spawns a process, `http`/`sse` reach the network). */ -export function mergePermissions(components: ComponentContractIr[]): PermissionGrant[] { +export function mergePermissions(capabilities: CapabilityIr[]): PermissionGrant[] { const byDomain = new Map>(); - for (const component of components) { - for (const grant of component.permissions) { - const accesses = byDomain.get(grant.domain) ?? new Set(); - accesses.add(grant.access); - byDomain.set(grant.domain, accesses); + const add = (grant: PermissionGrant): void => { + const accesses = byDomain.get(grant.domain) ?? new Set(); + accesses.add(grant.access); + byDomain.set(grant.domain, accesses); + }; + for (const capability of capabilities) { + if (capability.kind === "mcp") { + add( + capability.transport === "stdio" + ? { domain: "process", access: "allow" } + : { domain: "network", access: "allow" }, + ); + continue; + } + for (const grant of capability.permissions) { + add(grant); } } const merged: PermissionGrant[] = []; @@ -246,18 +288,21 @@ export function mergePermissions(components: ComponentContractIr[]): PermissionG } function realize( + agent: AgentIr, requirement: CapabilityRequirementIr, - binding: TargetBindingIr | undefined, - materialization: V01Materialization, + binding: CapabilityBindingIr, + materialization: V02Materialization, action: Realization["action"], reason?: string, ): Realization { return { - componentId: requirement.componentId, + agentId: agent.id, + capabilityId: requirement.capabilityId, + capabilityKind: requirement.capabilityKind, requestedMinimum: requirement.minimum, ...(requirement.preferred !== undefined ? { requestedPreferred: requirement.preferred } : {}), - declaredStrength: binding?.strength ?? "unsupported", - declaredMechanism: binding?.mechanism ?? null, + declaredStrength: binding.strength, + declaredMechanism: binding.mechanism, realized: materialization.strength, materializedMechanism: materialization.mechanism, action, @@ -265,79 +310,59 @@ function realize( }; } -function degradeReason(binding: TargetBindingIr | undefined, host: string): string { - if (!binding) { - return `host '${host}' has no binding for this component`; - } +function degradeReason(binding: CapabilityBindingIr, runtime: string): string { const detail = - `host '${host}' declares '${binding.strength}' via '${binding.mechanism}', ` + - "but the v0.1 executor materializes prompt guidance at 'advisory' strength"; + `runtime '${runtime}' declares '${binding.strength}' via '${binding.mechanism}', ` + + "but the v0.2 executor materializes prompt guidance at 'advisory' strength"; return binding.notes ? `${detail}: ${binding.notes}` : detail; } /** - * A directly-`require`d component without an explicit binding is treated as an - * advisory prompt-guidance binding: the same floor a written binding would - * declare, so authoring the binding is optional rather than required. Plugin - * components keep their explicit-binding-or-`unsupported` semantics. + * A capability without an explicit binding is treated as an advisory + * prompt-guidance binding: the v0.2 floor. Adapters raise the ceiling by + * declaring host-native mechanisms (`qoder.plugin`, `pi.extension`) with + * stronger declared strengths. */ -function defaultBindingFor( - componentId: string, - host: string, - directComponentIds: Set, -): TargetBindingIr | undefined { - if (!directComponentIds.has(componentId)) { - return undefined; - } +function defaultBindingFor(capabilityId: string, runtime: string): CapabilityBindingIr { return { irVersion: IR_VERSION, - kind: "target-binding", - componentId, - host, + kind: "capability-binding", + capabilityId, + runtime, mechanism: "prompt-preamble", strength: "advisory", }; } -interface V01Materialization { +interface V02Materialization { strength: Realization["realized"]; mechanism: string | null; } -function materializeV01(binding: TargetBindingIr | undefined): V01Materialization { - if (!binding || binding.strength === "unsupported") { +function materializeV02(binding: CapabilityBindingIr): V02Materialization { + if (binding.strength === "unsupported") { return { strength: "unsupported", mechanism: null }; } return { strength: "advisory", mechanism: "prompt-preamble" }; } function failedReport( - compositionId: string, - target: string, + harnessId: string, + runtime: string, realizations: Realization[], errors: string[], ): ResolutionReport { return { irVersion: IR_VERSION, kind: "resolution-report", - compositionId, - target, + harnessId, + runtime, status: "failed", realizations, errors, }; } -function satisfiesRange(version: string, range: string): boolean { - const normalized = normalizeVersion(version); - const validRange = semver.validRange(range); - return validRange !== null && semver.satisfies(normalized, validRange); -} - -function normalizeVersion(version: string): string { - return semver.coerce(version)?.version ?? version; -} - function compareByKey(a: string, b: string): number { return a < b ? -1 : a > b ? 1 : 0; } diff --git a/packages/harness/test/compare.test.ts b/packages/harness/test/compare.test.ts index 2b1645a..233a826 100644 --- a/packages/harness/test/compare.test.ts +++ b/packages/harness/test/compare.test.ts @@ -49,7 +49,7 @@ describe("harness compare manifest", () => { await expect(loadHarnessCompareManifest(path)).rejects.toThrow(/allowedTools must be empty/); }); - it("accepts an isolated same-composition comparison when runtime profiles differ", async () => { + it("accepts an isolated same-harness comparison when runtime profiles differ", async () => { const loaded = await loadHarnessCompareManifest(MINIMAL_EXPERIMENT_PATH); expect(loaded.value.variants).toEqual({ @@ -181,7 +181,7 @@ describe("README coding comparison", () => { executorFactory: ({ trialRoot }): HarnessExecutor => ({ host: "qoder", execute: async (revision) => { - if (revision.composition.id === "readme-grounded") { + if (revision.harness.id === "readme-grounded") { await writeFile(join(trialRoot, "README.md"), VALID_README, "utf8"); } return { diff --git a/packages/harness/test/compile.test.ts b/packages/harness/test/compile.test.ts index 24fe392..568a01d 100644 --- a/packages/harness/test/compile.test.ts +++ b/packages/harness/test/compile.test.ts @@ -3,49 +3,76 @@ import { Value } from "@sinclair/typebox/value"; import { describe, expect, it } from "vitest"; import { compileHarness } from "../src/compiler/compile.js"; import { HarnessIrBundleSchema, IR_VERSION } from "../src/ir/index.js"; -import { resolveComposition } from "../src/resolver/resolve.js"; +import { resolveHarness } from "../src/resolver/resolve.js"; const EXAMPLE_URL = new URL("../examples/standard-coding.harness", import.meta.url); const FULL_SURFACE_URL = new URL("../examples/full-surface.harness", import.meta.url); describe("compileHarness", () => { - it("compiles every authored v0.1 syntax branch into typed IR", async () => { + it("compiles every authored v0.2 syntax branch into typed IR", async () => { const source = await readFile(FULL_SURFACE_URL, "utf8"); const result = await compileHarness(source); expect(result.diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]); expect(Value.Check(HarnessIrBundleSchema, result.bundle)).toBe(true); - expect(result.bundle!.components.map((component) => component.componentKind)).toEqual([ - "skill", "tool", "program", "workflow", "hook", "policy", "observer", "ui", + const bundle = result.bundle!; + expect(bundle.skills.map((skill) => skill.id)).toEqual([ + "repository-grounding", + "inline-guidance", + ]); + expect(bundle.skills[0].source).toBe("./skills/repository-grounding"); + expect(bundle.tools.map((tool) => `${tool.id}:${tool.implicit}`)).toEqual([ + "workspace.read:false", + "process.exec:false", + "workspace.write:true", + ]); + expect(bundle.mcps.map((mcp) => mcp.transport)).toEqual(["http", "sse", "stdio"]); + expect(bundle.mcps[0].url).toEqual({ type: "env", variable: "PACKAGE_REGISTRY_MCP" }); + expect(bundle.mcps[1].url).toEqual({ type: "literal", value: "https://docs.example.test/mcp" }); + expect(bundle.mcps[2].command).toBe("indexer --stdio"); + expect(bundle.workflows.map((workflow) => `${workflow.id}:${workflow.mode}`)).toEqual([ + "coding-loop:declarative", + "scripted-loop:programmatic", + ]); + expect(bundle.workflows[0]).toMatchObject({ + edges: [{ from: "author", to: "verifier" }], + events: [{ agent: "verifier", outcome: "failed", to: "author" }], + stops: [{ agent: "verifier", outcome: "passed" }], + }); + expect(bundle.workflows[1].program).toEqual({ language: "deno", entry: "./flows/coding-loop.ts" }); + expect(bundle.runtimes.map((runtime) => runtime.execution)).toEqual([ + { style: "programmatic", language: "python", options: [{ key: "repl", value: "persistent" }] }, + { style: "programmatic", language: "typescript", options: [{ key: "mode", value: "code" }] }, + { style: "tool-calling" }, ]); - expect(new Set(result.bundle!.bindings.map((binding) => binding.strength))).toEqual( + expect(bundle.targets).toEqual([{ runtime: "qoder", adapter: "qoder" }, { runtime: "pi" }]); + expect(new Set(bundle.bindings.map((binding) => binding.strength))).toEqual( new Set(["unsupported", "advisory", "wired", "enforced"]), ); - expect(result.bundle!.components.flatMap((component) => component.permissions)).toEqual( - expect.arrayContaining([ - { domain: "workspace", access: "read" }, - { domain: "workspace", access: "write" }, - { domain: "process", access: "allow" }, - { domain: "process", access: "deny" }, - { domain: "network", access: "allow" }, - { domain: "network", access: "deny" }, - { domain: "model", access: "allow" }, - { domain: "model", access: "deny" }, - ]), + expect(bundle.bindings.map((binding) => binding.mechanism)).toEqual( + expect.arrayContaining(["qoder.plugin", "deepseek.plugin", "pi.extension"]), ); - expect(result.bundle!.compositions[0].settings).toEqual([ + expect(bundle.harnesses[0].settings).toEqual([ { key: "runtime.max-turns", value: { type: "int", value: 24 } }, { key: "runtime.timeout", value: { type: "duration", value: "10m", ms: 600_000 } }, { key: "runtime.label", value: { type: "string", value: "full-surface" } }, { key: "runtime.checkpoints", value: { type: "boolean", value: true } }, { key: "runtime.network-enabled", value: { type: "boolean", value: false } }, ]); - const resolved = resolveComposition(result.bundle!, "full-surface-qoder"); + const resolved = resolveHarness(result.bundle!, "full-surface", "pi"); expect(resolved.report.status).toBe("resolved"); expect(resolved.report.realizations).toEqual( expect.arrayContaining([ - expect.objectContaining({ componentId: "repository-skill", action: "satisfied" }), - expect.objectContaining({ componentId: "test-program", action: "degraded" }), + expect.objectContaining({ + agentId: "author", + capabilityId: "repository-grounding", + action: "satisfied", + }), + expect.objectContaining({ + agentId: "verifier", + capabilityId: "process.exec", + action: "degraded", + }), ]), ); }); @@ -59,103 +86,166 @@ describe("compileHarness", () => { const bundle = result.bundle!; expect(Value.Check(HarnessIrBundleSchema, bundle)).toBe(true); expect(bundle.irVersion).toBe(IR_VERSION); - expect(bundle.components.map((component) => component.id)).toEqual([ + expect(bundle.skills.map((skill) => skill.id)).toEqual([ "impact-analysis", "verification-before-complete", ]); - expect(bundle.compositions.map((composition) => composition.id)).toEqual([ - "standard-coding", - "standard-coding-qoder", + expect(bundle.harnesses.map((harness) => harness.id)).toEqual(["standard-coding"]); + expect(bundle.harnesses[0].agents.map((agent) => agent.id)).toEqual(["author", "verifier"]); + expect(bundle.harnesses[0].agents[0].requirements).toEqual([ + { capabilityId: "impact-analysis", capabilityKind: "skill", minimum: "advisory", onDegrade: "report" }, + { capabilityId: "workspace.read", capabilityKind: "tool", minimum: "advisory", onDegrade: "report" }, + { capabilityId: "workspace.write", capabilityKind: "tool", minimum: "advisory", onDegrade: "report" }, ]); - }); - - it("joins declared bindings into the plugin manifest of provided components", async () => { - const source = await readFile(EXAMPLE_URL, "utf8"); - const { bundle } = await compileHarness(source); - - const plugin = bundle!.plugins.find((candidate) => candidate.id === "change-verification")!; - expect(plugin.provides).toEqual(["verification-before-complete"]); - expect(plugin.bindings).toEqual([ - expect.objectContaining({ host: "pi", mechanism: "extension-event", strength: "enforced" }), - expect.objectContaining({ host: "qoder", mechanism: "system-instruction", strength: "advisory" }), + expect(bundle.harnesses[0].settings).toEqual([ + { key: "shell.timeout", value: { type: "duration", value: "60s", ms: 60_000 } }, + { key: "tool-call-budget", value: { type: "int", value: 80 } }, + ]); + expect(bundle.targets).toEqual([ + { runtime: "pi", adapter: "pi" }, + { runtime: "qoder", adapter: "qoder" }, ]); }); - it("normalizes version ranges and duration settings", async () => { - const source = await readFile(EXAMPLE_URL, "utf8"); - const { bundle } = await compileHarness(source); + it("keeps generic plugin and composition out of the authored surface", async () => { + const result = await compileHarness(` + plugin generic { version "1.0.0" } + `); + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true); - const composition = bundle!.compositions.find((candidate) => candidate.id === "standard-coding")!; - expect(composition.includes).toEqual([ - { pluginId: "repository-understanding", range: "^1" }, - { pluginId: "change-verification", range: "^2" }, - ]); - expect(composition.settings).toEqual([ - { key: "shell.timeout", value: { type: "duration", value: "60s", ms: 60_000 } }, - { key: "tool-call-budget", value: { type: "int", value: 80 } }, - ]); + const composition = await compileHarness(` + composition run { target qoder } + `); + expect(composition.bundle).toBeUndefined(); + expect(composition.diagnostics.some((d) => d.severity === "error")).toBe(true); }); it("resolves cross-file references across multiple sources", async () => { - const contracts = ` - component impact-analysis { - kind skill + const capabilities = ` + skill impact-analysis { + description "Map the blast radius." + } + `; + const flow = ` + workflow solo { + stop when coder.done } `; const assembly = ` + harness review { + workflow solo + agent coder { + use skill impact-analysis + } + } binding impact-analysis for qoder { - mechanism skill-routing + mechanism qoder.skill-routing strength advisory } - plugin repo-tools { - version "1.0.0" - provides [ component.impact-analysis ] - } + target qoder `; - const result = await compileHarness([{ text: contracts }, { text: assembly }]); + const result = await compileHarness([{ text: capabilities }, { text: flow }, { text: assembly }]); expect(result.diagnostics.filter((d) => d.severity === "error")).toEqual([]); - expect(result.bundle!.plugins[0].bindings).toEqual([ - expect.objectContaining({ componentId: "impact-analysis", host: "qoder" }), + expect(result.bundle!.bindings).toEqual([ + expect.objectContaining({ + capabilityId: "impact-analysis", + runtime: "qoder", + mechanism: "qoder.skill-routing", + }), ]); + expect(result.bundle!.harnesses[0].workflow).toBe("solo"); }); it("fails compilation when preferred strength is below minimum", async () => { - const source = ` - component gate { kind policy } - binding gate for pi { mechanism completion-hook strength enforced } - plugin p { version "1.0.0" provides [ component.gate ] } - composition c { - target pi - include [ plugin.p@1 ] - require gate { - preferred advisory - minimum enforced + const result = await compileHarness(` + skill gate { description "Verify first." } + workflow solo { stop when coder.done } + harness run { + workflow solo + agent coder { + use skill gate { + preferred advisory + minimum enforced + } } } - `; - const result = await compileHarness(source); + `); expect(result.bundle).toBeUndefined(); - expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true); + expect(result.diagnostics.some((d) => d.message.includes("below minimum"))).toBe(true); + }); + + it("fails compilation when a verb does not match the declared capability kind", async () => { + const result = await compileHarness(` + skill gate { description "Verify first." } + tool workspace.read {} + mcp registry { transport http url "https://registry.test/mcp" } + workflow solo { stop when coder.done } + harness run { + workflow solo + agent coder { + require tool gate + use skill workspace.read + connect mcp gate + use skill registry + } + } + `); + + expect(result.bundle).toBeUndefined(); + const messages = result.diagnostics.map((diagnostic) => diagnostic.message); + expect(messages).toEqual( + expect.arrayContaining([ + expect.stringContaining("'require' expects a tool, but 'gate' is declared as a skill"), + expect.stringContaining("'use' expects a skill, but 'workspace.read' is declared as a tool"), + expect.stringContaining("'connect' expects a mcp, but 'gate' is declared as a skill"), + expect.stringContaining("'use' expects a skill, but 'registry' is declared as a mcp"), + ]), + ); + }); + + it("fails compilation when skills or MCP entries are referenced but never declared", async () => { + const result = await compileHarness(` + workflow solo { stop when coder.done } + harness run { + workflow solo + agent coder { + use skill missing-skill + connect mcp missing-mcp + } + } + `); + + expect(result.bundle).toBeUndefined(); + const messages = result.diagnostics.map((diagnostic) => diagnostic.message); + expect(messages).toEqual( + expect.arrayContaining([ + expect.stringContaining("unknown skill 'missing-skill'"), + expect.stringContaining("unknown mcp 'missing-mcp'"), + ]), + ); }); - it("fails compilation on unresolved component references", async () => { + it("fails compilation when a binding references an unknown capability", async () => { const result = await compileHarness(` binding does-not-exist for pi { - mechanism completion-hook + mechanism pi.extension strength advisory } `); expect(result.bundle).toBeUndefined(); - expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true); + expect(result.diagnostics.some((d) => d.message.includes("unknown capability 'does-not-exist'"))).toBe( + true, + ); }); - it("fails compilation on duplicate bindings for the same component and host", async () => { + it("fails compilation on duplicate bindings for the same capability and runtime", async () => { const result = await compileHarness(` - component gate { kind policy } - binding gate for pi { mechanism completion-hook strength advisory } + skill gate { description "Verify first." } + binding gate for pi { mechanism pi.extension strength advisory } binding gate for pi { mechanism other strength enforced } `); @@ -168,13 +258,13 @@ describe("compileHarness", () => { { uri: "memory://harness/first.harness", text: ` - component gate { kind policy } - binding gate for pi { mechanism prompt strength advisory } + skill gate { description "Verify first." } + binding gate for pi { mechanism prompt-preamble strength advisory } `, }, { uri: "memory://harness/second.harness", - text: "binding gate for pi { mechanism extension-event strength enforced }", + text: "binding gate for pi { mechanism pi.extension strength enforced }", }, ]); @@ -183,61 +273,141 @@ describe("compileHarness", () => { expect.objectContaining({ severity: "error", source: "memory://harness/second.harness", - message: expect.stringContaining("Duplicate component/host binding 'gate::pi'"), + message: expect.stringContaining("Duplicate capability/runtime binding 'gate::pi'"), }), ); }); - it("rejects duplicate globally named declarations and repeated composition members", async () => { + it("rejects duplicate declarations and repeated harness members", async () => { const result = await compileHarness(` - component gate { kind policy } - component gate { kind skill } - plugin p { version "1.0.0" provides [ component.gate, component.gate ] } - composition run { - target pi - include [ plugin.p@1, plugin.p@^1 ] - require gate { minimum advisory } - require gate { minimum advisory } + skill gate { description "Verify first." } + tool gate {} + workflow solo { stop when coder.done } + workflow solo { stop when coder.done } + runtime pi { adapter "@harness/adapter-pi" } + runtime pi { adapter "@harness/adapter-pi" } + harness run { + workflow solo + agent coder { + use skill gate + use skill gate + } + agent coder { + } configure { shell.timeout = 1s shell.timeout = 2s } } - composition run { target pi include [ plugin.p@1 ] } + harness run { workflow solo agent coder { } } + target pi + target pi `); expect(result.bundle).toBeUndefined(); const messages = result.diagnostics.map((diagnostic) => diagnostic.message); expect(messages).toEqual( expect.arrayContaining([ - expect.stringContaining("Duplicate component 'gate'"), - expect.stringContaining("Duplicate provided component 'gate'"), - expect.stringContaining("Duplicate included plugin 'p'"), - expect.stringContaining("Duplicate composition requirement 'gate'"), + expect.stringContaining("Duplicate capability 'gate'"), + expect.stringContaining("Duplicate workflow 'solo'"), + expect.stringContaining("Duplicate runtime 'pi'"), + expect.stringContaining("Duplicate agent 'coder'"), + expect.stringContaining("Duplicate agent 'coder' requirement 'gate'"), expect.stringContaining("Duplicate configuration key 'shell.timeout'"), - expect.stringContaining("Duplicate composition 'run'"), + expect.stringContaining("Duplicate harness 'run'"), + expect.stringContaining("Duplicate target runtime 'pi'"), + ]), + ); + }); + + it("rejects a skill with neither source nor description", async () => { + const result = await compileHarness(` + skill empty {} + `); + + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics).toContainEqual( + expect.objectContaining({ + severity: "error", + message: expect.stringContaining("neither 'source' nor 'description'"), + }), + ); + }); + + it("rejects workflows that are empty or mix a program with a graph", async () => { + const result = await compileHarness(` + workflow empty {} + workflow mixed { + program deno "./flow.ts" + a -> b + } + `); + + expect(result.bundle).toBeUndefined(); + const messages = result.diagnostics.map((diagnostic) => diagnostic.message); + expect(messages).toEqual( + expect.arrayContaining([ + expect.stringContaining("Workflow 'empty' is empty"), + expect.stringContaining("Workflow 'mixed' mixes 'program' with graph statements"), + ]), + ); + }); + + it("rejects a harness whose workflow references undeclared agent roles", async () => { + const result = await compileHarness(` + workflow coding-loop { + author -> verifier + stop when verifier.passed + } + harness incomplete { + workflow coding-loop + agent author { } + } + `); + + expect(result.bundle).toBeUndefined(); + expect(result.diagnostics).toContainEqual( + expect.objectContaining({ + severity: "error", + message: expect.stringContaining( + "Workflow 'coding-loop' references agent 'verifier', which harness 'incomplete' does not declare", + ), + }), + ); + }); + + it("rejects MCP declarations whose transport and endpoint disagree", async () => { + const result = await compileHarness(` + mcp local { transport stdio } + mcp remote { transport http } + `); + + expect(result.bundle).toBeUndefined(); + const messages = result.diagnostics.map((diagnostic) => diagnostic.message); + expect(messages).toEqual( + expect.arrayContaining([ + expect.stringContaining("MCP 'local' uses 'stdio' transport but declares no 'command'"), + expect.stringContaining("MCP 'remote' uses 'http' transport but declares no 'url'"), ]), ); }); - it("rejects invalid exact plugin versions before resolution", async () => { + it("rejects a target adapter that conflicts with a declared runtime", async () => { const result = await compileHarness(` - component gate { kind policy } - plugin verification { version "not-semver" provides [ component.gate ] } - composition run { target pi include [ plugin.verification@1 ] } + runtime prime { adapter "@harness/adapter-prime" } + target prime uses adapter.other `); expect(result.bundle).toBeUndefined(); expect(result.diagnostics).toContainEqual( expect.objectContaining({ severity: "error", - message: "Plugin 'verification' declares invalid semantic version 'not-semver'.", + message: expect.stringContaining("already owns adapter '@harness/adapter-prime'"), }), ); }); - it("rejects repeated component inputs and outputs", async () => { + it("rejects repeated tool inputs and outputs", async () => { const result = await compileHarness(` - component analyzer { - kind skill - input { source source } + tool analyzer.run { + input { src src } output { result result } } `); @@ -245,8 +415,8 @@ describe("compileHarness", () => { expect(result.bundle).toBeUndefined(); expect(result.diagnostics.map((diagnostic) => diagnostic.message)).toEqual( expect.arrayContaining([ - "Duplicate component input 'source'.", - "Duplicate component output 'result'.", + "Duplicate tool input 'src'.", + "Duplicate tool output 'result'.", ]), ); }); diff --git a/packages/harness/test/exec.test.ts b/packages/harness/test/exec.test.ts index 8859f50..495c08b 100644 --- a/packages/harness/test/exec.test.ts +++ b/packages/harness/test/exec.test.ts @@ -4,68 +4,56 @@ import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { compileHarness } from "../src/compiler/compile.js"; import type { HarnessIrBundle, HarnessRevision } from "../src/ir/index.js"; -import { resolveComposition } from "../src/resolver/resolve.js"; +import { resolveHarness } from "../src/resolver/resolve.js"; import { PiSdkExecutor, materializePiPackage, type PiSdkLike } from "../src/exec/pi-sdk.js"; import { QoderSdkExecutor, type QoderSdkLike } from "../src/exec/qoder-sdk.js"; const SOURCE = ` - component impact-analysis { - kind skill + skill impact-analysis { description "Impact analysis: map the blast radius before editing." } - component verification-before-complete { - kind policy + tool verification-before-complete { description "Do not complete without verification evidence." } - binding impact-analysis for qoder { - mechanism skill-routing + workflow solo-loop { + stop when coder.done + } + harness assembly { + workflow solo-loop + agent coder { + use skill impact-analysis + require tool verification-before-complete { + preferred enforced + minimum advisory + on-degrade report + } + } + } + binding impact-analysis for [qoder, pi] { + mechanism prompt-preamble strength advisory } binding verification-before-complete for qoder { - mechanism stop-hook + mechanism qoder.stop-hook strength enforced } - binding impact-analysis for pi { - mechanism skill-package - strength advisory - } binding verification-before-complete for pi { - mechanism extension-event + mechanism pi.extension strength enforced } - plugin all { - version "1.0.0" - provides [ component.impact-analysis, component.verification-before-complete ] - } - composition on-qoder { - target qoder - include [ plugin.all@1 ] - require verification-before-complete { - preferred enforced - minimum advisory - on-degrade report - } - } - composition on-pi { - target pi - include [ plugin.all@1 ] - require verification-before-complete { - preferred enforced - minimum advisory - on-degrade report - } - } + target qoder + target pi `; -async function resolveFor(compositionId: string): Promise<{ bundle: HarnessIrBundle; revision: HarnessRevision }> { +async function resolveFor(runtimeId: string): Promise<{ bundle: HarnessIrBundle; revision: HarnessRevision }> { const { bundle } = await compileHarness(SOURCE); - const { revision } = resolveComposition(bundle!, compositionId); + const { revision } = resolveHarness(bundle!, "assembly", runtimeId); return { bundle: bundle!, revision: revision! }; } describe("QoderSdkExecutor", () => { it("streams one Qoder SDK query with explicit auth, cwd, and tool authorization", async () => { - const { bundle, revision } = await resolveFor("on-qoder"); + const { bundle, revision } = await resolveFor("qoder"); const queries: Parameters[0][] = []; const auth = { kind: "test-auth" }; const sdk: QoderSdkLike = { @@ -137,7 +125,7 @@ describe("QoderSdkExecutor", () => { }); it("retains usage evidence while redacting credential-shaped trace fields", async () => { - const { bundle, revision } = await resolveFor("on-qoder"); + const { bundle, revision } = await resolveFor("qoder"); const sdk: QoderSdkLike = { qodercliAuth: () => ({}), query: async function* () { @@ -194,7 +182,7 @@ describe("QoderSdkExecutor", () => { }); it("materializes the frozen qoder-minimal-v1 SDK surface and receipt", async () => { - const { bundle, revision } = await resolveFor("on-qoder"); + const { bundle, revision } = await resolveFor("qoder"); const queries: Parameters[0][] = []; const canUseTool = async () => ({ behavior: "allow" as const }); const sdk: QoderSdkLike = { @@ -280,7 +268,7 @@ describe("QoderSdkExecutor", () => { }); it("reports declared native strength as an advisory degradation", async () => { - const { bundle, revision } = await resolveFor("on-qoder"); + const { bundle, revision } = await resolveFor("qoder"); const sdk: QoderSdkLike = { qodercliAuth: () => ({}), query: async function* () { @@ -299,7 +287,7 @@ describe("QoderSdkExecutor", () => { }); it("rejects a revision targeting another host before loading the Qoder SDK", async () => { - const { bundle, revision } = await resolveFor("on-pi"); + const { bundle, revision } = await resolveFor("pi"); let loaded = false; const executor = new QoderSdkExecutor({ loadSdk: async () => { @@ -309,13 +297,13 @@ describe("QoderSdkExecutor", () => { }); await expect(executor.execute(revision, bundle, { prompt: "Fix the bug" })).rejects.toThrow( - /targets host 'pi'.*executor host is 'qoder'/, + /targets runtime 'pi'.*executor host is 'qoder'/, ); expect(loaded).toBe(false); }); it("reports an SDK query failure without leaking non-error messages", async () => { - const { bundle, revision } = await resolveFor("on-qoder"); + const { bundle, revision } = await resolveFor("qoder"); const sdk: QoderSdkLike = { qodercliAuth: () => ({}), query: async function* () { @@ -339,7 +327,7 @@ describe("QoderSdkExecutor", () => { }); it("fails closed when the Qoder SDK stream ends without a result message", async () => { - const { bundle, revision } = await resolveFor("on-qoder"); + const { bundle, revision } = await resolveFor("qoder"); const sdk: QoderSdkLike = { qodercliAuth: () => ({}), query: async function* () { @@ -363,7 +351,7 @@ describe("QoderSdkExecutor", () => { describe("PiSdkExecutor", () => { it("drives the Pi SDK session with the composed prompt", async () => { - const { bundle, revision } = await resolveFor("on-pi"); + const { bundle, revision } = await resolveFor("pi"); const prompts: string[] = []; const workingDirectories: Array = []; const selectedModel = { provider: "deepseek", id: "deepseek-chat" }; @@ -429,7 +417,7 @@ describe("PiSdkExecutor", () => { }); it("surfaces a model failure encoded in the final Pi assistant message", async () => { - const { bundle, revision } = await resolveFor("on-pi"); + const { bundle, revision } = await resolveFor("pi"); let listener: | ((event: { type?: string; @@ -480,7 +468,7 @@ describe("PiSdkExecutor", () => { }); it("rejects a revision targeting another host before loading the Pi SDK", async () => { - const { bundle, revision } = await resolveFor("on-qoder"); + const { bundle, revision } = await resolveFor("qoder"); let loaded = false; const executor = new PiSdkExecutor({ loadSdk: async () => { @@ -490,13 +478,13 @@ describe("PiSdkExecutor", () => { }); await expect(executor.execute(revision, bundle, { prompt: "Fix the bug" })).rejects.toThrow( - /targets host 'qoder'.*executor host is 'pi'/, + /targets runtime 'qoder'.*executor host is 'pi'/, ); expect(loaded).toBe(false); }); it("reports the missing optional peer dependency with install guidance", async () => { - const { bundle, revision } = await resolveFor("on-pi"); + const { bundle, revision } = await resolveFor("pi"); const executor = new PiSdkExecutor({ loadSdk: async () => { throw new Error("simulated missing optional peer"); @@ -537,7 +525,7 @@ describe("materializePiPackage", () => { }); it("writes an installable Pi package with skills for advisory components", async () => { - const { bundle, revision } = await resolveFor("on-pi"); + const { bundle, revision } = await resolveFor("pi"); directory = await mkdtemp(join(tmpdir(), "harness-pi-")); const written = await materializePiPackage(revision, bundle, directory); @@ -549,7 +537,7 @@ describe("materializePiPackage", () => { expect(skill).toContain("name: impact-analysis"); expect(skill).toContain('description: "Impact analysis: map the blast radius before editing."'); expect(skill).toContain(revision.revisionId); - // Policies remain prompt guidance and must not be mislabeled as Pi skills. + // Tool capabilities remain prompt guidance and must not be mislabeled as Pi skills. expect(written.some((path) => path.includes("verification-before-complete"))).toBe(false); await expect( access(join(directory, "skills", "verification-before-complete", "SKILL.md")), @@ -557,16 +545,16 @@ describe("materializePiPackage", () => { }); it("rejects materialization for a revision targeting another host", async () => { - const { bundle, revision } = await resolveFor("on-qoder"); + const { bundle, revision } = await resolveFor("qoder"); directory = await mkdtemp(join(tmpdir(), "harness-pi-")); await expect(materializePiPackage(revision, bundle, directory)).rejects.toThrow( - /targets host 'qoder'.*executor host is 'pi'/, + /targets runtime 'qoder'.*executor host is 'pi'/, ); }); it("fails closed instead of mixing a revision with pre-existing files", async () => { - const { bundle, revision } = await resolveFor("on-pi"); + const { bundle, revision } = await resolveFor("pi"); directory = await mkdtemp(join(tmpdir(), "harness-pi-")); await writeFile(join(directory, "keep.txt"), "user-owned\n", "utf8"); diff --git a/packages/harness/test/highlight.test.ts b/packages/harness/test/highlight.test.ts index a0596be..17c4a9c 100644 --- a/packages/harness/test/highlight.test.ts +++ b/packages/harness/test/highlight.test.ts @@ -2,12 +2,15 @@ import { describe, expect, it } from "vitest"; import { highlightHarness, tokenizeHarness } from "../src/highlight/shiki.js"; const SAMPLE = `// assemble -composition standard-coding { - target pi - include [ plugin.change-verification@^2 ] - require verification-before-complete { - minimum wired +harness standard-coding { + workflow coding-loop + + agent verifier { + require tool process.exec { + minimum wired + } } + configure { shell.timeout = 60s } @@ -36,7 +39,7 @@ describe("harness syntax highlighting", () => { it("tokenizes keywords, constants, and comments with distinct colors", async () => { const tokens = await tokenizeHarness(SAMPLE); - const keyword = findToken(tokens, "composition"); + const keyword = findToken(tokens, "harness"); const strength = findToken(tokens, "wired"); const name = findToken(tokens, "standard-coding"); const comment = findToken(tokens, "// assemble"); @@ -53,10 +56,18 @@ describe("harness syntax highlighting", () => { }); it("splits declaration keyword and declared name into separate tokens", async () => { - const tokens = await tokenizeHarness("composition standard-coding {\n}\n"); + const tokens = await tokenizeHarness("harness standard-coding {\n}\n"); const firstLine = tokens[0].map((token) => token.content.trim()).filter(Boolean); - expect(firstLine[0]).toBe("composition"); + expect(firstLine[0]).toBe("harness"); expect(firstLine).toContain("standard-coding"); }); + + it("tokenizes dotted tool names in declarations", async () => { + const tokens = await tokenizeHarness("tool workspace.read {\n}\n"); + const firstLine = tokens[0].map((token) => token.content.trim()).filter(Boolean); + + expect(firstLine[0]).toBe("tool"); + expect(firstLine).toContain("workspace.read"); + }); }); diff --git a/packages/harness/test/resolve.test.ts b/packages/harness/test/resolve.test.ts index 6594141..64111c8 100644 --- a/packages/harness/test/resolve.test.ts +++ b/packages/harness/test/resolve.test.ts @@ -3,7 +3,7 @@ import { Value } from "@sinclair/typebox/value"; import { describe, expect, it } from "vitest"; import { compileHarness } from "../src/compiler/compile.js"; import { HarnessRevisionSchema, ResolutionReportSchema, type HarnessIrBundle } from "../src/ir/index.js"; -import { resolveComposition } from "../src/resolver/resolve.js"; +import { resolveHarness } from "../src/resolver/resolve.js"; const EXAMPLE_URL = new URL("../examples/standard-coding.harness", import.meta.url); @@ -19,81 +19,112 @@ async function compileSource(source: string): Promise { return result.bundle!; } -describe("resolveComposition", () => { +describe("resolveHarness", () => { it("produces a schema-valid revision and reports declared versus materialized strength", async () => { const bundle = await compileExample(); - const { revision, report } = resolveComposition(bundle, "standard-coding"); + const { revision, report } = resolveHarness(bundle, "standard-coding", "pi"); expect(report.status).toBe("resolved"); expect(Value.Check(ResolutionReportSchema, report)).toBe(true); expect(revision).toBeDefined(); expect(Value.Check(HarnessRevisionSchema, revision)).toBe(true); - expect(revision!.target.host).toBe("pi"); - expect(revision!.resolved.plugins.map((plugin) => `${plugin.id}@${plugin.version}`)).toEqual([ - "change-verification@2.1.3", - "repository-understanding@1.4.2", + expect(revision!.target).toEqual({ + runtime: "pi", + adapter: "@harness/adapter-pi", + execution: { style: "tool-calling" }, + }); + expect(revision!.workflow).toMatchObject({ id: "coding-loop", mode: "declarative" }); + expect(revision!.agents).toEqual([ + { id: "author", capabilities: ["impact-analysis", "workspace.read", "workspace.write"] }, + { id: "verifier", capabilities: ["process.exec", "verification-before-complete"] }, ]); - expect(report.realizations).toEqual([ - expect.objectContaining({ - componentId: "impact-analysis", - declaredStrength: "advisory", - realized: "advisory", - materializedMechanism: "prompt-preamble", - action: "satisfied", - reason: "implicitly enabled by an included plugin", - }), - expect.objectContaining({ - componentId: "verification-before-complete", - declaredStrength: "enforced", - declaredMechanism: "extension-event", - realized: "advisory", - materializedMechanism: "prompt-preamble", - action: "degraded", - }), + expect(revision!.resolved.capabilities.map((capability) => `${capability.kind}:${capability.id}`)).toEqual([ + "skill:impact-analysis", + "tool:process.exec", + "skill:verification-before-complete", + "tool:workspace.read", + "tool:workspace.write", ]); + expect(report.realizations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + agentId: "author", + capabilityId: "impact-analysis", + declaredStrength: "advisory", + realized: "advisory", + materializedMechanism: "prompt-preamble", + action: "satisfied", + }), + expect.objectContaining({ + agentId: "verifier", + capabilityId: "verification-before-complete", + declaredStrength: "enforced", + declaredMechanism: "pi.extension", + realized: "advisory", + materializedMechanism: "prompt-preamble", + action: "degraded", + }), + ]), + ); }); it("is deterministic: the same bundle resolves to the same revision id", async () => { - const first = resolveComposition(await compileExample(), "standard-coding"); - const second = resolveComposition(await compileExample(), "standard-coding"); + const first = resolveHarness(await compileExample(), "standard-coding", "pi"); + const second = resolveHarness(await compileExample(), "standard-coding", "pi"); expect(first.revision!.revisionId).toBe(second.revision!.revisionId); expect(first.revision!.revisionId).toMatch(/^hr_[0-9a-f]{32}$/); }); + it("requires an explicit runtime when several targets are deployable", async () => { + const bundle = await compileExample(); + const { revision, report } = resolveHarness(bundle, "standard-coding"); + + expect(revision).toBeUndefined(); + expect(report.status).toBe("failed"); + expect(report.errors[0]).toContain("Multiple runtimes are available"); + }); + + it("fails for a runtime that is neither declared nor targeted", async () => { + const bundle = await compileExample(); + const { report } = resolveHarness(bundle, "standard-coding", "prime"); + + expect(report.status).toBe("failed"); + expect(report.errors[0]).toContain("'prime' is neither declared as a runtime nor listed as a target"); + }); + const degradedSource = (onDegrade: string) => ` - component gate { - kind policy + skill gate { description "Verify before completing." } binding gate for codex { mechanism system-instruction strength advisory } - plugin verification { - version "2.0.0" - provides [ component.gate ] - } - composition run-on-codex { - target codex - include [ plugin.verification@^2 ] - require gate { - preferred enforced - minimum advisory - on-degrade ${onDegrade} + workflow solo { stop when coder.done } + harness run-on-codex { + workflow solo + agent coder { + use skill gate { + preferred enforced + minimum advisory + on-degrade ${onDegrade} + } } } + target codex `; it("fails resolution when a degradation below preferred hits 'on-degrade fail'", async () => { const bundle = await compileSource(degradedSource("fail")); - const { revision, report } = resolveComposition(bundle, "run-on-codex"); + const { revision, report } = resolveHarness(bundle, "run-on-codex"); expect(revision).toBeUndefined(); expect(report.status).toBe("failed"); expect(report.errors.length).toBeGreaterThan(0); expect(report.realizations[0]).toMatchObject({ - componentId: "gate", + agentId: "coder", + capabilityId: "gate", requestedPreferred: "enforced", declaredStrength: "advisory", realized: "advisory", @@ -103,7 +134,7 @@ describe("resolveComposition", () => { it("records the degradation and still resolves under 'on-degrade report'", async () => { const bundle = await compileSource(degradedSource("report")); - const { revision, report } = resolveComposition(bundle, "run-on-codex"); + const { revision, report } = resolveHarness(bundle, "run-on-codex"); expect(report.status).toBe("resolved"); expect(revision).toBeDefined(); @@ -120,19 +151,21 @@ describe("resolveComposition", () => { it("always fails when the realized strength is below minimum", async () => { const bundle = await compileSource(` - component gate { kind policy } + skill gate { description "Verify before completing." } binding gate for codex { mechanism system-instruction strength advisory } - plugin verification { version "2.0.0" provides [ component.gate ] } - composition strict { - target codex - include [ plugin.verification@^2 ] - require gate { - minimum wired - on-degrade report + workflow solo { stop when coder.done } + harness strict { + workflow solo + agent coder { + use skill gate { + minimum wired + on-degrade report + } } } + target codex `); - const { revision, report } = resolveComposition(bundle, "strict"); + const { revision, report } = resolveHarness(bundle, "strict"); expect(revision).toBeUndefined(); expect(report.status).toBe("failed"); @@ -140,108 +173,151 @@ describe("resolveComposition", () => { it("does not treat a declared enforced binding as materialized enforcement", async () => { const bundle = await compileSource(` - component gate { kind policy } - binding gate for pi { mechanism completion-hook strength enforced } - plugin verification { version "1.0.0" provides [ component.gate ] } - composition strict { - target pi - include [ plugin.verification@1 ] - require gate { minimum enforced } + skill gate { description "Verify before completing." } + binding gate for pi { mechanism pi.extension strength enforced } + workflow solo { stop when coder.done } + harness strict { + workflow solo + agent coder { + use skill gate { minimum enforced } + } } + target pi `); - const { revision, report } = resolveComposition(bundle, "strict"); + const { revision, report } = resolveHarness(bundle, "strict"); expect(revision).toBeUndefined(); expect(report.status).toBe("failed"); expect(report.realizations[0]).toMatchObject({ declaredStrength: "enforced", - declaredMechanism: "completion-hook", + declaredMechanism: "pi.extension", realized: "advisory", materializedMechanism: "prompt-preamble", action: "failed", }); }); - it("fails when no plugin version satisfies the requested range", async () => { + it("fails a required capability whose binding declares it unsupported", async () => { const bundle = await compileSource(` - component gate { kind policy } - binding gate for pi { mechanism completion-hook strength enforced } - plugin verification { version "1.0.0" provides [ component.gate ] } - composition wants-v2 { - target pi - include [ plugin.verification@^2 ] - require gate { minimum advisory } + skill gate { description "Verify before completing." } + binding gate for qoder { mechanism unavailable strength unsupported } + workflow solo { stop when coder.done } + harness run { + workflow solo + agent coder { use skill gate } } + target qoder `); - const { revision, report } = resolveComposition(bundle, "wants-v2"); + const { revision, report } = resolveHarness(bundle, "run"); expect(revision).toBeUndefined(); expect(report.status).toBe("failed"); - expect(report.errors.some((error) => error.includes("verification"))).toBe(true); + expect(report.realizations[0]).toMatchObject({ + declaredStrength: "unsupported", + realized: "unsupported", + action: "failed", + }); }); - it("merges permissions monotonically: an explicit deny wins over allow", async () => { + it("rejects deploying a programmatic workflow onto a tool-calling runtime", async () => { const bundle = await compileSource(` - component fetcher { - kind tool - permissions { network allow } + workflow scripted { program python "./flows/loop.py" } + harness run { + workflow scripted + agent driver { require tool process.exec } } - component gate { - kind policy - permissions { network deny workspace read } + target pi + `); + const { revision, report } = resolveHarness(bundle, "run"); + + expect(revision).toBeUndefined(); + expect(report.status).toBe("failed"); + expect(report.errors[0]).toContain("programmatic (python)"); + expect(report.errors[0]).toContain("ACP"); + }); + + it("rejects a programmatic workflow whose language differs from the runtime's", async () => { + const bundle = await compileSource(` + workflow scripted { program deno "./flows/loop.ts" } + runtime prime { + adapter "@harness/adapter-prime" + execution programmatic.python { repl persistent } } - binding fetcher for pi { mechanism tool-adapter strength wired } - binding gate for pi { mechanism completion-hook strength enforced } - plugin all { version "1.0.0" provides [ component.fetcher, component.gate ] } - composition run { - target pi - include [ plugin.all@1 ] + harness run { + workflow scripted + agent driver { require tool process.exec } } + target prime `); - const { revision } = resolveComposition(bundle, "run"); + const { revision, report } = resolveHarness(bundle, "run"); - expect(revision!.permissions).toEqual([ - { domain: "network", access: "deny" }, - { domain: "workspace", access: "read" }, - ]); + expect(revision).toBeUndefined(); + expect(report.status).toBe("failed"); + expect(report.errors[0]).toContain("executes programmatic.python"); }); - it("marks an implicitly enabled component without a host binding as degraded", async () => { + it("resolves a programmatic workflow on a runtime with matching programmatic execution", async () => { const bundle = await compileSource(` - component optional-observer { kind observer } - plugin diagnostics { version "1.0.0" provides [ component.optional-observer ] } - composition run { target pi include [ plugin.diagnostics@1 ] } + workflow scripted { program python "./flows/loop.py" } + runtime prime { + adapter "@harness/adapter-prime" + execution programmatic.python { repl persistent } + } + harness run { + workflow scripted + agent driver { require tool process.exec } + } + target prime `); - - const { revision, report } = resolveComposition(bundle, "run"); + const { revision, report } = resolveHarness(bundle, "run"); expect(report.status).toBe("resolved"); - expect(revision!.realization[0]).toMatchObject({ - componentId: "optional-observer", - declaredStrength: "unsupported", - realized: "unsupported", - action: "degraded", + expect(revision!.target.execution).toEqual({ + style: "programmatic", + language: "python", + options: [{ key: "repl", value: "persistent" }], }); - expect(revision!.realization[0].reason).toContain("has no binding"); + expect(revision!.workflow.mode).toBe("programmatic"); }); - it("marks an implicitly enabled component with an unsupported binding as degraded", async () => { + it("merges permissions monotonically and derives MCP transport grants", async () => { const bundle = await compileSource(` - component optional-observer { kind observer } - binding optional-observer for pi { mechanism unavailable strength unsupported } - plugin diagnostics { version "1.0.0" provides [ component.optional-observer ] } - composition run { target pi include [ plugin.diagnostics@1 ] } + skill fetch-policy { + description "Fetch only from the registry." + permissions { network deny workspace read } + } + tool fetcher.run { + permissions { network allow } + } + mcp registry { transport http url env.REGISTRY_MCP } + mcp indexer { transport stdio command "indexer --stdio" } + workflow solo { stop when coder.done } + harness run { + workflow solo + agent coder { + use skill fetch-policy + require tool fetcher.run + connect mcp registry + connect mcp indexer + } + } + target pi `); + const { revision } = resolveHarness(bundle, "run"); - const { revision } = resolveComposition(bundle, "run"); + expect(revision!.permissions).toEqual([ + { domain: "network", access: "deny" }, + { domain: "process", access: "allow" }, + { domain: "workspace", access: "read" }, + ]); + }); - expect(revision!.realization[0]).toMatchObject({ - declaredStrength: "unsupported", - declaredMechanism: "unavailable", - realized: "unsupported", - action: "degraded", - }); - expect(revision!.realization[0].reason).toContain("declares this component unsupported"); + it("fails with a clear error for an unknown harness", async () => { + const bundle = await compileExample(); + const { report } = resolveHarness(bundle, "missing", "pi"); + + expect(report.status).toBe("failed"); + expect(report.errors).toEqual(["Harness 'missing' is not defined in the bundle."]); }); }); diff --git a/packages/harness/test/skill.test.ts b/packages/harness/test/skill.test.ts index 4ee2dfb..4ad859e 100644 --- a/packages/harness/test/skill.test.ts +++ b/packages/harness/test/skill.test.ts @@ -21,8 +21,8 @@ afterEach(async () => { ); }); -function runValidator(file: string, ...compositionIds: string[]) { - return spawnSync(process.execPath, [VALIDATOR_PATH, file, ...compositionIds], { +function runValidator(file: string, ...harnessIds: string[]) { + return spawnSync(process.execPath, [VALIDATOR_PATH, file, ...harnessIds], { encoding: "utf8", }); } @@ -35,13 +35,14 @@ describe("generate-harness-dsl validator", () => { const output = JSON.parse(result.stdout); expect(output.valid).toBe(true); expect(output.diagnostics).toEqual([]); - expect(output.compositions).toEqual([ - expect.objectContaining({ compositionId: "standard-coding", status: "resolved" }), - expect.objectContaining({ compositionId: "standard-coding-qoder", status: "resolved" }), + expect(output.harnesses).toEqual([ + expect.objectContaining({ harnessId: "standard-coding", runtime: "pi", status: "resolved" }), + expect.objectContaining({ harnessId: "standard-coding", runtime: "qoder", status: "resolved" }), ]); - expect(output.compositions[0].realizations).toContainEqual( + expect(output.harnesses[0].realizations).toContainEqual( expect.objectContaining({ - componentId: "verification-before-complete", + agentId: "verifier", + capabilityId: "verification-before-complete", declaredStrength: "enforced", realized: "advisory", action: "degraded", @@ -55,7 +56,7 @@ describe("generate-harness-dsl validator", () => { const file = join(directory, "invalid.harness"); await writeFile( file, - "composition broken { target qoder include [ plugin.missing@1 ] }", + "harness broken { workflow missing-workflow agent coder { use skill missing-skill } }", "utf8", ); @@ -64,32 +65,33 @@ describe("generate-harness-dsl validator", () => { expect(result.status).toBe(1); const output = JSON.parse(result.stdout); expect(output.valid).toBe(false); - expect(output.compositions).toEqual([]); + expect(output.harnesses).toEqual([]); expect(output.diagnostics).toContainEqual(expect.objectContaining({ severity: "error" })); }); - it("fails when a requested composition does not exist", () => { - const result = runValidator(EXAMPLE_PATH, "missing-composition"); + it("fails when a requested harness does not exist", () => { + const result = runValidator(EXAMPLE_PATH, "missing-harness"); expect(result.status).toBe(1); const output = JSON.parse(result.stdout); expect(output.valid).toBe(false); - expect(output.compositions).toEqual([ + expect(output.harnesses).toEqual([ expect.objectContaining({ - compositionId: "missing-composition", + harnessId: "missing-harness", status: "failed", - errors: ["Composition 'missing-composition' is not defined in the bundle."], + errors: ["Harness 'missing-harness' is not defined in the bundle."], }), + expect.objectContaining({ harnessId: "missing-harness", status: "failed" }), ]); }); - it("requires generated files to declare an independently resolvable composition", async () => { + it("requires generated files to declare an independently resolvable harness", async () => { const directory = await mkdtemp(join(tmpdir(), "harness-skill-test-")); temporaryDirectories.push(directory); const file = join(directory, "library-only.harness"); await writeFile( file, - "component repository-analysis { kind skill permissions { workspace read } }", + 'skill repository-analysis { description "Analyze the repository." permissions { workspace read } }', "utf8", ); @@ -98,11 +100,11 @@ describe("generate-harness-dsl validator", () => { expect(result.status).toBe(1); const output = JSON.parse(result.stdout); expect(output.valid).toBe(false); - expect(output.compositions).toEqual([]); + expect(output.harnesses).toEqual([]); expect(output.diagnostics).toContainEqual( expect.objectContaining({ severity: "error", - message: "No composition is declared; generated DSL must be independently resolvable.", + message: "No harness is declared; generated DSL must be independently resolvable.", }), ); }); diff --git a/packages/harness/test/sugar.test.ts b/packages/harness/test/sugar.test.ts index d8242fa..d2cb7cf 100644 --- a/packages/harness/test/sugar.test.ts +++ b/packages/harness/test/sugar.test.ts @@ -3,7 +3,7 @@ import { Value } from "@sinclair/typebox/value"; import { describe, expect, it } from "vitest"; import { compileHarness } from "../src/compiler/compile.js"; import { HarnessIrBundleSchema, type HarnessIrBundle } from "../src/ir/index.js"; -import { resolveComposition } from "../src/resolver/resolve.js"; +import { resolveHarness } from "../src/resolver/resolve.js"; const MINIMAL_URL = new URL("../examples/minimal.harness", import.meta.url); @@ -14,18 +14,26 @@ async function compile(source: string): Promise { } describe("progressive-disclosure sugar", () => { - it("resolves a minimal file: a bare require with no plugin or binding", async () => { + it("resolves a minimal file: a bare use skill with no runtime or binding", async () => { const bundle = await compile(await readFile(MINIMAL_URL, "utf8")); expect(Value.Check(HarnessIrBundleSchema, bundle)).toBe(true); - const { revision, report } = resolveComposition(bundle, "my-agent"); + const { revision, report } = resolveHarness(bundle, "my-agent"); expect(report.status).toBe("resolved"); - expect(revision!.resolved.components.map((component) => component.id)).toEqual([ + expect(revision!.resolved.capabilities.map((capability) => capability.id)).toEqual([ "require-tests", ]); + // The bare `target qoder` synthesizes a tool-calling runtime with the + // conventional adapter package. + expect(revision!.target).toEqual({ + runtime: "qoder", + adapter: "@harness/adapter-qoder", + execution: { style: "tool-calling" }, + }); expect(report.realizations).toEqual([ expect.objectContaining({ - componentId: "require-tests", + agentId: "coder", + capabilityId: "require-tests", declaredStrength: "advisory", realized: "advisory", materializedMechanism: "prompt-preamble", @@ -34,109 +42,107 @@ describe("progressive-disclosure sugar", () => { ]); }); - it("a bare require defaults to minimum advisory with on-degrade report", async () => { + it("a bare requirement defaults to minimum advisory with on-degrade report", async () => { + const bundle = await compile(` + skill gate { description "Verify first." } + workflow solo { stop when coder.done } + harness run { + workflow solo + agent coder { use skill gate } + } + target pi + `); + expect(bundle.harnesses[0].agents[0].requirements).toEqual([ + { capabilityId: "gate", capabilityKind: "skill", minimum: "advisory", onDegrade: "report" }, + ]); + }); + + it("synthesizes an implicit tool contract for an undeclared require tool", async () => { const bundle = await compile(` - component gate { kind policy } - composition run { target pi require gate } + workflow solo { stop when coder.done } + harness run { + workflow solo + agent coder { + require tool workspace.read + require tool process.exec + } + } + target pi `); - expect(bundle.compositions[0].requirements).toEqual([ - { componentId: "gate", minimum: "advisory", onDegrade: "report" }, + expect(bundle.tools).toEqual([ + expect.objectContaining({ id: "process.exec", implicit: true, permissions: [] }), + expect.objectContaining({ id: "workspace.read", implicit: true, permissions: [] }), ]); + expect(resolveHarness(bundle, "run").report.status).toBe("resolved"); }); - it("expands a for [a, b] binding into one binding per host", async () => { + it("expands a for [a, b] binding into one binding per runtime", async () => { const bundle = await compile(` - component gate { kind policy } - binding gate for [pi, qoder] { mechanism completion-hook strength enforced } + skill gate { description "Verify first." } + binding gate for [pi, qoder] { mechanism pi.extension strength enforced } `); expect(bundle.bindings).toEqual([ - expect.objectContaining({ host: "pi", mechanism: "completion-hook", strength: "enforced" }), - expect.objectContaining({ host: "qoder", mechanism: "completion-hook", strength: "enforced" }), + expect.objectContaining({ runtime: "pi", mechanism: "pi.extension", strength: "enforced" }), + expect.objectContaining({ runtime: "qoder", mechanism: "pi.extension", strength: "enforced" }), ]); }); it("defaults an omitted binding mechanism and strength to advisory prompt guidance", async () => { const bundle = await compile(` - component gate { kind policy } + skill gate { description "Verify first." } binding gate for pi {} `); expect(bundle.bindings[0]).toMatchObject({ - host: "pi", + runtime: "pi", mechanism: "prompt-preamble", strength: "advisory", }); }); - it("inherits target, includes, requirements, and settings through extends", async () => { + it("uses the target's adapter shorthand when synthesizing a runtime", async () => { const bundle = await compile(` - component gate { kind policy } - binding gate for [pi, qoder] { strength advisory } - plugin p { version "1.0.0" provides [ component.gate ] } - composition base { - target pi - include [ plugin.p@1 ] - require gate { minimum advisory } - configure { shell.timeout = 30s } - } - composition derived extends base { - target qoder + skill gate { description "Verify first." } + workflow solo { stop when coder.done } + harness run { + workflow solo + agent coder { use skill gate } } + target custom-host uses adapter.custom `); - const derived = bundle.compositions.find((composition) => composition.id === "derived")!; - expect(derived.target).toBe("qoder"); - expect(derived.includes).toEqual([{ pluginId: "p", range: "1" }]); - expect(derived.requirements).toEqual([ - { componentId: "gate", minimum: "advisory", onDegrade: "report" }, - ]); - expect(derived.settings).toEqual([ - { key: "shell.timeout", value: { type: "duration", value: "30s", ms: 30_000 } }, - ]); - expect(resolveComposition(bundle, "derived").report.status).toBe("resolved"); + const { revision } = resolveHarness(bundle, "run"); + expect(revision!.target.adapter).toBe("@harness/adapter-custom"); }); - it("lets a derived composition override an inherited setting by key", async () => { + it("prefers a declared runtime over synthesis for the same target", async () => { const bundle = await compile(` - component gate { kind policy } - composition base { target pi require gate configure { shell.timeout = 30s } } - composition derived extends base { configure { shell.timeout = 90s } } - `); - const derived = bundle.compositions.find((composition) => composition.id === "derived")!; - expect(derived.settings).toEqual([ - { key: "shell.timeout", value: { type: "duration", value: "90s", ms: 90_000 } }, - ]); - }); - - it("fails compilation when a composition has neither target nor an extends target", async () => { - const result = await compileHarness(` - component gate { kind policy } - composition run { require gate } - `); - expect(result.bundle).toBeUndefined(); - expect(result.diagnostics).toContainEqual( - expect.objectContaining({ - severity: "error", - message: expect.stringContaining("has no target"), - }), - ); - }); - - it("fails compilation on a circular extends chain", async () => { - const result = await compileHarness(` - composition a extends b { target pi } - composition b extends a { target qoder } + skill gate { description "Verify first." } + workflow solo { stop when coder.done } + runtime prime { + adapter "@harness/adapter-prime" + execution programmatic.python { repl persistent } + } + harness run { + workflow solo + agent coder { use skill gate } + } + target prime `); - expect(result.bundle).toBeUndefined(); - expect(result.diagnostics.some((d) => d.message.includes("circular 'extends'"))).toBe(true); + const { revision } = resolveHarness(bundle, "run"); + expect(revision!.target).toEqual({ + runtime: "prime", + adapter: "@harness/adapter-prime", + execution: { style: "programmatic", language: "python", options: [{ key: "repl", value: "persistent" }] }, + }); }); - it("still reports duplicate hosts within a single multi-host binding", async () => { + it("still reports duplicate runtimes within a single multi-runtime binding", async () => { const result = await compileHarness(` - component gate { kind policy } + skill gate { description "Verify first." } binding gate for [pi, pi] { strength advisory } `); expect(result.bundle).toBeUndefined(); - expect(result.diagnostics.some((d) => d.message.includes("Duplicate component/host binding"))).toBe( - true, - ); + expect( + result.diagnostics.some((d) => d.message.includes("Duplicate capability/runtime binding")), + ).toBe(true); }); }); From f41594fbd4c077bbb6fc7b0c122d229b1db0da7b Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 15 Aug 2026 16:29:19 +0800 Subject: [PATCH 07/25] feat(harness): add interactive AG-UI studio Add host-neutral streaming events, an AG-UI SSE adapter, and a React studio for live runs and comparison evidence. Stream Qoder partial messages and bounded tool results into keyboard-expandable cards with honest failed, unavailable, truncated, and interrupted states. Harden the loopback browser boundary against client-controlled Host trust, return structured 413 responses for oversized bodies, and add a built-app Playwright regression at 390px. Wire the new packages into CI and the protected GitHub Actions publication selector; local workflows only build, test, pack, or dry-run. Implements docs/specs/2026-08-15-harness-ui-studio.md. Validated with npm run check, the doc-link graph test, workspace pack dry-runs, workflow YAML parsing, and the Playwright interaction test. Co-authored-by: Codex (GPT 5.6 Sol) --- .github/workflows/ci.yml | 14 + .github/workflows/release.yml | 23 +- docs/specs/2026-08-15-harness-ui-studio.md | 261 +++++++++++ package-lock.json | 177 +++++++- package.json | 11 +- packages/harness-studio/LICENSE | 21 + packages/harness-studio/README.md | 66 +++ packages/harness-studio/package.json | 58 +++ packages/harness-studio/playwright.config.mjs | 15 + packages/harness-studio/scripts/build-app.mjs | 26 ++ packages/harness-studio/src/app/App.tsx | 67 +++ .../harness-studio/src/app/CompareView.tsx | 116 +++++ packages/harness-studio/src/app/RunView.tsx | 199 +++++++++ packages/harness-studio/src/app/agui-store.ts | 152 +++++++ .../harness-studio/src/app/compare-model.ts | 82 ++++ packages/harness-studio/src/app/index.html | 91 ++++ packages/harness-studio/src/app/main.tsx | 8 + packages/harness-studio/src/app/sse-client.ts | 46 ++ .../harness-studio/src/app/tool-call-model.ts | 47 ++ packages/harness-studio/src/index.ts | 27 ++ packages/harness-studio/src/server/cli.ts | 141 ++++++ packages/harness-studio/src/server/server.ts | 152 +++++++ .../harness-studio/test/agui-store.test.ts | 158 +++++++ .../test/browser/tool-call.spec.mjs | 93 ++++ .../harness-studio/test/compare-model.test.ts | 80 ++++ .../harness-studio/test/fixtures/verdict.json | 68 +++ packages/harness-studio/test/server.test.ts | 193 ++++++++ .../test/tool-call-model.test.ts | 25 ++ packages/harness-studio/tsconfig.json | 17 + packages/harness-studio/vitest.config.ts | 10 + packages/harness-ui/LICENSE | 21 + packages/harness-ui/README.md | 93 ++++ packages/harness-ui/package.json | 55 +++ packages/harness-ui/src/cli.ts | 156 +++++++ packages/harness-ui/src/index.ts | 40 ++ packages/harness-ui/src/protocol.ts | 192 ++++++++ packages/harness-ui/src/run.ts | 136 ++++++ packages/harness-ui/src/server.ts | 291 ++++++++++++ packages/harness-ui/src/sse.ts | 27 ++ packages/harness-ui/src/translate.ts | 133 ++++++ packages/harness-ui/test/server.test.ts | 332 ++++++++++++++ packages/harness-ui/test/translate.test.ts | 182 ++++++++ packages/harness-ui/tsconfig.json | 16 + packages/harness-ui/vitest.config.ts | 10 + packages/harness/README.md | 15 + packages/harness/package.json | 4 + packages/harness/src/compare/index.ts | 1 + packages/harness/src/compare/verdict.ts | 179 ++++++++ packages/harness/src/compiler/compile.ts | 4 +- packages/harness/src/exec/events.ts | 188 ++++++++ packages/harness/src/exec/executor.ts | 1 + packages/harness/src/exec/index.ts | 13 + packages/harness/src/exec/pi-sdk.ts | 32 ++ packages/harness/src/exec/qoder-sdk.ts | 162 ++++++- packages/harness/test/compare.test.ts | 27 +- packages/harness/test/compile.test.ts | 8 + packages/harness/test/events.test.ts | 413 ++++++++++++++++++ packages/harness/test/exec.test.ts | 2 + 58 files changed, 5165 insertions(+), 12 deletions(-) create mode 100644 docs/specs/2026-08-15-harness-ui-studio.md create mode 100644 packages/harness-studio/LICENSE create mode 100644 packages/harness-studio/README.md create mode 100644 packages/harness-studio/package.json create mode 100644 packages/harness-studio/playwright.config.mjs create mode 100644 packages/harness-studio/scripts/build-app.mjs create mode 100644 packages/harness-studio/src/app/App.tsx create mode 100644 packages/harness-studio/src/app/CompareView.tsx create mode 100644 packages/harness-studio/src/app/RunView.tsx create mode 100644 packages/harness-studio/src/app/agui-store.ts create mode 100644 packages/harness-studio/src/app/compare-model.ts create mode 100644 packages/harness-studio/src/app/index.html create mode 100644 packages/harness-studio/src/app/main.tsx create mode 100644 packages/harness-studio/src/app/sse-client.ts create mode 100644 packages/harness-studio/src/app/tool-call-model.ts create mode 100644 packages/harness-studio/src/index.ts create mode 100644 packages/harness-studio/src/server/cli.ts create mode 100644 packages/harness-studio/src/server/server.ts create mode 100644 packages/harness-studio/test/agui-store.test.ts create mode 100644 packages/harness-studio/test/browser/tool-call.spec.mjs create mode 100644 packages/harness-studio/test/compare-model.test.ts create mode 100644 packages/harness-studio/test/fixtures/verdict.json create mode 100644 packages/harness-studio/test/server.test.ts create mode 100644 packages/harness-studio/test/tool-call-model.test.ts create mode 100644 packages/harness-studio/tsconfig.json create mode 100644 packages/harness-studio/vitest.config.ts create mode 100644 packages/harness-ui/LICENSE create mode 100644 packages/harness-ui/README.md create mode 100644 packages/harness-ui/package.json create mode 100644 packages/harness-ui/src/cli.ts create mode 100644 packages/harness-ui/src/index.ts create mode 100644 packages/harness-ui/src/protocol.ts create mode 100644 packages/harness-ui/src/run.ts create mode 100644 packages/harness-ui/src/server.ts create mode 100644 packages/harness-ui/src/sse.ts create mode 100644 packages/harness-ui/src/translate.ts create mode 100644 packages/harness-ui/test/server.test.ts create mode 100644 packages/harness-ui/test/translate.test.ts create mode 100644 packages/harness-ui/tsconfig.json create mode 100644 packages/harness-ui/vitest.config.ts create mode 100644 packages/harness/src/compare/verdict.ts create mode 100644 packages/harness/src/exec/events.ts create mode 100644 packages/harness/test/events.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b05ffbe..254b129 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,20 @@ jobs: run: npm run harness:build - name: Test Harness DSL package run: npm run harness:test + - name: Build Harness UI package + run: npm run harness-ui:build + - name: Test Harness UI package + run: npm run harness-ui:test + - name: Build Harness Studio package + run: npm run harness-studio:build + - name: Test Harness Studio package + run: npm run harness-studio:test + - name: Install Chromium for Harness Studio browser test + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '22.20.0' }} + run: npx playwright install --with-deps chromium + - name: Test Harness Studio browser interaction + if: ${{ matrix.os == 'ubuntu-latest' && matrix.node == '22.20.0' }} + run: npm run harness-studio:test:browser - name: Upload JUnit test results if: ${{ always() }} uses: actions/upload-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index df20143..ef78f29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,17 @@ name: Publish npm on: workflow_dispatch: + inputs: + package: + description: Package to publish + required: true + default: better-harness + type: choice + options: + - better-harness + - harness + - harness-ui + - harness-studio permissions: contents: read @@ -20,8 +31,14 @@ jobs: registry-url: https://registry.npmjs.org package-manager-cache: false - run: npm ci - - run: npm test - - run: npm run pack:verify - - run: npm publish --access public + - run: npm run check + - name: Publish Better Harness + if: ${{ inputs.package == 'better-harness' }} + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Publish selected Harness workspace + if: ${{ inputs.package != 'better-harness' }} + run: npm publish --workspace "@qoder-ai/${{ inputs.package }}" --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/docs/specs/2026-08-15-harness-ui-studio.md b/docs/specs/2026-08-15-harness-ui-studio.md new file mode 100644 index 0000000..39ebee4 --- /dev/null +++ b/docs/specs/2026-08-15-harness-ui-studio.md @@ -0,0 +1,261 @@ +# Harness UI and Studio: streaming run events, AG-UI adapter, React studio + +## Traceability + +- Spec ID: `2026-08-15-harness-ui-studio` +- Status: Implemented + +## Intent + +The Harness DSL currently runs only in batch mode: `compileHarness → +resolveHarness → executor.execute()` returns one `HarnessRunResult` after the +run ends, and the compare CLI writes evidence JSON to disk. Neither path gives +a user any live view of a running harness, and the compare/inspector evidence +has no interactive UI. + +This spec adds three bounded layers without changing DSL semantics: + +1. **Streaming run-event seam** in `@qoder-ai/harness`: executors emit + host-neutral `HarnessRunEvent` values while a run is in flight. +2. **`@qoder-ai/harness-ui`**: a protocol adapter that translates neutral run + events into [AG-UI protocol](https://docs.ag-ui.com/) events and serves + them over SSE, so any AG-UI-compatible frontend (CopilotKit, custom + clients) can drive a harness run. +3. **`@qoder-ai/harness-studio`**: a React application (bundled with esbuild, + served by a small Node CLI) with a live Run view fed by the AG-UI stream + and a Compare view fed by `harness-compare.v1` evidence directories. + +The DSL core stays UI-free: no grammar, IR, or resolver change. The existing +static-HTML harness inspector remains the zero-dependency offline report; the +studio is an additive, richer surface, not a replacement. + +## Acceptance scenarios + +- **AC-1 (neutral event seam)**: `QoderSdkExecutor` accepts an + `onRunEvent` listener, requests the SDK's partial-message stream, and emits + text deltas as they arrive rather than waiting for a completed assistant + turn. The lifecycle-ordered sequence contains one `run-started` first, + framed `message-started`/`text-delta`/`message-finished` text, paired + `tool-call-started`/`tool-call-finished` events, `run-error` only on + failure, and exactly one final neutral `run-finished` with the exit code and + metrics. When Qoder exposes a retained `tool_result`, the seam correlates it + back to the originating tool-call id without leaking credential-shaped + fields (the existing trace redaction applies before mapping). +- **AC-2 (event mapping is deterministic and tested)**: the lifecycle emitter + and SDK-message mapping (`applyQoderSdkMessage`) are exported and covered by + unit tests using scripted partial and completed SDK messages, with no live + SDK required. Completed assistant payloads do not duplicate text already + emitted from partial messages, and completed-only messages remain a fallback + for older or injected SDK implementations. +- **AC-3 (AG-UI translation)**: `@qoder-ai/harness-ui` translates a neutral + event sequence into a valid AG-UI event sequence: `RUN_STARTED`, paired + `TEXT_MESSAGE_START`/`TEXT_MESSAGE_CONTENT`/`TEXT_MESSAGE_END`, + `TOOL_CALL_START`/`TOOL_CALL_ARGS`/`TOOL_CALL_END`, optional correlated + `TOOL_CALL_RESULT`, and exactly one terminal + event: `RUN_FINISHED` on success or `RUN_ERROR` on failure. Open text + messages are always closed before a tool call, an error, or run completion. + Message and tool-call ids are namespaced by `runId`, so repeated runs in one + thread cannot overwrite earlier AG-UI entities. +- **AC-4 (SSE endpoint)**: the harness-ui server accepts an AG-UI + `RunAgentInput` POST (threadId, runId, messages) and responds with an SSE + stream (`data: \n\n` frames) of the translated events, echoing the + caller's threadId/runId. The run layer owns the outer lifecycle: failures + while constructing or invoking an injected executor still produce + `RUN_STARTED` followed by one `RUN_ERROR`. Browser requests are same-origin + by default; explicitly allowlisted origins receive an exact CORS response, + while untrusted origins, non-loopback `Host`/matching-`Origin` pairs, and + non-JSON POST bodies are rejected before an executor is created. A request + larger than 1 MiB receives a structured HTTP 413 response rather than a + reset socket. Verified by in-process and raw-HTTP tests with injected fake + executors; no network or live SDK in tests. +- **AC-5 (interactive studio run trace)**: the studio's AG-UI event reducer + folds an event stream into UI state (messages, tool calls, results, run + status, error). Run renders workbench-style, keyboard-expandable tool cards + with a compact argument preview, formatted arguments, retained result, call + id, and preparing/running/completed/failed/result-unavailable/interrupted + status. Tool-result error metadata survives the neutral and AG-UI seams; a + successful run without a retained result is not labelled as a completed + result. Retained results are capped at 64 KiB with explicit truncation + metadata. Pure model tests cover streamed arguments, result correlation, + terminal settlement, failure/truncation metadata, and malformed/plain-text + payload formatting; browser verification expands a real rendered tool card. +- **AC-6 (studio compare view)**: the studio server exposes the evidence + directory's `verdict.json` via `/api/evidence`, and the compare summary + model derives per-variant rows (pass rate, mean score, cost) from a + core-validated `HarnessCompareVerdict` value. Malformed aggregates or trial + rows produce a readable evidence error rather than crashing React. Both are + covered by tests against fixture and malformed data. +- **AC-7 (pipeline parity)**: both new packages follow the existing + `packages/harness` pipeline rules: npm workspace membership, root + `:build` / `:test` scripts, CI build+test steps, `prepack` build + and test, `publishConfig.access: public`, MIT license, repo `engines` range. +- **AC-8 (CLI help contract)**: `harness-ui --help` and `harness-studio + --help` print usage and exit 0 without reading the workspace, compiling a + harness, or opening a port. +- **AC-9 (responsive studio shell)**: Run controls and Compare tables remain + usable at desktop and 390px-wide browser viewports. Wide evidence tables + scroll inside labelled containers without increasing the document width; + browser verification records console/page errors and screenshots. +- **AC-10 (automated interaction regression)**: a Playwright test drives the + built Studio through its real HTTP server, starts a scripted run, expands and + collapses a failed/truncated Tool Call with the keyboard, verifies its + retained details and status, checks 390px document containment, and fails on + browser console/page errors. CI runs this gate once on Ubuntu/Node 22 after + installing Chromium; package unit tests remain cross-platform and do not + require a browser download. +- **AC-11 (GitHub-owned publication)**: `.github/workflows/release.yml` remains + the only publication entrypoint. Its manual dispatch selects exactly one of + the root package, `@qoder-ai/harness`, `@qoder-ai/harness-ui`, or + `@qoder-ai/harness-studio`, runs the complete repository check first, and + publishes with the protected `npm` environment and repository secret. Local + development commands build, test, pack, and dry-run only; this change never + executes a local `npm publish`. + +## Non-goals + +- No change to DSL grammar, IR version, resolver semantics, or the + `advisory` materialization cap. +- No dependency on `@ag-ui/core` (0.0.x, pre-stable, pulls rxjs/zod). The + adapter implements the AG-UI **wire format** with local types; conformance + is asserted by tests on the emitted JSON shapes. Revisit when AG-UI reaches + a stable major. +- No replacement of the static-HTML harness inspector or the compare + `verdict.html`; the studio reads the same evidence, it does not own it. +- No Pi live-run view in the studio (the seam is executor-generic; the Qoder + executor is the first emitter, and the Pi executor emits text deltas only). +- No local publication or registry mutation. Package versions and the selected + release target must be reviewed in git, then publication is dispatched from + the repository's protected GitHub Actions `Publish npm` workflow. +- No authentication or remote deployment story for the SSE server; it binds + to `127.0.0.1` by default and is a local development surface. This does not + waive browser-origin protection: cross-origin access requires an explicit + exact-origin allowlist. + +## Design + +```text +.harness ── @qoder-ai/harness ── HarnessRunEvent (neutral, streaming) + │ + @qoder-ai/harness-ui: AG-UI translator + SSE server + │ + any AG-UI client ◄── SSE ──┤ + │ + @qoder-ai/harness-studio: React Run view + Compare view + (verdict.json / evidence) +``` + +- `packages/harness/src/exec/events.ts`: `HarnessRunEvent` union plus + `HarnessRunEmitter`, the lifecycle guard that enforces the framing + invariants (single started/finished, message frames, paired tool calls). + `QoderSdkExecutor` and `PiSdkExecutor` gain `onRunEvent`; the mapping from + redacted SDK messages lives in `applyQoderSdkMessage()`. Qoder enables SDK + partial messages and keeps per-parent mapping state so the later completed + assistant message is a fallback, not duplicate output. +- `packages/harness-ui`: `protocol.ts` (AG-UI wire types), `translate.ts` + (stateful neutral→AG-UI translator), `sse.ts` (frame encoding), `run.ts` + (compile+resolve+execute with an injected executor factory), `server.ts` + (`POST /agui`, `GET /healthz`, origin/content-type policy), `cli.ts` + (`harness-ui serve`, repeatable `--allow-origin`). +- `packages/harness-studio`: React app under `src/app/` (pure state modules + `agui-store.ts`, `compare-model.ts` kept separate from components), Node + server under `src/server/` serving the esbuild bundle, `/api/evidence`, and + an embedded harness-ui `/agui` route when a `.harness` file is provided. + +## Plan and tasks + +1. Tighten the neutral Qoder mapper around the SDK's `stream_event` contract, + retain completed-message fallback behavior, and test de-duplication. +2. Make the AG-UI run layer own start/terminal framing and namespace protocol + entity ids with the caller's run id. +3. Add a shared local-browser request policy to the standalone and embedded + `/agui` handlers, plus an explicit origin allowlist for external local UIs. +4. Move verdict validation to the core compare owner and make Studio consume + that validated contract. +5. Add workbench-style interactive tool cards, correlated result rendering, + responsive table containers, and verify Run/Compare in a real browser at + desktop and narrow viewports. +6. Keep the truncated-declaration compiler diagnostic fix as a supporting + robustness change: malformed harness input must reach the AG-UI error path + as diagnostics rather than an uncaught compiler exception. +7. Harden the browser boundary against client-controlled Host trust and make + oversized request handling return a real 413 response without destroying the + socket before the response is written. +8. Carry tool-result error/truncation metadata through a namespaced AG-UI custom + event, distinguish failed and result-unavailable cards, memoize payload + formatting, and add one built-app Playwright regression to CI. +9. Extend the existing GitHub Actions publication workflow with a constrained + package selector so each Harness workspace is publishable without adding a + local release path. + +## Risks + +- **Duplicate streaming text:** Qoder emits both partial events and a completed + assistant message. Mapping state must suppress only the matching completed + text and reset at the assistant boundary. +- **Browser-triggered runs:** a loopback listener is reachable from arbitrary + webpages. Reject untrusted `Origin` values, DNS-rebinding-shaped + non-loopback `Host` values, and simple non-JSON POSTs before reading or + executing the harness; never derive trust from an arbitrary client-supplied + Host or emit wildcard CORS. +- **Protocol identity collisions:** neutral emitter counters are run-local. + The AG-UI adapter must namespace every correlated message/tool id + consistently without changing the neutral contract. +- **Evidence drift:** Studio consumes persisted JSON that can be stale, + truncated, or hand-edited. Validate the core verdict contract before + deriving render rows and return bounded diagnostics. +- **Layout regression:** compare tables are intentionally wide. Constrain + overflow to the table region and verify both desktop and narrow viewports. +- **False tool completion:** `TOOL_CALL_END` ends the streamed argument + declaration, not necessarily execution. Keep the card in a running state + until `TOOL_CALL_RESULT` or the run terminal settles it; retain failed and + result-unavailable as distinct terminal states. +- **Large retained results:** tool output can be megabytes and otherwise + amplify SSE buffering, React state, JSON parsing, and DOM work. Bound retained + output at the neutral event owner, carry the original byte length, and memoize + Tool Call rendering so unrelated text deltas do not reformat settled cards. + +## Test evidence + +- `packages/harness/test/events.test.ts` — AC-1, AC-2, including partial/full + de-duplication and completed-message fallback +- `packages/harness-ui/test/translate.test.ts` — AC-3, including run-id + namespacing and mutually exclusive terminal events +- `packages/harness-ui/test/server.test.ts` — AC-4, AC-8, including factory + failure, same-origin/allowlisted CORS, and rejected hostile/simple requests +- `packages/harness-studio/test/agui-store.test.ts`, + `packages/harness-studio/test/tool-call-model.test.ts` — AC-5, including + result correlation, interrupted calls, and payload formatting +- `packages/harness-studio/test/compare-model.test.ts`, + `packages/harness-studio/test/server.test.ts` — AC-6, AC-8, including + malformed verdict rejection +- CI: `harness-ui:build` / `harness-ui:test` / `harness-studio:build` / + `harness-studio:test` steps in `.github/workflows/ci.yml` — AC-7 +- Release: manual `Publish npm` workflow package choices plus its protected + environment, full-check, and workspace-scoped publish steps — AC-11 +- Browser: built Studio Run and Compare views at desktop and 390px width, + console/page error inspection, and saved screenshots — AC-5, AC-6, AC-9 + +## Validation record + +- `npm run check` passed on 2026-08-15: root 94 files / 1324 tests, + harness 9 files / 93 tests, harness-ui 2 files / 20 tests, harness-studio + 4 files / 25 tests, plus generated-source and package verification gates. +- `npx vitest run test/skills-docs/doc-link-graph.test.mjs` passed after + regenerating `docs/better-harness-doc-links.mmd`. +- Browser fixture verification exercised an expanded `Read` tool card with + formatted arguments and correlated result. At an exact 390px content + viewport, the document remained 390px wide; tool cards stayed within their + container and Compare tables scrolled inside their labelled regions. + Browser console/page logs were empty. +- Automated Playwright verification exercised a failed 64 KiB-truncated Bash + result through the built Studio server at 390px, expanded and collapsed the + card by keyboard, asserted document containment, saved a screenshot, and + observed no console/page errors. +- Raw HTTP tests reject a matching attacker Host/Origin pair and return JSON + 413 responses for both declared-length and chunked bodies over 1 MiB. The + GitHub `Publish npm` workflow parsed successfully with constrained package + choices; no local publication command was executed. +- Workspace `npm pack --dry-run` gates passed without registry mutation: + Harness UI contained 17 entries (12.2 kB) including the browser-safe protocol + entrypoint; Harness Studio contained 28 entries (296.1 kB) including the + built React application. diff --git a/package-lock.json b/package-lock.json index a3ab0ec..12ea4bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,9 @@ "version": "0.6.3", "license": "MIT", "workspaces": [ - "packages/harness" + "packages/harness", + "packages/harness-ui", + "packages/harness-studio" ], "dependencies": { "@vscode/tree-sitter-wasm": "0.3.1", @@ -2084,10 +2086,34 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@playwright/test": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@qoder-ai/harness": { "resolved": "packages/harness", "link": true }, + "node_modules/@qoder-ai/harness-studio": { + "resolved": "packages/harness-studio", + "link": true + }, + "node_modules/@qoder-ai/harness-ui": { + "resolved": "packages/harness-ui", + "link": true + }, "node_modules/@qoder-ai/qoder-agent-sdk": { "version": "1.0.21", "resolved": "https://registry.npmjs.org/@qoder-ai/qoder-agent-sdk/-/qoder-agent-sdk-1.0.21.tgz", @@ -2533,6 +2559,26 @@ "undici-types": "~8.3.0" } }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/semver": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.8.0.tgz", @@ -3321,6 +3367,13 @@ "node": ">= 8" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4634,6 +4687,53 @@ "node": ">=16.20.0" } }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.26", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", @@ -4737,6 +4837,27 @@ "node": ">= 0.10" } }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", @@ -4825,6 +4946,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -5637,6 +5764,9 @@ "semver": "7.8.5", "shiki": "4.4.3" }, + "bin": { + "harness-compare": "dist/compare/cli.js" + }, "devDependencies": { "@earendil-works/pi-coding-agent": "0.84.2", "@types/node": "26.2.0", @@ -5656,6 +5786,51 @@ "optional": true } } + }, + "packages/harness-studio": { + "name": "@qoder-ai/harness-studio", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@qoder-ai/harness": "0.1.0", + "@qoder-ai/harness-ui": "0.1.0", + "react": "19.2.8", + "react-dom": "19.2.8" + }, + "bin": { + "harness-studio": "dist/server/cli.js" + }, + "devDependencies": { + "@playwright/test": "^1.62.1", + "@types/node": "26.2.0", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", + "esbuild-wasm": "0.28.1", + "typescript": "7.0.2", + "vitest": "4.1.10" + }, + "engines": { + "node": ">=22.20.0 <25.0.0" + } + }, + "packages/harness-ui": { + "name": "@qoder-ai/harness-ui", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@qoder-ai/harness": "0.1.0" + }, + "bin": { + "harness-ui": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "26.2.0", + "typescript": "7.0.2", + "vitest": "4.1.10" + }, + "engines": { + "node": ">=22.20.0 <25.0.0" + } } } } diff --git a/package.json b/package.json index c32a521..26a87d9 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ ], "type": "module", "workspaces": [ - "packages/harness" + "packages/harness", + "packages/harness-ui", + "packages/harness-studio" ], "pi": { "skills": [ @@ -78,10 +80,15 @@ }, "scripts": { "bundle:zip": "node scripts/npm-package/create-bundle.mjs", - "check": "npm test && npm run harness:generated && npm run harness:build && npm run harness:test && npm run pack:verify", + "check": "npm test && npm run harness:generated && npm run harness:build && npm run harness:test && npm run harness-ui:build && npm run harness-ui:test && npm run harness-studio:build && npm run harness-studio:test && npm run pack:verify", "harness:build": "npm run build -w @qoder-ai/harness", "harness:generated": "npm run check:generated -w @qoder-ai/harness", "harness:test": "npm test -w @qoder-ai/harness", + "harness-ui:build": "npm run build -w @qoder-ai/harness-ui", + "harness-ui:test": "npm test -w @qoder-ai/harness-ui", + "harness-studio:build": "npm run build -w @qoder-ai/harness-studio", + "harness-studio:test": "npm test -w @qoder-ai/harness-studio", + "harness-studio:test:browser": "npm run test:browser -w @qoder-ai/harness-studio", "inspector": "node scripts/harness-inspector/cli.mjs render", "inspector:open": "node scripts/harness-inspector/cli.mjs render --open", "pack:verify": "node scripts/npm-package/verify-pack.mjs", diff --git a/packages/harness-studio/LICENSE b/packages/harness-studio/LICENSE new file mode 100644 index 0000000..e5b5c28 --- /dev/null +++ b/packages/harness-studio/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Qoder + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/harness-studio/README.md b/packages/harness-studio/README.md new file mode 100644 index 0000000..23252f3 --- /dev/null +++ b/packages/harness-studio/README.md @@ -0,0 +1,66 @@ +# @qoder-ai/harness-studio + +A local React studio for [`@qoder-ai/harness`](../harness/README.md): one UI +over the two evidence surfaces the DSL toolchain produces. + +- **Run view** — drives a live harness run over the AG-UI protocol served by + [`@qoder-ai/harness-ui`](../harness-ui/README.md) (embedded under `/agui`), + rendering streamed assistant messages, warnings, and workbench-style + expandable tool cards with arguments, retained results, execution state, + failed/result-unavailable states, bounded-result truncation evidence, and the + final run result. +- **Compare view** — loads a `harness-compare.v1` evidence directory and + renders the frozen `verdict.json`: per-variant pass rate, mean score, cost, + and per-trial outcomes. + +The existing static outputs (the zero-dependency harness inspector HTML and +the compare `verdict.html`) stay authoritative and offline-friendly; the +studio reads the same evidence and adds interactivity on top. + +## Usage + +```sh +# Compare evidence only +npx @qoder-ai/harness-studio --evidence ./harness-readme-compare-evidence + +# Live runs only +npx @qoder-ai/harness-studio --harness my-agent.harness + +# Both surfaces on one port +npx @qoder-ai/harness-studio --harness my-agent.harness --evidence ./evidence +``` + +Then open the printed URL (default `http://127.0.0.1:3311`). The server binds +to loopback; live runs execute through the same v0.2 executors and redaction +rules as the core package. The embedded run endpoint accepts same-origin JSON +browser requests only; use the standalone `@qoder-ai/harness-ui` server with +an explicit `--allow-origin` when the frontend is hosted on another origin. + +## Architecture + +```text +dist/app/ esbuild-bundled React app (index.html + assets/app.js) +src/app/ components plus pure state modules: + agui-store.ts AG-UI event → run view state reducer + compare-model.ts verdict.json → table model + sse-client.ts incremental SSE frame parser +src/server/ static host + /api/config + /api/evidence + embedded /agui +``` + +The pure modules are the tested seam; the React components are direct renders +of their outputs. + +## Development + +```sh +npm run harness-studio:build # tsc + esbuild-wasm bundle +npm run harness-studio:test +npm run harness-studio:test:browser # built-app Playwright interaction +``` + +Publication is repository-owned: select `harness-studio` in the protected +GitHub Actions `Publish npm` workflow. Local commands only build, test, pack, +or dry-run; do not publish this workspace from a developer machine. + +See the spec: +[Harness UI and Studio](https://github.com/QoderAI/better-harness/blob/main/docs/specs/2026-08-15-harness-ui-studio.md). diff --git a/packages/harness-studio/package.json b/packages/harness-studio/package.json new file mode 100644 index 0000000..b73b4b5 --- /dev/null +++ b/packages/harness-studio/package.json @@ -0,0 +1,58 @@ +{ + "name": "@qoder-ai/harness-studio", + "version": "0.1.0", + "description": "React studio for @qoder-ai/harness: live AG-UI run view plus harness-compare evidence explorer.", + "license": "MIT", + "type": "module", + "engines": { + "node": ">=22.20.0 <25.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/QoderAI/better-harness.git", + "directory": "packages/harness-studio" + }, + "homepage": "https://github.com/QoderAI/better-harness/tree/main/packages/harness-studio#readme", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "harness-studio": "./dist/server/cli.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist/", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "npm run clean && tsc && node scripts/build-app.mjs", + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "prepack": "npm run build && npm test", + "test": "vitest run", + "test:browser": "playwright test", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@qoder-ai/harness": "0.1.0", + "@qoder-ai/harness-ui": "0.1.0", + "react": "19.2.8", + "react-dom": "19.2.8" + }, + "devDependencies": { + "@playwright/test": "^1.62.1", + "@types/node": "26.2.0", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.4", + "esbuild-wasm": "0.28.1", + "typescript": "7.0.2", + "vitest": "4.1.10" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/harness-studio/playwright.config.mjs b/packages/harness-studio/playwright.config.mjs new file mode 100644 index 0000000..f546f22 --- /dev/null +++ b/packages/harness-studio/playwright.config.mjs @@ -0,0 +1,15 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./test/browser", + timeout: 30_000, + fullyParallel: false, + workers: 1, + reporter: "list", + use: { + browserName: "chromium", + headless: true, + viewport: { width: 390, height: 844 }, + screenshot: "only-on-failure", + }, +}); diff --git a/packages/harness-studio/scripts/build-app.mjs b/packages/harness-studio/scripts/build-app.mjs new file mode 100644 index 0000000..67c65c1 --- /dev/null +++ b/packages/harness-studio/scripts/build-app.mjs @@ -0,0 +1,26 @@ +// Bundle the React app with the repo-conventional esbuild-wasm toolchain. +import { copyFile, mkdir } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { build } from "esbuild-wasm"; + +const packageRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const appDir = join(packageRoot, "dist", "app"); + +await mkdir(join(appDir, "assets"), { recursive: true }); +await build({ + entryPoints: [join(packageRoot, "src", "app", "main.tsx")], + outfile: join(appDir, "assets", "app.js"), + bundle: true, + format: "esm", + platform: "browser", + target: "es2022", + jsx: "automatic", + minify: true, + sourcemap: true, + define: { "process.env.NODE_ENV": '"production"' }, + logLevel: "warning", +}); +await copyFile(join(packageRoot, "src", "app", "index.html"), join(appDir, "index.html")); +process.stdout.write(`Built studio app into ${appDir}\n`); +process.exit(0); diff --git a/packages/harness-studio/src/app/App.tsx b/packages/harness-studio/src/app/App.tsx new file mode 100644 index 0000000..3454832 --- /dev/null +++ b/packages/harness-studio/src/app/App.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from "react"; +import { CompareView } from "./CompareView.js"; +import { RunView } from "./RunView.js"; + +interface StudioConfig { + aguiEnabled: boolean; + evidenceEnabled: boolean; +} + +type Tab = "run" | "compare"; + +export function App(): React.JSX.Element { + const [config, setConfig] = useState(undefined); + const [tab, setTab] = useState("run"); + + useEffect(() => { + let cancelled = false; + void (async () => { + try { + const response = await fetch("api/config"); + const loaded = (await response.json()) as StudioConfig; + if (!cancelled) { + setConfig(loaded); + setTab(loaded.aguiEnabled ? "run" : "compare"); + } + } catch { + if (!cancelled) { + setConfig({ aguiEnabled: false, evidenceEnabled: false }); + } + } + })(); + return () => { + cancelled = true; + }; + }, []); + + if (config === undefined) { + return

Loading studio…

; + } + return ( +
+
+

Harness Studio

+ +
+ {tab === "run" ? ( + config.aguiEnabled ? ( + + ) : ( +

+ No harness loaded. Start the studio with --harness <file.harness> to + enable live runs. +

+ ) + ) : ( + + )} +
+ ); +} diff --git a/packages/harness-studio/src/app/CompareView.tsx b/packages/harness-studio/src/app/CompareView.tsx new file mode 100644 index 0000000..4c5a15b --- /dev/null +++ b/packages/harness-studio/src/app/CompareView.tsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from "react"; +import { parseVerdict, summarizeVerdict, type CompareSummary } from "./compare-model.js"; + +type LoadState = + | { phase: "loading" } + | { phase: "missing"; detail: string } + | { phase: "ready"; summary: CompareSummary }; + +export function CompareView(): React.JSX.Element { + const [state, setState] = useState({ phase: "loading" }); + + useEffect(() => { + let cancelled = false; + void (async () => { + try { + const response = await fetch("api/evidence"); + if (!response.ok) { + const payload = (await response.json().catch(() => ({}))) as { error?: string }; + throw new Error(payload.error ?? `Evidence request failed (${response.status}).`); + } + const summary = summarizeVerdict(parseVerdict(await response.json())); + if (!cancelled) { + setState({ phase: "ready", summary }); + } + } catch (error) { + if (!cancelled) { + setState({ phase: "missing", detail: error instanceof Error ? error.message : String(error) }); + } + } + })(); + return () => { + cancelled = true; + }; + }, []); + + if (state.phase === "loading") { + return

Loading compare evidence…

; + } + if (state.phase === "missing") { + return ( +
+

No compare evidence loaded: {state.detail}

+

+ Produce one with harness-compare run <experiment.json> --out <dir>{" "} + and start the studio with --evidence <dir>. +

+
+ ); + } + const { summary } = state; + return ( +
+

+ {summary.status}: {summary.reason} +

+
+ + + + + + + + + + + + + + {summary.rows.map((row) => ( + + + + + + + + + + ))} + +
VariantPassedPass rateMean scoreInfra errorsCost (USD)Credits
{row.label}{row.passedTrials}/{row.completedTrials}{(row.passRate * 100).toFixed(0)}%{row.meanScore}{row.infrastructureErrors}{row.totalCostUsd.toFixed(4)}{row.totalCredits.toFixed(3)}
+
+

Trials

+
+ + + + + + + + + + + + + + {summary.trials.map((trial) => ( + + + + + + + + + + ))} + +
Variant#HarnessProfileOutcomeDurationChanged files
{trial.variant}{trial.trial}{trial.harnessId}{trial.runtimeProfile}{trial.classification}{(trial.durationMs / 1000).toFixed(1)}s{trial.changedFiles.join(", ") || "—"}
+
+

Manifest {summary.manifestHash}

+
+ ); +} diff --git a/packages/harness-studio/src/app/RunView.tsx b/packages/harness-studio/src/app/RunView.tsx new file mode 100644 index 0000000..c403f57 --- /dev/null +++ b/packages/harness-studio/src/app/RunView.tsx @@ -0,0 +1,199 @@ +import { memo, useCallback, useMemo, useRef, useState } from "react"; +import type { AguiEvent } from "@qoder-ai/harness-ui"; +import { applyAguiEvent, initialRunState, type AguiRunState, type TimelineItem } from "./agui-store.js"; +import { createSseParser } from "./sse-client.js"; +import { describeToolPayload } from "./tool-call-model.js"; + +/** Post one AG-UI run and fold the SSE stream into state updates. */ +async function streamRun( + endpoint: string, + prompt: string, + onUpdate: (updater: (state: AguiRunState) => AguiRunState) => void, +): Promise { + const response = await fetch(endpoint, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + threadId: `thread_${Date.now()}`, + runId: `run_${Date.now()}`, + messages: [{ id: "m1", role: "user", content: prompt }], + }), + }); + if (!response.ok || response.body === null) { + const detail = await response.text().catch(() => ""); + throw new Error(`Run request failed (${response.status}): ${detail}`); + } + const apply = (event: AguiEvent): void => onUpdate((state) => applyAguiEvent(state, event)); + const parser = createSseParser(apply); + const decoder = new TextDecoder(); + const reader = response.body.getReader(); + for (;;) { + const { done, value } = await reader.read(); + if (done) { + break; + } + parser.push(decoder.decode(value, { stream: true })); + } + parser.push(decoder.decode()); + parser.end(); +} + +type MessageTimelineItem = Extract; +type ToolCallTimelineItem = Extract; + +const MessageEntry = memo(function MessageEntry({ item }: { item: MessageTimelineItem }): React.JSX.Element { + return ( +
+ Assistant +
{item.text}{item.complete ? "" : " ▌"}
+
+ ); +}); + +const ToolCallEntry = memo(function ToolCallEntry({ item }: { item: ToolCallTimelineItem }): React.JSX.Element { + const argumentsView = useMemo( + () => describeToolPayload(item.argsText, "No arguments retained"), + [item.argsText], + ); + const resultView = useMemo( + () => item.resultText === undefined + ? undefined + : describeToolPayload(item.resultText, "Empty result"), + [item.resultText], + ); + return ( +
+ + + + Tool call + {item.name} + {argumentsView.summary} + + {toolStatusLabel(item.status)} + + +
+
+

Arguments

+
{argumentsView.formatted}
+
+
+

Result

+ {resultView ? ( + <> + {item.resultTruncated ? ( +

+ Result truncated{item.resultOriginalBytes === undefined + ? "" + : ` from ${item.resultOriginalBytes.toLocaleString()} bytes`}. +

+ ) : null} +
{resultView.formatted}
+ + ) : ( +

+ {item.status === "running" || item.status === "preparing" + ? "Waiting for the tool result…" + : item.status === "result-unavailable" + ? "The run finished without a retained result payload." + : "No result payload was retained."} +

+ )} +
+
+ Call ID + {item.id} +
+
+
+ ); +}); + +const TimelineEntry = memo(function TimelineEntry({ item }: { item: TimelineItem }): React.JSX.Element { + return item.kind === "message" ? : ; +}); + +function toolStatusLabel(status: ToolCallTimelineItem["status"]): string { + switch (status) { + case "preparing": return "Preparing"; + case "running": return "Running"; + case "completed": return "Completed"; + case "failed": return "Failed"; + case "result-unavailable": return "Result unavailable"; + case "interrupted": return "Interrupted"; + } +} + +export function RunView({ aguiEndpoint }: { aguiEndpoint: string }): React.JSX.Element { + const [prompt, setPrompt] = useState(""); + const [state, setState] = useState(initialRunState); + const busy = useRef(false); + const toolCount = state.timeline.filter((item) => item.kind === "tool-call").length; + + const start = useCallback(async () => { + if (busy.current || prompt.trim().length === 0) { + return; + } + busy.current = true; + setState({ ...initialRunState(), status: "running" }); + try { + await streamRun(aguiEndpoint, prompt, setState); + } catch (error) { + setState((previous) => ({ + ...previous, + status: "error", + error: error instanceof Error ? error.message : String(error), + })); + } finally { + busy.current = false; + } + }, [aguiEndpoint, prompt]); + + return ( +
+
+