Skip to main content

Norn: a provider-neutral multi-agent development harness

Project description

Norn

Provider-neutral multi-agent development harness for local subscription CLIs. The name evokes the Norns who shape fate: the harness coordinates independent agents toward a bounded outcome.

Status: beta. The norn-cli distribution is published to PyPI; the source repository remains private during the beta period.

Norn provides configuration, CLI, run-state, event journal, recovery, human-readable escalation reports, provider-neutral adapters, and an executable multi-role delivery pipeline for Codex, Claude Code, Grok Build, and future command-based subscription CLIs.

norn init
norn --version
norn doctor
norn validate-config
norn providers
norn plan "add an audit log"
norn run "add an audit log"

plan and run immediately print the Run ID and stream append-only lifecycle progress to stderr: current phase, bounded round counts, role start/completion, local gates, E2E, quality decisions, and the terminal outcome. Prompts and private model reasoning are not printed. The same lifecycle events are durably recorded in .norf/runs/<run-id>/events.jsonl; use --no-progress to suppress only the live stderr rendering for automation.

Install the bundled explicit-invocation skill for both Codex and Claude Code:

norn install-skill --target all --scope project --project /path/to/repository

This writes .agents/skills/norn for Codex and .claude/skills/norn for Claude Code. Use $norn in Codex or /norn in Claude Code. Install a single user-level copy with --target codex|claude --scope user; use --force only to replace an existing copy intentionally. Loading the skill never starts a model-consuming lifecycle unless the user explicitly requests plan or run.

See Installation for isolated uv tool, pipx, and virtual-environment installs from PyPI.

plan runs orchestration, planning, and plan review. run continues through implementation, language-aware local tests/lint, tester roles, triage, and final gate. UI changes require validated E2E execution. A repository-owned managed runner is the recommended path; the legacy policy can instead require each tester to execute E2E independently. Every required result includes the executed argv, exit code, a concise result summary, and at least one non-empty artifact in its assigned evidence directory. The harness rejects missing, stale, symlinked, or out-of-directory evidence and archives accepted files under .norf/runs/<run-id>/artifacts/e2e/.

After triage requests reimplementation, Norn fingerprints the Git workspace around each provider attempt. An attempt that changes neither tracked content or mode nor non-ignored untracked files is recorded as implementation.noop and rejected before local gates, tester roles, or E2E run again. The existing provider retry and fallback limits apply, and the next attempt receives the no-op diagnostic. An initial implementation may still be a legitimate no-op when the requested behavior already exists.

To implement a previously approved plan without invoking orchestration, planning, or plan review again, pass the successful plan run ID with the identical request:

norn plan "add an audit log"
norn run "add an audit log" --from-plan <plan-run-id>

Norn verifies the source run mode and status, project root, request text, and saved artifact schemas before creating the implementation worktree.

For Codex tester roles, the adapter keeps workspace-write filesystem isolation and enables command networking through a loopback-only proxy allowlist (localhost and 127.0.0.1). Planning and implementation roles retain their normal network-off policy.

Docker-backed tests additionally need access to the host Docker API socket. Grant that access explicitly for one run:

norn run "task" --allow-tester-host-services

The flag resolves the local Docker Unix socket from DOCKER_HOST, the active Docker context, or a detected rootless, Docker Desktop, or system-default socket, then adds only that socket as a writable root to Codex TEST invocations. Remote TCP Docker endpoints are not added as filesystem roots. The flag does not remove the workspace sandbox or promise that raw TCP clients can reach services bound to the host's loopback interface. Docker socket access effectively delegates host control, so use the flag only with a trusted repository; the resolved path and opt-in are recorded in the run manifest and reports.

Configure one shared loopback URL for parallel browser testers. For a project-local development server, also provide its shell-free command:

e2e:
  base_url: http://127.0.0.1:4173
  server:
    command: [pnpm, --filter, "@trato/marchant-admin", dev, --host, "127.0.0.1", --port, "4173", --strictPort]
    startup_timeout_seconds: 30
  tcp_forwards:
    - {listen: 4173, to: "127.0.0.1:4173"}

