Files
BillyOutlastandClaude Opus 5.5 020ba87f08 Release 1.6.0: 1.0 cutover, phases 0–5 and the review remediation (#151)
- 1.0 cutover: the rewrite monorepo lands on main. 1.1.5 through 1.5.0
  never published on their own and ship here as 1.6.0, in lockstep for
  es-core, es-cli and epistemic-swarm (ADR 0001).
- Phase 0 (#140): fail-closed seat models, one CLI grammar for the
  human-only rule, sealed source-cache readers, pinned MCP identity, a
  registry-generated permission probe suite, one package list.
- Phase 1 (#141): research backends with failover, the Scraper-Swarm
  gateway, callable brainstorm/harvest, research-only runs, the
  deep-research coordinator, Markdown/HTML dossiers, domain-pack tiers.
- Phase 2 (#142): ADR 0002 and the shared approveStage service, masked
  TUI approvals, the headless runner (JSONL events, metrics, cancel).
- Phase 3 (#144): the human-only es-fleet daemon, DAG scheduler, worktree
  coordinator with a gated integration branch, telemetry bus and watch.
- Phase 4 (#146): the loopback web control plane (SolidJS, ADR 0003):
  dashboard, browser approvals, config preview, evidence explorer.
- Phase 5 (#149): telemetry harvest, distillation into proposals, the
  self-dogfood preset.
- Cutover review remediation (#152): CLI approvals bound to the preview
  with attempt limiting (#143), human-only `factory run` and fleet token,
  DNS-resolving SSRF guard, value-level secret scrubbing, fleet gc and
  allocation races, CodeQL #84, boundary-gate gaps, one REPORT.md writer
  per run mode.

Closes #96.
Closes #97.
Closes #98.
Closes #99.
Closes #100.
Closes #101.
Closes #103.
Closes #106.
Closes #107.
Closes #108.
Closes #109.
Closes #110.
Closes #111.
Closes #112.
Closes #113.
Closes #114.
Closes #117.
Closes #118.
Closes #119.
Closes #120.
Closes #122.
Closes #123.
Closes #124.
Closes #125.
Closes #126.
Closes #127.
Closes #129.
Closes #130.
Closes #131.
Closes #132.
Closes #133.
Closes #135.
Closes #136.
Closes #147.
Closes #148.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 21:33:44 -04:00

111 lines
6.0 KiB
TypeScript

// The plan's minimal-dependency rule, enforced on this repository: core's
// runtime dependencies must stay within the allowlist, and anything else needs
// a recorded justification in .factory/dependencies.json. Run by `bun run check`
// in CI (AGENTS.md: the allowlist is "enforced by our own gate"). It also holds
// the package boundaries from docs/adr/0001-monorepo.md.
import { readdir, readFile } from "node:fs/promises"
import path from "node:path"
import { CORE_DEPENDENCY_ALLOWLIST, checkDependencies } from "../packages/core/src/gates/minimal-deps.ts"
import { importsOf } from "./imports.ts"
import { workspacePackages } from "./packages.ts"
const root = path.resolve(import.meta.dir, "..")
const pkg = JSON.parse(await readFile(path.join(root, "packages/core/package.json"), "utf8")) as {
dependencies?: Record<string, string>
}
const justifications = await readFile(path.join(root, ".factory/dependencies.json"), "utf8")
.then((text) => JSON.parse(text) as Record<string, string>)
.catch(() => ({}) as Record<string, string>)
const dependencies = pkg.dependencies ?? {}
const check = checkDependencies(dependencies, justifications)
if (!check.ok) {
console.error("Core dependency gate failed:")
for (const violation of check.violations) console.error(` - ${violation.name}: ${violation.reason}`)
console.error(`Allowlist: ${CORE_DEPENDENCY_ALLOWLIST.join(", ")}`)
console.error("Add the dependency to .factory/dependencies.json with a reason, or remove it.")
process.exit(1)
}
// Package boundaries (docs/adr/0001-monorepo.md): core stays harness-neutral,
// so it may not import the CLI or the plugin, and the CLI may not import the
// plugin. Both bare specifiers and relative paths into another package count.
// Every workspace package needs an entry: a new surface fails the gate until
// it declares which packages it may not import, and core and cli may never
// import a new surface (their entries must ban it).
const BOUNDARIES = [
{ pkg: "core", banned: ["cli", "opencode", "testkit", "fleet", "web"] },
{ pkg: "cli", banned: ["opencode", "testkit", "fleet", "web"] },
// The plugin never imports the fleet daemon or the web UI (AGENTS.md: core,
// CLI and plugin never import fleet; nothing imports web).
{ pkg: "opencode", banned: ["fleet", "web"] },
// The test harness never imports the fleet daemon or the web UI either.
{ pkg: "testkit", banned: ["fleet", "web"] },
// Fleet depends on core only (#122): never on the CLI or the plugin.
// It serves the built web UI as static files (#129), never as an import.
{ pkg: "fleet", banned: ["cli", "opencode", "web"] },
// Web is a leaf surface (#129): nothing imports it, and it imports
// nothing at runtime except the fleet API's schema types (type-only).
// `fleet` stays banned with a type-only exemption below: a top-level
// `import type` (or `export type`) is erased, so only a value import of
// fleet from web fails the gate. Inline `{ type A }` members are not
// erased under verbatimModuleSyntax (scripts/imports.ts).
// The test-only exception lives in `test/`, outside this gate's `src/`
// scan: packages/web/test/bus.contract.test.ts runtime-imports the fleet
// server harness for the WS contract test.
{ pkg: "web", banned: ["core", "cli", "opencode", "testkit", "fleet"] },
] as const
const crossings: string[] = []
// The workspace is the universe: a package with no BOUNDARIES entry fails
// until it declares its bans, and core/cli entries must ban every other
// surface, so a new package cannot be silently imported by (or silently
// importable from) the harness-neutral layers. Bare-specifier names come
// from the same list (AGENTS.md one-list rule, #102): no second hand map
// that could miss a surface (a bare `@heretek-ai/es-core` import from web
// used to pass because the old map had no `core` key).
const workspace = await workspacePackages(root)
const NPM_NAME: Record<string, string> = Object.fromEntries(workspace.map((pkg) => [pkg.dir, pkg.name]))
const universe = workspace.map((pkg) => pkg.dir)
for (const pkg of universe)
if (!BOUNDARIES.some((entry) => entry.pkg === pkg))
crossings.push(`packages/${pkg} has no BOUNDARIES entry: declare which packages ${pkg} may not import`)
for (const outer of ["core", "cli"] as const) {
const bans = BOUNDARIES.find((entry) => entry.pkg === outer)?.banned ?? []
for (const pkg of universe)
if (pkg !== outer && pkg !== "core" && pkg !== "cli" && !(bans as readonly string[]).includes(pkg))
crossings.push(`packages/${outer} may import packages/${pkg}: new surfaces stay out of core and the CLI`)
}
for (const { pkg, banned } of BOUNDARIES) {
const src = path.join(root, "packages", pkg, "src")
const files = (await readdir(src, { recursive: true })).filter((file) => /\.tsx?$/.test(file))
for (const file of files) {
const absolute = path.join(src, file)
const text = await readFile(absolute, "utf8")
for (const { specifier, typeOnly } of importsOf(text)) {
const target = specifier.startsWith(".") ? path.resolve(path.dirname(absolute), specifier) : undefined
const hit = banned.find((other) =>
target
? target.startsWith(path.join(root, "packages", other) + path.sep)
: specifier === NPM_NAME[other] || specifier.startsWith(`${NPM_NAME[other]}/`),
)
if (!hit) continue
// Web's fleet exemption: a type-only import carries no runtime
// dependency, so only a value import of fleet from web crosses.
if (pkg === "web" && hit === "fleet" && typeOnly) continue
crossings.push(`packages/${pkg}/src/${file} imports ${specifier} (${hit})`)
}
}
}
if (crossings.length) {
console.error("Package boundary gate failed (docs/adr/0001-monorepo.md):")
for (const crossing of crossings) console.error(` - ${crossing}`)
console.error("Core may not import the CLI or the plugin; the CLI may not import the plugin.")
process.exit(1)
}
console.log(
`deps: ok (${Object.keys(dependencies).length} core runtime dependencies; all allowlisted or justified; package boundaries hold)`,
)