Skip to content

Latest commit

 

History

History
128 lines (103 loc) · 6.57 KB

File metadata and controls

128 lines (103 loc) · 6.57 KB

Architecture

The shape of the system, the boundaries that matter, and the invariants you can break without any test going red.

The one-sentence version

A coding assistant claims it is done; this tool runs the command the developer chose, writes the verdict to a file, and refuses the assistant's exit until that command exits zero.

The invariant

The tool never decides whether the work is good. A command the user chose decides, and its exit code is the only evidence.

There is no model anywhere in the verdict path. src/gate.ts spawns a shell command and reads its exit code; src/proofloopToolUse.ts compares a captured log against a declared contract with string and regex matching. If you find yourself adding a heuristic that infers whether something passed, you are breaking the property the product sells.

The corollary matters just as much: absence of evidence is never a pass. No verdict on disk is exit 2, not 0. An empty tool-use trace fails a required-tools contract. A domain that has not proved ownership is refused, not queued.

The two surfaces

Surface 1: the CLI                     Surface 2: the hosted page
------------------                     --------------------------
terminal / agent hook                  browser
    |                                      |
dist/cli.js  (compiled src/cli.ts)     public/index.html + app.js
    |                                      | fetch
src/*.ts                               api/**/*.js  (Vercel functions)
    |                                      | require()
.proofloop/*.json                      dist/hosted.js  <- same compiled core
                                           | workflow_dispatch
                                       .github/workflows/hosted-proofloop.yml

They share the core: api/hosted/_shared.js:2 requires ../../dist/hosted.js, so domain-permission rules and request validation exist once, in TypeScript, and the serverless function is a thin transport wrapper. That is why dist/ is committed — see "The dist/ seam" below.

Layers, and what each may know

Layer Files May depend on
Entry src/cli.ts, api/**, public/app.js anything below
Domain src/gate.ts, src/hosted.ts, src/proofloopToolUse.ts, src/config.ts Node standard library only
Orchestration src/agentLoop.ts, src/runner.ts, src/agentAdapters.ts domain
Reporting src/project.ts, src/maturity.ts, src/productivity.ts, src/contextReport.ts domain, filesystem

npx dependency-cruiser --no-config src api scripts public reports zero circular dependencies. The rule that keeps it that way in practice: a domain module never imports an entry module, and never reads process.argv.

The trust boundaries

There are exactly four places where input from outside becomes something this code acts on. Each has one function that owns it.

  1. proofloop.config.jsonnormalizeConfig (src/config.ts:63). The only parser of that file. Unparseable throws; every field gets a default.
  2. A submitted URLverifyHostedDomainPermission (src/hosted.ts) plus the network half in verifiedHostAllowlist (api/hosted/_shared.js:72). Unverified hosts are refused with the token to publish. The browser never gets a votepublic/app.js sends the request; the server decides.
  3. A file edit proposed by an agent → the PreToolUse guard generated by src/proofloopHooks.ts. Pure string prefix checks against protected and immutable paths, deliberately not regex, so it cannot fail open on a clever path. Its default set (src/scaffoldConstants.ts) is not removable by config; user entries only add.
  4. An interop envelope from another toolchainvalidateSoloInteropEnvelope (src/soloInterop.ts), pinned to a schema whose digest is a constant in the source, so swapping the schema file on disk is detected.

Invariants a test will not catch

These are the ones to re-read before you change the files they live in.

  • The Stop hook must never trap a session. The per-session block counter (DEFAULT_MAX_STOP_BLOCKS = 5) is checked before the gate, and no gate, no verdict, or a gate error all allow the stop. A change that makes the hook block on an error would be invisible to the suite and terrible in the field.
  • The generated hook scripts import nothing from this package. They are standalone Node, so they survive uninstalling proofloop. Adding an import would work in every test here and break on a user's machine.
  • The MCP server is read-only. Five tools, none of which writes. Adding a write tool moves an agent's blast radius, silently.
  • The default gate command is check-only. npx proofloop gate --check reads the persisted verdict; making the hook re-run the suite would put the npm registry and the network in the path of every attempted stop.
  • Nothing writes outside .proofloop/ except three named commands (init, ci install, hooks install). This tool runs inside other people's repositories.

The dist/ seam

dist/ is compiled output and it is committed and it is a runtime dependency of the deployed functions. All three at once, which is unusual, so:

  • api/hosted/_shared.js and scripts/hosted-worker.mjs require("../dist/…"). They are plain JavaScript because Vercel functions and npm scripts run them directly, un-compiled.
  • Committing dist/ means those requires resolve even if a build step is skipped, and it is what package.json files publishes.
  • The cost is real: dist/ is 60 of the repository's 215 tracked files — more than a quarter of everything you can open — it can silently drift from src/, and on Windows a rebuild marks all 60 modified in git status while git diff shows no content change. docs/codebase/CONCERNS.md records this as an accepted, unresolved finding with the reason it was not changed in this pass.

Rule for you: never edit dist/. Edit src/ and run npm run build.

Extension points

  • New proof for an existing project — add a check to gate.checks in proofloop.config.json. Data, not code. This is the intended way.
  • New CLI command — a case in src/cli.ts and a module beside it. Copy the shape of src/gate.ts: exported pure functions, plus one run*Cli wrapper that returns an exit code and does all the printing.
  • New agent host — an entry in PROOFLOOP_AGENT_ADAPTER_IDS (src/agentAdapters.ts:17) and its settings path.
  • New MCP tool — an entry in TOOLS (src/mcp.ts:28). Keep it read-only.