Skip to content

Release: the behavioral agent harness — faculties, one wire, guards-as-threads - #345

Merged
EdwardIrby merged 1429 commits into
mainfrom
dev
Sep 23, 2026
Merged

EdwardIrby merged 1429 commits into
mainfrom
dev

Conversation

@EdwardIrby

Copy link
Copy Markdown
Member

Context

The repo's default line of work (dev) has grown a complete behavioral
agent harness since main last took a release cut. A deep review of the
harness (findings, remediation, and final re-review) completed with a
SHIP verdict at a300135e; everything after that tip is docs/vocabulary
polish. This PR carries the reviewed work to main so the default branch
represents the harness and feature work can branch from it.

Summary

  • The engine is in-process and faculties are processes: a behavioral-programming
    interpreter (src/behavioral/) driven directly by the composition (bProgram,
    src/cli/b-program.ts), with capability faculties (shell, store, mcp,
    systemOne, systemTwo, frontier) as Bun.spawn processes speaking one
    behavior event wire over stdio lines.
  • One vocabulary: capability units are faculties (src/faculties/,
    useFaculty, the faculties: allow-list, faculty_error); their
    default thread sets are threads (no "packs"); behavioral stays the
    paradigm layer.
  • Validation lives in guard threads — malformed events are blocked at
    their schema and the rejects are visible in the frontier/pending-bids/
    deadlock traces; the pump re-enters parsed-but-invalid results (never
    silently discards) and the lane seal blocks cross-faculty injection.
  • System faculties are endpoint-carrying overrides with the TypeSafe/
    OpenRouter Decisions and Open Responses providers bundled; behavioral init
    generates <home>/config.ts (interactive at a TTY, agent JSON otherwise,
    env-name secrets that fail fast, optional provider scaffolding, and a
    home-resolving package link so init → serve boots from a clean home).
  • The IPC host (behavioral serve) — line-framed JSON-RPC over stdio,
    ingress → triggers, ui_* selections → client notifications, redacted
    traces out — with a hardened codec (a throwing notification no longer
    kills the loop).
  • The full review remediation: both blockers (init → serve resolution;
    the allow-list now gates the shell faculty), follow-ups 1-8, and the
    docs sweep.

Changed Files

git diff main...dev: 504 files, +29,138 / −78,804. The count reflects
history (the pre-harness kernel/tools surface is deleted) — the arc is
visible in git log main...dev --oneline (1,422 commits). Headline
surfaces:

  • src/behavioral/ — the pure language layer (engine, traces, jq worker)
  • src/faculties/ — one folder per faculty + shared top (wire, useFaculty,
    process lane, home, entry resolution); colocated tests/ per faculty
  • src/cli/ — b-program.ts (the composition), init, serve +
    json-rpc, load-config/defineConfig, trace-consumer
  • src/controller/ — the dumb-relay Controller, ui_* vocabulary, AJV
    detail schemas (never in the browser bundle)
  • src/faculties.ts / src/main.ts / package.json — the public surface
    (././faculties/./controller/./utils)
  • README.md — rewritten for the harness (two diagrams: the assembly;
    the life of a request), AGENTS.md — the working law, skills/ synced
  • .gitignore / untracked: plan.md, .prompts/, .research/ are
    local-only working docs by design; .github/pull_request_template.md
    pruned to its four required headings

Known Failures / Drift

  • None blocking (review verdict: SHIP). Gates at the PR head:
    bun run check clean; bun test 603 pass / 0 fail.
  • Recorded follow-ups for the thread-authoring phase (pilot-ruled
    deferrals): default-lane guards (shell/store/mcp results surface as
    selected-but-unmatched rather than guard-blocked — the README states
    this honestly); the empty store threads file (populate in later work).
  • Out-of-scope by ruling: interface-thread home and view-policy
    authorship (open questions for the ui_* producers), the keychain
    secret resolver, mid-stream delta publishing, concurrency caps.

Review Notes / Residual Risks

  • The deep review + remediation + final re-review ran as
    agent/agent-execute-* branches; the final verdict verified every
    remediation claim independently (both blocker reproductions re-run by
    hand) before SHIP. The response summary is
    .prompts/harness-review-response.md (local-only by the working-docs
    policy).
  • Merge is fast-forward-eligible (dev is 0 behind main); a merge commit
    is fine either way.
  • After merge: the default branch switch on GitHub is a repo-settings
    action, not part of this PR.

Change input from single { old_text, new_text, replace_all } to
edits: [{ old_text, new_text }, ...]. Multiple disjoint edits are
matched against the original file, not incrementally. Overlapping or
nested edits are rejected with a clear isError. Non-unique old_text
per edit returns an error.

Drop replace_all — each edit must be unique. Accept legacy top-level
old_text/new_text and migrate to edits[0] (pi's prepareArguments
pattern).

Stop returning full file content — return { patch, replacements,
notice } where notice is "Successfully replaced N block(s) in PATH."
Full content was context bloat.

Keep CRLF normalize/restore and unified-patch generation (buildPatch
already handles multiple ranges). MINIMAL: no fuzzy/whitespace-tolerant
retry. Upgrade path: port pi's fuzzyFindText.

Tests: multi-edit disjoint, overlap rejected, non-unique error,
full-content no longer returned, patch across multiple ranges is
truthful (apply patch to original → equals new content).
Change bytesWritten from content.length (UTF-16 code units) to
Buffer.byteLength(content, 'utf-8') — the actual number of bytes
written to disk. Test with multibyte content ('✓x✓x✓' = 11 bytes,
not 5 code units).
…esult

Nit 1: drop the non-null assertion `truncatedBy!` in the truncation
branch. The full TruncationResult is now passed straight through from
truncateHead, so the type flows without manual assertion.

Nit 2: stop hand-maintaining a reduced TruncationDetail shape. Delete
the local TruncationDetail type and the per-branch construction. Use
the full TruncationResult from ./truncate.ts as the output field type,
passing the truncation result directly (no re-computation drift).

Extend ReadOutputSchema to describe all 11 TruncationResult fields.
The schema is cast through `unknown` at the top level because
TruncationResult.truncatedBy ('lines' | 'bytes' | null) is a nullable
enum that AJV's JSONSchemaType cannot statically verify — same
limitation as the existing inputContentPartJsonSchema cast. AJV
validates the shape at runtime.