Norn starts the command after implementation, waits for the URL, directs all tester roles to reuse it without starting Vite, and always stops it afterward. Omit server.command to reuse an already-running Docker or external Web server; external servers are checked once and are not stopped by Norn. tcp_forwards is an explicit allowlist for services bound to the Norn host's loopback interface. For every E2E command, Norn supplies a Python argv wrapper that listens only inside that command's sandbox and exchanges byte streams through a run-scoped file spool with the host relay. The host accepts only strictly named, regular-file requests for declared ports; duplicate listen ports, non-loopback targets, and undeclared host ports are rejected. Runtime files are contained in a mode 0700 temporary directory and are removed when testing ends.

For distributable projects, prefer a reviewed, repository-owned E2E command over granting a model browser or host permissions:

# Adapt the one detected Playwright/Cypress suite. This is a dry run.
norn init-e2e --adapter existing --project /path/to/repository

# Or scaffold a new Playwright entrypoint in a selected workspace package.
norn init-e2e --preset playwright --package packages/web --project /path/to/repository

# Or scaffold a deterministic Agent Browser smoke runner without Playwright.
norn init-e2e --preset agent-browser --package packages/web --project /path/to/repository

init-e2e detects npm, pnpm, yarn, or bun and common E2E scripts/configs. It prints the files, minimal Norn YAML, evidence limits, and an optional git.worktree_setup copy step without changing the repository. Add --write after review. Use --external-server for an already-running Docker/external server, or --server-script NAME when the project script is not dev, start, or serve. --base-url must be a loopback HTTP(S) URL.

The existing-suite adapter generates a repository-owned runner and, for Playwright, a small config that extends the existing config while disabling its webServer so Norn remains the only server owner. The Playwright preset additionally creates a bounded evidence helper, a smoke spec, a JSON reporter, and low-volume artifact defaults. It does not install Playwright unless --install-dependency is explicitly combined with --write; existing framework versions and fixtures remain owned by the project.

The agent-browser preset instead generates a safe wrapper plus a user-owned tests/e2e/norn-agent-browser.json shell-free command sequence. Each run uses an isolated session and a loopback-only domain allowlist, opens the configured URL, captures an accessibility snapshot and full-page screenshot, writes a structured result, and always closes its session. Customize the JSON sequence with app-specific semantic find/click/fill/wait assertions; scaffold reruns and --force preserve that scenario. Install Agent Browser and its Chromium separately with npm install -g agent-browser && agent-browser install; init-e2e --check verifies the command without launching a browser.

Re-running --write is idempotent. Existing generated files, package scripts, or Norn config values are not replaced when they differ; --force is the explicit migration path. Validate the checked-in contract without launching a browser:

norn init-e2e --adapter existing --external-server --check --project /path/to/repository
norn init-e2e --preset playwright --package packages/web --check --project /path/to/repository

The generated runner receives NORN_E2E_BASE_URL and NORN_E2E_EVIDENCE_DIR, writes its own result record, and keeps evidence within the configured file-count and total-size limits. Its corresponding configuration is a shell-free argv list:

e2e:
  require_all_testers: false
  evidence:
    max_files: 100
    max_total_mib: 128
  base_url: http://127.0.0.1:4173
  server:
    command: [pnpm, dev, --host, "127.0.0.1", --port, "4173", --strictPort]
  runner:
    command: [pnpm, test:e2e]

e2e.evidence.max_files defaults to 20 and e2e.evidence.max_total_mib defaults to 64; both must be positive integers. Set them in .norf/harness.yaml for persistent project limits, or override them for one invocation with norn run "task" --max-evidence-files 100 --max-evidence-mib 128. Norn passes the resolved values to managed runners as NORN_E2E_MAX_EVIDENCE_FILES and NORN_E2E_MAX_EVIDENCE_BYTES; delegated tester roles receive the same limits in their instruction payload.

Norn runs the four static tester roles in parallel, starts or checks the one shared server, then executes runner.command exactly once as a shell-free argv list. The implementer may add or update E2E specs but is explicitly told not to launch a browser, start the server, or execute this runner itself. The implementer writes its detailed outcome as a run-scoped Markdown artifact and returns only a small structured control object containing status and the assigned artifact path. Norn rejects missing, empty, stale, symlinked, hard-linked, oversized, or out-of-contract reports before accepting the provider attempt; changed files are derived from Git rather than trusted from the model response. The command receives NORN_E2E_BASE_URL, NORN_E2E_EVIDENCE_DIR, NORN_E2E_MAX_EVIDENCE_FILES, NORN_E2E_MAX_EVIDENCE_BYTES, NORN_RUN_ID, and NORN_IMPLEMENTATION_ROUND. It must write at least one non-empty log, screenshot, trace, or result file below NORN_E2E_EVIDENCE_DIR; Norn's own stdout and stderr logs do not count as evidence. Norn owns the authoritative command, timeout, exit code, and evidence validation, then gives those results to e2e_tester for a read-only review with no command tools. The reviewer returns only its summary and findings; Norn injects the authoritative test and E2E records into the run artifact. Likewise, static testers do not echo a fixed E2E placeholder or shared gate results in their structured responses. The reviewer cannot change a failed run into a pass. Managed runners cannot be combined with e2e.tcp_forwards or --allow-e2e-danger-full-access because execution occurs in Norn's process, outside a tester provider sandbox.

Heavy repositories receive bounded but generous defaults: model roles and project test commands each have 60 minutes, while managed or delegated E2E has 40 minutes. Override them per project when needed:

limits:
  timeouts:
    role_seconds: 3600
    test_seconds: 3600
    e2e_seconds: 2400

These are timeout ceilings, not delays. Existing project configurations that explicitly contain older values keep those values until updated.

The default require_all_testers: true keeps independent browser evidence from spec, clean, security, and chaos testers. Projects where parallel quality commands interfere with a watched development server can opt into one dedicated browser role:

e2e:
  require_all_testers: false
roles:
  e2e_tester: {profile: e2e-browser}

Norn first runs the four non-browser tester roles in parallel, then starts or checks the shared server and runs e2e_tester alone. The dedicated role is still required to produce valid browser evidence. Its profile is independently configurable, so a non-Codex provider can be selected when Chromium is incompatible with the Codex TEST sandbox. In this mode the four static roles are explicitly prohibited from starting a browser or development server, connecting to the E2E URL, or rerunning shared project-wide lint, test, coverage, and build commands already owned by Norn's deterministic gate.

The shipped e2e-browser profile uses Sonnet at medium effort, with Grok 4.5 at medium effort as its fallback. It intentionally has no Codex or Fable candidate. When no managed runner is configured and the provider's non-interactive permission policy still blocks browser or loopback access, a trusted repository may opt in at run time with --allow-e2e-danger-full-access. This bypass applies only to the dedicated e2e_tester; the four static testers keep their normal TEST sandbox.

Profiles keep a stable logical model name while optionally mapping it to a provider-specific identifier. For example, Codex Sol is configured as:

candidates:
  - provider: codex-cli
    model: sol
    model_id: gpt-5.6-sol
    effort: high

Round limits are configured under limits or overridden per invocation:

norn run "task" --max-rounds 5 --max-total-rounds 12 \
  --max-planner-retries 2 --max-final-replans 2

Known Python, Node, Rust, and Go gates are auto-detected, including common Node typecheck scripts. All gates are executed once by Norn outside provider agents. Projects can define additional test, lint, typecheck, and build commands as shell-free argv lists:

gates:
  enabled: true
  auto_detect: true
  commands:
    - name: integration
      kind: test
      command: [python3, -m, pytest, tests/integration]
    - name: frontend-types
      kind: typecheck
      command: [npm, run, typecheck]
      container:
        image: node:22-bookworm
        workdir: /workspace/apps/web
        network: none

