Repository navigation
feat(mt#5130): Computed autonomy class, consumer default-deny, and the BLOCKED hack retired - #3746
Conversation
…nd the BLOCKED hack retired Every open task resolves to principal-gated / pull-only / contained / unknown from signals (kind, tags, title, the spec's Scope and Summary, origin) by a pure classifier in packages/domain/src/tasks/autonomy-class.ts — never a stored field. TaskRoutingService.findAvailableTasksWithClass loads the two spec sections in one query (autonomy-class-store.ts) and serves only pull-only/contained, reporting excludedByClass; findAvailableTasks delegates so the old shape denies too. tasks_route annotates the target and each step and never calls a gated target "ready". Pointing candidate items carry the class. The listed surface is the deploy BOUNDARIES (CONFIG_SURFACE_PATTERNS, now exported) plus security surfaces — not isDeploySurfaceFile, whose ^src/ entries would gate every code task. EngProd proposals are filed TODO; the engprod-proposal tag is what gates them. Acceptance is a tag swap to engprod-accepted (the cockpit's Accept), and the ledger reconciles on disposition (tag or planned past TODO) instead of "left BLOCKED", so the 15-row BLOCKED → TODO migration records nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147JJ3SVgqRz9JyUD4NZBz9
… live verification script findAvailableTasks fetched each dependency with its own getTask, serially per task — measured on prod at 159 round-trips and ~18.5 s per tasks_available call. One bulk read (with a per-id fallback for fakes without getTasks) brings the same 644-task result to ~0.9 s. scripts/verify-autonomy-class.ts runs the real routing service read-only against the configured DB, prints the class distribution and the principal-gated reasons histogram, and fails if any engprod-proposal task is served (AT4's prod probe). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147JJ3SVgqRz9JyUD4NZBz9
Minsky Reviewer StatusVerdict: APPROVED — no blocking findings Commands
|
Cockpit PreviewPreview URL: https://cockpit-preview-production.up.railway.app Deployed from commit Updated by Cockpit Preview. |
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Solid, well-tested introduction of computed autonomy class across routing and adapters, with thoughtful SQL/no-SQL paths and visibility of withheld counts. However, I found a blocking behavioral defect in the EngProd proposals route: accepting a pre-migration BLOCKED proposal leaves it BLOCKED (status untouched), contradicting the retirement goal and keeping the task potentially unroutable by status. I also noted a couple of non-blocking robustness/readability issues: possible duplicate merging when a task carries both proposal tags, narrow fallback gating in tasks_available that may miss honest-degraded behavior on other DB errors, and a minor clarity nit in the bulk dependency-status path. Documentation impacts are significant and should be checked: cockpit proposals flow and CLI outputs changed. Please address the BLOCKING item before merge; the others can be follow-ups.
Findings
- [BLOCKING] src/cockpit/routes/engprod-proposals.ts:351 — Accepting a pre-migration BLOCKED proposal leaves it BLOCKED (status untouched), contradicting the intent to retire BLOCKED containment and likely keeping the task unroutable
InhandleDecision(...)the accept path computesnewStatusas the current row status (const newStatus = decision === "accept" ? currentStatus : "CLOSED";) andcheckProposalGuardexplicitly allows pending proposals withstatus === "BLOCKED"(seecheckProposalGuardreturning{ kind: "ok" }for BLOCKED at lines 270–279). As a result, accepting a not-yet-migrated BLOCKED proposal will swap the tag but persist statusBLOCKED(lines 351–386), leaving the task in a legacy containment state. This undermines the retirement goal and can keep the accepted task out oftasks_availableby status filter even after its provenance gate is lifted by the tag swap. Suggested fix: on accept, ifcurrentStatus === "BLOCKED"(legacy shape), setnewStatus = "TODO"(or the intended post-migration default) while still swapping the tag; keep the "status untouched" rule only for already-migrated (non-BLOCKED) rows. Update tests to cover this case. - [NON-BLOCKING] packages/domain/src/tasks/pointings.ts:224 — Potential silent omission of
autonomyClasswhen classifier returns falsy; rely on explicit undefined check
selectCandidatessetsitem.autonomyClassonly whenclsis truthy (if (cls) item.autonomyClass = cls;). All current classes are non-empty strings, so this works today, but it relies on truthiness rather than explicit undefined/null checks. If a future refactor introduces an empty-string sentinel or a narrowed union, the annotation could be silently dropped. Suggest usingif (cls !== undefined)to guard instead, mirroring the option contract (classifier absent vs. present). - [NON-BLOCKING] packages/domain/src/tasks/autonomy-class-store.ts:18 — Section-extraction regex duplicated in SQL and TS — drift risk
sectionPattern()(SQL-side) andextractSpecSection()inautonomy-class.tsimplement the same## <Heading>body matcher separately. This is a classic SoT-duplication risk (Constitution §14): any tweak to one (e.g., permitting trailing whitespace in headings) can leave the other subtly out of sync and hard to diagnose. Consider exporting a single canonical pattern (or small builder) from one module and importing it, or centralizing tests that exercise BOTH paths against shared fixtures. - [NON-BLOCKING] packages/domain/src/composition/domain.ts:224 — Constructor-arity change coupling:
TaskRoutingServicenow constructed with 3 args — verify all environments updated
This container now instantiatesTaskRoutingServicewith a third dependency (createDbAutonomySignalSource(db)). If any alternate composition (tests, scripts, or non-standard runtime containers) still constructs it with 2 args, TypeScript or runtime will break. You updatedpackages/domain/src/composition/test.tsstubs, but please confirm all other constructors (e.g., any directnew TaskRoutingService(...)in tests/utilities) have been migrated in the other chunk; otherwise this change is fragile across environments. [NEEDS VERIFICATION across the repo; not in this chunk.] - [NON-BLOCKING] packages/domain/src/deployment/deploy-surface.ts:39 — Newly exported
CONFIG_SURFACE_PATTERNSexpands API surface — ensure no external consumers depend on non-exported behavior
CONFIG_SURFACE_PATTERNSwas madeexport constto satisfy the autonomy classifier. This is sensible, but it promotes an internal list to a semi-public API. If other modules begin to import it for unrelated purposes, future adjustments (e.g., adding/removing patterns for deploy, not autonomy) could have unintended ripple effects. Consider documenting its contract more explicitly (already partially done in the comment) and re-exporting via a dedicatedautonomy-surface.tsfacade to avoid consumers taking a dependency on deploy-specific semantics. - [NON-BLOCKING] packages/domain/src/engprod/ledger-service.ts:115 —
decideReconciliationcontract changed shape — audit all callers beyondtoil-miner-tick.ts
The signature changed from(status: string|undefined)to(task: {status, tags}|undefined)and semantics moved from containment to disposition. You updatedtoil-miner-tick.tsand tests here, but any other callers (even test helpers) must be migrated. Please ensure the cockpit-side mirror (deriveDisposition) is kept in lockstep (its update is outside this chunk). - [NON-BLOCKING] src/cockpit/routes/engprod-proposals.ts:149 — GET /proposals merges two tag-filtered lists without de-duplication; a task carrying both tags would be duplicated
The route fetchespendingTasksandacceptedTasksvia twolistTaskscalls and then concatenates them (const proposalTasks = [...pendingTasks, ...acceptedTasks];at line ~156). While the intended state is mutually exclusive tags, a transient or bad state could carry both and produce duplicates downstream. Consider de-duplicating byidwhen merging to make the route robust to inconsistent rows. - [NON-BLOCKING] src/adapters/shared/commands/tasks/routing-commands.ts:139 — Fallback path only downgrades on a specific error string; other SQL-path failures will bubble and skip the honest-degraded path
IncreateTasksAvailableCommand, the SQL path falls back to the no-SQL path only when the thrown error message contains"does not expose a database connection"(lines 139–188). Other initialization errors (e.g., transient DB outage) will surface as hard failures instead of using the bounded per-task spec-read fallback the comments describe. Consider widening the catch condition (e.g., a well-typed capability/availability guard) or offering a--force-fallbackflag for operators to get degraded-but-honest behavior. - [NON-BLOCKING] packages/domain/src/tasks/task-routing-service.ts:224 — Bulk dependency-status path detection does a presence check but still calls through the interface; slight clarity/readability nit
loadDependencyStatuseschecks forgetTaskspresence via(this.taskService as { getTasks?: unknown }).getTasksand then callsawait this.taskService.getTasks(ids)inside the same branch (lines ~200–216). This works, but using thebulkvariable (already bound) would better reflect the capability check and avoid confusion in hand-written fakes. Not blocking — purely a readability cleanup.
Documentation impact
- blocking-needs-update — This chunk changes documented behavior for EngProd proposals: acceptance is now a tag swap (
engprod-proposal→engprod-accepted) and proposals are filed TODO, not BLOCKED. In-code docs and comments were updated (e.g.,src/cockpit/routes/engprod-proposals.tsheader), but repo docs are not included in this chunk. Verify thatdocs/cockpit-ui.mdand any EngProd RFC amendment are updated elsewhere; if not, they need edits to remove "BLOCKED → TODO" acceptance and to describe dual-tag listing. Also,tasks_availableoutput now reportsexcludedByClassand items includeautonomyClass; CLI/manifests updated here (src/generated/completion-manifest.json), but user-facing docs for these commands should reflect the new fields. Affected docs likely:docs/cockpit-ui.md,docs/autonomy-class.md(new), and any task-routing command docs if present.
…; one section pattern; dedupe BLOCKING: accepting a proposal filed before mt#5130 and not yet migrated swapped the tag but left it BLOCKED. `acceptedTaskRow` (pure, tested) now lifts that shape to TODO and leaves any other status alone. Also from the review: the proposals list de-duplicates by id; the section regex is one builder (`specSectionPattern`) sent to SQL and compiled in TS; the pointing annotation guards on `!== undefined`; `getTasks` is checked and called through the same narrowed view. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147JJ3SVgqRz9JyUD4NZBz9
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
R2 verification focused on the fixes since R1. The prior BLOCKING concern around accepting legacy BLOCKED proposals was addressed: checkProposalGuard now treats BLOCKED and TODO as pending when tagged engprod-proposal, and acceptedTaskRow lifts legacy BLOCKED to TODO on accept. Tests assert both. The autonomy classifier and store are pure and aligned with the spec, and TaskRoutingService applies the class filter before dependency reads, returning excludedByClass and annotating items and routes as required. Proposals listing now merges pending+accepted via two queries and de-duplicates by id. I found no new critical defects introduced by the fix commit; remaining notes are non-blocking acknowledgements of improvements. Docs were updated to reflect the new behavior. Verdict: APPROVE.
Findings
- [NON-BLOCKING] src/cockpit/routes/engprod-proposals.ts:248 — Guard and accept path now handle legacy BLOCKED proposals; prior BLOCKING concern appears resolved
The newcheckProposalGuardno longer requiresstatus === "BLOCKED"and treats bothBLOCKEDandTODOas pending when taggedengprod-proposal(lines ~214–248). The newacceptedTaskRowlifts a legacyBLOCKEDrow toTODOon accept and swaps the tag (lines ~264–281). Tests insrc/cockpit/routes/engprod-proposals.test.tsassert both behaviors, including the lift. This addresses the earlier BLOCKING issue where accepting a pre-mt#5130 proposal left itBLOCKED. - [NON-BLOCKING] packages/domain/src/tasks/task-routing-service.ts:146 — Class filter applied before dependency lookups; behavior aligns with spec and improves perf
findAvailableTasksWithClasscomputes classes for the status-filtered set, excludes unservable (principal-gated/unknown), and only then does a bulk dependency status read (lines ~171–236). This matches SC3 and avoids wasted per-dependency reads for excluded tasks. No issues found; noting the ordering as a positive change. - [NON-BLOCKING] packages/domain/src/tasks/autonomy-class-store.ts:34 — Section-pattern single source used for SQL and TS paths
The store usesspecSectionPattern("Scope"/"Summary")imported fromautonomy-class.ts(lines 21–26, 34–41), ensuring the Postgressubstring(... from pattern)and the TSRegExpstay in sync (addressing a prior nit about duplication). Good cohesion; no action needed. - [NON-BLOCKING] src/cockpit/routes/engprod-proposals.ts:120 — Proposals list merges pending+accepted via two queries and de-duplicates by id
GET /api/engprod/proposalsnow reads bothengprod-proposalandengprod-acceptedtags and merges with a Map to avoid duplicates (lines ~103–132). This prevents double rendering if a row transiently carries both tags. Tests cover behavior. No issues. - [NON-BLOCKING] packages/domain/src/tasks/pointings.ts:213 — Candidate items annotate
autonomyClassonly when supplied
selectCandidatesaccepts an optionalclassOfand setsitem.autonomyClassonly whencls !== undefined(lines ~205–227). This avoids writing an explicitundefinedand keeps annotation strictly additive.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
packages/domain/src/tasks/autonomy-class.ts exports computeAutonomyClass(input): { class, reasons[] } as a pure function over the task row (kind, tags, status, title), the spec's ## Scope and ## Summary text (or undefined when not loaded), a humanOrigin: boolean | undefined signal, and isListedSurface(path). The listed surface is this task's own list, AUTONOMY_LISTED_SURFACE_PATTERNS, NOT isDeploySurfaceFile (planning correction, 2026-09-13: DEPLOY_SURFACE_PATTERNS includes ^src/, ^packages/domain/src/, ^packages/shared/src/ since mt#4013 widened it to the whole bundled tree for deploy VERIFICATION — as an autonomy surface it would gate every code task). The list = the deploy BOUNDARIES (CONFIG_SURFACE_PATTERNS in deploy-surface.ts, exported by this task: infra/, Dockerfile, services/*/{Dockerfile,railway.json,deploy.config.ts,railway.config.ts}, .github/workflows/deploy*.yml) plus LOCAL_APP_DEPLOY_SURFACE_PATTERNS (cockpit-tray/src-tauri/) plus the security surfaces: .github/workflows/** (every workflow — a CI security surface, gate (l) sub-case 1), .minsky/hooks/**, .claude/hooks/**, packages/domain/src/persistence/**, packages/domain/src/storage/migrations/**, drizzle.pg.config.ts, packages/domain/src/auth/**. A test pins the list. Scope paths are extracted from ## Scope by a path-shaped-token regex (repo-relative tokens containing / or ending in a known extension), so a scope written as prose with backticked paths is read the same as a bullet list. Rules, in dominance order: |
principal-gatedif ANY: kindumbrella; kindstate-ops(a shared-state operation —principal-context.mdc §Authorization for shared / production state changes); a tag in {rfc,adr,naming,product-surface,vendor,engprod-proposal}; the title starts withRFCorADR(measured 2026-09-13: 16 open tasks by title prefix, only 8 of them tagged);## Scopenames a listed surface; the title or## Summarymatches a principal-reserved marker from the pinned set {rename,naming,product name,customer-facing,product surface,vendor,paid plan,signup,pricing} (measured on the 868-task default population: this set gates 56; a broad set withname/architecture/product/productiongates 211 on incidental prose and is rejected).- else
containedif ALL:humanOrigin === true(criterion 2); status READY (gate-passed); every extracted scope path outside listed surfaces; and one RFC shape — bugfix with a reproducing test named in the spec, incident follow-up (incidenttag or anOrigin:line matching/incident/i), cleared-blocker re-surface (acleared-blockertag — mt#2825's marker when it ships), docs-only or generated-only scope (every extracted path underdocs/orsrc/generated/). - else
pull-only. Kindwork-packageispull-onlywith the reasonADR-046: claimed deliberately—findAvailableTasks' existing kind exclusion is untouched. unknownonly when inputs are unavailable (spec signals not loaded and the row signals alone do not resolveprincipal-gated; no task row) — never as a default for a well-formed task. | Met |computeAutonomyClassand helpers are implemented inpackages/domain/src/tasks/autonomy-class.ts:151–287. The listed surface lives inAUTONOMY_LISTED_SURFACE_PATTERNS(lines 83–108) built fromCONFIG_SURFACE_PATTERNSandLOCAL_APP_DEPLOY_SURFACE_PATTERNSplus security patterns;isListedSurfaceuses it (lines 109–121). Scope extraction viaextractScopePaths(lines 123–150). Dominance rules implemented incomputeAutonomyClass(lines 199–287), includingprincipal-gatedsignals,containedconjuncts,work-packagepull-only, andunknownwhenspecisundefined. |
| Human-origin evidence. There is NO author column ontasks(tasksTable,packages/domain/src/storage/schemas/task-embeddings.ts), and provenance rows (taskOrigin: human\|agent\|automated,packages/domain/src/provenance/types.ts) are written at PR creation bysession-pr-operations.ts, so a backlog task has none. This task does NOT add a column. Absence of evidence ⇒ agent-authored ⇒ cannot becontained— default-deny applied to the signal itself.containedis therefore unreachable in production until a human-origin marker exists; a follow-up may addoriginattasks_create, set by channel (cockpit form = human, MCP = agent, miner = automated). File that follow-up from this task; do not build it here. | Met |computeAutonomyClassrequiresinput.humanOrigin === trueforcontainedand otherwise adds "no human-origin evidence" to reasons before returningpull-only(lines 247–257). The new code does not modify schemas; no author/origin column is introduced in the diff. Production call sites passhumanOrigin: undefined(seepackages/domain/src/tasks/task-routing-service.ts:114–129). |
|findAvailableTaskscomputes the class per candidate, excludesprincipal-gatedandunknown, and each served item carriesautonomyClass. Because the method's return type isAvailableTask[]with two production callers, the count lands on a siblingfindAvailableTasksWithClass(options) → { tasks, excludedByClass: { principalGated, unknown } }thatfindAvailableTasksdelegates to (so the old signature also denies); thetasks_availableadapter calls the sibling and reportsexcludedByClassin both JSON and text output. Data path (planning, 2026-09-13):listTasksreturns no spec text, and the classifier needs## Scope+## Summary; a per-candidategetTaskSpecContentwould be ~870 queries per call.packages/domain/src/tasks/autonomy-class-store.tsexportsloadAutonomySpecSignals(db, taskIds)— ONE query overtasks ⋈ task_specsextracting the two sections with a Postgres AREsubstring(content from '(?:^\|\n)## Scope[^\n]*\n((?:[^\n]\|\n(?!## ))*)')(measured on prod: 418 KB Scope + 779 KB Summary across the 868-task default population, vs 6.3 MB of full content; Summary capped at 2,000 chars in SQL). It is injected intoTaskRoutingServiceas an optional third constructor dependency (AutonomySignalSource), registered incomposition/domain.tsthe wayTaskGraphServicegets its db. The adapter's no-SQL fallback path has no bulk source: it classifies on row signals alone, so anything notprincipal-gatedby row isunknownand excluded, counted, and named in the fallback's warning line — honest default-deny rather than an N+1.
tasks_route(correction):generateRoutenever callsfindAvailableTasks— it walks the dependency graph toward a caller-named target and returns steps regardless of status, so "inherits via the same service" was false and "serves" does not apply to it. What it must not do is present a principal-gated task as work to start: the route's target and eachRouteStepcarryautonomyClass(additive fields),readyTaskscounts only steps whose class ispull-only/contained, and the "ready to start immediately" line is replaced by "target is principal-gated () — a person decides" when the target's class isprincipal-gated/unknown. A route to an umbrella stays a legal navigation query; nothing is refused.
Pointing candidate sets (mt#5129, additive):eachCandidateSet.items[]entry carriesautonomyClass— the RFC's "membership in a pointing never overrides this" made visible on the surface an agent reads in Phase 1.loadCandidateRowsinpointings-store.tsadds the two section columns to its existingtasks ⋈ task_specsselect;selectCandidatestakes an optionalclassOf(id). Annotation only — a candidate set is the principal's own declaration, and the deny lands on Phase 2's pull.
A refusal names the class (AT5):tasks_availablereportsexcludedByClassand, when a specific task is requested by id viatasks_route, the class and reason are on the target. | Met |TaskRoutingService.findAvailableTasksnow delegates tofindAvailableTasksWithClass(lines 97–111). The sibling computes classes, excludes unservable, returnsexcludedByClass, and annotates each item withautonomyClass(lines 146–205, 256–267, 292–300). The bulk spec-signal loader is implemented inpackages/domain/src/tasks/autonomy-class-store.ts(entire file), exportingloadAutonomySpecSignalsandcreateDbAutonomySignalSource.generateRouteannotates steps and target and counts ready tasks by class (lines 307–390).pointings.tsannotatesCandidateItem.autonomyClasswhenclassOfis provided (lines 199–228). |
| Retirement, ordered inside ONE PR so there is no routable window: (a) the consumer default-deny lands; (b)packages/domain/src/engprod/proposal-filing-service.tsfiles proposals asTODO— theengprod-proposaltag is what now contains them; its header comment and the filed spec text ("This task was filed automatically and is BLOCKED…") are updated; (c) the live proposals migrate BLOCKED → TODO (legal:workflows.ts:200) viatasks_bulk-editdry-run then execute. This task is the >10-record wrapper (operational-safety-dry-run-first.mdc); scope-match is exactly theengprod-proposal-tagged BLOCKED set, count re-derived at run time (15 on 2026-09-13 ~07:55Z, SQL overtasks). | Met | This diff shows the consumer default-deny and cockpit acceptance path changes. The filing flip and migration scripts are not in this narrowed diff segment, but the tests referenced in the PR description cover filing as TODO and the cockpit route now treats pending by tag rather than status (engprod-proposals.ts:200–248). Given the current diff scope, retirement ordering for consumer + cockpit is implemented; the actual migration run is post-deploy per spec and out of diff scope. |
| Override of a computed class is principal-only via an ask (RFC sub-decision, accepted 2026-09-13): no override write path exists in this task.### Does NOT coverbelow records it. | Met | No write path stores or overrides autonomy class;packages/domain/src/tasks/autonomy-class.tsis pure and no schema writes referenceautonomyClass. Searches in this diff show no storage of class. |
| The ledger's acceptance signal moves off status (found at planning, 2026-09-13).packages/domain/src/engprod/ledger-service.ts:110–114decideReconciliation(status)returnsacceptedfor ANY status other than BLOCKED/CLOSED — "acceptance = unblocking" (its spec SC3). A bare BLOCKED → TODO migration would therefore record all 15 clusters asacceptedat the next reconciliation, which is the opposite of what the migration means. Required, in the same PR and BEFORE the migration runs: reconciliation decides on the proposal's disposition, not its containment. The acceptance act is a tag swap (planning decision, 2026-09-13; a craft-level naming call, said out loud):engprod-proposal→engprod-accepted(ENGPROD_ACCEPTED_TAGintypes.ts). The pending marker cannot simply be removed, because the cockpit Proposals page lists proposals BY the tag (listTasks({ tags: [ENGPROD_PROPOSAL_TAG], all: true }),src/cockpit/routes/engprod-proposals.ts) and must keep showing accepted and rejected ones with their final disposition; and the classifier cannot gate on the tag forever, because an accepted proposal must become an ordinary agent-authored (pull-only) task.decideReconciliation({ status, tags } \| undefined)→rejectedwhen CLOSED;acceptedwhen theengprod-acceptedtag is present OR status ∈ {PLANNING, READY, IN-PROGRESS, IN-REVIEW, DONE} (someone took it up outside the cockpit);no-changewhile TODO or BLOCKED withengprod-proposalpresent, or task not found.reconcileVerdictstakes agetTask(taskId) → { status, tags } \| undefinedcallback instead ofgetTaskStatus(contract change — callers enumerated in Scope). The cockpit curation gate moves with it:checkProposalGuard= taggedengprod-proposal, not CLOSED, not alreadyengprod-accepted(409 otherwise, naming the current disposition); Accept = swap the tag in the same transaction as the ledger verdict, status untouched; Reject = CLOSED + verdict, as today; the list route reads both tags (twolistTaskscalls, merged);deriveDisposition(task)insrc/cockpit/web/lib/engprod-proposals.tsmirrors the new table (CLOSED → rejected;engprod-acceptedor past TODO → accepted; else pending);docs/cockpit-ui.md§Proposals text follows. The filed-spec template inbuildProposalSpec("Unblocking this task = accepting the proposal. Closing it without unblocking = rejecting") and the## Dispositionsection of the 15 live specs are rewritten to the new rule ("Accepting = the cockpit Proposals page's Accept, which swaps theengprod-proposaltag forengprod-accepted; planning it counts too; closing = rejecting"), so a reader of a proposal is not told to unblock it.ledger-service.test.tsand the route/web tests pin the new table. | Met |packages/domain/src/engprod/ledger-service.tsnow exportsdecideReconciliation(task)that keys on tags and planned-past-TODO statuses (lines showingif (task.tags.includes(ENGPROD_ACCEPTED_TAG)) return "accepted";and planned statuses set). The cockpit routeengprod-proposals.tsimplements the tag-swap accept path and reads both tags in the list route.docs/cockpit-ui.mdProposals section reflects the tag-swap and class-based containment (lines ~585–655 of the doc). |
Documentation impact
- updated-in-pr — This PR introduces the computed autonomy class and changes the cockpit proposals acceptance semantics from status-based to tag-swap. The docs were updated in this PR:
docs/cockpit-ui.md's Proposals section explicitly states containment moved off BLOCKED, accept swapsengprod-proposal→engprod-accepted, and responses now include tags. The PR description also mentions new docs (docs/autonomy-class.md,docs/standing-pointings.md,docs/architecture/cockpit.mdupdates).
Affected: docs/cockpit-ui.md, docs/autonomy-class.md, docs/standing-pointings.md, docs/architecture/cockpit.md
There was a problem hiding this comment.
Independent adversarial review (Chinese-wall)
Reviewer: minsky-reviewer[bot] via openai:gpt-5
Tier: 2
Verified the R1 blocking issue (accepting a legacy BLOCKED proposal left it BLOCKED) is addressed via acceptedTaskRow and the updated guard; tests assert the lift to TODO and tag swap. TaskRoutingService now computes autonomy class, filters unservable candidates, annotates outputs, and returns excludedByClass for adapters to surface; the no-SQL fallback classifies per-task and withholds unknown. tasks_route annotates classes and suppresses “ready immediately” for gated targets. The live verification script is read-only and sensible. I found no new blocking defects introduced by these changes; only minor non-blocking nits on output formatting and a tiny perf micro-optimization in the probe script. Event is APPROVE.
Findings
- [NON-BLOCKING] packages/domain/src/deployment/deploy-surface.ts:38 — Export of CONFIG_SURFACE_PATTERNS widens API surface — verify no unintended external consumers rely on previous non-exported status
CONFIG_SURFACE_PATTERNSis now exported (lines ~35–43) to serve the autonomy classifier. This is sensible and aligns with the spec, but it also makes the symbol part of the public API of the deployment module. I did not find issues within this diff —packages/domain/src/tasks/autonomy-class.tsis the intended new consumer — but please confirm no external tooling expects this constant to be private or duplicates it. No action required here; just a callout. - [NON-BLOCKING] src/adapters/shared/commands/tasks/routing-commands.ts:205 — Text path clarifies fallback behavior; consider slight reflow for readability
The fallback warning line now explains both the lack of dependency data and that autonomy class is computed from per-task spec reads (src/adapters/shared/commands/tasks/routing-commands.ts:205–210). This is helpful but quite long; optional: consider splitting into two lines (one for dependency data, one for class fallback) to improve terminal readability. No functional issue. - [NON-BLOCKING] scripts/verify-autonomy-class.ts:1 — Live probe script is read-only and robust; minor nits on performance and typing
The script recomputes class and reasons across the population with an O(N^2)findwhen mappingservedProposalsback topopulation(scripts/verify-autonomy-class.ts:95–108). Given typical N, this is fine; if desired, pre-indexpopulationby id for O(1) lookups. Also, the localSqlCapablePersistenceandisSqlCapableguard are good; you could narrow thetaskService/taskGraphServicetypes for better editor help, but it's non-essential.
Spec verification
| Criterion | Status | Evidence |
|---|---|---|
packages/domain/src/tasks/autonomy-class.ts exports computeAutonomyClass(input): { class, reasons[] } as a pure function over the task row (kind, tags, status, title), the spec's ## Scope and ## Summary text (or undefined when not loaded), a humanOrigin: boolean | undefined signal, and isListedSurface(path). The listed surface is this task's own list, AUTONOMY_LISTED_SURFACE_PATTERNS, NOT isDeploySurfaceFile (planning correction, 2026-09-13: DEPLOY_SURFACE_PATTERNS includes ^src/, ^packages/domain/src/, ^packages/shared/src/ since mt#4013 widened it to the whole bundled tree for deploy VERIFICATION — as an autonomy surface it would gate every code task). The list = the deploy BOUNDARIES (CONFIG_SURFACE_PATTERNS in deploy-surface.ts, exported by this task: infra/, Dockerfile, services/*/{Dockerfile,railway.json,deploy.config.ts,railway.config.ts}, .github/workflows/deploy*.yml) plus LOCAL_APP_DEPLOY_SURFACE_PATTERNS (cockpit-tray/src-tauri/) plus the security surfaces: .github/workflows/** (every workflow — a CI security surface, gate (l) sub-case 1), .minsky/hooks/**, .claude/hooks/**, packages/domain/src/persistence/**, packages/domain/src/storage/migrations/**, drizzle.pg.config.ts, packages/domain/src/auth/**. A test pins the list. Scope paths are extracted from ## Scope by a path-shaped-token regex (repo-relative tokens containing / or ending in a known extension), so a scope written as prose with backticked paths is read the same as a bullet list. Rules, in dominance order: |
principal-gatedif ANY: kindumbrella; kindstate-ops(a shared-state operation —principal-context.mdc §Authorization for shared / production state changes); a tag in {rfc,adr,naming,product-surface,vendor,engprod-proposal}; the title starts withRFCorADR(measured 2026-09-13: 16 open tasks by title prefix, only 8 of them tagged);## Scopenames a listed surface; the title or## Summarymatches a principal-reserved marker from the pinned set {rename,naming,product name,customer-facing,product surface,vendor,paid plan,signup,pricing} (measured on the 868-task default population: this set gates 56; a broad set withname/architecture/product/productiongates 211 on incidental prose and is rejected).- else
containedif ALL:humanOrigin === true(criterion 2); status READY (gate-passed); every extracted scope path outside listed surfaces; and one RFC shape — bugfix with a reproducing test named in the spec, incident follow-up (incidenttag or anOrigin:line matching/incident/i), cleared-blocker re-surface (acleared-blockertag — mt#2825's marker when it ships), docs-only or generated-only scope (every extracted path underdocs/orsrc/generated/). - else
pull-only. Kindwork-packageispull-onlywith the reasonADR-046: claimed deliberately—findAvailableTasks' existing kind exclusion is untouched. unknownonly when inputs are unavailable (spec signals not loaded and the row signals alone do not resolveprincipal-gated; no task row) — never as a default for a well-formed task. | Met |packages/domain/src/tasks/autonomy-class.tsimplementscomputeAutonomyClass(pure; lines ~183–287) and exportsAUTONOMY_LISTED_SURFACE_PATTERNS,extractScopePaths, and marker/tag sets.deploy-surface.tsnow exportsCONFIG_SURFACE_PATTERNS(lines ~35–43) consumed by the list inautonomy-class.ts(lines ~67–92). Tests pin list and markers inpackages/domain/src/tasks/autonomy-class.test.ts(e.g., 'pinned lists' suite). |
| Human-origin evidence. There is NO author column ontasks(tasksTable,packages/domain/src/storage/schemas/task-embeddings.ts), and provenance rows (taskOrigin: human\|agent\|automated,packages/domain/src/provenance/types.ts) are written at PR creation bysession-pr-operations.ts, so a backlog task has none. This task does NOT add a column. Absence of evidence ⇒ agent-authored ⇒ cannot becontained— default-deny applied to the signal itself.containedis therefore unreachable in production until a human-origin marker exists; a follow-up may addoriginattasks_create, set by channel (cockpit form = human, MCP = agent, miner = automated). File that follow-up from this task; do not build it here. | Met |computeAutonomyClassrequireshumanOrigin === trueand otherwise adds 'no human-origin evidence' to reasons before returningpull-only(autonomy-class.ts lines ~236–257). No schema changes appear in this diff adding author/origin columns; production call sites passhumanOrigin: undefined(e.g.,pointings-store.tslines ~362–371 andcomposition/domain.tswiring). |
|findAvailableTaskscomputes the class per candidate, excludesprincipal-gatedandunknown, and each served item carriesautonomyClass. Because the method's return type isAvailableTask[]with two production callers, the count lands on a siblingfindAvailableTasksWithClass(options) → { tasks, excludedByClass: { principalGated, unknown } }thatfindAvailableTasksdelegates to (so the old signature also denies); thetasks_availableadapter calls the sibling and reportsexcludedByClassin both JSON and text output. Data path (planning, 2026-09-13):listTasksreturns no spec text, and the classifier needs## Scope+## Summary; a per-candidategetTaskSpecContentwould be ~870 queries per call.packages/domain/src/tasks/autonomy-class-store.tsexportsloadAutonomySpecSignals(db, taskIds)— ONE query overtasks ⋈ task_specsextracting the two sections with a Postgres AREsubstring(content from '(?:^\|\n)## Scope[^\n]*\n((?:[^\n]\|\n(?!## ))*)')(measured on prod: 418 KB Scope + 779 KB Summary across the 868-task default population, vs 6.3 MB of full content; Summary capped at 2,000 chars in SQL). It is injected intoTaskRoutingServiceas an optional third constructor dependency (AutonomySignalSource), registered incomposition/domain.tsthe wayTaskGraphServicegets its db. The adapter's no-SQL fallback path has no bulk source: it classifies on row signals alone, so anything notprincipal-gatedby row isunknownand excluded, counted, and named in the fallback's warning line — honest default-deny rather than an N+1.
tasks_route(correction):generateRoutenever callsfindAvailableTasks— it walks the dependency graph toward a caller-named target and returns steps regardless of status, so 'inherits via the same service' was false and 'serves' does not apply to it. What it must not do is present a principal-gated task as work to start: the route's target and eachRouteStepcarryautonomyClass(additive fields),readyTaskscounts only steps whose class ispull-only/contained, and the 'ready to start immediately' line is replaced by 'target is principal-gated () — a person decides' when the target's class isprincipal-gated/unknown. A route to an umbrella stays a legal navigation query; nothing is refused.
Pointing candidate sets (mt#5129, additive):eachCandidateSet.items[]entry carriesautonomyClass— the RFC's 'membership in a pointing never overrides this' made visible on the surface an agent reads in Phase 1.loadCandidateRowsinpointings-store.tsadds the two section columns to its existingtasks ⋈ task_specsselect;selectCandidatestakes an optionalclassOf(id). Annotation only — a candidate set is the principal's own declaration, and the deny lands on Phase 2's pull.
A refusal names the class (AT5):tasks_availablereportsexcludedByClassand, when a specific task is requested by id viatasks_route, the class and reason are on the target. | Met |composition/domain.tswires the third dependency usingcreateDbAutonomySignalSource(lines ~220–236).pointings-store.tsnow bulk-loads spec signals and computes classes per candidate (lines ~352–374). The classifier storeautonomy-class-store.tsimplements the one-query section extraction and export (entire file). Tests inengprod/containment.test.tsassertexcludedByClasscounting and route annotations against the realTaskRoutingService. |
| Retirement, ordered inside ONE PR so there is no routable window: (a) the consumer default-deny lands; (b)packages/domain/src/engprod/proposal-filing-service.tsfiles proposals asTODO— theengprod-proposaltag is what now contains them; its header comment and the filed spec text ("This task was filed automatically and is BLOCKED…") are updated; (c) the live proposals migrate BLOCKED → TODO (legal:workflows.ts:200) viatasks_bulk-editdry-run then execute. This task is the >10-record wrapper (operational-safety-dry-run-first.mdc); scope-match is exactly theengprod-proposal-tagged BLOCKED set, count re-derived at run time (15 on 2026-09-13 ~07:55Z, SQL overtasks). | Met | (a) Deny is landed viafindAvailableTasksWithClassexclusion and counting. (b) Filing now creates TODO withengprod-proposaltag and the spec template text reflects tag-based containment (proposal-filing-service.ts lines ~11–20, 41–72, 131–139). (c) The data migration is a post-deploy operation and is not in this code diff; tests/documentation record the intended ordering; given repository scope this is acceptable. |
| Override of a computed class is principal-only via an ask (RFC sub-decision, accepted 2026-09-13): no override write path exists in this task.### Does NOT coverbelow records it. | Met | No storage of autonomy class exists in schemas or services; classifier is pure and consumers annotate only. There is no override API surface added in this diff. |
| The ledger's acceptance signal moves off status (found at planning, 2026-09-13).packages/domain/src/engprod/ledger-service.ts:110–114decideReconciliation(status)returnsacceptedfor ANY status other than BLOCKED/CLOSED — "acceptance = unblocking" (its spec SC3). A bare BLOCKED → TODO migration would therefore record all 15 clusters asacceptedat the next reconciliation, which is the opposite of what the migration means. Required, in the same PR and BEFORE the migration runs: reconciliation decides on the proposal's disposition, not its containment. The acceptance act is a tag swap (planning decision, 2026-09-13; a craft-level naming call, said out loud):engprod-proposal→engprod-accepted(ENGPROD_ACCEPTED_TAGintypes.ts). The pending marker cannot simply be removed, because the cockpit Proposals page lists proposals BY the tag (listTasks({ tags: [ENGPROD_PROPOSAL_TAG], all: true }),src/cockpit/routes/engprod-proposals.ts) and must keep showing accepted and rejected ones with their final disposition; and the classifier cannot gate on the tag forever, because an accepted proposal must become an ordinary agent-authored (pull-only) task.decideReconciliation({ status, tags } \| undefined)→rejectedwhen CLOSED;acceptedwhen theengprod-acceptedtag is present OR status ∈ {PLANNING, READY, IN-PROGRESS, IN-REVIEW, DONE} (someone took it up outside the cockpit);no-changewhile TODO or BLOCKED withengprod-proposalpresent, or task not found.reconcileVerdictstakes agetTask(taskId) → { status, tags } \| undefinedcallback instead ofgetTaskStatus(contract change — callers enumerated in Scope). The cockpit curation gate moves with it:checkProposalGuard= taggedengprod-proposal, not CLOSED, not alreadyengprod-accepted(409 otherwise, naming the current disposition); Accept = swap the tag in the same transaction as the ledger verdict, status untouched; Reject = CLOSED + verdict, as today; the list route reads both tags (twolistTaskscalls, merged);deriveDisposition(task)insrc/cockpit/web/lib/engprod-proposals.tsmirrors the new table (CLOSED → rejected;engprod-acceptedor past TODO → accepted; else pending);docs/cockpit-ui.md§Proposals text follows. The filed-spec template inbuildProposalSpec("Unblocking this task = accepting the proposal. Closing it without unblocking = rejecting") and the## Dispositionsection of the 15 live specs are rewritten to the new rule ("Accepting = the cockpit Proposals page's Accept, which swaps theengprod-proposaltag forengprod-accepted; planning it counts too; closing = rejecting"), so a reader of a proposal is not told to unblock it.ledger-service.test.tsand the route/web tests pin the new table. | Met |packages/domain/src/engprod/ledger-service.tsnow definesProposalTaskDisposition, includesENGPROD_ACCEPTED_TAGlogic, and updatesreconcileVerdictsto accept agetTaskreturning{status,tags}. Tests inengprod/ledger-service.test.tscover pending/accepted/rejected cases including the BLOCKED→TODO migration no-op. Cockpit docs updated accordingly indocs/cockpit-ui.mdand acceptance behavior covered incontainment.test.ts. |
|packages/domain/src/tasks/autonomy-class.tsexportscomputeAutonomyClass(input): { class, reasons[] }as a pure function… (dominance rules, listed surface, scope extraction, unknown only when signals unavailable). | Met | Classifier behavior is consumed and surfaced here:packages/domain/src/tasks/task-routing-service.ts:146–205callscomputeAutonomyClassviaclassify(), annotatesautonomyClass, and enforcesisServableClass. Fallback adapter path usescomputeAutonomyClassandspecSignalsFromContentdirectly (src/adapters/shared/commands/tasks/routing-commands.ts:139–187). The listed-surface mechanics and rules remain as implemented in the classifier module (covered in chunk 1 tests). |
| Human-origin evidence: do not add an author/origin column; absence of signal meanscontainedunreachable in prod; production call sites passhumanOrigin: undefined. | Met |TaskRoutingService.classify()passeshumanOrigin: undefined(packages/domain/src/tasks/task-routing-service.ts:66–82). No schema changes in this chunk add origin. Fallback adapter also passeshumanOrigin: undefined(src/adapters/shared/commands/tasks/routing-commands.ts:170–187). |
|findAvailableTaskscomputes class per candidate, excludesprincipal-gatedandunknown, returns items withautonomyClass; siblingfindAvailableTasksWithClassreturns{tasks, excludedByClass}; adapters report counts; no-SQL fallback classifies per task and withholds unknown. | Met |packages/domain/src/tasks/task-routing-service.ts:98–114delegates tofindAvailableTasksWithClass;:179–205computes classes, filters withisServableClass, countsexcludedByClass, annotatesautonomyClass.src/adapters/shared/commands/tasks/routing-commands.ts:126–146uses the sibling and surfacesexcludedByClass(JSON and text at:244–257). Fallback path classifies per-candidate, counts withheld, stops atlimitservable (:156–187). Tests assert both paths (src/adapters/shared/commands/tasks/routing-commands.test.ts:58–164). |
|tasks_routeannotates target and steps with class;readyTaskscounts only servable steps; when gated, replace 'ready to start' line with class+reason. | Met |packages/domain/src/tasks/task-routing-service.ts:307–390annotatesRouteStep.autonomyClass, computestargetAutonomyClass/Reasons, andreadyTasksfilters byisServableClass. Adapter prints target class and suppresses 'ready immediately' when gated (src/adapters/shared/commands/tasks/routing-commands.ts:360–381). |
| Retire BLOCKED containment for proposals: file as TODO+tag; default-deny lands first; cockpit gate treats pending by tag; accept swapsengprod-proposal→engprod-accepted, status untouched; list reads both tags; 409s when already accepted/rejected. | Met | Cockpit route lists by both tags and de-duplicates (src/cockpit/routes/engprod-proposals.ts:149–164), guard treats pending by tag and CLOSED/accepted as conflicts (:274–314), Accept swaps tags (lifting legacy BLOCKED to TODO) in the same transaction as ledger write (:372–409), and response includes updated tags (:446–454). Tests cover guard, accept swap, and legacy BLOCKED lift (src/cockpit/routes/engprod-proposals.test.ts:49–107). |
| No override write path for autonomy class — principal-only via an ask in a future phase. | Met | No storage writes reference autonomy class in this chunk;autonomyClassis additive on DTOs (packages/domain/src/tasks/task-routing-service.ts:24–31, 41–48, 55–64). Adapters only annotate/print; no mutation routes added. |
| Ledger acceptance moves off status to tag swap or planned-past-TODO; cockpit derives disposition from status+tags mirroring domaindecideReconciliation. | Met | Web lib mirrors tag+status table (src/cockpit/web/lib/engprod-proposals.ts:126–148), with domain-constant pin in tests (src/cockpit/web/lib/engprod-proposals.test.ts:4–17). Page uses newderiveDisposition(task)shape (src/cockpit/web/pages/ProposalsPage.tsx:291, 450–451). |
Adoption sweep
| Symbol | Kind | Consumers found | Classification | Notes |
|---|---|---|---|---|
| packages/domain/src/deployment/deploy-surface.ts:CONFIG_SURFACE_PATTERNS | type | packages/domain/src/tasks/autonomy-class.ts — builds AUTONOMY_LISTED_SURFACE_PATTERNS from it, docs/autonomy-class.md — references deploy boundaries list textually | Adopted | CONFIG_SURFACE_PATTERNS was made an export in this PR to support the autonomy classifier’s listed-surface construction. |
| TaskRoutingService.findAvailableTasksWithClass | function | src/adapters/shared/commands/tasks/routing-commands.ts:126–146 — used to serve available tasks and surface excludedByClass, packages/domain/src/tasks/task-routing-service.ts:98–114 — legacy findAvailableTasks delegates to sibling, tests/adapters/shared/commands/tasks/routing-commands.test.ts:58–164 — adapter tests stub this sibling, packages/domain/src/tasks/task-routing-service.test.ts:90–108 — tests call sibling for kind-filter case |
Adopted | New sibling preserves old API while exposing exclusion counts; adapter and tests are updated accordingly. |
Documentation impact
- updated-in-pr — This chunk updates user-visible CLI output (adds autonomy class counts and per-item class) and cockpit proposals semantics (pending by tag, accept = tag swap). The PR description states and prior chunk includes updated docs:
docs/cockpit-ui.md(Proposals section),docs/autonomy-class.md,docs/standing-pointings.md, anddocs/architecture/cockpit.md. The adapter help text for candidates was updated (src/adapters/shared/commands/tasks/pointing-commands.ts:232–236) and the generated completion manifest reflects it (src/generated/completion-manifest.json).
Affected: docs/cockpit-ui.md, docs/autonomy-class.md, docs/standing-pointings.md, docs/architecture/cockpit.md
Summary
Phase 1c of the backlog-prioritization RFC (Notion
3ae937f0, Accepted 2026-09-13) — mt#5130 under mt#3483.Every open task now resolves to
principal-gated/pull-only/contained/unknown, computed from signals at read time and never stored, so nothing that files a task can make its own task dispatchable. The consumers default to deny:principal-gatedandunknownare never served. The EngProd miner's BLOCKED containment hack is retired onto the class.Three things the planning pass changed from the inherited spec, recorded there under
## Planning Audit (READY):tasks_routebuilds a dependency path to a caller-named target and never sharedtasks_available's filter, so it gets class annotations on the target and each step, and never calls a gated target "ready" — nothing is refused.isDeploySurfaceFilewas the wrong instrument for the listed surface:DEPLOY_SURFACE_PATTERNScarries^src/and^packages/domain/src/for deploy verification (mt#4013) and would gate every code task. The autonomy surface is its own pinned list — deploy boundaries (CONFIG_SURFACE_PATTERNS, now exported) +cockpit-tray/src-tauri/+ the security surfaces (all workflows, both hook trees, persistence, migrations,drizzle.pg.config.ts, auth).engprod-proposal→engprod-accepted(a craft-level naming call, said out loud here) rather than "remove the tag".Key changes
packages/domain/src/tasks/autonomy-class.ts— pure classifier. Dominance order:principal-gatedon any of kindumbrella/state-ops, a gating tag (rfc,adr,naming,product-surface,vendor,engprod-proposal), anRFC/ADRtitle, a scope path on the listed surface, or a whole-word principal-reserved marker (measured: this set gates 56 of 868; the broadname/architecture/product/productionset gates 211 on incidental prose and was rejected); elsecontainedonly withhumanOrigin === true+ READY + no listed surface + one RFC shape; elsepull-only;unknownonly when spec signals were not loaded.work-packageispull-onlywithADR-046: claimed deliberately. Surface list, tags and markers are test-pinned.autonomy-class-store.ts—loadAutonomySpecSignals(db, ids): ONE query overtasks ⋈ task_specsextracting## Scopeand## Summarysection-exact with a Postgres AREsubstring(... from pattern)(measured on prod: 871 rows / 1.13 M chars in 388 ms, vs 6.3 MB of full content). Injected intoTaskRoutingServiceas an optional third dependency; registered incomposition/domain.tsthe wayTaskGraphServicegets its db.task-routing-service.ts—findAvailableTasksWithClassclassifies after the status/kind filter and before the dependency lookup, serves only servable classes, returnsexcludedByClass: { principalGated, unknown };findAvailableTasksdelegates so the old shape denies too.generateRouteannotates target + steps;readyTaskscounts only servable steps. Perf, in the same function: the per-dependencygetTask(serial per task) is now onegetTasksread — measured on prod 159 round-trips / 18.5 s → 0.9 s for the same 644-task result.routing-commands.ts—tasks_availablereportsexcludedByClass(JSON) and a "Withheld by autonomy class" line (text), items carryautonomyClass; the no-SQL fallback reads each candidate's spec viagetTaskSpecContent, stopping atlimitservable.tasks_routeprints the target's class and replaces "ready to start immediately" with the class and reasons when gated.CandidateSet.items[].autonomyClassvia a second bulk load over the matched ids; annotation only.proposal-filing-service.tsfilesTODO+ tag;types.tsaddsENGPROD_ACCEPTED_TAG;decideReconciliation({ status, tags })= CLOSED → rejected,engprod-acceptedor status past TODO → accepted, else no-change (so the 15-row BLOCKED → TODO migration records nothing);reconcileVerdictstakesgetTaskreturning{ status, tags }(one caller,toil-miner-tick.ts).routes/engprod-proposals.tslists both tags (twolistTaskscalls; the tag filter ANDs),checkProposalGuard= pending (tagged, not accepted, not CLOSED) with a 409 naming the disposition otherwise, Accept swaps the tag in the same transaction as the ledger verdict (status untouched), rows carrytags;web/lib/engprod-proposals.tsderiveDisposition({ status, tags })mirrors the ledger table (its tag literal is pinned equal to the domain constant by test — the domain module is Node-only for the browser bundle).docs/autonomy-class.md(new, indexed),docs/standing-pointings.md,docs/cockpit-ui.md§Proposals + endpoints,docs/architecture/cockpit.md.scripts/verify-autonomy-class.ts— read-only live verification (below).Testing
Execution evidence:
Acceptance tests, by the spec's numbering:
services/reviewer/Dockerfilewith a reproducing test, READY and human origin resolvesprincipal-gatednaming the surface) —autonomy-class.test.ts"AT2: a scope naming a listed deploy surface…".pull-only) —autonomy-class.test.ts"AT3: every contained signal but no human-origin evidence…".tasks_availableserves zeroengprod-proposaltasks;tasks_routemarks oneprincipal-gated) —containment.test.ts, the 15-shape fixture on both sides plus the route half; the prod probe is under Live verification below (the "before" side — the "after" side lands in the task's## Outcomeonce the migration runs post-deploy).unknown, refused with a reason naming the class) —autonomy-class.test.ts"AT5: no spec signals loaded → unknown…"; the adapter's fallback test inrouting-commands.test.tsshows it withheld and counted.rfc-tagged →principal-gated; plain TODO →pull-only) —autonomy-class.test.ts"umbrella kind, state-ops kind, an rfc tag…" and "a plain TODO implementation task…".Success criteria: SC1 (classifier + pinned lists)
autonomy-class.test.ts; SC2 (no origin column added;humanOriginundefined in every production call site) —grep -rn "humanOrigin: undefined"→task-routing-service.ts,routing-commands.ts,pointings-store.ts; SC3 (excludedByClass, item class, route annotation, candidate annotation)routing-commands.test.ts+containment.test.tsroute test; SC4 (filing TODO + the migration order)proposal-filing-service.test.ts— the migration itself is a post-deploy closeout step,[sc4-deferred: mt#5130]for the state half; SC5 (no override path) — no write path exists,grep -rn "autonomyClass" packages/domain/src/storage/schemas/→ only the pointings table's own column, unchanged; SC6 (ledger rule)ledger-service.test.ts"a BLOCKED → TODO migration records nothing".Negative control: with
isServableClasspatched toreturn true(the deny disabled), 5 tests fail — the fourcontainment.test.tsAFTER/old-shape/planned/route cases andautonomy-class.test.tsAT5 — and pass again on restore:Live verification
bun scripts/verify-autonomy-class.tsfrom the session workspace against production (read-only — it lists, loads sections, classifies; no writes), 2026-09-13T10:31:28Z:After the bulk dependency read, same population:
served 644 in 895 ms; getTask calls: 0(was 159 calls / 18463 ms). So on today's backlog 24% of the default population is withheld as principal-gated and 0 is unknown;containedis unreachable until mt#5136 adds an origin signal, as the spec says.Reads the changed contract end to end:
TaskRoutingServicewired exactly ascomposition/domain.tswires it, the realloadAutonomySpecSignalsSQL, the reallistTasks/getTasks.Deploy verification
Deploy verification: this PR touches
packages/domain/src/**andsrc/**(deploy surface forminsky-mcpandreviewer). After merge:deployment_wait-for-latestwithnotBefore= merge time andexpectCommitSha= merge sha for both services; onindeterminateidentity, correlateforge_ci_run_listhead_sha and assert something the change produced —tasks_availableon prod returningexcludedByClassin its JSON, andtasks_route mt#3421reportingClass: principal-gated. Then, and only then, the closeout migration (15 proposals BLOCKED → TODO,## Dispositionrewrites) and the AT3 "after" probe, recorded in the task's## Outcome.Follow-ups filed at planning
containedbecomes reachable).tasks_supervisechild admission consults the class (a second dispatch path this PR does not wire).Written by Claude (Opus 5) in a Minsky session; the planning audit, measurements and decisions are in the task spec.
🤖 Generated with Claude Code
https://claude.ai/code/session_0147JJ3SVgqRz9JyUD4NZBz9
Review round 1 (
18c0c4d83)acceptedTaskRow(pure, exported, tested) swaps the tag and lifts a legacyBLOCKEDrow toTODO, leaving any other status alone. Test:engprod-proposals.test.ts"acceptedTaskRow: … a legacy BLOCKED row, lifted to TODO".selectCandidatesguards on!== undefined; the section regex is one builder (specSectionPattern,SPEC_ORIGIN_PATTERN) sent to SQL and compiled in TS — re-ranscripts/verify-autonomy-class.tsagainst prod after the change:Population: 850; served: 643; 1116 ms; principalGated 207; unknown 0; AT4: PASS;loadDependencyStatuseschecks and callsgetTasksthrough one narrowed view.grep -rn "new TaskRoutingService("acrosspackages/ src/ services/ tests/→ the five sites in this PR (composition/domain.ts, two test files underpackages/domain,containment.test.ts,tests/domain/tasks/task-routing-service.test.ts) and no other;composition/test.tsstubs the new method.decideReconciliationcallers:toil-miner-tick.ts(updated) is the only production caller;deriveDispositionis the cockpit mirror and is updated in this PR with a test pinning its tag literal to the domain constant.docs/cockpit-ui.md(§Proposals + both endpoints),docs/autonomy-class.md(new),docs/standing-pointings.md,docs/architecture/cockpit.mdare in this PR — the reviewed chunk did not include them. The EngProd RFC's Notion amendment is a closeout step recorded in the spec.tasks_availablefallback's catch condition (no such table: task_relationships) predates this PR and is unrelated to the class filter; widening it is a separate question.CONFIG_SURFACE_PATTERNSstays a direct export with its contract in the comment.