Tests: add pass-through assertion pinning truncation.outputBytes ===
Buffer.byteLength(content-without-notice) on a truncated read.
Delete src/tools/skill-client.ts (untracked, never committed) and
src/tools/tests/markdown.spec.ts. The useTool sweep deleted
use-mcp-server.ts but left skill-client.ts importing it and
markdown.spec.ts importing skill-client.ts — the one failing test
and the source of all remaining tsc errors in src/tools/.

Resolution: deletion (option 1). The plaited markdown CLI
(src/cli/markdown.ts, registered in bin/plaited.ts) supersedes
skill-client.ts with the same three modes (extract-links,
validate-links, frontmatter). The markdown skill
(.agents/skills/markdown/SKILL.md) documents the CLI as the
operator surface. No live imports of skill-client remain outside
its own spec. use-mcp-server.ts is not restored — the plan's
Decision Log explicitly dropped the MCP-serving layer.

Gate: tsc zero errors in all of src/tools/; 158 pass, 0 fail;
no use-mcp-server/useMCPServer references remain.
Removes use-mcp-server.ts and the MCP-backed git-context, markdown, and
typescript-lsp tools plus their specs. The frontier-tooling decision dropped
the MCP SDK in favor of the AJV/JSONSchemaType useTool pattern
(src/tools/use-tool.ts). package.json drops the "." root export in favor of
"./tools"; plan.md records the resolved wiring decision.
Wraps replayToFrontier as the replay-frontier tool — JSON in/out through
useTool. Output serializes the pending Set to { stateKey, pendingCount }
and passes the frontier through verbatim; a disabled selection (the raw
fn's throw) is caught into { isError, message } so it never crosses the
model channel.

TDD: red-green on the public tool boundary (5 tests), not the raw fn.
Raw algorithm fns renamed to a `Raw` suffix to free the clean camelCase
tool const names; still exported pending the privatize refactor.

Convention divergence noted: repo tools are single-word; these are
multi-word dash-case names (replay-frontier) with camelCase consts.
Wraps exploreFrontiers as the explore-frontiers tool. The internal
Map<string, StateNode> is serialized to a plain object keyed by stateKey
(Object.fromEntries, root-first); traces, findings, and report cross
verbatim (all JSON-safe). maxDepth is required (int ≥1) per the schema,
naming the non-termination hazard for unbounded-state programs.

TDD: red-green on the public tool boundary (9 tests). The tests assert on
the serialized plain-object stateGraph (Object.values(graph)[0]) rather
than the raw Map.
Wraps verifyFrontiers as the verify-frontiers tool. The raw result —
{ status, findings, report, livelocks } — is already verdict-shaped and
JSON-safe, so the tool constructs the output object explicitly (no as
cast) and passes it through. maxDepth is required (int ≥1); the progress
spec drives livelock detection (a cycle that never selects a progress
event is failed).

TDD: red-green on the public tool boundary (8 tests covering verified,
failed, truncated, livelock-with/without-progress, empty-progress,
deadlock-still-fails, omit-progress).
The interface is the tool, full stop. Removes export from the raw
algorithm functions (replayToFrontierRaw, exploreFrontiersRaw,
verifyFrontiersRaw) and all graph internals (frontierStateKey,
findStronglyConnectedComponents, findLivelocks, isCycle, StateNode,
TraceRecord, DeadlockFinding, LivelockFinding, ExploreFrontiersArgs,
ExploreFrontiersResult, VerifyFrontiersArgs, VerifyFrontiersResult).
They stay module-private — the three useTool wrappers call them
internally. Only the three tools + their Input/Output/Schema types
(and FrontierStateNode/FrontierReport output-helper types) are
exported.

Rewrites frontier-analysis.liveness.spec.ts per the TDD "public
interface, not private helpers" rule: the fake-graph builder helpers
(fakeNode, graph, labeledGraph) and tests that pinned internal
algorithm identities are deleted. SCC/livelock/stateKey behavior is
now driven through explore-frontiers and verify-frontiers on real
thread programs: a 60-step ring terminates (large-cycle SCC), a
self-loop ticker with no progress is failed (livelock), a two-state
cycle selecting progress internally is verified, escape edges don't
redeem a livelock (cycle exit to a sink is not in-cycle progress),
and two structurally-equal programs yield the same serialized state
graph (order/generator-identity invariance without fake PendingBids).

Registers the six schemas in src/tools.ts.

Convention divergence: repo tools are single-word; these three are
multi-word dash-case (replay-frontier, explore-frontiers,
verify-frontiers) with camelCase consts.
Renames the three frontier tools to domain-first names so the group
sorts together in tool listings and schema discovery:

- replay-frontier  -> frontier-replay
- explore-frontiers -> frontier-explore
- verify-frontiers  -> frontier-verify

Tool consts (replayFrontier/exploreFrontiers/verifyFrontiers), the
exported *Input/*Output/*InputSchema/*OutputSchema types, and TSDoc
self-references are updated; test imports and describe labels updated.

Module file frontier-analysis.ts -> frontier.ts (git mv, history
preserved). Private raw fns and the *Args/*Result internal types keep
their names -- not exported, no churn.
The @packageDocumentation header listed the private raw fns
(replayToFrontierRaw, exploreFrontiersRaw, verifyFrontiersRaw) as the
module's entry points, inverting the public/private surface. List the
three tools (frontierReplay, frontierExplore, frontierVerify) as entry
points and relegate the raw fns to an "internal" note. The args/result
doc {@link}s that genuinely reference the raw fns are unchanged.
The ## Trace kind filters section linked isSelectionTrace,
isFrontierTrace, and isDeadlockTrace -- symbols that exist nowhere in
src/ (the names implied guard functions that were never written).
Replace the dead links with the real discrimination surface: the Trace
union discriminated by kind (values from TRACE_MESSAGE_KINDS), linking
SelectionTrace, FrontierTrace, and DeadlockTrace -- all exported from
behavioral.types.ts and resolvable cross-module.
Finish the html refactor: five useTool tools (html-validate-and-escape,
html-validate-attribute-value, html-render, html-update-attributes,
html-scale-check) with real JSON schemas, registered in src/tools.ts.

Violations are returned as data, never thrown. HtmlViolation/CssViolation
bags replace the deleted ValidationError class. Module-private
validateAndEscapeHtmlRaw and validateAttributeValueRaw return { ok }
unions; the wrappers map them to the tool output shape. Mutating tools
return the original input html unchanged on validation failure.

Flatten src/tools/html/ into src/tools/{css,html}.schemas.ts plus html.ts;
delete the old module and its Renderer/ValidationError classes. Add
src/tools/tests/html.spec.ts (TDD from the dev-branch specs: 65 tests
covering all six swap modes, all four match operators, on* security,
quote-breakout neutralization, CSS validation, attribute rules, and
scale-check boundaries). Update the css-schemas generator/workflow paths.
Convert src/tools/mcp-client.ts from a makeCli command to the useTool
shape ({ name, description, inputSchema, outputSchema, run }) with
hand-written AJV JSON schemas — a 7-branch oneOf on `mode` per branch
(call-tool/list-tools/list-prompts/get-prompt/list-resources/
read-resource/discover), cast through `unknown` as JSONSchemaType since
discriminated unions exceed its static power (read.ts/frontier.ts
precedent). All seven modes survive; src/cli/mcp-client.ts untouched.

Auth is a permissive object in the model-facing schema and validated at
the trust boundary with an AJV-compiled discriminated-union schema
(single source, no Zod, no parallel schema source).

Connections now route through src/kernel/use-plugin-adapter.ts (renamed
from the empty use-plugin-adaptert.ts): a Map<serverUrl, PoolEntry>
lazily connected and reused across calls, evicted on connect failure,
closed on teardown — mirroring the pi-extension getSharedClient +
session_shutdown -> closeSharedClient pattern. The adapter owns no
discovery data; the tool never closes a client itself.

Tests use a real in-process MCP server fixture (McpServer +
WebStandardStreamableHTTPServerTransport over loopback HTTP) rather
than mocking the client SDK. 12 tests cover the schema contract and a
round-trip of all seven modes through the pool, plus pool reuse and
multi-url separation.

Records the 2026-09-07 Decision Log entry this implements.
Replace the in-memory createOAuthProvider + ~/.plaited/mcp/tokens file
persistence with a BunKeychainOAuthProvider that implements the v2
OAuthClientProvider shape from @modelcontextprotocol/client:

- issuer-keyed clientInformation(ctx)/tokens(ctx)/saveTokens(tokens,ctx)/
  saveClientInformation(ci,ctx) — credentials are bound to the
  authorization server that issued them (the SDK's discardIfIssuerMismatch
  enforces the stamp); ctx === undefined returns the most-recently-saved
  set for the resource-server read.
- state(), saveDiscoveryState()/discoveryState() — persist RFC 9728
  discovery state to skip re-discovery.
- validateResourceURL(serverUrl, resource) — RFC 8707 origin binding;
  rejects a resource on a different origin with IssuerMismatchError.
- invalidateCredentials(scope) — per-scope keychain eviction.
- prepareTokenRequest/addClientAuthentication — drive the v2 SDK's
  fetchToken for the client_credentials and refresh_token grants.

Tokens and client info persist to the OS keychain via Bun.secrets
(BunKeychain) — one provider per server-url, reused across process
restarts (keychain persists; the connection doesn't, but reconnect
reads tokens back). The v2 SDK's auth() orchestrator (invoked by the
transport on 401) does RFC 9728 discovery and the token exchange; the
provider supplies grant params + credentials and persists the
issuer-stamped results, so the hand-rolled buildOAuthRequest/
exchangeOAuthTokens/file persistence are deleted.

Migrate the shared adapter pool (use-plugin-adapter.ts) to the v2
@modelcontextprotocol/client Client + StreamableHTTPClientTransport,
whose authProvider option accepts the v2 OAuthClientProvider directly.

The keychain is abstracted behind a Keychain interface (BunKeychain
default; InMemoryKeychain test double) — the only boundary mocked.
9 provider tests cover token round-trip across a reconnect, the
refresh_token rotation after reconnect, issuer-binding on
clientInformation, validateResourceURL mismatch rejection, and
scoped invalidateCredentials. The 12 mcp-client round-trip tests
remain green against the v2-migrated adapter.
Add a stateless skill-client useTool implementing the agentskills.io
three-tier progressive-disclosure pattern for local skills, with
search-on-demand replacing the spec's static catalog (the deliberate
2026-09-07 decision — marked MINIMAL so the deviation is greppable).

Three modes (discriminated union on mode, hand-written AJV oneOf,
cast through unknown as JSONSchemaType — same pattern as mcp-client/
read/frontier; no Zod):

- discover (tier 1): scan .agents/skills/ at project (relative to the
  provisioned cwd) and user (~/.agents/skills/) level, parse YAML
  frontmatter into { name, description, location, ...frontmatter }
  records. Lenient validation per spec — warn but load when the name
  doesn't match the parent dir or exceeds 64 chars; skip (with a
  warning) on unparseable YAML or a missing/empty description.
  Project-level overrides user-level on name collision (deterministic
  precedence, first-found within a scope).
- read-skill (tier 2): load the SKILL.md body with frontmatter stripped;
  returns isError when the file is absent or the frontmatter is
  unparseable.
- list-resources (tier 3): enumerate bundled files in the skill
  directory (parent of the SKILL.md location) as relative paths,
  without reading them; SKILL.md itself is excluded (it is the
  instructions, not a bundled resource).

Own frontmatter parsing (does not import from src/cli/markdown.ts);
src/cli/markdown.ts stays a read-only reference. Returns data only —
never writes. cwd is provisioner-supplied (same trust-boundary
treatment as read/ls/write).

15 tests cover the schema contract and a fixture skill dir exercising
discovery, lenient-validation skips/warnings, project-over-user
precedence, body stripping, and the bundled-resource walk.
Add a stateless discovery useTool backed by .plaited/discovery.sqlite
(bun:sqlite) that holds a unified catalog of remote MCP tools and local
skills for the search-mediated progressive-disclosure loop. This is the
only tool that touches the store file.

Five modes (discriminated union on mode, hand-written AJV oneOf, cast
through unknown as JSONSchemaType — same pattern as mcp-client/
skill-client/read/frontier; no Zod):

- create: insert a row (kind 'mcp-tool' | 'skill', name, description,
  handle = server-url for mcp-tool / SKILL.md path for skill,
  metadata = inputSchema for mcp-tool / frontmatter for skill).
- read: fetch one row by id (null when absent).
- update: patch name/description/handle/metadata (null row + isError
  when the id is absent).
- delete: remove by id (deleted: true/false).
- search: case-insensitive LIKE over name + description, optional kind
  filter, limit (default 100); an empty query matches all rows (tier-1
  catalog).

dbPath is provisioner-injected, not model-facing: createDiscoveryTool
takes a resolved dbPath from the provisioner; the input schema omits
dbPath entirely, so a model-supplied dbPath is rejected by
additionalProperties: false at the boundary. This is the deliberate
deviation from the other file tools (which take a path relative to a
provisioned cwd, not an absolute store path). The SQLite connection is
lazily opened on first use so tool construction is side-effect-free.

The store is not git-backed — local SQLite, regenerable (re-scan the
filesystem, re-discover servers). Population/refresh/search are
kernel-thread policy via this tool, not adapter provisioning.

19 tests cover the schema contract (including the model-supplied
dbPath rejection), CRUD round-trips for both kinds, missing-row error
shapes, and search across name/description with kind filter, limit, and
the empty-query catalog case.
Add a Phase 3.5 section recording the search-mediated progressive-
disclosure tool primitives delivered in Slices A–E (mcp-client
useTool conversion + adapter pool, v2 keychain OAuth provider,
skill-client useTool, discovery useTool), and record Slice F
(provisioning via provision-defaults.ts + the kernel progressive-
disclosure behavioral thread) as a separate, separately-tackled
body of work — not folded into this phase.

Update the two resolved Open Questions: the discovery-tool schema
contracts are landed (hand-written AJV oneOf, no Zod); the phase
placement is confirmed as new Phase 3.5, not folded into Phase 3.
The adapter pool carried a write-only McpDiscovery cache on each PoolEntry
(getPoolDiscovery/setPoolDiscovery). getPoolDiscovery had zero readers across
src/; setPoolDiscovery was called exactly once in mcp-client discover mode,
behind a MINIMAL: comment admitting the cache was "write-only here". It was a
parallel, transient, non-authoritative discovery store duplicating the
discovery tool's responsibility (.plaited/discovery.sqlite).

Per the design rule, the discovery tool is the sole canonical discovery store;
the adapter pool owns connection lifecycle only. A refresh/TTL optimization, if
ever wanted, belongs in kernel-thread policy (when to call discovery search vs
re-discover), not a hidden adapter field.

src/kernel/use-plugin-adapter.ts:
- Remove the McpDiscovery type (the discover-mode output is typed by
  McpServerCapabilities in mcp-client.ts; McpDiscovery was a looser parallel
  used only as a pool-cache field).
- Remove discovery?: McpDiscovery from PoolEntry.
- Remove getPoolDiscovery/setPoolDiscovery exports.
- Rewrite the @packageDocumentation header: the pool owns connection
  lifecycle only; discovery data is the discovery tool's store, populated by
  kernel-thread policy.

src/tools/mcp-client.ts:
- In the discover case, drop the setPoolDiscovery(url, result satisfies
  McpDiscovery) call and the MINIMAL: comment above it. Just return
  { mode: 'discover', result }; the caller (kernel thread) decides whether
  to persist via discovery create/update.
- Drop the now-unused setPoolDiscovery / McpDiscovery imports.

Gates: tsc --noEmit clean; rg "getPoolDiscovery|setPoolDiscovery|poolDiscovery|
pool.*discovery" src/ empty; rg "McpDiscovery" src/ empty. mcp-client.spec.ts
12 pass / 0 fail (43 expect() calls) — the discover result shape is covered by
the existing round-trip test, which never pinned the cache.
Replace Zod with AJV (draft 2020-12) compiled validators, preserving the
public .parse()/.safeParse() API so existing call sites are unchanged.
Schemas are plain JSON Schema objects exposed via .schema (embeddable in
other tool schemas) backed by a shared Ajv2020 instance mirroring
html.schemas.ts and css.schemas.ts.

Discriminated unions use oneOf + const; the lax stream fallback uses
anyOf + not:{enum} so unknown types pass through while malformed known
frames still throw — the same semantics as the prior Zod .refine.

Relocate the module from src/tools/responses/ to src/tools/open-responses.ts
and its tests to src/tools/tests/, dropping the superseded Zod files to
avoid a parallel schema source. read.ts now imports the JSON Schema
directly via .schema instead of z.toJSONSchema.
Convert mcp-client from a module singleton importing getSharedClient to a
factory `createMcpClientTool({ getClient })`. The tool is now stateless; the
connection pool is injected at provisioning, matching the createDiscoveryTool
precedent. Drop the dead `mcpClient` singleton export (nothing imported it).

Expose `createConnectionPool()` in use-plugin-adapter as a closure factory
returning { getClient, closeClient, closeAll, size } — no module-level Map.
The legacy singleton exports (getSharedClient, closeAllClients,
pooledClientCount) now delegate to a default pool instance so pre-refactor
imports keep working during the collapse.

mcp-client.spec.ts constructs the tool via the factory against a test-owned
pool instance so teardown (pool.closeAll / pool.size) is isolated per suite.
All 12 mode tests still pass against the real loopback mcp-server-fixture.

`rg "import { getSharedClient }" src/tools/` is now empty — the tool no longer
imports a global pool. AdapterSessionOptions stays in the adapter for this
slice (re-exported from mcp-client); it moves to kernel.ts in Slice 2.
Move the connection pool contract out of use-plugin-adapter into kernel.ts:
createConnectionPool (closure factory), AdapterSessionOptions, PoolEntry,
ConnectionPool, and GetClientFn now live on the kernel floor. kernel.ts
instantiates the pool once via createKernel(), wires the MCP client tool
(createMcpClientTool({ getClient: pool.getClient })) at provisioning, and
owns the pool's lifecycle — shutdown() drains every pooled connection and is
registered on process beforeExit so no client leaks across an agent run.

mcp-client now imports GetClientFn + AdapterSessionOptions (type-only) from
kernel.ts instead of the adapter; it re-exports them for tool consumers. The
mcp-client test pulls createConnectionPool from kernel.ts. The kernel
teardown test asserts shutdown() drains the pool (size → 0) and is
idempotent.

use-plugin-adapter is reduced to a thin re-export shim of the pool surface
from kernel.ts so any lingering import keeps resolving mid-refactor; it is
deleted in Slice 4. The legacy singleton exports (getSharedClient,
closeAllClients, pooledClientCount) are gone — nothing imported them after
Slice 1.

Gates: tsc clean; 57 tests pass across kernel + mcp-client + discovery +
skill-client + oauth. `rg "import { getSharedClient }" src/tools/` is empty.
A useTool unit that parses a plugin.json manifest at a path resolved against
the provisioned cwd and returns the structurally-validated declarations
{ mcps, skills, models, threads }. Stateless — no writes, no provisioning;
loading only. A kernel thread reacts to plugin.loaded later (threads.ts,
deferred).

PluginManifestSchema is the contract threads consume, compiled once with
AJV. Strict object composition (additionalProperties: false) at every level:
mcps[].url required; models[] require provider/modelId/endpointUrl with
optional apiKeyRef (a keychain key name) and locality; skills/threads are
arrays of non-empty path strings. The schema rejects a raw `apiKey` field on a
model so secrets stay in the keychain — only `apiKeyRef` (a key name) is
allowed.

Errors (missing file, invalid JSON, schema violation) → { isError, message }.
The output schema is a oneOf of the manifest shape and the error branch; the
success branch reuses the manifest's component schemas so there is no parallel
schema source.

Tests (18): schema contract per field group, apiKey rejection, and run-time
behavior against real temp plugin.json files (valid parse, missing file,
invalid JSON, invalid manifest, raw apiKey at the boundary).
The connection pool contract now lives entirely in kernel.ts (Slice 2);
nothing imports use-plugin-adapter. Delete the shim. `rg "use-plugin-adapter"
src/` is now empty.

Final src/kernel/ layout: kernel.ts (engine floor + pool + future endpoint
registry), oauth/ (keychain + provider), threads.ts (empty placeholder — the
mutable surface for initial system threads, explicitly last; do not populate
in this task). Also drop two empty, unreferenced dead files left in dev
(use-server.ts, use-trace-router.ts) so the kernel layout matches the target.

No tools.ts barrel change: mcp-client and discovery (the kernel-provisioned
factory tools) are not re-exported from src/tools.ts, and plugin-loader
matches that convention — it is a provisioned manifest parser, not a
primitive file tool.

Gates: tsc clean; 375 tests pass across src/kernel + src/tools/tests;
`rg "use-plugin-adapter" src/` empty; `rg "import { getSharedClient }"
src/tools/` empty.
…emas

The UseResponse/Adapter/useResponse/CompactionResult seam lost its only
caller in 0b7baac and the upcoming model tools fetch inline with
provisioner-injected endpoint config, so the seam has no future consumer.

- delete the seam from open-responses.ts; keep all *Schema vocabulary
- rename open-responses.ts to open-responses.schemas.ts per the
  schema-only module convention (matches html.schemas.ts/css.schemas.ts)
- update the 3 import sites: read.ts, open-responses.spec.ts,
  open-responses-input-content.spec.ts
- drop the scripted-adapter test double and the two useResponse factory
  describes; the failed-stream schema test now iterates events directly

read/open-responses specs still pass (92 tests).
Treat talking to an Open Responses endpoint as two stateless tools, same
shape as mcp-client/discovery — the model is one tool in the fixed set,
threads orchestrate the agentic loop later. No router object, no adapter
layer; multi-model routing is threads choosing a provider label.

- createModelTools({ endpoints }) — provisioner-injected provider→endpoint
  map (url, keychain-resolved apiKey, extra headers); model-facing input
  carries provider/modelId only, never a URL or key
- model-respond: builds the spec wire request (model is a string on the
  wire), POSTs /v1/responses; non-streaming validates the ResponseResource
  against a schema composed from OutputItemSchema/UsageSchema/ErrorSchema;
  streaming buffers SSE frames (data: [DONE] terminates), validates each
  with KnownStreamEventSchema/lax passthrough, assembles items from
  response.output_item.done, passes response.failed through as
  status:'failed' + error
- model-compact: POSTs /v1/responses/compact, returns the compaction
  item's encrypted_content + usage
- errors are data, never throws: unknown provider names the label,
  non-2xx structured error bodies surface as { isError, message },
  function_call items come back as untouched data (never dispatched)
- MINIMAL: streaming buffers whole-body before returning; upgrade path is
  a provisioner-injected onEvent for mid-stream triggers
- fixture: startOpenResponsesServer (Bun.serve loopback) built from the
  openresponses compliance-suite contract — basic JSON response, SSE
  happy path, function_call, response.failed stream, compact resource,
  400/401 structured error bodies; records every request for routing and
  auth assertions
- tests cover non-streaming, streaming, tool-call passthrough, provider
  routing across two fixture servers, unknown provider, HTTP 400,
  streaming failure, bearer key injection, compact round-trip and errors
…LI seam

The thinnest end-to-end agent: one turn runs from a JSON prompt to a JSON
result. Tracer-bullet that makes the kernel runnable; the CLI seam Harbor
will drive later.

- createScriptedModelTools({ script }): a deterministic canned model-respond
  with no fetch, same shape and schemas as the live createModelTools.
  DEFAULT_SCRIPTED_RESPONSE keeps createKernel() deterministic with no
  endpoint configured; a live createKernel swaps in createModelTools.
- createDispatchBridge({ tools }): the action channel. Maps a function_call
  item to a tool invocation and back to a spec-valid function_call_output
  (fresh id via ueid(), call_id correlation). Unknown tool, malformed args,
  a tool isError result, and unexpected throws all become failed output items
  carried as data — never thrown into the space.
- TURN_LOOP_THREAD: a minimal, looping five-rule thread (MINIMAL: scaffolding
  turn-loop — to be replaced by autoresearch-evolved threads, Phase 5.5). The
  dynamic decisions live in the bridge (useTrace action channel, per plan.md
  Decision 2024-09-03); the thread is the static coordination skeleton.
- createKernel() owns the pool, the (scripted) model tools, the dispatch
  registry, and runTurn({ space, prompt }) -> TurnResult JSON. runTurn composes
  a fresh behavioral() per turn; the bridge bounds the loop with a
  max-iteration guard (no infinite loop).
- plaited turn '<json>': the --no-serve CLI slice. input { space, prompt } ->
  runTurn -> JSON out. No daemon, no --seed, no permission flow (Phase 6).

Gates: bun --bun tsc --noEmit clean; 57 pass across src/kernel/ +
model.spec.ts + cli/turn.spec.ts (0 fail). End-to-end determinism:
plaited turn run twice -> byte-identical JSON. A scripted function_call
dispatches and the output correlates by call_id; the turn completes.
Two Harbor tasks (build-git-context-skill, build-typescript-lsp-skill) where a
coding agent must author and install an AgentSkills-compliant skill — SKILL.md
plus a JSON-in/JSON-out CLI script — at a conventional discovery location
(.agents/skills/<name>/). Each task is self-contained: a Bun-based no-network
environment bakes in SPEC.md and deterministic fixtures, and the verifier
recomputes truth from those fixtures at grading time (no golden repo, no
oracle).

Verifier design (tests/test.sh):
- Layer 1 gates the reward: skill located at project-level
  <workdir>/.agents/skills/<name>/ with user-level ~/.agents/skills/<name>/
  fallback (project wins on collision); frontmatter name/description/body
  validated; CLI script present and executable.
- Layer 2 grades CLI behavior against recomputed expectations:
  git-context 12 checks (status staged/unstaged/untracked, merge-base history,
  worktrees, context includeWorktrees edge case, input validation);
  typescript-lsp 9 checks (discover capabilities, documentSymbol
  names/kinds/offsets recomputed via the TypeScript 7 API, hover, inline
  unsupported-method errors, input validation).

Validated with harbor run (docker oracle; daytona -k 2 -n 2): reward.json
1.0 on a reference solve, 0.0 when the skill is missing, 0.25 fractional
gradient on a partial solve.

biome.json: extend the test/CLI noConsole override to tasks/**/tests.
Rename the controller island attributes to the b-* namespace:
P_TARGET/P_TRIGGER/P_SCALE/P_FORM -> B_TARGET/B_TRIGGER/B_SCALE/B_FORM
(attribute values p-target/p-trigger/p-scale/p-form -> b-*). Updates
constants, controller runtime, html tools + schemas, behavioral/controller
test fixtures, README scaleCheck section, and framework skill references.

Behavioral-domain vocabulary (behavioral(), b-threads, the engine) is
untouched. One stale TSDoc p-target reference in src/tools/html.ts remains
and is fixed in a follow-up phase.

Verified: bun --bun tsc --noEmit clean; 19 controller Playwright failures
are pre-existing browser-launch env issues (evalJs returns no result),
unchanged by this refactor.
Phase A — package identity + CLI rename.

Package: name plaited -> @behavioral/sh, version 7.2.0 -> 0.0.1
(greenfield). Updated description, repository/homepage/bugs URLs to
github.com/behavioral-sh/behavioral + behavioral.sh, keywords, and the
bin entry (plaited -> behavioral).

CLI: bin/plaited.ts -> bin/behavioral.ts; the router name/description
become `behavioral`. Updated example/usage strings in turn, git-context,
and typescript-lsp CLI help text, plus all subprocess bin paths and
temp-dir/email/name fixtures in the CLI test suites. The Harbor task
identity in tasks/*.toml (name namespace, author name/email) is renamed.

Runtime dir: .plaited/ -> .behavioral/. Covers the connect route
(CONNECT_PLAITED_ROUTE -> CONNECT_BEHAVIORAL_ROUTE, the .behavioral/connect
URL the controller island script tag uses), the discovery.sqlite path
docs, the keychain service label (plaited.mcp -> behavioral.mcp), and
the OAuth token cache (~/.behavioral/mcp/tokens/). No migration shim —
v0.0.1 greenfield, nothing to migrate.

Identity strings: CLIENT_INFO name, OAuth client_name, and the
test-server name become `behavioral`. The dangling @see PlaitedTrigger
TSDoc ref (no such type existed) is removed; PlaitedAttributesSchema ->
BehavioralAttributesSchema (the b-* attribute schema).

Verified: bun --bun tsc --noEmit clean; src/cli + src/tools tests pass
(202 tests, 0 fail); `behavioral turn '{"space":"s","prompt":"x"}'` runs
and prints JSON. The 19 controller Playwright failures are pre-existing
browser-launch env issues (evalJs returns no result) and are unchanged by
the connect-path rename — the browser never launches, so the
.script[src*=...] selector is never even evaluated.

Behavioral-domain vocabulary (behavioral(), b-threads, the engine) is
untouched. docs/AGENTS.md/README/skills rename is Phase C.
The README described the pre-harness product: a src/kernel/ coordination
floor, a src/tools/ useTool fleet, the dispatch-bridge turn loop — all
dissolved or deleted. As the landing page of the branch about to become
main, it must describe what ships.

The rewrite follows the tree: engine in-process and families as processes
behind one wire; the composition and its guard threads; the system families
as endpoint-carrying overrides; the repository map per the per-family
folders; the public API over the four package exports; and a composing
example that imports defineConfig from the root and the system config
helpers from ./behaviors, matching src/main.ts and src/behaviors.ts.
The units were named two ways: the public/config surface said behavior
everywhere (src/behaviors/, the Behavior union, the behaviors allow-list
key, useBehavior, BEHAVIORAL_HOME, behavior_error), while prose and internal
identifiers said family — a config author read behaviors: ['shell'] and was
then told shell is a family. Every naming decision to date had already
chosen behavior; family was the borrowed word.

This standardizes on behavior and retires family (git history keeps it):

- identifiers: familyAddThreads -> behaviorAddThreads, FamilyPort ->
  BehaviorPort, FamilyResult -> BehaviorResult, spawnFamily ->
  spawnBehavior, frontierFamily -> frontierBehavior, entryByFamily ->
  entryByBehavior; the family-harness test helper becomes behavior-harness
- the routing map in b-program.ts was renamed off "behaviors" to lanes —
  it collided with the allow-list param of the same name
- prose across every module doc, AGENTS.md, and the README (including the
  mermaid subgraph), with the two blind-rename artifacts hand-fixed: the
  stale wire list shell/store/responses/mcp in use-behavior.ts, and the
  sentence-case Each BEHAVIOR in AGENTS.md

The css-schemas drift contract caught its header change and was
regenerated. Full suite 595 pass; check clean.
"Behaviors" was the borrowed word: the paradigm owns behavior (behavioral
programming, the engine, the threads), and the capability units borrowed it
for infrastructure that has no canonical name. The cognitive-science term
fits them exactly — faculty, an inherent capacity of a system: shell the
motor faculty, store memory, mcp tool use, systemOne intuition,
systemTwo reasoning, frontier metacognition — and it extends the Kahneman
naming the system faculties already used. It also untangles the
src/behavioral/ vs src/behaviors/ one-letter collision.

The split, now two layers: behavioral = the paradigm (engine, threads,
config type, BEHAVIORAL_HOME, @behavioral/sh — unchanged everywhere);
faculty = a capability unit.

- src/behaviors/ -> src/faculties/, src/behaviors.ts -> src/faculties.ts,
  package export ./behaviors -> ./faculties
- Behavior -> Faculty (union), useBehavior -> useFaculty (file
  use-faculty.ts), the config key behaviors: -> faculties:, the wire
  registry BEHAVIOR_MESSAGE_KINDS -> FACULTY_MESSAGE_KINDS with
  behavior_error -> faculty_error
- each family folder's process entry behavior.ts -> faculty.ts and its
  spec; shared wire files faculties.types/constants/threads; the harness
  test helper and resolve-faculty-entry
- AGENTS.md, the readme, and the skills follow; the engine and controller
  are untouched by scope and by grep (the law held: zero references)

Full suite 595 pass; check clean; css-schemas drift contract unchanged.
…works

The review's first blocker: a config generated by init could not load from
a host process. serve resolves the config's `@behavioral/sh` imports by
walking node_modules from the CONFIG FILE's directory; a bare home has
nothing on that walk, and Bun's global node_modules fallback covers only
entry execution, not dynamic imports — so `behavioral init && behavioral
serve` failed with "Cannot find module '@behavioral/sh'".

Two parts:

- bun.lock regenerated. Its root name said "plaited" (a rename casualty),
  which made `bun add -g <path>` fail while reconciling global deps — the
  mechanism for putting the CLI on PATH. The stale "plaited": "link:plaited"
  entry in the user's global manifest was the same drift on the other side.
- init now links the running package under <home>/node_modules/@behavioral/sh
  (resolved from init's own location, so it points at whatever install is
  running). The generated config resolves from any host; the link appears
  in init's file output and is idempotent. MINIMAL: symlinkSync assumes
  POSIX/Bun; on Windows without symlink privileges, the documented fallback
  is a global install.

The missing load-test lands with the fix (the gap that let this ship):
runInit into a temp home → loadConfig the generated file → bProgram with
it — the generated config must load and compose, not merely look right.

Verified by hand from a clean home: init → serve boots the full
composition (thread_added traces, zero errors) with no manual symlinks.
…cution

The review's second blocker: shell_request/shell_cancel routed
UNCONDITIONALLY while store and mcp sat behind has(). A host could compose
bProgram({ faculties: ['store'] }) and a client-injected shell_request still
spawned the shell process and was answered — contradicting the param's
contract, the Faculty union's allow-list meaning, and the serve spec's own
claim that an empty allow-list means "no spawns". Shell is the
arbitrary-execution faculty; a pruning boundary that does not bound it is
a violated design law.

The route now sits behind has('shell'), matching store and mcp: a pruned
shell has no route, spawns nothing, and mounts no pack. The allow-list
spec proves the boundary end-to-end: with faculties: ['store'], a
triggered shell_request is never answered.

Re-verified by hand: bProgram({ faculties: ['store'] }) + a shell_request
trigger produces no shell process and no result.
The notification branch awaited its handler without a guard — one bad
notification rejected the read loop, serve exited 1, and runtime.terminate
never ran. A notification has no response channel (no id), so the failure
is logged to stderr and the loop keeps dispatching; requests keep their
-32603 error response. Spec: a throwing notification is survived and the
message after it still answers.
…bundle

Three follow-up findings in the init command, one coherent bundle:

- follow-up 3 — the provider template was systemOne-shaped for both
  faculties: systemTwo's stub destructured `{ endpoint, signal }` (its
  context is the endpoint MAP) and returned `{ model, answers }` (the
  Decisions output; systemTwo's is the Open Responses `{ items, status }`),
  so a scaffolded systemTwo provider failed its own typecheck. Each stub
  now matches its faculty's respond contract; specs assert both shapes
  and the negative, and the generated file typechecks against the surface.
- follow-up 6 — ts() escaped quotes and backslashes but not control
  characters: a newline in a URL/header wrote an unterminated literal
  (init exits 0, the config dies at load). The escape funnel now covers
  the whole control range (\n/\r/\t named, others \uXXXX).
- follow-up 7 — the bare-file-name pattern that blocks path traversal
  lived only in the input schema; the interactive collector's free-text
  file answer flowed into runInit raw. The gate is enforced inside
  runInit, the one funnel both paths share; the spec proves it via a
  direct bad input and the collected tour input.
The follow-up 4 finding: two readers of the same schema disagreed on
failure. eventGuardEntries threw when properties.type.const was absent;
useFaculty's pump silently computed an undefined lane seal and dropped
EVERY inbound result — a wiring defect surfacing as "requests are
answered by nothing, no error, no trace".

The extraction now has one home — eventTypeOf, exported from
faculties.threads — and both readers use it: the guard generator and
useFaculty's seal, which now throws at wiring time (the outer call,
where schemas are handed over), same check and message. Specs pin both
throws; eventGuardEntries' throw was previously untested.
Follow-up 2 (plus item 5's ruling and the two nits), one sweep:

- README (item 5, per the pilot's ruling): the faculties-are-processes
  paragraph claimed the faculty guard blocks every parsed-but-invalid
  result — true only for the systemOne/systemTwo overrides. The sentence
  now states both lanes honestly and names guarding the default lanes a
  recorded follow-up (the pilot reversed the doc's mount-them ruling).
- frontier-analysis reference: described the pre-refactor Worker world and
  cited `frontier.worker.ts`/`workers.types.ts` (paths that never existed
  post-rename); now describes the in-process embed, bindEmit, and the
  wire's real one home (faculties.types.ts).
- mcp-client reference: the result envelope was the retired
  {id, status, durationMs}; now the uniform {id, ok, result|error} with
  the real codes (authorization_required/timeout/canceled/error).
- three links to the untracked prompts/ dir (gone in 789b21e) replaced
  with the tracked artifact (css.schemas.ts as classifier context).
- rename-prose corruption: "runtime/tool faculty" back to behavior (the
  conduct sense) in AGENTS.md and mcp types; "mcp-client worker" is now
  the mcp faculty process.
- retired-Worker headers: shell/mcp/system-two types, faculties.types,
  frontier faculty (postMessage/zod/"worker faculty" → the process/embed
  world); init/cli/b-program header debris (the cut --interactive, the
  stale "shared tools AJV", the duplicated host-constructed fragment).
- nits: the duplicated postResult doc block in shell merged; the SSE
  first-data-line ceiling gets a MINIMAL comment naming the upgrade path.
The diagram spread horizontally: every rank child of COMPOSE (frontier
embed, faculties, serve) sat side by side because they all link upward to
the composition. Now the subgraph members stack (direction TB in HOST
and FACULTIES), and invisible rank hints (~~~, layout-only) chain
embed -> faculties -> serve so the whole diagram reads top to bottom:
host, composition, in-process embed, spawned faculties, the serve edge.
No visible edge or label changed.
The rename sweep fixed AGENTS.md:61's 'runtime/tool faculty' corruption
but left its neighbor one paragraph down: 'when they do not change
faculty' was the conduct-sense 'behavior' before the rename. Restored.
Two flowchart attempts sprawled horizontally for a structural reason: the
embed, the faculties, and serve are all rank-children of the composition,
so every flowchart layout fans them sideways. block-beta is mermaid's
layered-diagram type — a single column of stacked layer blocks with the
fan-out as connectors, which is the shape this architecture actually is.

All content is preserved: the host layer (config/ingress/observation),
the composition's guards-packs-engine-pump stack, the frontier embed, the
five capability faculties, and serve. The wire annotation rides the
faculties block title (block diagrams do not label edges).
…d block

The SERVE layer was declared with the block: prefix, which opens a
nested block that requires an end. With no body and no end, the parser
consumed the connector lines as its children and died at EOF wanting
'end'. A bare top-level block takes no prefix — the four true layers
(HOST, COMPOSITION, EMBED, FACULTIES) each pair a block: with an end;
SERVE is now a plain block. All content unchanged.
The block diagram read as stacked rows: columns 1 forces one box-wide
column, so the architecture became a list with borders. Back to
flowchart TD, but with the structural cause of the old horizontal sprawl
removed: frontier (the in-process embed) now sits INSIDE the composition
block and serve (the host's CLI face) INSIDE the host block — both are
architecturally true — so no loose rank-siblings remain and the rank
math yields exactly three vertical bands: host, composition, faculties.
The faculties spread side by side within their band (a process bus),
composition stacks its guard/pack/engine/pump/frontier internals, and
all wire labels are back on the edges (block diagrams cannot label
edges).
…ts job

One diagram was carrying three jobs — the layered assembly, the faculty
catalog, and the request lifecycle — which is why no layout type felt
right. Now:

- the assembly (flowchart TD): terse three bands — host, composition,
  faculties — with one-line labels; the prose paragraphs carry the
  detail the old node labels were folding in, and the faculties render
  as a five-slot process bus.
- the life of a request (sequenceDiagram): the lane contract as an
  actual message flow — request line in, result re-enters, valid
  selects, malformed is blocked by the guard visibly, cancel aborts
  with the first stop reason winning, crash synthesizes exactly one
  faculty_error with respawn on demand. The type is built for exactly
  this story; it sits after the 'faculties are processes' paragraph it
  illustrates.
The collections of threads a faculty ships were called packs (thread
pack, root guard pack, the ICL pack) — a second word for a thing the
repo already names: the faculty's threads. The files are threads.ts, the
exports are shellThreads/mcpThreads/facultiesThreads; "pack" was the
odd term out against the settled faculty vocabulary.

Every occurrence is now "threads": the composition's root guard
threads, each faculty's default threads, thread mounting in useFaculty
and bProgram, spec names and comments, AGENTS.md's thread section, and
the readme diagram node. No identifier changed — this was prose and
comments only.

Also fixes the sequence diagram parse error (semicolons are statement
separators in mermaid sequence diagrams — the note now uses a comma).
…o AGENTS.md law

The template keeps four headings (Context, Summary, Changed Files,
Known Failures/Drift, Review Notes). The dropped sections duplicated
the working law: targeted tests and tsc are the repo's quality gate,
and the agent checklist is behavioral guidance that belongs in
AGENTS.md, not a form.
Comment thread .github/workflows/css-schema-drift.yml Fixed
Comment thread .github/workflows/publish.yml Fixed
Comment thread src/behavioral/jq.worker.ts Fixed
CI floated `bun-version: latest`, which is how the bun 1.4.x worker regression
reached the PR unannounced. Pin `bun-version: 1.4.2` in ci and css-schema-drift,
raise the engines floor to `>= v1.4.2`, and pin `oven-sh/setup-bun` to its
v2.2.0 commit (the CodeQL unpinned-action findings).
…m pool

bun 1.4.x drops a worker's first postMessage when `self.onmessage` is assigned
after a top-level await, so every transform timed out as jq_timeout and the
shell/mcp/skill threads cascaded. Register the handler synchronously and queue
until the wasm compile resolves.

The worker is now a persistent pool of one: the host resets the status slot and
reuses the worker across evaluations, paying worker boot + jq.wasm compile once
per process instead of per eval. It is `unref`'d so it never holds the host
process open (the runtime is `bun run`, which blocks on a live worker), and a
timeout terminates and lazily respawns it. The single wait splits into a 30s
startup budget (boot + compile) and the 1s eval budget, so a slow runner no
longer reads as a jq timeout. The handler rejects any non-empty origin (CodeQL
js/missing-origin-check).
Swap the hand-rolled `deepEqual` for the native `Bun.deepEquals` in
`eventMatchesCandidate`. Event details are JSON-shaped, so the semantics match
and the native call is faster.
…gistry

The default CodeQL setup has no config support, so the four high-severity
regex alerts in the auto-generated `src/cli/credential-patterns.ts` could not
be suppressed. Switch to an advanced workflow with a `paths-ignore` for that
file (betterleaks-generated, drift-checked, over-redaction by design) while
keeping the `security-extended` suite the default setup ran.
@github-advanced-security

Copy link
Copy Markdown
Contributor

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

The advanced workflow uploaded under `.github/workflows/codeql.yml:analyze/...`
while the already-open alerts live in the default setup's `/language:<lang>`
category. A zero-result analysis only closes alerts in its own category, so the
stale alerts stayed open. Pin `category: /language:<lang>` so the paths-ignored
analysis supersedes and closes them.
`category` is an input to `codeql-action/analyze`, not `init` — the previous
commit set it on `init`, where it was silently ignored and the analyses kept
the workflow-path category. Setting it on `analyze` makes the zero-result,
paths-ignored analysis land in the default setup's `/language:<lang>` category
so it closes the orphaned alerts there.
@EdwardIrby
EdwardIrby merged commit 949b86b into main Sep 23, 2026
7 checks passed
@EdwardIrby
EdwardIrby deleted the dev branch September 23, 2026 13:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants