Skip to content

feat(mt#5130): Computed autonomy class, consumer default-deny, and the BLOCKED hack retired - #3746

Merged
edobry merged 3 commits into
mainfrom
task/mt-5130
Sep 13, 2026
Merged

edobry merged 3 commits into
mainfrom
task/mt-5130

Conversation

@minsky-ai

@minsky-ai minsky-ai Bot commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

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-gated and unknown are 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_route builds a dependency path to a caller-named target and never shared tasks_available's filter, so it gets class annotations on the target and each step, and never calls a gated target "ready" — nothing is refused.
  • isDeploySurfaceFile was the wrong instrument for the listed surface: DEPLOY_SURFACE_PATTERNS carries ^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).
  • The cockpit Proposals curation gate reads BLOCKED as "pending" and lists proposals by tag, so acceptance became a tag swap 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-gated on any of kind umbrella/state-ops, a gating tag (rfc, adr, naming, product-surface, vendor, engprod-proposal), an RFC/ADR title, a scope path on the listed surface, or a whole-word principal-reserved marker (measured: this set gates 56 of 868; the broad name/architecture/product/production set gates 211 on incidental prose and was rejected); else contained only with humanOrigin === true + READY + no listed surface + one RFC shape; else pull-only; unknown only when spec signals were not loaded. work-package is pull-only with ADR-046: claimed deliberately. Surface list, tags and markers are test-pinned.
  • autonomy-class-store.ts — loadAutonomySpecSignals(db, ids): ONE query over tasks ⋈ task_specs extracting ## Scope and ## Summary section-exact with a Postgres ARE substring(... from pattern) (measured on prod: 871 rows / 1.13 M chars in 388 ms, vs 6.3 MB of full content). Injected into TaskRoutingService as an optional third dependency; registered in composition/domain.ts the way TaskGraphService gets its db.
  • task-routing-service.ts — findAvailableTasksWithClass classifies after the status/kind filter and before the dependency lookup, serves only servable classes, returns excludedByClass: { principalGated, unknown }; findAvailableTasks delegates so the old shape denies too. generateRoute annotates target + steps; readyTasks counts only servable steps. Perf, in the same function: the per-dependency getTask (serial per task) is now one getTasks read — measured on prod 159 round-trips / 18.5 s → 0.9 s for the same 644-task result.
  • routing-commands.ts — tasks_available reports excludedByClass (JSON) and a "Withheld by autonomy class" line (text), items carry autonomyClass; the no-SQL fallback reads each candidate's spec via getTaskSpecContent, stopping at limit servable. tasks_route prints the target's class and replaces "ready to start immediately" with the class and reasons when gated.
  • Pointings (mt#5129, additive) — CandidateSet.items[].autonomyClass via a second bulk load over the matched ids; annotation only.
  • EngProd — proposal-filing-service.ts files TODO + tag; types.ts adds ENGPROD_ACCEPTED_TAG; decideReconciliation({ status, tags }) = CLOSED → rejected, engprod-accepted or status past TODO → accepted, else no-change (so the 15-row BLOCKED → TODO migration records nothing); reconcileVerdicts takes getTask returning { status, tags } (one caller, toil-miner-tick.ts).
  • Cockpit — routes/engprod-proposals.ts lists both tags (two listTasks calls; 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 carry tags; web/lib/engprod-proposals.ts deriveDisposition({ 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 — 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:

$ bun test --preload ./tests/setup.ts --timeout=15000 packages/domain/src/tasks/autonomy-class.test.ts packages/domain/src/tasks/task-routing-service.test.ts packages/domain/src/tasks/pointings.test.ts packages/domain/src/tasks/pointings-store.test.ts packages/domain/src/engprod/containment.test.ts packages/domain/src/engprod/ledger-service.test.ts packages/domain/src/engprod/proposal-filing-service.test.ts packages/domain/src/engprod/toil-miner-tick.test.ts src/adapters/shared/commands/tasks/routing-commands.test.ts src/cockpit/routes/engprod-proposals.test.ts tests/domain/tasks/task-routing-service.test.ts tests/adapters/shared/commands/tasks/routing-commands.test.ts
139 pass
 0 fail
 416 expect() calls
Ran 139 tests across 12 files. [140.00ms]

$ bun run test:components   (happy-dom: ProposalsPage.test.tsx + web/lib/engprod-proposals.test.ts among them)
2870 pass
 0 fail
Ran 2870 tests across 214 files. [17.40s]

$ bun scripts/run-related-tests.ts <every changed file>   (the pre-commit gate)
1732 pass
 0 fail
Ran 1732 tests across 132 files. [38.90s]

Acceptance tests, by the spec's numbering:

  • AT1 (a scope naming services/reviewer/Dockerfile with a reproducing test, READY and human origin resolves principal-gated naming the surface) — autonomy-class.test.ts "AT2: a scope naming a listed deploy surface…".
  • AT2 (every contained signal but no human origin → pull-only) — autonomy-class.test.ts "AT3: every contained signal but no human-origin evidence…".
  • AT3 (before and after the migration tasks_available serves zero engprod-proposal tasks; tasks_route marks one principal-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 ## Outcome once the migration runs post-deploy).
  • AT4 (missing spec row → unknown, refused with a reason naming the class) — autonomy-class.test.ts "AT5: no spec signals loaded → unknown…"; the adapter's fallback test in routing-commands.test.ts shows it withheld and counted.
  • AT5 (umbrella and 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; humanOrigin undefined 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.ts route 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 isServableClass patched to return true (the deny disabled), 5 tests fail — the four containment.test.ts AFTER/old-shape/planned/route cases and autonomy-class.test.ts AT5 — and pass again on restore:

(fail) computeAutonomyClass — unknown only when inputs are unavailable > AT5: no spec signals loaded → unknown, and unknown is never servable
(fail) AT4: engprod-proposal containment against the real TaskRoutingService > AFTER the migration (TODO + tag): zero proposals served, all 15 counted principal-gated
(fail) AT4: engprod-proposal containment against the real TaskRoutingService > the old `findAvailableTasks` shape denies the same way — no caller can be served a proposal
(fail) AT4: engprod-proposal containment against the real TaskRoutingService > a PLANNED proposal that still carries the pending tag stays withheld — planning is not curation
(fail) AT4: engprod-proposal containment against the real TaskRoutingService > tasks_route: a proposal target is marked principal-gated, never 'ready' (AT4's route half)
 20 pass
 5 fail

Live verification

bun scripts/verify-autonomy-class.ts from the session workspace against production (read-only — it lists, loads sections, classifies; no writes), 2026-09-13T10:31:28Z:

Population: 851; served: 644; 18978 ms      ← before the getTasks change
Excluded by class: {"principalGated":207,"unknown":0}
Class counts: {"pull-only":644,"principal-gated":207}
Principal-gated reasons: {"scope":146,"marker":55,"kind":52,"title":16,"tag":8}
AT4: PASS — 0 engprod-proposal task(s) in population, 0 served

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; contained is unreachable until mt#5136 adds an origin signal, as the spec says.

Reads the changed contract end to end: TaskRoutingService wired exactly as composition/domain.ts wires it, the real loadAutonomySpecSignals SQL, the real listTasks/getTasks.

Deploy verification

Deploy verification: this PR touches packages/domain/src/** and src/** (deploy surface for minsky-mcp and reviewer). After merge: deployment_wait-for-latest with notBefore = merge time and expectCommitSha = merge sha for both services; on indeterminate identity, correlate forge_ci_run_list head_sha and assert something the change produced — tasks_available on prod returning excludedByClass in its JSON, and tasks_route mt#3421 reporting Class: principal-gated. Then, and only then, the closeout migration (15 proposals BLOCKED → TODO, ## Disposition rewrites) and the AT3 "after" probe, recorded in the task's ## Outcome.

Follow-ups filed at planning

  • mt#5136 — origin channel on tasks (contained becomes reachable).
  • mt#5137 — tasks_supervise child 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)

  • BLOCKING — Accept on a not-yet-migrated BLOCKED proposal left it BLOCKED. Fixed: acceptedTaskRow (pure, exported, tested) swaps the tag and lifts a legacy BLOCKED row to TODO, leaving any other status alone. Test: engprod-proposals.test.ts "acceptedTaskRow: … a legacy BLOCKED row, lifted to TODO".
  • Proposals list de-duplicated by id; selectCandidates guards on !== undefined; the section regex is one builder (specSectionPattern, SPEC_ORIGIN_PATTERN) sent to SQL and compiled in TS — re-ran scripts/verify-autonomy-class.ts against prod after the change: Population: 850; served: 643; 1116 ms; principalGated 207; unknown 0; AT4: PASS; loadDependencyStatuses checks and calls getTasks through one narrowed view.
  • Constructor arity: grep -rn "new TaskRoutingService(" across packages/ src/ services/ tests/ → the five sites in this PR (composition/domain.ts, two test files under packages/domain, containment.test.ts, tests/domain/tasks/task-routing-service.test.ts) and no other; composition/test.ts stubs the new method.
  • decideReconciliation callers: toil-miner-tick.ts (updated) is the only production caller; deriveDisposition is the cockpit mirror and is updated in this PR with a test pinning its tag literal to the domain constant.
  • Documentation impact: docs/cockpit-ui.md (§Proposals + both endpoints), docs/autonomy-class.md (new), docs/standing-pointings.md, docs/architecture/cockpit.md are in this PR — the reviewed chunk did not include them. The EngProd RFC's Notion amendment is a closeout step recorded in the spec.
  • Not changed: the tasks_available fallback'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_PATTERNS stays a direct export with its contract in the comment.

edobry and others added 2 commits September 13, 2026 06:28
…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-ai minsky-ai Bot added the authorship/co-authored Co-authored by human and AI agent label Sep 13, 2026
@minsky-reviewer

minsky-reviewer Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Minsky Reviewer Status

Verdict: APPROVED — no blocking findings
Review: View review
Model: openai/gpt-5 | Tokens: 504K prompt, 10K completion | Duration: 127s
Mode: normal

Commands

  • /review — request a fresh review

@github-actions

github-actions Bot commented Sep 13, 2026 •

Copy link
Copy Markdown

Cockpit Preview

Preview URL: https://cockpit-preview-production.up.railway.app

Deployed from commit 18c0c4d83.
Preview runs in read-only mode — mutations are disabled.

Updated by Cockpit Preview.

@minsky-reviewer minsky-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
    In handleDecision(...) the accept path computes newStatus as the current row status (const newStatus = decision === "accept" ? currentStatus : "CLOSED";) and checkProposalGuard explicitly allows pending proposals with status === "BLOCKED" (see checkProposalGuard returning { kind: "ok" } for BLOCKED at lines 270–279). As a result, accepting a not-yet-migrated BLOCKED proposal will swap the tag but persist status BLOCKED (lines 351–386), leaving the task in a legacy containment state. This undermines the retirement goal and can keep the accepted task out of tasks_available by status filter even after its provenance gate is lifted by the tag swap. Suggested fix: on accept, if currentStatus === "BLOCKED" (legacy shape), set newStatus = "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 autonomyClass when classifier returns falsy; rely on explicit undefined check
    selectCandidates sets item.autonomyClass only when cls is 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 using if (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) and extractSpecSection() in autonomy-class.ts implement 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: TaskRoutingService now constructed with 3 args — verify all environments updated
    This container now instantiates TaskRoutingService with 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 updated packages/domain/src/composition/test.ts stubs, but please confirm all other constructors (e.g., any direct new 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_PATTERNS expands API surface — ensure no external consumers depend on non-exported behavior
    CONFIG_SURFACE_PATTERNS was made export const to 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 dedicated autonomy-surface.ts facade to avoid consumers taking a dependency on deploy-specific semantics.
  • [NON-BLOCKING] packages/domain/src/engprod/ledger-service.ts:115 — decideReconciliation contract changed shape — audit all callers beyond toil-miner-tick.ts
    The signature changed from (status: string|undefined) to (task: {status, tags}|undefined) and semantics moved from containment to disposition. You updated toil-miner-tick.ts and 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 fetches pendingTasks and acceptedTasks via two listTasks calls 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 by id when 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
    In createTasksAvailableCommand, 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-fallback flag 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
    loadDependencyStatuses checks for getTasks presence via (this.taskService as { getTasks?: unknown }).getTasks and then calls await this.taskService.getTasks(ids) inside the same branch (lines ~200–216). This works, but using the bulk variable (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.ts header), but repo docs are not included in this chunk. Verify that docs/cockpit-ui.md and 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_available output now reports excludedByClass and items include autonomyClass; 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

@minsky-reviewer minsky-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 new checkProposalGuard no longer requires status === "BLOCKED" and treats both BLOCKED and TODO as pending when tagged engprod-proposal (lines ~214–248). The new acceptedTaskRow lifts a legacy BLOCKED row to TODO on accept and swaps the tag (lines ~264–281). Tests in src/cockpit/routes/engprod-proposals.test.ts assert both behaviors, including the lift. This addresses the earlier BLOCKING issue where accepting a pre-mt#5130 proposal left it BLOCKED.
  • [NON-BLOCKING] packages/domain/src/tasks/task-routing-service.ts:146 — Class filter applied before dependency lookups; behavior aligns with spec and improves perf
    findAvailableTasksWithClass computes 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 uses specSectionPattern("Scope"/"Summary") imported from autonomy-class.ts (lines 21–26, 34–41), ensuring the Postgres substring(... from pattern) and the TS RegExp stay 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/proposals now reads both engprod-proposal and engprod-accepted tags 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 autonomyClass only when supplied
    selectCandidates accepts an optional classOf and sets item.autonomyClass only when cls !== undefined (lines ~205–227). This avoids writing an explicit undefined and 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-gated if ANY: kind umbrella; kind state-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 with RFC or ADR (measured 2026-09-13: 16 open tasks by title prefix, only 8 of them tagged); ## Scope names a listed surface; the title or ## Summary matches 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 with name/architecture/product/production gates 211 on incidental prose and is rejected).
  • else contained if 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 (incident tag or an Origin: line matching /incident/i), cleared-blocker re-surface (a cleared-blocker tag — mt#2825's marker when it ships), docs-only or generated-only scope (every extracted path under docs/ or src/generated/).
  • else pull-only. Kind work-package is pull-only with the reason ADR-046: claimed deliberately — findAvailableTasks' existing kind exclusion is untouched.
  • unknown only when inputs are unavailable (spec signals not loaded and the row signals alone do not resolve principal-gated; no task row) — never as a default for a well-formed task. | Met | computeAutonomyClass and helpers are implemented in packages/domain/src/tasks/autonomy-class.ts:151–287. The listed surface lives in AUTONOMY_LISTED_SURFACE_PATTERNS (lines 83–108) built from CONFIG_SURFACE_PATTERNS and LOCAL_APP_DEPLOY_SURFACE_PATTERNS plus security patterns; isListedSurface uses it (lines 109–121). Scope extraction via extractScopePaths (lines 123–150). Dominance rules implemented in computeAutonomyClass (lines 199–287), including principal-gated signals, contained conjuncts, work-package pull-only, and unknown when spec is undefined. |
    | Human-origin evidence. There is NO author column on tasks (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 by session-pr-operations.ts, so a backlog task has none. This task does NOT add a column. Absence of evidence ⇒ agent-authored ⇒ cannot be contained — default-deny applied to the signal itself. contained is therefore unreachable in production until a human-origin marker exists; a follow-up may add origin at tasks_create, set by channel (cockpit form = human, MCP = agent, miner = automated). File that follow-up from this task; do not build it here. | Met | computeAutonomyClass requires input.humanOrigin === true for contained and otherwise adds "no human-origin evidence" to reasons before returning pull-only (lines 247–257). The new code does not modify schemas; no author/origin column is introduced in the diff. Production call sites pass humanOrigin: undefined (see packages/domain/src/tasks/task-routing-service.ts:114–129). |
    | findAvailableTasks computes the class per candidate, excludes principal-gated and unknown, and each served item carries autonomyClass. Because the method's return type is AvailableTask[] with two production callers, the count lands on a sibling findAvailableTasksWithClass(options) → { tasks, excludedByClass: { principalGated, unknown } } that findAvailableTasks delegates to (so the old signature also denies); the tasks_available adapter calls the sibling and reports excludedByClass in both JSON and text output. Data path (planning, 2026-09-13): listTasks returns no spec text, and the classifier needs ## Scope + ## Summary; a per-candidate getTaskSpecContent would be ~870 queries per call. packages/domain/src/tasks/autonomy-class-store.ts exports loadAutonomySpecSignals(db, taskIds) — ONE query over tasks ⋈ task_specs extracting the two sections with a Postgres ARE substring(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 into TaskRoutingService as an optional third constructor dependency (AutonomySignalSource), registered in composition/domain.ts the way TaskGraphService gets its db. The adapter's no-SQL fallback path has no bulk source: it classifies on row signals alone, so anything not principal-gated by row is unknown and excluded, counted, and named in the fallback's warning line — honest default-deny rather than an N+1.
    tasks_route (correction): generateRoute never calls findAvailableTasks — 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 each RouteStep carry autonomyClass (additive fields), readyTasks counts only steps whose class is pull-only/contained, and the "ready to start immediately" line is replaced by "target is principal-gated () — a person decides" when the target's class is principal-gated/unknown. A route to an umbrella stays a legal navigation query; nothing is refused.
    Pointing candidate sets (mt#5129, additive): each CandidateSet.items[] entry carries autonomyClass — the RFC's "membership in a pointing never overrides this" made visible on the surface an agent reads in Phase 1. loadCandidateRows in pointings-store.ts adds the two section columns to its existing tasks ⋈ task_specs select; selectCandidates takes an optional classOf(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_available reports excludedByClass and, when a specific task is requested by id via tasks_route, the class and reason are on the target. | Met | TaskRoutingService.findAvailableTasks now delegates to findAvailableTasksWithClass (lines 97–111). The sibling computes classes, excludes unservable, returns excludedByClass, and annotates each item with autonomyClass (lines 146–205, 256–267, 292–300). The bulk spec-signal loader is implemented in packages/domain/src/tasks/autonomy-class-store.ts (entire file), exporting loadAutonomySpecSignals and createDbAutonomySignalSource. generateRoute annotates steps and target and counts ready tasks by class (lines 307–390). pointings.ts annotates CandidateItem.autonomyClass when classOf is 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.ts files proposals as TODO — the engprod-proposal tag 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) via tasks_bulk-edit dry-run then execute. This task is the >10-record wrapper (operational-safety-dry-run-first.mdc); scope-match is exactly the engprod-proposal-tagged BLOCKED set, count re-derived at run time (15 on 2026-09-13 ~07:55Z, SQL over tasks). | 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 cover below records it. | Met | No write path stores or overrides autonomy class; packages/domain/src/tasks/autonomy-class.ts is pure and no schema writes reference autonomyClass. 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–114 decideReconciliation(status) returns accepted for ANY status other than BLOCKED/CLOSED — "acceptance = unblocking" (its spec SC3). A bare BLOCKED → TODO migration would therefore record all 15 clusters as accepted at 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_TAG in types.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) → rejected when CLOSED; accepted when the engprod-accepted tag is present OR status ∈ {PLANNING, READY, IN-PROGRESS, IN-REVIEW, DONE} (someone took it up outside the cockpit); no-change while TODO or BLOCKED with engprod-proposal present, or task not found. reconcileVerdicts takes a getTask(taskId) → { status, tags } \| undefined callback instead of getTaskStatus (contract change — callers enumerated in Scope). The cockpit curation gate moves with it: checkProposalGuard = tagged engprod-proposal, not CLOSED, not already engprod-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 (two listTasks calls, merged); deriveDisposition(task) in src/cockpit/web/lib/engprod-proposals.ts mirrors the new table (CLOSED → rejected; engprod-accepted or past TODO → accepted; else pending); docs/cockpit-ui.md §Proposals text follows. The filed-spec template in buildProposalSpec ("Unblocking this task = accepting the proposal. Closing it without unblocking = rejecting") and the ## Disposition section of the 15 live specs are rewritten to the new rule ("Accepting = the cockpit Proposals page's Accept, which swaps the engprod-proposal tag for engprod-accepted; planning it counts too; closing = rejecting"), so a reader of a proposal is not told to unblock it. ledger-service.test.ts and the route/web tests pin the new table. | Met | packages/domain/src/engprod/ledger-service.ts now exports decideReconciliation(task) that keys on tags and planned-past-TODO statuses (lines showing if (task.tags.includes(ENGPROD_ACCEPTED_TAG)) return "accepted"; and planned statuses set). The cockpit route engprod-proposals.ts implements the tag-swap accept path and reads both tags in the list route. docs/cockpit-ui.md Proposals 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 swaps engprod-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.md updates).
    Affected: docs/cockpit-ui.md, docs/autonomy-class.md, docs/standing-pointings.md, docs/architecture/cockpit.md

@edobry
edobry merged commit 532be5d into main Sep 13, 2026
23 checks passed
@edobry
edobry deleted the task/mt-5130 branch September 13, 2026 10:58

@minsky-reviewer minsky-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_PATTERNS is 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.ts is 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) find when mapping servedProposals back to population (scripts/verify-autonomy-class.ts:95–108). Given typical N, this is fine; if desired, pre-index population by id for O(1) lookups. Also, the local SqlCapablePersistence and isSqlCapable guard are good; you could narrow the taskService/taskGraphService types 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-gated if ANY: kind umbrella; kind state-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 with RFC or ADR (measured 2026-09-13: 16 open tasks by title prefix, only 8 of them tagged); ## Scope names a listed surface; the title or ## Summary matches 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 with name/architecture/product/production gates 211 on incidental prose and is rejected).
  • else contained if 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 (incident tag or an Origin: line matching /incident/i), cleared-blocker re-surface (a cleared-blocker tag — mt#2825's marker when it ships), docs-only or generated-only scope (every extracted path under docs/ or src/generated/).
  • else pull-only. Kind work-package is pull-only with the reason ADR-046: claimed deliberately — findAvailableTasks' existing kind exclusion is untouched.
  • unknown only when inputs are unavailable (spec signals not loaded and the row signals alone do not resolve principal-gated; no task row) — never as a default for a well-formed task. | Met | packages/domain/src/tasks/autonomy-class.ts implements computeAutonomyClass (pure; lines ~183–287) and exports AUTONOMY_LISTED_SURFACE_PATTERNS, extractScopePaths, and marker/tag sets. deploy-surface.ts now exports CONFIG_SURFACE_PATTERNS (lines ~35–43) consumed by the list in autonomy-class.ts (lines ~67–92). Tests pin list and markers in packages/domain/src/tasks/autonomy-class.test.ts (e.g., 'pinned lists' suite). |
    | Human-origin evidence. There is NO author column on tasks (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 by session-pr-operations.ts, so a backlog task has none. This task does NOT add a column. Absence of evidence ⇒ agent-authored ⇒ cannot be contained — default-deny applied to the signal itself. contained is therefore unreachable in production until a human-origin marker exists; a follow-up may add origin at tasks_create, set by channel (cockpit form = human, MCP = agent, miner = automated). File that follow-up from this task; do not build it here. | Met | computeAutonomyClass requires humanOrigin === true and otherwise adds 'no human-origin evidence' to reasons before returning pull-only (autonomy-class.ts lines ~236–257). No schema changes appear in this diff adding author/origin columns; production call sites pass humanOrigin: undefined (e.g., pointings-store.ts lines ~362–371 and composition/domain.ts wiring). |
    | findAvailableTasks computes the class per candidate, excludes principal-gated and unknown, and each served item carries autonomyClass. Because the method's return type is AvailableTask[] with two production callers, the count lands on a sibling findAvailableTasksWithClass(options) → { tasks, excludedByClass: { principalGated, unknown } } that findAvailableTasks delegates to (so the old signature also denies); the tasks_available adapter calls the sibling and reports excludedByClass in both JSON and text output. Data path (planning, 2026-09-13): listTasks returns no spec text, and the classifier needs ## Scope + ## Summary; a per-candidate getTaskSpecContent would be ~870 queries per call. packages/domain/src/tasks/autonomy-class-store.ts exports loadAutonomySpecSignals(db, taskIds) — ONE query over tasks ⋈ task_specs extracting the two sections with a Postgres ARE substring(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 into TaskRoutingService as an optional third constructor dependency (AutonomySignalSource), registered in composition/domain.ts the way TaskGraphService gets its db. The adapter's no-SQL fallback path has no bulk source: it classifies on row signals alone, so anything not principal-gated by row is unknown and excluded, counted, and named in the fallback's warning line — honest default-deny rather than an N+1.
    tasks_route (correction): generateRoute never calls findAvailableTasks — 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 each RouteStep carry autonomyClass (additive fields), readyTasks counts only steps whose class is pull-only/contained, and the 'ready to start immediately' line is replaced by 'target is principal-gated () — a person decides' when the target's class is principal-gated/unknown. A route to an umbrella stays a legal navigation query; nothing is refused.
    Pointing candidate sets (mt#5129, additive): each CandidateSet.items[] entry carries autonomyClass — the RFC's 'membership in a pointing never overrides this' made visible on the surface an agent reads in Phase 1. loadCandidateRows in pointings-store.ts adds the two section columns to its existing tasks ⋈ task_specs select; selectCandidates takes an optional classOf(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_available reports excludedByClass and, when a specific task is requested by id via tasks_route, the class and reason are on the target. | Met | composition/domain.ts wires the third dependency using createDbAutonomySignalSource (lines ~220–236). pointings-store.ts now bulk-loads spec signals and computes classes per candidate (lines ~352–374). The classifier store autonomy-class-store.ts implements the one-query section extraction and export (entire file). Tests in engprod/containment.test.ts assert excludedByClass counting and route annotations against the real TaskRoutingService. |
    | 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.ts files proposals as TODO — the engprod-proposal tag 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) via tasks_bulk-edit dry-run then execute. This task is the >10-record wrapper (operational-safety-dry-run-first.mdc); scope-match is exactly the engprod-proposal-tagged BLOCKED set, count re-derived at run time (15 on 2026-09-13 ~07:55Z, SQL over tasks). | Met | (a) Deny is landed via findAvailableTasksWithClass exclusion and counting. (b) Filing now creates TODO with engprod-proposal tag 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 cover below 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–114 decideReconciliation(status) returns accepted for ANY status other than BLOCKED/CLOSED — "acceptance = unblocking" (its spec SC3). A bare BLOCKED → TODO migration would therefore record all 15 clusters as accepted at 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_TAG in types.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) → rejected when CLOSED; accepted when the engprod-accepted tag is present OR status ∈ {PLANNING, READY, IN-PROGRESS, IN-REVIEW, DONE} (someone took it up outside the cockpit); no-change while TODO or BLOCKED with engprod-proposal present, or task not found. reconcileVerdicts takes a getTask(taskId) → { status, tags } \| undefined callback instead of getTaskStatus (contract change — callers enumerated in Scope). The cockpit curation gate moves with it: checkProposalGuard = tagged engprod-proposal, not CLOSED, not already engprod-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 (two listTasks calls, merged); deriveDisposition(task) in src/cockpit/web/lib/engprod-proposals.ts mirrors the new table (CLOSED → rejected; engprod-accepted or past TODO → accepted; else pending); docs/cockpit-ui.md §Proposals text follows. The filed-spec template in buildProposalSpec ("Unblocking this task = accepting the proposal. Closing it without unblocking = rejecting") and the ## Disposition section of the 15 live specs are rewritten to the new rule ("Accepting = the cockpit Proposals page's Accept, which swaps the engprod-proposal tag for engprod-accepted; planning it counts too; closing = rejecting"), so a reader of a proposal is not told to unblock it. ledger-service.test.ts and the route/web tests pin the new table. | Met | packages/domain/src/engprod/ledger-service.ts now defines ProposalTaskDisposition, includes ENGPROD_ACCEPTED_TAG logic, and updates reconcileVerdicts to accept a getTask returning {status,tags}. Tests in engprod/ledger-service.test.ts cover pending/accepted/rejected cases including the BLOCKED→TODO migration no-op. Cockpit docs updated accordingly in docs/cockpit-ui.md and acceptance behavior covered in containment.test.ts. |
    | packages/domain/src/tasks/autonomy-class.ts exports computeAutonomyClass(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–205 calls computeAutonomyClass via classify(), annotates autonomyClass, and enforces isServableClass. Fallback adapter path uses computeAutonomyClass and specSignalsFromContent directly (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 means contained unreachable in prod; production call sites pass humanOrigin: undefined. | Met | TaskRoutingService.classify() passes humanOrigin: undefined (packages/domain/src/tasks/task-routing-service.ts:66–82). No schema changes in this chunk add origin. Fallback adapter also passes humanOrigin: undefined (src/adapters/shared/commands/tasks/routing-commands.ts:170–187). |
    | findAvailableTasks computes class per candidate, excludes principal-gated and unknown, returns items with autonomyClass; sibling findAvailableTasksWithClass returns {tasks, excludedByClass}; adapters report counts; no-SQL fallback classifies per task and withholds unknown. | Met | packages/domain/src/tasks/task-routing-service.ts:98–114 delegates to findAvailableTasksWithClass; :179–205 computes classes, filters with isServableClass, counts excludedByClass, annotates autonomyClass. src/adapters/shared/commands/tasks/routing-commands.ts:126–146 uses the sibling and surfaces excludedByClass (JSON and text at :244–257). Fallback path classifies per-candidate, counts withheld, stops at limit servable (:156–187). Tests assert both paths (src/adapters/shared/commands/tasks/routing-commands.test.ts:58–164). |
    | tasks_route annotates target and steps with class; readyTasks counts only servable steps; when gated, replace 'ready to start' line with class+reason. | Met | packages/domain/src/tasks/task-routing-service.ts:307–390 annotates RouteStep.autonomyClass, computes targetAutonomyClass/Reasons, and readyTasks filters by isServableClass. 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 swaps engprod-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; autonomyClass is 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 domain decideReconciliation. | 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 new deriveDisposition(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, and docs/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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

authorship/co-authored Co-authored by human and AI agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant