diff --git a/docs/adapters/contributing-new-coding-agent.md b/docs/adapters/contributing-new-coding-agent.md index e7352670..ee44d9d3 100644 --- a/docs/adapters/contributing-new-coding-agent.md +++ b/docs/adapters/contributing-new-coding-agent.md @@ -232,7 +232,7 @@ been verified. Use a host-specific adapter page only when the matrix's split triggers are met. Before commit or review, use the -[Change Traceability Review](../../.agents/skills/change-traceability-review/SKILL.md) +[Change Traceability Review](https://github.com/QoderAI/better-harness/blob/main/.agents/skills/change-traceability-review/SKILL.md) in Review Readiness Check mode. The pull request should state: - the host/version and primary contract evidence; @@ -242,7 +242,7 @@ in Review Readiness Check mode. The pull request should state: - privacy, compatibility, generated-file, rollback, and residual risks; - AI involvement and the human verification performed. -Use the repository [pull request template](../../.github/pull_request_template.md). +Use the repository [pull request template](https://github.com/QoderAI/better-harness/blob/main/.github/pull_request_template.md). Do not infer Story ids, AI involvement, CI status, or native compatibility from branch names, prose, passing synthetic tests, or similarity to another host. diff --git a/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md b/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md new file mode 100644 index 00000000..67f654ac --- /dev/null +++ b/docs/specs/2026-08-19-67-antigravity-plugin-artifact.md @@ -0,0 +1,352 @@ +# Package Better Harness for Antigravity + +## Traceability + +- Spec ID: antigravity-plugin-artifact +- Story: #67 +- Status: Implemented +- Scope: implemented static buildable and verifiable Antigravity plugin artifact + slice; native Antigravity integration and support remain unverified. + +## Intent + +Implement the smallest static source, build, verification, and publication slice +that produces a self-contained Better Harness plugin-shaped artifact for +Antigravity. The implementation does not establish native Antigravity install, +discovery, selection, invocation, compatibility, distribution, or first-class +support. + +The work follows the maintainer-authorized direction in +[Issue #67](https://github.com/QoderAI/better-harness/issues/67). The issue +conversation records phodal, a repository MEMBER, responding "Welcome to PR". +That comment authorizes a reviewable contribution; it is not a merge, release, +compatibility, Marketplace, or support commitment. + +## Pinned Evidence + +- The implementation base is Better Harness commit + [`842a6b5070d401f983693d4c4d6a83b7ad1841c6`](https://github.com/QoderAI/better-harness/tree/842a6b5070d401f983693d4c4d6a83b7ad1841c6). +- The pinned [architecture owner](https://github.com/QoderAI/better-harness/blob/842a6b5070d401f983693d4c4d6a83b7ad1841c6/docs/ARCHITECTURE.md) + places source-local artifact assembly under `scripts/packaging/` and treats + generated artifacts as outputs rather than canonical product owners. +- The existing [generic host builder](https://github.com/QoderAI/better-harness/blob/842a6b5070d401f983693d4c4d6a83b7ad1841c6/scripts/packaging/build-host-plugin.mjs), + [generic verifier](https://github.com/QoderAI/better-harness/blob/842a6b5070d401f983693d4c4d6a83b7ad1841c6/scripts/packaging/verify-host-plugin.mjs), + and [Codex artifact tests](https://github.com/QoderAI/better-harness/blob/842a6b5070d401f983693d4c4d6a83b7ad1841c6/test/plugins/host-plugin-artifact.test.mjs) + remain separate owners and regression evidence. +- The target-specific authority is the official + [Antigravity CLI Plugins documentation](https://www.antigravity.google/docs/cli/plugins), + observed 2026-08-20 under Antigravity CLI v1.1.14. Its full manifest schema + requires string `name`, permits optional string `description`, and closes + additional properties. The page's pattern is implemented as + `^[A-Za-z0-9_-]+$`, following its stated alphanumeric, hyphen, and underscore + intent without treating `9-_` as a character range. It also documents + `agy plugin install`; that command was not run locally. +- The official [Antigravity Plugins documentation](https://www.antigravity.google/docs/plugins), + observed 2026-08-19 under generic Antigravity 2.0 v2.8.1, supplies shared + plugin-root and Skill-layout context. Its optional-name behavior is not the + contract for the Agy CLI v1.1.14 profile implemented here. +- The official [Antigravity Skills documentation](https://www.antigravity.google/docs/skills), + observed on the same date and generic documentation version, confirms that a + Skill is a folder containing `SKILL.md`. The CLI page shows a plugin `skills/` + directory but does not independently prove discovery or invocation of this + artifact's nested Skill in a native CLI build. + +The official pages are live rather than commit-addressed. Their observation +date and displayed documentation version bound the facts consumed here; static +documentation does not prove behavior in a selected IDE or CLI build. + +## Official Contract Boundary + +The Agy CLI v1.1.14 profile relies on these target-specific facts: + +1. A plugin root contains required `plugin.json`. +2. The manifest is an object closed to `name` and optional `description`. +3. `name` is a required string using only ASCII alphanumerics, hyphens, and + underscores, enforced as `^[A-Za-z0-9_-]+$`. +4. `description`, when present, is a string; an empty string remains valid. +5. `agy plugin install` is a documented command, not evidence that installation + or invocation succeeded in this environment. + +The generic 2.0 pages supply shared layout evidence only. Their optional-name +default is deliberately excluded from this CLI profile. The CLI page includes +an example `$schema` editor hint, while its listed full JSON Schema closes +properties to `name` and `description`; this frozen verifier follows the listed +schema and rejects `$schema` as profile drift. Static conformance does not prove +native install, Skill discovery, selection, invocation, or compatibility. + +## Implemented Surfaces and Ownership + +| Surface | Owner | Implemented contract | +| --- | --- | --- | +| `scripts/packaging/antigravity/plugin-manifest.json` | Agy CLI packaging source | Exact source value `{ "name": "better-harness" }`; a valid name-only subset of the official CLI schema, source-only, and never an install root. | +| `scripts/packaging/antigravity/build-antigravity-plugin.mjs` | Antigravity artifact builder | Allowlisted assembly, dependency copy, staged verification, canonical boundary checks, and transactional publication. | +| `scripts/packaging/antigravity/verify-antigravity-plugin.mjs` | Antigravity artifact verifier | Closed tree, identity, marker, package, Markdown/runtime closure, license, bounds, and runtime smoke validation. | +| `test/plugins/antigravity-plugin-artifact.test.mjs` | Artifact conformance evidence | Positive build/replace/run coverage and adversarial identity, parser, path, race, rollback, and cleanup cases. | +| `skills/better-harness/**` and Better Harness runtime | Existing canonical capability owners | Copied and validated without redirecting, renaming, or forking product judgment. | +| Native Antigravity behavior | Antigravity | Unobserved; no approved `agy` binary or selected IDE/CLI build was exercised. | + +Canonical sources feed the generated host artifact in one direction. Generated +files never become canonical input and never write back to the repository. + +The verifier requires an own string `name` matching the CLI pattern; absence, +blank or non-string values, invalid characters, and unknown properties fail +without a basename fallback. It accepts optional string `description` because +that field is valid in the official CLI schema, while the owned source and +builder deliberately produce the narrower exact name-only subset. Manifest +name, root basename, ownership marker, and package identity must all resolve to +the Better Harness artifact contract. + +## Implemented Artifact Profile + +The generated root basename is `better-harness`. Its positive allowlist is: + +- root `plugin.json`, `.antigravity-plugin-artifact.json`, and private + `package.json`; +- `README.md`, `AGENTS.md`, `DESIGN.md`, `LICENSE`, `CHANGELOG.md`, + `CODE_OF_CONDUCT.md`, and `CONTRIBUTING.md`; +- canonical `skills/better-harness/**` only; +- `scripts/**` except `scripts/packaging/**`; +- `references/**`, `templates/**`, `models/**`, `hooks/**`, `docs/**`, and + `case-studies/**`; +- `node_modules/@vscode/tree-sitter-wasm/**` and + `node_modules/esbuild-wasm/**`, including package metadata and licenses. + +The builder copies only regular files and directories reached without following +symbolic links. The verifier rejects unknown roots, other Skills and host +shells, development/test state, caches, environment files, special files, +symlinks or junctions, traversal, canonical escape, and case or Unicode +identity collisions. + +The generated root `package.json` is a closed Better Harness schema with exactly +these keys: `name`, `version`, `private`, `license`, `type`, `bin`, `engines`, +and `dependencies`. It binds: + +- `name` to `@qoder-ai/better-harness`, `private` to `true`, and `type` to + `module`; +- `bin` to the singleton `{ "better-harness": "scripts/better-harness.mjs" }`; +- `engines` to exact nonblank `node` and `npm` entries projected from source; +- `dependencies` to exact version-bound `@vscode/tree-sitter-wasm` and + `esbuild-wasm` entries. + +The builder projects fresh nested objects and rejects missing, extra, blank, +non-string, or wrong source runtime metadata rather than aliasing source +objects into the artifact. + +## Canonical Closure and Link Integrity + +The canonical artifact entry is unchanged at +`skills/better-harness/SKILL.md`; there is no wrapper, flat Skill, redirect, +generated mirror, or copied business-logic owner. `DESIGN.md` is required +because shipped `AGENTS.md` links to it as an offline dependency. + +Every relative local Markdown link is an artifact hard dependency. It must +resolve uniquely to an allowlisted regular file inside the canonical root. +Repository-only navigation uses absolute upstream HTTPS URLs and is not copied +or traversed. `pathname:` is treated only as a non-local Docusaurus route; +`file:` and unknown schemes fail closed. + +The verified pinned artifact contains a Markdown closure of exactly 93 nodes, +290 edges, and 96 files. Runtime analysis starts at +`scripts/better-harness.mjs` and proves a syntax-aware ESM closure of 19 modules +and 39 edges, plus the exact two packaged dependencies and their licenses. +Limits bound files, bytes, depth, nodes, and edges. + +Three target-only source-integrity repair groups make that shipped closure +self-contained without changing product behavior: + +1. repository-only governance links in + `docs/adapters/contributing-new-coding-agent.md` use upstream HTTPS targets; +2. Codex and Qoder Custom Agent Review links resolve to the shared parent + reference; +3. the observability instructions link to the canonical Agent instructions + review reference. + +Groups 2 and 3 correct links that were already broken in the repository. They +survived because `classify` in `scripts/doc-link-graph/cli.mjs` cannot tell a +slash-free `custom-agents-review.md` (a real sibling one directory up) from +`AGENTS.md` (a file convention in a repository under analysis), so it checks +neither. `test/skills-docs/doc-link-graph.test.mjs` now checks slash-free +targets written as link syntax, excluding the convention names, and checks that +repository URLs at a mutable ref still resolve — the guarantee group 1 gives up +by leaving the artifact boundary. + +## Build, Boundary, and Publication Contract + +`build-antigravity-plugin.mjs` stages into a unique sibling container, assembles +only allowlisted inputs, writes the manifest and ownership marker, invokes the +full verifier, and publishes only after staged verification succeeds. + +Output boundary validation is read-only. It rejects invalid basenames, roots, +lexical repository overlap, and canonical overlap by resolving the nearest +existing ancestor and projecting missing suffixes. The first parent creation is +pinned to the approved preflight `canonicalParent`; the builder then rechecks +the original lexical output. Only a successful second check authorizes staging +under the post-check canonical parent and all destination operations against +the frozen post-check `canonicalOutput`. This prevents a parent symlink or +junction retarget from redirecting writes into the repository. + +An existing destination is only an early ownership gate. At replacement time, +the builder atomically moves the actual live destination into a unique sibling +backup container at `better-harness`, runs the full verifier on that moved tree, +and publishes the stage only if revalidation succeeds. Concurrent unowned, +corrupt, or symlink swaps are restored without publication; rollback conflicts +retain the moved tree rather than deleting it. + +Publication reports stable states: + +| Outcome | State and observability | +| --- | --- | +| New or clean replacement publication | `state="published"`, `backupCleanup="complete"`. | +| New artifact live but old backup removal failed | API resolves with `state="published"`, `backupCleanup="pending"`, warning `backup-cleanup-pending`; JSON and human CLI remain successful and expose no absolute path. | +| Destination changed and was restored | `destination-changed`, `not-published-destination-restored`. | +| Changed destination rollback conflicted | `destination-revalidation-rollback-failed`, `not-published-backup-retained`. | +| Stage publish failed and verified prior artifact was restored | `publish-failed`, `not-published-destination-restored`. | +| Stage publish rollback failed | `publish-rollback-failed`, `not-published-verified-backup-retained`. | +| Initial destination-to-backup rename failed | `publish-backup-rename-failed`, `not-published-destination-unchanged`, with bounded backup-container cleanup status. | + +Errors and warnings expose only stable codes, states, and safe backup basenames; +they do not disclose absolute private paths or claim that a partially cleaned +backup remains complete or recoverable. + +## Acceptance Scenarios + +- **AC-1 (traceability):** The implementation links Issue #67, MEMBER + contribution authorization, pinned base, implementation paths, and the + target Agy CLI v1.1.14 page plus generic layout pages with their observation + dates and distinct authority boundaries. +- **AC-2 (source manifest):** The source manifest parses to exactly + `{ "name": "better-harness" }`, a valid name-only subset of the closed CLI + schema; no optional field or native behavior is claimed by the source. +- **AC-3 (artifact root):** The builder writes the source manifest as root + `plugin.json`, the verifier requires its closed CLI `name`/`description` + schema with required patterned name, and the source directory is never an + install root. +- **AC-4 (canonical Skill):** The artifact contains only + `skills/better-harness/**` under `skills/` and preserves the canonical + `SKILL.md` without redirect or duplicate implementation. +- **AC-5 (allowlist):** Positive builds contain only the root files, runtime + roots, canonical Skill, and two dependency roots in the implemented profile. +- **AC-6 (closed exclusions):** Unknown, development, host-shell, special-file, + link, collision, traversal, and canonical-escape cases fail closed. +- **AC-7 (Markdown closure):** Verification proves the complete recursive + 93-node, 290-edge, 96-file Markdown closure and rejects missing, escaping, + ambiguous, cyclic-over-limit, and unsupported-scheme targets. +- **AC-8 (runtime closure):** Verification proves the 19-module ESM runtime + closure, exact dependencies and licenses, bounded parsing, and artifact-local + Better Harness CLI `--help` smoke before publication. +- **AC-9 (identity and replacement):** Required patterned CLI manifest name, + optional string description, root basename, ownership marker, closed package + identity, host, and schema are bound without absent-name fallback; the actual + moved destination passes full revalidation before replacement. +- **AC-10 (atomicity and concurrency):** Pre/post canonical boundary checks, + concurrent swap tests, bounded rollback states, and truthful cleanup-pending + success warnings preserve user data without accepting a partial artifact. +- **AC-11 (regression):** The generic Codex builder/verifier remain unchanged; + their focused artifact tests pass with the Antigravity suite. +- **AC-12 (product honesty):** README, support and adapter matrices, host + catalogs, root CLI, package scripts, release metadata, and generated graph + content are not widened; no native or first-class support is claimed. + +## Implemented Negative Evidence + +| Area | Covered evidence | +| --- | --- | +| Manifest and identity | Missing/malformed/non-object manifest; required-name absence, blank/non-string/pattern-invalid/wrong name; non-string description; `unexpected`, `$schema`, and other extra fields; exact name-only source; closed marker and package schemas. | +| Filesystem safety | Invalid output, lexical/canonical overlap, missing-parent zero-write rejection, pre/post junction retarget, symlink/junction/special file, traversal, collision, and bounds. | +| Closure | Missing/escaping/transitive Markdown targets, cycles and limits, syntax-authoritative ESM parsing, missing runtime modules, forbidden packaging imports, and unexpected dependencies. | +| Dependencies and licenses | Missing/wrong dependency metadata, versions, roots, imported subpaths, licenses, and non-regular inputs. | +| Ownership and publication | Unowned/corrupt destination, real moved-tree revalidation, concurrent swaps, first backup rename fault, stage rename fault, rollback conflict, and backup cleanup fault. | +| Product boundary | Frozen generic/package/product surfaces and no native-support assertion without an approved `agy` smoke. | + +## Non-goals + +- Install or invoke Antigravity, mutate a native plugin home, inspect private + host state, or select an Antigravity IDE/CLI version. +- Claim native install, discovery, selection, invocation, uninstall, + compatibility, lifecycle, configured assets, or first-class support. +- Add an adapter, session/evidence integration, host catalog entry, support + matrix row, README installation path, root CLI route, package script, + Marketplace publication, release metadata, or native install receipt. +- Change the generic builder/verifier, existing host shells, package or lock + files, generated documentation graph content, or canonical product judgment. +- Infer manifest fields beyond the target CLI's closed `name`/`description` + schema or apply the generic 2.0 optional-name default to this profile. + +## Plan and Tasks + +1. **Gate 1 - source contract (complete):** Establish the exact source manifest, + official-fact boundary, ownership, allowlist, closure, and product non-goals. +2. **Gate 2 - static build and verification (complete):** Implement the + Antigravity-specific builder, verifier, artifact tests, source link-integrity + repairs, canonical boundary checks, and transactional publication states. +3. **Gate 3 - static conformance and review readiness (complete):** Run focused, + Codex regression, doc-link, real build/replace/verify/Better CLI smoke, + package, full-repository baseline, graph, scope, and privacy checks. + +Native evidence is a later, independent, maintainer-approved contribution. It +is not a completion condition for this static artifact spec and must not widen +support claims without a selected native build and explicit review. + +## Risks and Controls + +| Residual risk | Control and current evidence | +| --- | --- | +| No approved native binary or IDE/CLI build was tested | Keep native compatibility and support unverified; require isolated install/discovery/invocation/uninstall evidence in a separate contribution. | +| Windows lacks privileges for some symlink/junction fixtures | Capability-gated focused tests skip rather than weaken production checks; independent non-link race and parser cases still run. | +| Live CLI/generic docs, upstream `main`, Issue, and PR state can drift | Bind CLI authority to v1.1.14 observed 2026-08-20, generic layout facts to 2.0 v2.8.1 observed 2026-08-19, and implementation facts to the pinned base; recheck immediately before PR creation. | +| Upstream absolute URLs do not follow forks or topic branches | Use them only for source-only governance/navigation dependencies; keep offline artifact dependencies relative and verified. | +| Handwritten syntax-aware Markdown and ESM parsers can miss new syntax | Bound traversal, fail closed on unsupported forms, assert adversarial syntax cases, and pin real closure cardinalities. | +| Static conformance may be mistaken for native support | Keep README, matrices, catalogs, native homes, Marketplace, and release surfaces unchanged. | + +## Open Questions + +- [NEEDS CLARIFICATION: Should a future native smoke install Antigravity CLI + v1.1.14 exactly or a maintainer-selected successor, and what compatibility + drift must be accepted?] +- [NEEDS CLARIFICATION: How does the selected native CLI discover and invoke + the generic-layout `skills/better-harness/SKILL.md`, which the CLI page does + not independently prove for this artifact?] +- [NEEDS CLARIFICATION: Which isolated native install, discovery, selection, + invocation, uninstall, and configured-asset evidence must pass before a + first-class support decision?] + +These questions block native-support activation, not completion of the static +buildable and verifiable artifact slice. + +## Test and Review Evidence + +- Antigravity artifact focused plus existing Codex artifact and doc-link tests: + 52 tests, 49 passed, 0 failed, and 3 Windows capability-gated skips, using the + installed offline Vitest 4.1.10 runner with the issue worktree as root. +- Real unique-output sequence: build JSON, verify JSON, artifact-local Better + Harness CLI `--help`, repeat replacement, and second verification all passed; + publication states were `published`, replacement changed from false to true, + and sibling stage/backup residual count was zero. This is a Better Harness + runtime smoke, not an `agy` native smoke. +- Full repository `npm test`, executed with the same offline runner/config: + 100 files total, 94 passed and 6 failed; 1,448 tests total, 1,420 passed, + 7 failed, and 21 skipped. Six failures were Windows `EPERM` symlink fixtures; + one governance reporter fixture expected a worktree-local + `node_modules/vitest/vitest.mjs` that the isolated worktree did not contain. + The full repository gate is therefore not green. +- `npm run pack:verify` passed after redirecting npm's cache from the denied + user cache to a writable task-local cache: npm package 546 entries and runtime + zip 575 entries. +- Builder and verifier `node --check` and direct `--help` passed. Source + manifest exactness, the three source link-integrity groups, generated package + schema, closure cardinalities, JSON output, and path redaction were inspected + through behavior tests and the real artifact. +- CLI manifest behavior accepts required valid `name` with absent, empty, or + nonempty string `description`; it rejects absent, blank, non-string, + pattern-invalid, or wrong names, non-string descriptions, malformed/non-object + JSON, and extra keys including `unexpected` and `$schema`. +- The canonical doc-link generator was run once after this reconciliation; + graph content hash remained equal to `HEAD`, with quiet content diff and zero + raw/numstat changes. The worktree may still show a same-blob line-ending `.M`. +- `git diff --check`, changed-surface privacy/absolute-local-path scan, and + status scope inspection passed. No dependency installation, native host + mutation, commit, push, or pull request occurred during implementation. + +The local environment has no approved `agy` executable. Native Antigravity +install, discovery, selection, invocation, uninstall, configured assets, IDE or +CLI compatibility, and Marketplace behavior remain unobserved. diff --git a/references/agent-customize/platforms/codex.md b/references/agent-customize/platforms/codex.md index aa7ced45..e031317b 100644 --- a/references/agent-customize/platforms/codex.md +++ b/references/agent-customize/platforms/codex.md @@ -137,7 +137,7 @@ when concurrent threads could edit the same files. Use subagents for bounded exploration, testing, triage, or independent review while the main thread owns the final decision and implementation. -Use [Custom Agent Review](custom-agents-review.md) when configured Agent +Use [Custom Agent Review](../custom-agents-review.md) when configured Agent profiles, their descriptions, prompts, tool boundaries, or inventory count need quality review. diff --git a/references/agent-customize/platforms/qoder.md b/references/agent-customize/platforms/qoder.md index 453d4077..18e101e9 100644 --- a/references/agent-customize/platforms/qoder.md +++ b/references/agent-customize/platforms/qoder.md @@ -206,7 +206,7 @@ Official scenario examples: > Official documentation: https://docs.qoder.com/extensions/subagent -Use [Custom Agent Review](custom-agents-review.md) for provider-neutral content +Use [Custom Agent Review](../custom-agents-review.md) for provider-neutral content quality, tool-boundary, count, and report-summary checks. This section owns the Qoder file format and invocation examples only. diff --git a/references/project-harness/observability.md b/references/project-harness/observability.md index a67dde60..fa084f34 100644 --- a/references/project-harness/observability.md +++ b/references/project-harness/observability.md @@ -93,7 +93,7 @@ entry/exit logs across every function. ## What Belongs in AGENTS.md -Load [agents-md-review.md](agents-md-review.md) when instructions are in scope. +Load [agents-md-review.md](../agent-customize/agents-md-review.md) when instructions are in scope. Keep only non-inferable facts: - actual logging facade and architecture link; diff --git a/scripts/packaging/antigravity/build-antigravity-plugin.mjs b/scripts/packaging/antigravity/build-antigravity-plugin.mjs new file mode 100644 index 00000000..92a1494e --- /dev/null +++ b/scripts/packaging/antigravity/build-antigravity-plugin.mjs @@ -0,0 +1,618 @@ +#!/usr/bin/env node + +import { createRequire } from "node:module"; +import { + copyFile, + lstat, + mkdir, + mkdtemp, + readFile, + realpath, + readdir, + rename, + rm, + writeFile, +} from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { randomUUID } from "node:crypto"; + +import { + ANTIGRAVITY_ARTIFACT_KIND, + ANTIGRAVITY_ARTIFACT_MARKER, + ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + ANTIGRAVITY_HOST, + ANTIGRAVITY_PLUGIN_NAME, + CANONICAL_SKILL, + RUNTIME_DEPENDENCIES, + RUNTIME_DEPENDENCY_LICENSES, + isAllowedArtifactPath, + validatePortablePathComponent, + verifyAntigravityPluginArtifact, +} from "./verify-antigravity-plugin.mjs"; + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); +const defaultRepoRoot = path.resolve(currentDir, "..", "..", ".."); +const SOURCE_MANIFEST = "scripts/packaging/antigravity/plugin-manifest.json"; +const ROOT_FILES = Object.freeze([ + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", +]); +const SOURCE_ROOTS = Object.freeze([ + "skills/better-harness", + "scripts", + "references", + "templates", + "models", + "hooks", + "docs", + "case-studies", +]); +const GENERATED_NAMES = new Set([ + ".DS_Store", + ".plugin-eval", + ".cache", + ".docusaurus", + ".vite", + "cache", + "coverage", + "dist", + "node_modules", + "outputs", + "temp", + "tmp", +]); + +export class AntigravityBuildError extends Error { + constructor(code, message, details = {}) { + super(message); + this.name = "AntigravityBuildError"; + this.code = code; + Object.assign(this, details); + } +} + +function fail(code, message, details) { + throw new AntigravityBuildError(code, message, details); +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isNonblankString(value) { + return typeof value === "string" && value.trim().length > 0; +} + +function isContained(candidate, parent) { + const relative = path.relative(parent, candidate); + return relative === "" || ( + relative !== ".." + && !relative.startsWith(`..${path.sep}`) + && !path.isAbsolute(relative) + ); +} + +async function readJsonObject(filePath, code) { + let value; + try { + value = JSON.parse(await readFile(filePath, "utf8")); + } catch { + fail(code, "Required source JSON is missing, unreadable, or malformed"); + } + if (!isObject(value)) fail(code, "Required source JSON must be an object"); + return value; +} + +async function requireRegularSource(filePath, canonicalRepoRoot, code) { + let stats; + try { + stats = await lstat(filePath); + } catch { + fail(code, "Required source file is missing or unreadable"); + } + if (stats.isSymbolicLink() || !stats.isFile()) fail(code, "Required source must be a regular file"); + const canonical = await realpath(filePath).catch(() => null); + if (!canonical || !isContained(canonical, canonicalRepoRoot)) { + fail(code, "Required source escapes the canonical repository root"); + } + return { stats, canonical }; +} + +function shouldSkipSource(relativePath) { + if (relativePath.startsWith("node_modules/")) return false; + if (relativePath === "scripts/packaging" || relativePath.startsWith("scripts/packaging/")) return true; + return relativePath.split("/").some((component) => ( + GENERATED_NAMES.has(component) + || component === ".env" + || component.startsWith(".env.") + || component.endsWith(".tmp") + || component.endsWith(".log") + )); +} + +async function copyRegularTree({ source, destination, relativePath, canonicalBoundary }) { + if (shouldSkipSource(relativePath)) return; + for (const component of relativePath.split("/")) validatePortablePathComponent(component); + // Name the entry. A dependency release that starts shipping a `dist/` or a + // stray `.log` fails the build here, and an error without the path leaves no + // way to tell which of a few hundred entries caused it. + if (!isAllowedArtifactPath(relativePath)) { + fail("source-path-forbidden", "Source path is outside the artifact allowlist", { path: relativePath }); + } + let stats; + try { + stats = await lstat(source); + } catch { + fail("source-entry-unreadable", "Allowlisted source entry is missing or unreadable", { path: relativePath }); + } + if (stats.isSymbolicLink()) fail("source-symlink-forbidden", "Source artifact input must not be a symbolic link", { path: relativePath }); + if (!stats.isDirectory() && !stats.isFile()) fail("source-special-file-forbidden", "Source artifact input must be regular", { path: relativePath }); + const canonical = await realpath(source).catch(() => null); + if (!canonical || !isContained(canonical, canonicalBoundary)) { + fail("source-path-escape", "Source artifact input escapes its canonical root", { path: relativePath }); + } + if (stats.isFile()) { + await mkdir(path.dirname(destination), { recursive: true }); + await copyFile(source, destination); + return; + } + await mkdir(destination, { recursive: true }); + const entries = await readdir(source, { withFileTypes: true }).catch(() => { + fail("source-directory-unreadable", "Allowlisted source directory cannot be read", { path: relativePath }); + }); + entries.sort((left, right) => left.name.localeCompare(right.name, "en")); + for (const entry of entries) { + const childRelative = `${relativePath}/${entry.name}`; + if (shouldSkipSource(childRelative)) continue; + await copyRegularTree({ + source: path.join(source, entry.name), + destination: path.join(destination, entry.name), + relativePath: childRelative, + canonicalBoundary, + }); + } +} + +export function artifactPackage(sourcePackage) { + const binKeys = isObject(sourcePackage.bin) ? Object.keys(sourcePackage.bin) : []; + const engineKeys = isObject(sourcePackage.engines) ? Object.keys(sourcePackage.engines).sort() : []; + if ( + sourcePackage.name !== "@qoder-ai/better-harness" + || !isNonblankString(sourcePackage.version) + || !isNonblankString(sourcePackage.license) + || sourcePackage.type !== "module" + || !isObject(sourcePackage.dependencies) + ) { + fail("source-package-invalid", "Source package identity or runtime metadata is invalid"); + } + if ( + binKeys.length !== 1 + || binKeys[0] !== "better-harness" + || sourcePackage.bin["better-harness"] !== "scripts/better-harness.mjs" + ) { + fail("source-bin-invalid", "Source package bin must match the artifact runtime entry"); + } + if ( + engineKeys.length !== 2 + || engineKeys[0] !== "node" + || engineKeys[1] !== "npm" + || !isNonblankString(sourcePackage.engines.node) + || !isNonblankString(sourcePackage.engines.npm) + ) { + fail("source-engines-invalid", "Source package engines must contain nonblank node and npm ranges"); + } + const dependencyKeys = Object.keys(sourcePackage.dependencies).sort(); + const expected = [...RUNTIME_DEPENDENCIES].sort(); + if ( + dependencyKeys.length !== expected.length + || dependencyKeys.some((dependency, index) => dependency !== expected[index]) + || RUNTIME_DEPENDENCIES.some((dependency) => !isNonblankString(sourcePackage.dependencies[dependency])) + ) { + fail("source-dependencies-invalid", "Source package dependencies do not match the artifact profile"); + } + return { + name: sourcePackage.name, + version: sourcePackage.version, + private: true, + license: sourcePackage.license, + type: sourcePackage.type, + bin: { "better-harness": sourcePackage.bin["better-harness"] }, + engines: { node: sourcePackage.engines.node, npm: sourcePackage.engines.npm }, + dependencies: Object.fromEntries(RUNTIME_DEPENDENCIES.map((dependency) => ( + [dependency, sourcePackage.dependencies[dependency]] + ))), + }; +} + +async function resolveDependencyRoot(repoRoot, dependency) { + const require = createRequire(path.join(repoRoot, "package.json")); + let packageJsonPath; + try { + packageJsonPath = require.resolve(`${dependency}/package.json`); + } catch { + fail("dependency-resolution-failed", "Required runtime dependency cannot be resolved"); + } + const packageRoot = path.dirname(packageJsonPath); + const canonicalRoot = await realpath(packageRoot).catch(() => null); + if (!canonicalRoot) fail("dependency-resolution-failed", "Runtime dependency root cannot be resolved"); + return { packageRoot, canonicalRoot }; +} + +async function copyDependency({ repoRoot, stageRoot, dependency, expectedVersion }) { + const { packageRoot, canonicalRoot } = await resolveDependencyRoot(repoRoot, dependency); + const metadata = await readJsonObject(path.join(packageRoot, "package.json"), "dependency-package-invalid"); + if (metadata.name !== dependency || metadata.version !== expectedVersion) { + fail("dependency-identity-invalid", "Resolved dependency identity or version does not match source package.json"); + } + await requireRegularSource( + path.join(packageRoot, RUNTIME_DEPENDENCY_LICENSES[dependency]), + canonicalRoot, + "dependency-license-invalid", + ); + await copyRegularTree({ + source: packageRoot, + destination: path.join(stageRoot, "node_modules", ...dependency.split("/")), + relativePath: `node_modules/${dependency}`, + canonicalBoundary: canonicalRoot, + }); +} + +export async function assertOutputBoundary(repoRoot, outputRoot) { + if ( + typeof outputRoot !== "string" + || !outputRoot + || outputRoot.includes("\0") + || path.basename(path.resolve(outputRoot)) !== ANTIGRAVITY_PLUGIN_NAME + ) { + fail("output-invalid", "Output must be a path whose basename is better-harness"); + } + const resolvedRepo = path.resolve(repoRoot); + const resolvedOutput = path.resolve(outputRoot); + const resolvedParent = path.dirname(resolvedOutput); + if (resolvedOutput === path.parse(resolvedOutput).root) fail("output-overlap", "Output must not be a filesystem root"); + if (isContained(resolvedOutput, resolvedRepo) || isContained(resolvedRepo, resolvedOutput)) { + fail("output-overlap", "Output must not overlap the repository source root"); + } + const canonicalRepo = await realpath(resolvedRepo).catch(() => null); + if (!canonicalRepo) fail("output-boundary-unavailable", "Output boundary cannot be resolved"); + + const missingComponents = []; + let existingAncestor = resolvedParent; + while (true) { + let stats; + try { + stats = await lstat(existingAncestor); + } catch (error) { + if (error?.code !== "ENOENT") { + fail("output-boundary-unavailable", "Output boundary cannot be resolved"); + } + const parent = path.dirname(existingAncestor); + if (parent === existingAncestor) { + fail("output-boundary-unavailable", "Output boundary cannot be resolved"); + } + missingComponents.unshift(path.basename(existingAncestor)); + existingAncestor = parent; + continue; + } + if (!stats.isDirectory() && !stats.isSymbolicLink()) { + fail("output-boundary-unavailable", "Output parent boundary must be a directory"); + } + break; + } + const canonicalAncestor = await realpath(existingAncestor).catch(() => null); + if (!canonicalAncestor) fail("output-boundary-unavailable", "Output boundary cannot be resolved"); + const canonicalAncestorStats = await lstat(canonicalAncestor).catch(() => null); + if (!canonicalAncestorStats?.isDirectory()) { + fail("output-boundary-unavailable", "Output parent boundary must resolve to a directory"); + } + const canonicalParent = path.resolve(canonicalAncestor, ...missingComponents); + const canonicalOutput = path.join(canonicalParent, path.basename(resolvedOutput)); + if (isContained(canonicalOutput, canonicalRepo) || isContained(canonicalRepo, canonicalOutput)) { + fail("output-overlap", "Output must not overlap the repository source root"); + } + return { + resolvedOutput, + resolvedParent, + canonicalRepo, + canonicalParent, + canonicalOutput, + }; +} + +function publicationComplete(replaced) { + return { + state: "published", + backupCleanup: "complete", + replaced, + }; +} + +function publicationError(code, message, state, backupName, details = {}) { + fail(code, message, { + publication: { state, backupName, ...details }, + }); +} + +export async function publishStagedArtifact({ + stageRoot, + outputRoot, + replaceExisting, + operations = { rename, remove: rm }, +}) { + if (!replaceExisting) { + await operations.rename(stageRoot, outputRoot); + return { publication: publicationComplete(false), warnings: [] }; + } + const backupName = `.better-harness-antigravity-backup-${randomUUID()}`; + const backupContainer = path.join(path.dirname(outputRoot), backupName); + const backupRoot = path.join(backupContainer, ANTIGRAVITY_PLUGIN_NAME); + await mkdir(backupContainer); + try { + await operations.rename(outputRoot, backupRoot); + } catch { + let backupCleanup = "complete"; + try { + await operations.remove(backupContainer, { recursive: true, force: true }); + } catch { + backupCleanup = "pending"; + } + publicationError( + "publish-backup-rename-failed", + "Artifact publication could not begin and the destination was left unchanged", + "not-published-destination-unchanged", + backupName, + { backupCleanup }, + ); + } + + try { + await verifyAntigravityPluginArtifact(backupRoot); + } catch { + try { + await operations.rename(backupRoot, outputRoot); + await operations.remove(backupContainer, { recursive: true, force: true }).catch(() => {}); + } catch { + publicationError( + "destination-revalidation-rollback-failed", + "Destination changed after preflight and its moved tree was retained", + "not-published-backup-retained", + backupName, + ); + } + publicationError( + "destination-changed", + "Destination changed after preflight and was restored without publication", + "not-published-destination-restored", + backupName, + ); + } + + try { + await operations.rename(stageRoot, outputRoot); + } catch { + try { + await operations.rename(backupRoot, outputRoot); + await operations.remove(backupContainer, { recursive: true, force: true }).catch(() => {}); + } catch { + publicationError( + "publish-rollback-failed", + "Artifact publication failed and the verified moved tree was retained", + "not-published-verified-backup-retained", + backupName, + ); + } + publicationError( + "publish-failed", + "Artifact publication failed and the prior artifact was restored", + "not-published-destination-restored", + backupName, + ); + } + + try { + await operations.remove(backupContainer, { recursive: true, force: true }); + } catch { + const warning = { + code: "backup-cleanup-pending", + state: "published", + backupCleanup: "pending", + backupName, + }; + return { + publication: { + state: "published", + backupCleanup: "pending", + replaced: true, + backupName, + }, + warnings: [warning], + }; + } + return { publication: publicationComplete(true), warnings: [] }; +} + +async function pathExists(candidate) { + return Boolean(await lstat(candidate).catch(() => null)); +} + +export async function buildAntigravityPluginArtifact({ + repoRoot, + outputRoot, + operations = { rename, remove: rm }, + boundaryOperations = { mkdir }, +} = {}) { + if (!repoRoot || !outputRoot) fail("build-arguments-invalid", "repoRoot and outputRoot are required"); + const preflightBoundary = await assertOutputBoundary(repoRoot, outputRoot); + await boundaryOperations.mkdir(preflightBoundary.canonicalParent, { recursive: true }); + const boundary = await assertOutputBoundary(repoRoot, outputRoot); + const sourceManifestPath = path.join(boundary.canonicalRepo, ...SOURCE_MANIFEST.split("/")); + await requireRegularSource(sourceManifestPath, boundary.canonicalRepo, "source-manifest-invalid"); + const sourceManifest = await readJsonObject(sourceManifestPath, "source-manifest-invalid"); + if ( + Object.keys(sourceManifest).length !== 1 + || sourceManifest.name !== ANTIGRAVITY_PLUGIN_NAME + ) { + fail("source-manifest-invalid", "Source Antigravity manifest must equal the frozen name-only object"); + } + + let replaceExisting = false; + const destinationStats = await lstat(boundary.canonicalOutput).catch(() => null); + if (destinationStats) { + if (destinationStats.isSymbolicLink() || !destinationStats.isDirectory()) { + fail("destination-unowned", "Existing output is not a fully verified Antigravity artifact"); + } + try { + await verifyAntigravityPluginArtifact(boundary.canonicalOutput); + replaceExisting = true; + } catch { + fail("destination-unowned", "Existing output is not a fully verified Antigravity artifact"); + } + } + + const stageContainer = await mkdtemp(path.join(boundary.canonicalParent, ".better-harness-antigravity-stage-")); + const stageRoot = path.join(stageContainer, ANTIGRAVITY_PLUGIN_NAME); + await mkdir(stageRoot); + try { + const sourcePackage = await readJsonObject( + path.join(boundary.canonicalRepo, "package.json"), + "source-package-invalid", + ); + const packageJson = artifactPackage(sourcePackage); + for (const rootFile of ROOT_FILES) { + await copyRegularTree({ + source: path.join(boundary.canonicalRepo, rootFile), + destination: path.join(stageRoot, rootFile), + relativePath: rootFile, + canonicalBoundary: boundary.canonicalRepo, + }); + } + for (const sourceRoot of SOURCE_ROOTS) { + if (!(await pathExists(path.join(boundary.canonicalRepo, ...sourceRoot.split("/"))))) continue; + await copyRegularTree({ + source: path.join(boundary.canonicalRepo, ...sourceRoot.split("/")), + destination: path.join(stageRoot, ...sourceRoot.split("/")), + relativePath: sourceRoot, + canonicalBoundary: boundary.canonicalRepo, + }); + } + for (const dependency of RUNTIME_DEPENDENCIES) { + await copyDependency({ + repoRoot: boundary.canonicalRepo, + stageRoot, + dependency, + expectedVersion: packageJson.dependencies[dependency], + }); + } + await writeFile(path.join(stageRoot, "plugin.json"), `${JSON.stringify(sourceManifest, null, 2)}\n`, "utf8"); + await writeFile(path.join(stageRoot, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`, "utf8"); + await writeFile(path.join(stageRoot, ANTIGRAVITY_ARTIFACT_MARKER), `${JSON.stringify({ + kind: ANTIGRAVITY_ARTIFACT_KIND, + schemaVersion: ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + host: ANTIGRAVITY_HOST, + pluginName: ANTIGRAVITY_PLUGIN_NAME, + version: packageJson.version, + canonicalSkill: CANONICAL_SKILL, + runtimeDependencies: [...RUNTIME_DEPENDENCIES], + }, null, 2)}\n`, "utf8"); + + const verified = await verifyAntigravityPluginArtifact(stageRoot); + const published = await publishStagedArtifact({ + stageRoot, + outputRoot: boundary.canonicalOutput, + replaceExisting, + operations, + }); + await rm(stageContainer, { recursive: true, force: true }); + return { + ...verified, + replaced: replaceExisting, + publication: published.publication, + warnings: published.warnings, + }; + } catch (error) { + await rm(stageContainer, { recursive: true, force: true }).catch(() => {}); + throw error; + } +} + +export function parseArgs(argv) { + const options = { outputRoot: null, json: false, help: false }; + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index]; + if (argument === "--out") { + if (options.outputRoot !== null) fail("argument-duplicate", "--out may be provided only once"); + const value = argv[index + 1]; + if (!value || value.startsWith("--")) fail("argument-value-missing", "--out requires a value"); + options.outputRoot = value; + index += 1; + } else if (argument.startsWith("--out=")) { + if (options.outputRoot !== null) fail("argument-duplicate", "--out may be provided only once"); + const value = argument.slice("--out=".length); + if (!value) fail("argument-value-missing", "--out requires a value"); + options.outputRoot = value; + } else if (argument === "--json") options.json = true; + else if (argument === "--help" || argument === "-h") options.help = true; + else fail("argument-unknown", "Unknown argument"); + } + if (!options.help && options.outputRoot === null) fail("argument-required", "--out is required"); + return options; +} + +export function usage() { + return [ + "Usage: node scripts/packaging/antigravity/build-antigravity-plugin.mjs --out [options]", + "", + "Options:", + " --out Output root named better-harness (required)", + " --json Emit parser-safe JSON", + " -h, --help Print help", + "", + ].join("\n"); +} + +export function formatBuildSuccess(result, { json = false } = {}) { + if (json) { + return `${JSON.stringify({ ok: true, data: result, warnings: result.warnings ?? [] }, null, 2)}\n`; + } + const summary = `Antigravity plugin artifact built: ${result.pluginName} ${result.version} (${result.fileCount} files)\n`; + if (result.publication?.backupCleanup !== "pending") return summary; + const warning = result.warnings?.find((entry) => entry.code === "backup-cleanup-pending"); + return `${summary}Warning [backup-cleanup-pending]: artifact published; state=published backupCleanup=pending (${warning?.backupName ?? "backup-retained"})\n`; +} + +export async function main(argv = process.argv.slice(2)) { + let options; + try { + options = parseArgs(argv); + if (options.help) { + process.stdout.write(usage()); + return 0; + } + const result = await buildAntigravityPluginArtifact({ + repoRoot: defaultRepoRoot, + outputRoot: options.outputRoot, + }); + process.stdout.write(formatBuildSuccess(result, { json: options.json })); + return 0; + } catch (error) { + const code = error instanceof AntigravityBuildError ? error.code : "artifact-build-failed"; + const message = error instanceof AntigravityBuildError ? error.message : "Artifact build failed"; + if (options?.json || argv.includes("--json")) { + process.stdout.write(`${JSON.stringify({ ok: false, error: { code, message } }, null, 2)}\n`); + } else { + process.stderr.write(`Antigravity plugin build failed [${code}]: ${message}\n`); + } + return 1; + } +} + +const currentFile = fileURLToPath(import.meta.url); +if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) { + process.exitCode = await main(); +} diff --git a/scripts/packaging/antigravity/plugin-manifest.json b/scripts/packaging/antigravity/plugin-manifest.json new file mode 100644 index 00000000..55b981cd --- /dev/null +++ b/scripts/packaging/antigravity/plugin-manifest.json @@ -0,0 +1,3 @@ +{ + "name": "better-harness" +} diff --git a/scripts/packaging/antigravity/verify-antigravity-plugin.mjs b/scripts/packaging/antigravity/verify-antigravity-plugin.mjs new file mode 100644 index 00000000..863faf64 --- /dev/null +++ b/scripts/packaging/antigravity/verify-antigravity-plugin.mjs @@ -0,0 +1,1412 @@ +#!/usr/bin/env node + +import { lstat, readFile, readdir, realpath } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export const ANTIGRAVITY_ARTIFACT_KIND = "better-harness-antigravity-plugin-artifact"; +export const ANTIGRAVITY_ARTIFACT_MARKER = ".antigravity-plugin-artifact.json"; +export const ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION = 1; +export const ANTIGRAVITY_HOST = "antigravity"; +export const ANTIGRAVITY_PLUGIN_NAME = "better-harness"; +export const CANONICAL_SKILL = "skills/better-harness/SKILL.md"; +export const RUNTIME_ENTRY = "scripts/better-harness.mjs"; +export const RUNTIME_DEPENDENCIES = Object.freeze([ + "@vscode/tree-sitter-wasm", + "esbuild-wasm", +]); +export const RUNTIME_DEPENDENCY_LICENSES = Object.freeze({ + "@vscode/tree-sitter-wasm": "LICENSE", + "esbuild-wasm": "LICENSE.md", +}); +export const GRAPH_LIMITS = Object.freeze({ + markdownNodes: 128, + markdownEdges: 1024, + markdownDepth: 32, + runtimeModules: 1024, + runtimeEdges: 8192, + runtimeDepth: 64, +}); +export const FILE_LIMITS = Object.freeze({ + entries: 25_000, + directoryDepth: 64, + componentLength: 255, + files: 20_000, + totalBytes: 256 * 1024 * 1024, + textBytes: 4 * 1024 * 1024, +}); + +const ROOT_FILES = new Set([ + "plugin.json", + ANTIGRAVITY_ARTIFACT_MARKER, + "package.json", + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", +]); +const BROAD_ROOTS = new Set([ + "references", + "templates", + "models", + "hooks", + "docs", + "case-studies", +]); +const RUNTIME_ROOTS = new Set([ + "scripts", + "references", + "templates", + "models", + "hooks", + "docs", + "case-studies", +]); +const GENERATED_NAMES = new Set([ + ".DS_Store", + ".plugin-eval", + ".cache", + ".docusaurus", + ".vite", + "cache", + "coverage", + "dist", + "outputs", + "temp", + "tmp", +]); +const MARKER_KEYS = Object.freeze([ + "canonicalSkill", + "host", + "kind", + "pluginName", + "runtimeDependencies", + "schemaVersion", + "version", +]); +const CLI_MANIFEST_KEYS = new Set(["description", "name"]); +const CLI_PLUGIN_NAME_PATTERN = /^[A-Za-z0-9_-]+$/u; +const PACKAGE_KEYS = Object.freeze([ + "bin", + "dependencies", + "engines", + "license", + "name", + "private", + "type", + "version", +]); +const REQUIRED_FILES = Object.freeze([ + "plugin.json", + ANTIGRAVITY_ARTIFACT_MARKER, + "package.json", + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + CANONICAL_SKILL, + RUNTIME_ENTRY, + "node_modules/@vscode/tree-sitter-wasm/package.json", + "node_modules/@vscode/tree-sitter-wasm/LICENSE", + "node_modules/esbuild-wasm/package.json", + "node_modules/esbuild-wasm/LICENSE.md", +]); + +export class AntigravityArtifactError extends Error { + constructor(code, message) { + super(message); + this.name = "AntigravityArtifactError"; + this.code = code; + } +} + +// `details` carries repository- or artifact-relative paths only. An absolute +// path would leak the operator's filesystem layout into an error a caller may +// log, which is why the messages themselves stay location-free. +function fail(code, message, details = {}) { + const error = new AntigravityArtifactError(code, message); + Object.assign(error, details); + throw error; +} + +function isObject(value) { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function isNonblankString(value) { + return typeof value === "string" && value.trim().length > 0; +} + +function posixPath(value) { + return value.split(path.sep).join("/"); +} + +function isContained(candidate, parent) { + const relative = path.relative(parent, candidate); + return relative === "" || ( + relative !== ".." + && !relative.startsWith(`..${path.sep}`) + && !path.isAbsolute(relative) + ); +} + +function hasGeneratedName(relativePath) { + const names = relativePath.split("/"); + return names.some((name) => ( + GENERATED_NAMES.has(name) + || name === ".env" + || name.startsWith(".env.") + || name.endsWith(".tmp") + || name.endsWith(".log") + )); +} + +export function isAllowedArtifactPath(relativePath) { + if (typeof relativePath !== "string" || !relativePath || relativePath.includes("\0")) { + return false; + } + const normalized = path.posix.normalize(relativePath); + if ( + normalized !== relativePath + || normalized === "." + || normalized === ".." + || normalized.startsWith("../") + || path.posix.isAbsolute(normalized) + || normalized.includes("\\") + || hasGeneratedName(normalized) + ) { + return false; + } + if (!normalized.includes("/")) return ROOT_FILES.has(normalized) || [ + "skills", + "scripts", + "references", + "templates", + "models", + "hooks", + "docs", + "case-studies", + "node_modules", + ].includes(normalized); + + if (normalized === "skills/better-harness" || normalized.startsWith("skills/better-harness/")) { + return true; + } + if (normalized.startsWith("skills/")) return false; + + if (normalized === "scripts/packaging" || normalized.startsWith("scripts/packaging/")) { + return false; + } + if (normalized.startsWith("scripts/")) return true; + + const first = normalized.split("/", 1)[0]; + if (BROAD_ROOTS.has(first)) return true; + + if (normalized === "node_modules/@vscode") return true; + if ( + normalized === "node_modules/@vscode/tree-sitter-wasm" + || normalized.startsWith("node_modules/@vscode/tree-sitter-wasm/") + ) { + return true; + } + if ( + normalized === "node_modules/esbuild-wasm" + || normalized.startsWith("node_modules/esbuild-wasm/") + ) { + return true; + } + return false; +} + +function assertSafeRelative(relativePath, code = "path-unsafe") { + if (!isAllowedArtifactPath(relativePath)) { + fail(code, "Artifact contains a path outside the Antigravity allowlist"); + } +} + +function identityKey(relativePath) { + return relativePath.normalize("NFC").toLowerCase(); +} + +const WINDOWS_RESERVED_COMPONENT = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu; + +export function validatePortablePathComponent(component) { + if ( + typeof component !== "string" + || component.length === 0 + || component.length > FILE_LIMITS.componentLength + || /[\u0000-\u001f\u007f]/u.test(component) + || component.includes(":") + || component.includes("\\") + || component.endsWith(".") + || component.endsWith(" ") + || WINDOWS_RESERVED_COMPONENT.test(component) + ) { + fail("path-component-invalid", "Artifact contains a non-portable path component"); + } + return component; +} + +export function validateTraversalBounds(entryCount, directoryDepth) { + if (!Number.isInteger(entryCount) || entryCount < 0 || entryCount > FILE_LIMITS.entries) { + fail("artifact-entry-limit", "Artifact exceeds the fixed entry limit"); + } + if ( + !Number.isInteger(directoryDepth) + || directoryDepth < 0 + || directoryDepth > FILE_LIMITS.directoryDepth + ) { + fail("artifact-depth-limit", "Artifact exceeds the fixed directory depth limit"); + } +} + +async function inspectArtifactTree(root, canonicalRoot) { + const inventory = new Map(); + const identities = new Map(); + let fileCount = 0; + let entryCount = 0; + let totalBytes = 0; + + async function visit(directory, relativeDirectory = "", depth = 0) { + validateTraversalBounds(entryCount, depth); + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch { + fail("tree-unreadable", "Artifact tree cannot be read safely"); + } + entries.sort((left, right) => left.name.localeCompare(right.name, "en")); + for (const entry of entries) { + validatePortablePathComponent(entry.name); + entryCount += 1; + validateTraversalBounds(entryCount, depth); + const relative = relativeDirectory + ? `${relativeDirectory}/${entry.name}` + : entry.name; + assertSafeRelative(relative, "path-not-allowed"); + + const key = identityKey(relative); + if (identities.has(key) && identities.get(key) !== relative) { + fail("path-identity-collision", "Artifact contains a case or Unicode path identity collision"); + } + identities.set(key, relative); + + const absolute = path.join(root, ...relative.split("/")); + let stats; + try { + stats = await lstat(absolute); + } catch { + fail("tree-entry-unreadable", "Artifact tree contains an unreadable entry"); + } + if (stats.isSymbolicLink()) { + fail("symlink-forbidden", "Artifact must not contain symbolic links or junctions"); + } + if (!stats.isDirectory() && !stats.isFile()) { + fail("special-file-forbidden", "Artifact must contain only regular files and directories"); + } + + let canonicalEntry; + try { + canonicalEntry = await realpath(absolute); + } catch { + fail("canonical-path-unavailable", "Artifact entry cannot be canonically resolved"); + } + if (!isContained(canonicalEntry, canonicalRoot)) { + fail("canonical-path-escape", "Artifact entry escapes the canonical plugin root"); + } + inventory.set(relative, { absolute, canonical: canonicalEntry, stats }); + + if (stats.isDirectory()) { + await visit(absolute, relative, depth + 1); + } else { + fileCount += 1; + totalBytes += stats.size; + if (fileCount > FILE_LIMITS.files || totalBytes > FILE_LIMITS.totalBytes) { + fail("artifact-bounds-exceeded", "Artifact exceeds fixed file or byte limits"); + } + } + } + } + + await visit(root); + return { inventory, entryCount, fileCount, totalBytes }; +} + +function requireRegularFile(inventory, relativePath) { + const entry = inventory.get(relativePath); + if (!entry?.stats.isFile()) { + fail("required-file-missing", `Artifact is missing required regular file: ${relativePath}`); + } + return entry; +} + +async function readBoundedText(entry, code) { + if (entry.stats.size > FILE_LIMITS.textBytes) { + fail("text-file-too-large", "Artifact metadata or closure source exceeds the fixed text limit"); + } + try { + return await readFile(entry.absolute, "utf8"); + } catch { + fail(code, "Artifact metadata or closure source cannot be read"); + } +} + +async function readJsonObject(inventory, relativePath, code) { + const text = await readBoundedText(requireRegularFile(inventory, relativePath), code); + let value; + try { + value = JSON.parse(text); + } catch { + fail(code, `Artifact JSON is malformed: ${relativePath}`); + } + if (!isObject(value)) { + fail(code, `Artifact JSON must be an object: ${relativePath}`); + } + return value; +} + +export function effectivePluginName(manifest) { + if (!isObject(manifest)) { + fail("manifest-object-required", "plugin.json must contain a JSON object"); + } + if (Object.keys(manifest).some((key) => !CLI_MANIFEST_KEYS.has(key))) { + fail("manifest-schema-invalid", "Agy CLI plugin.json permits only name and description"); + } + if (!Object.hasOwn(manifest, "name")) { + fail("manifest-name-required", "Agy CLI plugin.json requires name"); + } + if (typeof manifest.name !== "string") { + fail("manifest-name-type-invalid", "Agy CLI plugin.json name must be a string"); + } + if (manifest.name.length === 0) { + fail("manifest-name-blank", "Agy CLI plugin.json name must not be empty"); + } + if (!CLI_PLUGIN_NAME_PATTERN.test(manifest.name)) { + fail("manifest-name-pattern-invalid", "Agy CLI plugin.json name contains unsupported characters"); + } + if (Object.hasOwn(manifest, "description") && typeof manifest.description !== "string") { + fail("manifest-description-invalid", "Agy CLI plugin.json description must be a string"); + } + return manifest.name; +} + +function validateMarker(marker) { + const keys = Object.keys(marker).sort(); + if (keys.length !== MARKER_KEYS.length || keys.some((key, index) => key !== MARKER_KEYS[index])) { + fail("marker-schema-invalid", "Artifact ownership marker fields do not match schema version 1"); + } + if (marker.kind !== ANTIGRAVITY_ARTIFACT_KIND) { + fail("marker-kind-invalid", "Artifact ownership marker kind is invalid"); + } + if (marker.schemaVersion !== ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION) { + fail("marker-schema-version-invalid", "Artifact ownership marker schema version is unsupported"); + } + if (marker.host !== ANTIGRAVITY_HOST) { + fail("marker-host-invalid", "Artifact ownership marker host is invalid"); + } + if (!isNonblankString(marker.pluginName)) { + fail("marker-plugin-name-invalid", "Artifact ownership marker plugin name is invalid"); + } + if (!isNonblankString(marker.version)) { + fail("marker-version-invalid", "Artifact ownership marker version is invalid"); + } + if (marker.canonicalSkill !== CANONICAL_SKILL) { + fail("marker-skill-invalid", "Artifact ownership marker canonical Skill is invalid"); + } + if ( + !Array.isArray(marker.runtimeDependencies) + || marker.runtimeDependencies.length !== RUNTIME_DEPENDENCIES.length + || marker.runtimeDependencies.some((value, index) => value !== RUNTIME_DEPENDENCIES[index]) + ) { + fail("marker-dependencies-invalid", "Artifact ownership marker runtime dependencies are invalid"); + } +} + +function validatePackage(packageJson, marker) { + const packageKeys = Object.keys(packageJson).sort(); + if ( + packageKeys.length !== PACKAGE_KEYS.length + || packageKeys.some((key, index) => key !== PACKAGE_KEYS[index]) + ) { + fail("package-schema-invalid", "Artifact package.json must use the closed Better Harness artifact schema"); + } + if (packageJson.name !== "@qoder-ai/better-harness") { + fail("package-name-invalid", "Artifact package.json name must match Better Harness"); + } + if (packageJson.private !== true) { + fail("package-private-invalid", "Artifact package.json must be private"); + } + if (!isNonblankString(packageJson.version) || packageJson.version !== marker.version) { + fail("package-version-invalid", "Artifact package version must match the ownership marker"); + } + if (!isNonblankString(packageJson.license)) { + fail("package-license-invalid", "Artifact package.json must declare a nonblank license"); + } + if (packageJson.type !== "module") { + fail("package-type-invalid", "Artifact package.json must use the module type"); + } + if ( + !isObject(packageJson.bin) + || Object.keys(packageJson.bin).length !== 1 + || Object.keys(packageJson.bin)[0] !== "better-harness" + || packageJson.bin["better-harness"] !== RUNTIME_ENTRY + ) { + fail("package-bin-invalid", "Artifact package bin must match the runtime entry exactly"); + } + const engineKeys = isObject(packageJson.engines) ? Object.keys(packageJson.engines).sort() : []; + if ( + engineKeys.length !== 2 + || engineKeys[0] !== "node" + || engineKeys[1] !== "npm" + || !isNonblankString(packageJson.engines.node) + || !isNonblankString(packageJson.engines.npm) + ) { + fail("package-engines-invalid", "Artifact package engines must contain nonblank node and npm ranges"); + } + if (!isObject(packageJson.dependencies)) { + fail("package-dependencies-invalid", "Artifact package dependencies must be an object"); + } + const keys = Object.keys(packageJson.dependencies).sort(); + const expected = [...RUNTIME_DEPENDENCIES].sort(); + if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) { + fail("package-dependencies-invalid", "Artifact package dependencies must match the runtime profile"); + } + for (const dependency of RUNTIME_DEPENDENCIES) { + if (!isNonblankString(packageJson.dependencies[dependency])) { + fail("package-dependency-version-invalid", "Artifact dependency versions must be nonblank strings"); + } + } +} + +async function validateDependencyPackages(inventory, packageJson) { + for (const dependency of RUNTIME_DEPENDENCIES) { + const relative = `node_modules/${dependency}/package.json`; + const dependencyPackage = await readJsonObject(inventory, relative, "dependency-package-invalid"); + if (dependencyPackage.name !== dependency) { + fail("dependency-package-name-invalid", "Runtime dependency package identity does not match its root"); + } + if ( + !isNonblankString(dependencyPackage.version) + || dependencyPackage.version !== packageJson.dependencies[dependency] + ) { + fail("dependency-package-version-invalid", "Runtime dependency package version does not match package.json"); + } + } +} + +function maskMarkdownCode(source) { + const output = [...source]; + let index = 0; + let fence = null; + const listStack = []; + while (index < source.length) { + const lineStart = index === 0 || source[index - 1] === "\n"; + if (lineStart) { + const lineEnd = source.indexOf("\n", index); + const end = lineEnd < 0 ? source.length : lineEnd; + const line = source.slice(index, end); + const marker = line.match(/^ {0,3}(`{3,}|~{3,})/u)?.[1]; + if (marker && (!fence || marker[0] === fence[0] && marker.length >= fence.length)) { + fence = fence ? null : marker; + for (let cursor = index; cursor < end; cursor += 1) output[cursor] = " "; + index = end; + continue; + } + if (!fence && line.trim().length > 0) { + const leading = line.match(/^ */u)?.[0].length ?? 0; + const listMarker = line.match(/^( *)(?:[-+*]|\d{1,9}[.)])([ \t]+)/u); + if (listMarker) { + const markerIndent = listMarker[1].length; + while (listStack.length && markerIndent < listStack.at(-1).contentIndent) { + listStack.pop(); + } + const parentIndent = listStack.at(-1)?.contentIndent ?? 0; + if (markerIndent - parentIndent <= 3) { + listStack.push({ markerIndent, contentIndent: listMarker[0].length }); + } + } else { + while (listStack.length && leading < listStack.at(-1).contentIndent) listStack.pop(); + const contentIndent = listStack.at(-1)?.contentIndent ?? 0; + const codeIndent = leading - contentIndent; + if ((!listStack.length && leading >= 4) || (listStack.length && codeIndent >= 4)) { + for (let cursor = index; cursor < end; cursor += 1) output[cursor] = " "; + index = end; + continue; + } + } + } + } + if (fence) { + if (source[index] !== "\n") output[index] = " "; + index += 1; + continue; + } + if (source[index] === "`") { + let ticks = 1; + while (source[index + ticks] === "`") ticks += 1; + const closing = source.indexOf("`".repeat(ticks), index + ticks); + if (closing < 0) fail("markdown-link-unsupported", "Markdown contains an unterminated code span"); + for (let cursor = index; cursor < closing + ticks; cursor += 1) { + if (source[cursor] !== "\n") output[cursor] = " "; + } + index = closing + ticks; + continue; + } + index += 1; + } + if (fence) fail("markdown-link-unsupported", "Markdown contains an unterminated code fence"); + return output.join(""); +} + +function unescapeMarkdownDestination(value) { + return value.replace(/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/gu, "$1"); +} + +function parseMarkdownDestination(source, start, terminator) { + let index = start; + while (/[ \t]/u.test(source[index] ?? "")) index += 1; + if (source[index] === "<") { + let value = ""; + index += 1; + while (index < source.length && source[index] !== ">") { + if (source[index] === "\n" || source[index] === "\r" || source[index] === "<") { + fail("markdown-link-unsupported", "Markdown contains an unsupported angle destination"); + } + if (source[index] === "\\" && index + 1 < source.length) { + value += source[index] + source[index + 1]; + index += 2; + } else { + value += source[index]; + index += 1; + } + } + if (source[index] !== ">") fail("markdown-link-unsupported", "Markdown angle destination is unterminated"); + return { target: unescapeMarkdownDestination(value), end: index + 1 }; + } + + let value = ""; + let depth = 0; + while (index < source.length) { + const char = source[index]; + if (char === "\\" && index + 1 < source.length) { + value += char + source[index + 1]; + index += 2; + continue; + } + if (char === "(" ) { + depth += 1; + value += char; + index += 1; + continue; + } + if (char === ")") { + if (depth === 0 && terminator === ")") break; + if (depth === 0) fail("markdown-link-unsupported", "Markdown destination has unbalanced parentheses"); + depth -= 1; + value += char; + index += 1; + continue; + } + if (char === "\n" || char === "\r" || (depth === 0 && /[ \t]/u.test(char))) break; + value += char; + index += 1; + } + if (depth !== 0 || !value) fail("markdown-link-unsupported", "Markdown destination is malformed"); + return { target: unescapeMarkdownDestination(value), end: index }; +} + +function finishInlineMarkdownLink(source, destinationEnd) { + let index = destinationEnd; + while (/[ \t\n\r]/u.test(source[index] ?? "")) index += 1; + if (source[index] === ")") return index + 1; + const quote = source[index]; + if (!["\"", "'", "("].includes(quote)) { + fail("markdown-link-unsupported", "Markdown link has an unsupported title or suffix"); + } + const closing = quote === "(" ? ")" : quote; + index += 1; + while (index < source.length && source[index] !== closing) { + if (source[index] === "\\") index += 2; + else index += 1; + } + if (source[index] !== closing) fail("markdown-link-unsupported", "Markdown link title is unterminated"); + index += 1; + while (/[ \t\n\r]/u.test(source[index] ?? "")) index += 1; + if (source[index] !== ")") fail("markdown-link-unsupported", "Markdown link is unterminated"); + return index + 1; +} + +function closingMarkdownLabel(source, start) { + let depth = 1; + for (let index = start; index < source.length; index += 1) { + if (source[index] === "\\") { + index += 1; + continue; + } + if (source[index] === "[") depth += 1; + if (source[index] === "]") { + depth -= 1; + if (depth === 0) return index; + } + } + return -1; +} + +function normalizeMarkdownLabel(label) { + return label + .replace(/\\([\[\]\\])/gu, "$1") + .trim() + .toLowerCase() + .replace(/\s+/gu, " "); +} + +export function parseMarkdownTargets(source) { + if (typeof source !== "string") fail("markdown-source-invalid", "Markdown source must be text"); + const masked = maskMarkdownCode(source); + const targets = []; + const definitions = new Map(); + + for (let lineStart = 0; lineStart <= masked.length;) { + const lineEnd = masked.indexOf("\n", lineStart); + const end = lineEnd < 0 ? masked.length : lineEnd; + const line = masked.slice(lineStart, end); + const originalLine = source.slice(lineStart, end); + const indentation = line.match(/^ {0,3}/u)?.[0].length ?? 0; + const definitionClose = line[indentation] === "[" + ? closingMarkdownLabel(line, indentation + 1) + : -1; + if (definitionClose >= 0 && line[definitionClose + 1] === ":") { + let destinationStart = definitionClose + 2; + while (/[ \t]/u.test(line[destinationStart] ?? "")) destinationStart += 1; + const parsed = parseMarkdownDestination(line, destinationStart, "line"); + let cursor = parsed.end; + while (/[ \t]/u.test(line[cursor] ?? "")) cursor += 1; + if (cursor < line.length && !/^(?:["'(]).*(?:["')])$/u.test(line.slice(cursor))) { + fail("markdown-link-unsupported", "Markdown reference definition has an unsupported suffix"); + } + const label = normalizeMarkdownLabel(originalLine.slice(indentation + 1, definitionClose)); + if (!label) fail("markdown-link-unsupported", "Markdown reference label is empty"); + if (!definitions.has(label)) definitions.set(label, parsed.target); + } + if (lineEnd < 0) break; + lineStart = lineEnd + 1; + } + + for (let index = 0; index < masked.length; index += 1) { + if (masked[index] !== "[") continue; + const close = closingMarkdownLabel(masked, index + 1); + if (close < 0) fail("markdown-link-unsupported", "Markdown link label is unterminated"); + if (masked[close + 1] === ":") { + index = close; + continue; + } + if (masked[close + 1] === "(") { + if (!normalizeMarkdownLabel(source.slice(index + 1, close))) { + fail("markdown-link-unsupported", "Markdown link label is empty"); + } + const parsed = parseMarkdownDestination(masked, close + 2, ")"); + targets.push(parsed.target); + index = finishInlineMarkdownLink(masked, parsed.end) - 1; + continue; + } + if (masked[close + 1] === "[") { + const referenceEnd = closingMarkdownLabel(masked, close + 2); + if (referenceEnd < 0) fail("markdown-link-unsupported", "Markdown reference label is unterminated"); + const explicit = source.slice(close + 2, referenceEnd); + const label = normalizeMarkdownLabel(explicit || source.slice(index + 1, close)); + if (!label) fail("markdown-link-unsupported", "Markdown reference label is empty"); + if (!definitions.has(label)) fail("markdown-reference-missing", "Markdown reference definition is missing"); + targets.push(definitions.get(label)); + index = referenceEnd; + continue; + } + const shortcut = normalizeMarkdownLabel(source.slice(index + 1, close)); + if (definitions.has(shortcut)) { + targets.push(definitions.get(shortcut)); + index = close; + } + } + return targets; +} + +function localMarkdownTarget(rawTarget, sourceRelative) { + if (typeof rawTarget !== "string" || !rawTarget) return null; + if (rawTarget.startsWith("#") || rawTarget.startsWith("?")) return null; + if (/^[A-Za-z]:[\\/]/u.test(rawTarget) || rawTarget.startsWith("\\\\")) { + fail("markdown-absolute-path", "Markdown closure contains an absolute local path"); + } + const scheme = rawTarget.match(/^([A-Za-z][A-Za-z0-9+.-]*):/u)?.[1]?.toLowerCase(); + if (scheme) { + if (["http", "https", "mailto", "pathname"].includes(scheme)) return null; + if (scheme === "file") fail("markdown-file-uri", "Markdown closure contains a file URI"); + fail("markdown-scheme-invalid", "Markdown closure contains an unsupported URI scheme"); + } + if (rawTarget.startsWith("/") || rawTarget.startsWith("\\") || rawTarget.includes("\\")) { + fail("markdown-absolute-path", "Markdown closure contains an absolute or backslash path"); + } + + const boundary = [rawTarget.indexOf("#"), rawTarget.indexOf("?")] + .filter((index) => index >= 0) + .reduce((minimum, index) => Math.min(minimum, index), rawTarget.length); + const encodedPath = rawTarget.slice(0, boundary); + if (!encodedPath) return null; + let decodedPath; + try { + decodedPath = decodeURIComponent(encodedPath); + } catch { + fail("markdown-percent-invalid", "Markdown closure contains invalid percent encoding"); + } + if (decodedPath.includes("\0") || decodedPath.includes("\\")) { + fail("markdown-path-invalid", "Markdown closure contains an unsafe local path"); + } + const resolved = path.posix.normalize(path.posix.join(path.posix.dirname(sourceRelative), decodedPath)); + if (resolved === ".." || resolved.startsWith("../") || path.posix.isAbsolute(resolved)) { + fail("markdown-path-escape", "Markdown closure target escapes the plugin root"); + } + return resolved; +} + +function markdownTargetCandidates(relativePath) { + return path.posix.extname(relativePath) + ? [relativePath] + : [relativePath, `${relativePath}.md`, `${relativePath}/README.md`]; +} + +async function resolveMarkdownTarget(rawTarget, sourceRelative, candidateExists) { + const unresolved = localMarkdownTarget(rawTarget, sourceRelative); + if (!unresolved) return null; + const candidates = markdownTargetCandidates(unresolved).filter((candidate) => ( + isAllowedArtifactPath(candidate) + )); + if (candidates.length === 0) { + fail( + "markdown-target-forbidden", + "Markdown closure target is outside the artifact allowlist", + { source: sourceRelative, target: unresolved }, + ); + } + const matches = []; + for (const candidate of candidates) { + if (await candidateExists(candidate)) matches.push(candidate); + } + if (matches.length === 0) fail("required-file-missing", "Markdown closure target is missing"); + if (matches.length > 1) fail("markdown-target-ambiguous", "Markdown closure target is ambiguous"); + return matches[0]; +} + +async function traverseMarkdownClosure({ readSource, candidateExists }) { + const queue = [{ relative: CANONICAL_SKILL, depth: 0 }]; + const visited = new Set(); + const targets = new Set(); + let edges = 0; + + while (queue.length) { + const current = queue.shift(); + if (visited.has(current.relative)) continue; + if (visited.size >= GRAPH_LIMITS.markdownNodes) { + fail("markdown-node-limit", "Markdown closure exceeds the fixed node limit"); + } + visited.add(current.relative); + const text = await readSource(current.relative); + for (const rawTarget of parseMarkdownTargets(text)) { + const relative = await resolveMarkdownTarget(rawTarget, current.relative, candidateExists); + if (!relative) continue; + edges += 1; + if (edges > GRAPH_LIMITS.markdownEdges) { + fail("markdown-edge-limit", "Markdown closure exceeds the fixed edge limit"); + } + targets.add(relative); + if (relative.toLowerCase().endsWith(".md") && !visited.has(relative)) { + if (current.depth + 1 > GRAPH_LIMITS.markdownDepth) { + fail("markdown-depth-limit", "Markdown closure exceeds the fixed depth limit"); + } + queue.push({ relative, depth: current.depth + 1 }); + } + } + } + return { + nodes: visited.size, + edges, + files: [...new Set([...visited, ...targets])].sort(), + }; +} + +async function verifyMarkdownClosure(inventory) { + return traverseMarkdownClosure({ + readSource: async (relative) => readBoundedText( + requireRegularFile(inventory, relative), + "markdown-read-failed", + ), + candidateExists: async (relative) => Boolean(inventory.get(relative)?.stats.isFile()), + }); +} + +export async function verifyMarkdownSourceClosure(sourceRoot) { + if (typeof sourceRoot !== "string" || !sourceRoot || sourceRoot.includes("\0")) { + fail("markdown-source-root-invalid", "Markdown source root must be a filesystem path"); + } + const resolvedRoot = path.resolve(sourceRoot); + const canonicalRoot = await realpath(resolvedRoot).catch(() => null); + if (!canonicalRoot) fail("markdown-source-root-invalid", "Markdown source root cannot be resolved"); + + async function regularCandidate(relative, missingIsError = false) { + assertSafeRelative(relative, "markdown-target-forbidden"); + const absolute = path.join(resolvedRoot, ...relative.split("/")); + const stats = await lstat(absolute).catch(() => null); + if (!stats) { + if (missingIsError) fail("required-file-missing", "Markdown closure source is missing"); + return false; + } + if (stats.isSymbolicLink() || !stats.isFile()) { + fail("markdown-source-file-invalid", "Markdown closure source must be a regular non-symlink file"); + } + const canonical = await realpath(absolute).catch(() => null); + if (!canonical || !isContained(canonical, canonicalRoot)) { + fail("markdown-path-escape", "Markdown closure source escapes its canonical root"); + } + return true; + } + + return traverseMarkdownClosure({ + readSource: async (relative) => { + await regularCandidate(relative, true); + const absolute = path.join(resolvedRoot, ...relative.split("/")); + const stats = await lstat(absolute); + return readBoundedText({ absolute, stats }, "markdown-read-failed"); + }, + candidateExists: async (relative) => regularCandidate(relative), + }); +} + +function tokenizeJavaScript(source) { + const tokens = []; + let index = 0; + let canStartRegex = true; + let syntaxBraceDepth = 0; + const parenFrames = []; + const controlHeads = new Set(["catch", "for", "if", "switch", "while", "with"]); + + function push(token) { + tokens.push({ ...token, braceDepth: syntaxBraceDepth }); + if (token.type === "identifier") { + canStartRegex = ["case", "delete", "do", "else", "in", "instanceof", "new", "return", "throw", "typeof", "void", "yield"].includes(token.value); + } else if (["string", "number", "regex"].includes(token.type)) { + canStartRegex = false; + } else if (token.type === "punctuation") { + canStartRegex = ![")", "]", "}"].includes(token.value) && token.value !== "."; + } + } + + function scan(stopAtTemplateBrace = false) { + let braceDepth = 0; + while (index < source.length) { + const char = source[index]; + const next = source[index + 1]; + if (/\s/u.test(char)) { + index += 1; + continue; + } + if (stopAtTemplateBrace && char === "}" && braceDepth === 0) { + syntaxBraceDepth -= 1; + index += 1; + return; + } + if (char === "/" && next === "/") { + index += 2; + while (index < source.length && source[index] !== "\n") index += 1; + continue; + } + if (char === "/" && next === "*") { + const end = source.indexOf("*/", index + 2); + if (end < 0) fail("runtime-comment-invalid", "Runtime module contains an unterminated comment"); + index = end + 2; + continue; + } + if (char === "'" || char === '"') { + const quote = char; + let value = ""; + let escaped = false; + index += 1; + let closed = false; + while (index < source.length) { + if (source[index] === "\\") { + escaped = true; + if (index + 1 >= source.length || /[\r\n]/u.test(source[index + 1])) { + fail("runtime-string-invalid", "Runtime module contains an invalid string escape"); + } + value += source[index + 1]; + index += 2; + continue; + } + if (source[index] === quote) { + closed = true; + index += 1; + break; + } + value += source[index]; + index += 1; + } + if (!closed) fail("runtime-string-invalid", "Runtime module contains an unterminated string"); + push({ type: "string", value, escaped }); + continue; + } + if (char === "`") { + let closed = false; + index += 1; + while (index < source.length) { + if (source[index] === "\\") { + index += 2; + continue; + } + if (source[index] === "$" && source[index + 1] === "{") { + index += 2; + syntaxBraceDepth += 1; + scan(true); + continue; + } + if (source[index] === "`") { + closed = true; + index += 1; + break; + } + index += 1; + } + if (!closed) fail("runtime-template-invalid", "Runtime module contains an unterminated template"); + canStartRegex = false; + continue; + } + if (char === "/" && canStartRegex) { + index += 1; + let inClass = false; + let closed = false; + while (index < source.length) { + if (source[index] === "\\") { + index += 2; + continue; + } + if (source[index] === "[") inClass = true; + else if (source[index] === "]") inClass = false; + else if (source[index] === "/" && !inClass) { + closed = true; + index += 1; + while (/[A-Za-z]/u.test(source[index] ?? "")) index += 1; + break; + } else if (/[\r\n]/u.test(source[index])) { + break; + } + index += 1; + } + if (!closed) fail("runtime-regex-invalid", "Runtime module contains an unterminated regular expression"); + push({ type: "regex", value: "" }); + continue; + } + if (/[A-Za-z_$]/u.test(char)) { + let end = index + 1; + while (end < source.length && /[A-Za-z0-9_$]/u.test(source[end])) end += 1; + push({ type: "identifier", value: source.slice(index, end) }); + index = end; + continue; + } + if (/[0-9]/u.test(char)) { + let end = index + 1; + while (end < source.length && /[A-Za-z0-9_.]/u.test(source[end])) end += 1; + push({ type: "number", value: source.slice(index, end) }); + index = end; + continue; + } + if (char === "(") { + const previous = tokens.at(-1); + const beforePrevious = tokens.at(-2); + parenFrames.push({ + control: ( + previous?.type === "identifier" + && ( + controlHeads.has(previous.value) + || (previous.value === "await" && beforePrevious?.value === "for") + ) + ), + }); + } + if (char === "}" && braceDepth > 0) { + braceDepth -= 1; + syntaxBraceDepth -= 1; + } + push({ type: "punctuation", value: char }); + if (char === "{") { + braceDepth += 1; + syntaxBraceDepth += 1; + } + if (char === ")") { + const frame = parenFrames.pop(); + if (frame?.control) canStartRegex = true; + } + index += 1; + } + if (stopAtTemplateBrace) fail("runtime-template-invalid", "Runtime template expression is unterminated"); + } + + scan(); + return tokens; +} + +function importSpecifier(token, errorCode) { + if (token?.type !== "string") fail(errorCode, "Runtime import/export has a nonliteral specifier"); + if (token.escaped) fail("runtime-import-escape", "Runtime import specifiers must not use string escapes"); + return token.value; +} + +function matchingCloseParen(tokens, openIndex) { + let depth = 0; + for (let index = openIndex; index < tokens.length; index += 1) { + if (tokens[index].value === "(") depth += 1; + if (tokens[index].value === ")") { + depth -= 1; + if (depth === 0) return index; + } + } + return -1; +} + +function isImportMethodName(tokens, importIndex) { + const token = tokens[importIndex]; + if (token.braceDepth <= 0 || tokens[importIndex + 1]?.value !== "(") return false; + const close = matchingCloseParen(tokens, importIndex + 1); + if (close < 0 || tokens[close + 1]?.value !== "{") return false; + return ["{", "}", ";", ","].includes(tokens[importIndex - 1]?.value); +} + +export function parseEsmSpecifiers(source) { + const tokens = tokenizeJavaScript(source); + const specifiers = []; + for (let index = 0; index < tokens.length; index += 1) { + const token = tokens[index]; + if (token.type !== "identifier" || token.value !== "import") continue; + if (tokens[index - 1]?.value === ".") continue; + if (isImportMethodName(tokens, index)) continue; + const following = tokens[index + 1]; + if (following?.value === ".") continue; + if (following?.value === "(") { + const argument = tokens[index + 2]; + if (argument?.type !== "string" || tokens[index + 3]?.value !== ")") { + fail("runtime-dynamic-import-unresolved", "Runtime contains a nonliteral dynamic import"); + } + specifiers.push(importSpecifier(argument, "runtime-dynamic-import-unresolved")); + continue; + } + if (following?.type === "string") { + specifiers.push(importSpecifier(following, "runtime-import-invalid")); + continue; + } + for (let cursor = index + 1; cursor < tokens.length; cursor += 1) { + if (tokens[cursor].value === ";") break; + if (tokens[cursor].type === "identifier" && tokens[cursor].value === "from") { + specifiers.push(importSpecifier(tokens[cursor + 1], "runtime-import-invalid")); + break; + } + } + } + for (let index = 0; index < tokens.length; index += 1) { + if ( + tokens[index].type !== "identifier" + || tokens[index].value !== "export" + || tokens[index].braceDepth !== 0 + || tokens[index - 1]?.value === "." + ) continue; + for (let cursor = index + 1; cursor < tokens.length; cursor += 1) { + if (tokens[cursor].value === ";") break; + if (tokens[cursor].type === "identifier" && tokens[cursor].value === "from") { + specifiers.push(importSpecifier(tokens[cursor + 1], "runtime-export-invalid")); + break; + } + } + } + return specifiers; +} + +function isAllowedBareDependency(specifier) { + return RUNTIME_DEPENDENCIES.some((dependency) => ( + specifier === dependency || specifier.startsWith(`${dependency}/`) + )); +} + +function splitBareDependency(specifier) { + const dependency = RUNTIME_DEPENDENCIES.find((candidate) => ( + specifier === candidate || specifier.startsWith(`${candidate}/`) + )); + if (!dependency) return null; + return { dependency, subpath: specifier.slice(dependency.length + 1) }; +} + +function dependencySubpathTarget(inventory, dependency, subpath) { + if (!subpath) return null; + if (subpath.includes("\\") || subpath.includes("\0")) { + fail("runtime-dependency-subpath-invalid", "Runtime dependency subpath is unsafe"); + } + let decoded; + try { + decoded = decodeURIComponent(subpath); + } catch { + fail("runtime-dependency-subpath-invalid", "Runtime dependency subpath has invalid encoding"); + } + const normalized = path.posix.normalize(decoded); + if ( + normalized !== decoded + || normalized === "." + || normalized === ".." + || normalized.startsWith("../") + || path.posix.isAbsolute(normalized) + ) { + fail("runtime-dependency-subpath-invalid", "Runtime dependency subpath escapes its package root"); + } + const base = `node_modules/${dependency}/${normalized}`; + const extension = path.posix.extname(base); + const candidates = extension + ? [base] + : [base, `${base}.mjs`, `${base}.js`, `${base}.json`, `${base}/index.mjs`, `${base}/index.js`, `${base}/index.json`]; + const matches = candidates.filter((candidate) => inventory.get(candidate)?.stats.isFile()); + if (matches.length === 0) fail("runtime-dependency-subpath-missing", "Runtime dependency subpath is missing"); + if (matches.length > 1) fail("runtime-dependency-subpath-ambiguous", "Runtime dependency subpath is ambiguous"); + assertSafeRelative(matches[0], "runtime-dependency-subpath-invalid"); + return matches[0]; +} + +function assertAllowedRuntimeRelative(relativePath) { + const normalized = path.posix.normalize(relativePath); + const first = normalized.split("/", 1)[0]; + if ( + normalized !== relativePath + || normalized === ".." + || normalized.startsWith("../") + || path.posix.isAbsolute(normalized) + || !RUNTIME_ROOTS.has(first) + || normalized === "scripts/packaging" + || normalized.startsWith("scripts/packaging/") + ) { + fail("runtime-target-forbidden", "Runtime module target is outside the allowed runtime roots", { target: relativePath }); + } +} + +function resolveRuntimeTarget(inventory, sourceRelative, rawSpecifier) { + if (rawSpecifier.startsWith("node:")) return { kind: "builtin" }; + if (!rawSpecifier.startsWith("./") && !rawSpecifier.startsWith("../")) { + if (!isAllowedBareDependency(rawSpecifier)) { + fail("runtime-dependency-forbidden", "Runtime imports an unexpected bare dependency"); + } + const parsed = splitBareDependency(rawSpecifier); + const subpathTarget = dependencySubpathTarget(inventory, parsed.dependency, parsed.subpath); + return { kind: "external", dependency: parsed.dependency, subpathTarget }; + } + if (rawSpecifier.includes("\\") || rawSpecifier.includes("\0")) { + fail("runtime-specifier-invalid", "Runtime import contains an unsafe path"); + } + const boundary = [rawSpecifier.indexOf("#"), rawSpecifier.indexOf("?")] + .filter((index) => index >= 0) + .reduce((minimum, index) => Math.min(minimum, index), rawSpecifier.length); + let decoded; + try { + decoded = decodeURIComponent(rawSpecifier.slice(0, boundary)); + } catch { + fail("runtime-percent-invalid", "Runtime import contains invalid percent encoding"); + } + const base = path.posix.normalize(path.posix.join(path.posix.dirname(sourceRelative), decoded)); + assertAllowedRuntimeRelative(base); + const extension = path.posix.extname(base); + const candidates = extension + ? [base] + : [base, `${base}.mjs`, `${base}.js`, `${base}.json`, `${base}/index.mjs`, `${base}/index.js`, `${base}/index.json`]; + const matches = candidates.filter((candidate) => inventory.get(candidate)?.stats.isFile()); + if (matches.length === 0) fail("runtime-target-missing", "Runtime import target is missing"); + if (matches.length > 1) fail("runtime-target-ambiguous", "Runtime import target is ambiguous"); + assertAllowedRuntimeRelative(matches[0]); + return { kind: "local", relative: matches[0] }; +} + +async function verifyRuntimeClosure(inventory) { + const queue = [{ relative: RUNTIME_ENTRY, depth: 0 }]; + const visited = new Set(); + const externalDependencies = new Set(); + let edges = 0; + + while (queue.length) { + const current = queue.shift(); + if (visited.has(current.relative)) continue; + if (visited.size >= GRAPH_LIMITS.runtimeModules) { + fail("runtime-node-limit", "Runtime closure exceeds the fixed module limit"); + } + visited.add(current.relative); + const entry = requireRegularFile(inventory, current.relative); + const extension = path.posix.extname(current.relative).toLowerCase(); + if (extension === ".json") continue; + if (![".mjs", ".js"].includes(extension)) { + fail("runtime-module-type-invalid", "Runtime closure contains an unsupported module type"); + } + const source = await readBoundedText(entry, "runtime-read-failed"); + for (const specifier of parseEsmSpecifiers(source)) { + edges += 1; + if (edges > GRAPH_LIMITS.runtimeEdges) { + fail("runtime-edge-limit", "Runtime closure exceeds the fixed edge limit"); + } + const target = resolveRuntimeTarget(inventory, current.relative, specifier); + if (target.kind === "external") externalDependencies.add(target.dependency); + if (target.kind === "local" && !visited.has(target.relative)) { + if (current.depth + 1 > GRAPH_LIMITS.runtimeDepth) { + fail("runtime-depth-limit", "Runtime closure exceeds the fixed depth limit"); + } + queue.push({ relative: target.relative, depth: current.depth + 1 }); + } + } + } + return { + modules: visited.size, + edges, + files: [...visited].sort(), + externalDependencies: [...externalDependencies].sort(), + }; +} + +export async function verifyAntigravityPluginArtifact(pluginRoot) { + try { + if (typeof pluginRoot !== "string" || !pluginRoot || pluginRoot.includes("\0")) { + fail("plugin-root-invalid", "Plugin root must be a nonempty filesystem path"); + } + const resolvedRoot = path.resolve(pluginRoot); + let rootStats; + try { + rootStats = await lstat(resolvedRoot); + } catch { + fail("plugin-root-missing", "Plugin root is missing or unreadable"); + } + if (rootStats.isSymbolicLink() || !rootStats.isDirectory()) { + fail("plugin-root-type-invalid", "Plugin root must be a real directory"); + } + let canonicalRoot; + try { + canonicalRoot = await realpath(resolvedRoot); + } catch { + fail("plugin-root-unresolvable", "Plugin root cannot be canonically resolved"); + } + const artifactDirectoryName = path.basename(canonicalRoot); + if (artifactDirectoryName !== ANTIGRAVITY_PLUGIN_NAME) { + fail("plugin-root-name-invalid", "Plugin root directory must be named better-harness"); + } + + const tree = await inspectArtifactTree(resolvedRoot, canonicalRoot); + for (const requiredPath of REQUIRED_FILES) requireRegularFile(tree.inventory, requiredPath); + + const manifest = await readJsonObject(tree.inventory, "plugin.json", "manifest-invalid"); + const marker = await readJsonObject( + tree.inventory, + ANTIGRAVITY_ARTIFACT_MARKER, + "marker-invalid", + ); + const packageJson = await readJsonObject(tree.inventory, "package.json", "package-invalid"); + validateMarker(marker); + const pluginName = effectivePluginName(manifest); + if (pluginName !== ANTIGRAVITY_PLUGIN_NAME || marker.pluginName !== pluginName) { + fail("plugin-identity-mismatch", "Plugin root, manifest, and ownership marker identities do not match"); + } + validatePackage(packageJson, marker); + await validateDependencyPackages(tree.inventory, packageJson); + + const markdown = await verifyMarkdownClosure(tree.inventory); + const runtime = await verifyRuntimeClosure(tree.inventory); + return { + kind: ANTIGRAVITY_ARTIFACT_KIND, + schemaVersion: ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + host: ANTIGRAVITY_HOST, + pluginName, + version: marker.version, + canonicalSkill: CANONICAL_SKILL, + runtimeDependencies: [...RUNTIME_DEPENDENCIES], + entryCount: tree.entryCount, + fileCount: tree.fileCount, + totalBytes: tree.totalBytes, + markdownClosure: markdown, + runtimeClosure: runtime, + }; + } catch (error) { + if (error instanceof AntigravityArtifactError) throw error; + fail("artifact-verification-failed", "Artifact verification failed at a protected read boundary"); + } +} + +export function parseArgs(argv) { + const options = { pluginRoot: null, json: false, help: false }; + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + if (arg === "--plugin-root") { + if (options.pluginRoot !== null) fail("argument-duplicate", "--plugin-root may be provided only once"); + const value = argv[index + 1]; + if (!value || value.startsWith("--")) fail("argument-value-missing", "--plugin-root requires a value"); + options.pluginRoot = value; + index += 1; + } else if (arg.startsWith("--plugin-root=")) { + if (options.pluginRoot !== null) fail("argument-duplicate", "--plugin-root may be provided only once"); + const value = arg.slice("--plugin-root=".length); + if (!value) fail("argument-value-missing", "--plugin-root requires a value"); + options.pluginRoot = value; + } else if (arg === "--json") { + options.json = true; + } else if (arg === "--help" || arg === "-h") { + options.help = true; + } else { + fail("argument-unknown", "Unknown argument"); + } + } + if (!options.help && options.pluginRoot === null) { + fail("argument-required", "--plugin-root is required"); + } + return options; +} + +export function usage() { + return [ + "Usage: node scripts/packaging/antigravity/verify-antigravity-plugin.mjs --plugin-root [options]", + "", + "Options:", + " --plugin-root Generated Antigravity plugin root (required)", + " --json Emit parser-safe JSON", + " -h, --help Print help", + "", + ].join("\n"); +} + +export async function main(argv = process.argv.slice(2)) { + let options; + try { + options = parseArgs(argv); + if (options.help) { + process.stdout.write(usage()); + return 0; + } + const result = await verifyAntigravityPluginArtifact(options.pluginRoot); + process.stdout.write(options.json + ? `${JSON.stringify({ ok: true, data: result }, null, 2)}\n` + : `Antigravity plugin artifact verified: ${result.pluginName} ${result.version} (${result.fileCount} files)\n`); + return 0; + } catch (error) { + const code = error instanceof AntigravityArtifactError + ? error.code + : "artifact-verification-failed"; + const message = error instanceof AntigravityArtifactError + ? error.message + : "Artifact verification failed"; + if (options?.json || argv.includes("--json")) { + process.stdout.write(`${JSON.stringify({ ok: false, error: { code, message } }, null, 2)}\n`); + } else { + process.stderr.write(`Antigravity plugin verification failed [${code}]: ${message}\n`); + } + return 1; + } +} + +const currentFile = fileURLToPath(import.meta.url); +if (process.argv[1] && path.resolve(process.argv[1]) === currentFile) { + process.exitCode = await main(); +} diff --git a/test/plugins/antigravity-plugin-artifact.test.mjs b/test/plugins/antigravity-plugin-artifact.test.mjs new file mode 100644 index 00000000..7675d0ef --- /dev/null +++ b/test/plugins/antigravity-plugin-artifact.test.mjs @@ -0,0 +1,1514 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { + cp, + lstat, + mkdir, + mkdtemp, + readFile, + realpath, + readdir, + rename, + rm, + symlink, + writeFile, +} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { test } from "vitest"; + +import { + artifactPackage, + assertOutputBoundary, + buildAntigravityPluginArtifact, + formatBuildSuccess, + publishStagedArtifact, +} from "../../scripts/packaging/antigravity/build-antigravity-plugin.mjs"; + +import { + ANTIGRAVITY_ARTIFACT_KIND, + ANTIGRAVITY_ARTIFACT_MARKER, + ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + ANTIGRAVITY_HOST, + CANONICAL_SKILL, + FILE_LIMITS, + GRAPH_LIMITS, + RUNTIME_DEPENDENCIES, + parseEsmSpecifiers, + parseMarkdownTargets, + validatePortablePathComponent, + validateTraversalBounds, + verifyAntigravityPluginArtifact, + verifyMarkdownSourceClosure, +} from "../../scripts/packaging/antigravity/verify-antigravity-plugin.mjs"; + +const currentDir = path.dirname(fileURLToPath(import.meta.url)); +const repositoryRoot = path.resolve(currentDir, "../.."); +const verifierPath = path.resolve( + currentDir, + "../../scripts/packaging/antigravity/verify-antigravity-plugin.mjs", +); +const builderPath = path.resolve( + currentDir, + "../../scripts/packaging/antigravity/build-antigravity-plugin.mjs", +); + +async function writeArtifactFile(root, relativePath, content = "fixture\n") { + const target = path.join(root, ...relativePath.split("/")); + await mkdir(path.dirname(target), { recursive: true }); + await writeFile(target, content, "utf8"); +} + +function baseMarker(overrides = {}) { + return { + kind: ANTIGRAVITY_ARTIFACT_KIND, + schemaVersion: ANTIGRAVITY_ARTIFACT_SCHEMA_VERSION, + host: ANTIGRAVITY_HOST, + pluginName: "better-harness", + version: "1.2.3", + canonicalSkill: CANONICAL_SKILL, + runtimeDependencies: [...RUNTIME_DEPENDENCIES], + ...overrides, + }; +} + +function basePackage(overrides = {}) { + return { + name: "@qoder-ai/better-harness", + version: "1.2.3", + private: true, + license: "MIT", + type: "module", + bin: { "better-harness": "scripts/better-harness.mjs" }, + engines: { node: ">=22", npm: ">=10" }, + dependencies: { + "@vscode/tree-sitter-wasm": "1.0.0", + "esbuild-wasm": "1.0.0", + }, + ...overrides, + }; +} + +function baseSourcePackage(overrides = {}) { + return { + name: "@qoder-ai/better-harness", + version: "1.2.3", + license: "MIT", + type: "module", + bin: { "better-harness": "scripts/better-harness.mjs" }, + engines: { node: ">=22", npm: ">=10" }, + dependencies: { + "@vscode/tree-sitter-wasm": "1.0.0", + "esbuild-wasm": "1.0.0", + }, + ...overrides, + }; +} + +async function createArtifact({ + rootName = "better-harness", + manifest = { name: "better-harness" }, + manifestText, + marker = baseMarker(), + packageJson = basePackage(), +} = {}) { + const container = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-test-")); + const pluginRoot = path.join(container, rootName); + await mkdir(pluginRoot); + for (const relativePath of [ + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + ]) { + await writeArtifactFile(pluginRoot, relativePath); + } + await writeArtifactFile( + pluginRoot, + "plugin.json", + manifestText ?? `${JSON.stringify(manifest)}\n`, + ); + await writeArtifactFile( + pluginRoot, + ANTIGRAVITY_ARTIFACT_MARKER, + `${JSON.stringify(marker)}\n`, + ); + await writeArtifactFile(pluginRoot, "package.json", `${JSON.stringify(packageJson)}\n`); + await writeArtifactFile( + pluginRoot, + CANONICAL_SKILL, + [ + "# Better Harness", + "", + "[Direct](../../references/direct.md)", + "[Guide][guide]", + "[guide]: ../../docs/guide.md#start", + "", + ].join("\n"), + ); + await writeArtifactFile( + pluginRoot, + "references/direct.md", + "[Transitive](../templates/transitive.md?mode=test)\n", + ); + await writeArtifactFile( + pluginRoot, + "templates/transitive.md", + "[Cycle](../skills/better-harness/SKILL.md)\n", + ); + await writeArtifactFile(pluginRoot, "docs/guide.md", "# Guide\n"); + await writeArtifactFile( + pluginRoot, + "scripts/better-harness.mjs", + [ + 'import "./runtime/main.mjs";', + 'export { exported } from "./runtime/exported.mjs";', + 'export const dynamic = import("./runtime/dynamic.mjs");', + "", + ].join("\n"), + ); + await writeArtifactFile( + pluginRoot, + "scripts/runtime/main.mjs", + [ + 'import "node:path";', + 'import "@vscode/tree-sitter-wasm";', + 'export * from "./helper.mjs";', + "", + ].join("\n"), + ); + await writeArtifactFile(pluginRoot, "scripts/runtime/helper.mjs", "export const helper = true;\n"); + await writeArtifactFile(pluginRoot, "scripts/runtime/exported.mjs", "export const exported = true;\n"); + await writeArtifactFile(pluginRoot, "scripts/runtime/dynamic.mjs", "export default true;\n"); + await writeArtifactFile( + pluginRoot, + "node_modules/@vscode/tree-sitter-wasm/package.json", + '{"name":"@vscode/tree-sitter-wasm","version":"1.0.0"}\n', + ); + await writeArtifactFile(pluginRoot, "node_modules/@vscode/tree-sitter-wasm/LICENSE"); + await writeArtifactFile( + pluginRoot, + "node_modules/esbuild-wasm/package.json", + '{"name":"esbuild-wasm","version":"1.0.0"}\n', + ); + await writeArtifactFile(pluginRoot, "node_modules/esbuild-wasm/worker.mjs", "export default true;\n"); + await writeArtifactFile(pluginRoot, "node_modules/esbuild-wasm/LICENSE.md"); + return { container, pluginRoot }; +} + +async function withArtifact(options, callback) { + const artifact = await createArtifact(options); + try { + return await callback(artifact); + } finally { + await rm(artifact.container, { recursive: true, force: true }); + } +} + +async function createPublishFixture() { + const { container, pluginRoot: outputRoot } = await createArtifact(); + const stageRoot = path.join(container, "stage-container", "better-harness"); + await cp(outputRoot, stageRoot, { recursive: true }); + return { container, outputRoot, stageRoot }; +} + +async function createSourceRepo({ manifestText, packageJson } = {}) { + const repoRoot = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-source-")); + for (const relative of [ + "README.md", + "AGENTS.md", + "DESIGN.md", + "LICENSE", + "CHANGELOG.md", + "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", + ]) await writeArtifactFile(repoRoot, relative); + const sourcePackage = packageJson ?? baseSourcePackage(); + await writeArtifactFile(repoRoot, "package.json", `${JSON.stringify(sourcePackage)}\n`); + await writeArtifactFile( + repoRoot, + "scripts/packaging/antigravity/plugin-manifest.json", + manifestText ?? '{"name":"better-harness"}\n', + ); + await writeArtifactFile(repoRoot, CANONICAL_SKILL, "# Better Harness\n"); + await writeArtifactFile(repoRoot, "scripts/better-harness.mjs", "export const fixture = true;\n"); + for (const dependency of RUNTIME_DEPENDENCIES) { + await writeArtifactFile( + repoRoot, + `node_modules/${dependency}/package.json`, + `${JSON.stringify({ name: dependency, version: "1.0.0" })}\n`, + ); + const license = dependency === "esbuild-wasm" ? "LICENSE.md" : "LICENSE"; + await writeArtifactFile(repoRoot, `node_modules/${dependency}/${license}`); + } + return repoRoot; +} + +async function expectCode(promise, code) { + const outcome = promise.then( + (value) => ({ status: "fulfilled", value }), + (error) => ({ status: "rejected", error }), + ); + const tempPrefixes = [...new Set([path.resolve(os.tmpdir()), await realpath(os.tmpdir())])] + .filter((tempPrefix) => tempPrefix !== path.parse(tempPrefix).root); + await assert.rejects( + outcome.then((result) => { + if (result.status === "rejected") throw result.error; + return result.value; + }), + (error) => { + assert.equal(error.code, code); + for (const tempPrefix of tempPrefixes) { + assert.equal(error.message.includes(tempPrefix), false); + } + return true; + }, + ); +} + +test("verifies the closed Agy CLI manifest variants with recursive Markdown and runtime closure", async () => { + for (const manifest of [ + { name: "better-harness" }, + { name: "better-harness", description: "" }, + { name: "better-harness", description: "Better Harness" }, + ]) { + await withArtifact({ manifest }, async ({ pluginRoot }) => { + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.equal(result.pluginName, "better-harness"); + assert.equal(result.version, "1.2.3"); + assert.deepEqual(result.runtimeDependencies, RUNTIME_DEPENDENCIES); + assert.ok(result.markdownClosure.files.includes("references/direct.md")); + assert.ok(result.markdownClosure.files.includes("templates/transitive.md")); + assert.ok(result.markdownClosure.nodes < 10, "cycle must remain bounded"); + assert.ok(result.runtimeClosure.files.includes("scripts/runtime/helper.mjs")); + assert.ok(result.runtimeClosure.files.includes("scripts/runtime/dynamic.mjs")); + assert.equal(JSON.stringify(result).includes(pluginRoot), false); + }); + } +}); + +test("parses only syntax-authoritative ESM dependencies", () => { + const source = String.raw` + import "./side-effect.mjs"; + import value from "./value.mjs"; + export { value } from "./exported.mjs"; + const ordinary = "escaped\\nstring import('ignored-one')"; + const pattern = /import\("ignored-two"\)/gu; + const object = { import() {} }; + object.import("ignored-three"); + const exporter = { export() { const from = 1; return from; } }; + exporter.export(from); + const meta = import.meta.url; + if (ok) /import("ignored-six")/.test(value); + while (ok) /import("ignored-seven")/.test(value); + do /import("ignored-eight")/.test(value); while (ok); + for await (const item of items) /import("ignored-nine")/.test(item); + // import("ignored-four") + /* export * from "ignored-five" */ + const template = ` + "`text ${import(\"./template-literal.mjs\")} ${ordinary}`" + `; + `; + assert.deepEqual(parseEsmSpecifiers(source), [ + "./side-effect.mjs", + "./value.mjs", + "./template-literal.mjs", + "./exported.mjs", + ]); + assert.throws( + () => parseEsmSpecifiers("const value = `x ${import(target)}`;"), + (error) => error.code === "runtime-dynamic-import-unresolved", + ); + assert.deepEqual(parseEsmSpecifiers([ + "const object = { import(value = import('method-default')) { return import('method-body'); } };", + "class Example { import(value) { return value; } }", + ].join("\n")), ["method-default", "method-body"]); + assert.throws( + () => parseEsmSpecifiers("const value = import(target);"), + (error) => error.code === "runtime-dynamic-import-unresolved", + ); +}); + +test("verifies the pinned real runtime closure through better-harness-cli", async () => { + await withArtifact({}, async ({ pluginRoot }) => { + await rm(path.join(pluginRoot, "scripts"), { recursive: true, force: true }); + await cp(path.join(repositoryRoot, "scripts"), path.join(pluginRoot, "scripts"), { + recursive: true, + filter: (source) => !source.includes(`${path.sep}scripts${path.sep}packaging`), + }); + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.ok(result.runtimeClosure.files.includes("scripts/better-harness.mjs")); + assert.ok(result.runtimeClosure.files.includes("scripts/better-harness-cli/cli.mjs")); + assert.equal(result.runtimeClosure.modules, 19); + }); +}); + +test("parses Markdown destinations without treating code as closure", () => { + const targets = parseMarkdownTargets([ + "[angle](<../../docs/my file.md>)", + "[balanced](../../docs/a_(b).md)", + String.raw`[escaped](../../docs/a_\(b\).md)`, + "[reference][guide]", + "[collapsed][]", + "[shortcut]", + "[outer [inner]](../../docs/nested.md)", + String.raw`[outer \[escaped\]](../../docs/escaped.md)`, + "[`inline-code-label`](../../docs/code-label.md)", + "[multi", + " line](../../docs/multiline.md)", + "[guide]: ../../docs/reference.md", + "[collapsed]: ../../docs/collapsed.md", + "[shortcut]: ../../docs/shortcut.md", + "`[inline](../../docs/ignored-inline.md)`", + "```md", + "[fenced](../../docs/ignored-fenced.md)", + "```", + " [indented](../../docs/ignored-indented.md)", + "- item", + " [list-active](../../docs/list-active.md)", + " - nested", + " [nested-active](../../docs/nested-active.md)", + " [nested-code](../../docs/ignored-nested-code.md)", + "", + "[dedented](../../docs/dedented.md)", + ].join("\n")); + assert.deepEqual(targets, [ + "../../docs/my file.md", + "../../docs/a_(b).md", + "../../docs/a_(b).md", + "../../docs/reference.md", + "../../docs/collapsed.md", + "../../docs/shortcut.md", + "../../docs/nested.md", + "../../docs/escaped.md", + "../../docs/code-label.md", + "../../docs/multiline.md", + "../../docs/list-active.md", + "../../docs/nested-active.md", + "../../docs/dedented.md", + ]); + for (const malformed of [ + "[x]( parseMarkdownTargets(malformed), + (error) => error.code === "markdown-link-unsupported", + ); + } +}); + +test("resolves extensionless Markdown targets uniquely and classifies schemes", async () => { + for (const scenario of [ + { target: "../../docs/exact", file: "docs/exact" }, + { target: "../../docs/candidate", file: "docs/candidate.md" }, + { target: "../../docs/guide-dir", file: "docs/guide-dir/README.md" }, + ]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, `[target](${scenario.target})\n`); + await writeArtifactFile(pluginRoot, scenario.file, "fixture\n"); + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.ok(result.markdownClosure.files.includes(scenario.file)); + }); + } + + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, "[target](../../docs/ambiguous)\n"); + await writeArtifactFile(pluginRoot, "docs/ambiguous", "fixture\n"); + await writeArtifactFile(pluginRoot, "docs/ambiguous.md", "fixture\n"); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "markdown-target-ambiguous"); + }); + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, "[target](../../docs/extensionless-missing)\n"); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "required-file-missing"); + }); + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, [ + "[pathname](pathname:///demo/adapter-matrix)", + "[http](http://example.com)", + "[https](https://example.com)", + "[mail](mailto:test@example.com)", + "", + ].join("\n")); + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.equal(result.markdownClosure.edges, 0); + }); + for (const target of ["file:///private.md", "custom://route"]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, `[target](${target})\n`); + await expectCode( + verifyAntigravityPluginArtifact(pluginRoot), + target.startsWith("file:") ? "markdown-file-uri" : "markdown-scheme-invalid", + ); + }); + } +}); + +test("freezes the pinned canonical Markdown closure and source link classification", async () => { + const closure = await verifyMarkdownSourceClosure(repositoryRoot); + assert.deepEqual( + { nodes: closure.nodes, edges: closure.edges, files: closure.files.length }, + { nodes: 93, edges: 290, files: 96 }, + ); + for (const required of [ + "AGENTS.md", + "DESIGN.md", + "docs/adrs/checkpoint-backed-compare-sources.md", + "docs/adrs/harness-checkpoint-experiment-compare.md", + "docs/docs/hosts/adapter-matrix.md", + "docs/docs/hosts/contributing-new-coding-agent.md", + "references/agent-customize/platforms/qoder.md", + "references/agent-customize/agents-md-review.md", + "references/agent-customize/custom-agents-review.md", + "references/project-harness/observability.md", + ]) assert.ok(closure.files.includes(required), `missing frozen closure file: ${required}`); + assert.equal(closure.files.includes("references/agent-customize/platforms/codex.md"), false); + assert.equal(closure.files.some((file) => file.startsWith(".agents/") || file.startsWith(".github/")), false); + + const sourceAssertions = [ + ["references/project-harness/observability.md", "(../agent-customize/agents-md-review.md)"], + ["references/agent-customize/platforms/qoder.md", "(../custom-agents-review.md)"], + ["references/agent-customize/platforms/codex.md", "(../custom-agents-review.md)"], + ["docs/adapters/contributing-new-coding-agent.md", "https://github.com/QoderAI/better-harness/blob/main/.agents/skills/change-traceability-review/SKILL.md"], + ["docs/adapters/contributing-new-coding-agent.md", "https://github.com/QoderAI/better-harness/blob/main/.github/pull_request_template.md"], + ]; + for (const [relative, expected] of sourceAssertions) { + assert.ok((await readFile(path.join(repositoryRoot, relative), "utf8")).includes(expected)); + } +}); + +test("uses the first duplicate Markdown reference definition", async () => { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, [ + "[reference][duplicate]", + "[duplicate]: ../../../outside.md", + "[duplicate]: ../../docs/guide.md", + "", + ].join("\n")); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "markdown-path-escape"); + }); +}); + +test("rejects invalid manifest identities and shapes", async () => { + const cases = [ + { options: { rootName: "wrong-root", manifest: {} }, code: "plugin-root-name-invalid" }, + { options: { manifest: {} }, code: "manifest-name-required" }, + { options: { manifest: { description: "missing name" } }, code: "manifest-name-required" }, + { options: { manifest: { name: "" } }, code: "manifest-name-blank" }, + { options: { manifest: { name: 7 } }, code: "manifest-name-type-invalid" }, + { options: { manifest: { name: "better harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better/harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better.harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better:harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better\\harness" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "better[harness]" } }, code: "manifest-name-pattern-invalid" }, + { options: { manifest: { name: "other" } }, code: "plugin-identity-mismatch" }, + { options: { manifest: { name: "better-harness", description: null } }, code: "manifest-description-invalid" }, + { options: { manifest: { name: "better-harness", description: 7 } }, code: "manifest-description-invalid" }, + { options: { manifest: { name: "better-harness", unexpected: true } }, code: "manifest-schema-invalid" }, + { + options: { manifest: { name: "better-harness", $schema: "https://example.invalid/schema.json" } }, + code: "manifest-schema-invalid", + }, + { options: { manifestText: "null\n" }, code: "manifest-invalid" }, + { options: { manifestText: "[]\n" }, code: "manifest-invalid" }, + { options: { manifestText: "{\n" }, code: "manifest-invalid" }, + ]; + for (const scenario of cases) { + await withArtifact(scenario.options, async ({ pluginRoot }) => { + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } +}); + +test("rejects missing, escaped, forbidden, and oversized Markdown closure targets", async () => { + const cases = [ + { content: "[Missing](../../references/missing.md)\n", code: "required-file-missing" }, + { content: "[Escape](../../../outside.md)\n", code: "markdown-path-escape" }, + { content: "[Absolute](C:/private/file.md)\n", code: "markdown-absolute-path" }, + { content: "[File](file:///private/file.md)\n", code: "markdown-file-uri" }, + { content: "[Encoded](../../%2e%2e/outside.md)\n", code: "markdown-path-escape" }, + { content: "[Forbidden](../../scripts/packaging/private.md)\n", code: "markdown-target-forbidden" }, + ]; + for (const scenario of cases) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, scenario.content); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } + + await withArtifact({}, async ({ pluginRoot }) => { + const links = []; + for (let index = 0; index <= GRAPH_LIMITS.markdownNodes; index += 1) { + const relative = `docs/limit-${index}.md`; + links.push(`[${index}](../../${relative})`); + await writeArtifactFile(pluginRoot, relative, `# ${index}\n`); + } + await writeArtifactFile(pluginRoot, CANONICAL_SKILL, `${links.join("\n")}\n`); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "markdown-node-limit"); + }); +}); + +test("rejects unsafe runtime closure imports", async () => { + const cases = [ + { source: 'import "./missing.mjs";\n', code: "runtime-target-missing" }, + { source: 'import "./packaging/private.mjs";\n', code: "runtime-target-forbidden" }, + { source: 'import "unexpected-package";\n', code: "runtime-dependency-forbidden" }, + { source: "const target = './runtime/main.mjs'; import(target);\n", code: "runtime-dynamic-import-unresolved" }, + ]; + for (const scenario of cases) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "scripts/better-harness.mjs", scenario.source); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } +}); + +test("enforces every closed ownership marker field", async () => { + const withoutCanonicalSkill = baseMarker(); + delete withoutCanonicalSkill.canonicalSkill; + const cases = [ + { marker: withoutCanonicalSkill, code: "marker-schema-invalid" }, + { marker: { ...baseMarker(), extra: true }, code: "marker-schema-invalid" }, + { marker: baseMarker({ kind: "other" }), code: "marker-kind-invalid" }, + { marker: baseMarker({ schemaVersion: 2 }), code: "marker-schema-version-invalid" }, + { marker: baseMarker({ host: "other" }), code: "marker-host-invalid" }, + { marker: baseMarker({ pluginName: "other" }), code: "plugin-identity-mismatch" }, + { marker: baseMarker({ pluginName: 7 }), code: "marker-plugin-name-invalid" }, + { marker: baseMarker({ version: "" }), code: "marker-version-invalid" }, + { marker: baseMarker({ version: "9.9.9" }), code: "package-version-invalid" }, + { marker: baseMarker({ canonicalSkill: "skills/other/SKILL.md" }), code: "marker-skill-invalid" }, + { marker: baseMarker({ runtimeDependencies: [...RUNTIME_DEPENDENCIES].reverse() }), code: "marker-dependencies-invalid" }, + { marker: baseMarker({ runtimeDependencies: "invalid" }), code: "marker-dependencies-invalid" }, + ]; + for (const scenario of cases) { + await withArtifact({ marker: scenario.marker }, async ({ pluginRoot }) => { + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } +}); + +test("enforces the closed Better Harness artifact package schema", async () => { + const withoutName = basePackage(); + delete withoutName.name; + const withoutBin = basePackage(); + delete withoutBin.bin; + const cases = [ + { packageJson: { ...basePackage(), extra: true }, code: "package-schema-invalid" }, + { packageJson: withoutName, code: "package-schema-invalid" }, + { packageJson: withoutBin, code: "package-schema-invalid" }, + { packageJson: basePackage({ name: "other" }), code: "package-name-invalid" }, + { packageJson: basePackage({ private: false }), code: "package-private-invalid" }, + { packageJson: basePackage({ version: "" }), code: "package-version-invalid" }, + { packageJson: basePackage({ version: "9.9.9" }), code: "package-version-invalid" }, + { packageJson: basePackage({ license: " " }), code: "package-license-invalid" }, + { packageJson: basePackage({ type: "commonjs" }), code: "package-type-invalid" }, + { packageJson: basePackage({ bin: null }), code: "package-bin-invalid" }, + { packageJson: basePackage({ bin: [] }), code: "package-bin-invalid" }, + { packageJson: basePackage({ bin: {} }), code: "package-bin-invalid" }, + { packageJson: basePackage({ bin: { "better-harness": "wrong.mjs" } }), code: "package-bin-invalid" }, + { + packageJson: basePackage({ + bin: { "better-harness": "scripts/better-harness.mjs", extra: "wrong.mjs" }, + }), + code: "package-bin-invalid", + }, + { packageJson: basePackage({ engines: null }), code: "package-engines-invalid" }, + { packageJson: basePackage({ engines: [] }), code: "package-engines-invalid" }, + { packageJson: basePackage({ engines: { node: ">=22" } }), code: "package-engines-invalid" }, + { packageJson: basePackage({ engines: { node: ">=22", npm: "" } }), code: "package-engines-invalid" }, + { packageJson: basePackage({ engines: { node: 22, npm: ">=10" } }), code: "package-engines-invalid" }, + { + packageJson: basePackage({ engines: { node: ">=22", npm: ">=10", extra: "1" } }), + code: "package-engines-invalid", + }, + { packageJson: basePackage({ dependencies: null }), code: "package-dependencies-invalid" }, + { + packageJson: basePackage({ + dependencies: { ...basePackage().dependencies, unexpected: "1.0.0" }, + }), + code: "package-dependencies-invalid", + }, + { + packageJson: basePackage({ dependencies: { "esbuild-wasm": "1.0.0" } }), + code: "package-dependencies-invalid", + }, + { + packageJson: basePackage({ + dependencies: { + "@vscode/tree-sitter-wasm": "", + "esbuild-wasm": "1.0.0", + }, + }), + code: "package-dependency-version-invalid", + }, + { + packageJson: basePackage({ + dependencies: { + "@vscode/tree-sitter-wasm": 1, + "esbuild-wasm": "1.0.0", + }, + }), + code: "package-dependency-version-invalid", + }, + ]; + for (const scenario of cases) { + await withArtifact({ packageJson: scenario.packageJson }, async ({ pluginRoot }) => { + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } + + for (const packageText of ["null\n", "[]\n", "{\n"]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "package.json", packageText); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "package-invalid"); + }); + } +}); + +test("projects a fresh exact artifact package and rejects malformed source runtime metadata", () => { + const source = baseSourcePackage({ description: "allowed source-only metadata" }); + const projected = artifactPackage(source); + assert.deepEqual(Object.keys(projected).sort(), [ + "bin", + "dependencies", + "engines", + "license", + "name", + "private", + "type", + "version", + ]); + assert.deepEqual(projected, basePackage()); + + source.bin["better-harness"] = "mutated.mjs"; + source.engines.node = "mutated"; + source.dependencies["esbuild-wasm"] = "mutated"; + assert.equal(projected.bin["better-harness"], "scripts/better-harness.mjs"); + assert.equal(projected.engines.node, ">=22"); + assert.equal(projected.dependencies["esbuild-wasm"], "1.0.0"); + + const cases = [ + { value: baseSourcePackage({ name: "wrong" }), code: "source-package-invalid" }, + { value: baseSourcePackage({ type: "commonjs" }), code: "source-package-invalid" }, + { value: baseSourcePackage({ bin: null }), code: "source-bin-invalid" }, + { value: baseSourcePackage({ bin: [] }), code: "source-bin-invalid" }, + { value: baseSourcePackage({ bin: {} }), code: "source-bin-invalid" }, + { value: baseSourcePackage({ bin: { "better-harness": "wrong.mjs" } }), code: "source-bin-invalid" }, + { + value: baseSourcePackage({ + bin: { "better-harness": "scripts/better-harness.mjs", extra: "wrong.mjs" }, + }), + code: "source-bin-invalid", + }, + { value: baseSourcePackage({ engines: null }), code: "source-engines-invalid" }, + { value: baseSourcePackage({ engines: [] }), code: "source-engines-invalid" }, + { value: baseSourcePackage({ engines: { node: ">=22" } }), code: "source-engines-invalid" }, + { value: baseSourcePackage({ engines: { node: ">=22", npm: " " } }), code: "source-engines-invalid" }, + { value: baseSourcePackage({ engines: { node: 22, npm: ">=10" } }), code: "source-engines-invalid" }, + { + value: baseSourcePackage({ engines: { node: ">=22", npm: ">=10", extra: "1" } }), + code: "source-engines-invalid", + }, + ]; + for (const scenario of cases) { + assert.throws(() => artifactPackage(scenario.value), (error) => error.code === scenario.code); + } +}); + +test("binds dependency package identity, version, and imported subpaths", async () => { + const metadataCases = [ + { relative: "node_modules/esbuild-wasm/package.json", text: "{\n", code: "dependency-package-invalid" }, + { relative: "node_modules/esbuild-wasm/package.json", text: "null\n", code: "dependency-package-invalid" }, + { relative: "node_modules/esbuild-wasm/package.json", text: '{"name":"wrong","version":"1.0.0"}\n', code: "dependency-package-name-invalid" }, + { relative: "node_modules/esbuild-wasm/package.json", text: '{"name":"esbuild-wasm"}\n', code: "dependency-package-version-invalid" }, + { relative: "node_modules/esbuild-wasm/package.json", text: '{"name":"esbuild-wasm","version":"2.0.0"}\n', code: "dependency-package-version-invalid" }, + { relative: "node_modules/@vscode/tree-sitter-wasm/package.json", text: '{"name":"wrong","version":"1.0.0"}\n', code: "dependency-package-name-invalid" }, + ]; + for (const scenario of metadataCases) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, scenario.relative, scenario.text); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), scenario.code); + }); + } + + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "scripts/better-harness.mjs", 'import "esbuild-wasm/worker";\n'); + const result = await verifyAntigravityPluginArtifact(pluginRoot); + assert.deepEqual(result.runtimeClosure.externalDependencies, ["esbuild-wasm"]); + }); + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "scripts/better-harness.mjs", 'import "esbuild-wasm/missing";\n'); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "runtime-dependency-subpath-missing"); + }); + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "node_modules/esbuild-wasm/worker.js", "export default true;\n"); + await writeArtifactFile(pluginRoot, "scripts/better-harness.mjs", 'import "esbuild-wasm/worker";\n'); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "runtime-dependency-subpath-ambiguous"); + }); +}); + +test("enforces host-neutral portable components and traversal bounds", () => { + for (const component of [ + "CON", + "con.txt", + "PrN.json", + "COM1", + "lpt9.log", + "trailing.", + "trailing ", + "stream:name", + "back\\slash", + `control${String.fromCharCode(1)}`, + "x".repeat(FILE_LIMITS.componentLength + 1), + ]) { + assert.throws( + () => validatePortablePathComponent(component), + (error) => error.code === "path-component-invalid", + ); + } + assert.equal(validatePortablePathComponent("portable-name.md"), "portable-name.md"); + assert.throws( + () => validateTraversalBounds(FILE_LIMITS.entries + 1, 0), + (error) => error.code === "artifact-entry-limit", + ); + assert.throws( + () => validateTraversalBounds(0, FILE_LIMITS.directoryDepth + 1), + (error) => error.code === "artifact-depth-limit", + ); +}); + +test("rejects unknown, host, development, Skill, and packaging tree entries", async () => { + for (const relativePath of [ + "unknown.txt", + ".codex-plugin/plugin.json", + "skills/other/SKILL.md", + "scripts/packaging/private.mjs", + "test/fixture.txt", + "docs/cache/output.txt", + "docs/private.log", + ]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, relativePath); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "path-not-allowed"); + }); + } +}); + +test("requires the complete artifact and dependency license profile", async () => { + for (const relativePath of [ + "LICENSE", + CANONICAL_SKILL, + "scripts/better-harness.mjs", + "node_modules/@vscode/tree-sitter-wasm/package.json", + "node_modules/@vscode/tree-sitter-wasm/LICENSE", + "node_modules/esbuild-wasm/LICENSE.md", + ]) { + await withArtifact({}, async ({ pluginRoot }) => { + await rm(path.join(pluginRoot, ...relativePath.split("/"))); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "required-file-missing"); + }); + } + + await withArtifact({}, async ({ pluginRoot }) => { + const license = path.join(pluginRoot, "LICENSE"); + await rm(license); + await mkdir(license); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "required-file-missing"); + }); +}); + +test("rejects a missing transitive Markdown target", async () => { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, "references/direct.md", "[Missing](../docs/missing.md)\n"); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "required-file-missing"); + }); +}); + +test("rejects case-insensitive or NFC path identity collisions where constructable", async (context) => { + let exercised = false; + for (const pair of [ + ["docs/Case.md", "docs/case.md"], + ["docs/caf\u00e9.md", "docs/cafe\u0301.md"], + ]) { + await withArtifact({}, async ({ pluginRoot }) => { + await writeArtifactFile(pluginRoot, pair[0]); + await writeArtifactFile(pluginRoot, pair[1]); + const names = await readdir(path.join(pluginRoot, "docs")); + if (!pair.every((relative) => names.includes(path.basename(relative)))) return; + exercised = true; + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "path-identity-collision"); + }); + } + if (!exercised) context.skip("filesystem does not preserve a constructable case or NFC collision"); +}); + +test("rejects special files where a FIFO can be created", async (context) => { + if (process.platform === "win32") { + context.skip("portable FIFO creation is unavailable on Windows"); + return; + } + await withArtifact({}, async ({ pluginRoot }) => { + const fifo = path.join(pluginRoot, "docs", "fixture.fifo"); + const created = spawnSync("mkfifo", [fifo], { encoding: "utf8" }); + if (created.status !== 0) { + context.skip("mkfifo is unavailable in this environment"); + return; + } + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "special-file-forbidden"); + }); +}); + +test("CLI help and JSON envelopes are parser-safe and imports have no side effects", async () => { + const imported = spawnSync( + process.execPath, + ["--input-type=module", "--eval", `await import(${JSON.stringify(pathToFileURL(verifierPath).href)})`], + { encoding: "utf8" }, + ); + assert.equal(imported.status, 0, imported.stderr); + assert.equal(imported.stdout, ""); + assert.equal(imported.stderr, ""); + + const help = spawnSync(process.execPath, [verifierPath, "--help"], { encoding: "utf8" }); + assert.equal(help.status, 0, help.stderr); + assert.match(help.stdout, /--plugin-root /u); + + for (const args of [["--json"], ["--json", "--unknown"], ["--json", "--plugin-root"]]) { + const failure = spawnSync(process.execPath, [verifierPath, ...args], { encoding: "utf8" }); + assert.equal(failure.status, 1); + assert.equal(failure.stderr, ""); + const envelope = JSON.parse(failure.stdout); + assert.equal(envelope.ok, false); + assert.equal(typeof envelope.error.code, "string"); + } + + const duplicate = spawnSync( + process.execPath, + [verifierPath, "--json", "--plugin-root", "one", "--plugin-root", "two"], + { encoding: "utf8" }, + ); + assert.equal(duplicate.status, 1); + assert.equal(JSON.parse(duplicate.stdout).error.code, "argument-duplicate"); + + await withArtifact({}, async ({ pluginRoot }) => { + const success = spawnSync( + process.execPath, + [verifierPath, "--plugin-root", pluginRoot, "--json"], + { encoding: "utf8" }, + ); + assert.equal(success.status, 0, success.stderr); + const envelope = JSON.parse(success.stdout); + assert.equal(envelope.ok, true); + assert.equal(envelope.data.pluginName, "better-harness"); + assert.equal(success.stdout.includes(pluginRoot), false); + + const missingRoot = path.join(pluginRoot, "private", "missing"); + const failure = spawnSync( + process.execPath, + [verifierPath, `--plugin-root=${missingRoot}`, "--json"], + { encoding: "utf8" }, + ); + assert.equal(failure.status, 1); + assert.equal(failure.stdout.includes(missingRoot), false); + assert.equal(JSON.parse(failure.stdout).error.code, "plugin-root-missing"); + }); +}); + +test("rejects symbolic links when the platform permits creating one", async (context) => { + await withArtifact({}, async ({ pluginRoot }) => { + const target = path.join(pluginRoot, "docs", "guide.md"); + const link = path.join(pluginRoot, "docs", "linked.md"); + try { + await symlink(target, link, "file"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`symlink creation unavailable: ${error.code}`); + return; + } + throw error; + } + assert.equal((await lstat(link)).isSymbolicLink(), true); + await expectCode(verifyAntigravityPluginArtifact(pluginRoot), "symlink-forbidden"); + }); +}); + +test("builds, verifies, runs, and atomically replaces the real pinned artifact", async () => { + const container = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-build-")); + const outputRoot = path.join(container, "better-harness"); + try { + const first = await buildAntigravityPluginArtifact({ repoRoot: repositoryRoot, outputRoot }); + assert.equal(first.replaced, false); + assert.deepEqual(first.publication, { + state: "published", + backupCleanup: "complete", + replaced: false, + }); + assert.deepEqual(first.warnings, []); + assert.deepEqual( + JSON.parse(await readFile(path.join(outputRoot, "plugin.json"), "utf8")), + JSON.parse(await readFile(path.join(repositoryRoot, "scripts/packaging/antigravity/plugin-manifest.json"), "utf8")), + ); + for (const relative of [CANONICAL_SKILL, "DESIGN.md", "references/project-harness/observability.md"]) { + assert.deepEqual( + await readFile(path.join(outputRoot, ...relative.split("/"))), + await readFile(path.join(repositoryRoot, ...relative.split("/"))), + ); + } + assert.deepEqual(await readdir(path.join(outputRoot, "skills")), ["better-harness"]); + assert.deepEqual( + Object.keys(JSON.parse(await readFile(path.join(outputRoot, "package.json"), "utf8"))).sort(), + ["bin", "dependencies", "engines", "license", "name", "private", "type", "version"], + ); + for (const relative of [".agents", ".github", "test", "scripts/packaging"]) { + assert.equal(await lstat(path.join(outputRoot, ...relative.split("/"))).catch(() => null), null); + } + const verified = await verifyAntigravityPluginArtifact(outputRoot); + assert.deepEqual( + { nodes: verified.markdownClosure.nodes, edges: verified.markdownClosure.edges, files: verified.markdownClosure.files.length }, + { nodes: 93, edges: 290, files: 96 }, + ); + assert.equal(verified.runtimeClosure.modules, 19); + const help = spawnSync(process.execPath, ["scripts/better-harness.mjs", "--help"], { + cwd: outputRoot, + encoding: "utf8", + }); + assert.equal(help.status, 0, help.stderr); + assert.match(help.stdout, /Better Harness CLI/u); + + await writeArtifactFile(outputRoot, "docs/old-sentinel.md", "old\n"); + const second = await buildAntigravityPluginArtifact({ repoRoot: repositoryRoot, outputRoot }); + assert.equal(second.replaced, true); + assert.deepEqual(second.publication, { + state: "published", + backupCleanup: "complete", + replaced: true, + }); + assert.equal(await lstat(path.join(outputRoot, "docs/old-sentinel.md")).catch(() => null), null); + await verifyAntigravityPluginArtifact(outputRoot); + assert.equal( + (await readdir(container)).some((name) => name.includes("stage-") || name.includes("backup-")), + false, + ); + } finally { + await rm(container, { recursive: true, force: true }); + } +}, 30_000); + +test("preserves unowned and full-invalid destinations before copying", async () => { + const repoRoot = await createSourceRepo(); + const container = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-owned-")); + try { + for (const kind of ["unowned", "marker-only"]) { + const parent = path.join(container, kind); + const outputRoot = path.join(parent, "better-harness"); + await writeArtifactFile(outputRoot, "sentinel.txt", `${kind}\n`); + if (kind === "marker-only") { + await writeArtifactFile( + outputRoot, + ANTIGRAVITY_ARTIFACT_MARKER, + `${JSON.stringify(baseMarker())}\n`, + ); + } + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot }), + "destination-unowned", + ); + assert.equal(await readFile(path.join(outputRoot, "sentinel.txt"), "utf8"), `${kind}\n`); + } + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(container, { recursive: true, force: true }); + } +}); + +test("fails closed on source metadata, dependency, and staged closure errors", async () => { + const cases = [ + { manifestText: '{"name":"better-harness","extra":true}\n', code: "source-manifest-invalid" }, + { manifestText: "{\n", code: "source-manifest-invalid" }, + { + packageJson: baseSourcePackage({ + dependencies: { + "@vscode/tree-sitter-wasm": "1.0.0", + "esbuild-wasm": "1.0.0", + unexpected: "1.0.0", + }, + }), + code: "source-dependencies-invalid", + }, + ]; + for (const scenario of cases) { + const repoRoot = await createSourceRepo(scenario); + const parent = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-invalid-")); + try { + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + scenario.code, + ); + assert.deepEqual(await readdir(parent), []); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(parent, { recursive: true, force: true }); + } + } + + const repoRoot = await createSourceRepo(); + const parent = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-stage-invalid-")); + try { + await rm(path.join(repoRoot, "node_modules/esbuild-wasm/LICENSE.md")); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "dependency-license-invalid", + ); + assert.deepEqual(await readdir(parent), []); + await writeArtifactFile(repoRoot, "node_modules/esbuild-wasm/LICENSE.md"); + await writeArtifactFile(repoRoot, CANONICAL_SKILL, "[missing](../../references/missing.md)\n"); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "required-file-missing", + ); + assert.deepEqual(await readdir(parent), []); + await writeArtifactFile(repoRoot, CANONICAL_SKILL, "# Better Harness\n"); + await rm(path.join(repoRoot, "README.md")); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "source-entry-unreadable", + ); + assert.deepEqual(await readdir(parent), []); + await writeArtifactFile(repoRoot, "README.md"); + await rm(path.join(repoRoot, "node_modules/esbuild-wasm/package.json")); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "dependency-package-invalid", + ); + assert.deepEqual(await readdir(parent), []); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(parent, { recursive: true, force: true }); + } +}); + +test("rejects source and output-parent symlinks when the platform permits them", async (context) => { + const repoRoot = await createSourceRepo(); + const parent = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-symlink-")); + try { + const sourceTarget = path.join(repoRoot, "docs-target.md"); + const sourceLink = path.join(repoRoot, "docs", "linked.md"); + await writeArtifactFile(repoRoot, "docs-target.md"); + await mkdir(path.dirname(sourceLink), { recursive: true }); + try { + await symlink(sourceTarget, sourceLink, "file"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`source/output symlink creation unavailable: ${error.code}`); + return; + } + throw error; + } + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(parent, "better-harness") }), + "source-symlink-forbidden", + ); + + await rm(sourceLink); + const linkedParent = path.join(parent, "linked-parent"); + await symlink(repoRoot, linkedParent, "junction"); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot, outputRoot: path.join(linkedParent, "better-harness") }), + "output-overlap", + ); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(parent, { recursive: true, force: true }); + } +}); + +test("plans output boundaries without writes and rechecks canonical authority after parent creation", async () => { + const repoRoot = await createSourceRepo(); + const external = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-boundary-")); + try { + const rejectedParent = path.join(repoRoot, "missing", "deep"); + await expectCode( + assertOutputBoundary(repoRoot, path.join(rejectedParent, "better-harness")), + "output-overlap", + ); + assert.equal(await lstat(path.join(repoRoot, "missing")).catch(() => null), null); + + const approvedParent = path.join(external, "approved", "deep"); + const approvedOutput = path.join(approvedParent, "better-harness"); + const preflight = await assertOutputBoundary(repoRoot, approvedOutput); + assert.equal(await lstat(path.join(external, "approved")).catch(() => null), null); + await mkdir(preflight.resolvedParent, { recursive: true }); + const postCreate = await assertOutputBoundary(repoRoot, approvedOutput); + assert.equal(postCreate.canonicalParent, await realpath(approvedParent)); + assert.equal(postCreate.canonicalOutput, path.join(await realpath(approvedParent), "better-harness")); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(external, { recursive: true, force: true }); + } +}); + +test("rejects a parent junction retargeted into the repository between boundary checks", async (context) => { + const repoRoot = await createSourceRepo(); + const external = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-retarget-")); + const approvedTarget = path.join(external, "approved-target"); + const linkedParent = path.join(external, "linked-parent"); + await mkdir(approvedTarget); + try { + try { + await symlink(approvedTarget, linkedParent, "junction"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`junction retarget unavailable: ${error.code}`); + return; + } + throw error; + } + const outputRoot = path.join(linkedParent, "deep", "better-harness"); + const preflight = await assertOutputBoundary(repoRoot, outputRoot); + await mkdir(preflight.canonicalParent, { recursive: true }); + await rm(linkedParent); + await symlink(repoRoot, linkedParent, "junction"); + assert.ok(await lstat(path.join(approvedTarget, "deep"))); + assert.equal(await lstat(path.join(repoRoot, "deep")).catch(() => null), null); + await expectCode(assertOutputBoundary(repoRoot, outputRoot), "output-overlap"); + assert.equal(await lstat(path.join(repoRoot, "deep")).catch(() => null), null); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(external, { recursive: true, force: true }); + } +}); + +test("pins boundary mkdir to the safe canonical parent across a pre-mkdir junction retarget", async (context) => { + const repoRoot = await createSourceRepo(); + const external = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-mkdir-race-")); + const approvedTarget = path.join(external, "approved-target"); + const linkedParent = path.join(external, "linked-parent"); + await mkdir(approvedTarget); + try { + try { + await symlink(approvedTarget, linkedParent, "junction"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`junction retarget unavailable: ${error.code}`); + return; + } + throw error; + } + const outputRoot = path.join(linkedParent, "deep", "better-harness"); + const expectedCanonicalParent = path.join(await realpath(approvedTarget), "deep"); + let mkdirCalls = 0; + let publicationCalls = 0; + await expectCode( + buildAntigravityPluginArtifact({ + repoRoot, + outputRoot, + boundaryOperations: { + mkdir: async (target, options) => { + mkdirCalls += 1; + assert.equal(target, expectedCanonicalParent); + assert.notEqual(target, path.dirname(outputRoot)); + await rm(linkedParent); + await symlink(repoRoot, linkedParent, "junction"); + await mkdir(target, options); + }, + }, + operations: { + rename: async () => { publicationCalls += 1; }, + remove: async () => { publicationCalls += 1; }, + }, + }), + "output-overlap", + ); + assert.equal(mkdirCalls, 1); + assert.equal(publicationCalls, 0); + assert.ok(await lstat(expectedCanonicalParent)); + assert.equal(await lstat(path.join(repoRoot, "deep")).catch(() => null), null); + assert.equal(await lstat(path.join(repoRoot, "deep", "better-harness")).catch(() => null), null); + assert.equal( + (await readdir(approvedTarget)).some((name) => name.includes("stage-") || name.includes("backup-")), + false, + ); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(external, { recursive: true, force: true }); + } +}); + +test("publishes only after full backup verification and restores verified backup on publish failure", async () => { + for (const rollbackFails of [false, true]) { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + let renameCalls = 0; + const operations = { + rename: async (source, destination) => { + renameCalls += 1; + if (renameCalls === 2 || (rollbackFails && renameCalls === 3)) throw new Error("injected"); + await rename(source, destination); + }, + remove: rm, + }; + try { + await expectCode( + publishStagedArtifact({ stageRoot, outputRoot, replaceExisting: true, operations }), + rollbackFails ? "publish-rollback-failed" : "publish-failed", + ); + if (rollbackFails) { + assert.equal(await lstat(outputRoot).catch(() => null), null); + const backups = (await readdir(container)).filter((name) => name.includes("antigravity-backup-")); + assert.equal(backups.length, 1); + await verifyAntigravityPluginArtifact(path.join(container, backups[0], "better-harness")); + } else { + await verifyAntigravityPluginArtifact(outputRoot); + assert.equal((await readdir(container)).some((name) => name.includes("backup-")), false); + } + assert.ok(await lstat(stageRoot)); + } finally { + await rm(container, { recursive: true, force: true }); + } + } +}); + +test("reports a stable first-backup-rename failure and bounds empty-container cleanup", async () => { + for (const cleanupFails of [false, true]) { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + let removeCalls = 0; + try { + await assert.rejects( + publishStagedArtifact({ + stageRoot, + outputRoot, + replaceExisting: true, + operations: { + rename: async () => { throw new Error("injected first rename failure"); }, + remove: async (...args) => { + removeCalls += 1; + if (cleanupFails) throw new Error("injected cleanup failure"); + await rm(...args); + }, + }, + }), + (error) => { + assert.equal(error.code, "publish-backup-rename-failed"); + assert.equal(error.publication.state, "not-published-destination-unchanged"); + assert.equal(error.publication.backupCleanup, cleanupFails ? "pending" : "complete"); + assert.equal(JSON.stringify(error).includes(container), false); + return true; + }, + ); + assert.equal(removeCalls, 1); + await verifyAntigravityPluginArtifact(outputRoot); + await verifyAntigravityPluginArtifact(stageRoot); + const backups = (await readdir(container)).filter((name) => name.includes("antigravity-backup-")); + assert.equal(backups.length, cleanupFails ? 1 : 0); + } finally { + await rm(container, { recursive: true, force: true }); + } + } +}); + +test("revalidates the actual moved destination and restores concurrent unowned or corrupt swaps", async () => { + for (const swapKind of ["unowned", "corrupt"] ) { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + const originalRoot = path.join(container, "original-container", "better-harness"); + const swappedRoot = path.join(container, "concurrent-swap"); + try { + await mkdir(path.dirname(originalRoot)); + await rename(outputRoot, originalRoot); + if (swapKind === "unowned") { + await writeArtifactFile(swappedRoot, "concurrent.txt", "preserve me\n"); + } else { + await cp(originalRoot, swappedRoot, { recursive: true }); + await writeArtifactFile(swappedRoot, ANTIGRAVITY_ARTIFACT_MARKER, "{}\n"); + await writeArtifactFile(swappedRoot, "docs/concurrent.md", "preserve me\n"); + } + await rename(swappedRoot, outputRoot); + + await assert.rejects( + publishStagedArtifact({ stageRoot, outputRoot, replaceExisting: true }), + (error) => { + assert.equal(error.code, "destination-changed"); + assert.equal(error.publication.state, "not-published-destination-restored"); + return true; + }, + ); + const sentinel = swapKind === "unowned" ? "concurrent.txt" : "docs/concurrent.md"; + assert.equal(await readFile(path.join(outputRoot, ...sentinel.split("/")), "utf8"), "preserve me\n"); + await verifyAntigravityPluginArtifact(originalRoot); + await verifyAntigravityPluginArtifact(stageRoot); + assert.equal((await readdir(container)).some((name) => name.includes("backup-")), false); + } finally { + await rm(container, { recursive: true, force: true }); + } + } +}); + +test("retains the moved concurrent tree when destination revalidation rollback conflicts", async () => { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + const originalRoot = path.join(container, "original-container", "better-harness"); + const swappedRoot = path.join(container, "concurrent-swap"); + await mkdir(path.dirname(originalRoot)); + await rename(outputRoot, originalRoot); + await writeArtifactFile(swappedRoot, "concurrent.txt", "preserve me\n"); + await rename(swappedRoot, outputRoot); + let renameCalls = 0; + try { + await assert.rejects( + publishStagedArtifact({ + stageRoot, + outputRoot, + replaceExisting: true, + operations: { + rename: async (source, destination) => { + renameCalls += 1; + if (renameCalls === 2) { + await writeArtifactFile(outputRoot, "conflict.txt", "also preserve me\n"); + throw new Error("injected rollback conflict"); + } + await rename(source, destination); + }, + remove: rm, + }, + }), + (error) => { + assert.equal(error.code, "destination-revalidation-rollback-failed"); + assert.equal(error.publication.state, "not-published-backup-retained"); + assert.equal(JSON.stringify(error).includes(container), false); + return true; + }, + ); + assert.equal(await readFile(path.join(outputRoot, "conflict.txt"), "utf8"), "also preserve me\n"); + const backups = (await readdir(container)).filter((name) => name.includes("antigravity-backup-")); + assert.equal(backups.length, 1); + assert.equal( + await readFile(path.join(container, backups[0], "better-harness", "concurrent.txt"), "utf8"), + "preserve me\n", + ); + await verifyAntigravityPluginArtifact(stageRoot); + await verifyAntigravityPluginArtifact(originalRoot); + } finally { + await rm(container, { recursive: true, force: true }); + } +}); + +test("restores a concurrent destination symlink without dereferencing it when supported", async (context) => { + const { container, outputRoot, stageRoot } = await createPublishFixture(); + const originalRoot = path.join(container, "original-container", "better-harness"); + const targetRoot = path.join(container, "concurrent-target"); + try { + await mkdir(path.dirname(originalRoot)); + await rename(outputRoot, originalRoot); + await writeArtifactFile(targetRoot, "sentinel.txt", "preserve target\n"); + try { + await symlink(targetRoot, outputRoot, "junction"); + } catch (error) { + if (["EPERM", "EACCES", "ENOSYS"].includes(error.code)) { + context.skip(`destination symlink creation unavailable: ${error.code}`); + return; + } + throw error; + } + await expectCode( + publishStagedArtifact({ stageRoot, outputRoot, replaceExisting: true }), + "destination-changed", + ); + assert.equal((await lstat(outputRoot)).isSymbolicLink(), true); + assert.equal(await readFile(path.join(targetRoot, "sentinel.txt"), "utf8"), "preserve target\n"); + await verifyAntigravityPluginArtifact(stageRoot); + await verifyAntigravityPluginArtifact(originalRoot); + } finally { + await rm(container, { recursive: true, force: true }); + } +}); + +test("returns published cleanup-pending state and renders safe JSON and human warnings", async () => { + const repoRoot = await createSourceRepo(); + const container = await mkdtemp(path.join(os.tmpdir(), "better-harness-antigravity-cleanup-")); + const outputRoot = path.join(container, "better-harness"); + try { + await buildAntigravityPluginArtifact({ repoRoot, outputRoot }); + await writeArtifactFile(repoRoot, "README.md", "new artifact\n"); + let removeCalls = 0; + let renameCalls = 0; + const result = await buildAntigravityPluginArtifact({ + repoRoot, + outputRoot, + operations: { + rename: async (source, destination) => { + renameCalls += 1; + await rename(source, destination); + }, + remove: async () => { + removeCalls += 1; + throw new Error("injected backup cleanup failure"); + }, + }, + }); + assert.equal(renameCalls, 2); + assert.equal(removeCalls, 1); + assert.equal(result.publication.state, "published"); + assert.equal(result.publication.backupCleanup, "pending"); + assert.equal(result.warnings.length, 1); + assert.equal(result.warnings[0].code, "backup-cleanup-pending"); + assert.equal(await readFile(path.join(outputRoot, "README.md"), "utf8"), "new artifact\n"); + await verifyAntigravityPluginArtifact(outputRoot); + const backups = (await readdir(container)).filter((name) => name.includes("antigravity-backup-")); + assert.equal(backups.length, 1); + assert.equal( + await readFile(path.join(container, backups[0], "better-harness", "README.md"), "utf8"), + "fixture\n", + ); + + const human = formatBuildSuccess(result); + assert.match(human, /backup-cleanup-pending/u); + assert.match(human, /state=published backupCleanup=pending/u); + assert.equal(human.includes(container), false); + const jsonText = formatBuildSuccess(result, { json: true }); + const json = JSON.parse(jsonText); + assert.equal(json.ok, true); + assert.equal(json.data.publication.state, "published"); + assert.equal(json.warnings[0].code, "backup-cleanup-pending"); + assert.equal(jsonText.includes(container), false); + } finally { + await rm(repoRoot, { recursive: true, force: true }); + await rm(container, { recursive: true, force: true }); + } +}); + +test("builder CLI and output boundaries fail closed without path disclosure", async () => { + const imported = spawnSync( + process.execPath, + ["--input-type=module", "--eval", `await import(${JSON.stringify(pathToFileURL(builderPath).href)})`], + { encoding: "utf8" }, + ); + assert.equal(imported.status, 0, imported.stderr); + assert.equal(imported.stdout, ""); + assert.equal(imported.stderr, ""); + const help = spawnSync(process.execPath, [builderPath, "--help"], { encoding: "utf8" }); + assert.equal(help.status, 0, help.stderr); + assert.match(help.stdout, /--out /u); + for (const args of [["--json"], ["--json", "--unknown"], ["--json", "--out"], ["--json", "--out", "one", "--out", "two"]]) { + const result = spawnSync(process.execPath, [builderPath, ...args], { encoding: "utf8" }); + assert.equal(result.status, 1); + assert.equal(result.stderr, ""); + assert.equal(JSON.parse(result.stdout).ok, false); + } + + const privatePath = path.join(os.tmpdir(), "private-builder-fixture", "wrong-name"); + const failure = spawnSync( + process.execPath, + [builderPath, "--json", "--out", privatePath], + { encoding: "utf8" }, + ); + assert.equal(failure.status, 1); + assert.equal(failure.stdout.includes(privatePath), false); + assert.equal(JSON.parse(failure.stdout).error.code, "output-invalid"); + await expectCode( + buildAntigravityPluginArtifact({ repoRoot: repositoryRoot, outputRoot: path.join(repositoryRoot, "docs", "better-harness") }), + "output-overlap", + ); +}); diff --git a/test/skills-docs/doc-link-graph.test.mjs b/test/skills-docs/doc-link-graph.test.mjs index a17873eb..b3acf8d5 100644 --- a/test/skills-docs/doc-link-graph.test.mjs +++ b/test/skills-docs/doc-link-graph.test.mjs @@ -23,6 +23,35 @@ const DOC_DIRS = [ "schemas", ]; const ROOT_DOCS = ["AGENTS.md", "README.md"]; +// Docs that cannot reach a target with a relative path — the Docusaurus site +// tree, and repository docs that ship inside a host plugin artifact whose +// allowlist excludes `.agents/` and `.github/` — cite it as a repository URL +// instead. Those citations are real links to real files, so they need the same +// integrity guarantee a relative link gets. +const REPO_BLOB_URL = /https:\/\/github\.com\/QoderAI\/better-harness\/blob\/([^/\s)]+)\/([^)\s#]+)/g; +const PINNED_COMMIT = /^[0-9a-f]{40}$/u; +// A bare `foo.md` link usually names a file convention in an analyzed target +// repository rather than a file here, which is why `classify` leaves it +// conceptual. These names are the conventions; anything else that is written as +// link syntax is meant to resolve. +const CONVENTION_DOC_NAMES = new Set([ + "AGENTS.md", + "CHANGELOG.md", + "CLAUDE.local.md", + "CLAUDE.md", + "CODEX.md", + "CONTRIBUTING.md", + "DESIGN.md", + "GEMINI.md", + "MEMORY.md", + "QODER.md", + "README.md", + "SECURITY.md", + "SKILL.md", + "SUPPORT.md", + "report.md", +]); +const MARKDOWN_LINK = /\]\(([^)\s#]+\.md)(?:#[^)]*)?\)/g; const MD_TOKEN = /(?:\.\.?\/)*(?:[\w.-]+\/)*[\w.-]+\.md\b/g; // Bilingual reader surfaces: the Docusaurus site tree (including its zh-Hans // locale mirror and specs that quote Chinese reader copy) and the root README @@ -81,6 +110,49 @@ test("all relative markdown doc links across the repo resolve", () => { ); }); +test("repository URLs pointing at a mutable ref resolve to real files", () => { + // A relative link rots loudly when its target moves, because the link graph + // fails. A repository URL rots silently. Checking the path component keeps + // both citation styles under the same guarantee. Commit-pinned URLs are + // deliberate references to history and stay out of scope. + const broken = []; + for (const file of allRepoDocs()) { + for (const [, ref, target] of readFileSync(file, "utf8").matchAll(REPO_BLOB_URL)) { + if (PINNED_COMMIT.test(ref)) continue; + if (!existsSync(path.join(repoRoot, target))) { + broken.push(`${relId(file)} -> ${ref}/${target}`); + } + } + } + assert.deepEqual( + broken, + [], + `Repository URLs whose path no longer exists:\n${broken.join("\n")}`, + ); +}); + +test("bare filename markdown links resolve unless they name a file convention", () => { + // `classify` cannot tell `custom-agents-review.md` (a real sibling doc one + // directory up) from `AGENTS.md` (a convention in a repository under + // analysis), so it treats every slash-free token as conceptual and checks + // neither. Restricting the check to link syntax and excluding the convention + // names recovers the first case without reintroducing false positives. + const broken = []; + for (const file of allRepoDocs()) { + for (const [, target] of readFileSync(file, "utf8").matchAll(MARKDOWN_LINK)) { + if (target.includes("/") || target.startsWith(".") || CONVENTION_DOC_NAMES.has(target)) continue; + if (!existsSync(path.resolve(path.dirname(file), target))) { + broken.push(`${relId(file)} -> ${target}`); + } + } + } + assert.deepEqual( + broken, + [], + `Bare filename links that do not resolve next to their doc:\n${broken.join("\n")}`, + ); +}); + test("Better Harness skill doc graph has no missing link targets", () => { const seeds = markdownFilesUnder(path.join(repoRoot, "skills/better-harness")); const graph = buildGraph(seeds, { follow: true });