A container makes Norn construct docker run directly without a shell. It bind-mounts the detached implementation worktree at /workspace, uses the current uid/gid where supported, and defaults to no network. Set a reviewed Docker network name only when the gate requires services. Pin the image by digest when reproducibility across machines matters. Direct shell-free commands such as [docker, compose, exec, ...] remain available for project-owned Compose environments. Any configured or auto-detected deterministic command that does not pass creates a high-severity finding, so one failed lint/typecheck/build is blocking under the shipped quality policy.

Each run records role responses, model selections, attempts, findings, test results, and round history below .norf/runs/<run-id>/. Any non-successful stop also produces escalation-report.md and escalation.json for human judgment.

By default, run requires a clean Git repository and implements inside a detached .norf/worktrees/<run-id>/ worktree. Set git.worktree: false only when direct changes to the selected project directory are intentional.

Fresh Git worktrees contain tracked files only. Projects whose tests require ignored dependencies, local environment files, or a prepared database can provision the worktree before any provider role starts:

git:
  worktree: true
  worktree_setup:
    - name: php-dependencies
      copy: laravel/vendor
    - name: node-dependencies
      copy: node_modules
      optional: true
    - name: test-database
      command: [scripts/setup-norn-database, --template]
      timeout_seconds: 900

Each step has exactly one action. copy materializes a project-relative path from the source checkout; command is a shell-free argv list executed in the new worktree. Commands receive NORN_SOURCE_ROOT, NORN_WORKTREE, and NORN_RUN_ID. A failed required step stops the run before model invocation.

Development and packaging

Install the local development tools and run the same checks as CI:

python3 -m pip install --editable ".[dev]"
ruff format --check .
ruff check .
python3 -m unittest discover -v
python3 -m build
python3 scripts/package_smoke.py dist/norn_cli-0.7.3-py3-none-any.whl

CI runs the quality suite on Python 3.12, 3.13, and 3.14, then installs the built wheel in a clean virtual environment and verifies both Codex and Claude skill copies. It uploads the distributions as a short-lived workflow artifact. Publishing a GitHub Release runs the same deterministic gates before sending the verified artifact to PyPI through OIDC Trusted Publishing; neither workflow invokes subscription models.

Release acceptance is intentionally split in two: CI performs deterministic package checks, while local acceptance invokes the authenticated subscription CLIs. Follow Release acceptance before promoting a release. See CHANGELOG for version history and Security for private vulnerability reporting.

The canonical command is norn. The legacy norf-harness executable remains an alias during the pre-1.0 migration; new automation should use norn.

License

Copyright 2026 uruca-kk. Licensed under the Apache License 2.0.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

norn_cli-0.9.1.tar.gz (122.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

norn_cli-0.9.1-py3-none-any.whl (102.5 kB view details)

Uploaded Python 3

File details

Details for the file norn_cli-0.9.1.tar.gz.

File metadata

  • Download URL: norn_cli-0.9.1.tar.gz
  • Upload date:
  • Size: 122.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for norn_cli-0.9.1.tar.gz
Algorithm Hash digest
SHA256 7ce67182b649781227732dc8d508201dfed66ed0aed9ec6244b5484d3bf85c93
MD5 f3555cc8e488dddfa13c84f623683765
BLAKE2b-256 1aacc0f854d289c41ee8bbb32454a8bfeb3a220e7a1bfe0378bbc320866b199b

See more details on using hashes here.

Provenance

The following attestation bundles were made for norn_cli-0.9.1.tar.gz:

Publisher: publish.yml on uruca-kk/norn

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file norn_cli-0.9.1-py3-none-any.whl.

File metadata

  • Download URL: norn_cli-0.9.1-py3-none-any.whl
  • Upload date:
  • Size: 102.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for norn_cli-0.9.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0d849330baf9085d7fc87c724d6be23ec0c345c0189a5422c205011f641d6a9e
MD5 05b3b56adc2002decd6b95a52fb20ecf
BLAKE2b-256 5ccd339df3acc463a87434cae67217347ab518b3d440ad4711ec362fa523aead

See more details on using hashes here.

Provenance

The following attestation bundles were made for norn_cli-0.9.1-py3-none-any.whl:

Publisher: publish.yml on uruca-kk/norn

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page