Skip to content

fix(mt#5137): Gate the unattended supervisor's child admission on autonomy class - #3762

Merged
edobry merged 2 commits into
mainfrom
task/mt-5137
Sep 15, 2026
Merged

edobry merged 2 commits into
mainfrom
task/mt-5137

Conversation

@minsky-ai

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

Copy link
Copy Markdown
Contributor

Summary

The unattended supervisor (mt#4571) dispatched every unblocked child in its frontier with no regard for the child's autonomy class (RFC 3ae937f0 / mt#5130) — an rfc-tagged or state-ops child would be spawned unattended. Now the tick classifies the not-yet-dispatched frontier with the same classifyTasks that tasks_available uses, spawns only servable classes (pull-only, contained), and records every withheld child rather than silently shrinking the frontier.

Had Claude implement this against the mt#5137 spec.

What changed

  • Admission step in the tick (packages/domain/src/supervision/supervision-tick.ts, step 3b): principal-gated and unknown children go to SupervisionAdvance.excludedByClass with the classifier's reason strings. When every dispatchable child is withheld the hold reason is all-candidates-gated-by-autonomy-class (HOLD_ALL_GATED), distinct from all-children-blocked, so an umbrella whose remaining work is the principal's does not read as a dependency stall.
  • Durable record: new task_supervisions.last_excluded_by_class jsonb column (migration 0121_freezing_blonde_phantom.sql), written each tick (null when nothing was excluded, so a stale exclusion cannot persist).
  • Sweep log (src/cockpit/task-supervision-sweep.ts): the per-tick advance line carries excludedByClass per-class counts plus the <taskId> (<class>) list; a tick that spawned nothing but withheld something logs cockpit: supervision withheld on autonomy class <umbrella> with the same fields.
  • Status surface: tasks.supervision-status renders Withheld on autonomy class (N): / <taskId> <class>: <reasons>.
  • Shared classifier: classifyTasks(tasks, signalSource) extracted from TaskRoutingService's private classify into autonomy-class-store.ts; the routing service now calls it, so the two auto-selecting consumers cannot disagree.
  • Wiring: SupervisionTickDeps.classifyCandidates is REQUIRED (not optional — a consumer that forgets it must fail to typecheck rather than dispatch ungated). The cockpit sweep supplies it via createDbAutonomySignalSource(db); scripts/verify-task-supervision.ts likewise.
  • Design: the check lives in the tick, not in computeUmbrellaFrontier — tasks.orchestrate shares the frontier and answers an agent's own pull, where the default-deny lands on the agent (docs/autonomy-class.md).
  • Docs: docs/autonomy-class.md consumer list (replaces the "Not yet wired" line), docs/architecture/cockpit.md §Unattended task-supervision sweeper (new column, new step 4).

Success criteria

  1. Admission via computeAutonomyClass through the registered AutonomySignalSource, only pull-only/contained admitted, exclusions recorded with class + reason — supervision-tick.ts step 3b + last_excluded_by_class; AT1 below.
  2. Per-tick log names excludedByClass counts — task-supervision-sweep.ts advance/withheld lines.
  3. Fake-signal-source test: one pull-only READY child + one rfc-tagged READY child → one spawn, other recorded principal-gated — AT1 below.

Tests

  • AT1 (supervision-tick.test.ts): umbrella with a READY untagged child, a READY rfc-tagged child, and a TODO child, filter ["READY"] → exactly one spawn (the untagged child), one excludedByClass entry { mt#912, principal-gated, ["tag rfc"] }, the TODO child neither dispatched nor listed (status-filtered before classification), lastExcludedByClass persisted on the row, hold reason null. Uses the REAL classifier over the fake graph with a fake signal source — no stubbed verdicts.
  • All-gated hold: an RFC:-titled child + a state-ops child → 0 spawns, HOLD_ALL_GATED, both recorded.
  • Clearing: once the gated child leaves the filter, the next tick writes lastExcludedByClass: null.
  • AT2 (supervision-commands.test.ts): formatSupervisionStatus lists the withheld child with its reason string; omits the section when empty.

Negative control: with isServableClass(verdict.class) replaced by true in supervision-tick.ts, bun test ... -t "mt#5137" → 0 pass / 3 fail (AT1: Expected length: 0 / Received length: 2). Restored byte-identical (diff against backup: IDENTICAL).

Execution evidence:

  • validate_typecheck (session): pass, 0 errors, 8 workspaces
  • validate_lint (session): pass, 0 errors, 0 warnings
  • bun test packages/domain/src/supervision/: 24 pass, 0 fail
  • bun test src/adapters/shared/commands/tasks/supervision-commands.test.ts: 10 pass, 0 fail
  • bun test packages/domain/src/tasks/{autonomy-class,task-routing-service,pointings-store}.test.ts: 32 pass, 0 fail
  • bun test src/cockpit/task-supervision-sweep.test.ts: 8 pass, 0 fail (re-run after R1: 8 pass, 0 fail)
  • pre-commit fast gate (run-related-tests.ts, 134 files): passed on the committed tree; pre-push gated runner passed (pushed without a hatch)

Reviewer R1 (APPROVED, 3 non-blocking)

  • Withheld ids missing from the "advanced" log line — taken (909f85e6c): the <taskId> (<class>) list now rides on both lines.
  • lastExcludedByClass.class is string at the storage boundary, TaskAutonomyClass in the domain — deliberate. The column is jsonb, so a row written by an earlier build can carry a class name a later build has retired; keeping the storage type honest (string) and narrowing in DrizzleSupervisionStore's row mapper keeps the cast at the one seam that owns it, rather than letting the schema assert a guarantee the database does not give.
  • UNCLASSIFIED reason string can reach user surfaces — intended. The reason (classifier returned no verdict for this task) exists to be read: a status report that said unknown with no reason would send the operator to the logs for exactly this sentence. It is a default-deny made visible, not a leak.

Incidental fix: transcript-sweep-backstop.test.ts order-dependence

The pre-commit fast gate failed 14 tests in src/cockpit/transcript-sweep-backstop.test.ts on this branch and passes on main. Cause: packages/domain/src/composition/container.test.ts runs earlier in the same process and initializes configuration, so the sweep's Phase 0 refreshSchemaReadinessFromDb reached the operator's live DB, found migration 0121 pending, skipped every tick, and sweepsRun >= 1 never held. The file's 11 startTranscriptSweepBackstop starts injected deps but never passed schemaReadiness: false — the exact case the option's docblock names, and which the sibling transcript-backfill-sweep.test.ts already handles. All 11 now pass it. Reproduced with 3 files, fixed at the source; recorded on mt#3575 as a second same-day instance of the schema-ahead-branch hazard.

Deploy verification

Deploy surface: minsky-mcp (migration 0121 applied by deploy-minsky-mcp.yml before the image push; the supervisor sweep runs in the cockpit daemon, which picks up the code on restart). Post-merge: confirm task_supervisions.last_excluded_by_class exists in prod via information_schema.columns, correlate the Deploy MCP run head_sha with the merge commit, and read /health service identity.

Consumer account

No signal-producing call removed. TaskRoutingService.classify was replaced by a delegation to classifyTasks with identical inputs and outputs (task-routing-service.test.ts + tests/domain/tasks/task-routing-service.test.ts unchanged and green).

Spec: mt#5137. Parent handoff: mt#5144.

🤖 Generated with Claude Code

https://claude.ai/code/session_0147JJ3SVgqRz9JyUD4NZBz9

…onomy class

The supervisor tick now classifies the frontier's dispatchable children with the
same `classifyTasks` the routing service uses and spawns only servable classes
(`pull-only`, `contained`). A `principal-gated` or `unknown` child is recorded
rather than silently dropped: per tick on `SupervisionAdvance.excludedByClass`,
durably on the new `task_supervisions.last_excluded_by_class` jsonb column
(migration 0121), and in `tasks.supervision-status` under
`Withheld on autonomy class (N)` with the classifier's reason strings.

When every dispatchable child is gated the hold reason is
`all-candidates-gated-by-autonomy-class`, distinct from `all-children-blocked`
so an umbrella whose remaining work is the principal's does not read as a
dependency stall.

The check lives in the tick, not in `computeUmbrellaFrontier`: `tasks.orchestrate`
shares the frontier and answers an agent's own pull, where the default-deny
lands on the agent.

- `classifyTasks` extracted from `TaskRoutingService.classify` into
  `autonomy-class-store.ts` so both consumers run one classifier.
- `SupervisionTickDeps.classifyCandidates` is required; the sweep wires it
  through `createDbAutonomySignalSource(db)`.
- Tests: AT1 (READY pull-only + READY rfc-tagged + TODO child → one spawn, one
  recorded exclusion, TODO filtered by status), the all-gated hold, exclusion
  clearing on a clean tick, and AT2 on `formatSupervisionStatus`.
- Docs: `docs/autonomy-class.md` consumer list, `docs/architecture/cockpit.md`
  §Unattended task-supervision sweeper.
- `transcript-sweep-backstop.test.ts`: all 11 starts now pass
  `schemaReadiness: false` (the case the option's docblock names). Without it
  the pre-commit fast gate fails on any schema-ahead branch: an earlier file
  initializes config, Phase 0 reaches the live DB, finds 0121 pending, skips
  every tick, and 14 tests time out. Recorded on mt#3575.

Execution evidence:
- `validate_typecheck` (session): pass, 0 errors, 8 workspaces
- `validate_lint` (session): pass, 0 errors, 0 warnings
- `bun test packages/domain/src/supervision/`: 24 pass, 0 fail
- `bun test src/adapters/shared/commands/tasks/supervision-commands.test.ts`: 10 pass, 0 fail
- `bun test packages/domain/src/tasks/{autonomy-class,task-routing-service,pointings-store}.test.ts`: 32 pass, 0 fail
- `bun test src/cockpit/task-supervision-sweep.test.ts`: 8 pass, 0 fail
- `bun test container.test.ts domain-repository-backend.test.ts transcript-sweep-backstop.test.ts`: 71 pass, 0 fail (was 14 fail before the schemaReadiness fix)

Negative control: with `isServableClass(verdict.class)` replaced by `true` in
`supervision-tick.ts`, `bun test ... -t "mt#5137"` → 0 pass, 3 fail (AT1
`Expected length: 0 / Received length: 2`; all-gated hold; clearing test).
Restored byte-identical (diff against backup: IDENTICAL).

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 14, 2026
@minsky-reviewer

minsky-reviewer Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Minsky Reviewer Status

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

Commands

  • /review — request a fresh review

@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, spec-aligned implementation of autonomy-class gating for the unattended supervisor: classification is injected via a required dep, shared through the new classifyTasks to avoid drift, withheld children are durably recorded and surfaced, and cockpit logs carry per-class counts. Migration/schema wiring and tests (including mixed and all-gated cases, and clearing behavior) are present and meaningful. I raised a few non-blocking notes: storage type for the persisted class is a plain string (could narrow/validate), the UNCLASSIFIED reason text may leak internal phrasing to operators, and mixed-result logs only include counts without sample ids. None of these block merge. Overall, this meets the task’s success criteria and improves observability and consistency.

Findings

  • [NON-BLOCKING] packages/domain/src/storage/schemas/task-supervisions-schema.ts:158 — Type of lastExcludedByClass.class is a plain string at the storage boundary while domain types expect TaskAutonomyClass
    taskSupervisionsTable.lastExcludedByClass is typed with $type<Array<{ taskId: string; class: string; reasons: string[] }>>(), and downstream code (packages/domain/src/supervision/types.ts) casts it to ExcludedCandidate[] where class: TaskAutonomyClass. This compiles (runtime JSONB is untyped), but widens the surface for accidental values not in the union and shifts validation to later. Consider tightening the schema’s $type to the union, or validating/coercing on read to avoid persisting arbitrary strings.
  • [NON-BLOCKING] packages/domain/src/supervision/supervision-tick.ts:104 — UNCLASSIFIED reason string can leak into user surfaces if a classifier omission occurs
    When a candidate id is absent from classifyCandidates’s map, it resolves to UNCLASSIFIED with reason "classifier returned no verdict for this task". That reason will flow to last_excluded_by_class, CLI status, and cockpit logs. If an upstream bug or transient omission occurs, operators will see this internal diagnostic phrasing. Consider using a more operator-oriented wording (e.g., "insufficient signals to determine autonomy class") and/or distinguishing between hard-unknown vs. plumbing/omission cases for clearer actionability.
  • [NON-BLOCKING] src/cockpit/task-supervision-sweep.ts:256 — When a tick both dispatches/settles and withholds, only per-class counts are logged — no sample identifiers
    The new logs include excludedByClass aggregate counts on advancement lines and the explicit task-id list only when nothing dispatched. This meets SC2 (counts), but operators triaging mixed-result ticks may benefit from seeing a short sample (e.g., first N ids/classes) alongside counts to accelerate investigation without reaching for DB/CLI. Not blocking; raising as observability enhancement for operational clarity.

Spec verification

Criterion Status Evidence
Admission via computeAutonomyClass through the registered AutonomySignalSource, only pull-only/contained admitted, exclusions recorded with class + reason — supervision-tick.ts step 3b + last_excluded_by_class; AT1 below. Met packages/domain/src/supervision/supervision-tick.ts:209-245 implements step 3b — calls deps.classifyCandidates and filters with isServableClass; withheld candidates are appended to advance.excludedByClass with {taskId,class,reasons}. Persisted each tick at packages/domain/src/supervision/supervision-tick.ts:336-342 to task_supervisions.last_excluded_by_class (migration adds column at packages/domain/src/storage/migrations/pg/0121_freezing_blonde_phantom.sql:1; schema wired at packages/domain/src/storage/schemas/task-supervisions-schema.ts:146-160). AT1 test covers spawn vs exclusion at packages/domain/src/supervision/supervision-tick.test.ts:88-129.
Per-tick log names excludedByClass counts — task-supervision-sweep.ts:263-278. Met src/cockpit/task-supervision-sweep.ts:256-279 aggregates per-class counts into excludedByClass and logs them on both advancement and withheld-only paths. The withheld-only branch logs explicit excluded ids as well.
Fake-signal-source test: one pull-only READY child + one rfc-tagged READY child → one spawn, other recorded principal-gated — AT1 below. Met packages/domain/src/supervision/supervision-tick.test.ts:101-129 builds a fake DAG, wires classifyCandidates using real classifyTasks with a fake signal source, asserts exactly one dispatch (mt#911) and one excludedByClass entry { taskId: 'mt#912', class: 'principal-gated', reasons: ['tag rfc'] }, and that the persisted lastExcludedByClass matches.

Adoption sweep

Symbol Kind Consumers found Classification Notes
classifyTasks function packages/domain/src/tasks/task-routing-service.ts:88 — private classify delegates to classifyTasks, packages/domain/src/supervision/supervision-tick.test.ts:68 — deps.classifyCandidates uses classifyTasks over fake graph, scripts/verify-task-supervision.ts:335 — deps.classifyCandidates wires classifyTasks with DB source, src/cockpit/task-supervision-sweep.ts:196-205 — classifyCandidates wired via classifyTasks with DB source Adopted Extracted from TaskRoutingService to a shared store and consumed by both routing and supervision paths to prevent drift.

Documentation impact

  • updated-in-pr — Docs updated in this PR to describe new behavior: docs/architecture/cockpit.md adds last_excluded_by_class column and new step 4 'Admit on autonomy class' (lines around added section); docs/autonomy-class.md adds the unattended supervisor consumer wiring details. These reflect the added persistence field and gating behavior. No existing doc statements read as contradicted post-change within the touched docs.
    Affected: docs/architecture/cockpit.md, docs/autonomy-class.md

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Cockpit Preview

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

Deployed from commit 909f85e6c.
Preview runs in read-only mode — mutations are disabled.

Updated by Cockpit Preview.

…g line too

A tick that dispatched one child and withheld another logged only per-class
counts for the withheld one; the ids now ride on both the "advanced" and the
"withheld" lines.

Execution evidence:
- `bun test src/cockpit/task-supervision-sweep.test.ts`: 8 pass, 0 fail

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


Verified the fix commit 909f85e addresses prior R1 feedback: the cockpit sweep now includes the withheld child ids on the "advanced" log line as well as per-class counts. I re-checked the supervision tick’s admission gating and durability against the current spec — SC1–SC3 are met, and tests exercise the new paths. No correctness regressions surfaced in the updated code; the only note is a non-blocking reminder to ensure any log consumers tolerate the added excludedByClass and excluded fields. With that, this iteration is ready to merge.

Findings

  • [NON-BLOCKING] src/cockpit/task-supervision-sweep.ts:169 — Logging shape changed — ensure log consumers tolerate new excludedByClass and excluded fields
    The tick log for both the "advanced" and "withheld" cases now includes excludedByClass (an object of per-class counts) and excluded (an array of "<taskId> (<class>)"). If any downstream parser relies on the previous exact shape, it may need an update. Suggest confirming no strict JSON-schema consumer exists or making the addition explicitly backward-compatible in dashboards.

Spec verification

Criterion Status Evidence
The supervisor's child admission computes each candidate's class with computeAutonomyClass (via the same AutonomySignalSource mt#5130 registers) and admits only pull-only / contained; excluded children are recorded on the supervision run with their class and reason, not silently skipped. Met packages/domain/src/supervision/supervision-tick.ts:103-157 — Step 3b calls deps.classifyCandidates(...), uses isServableClass(verdict.class) to admit only servable classes, and pushes non-servable candidates into advance.excludedByClass with { taskId, class, reasons }. HOLD_ALL_GATED is set when every dispatchable child is withheld. The row update writes lastExcludedByClass each tick (lines 199-212).
The sweep's own log line per tick names excludedByClass counts, so a supervision that spawns nothing says why. Met src/cockpit/task-supervision-sweep.ts:150-187 — For each advance, the code aggregates excludedByClass counts and includes them in both the "advanced" and the "withheld" log lines; it also logs the excluded id list to make withheld tasks explicit.
A test over a fake signal source: an umbrella with one pull-only READY child and one rfc-tagged READY child spawns exactly one child and records the other as excluded principal-gated. Met packages/domain/src/supervision/supervision-tick.test.ts:93-141 — Test "spawns the pull-only child, withholds the principal-gated one, and records why (AT1)" builds a fake source, asserts one dispatch (mt#911) and one exclusion for mt#912 with class principal-gated and reasons ["tag rfc"], and verifies persistence on the supervision row.

Documentation impact

  • no-update-needed — This fix commit only augments the cockpit sweep log payload to include excludedByClass counts and the excluded id list on the "advanced" line (src/cockpit/task-supervision-sweep.ts:165-187). It does not change any user-facing commands, APIs, or documented behavior; the additional keys are internal log fields. Existing supervision and autonomy-class docs remain accurate.

@edobry
edobry merged commit 5b5385f into main Sep 15, 2026
23 checks passed
@edobry
edobry deleted the task/mt-5137 branch September 15, 2026 00:03

@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


Re-verified the fixes from the latest commit against the current spec. The supervisor tick now gates frontier candidates by autonomy class via the shared classifyTasks, records withheld children, and persists last_excluded_by_class; the cockpit sweep logs per-class counts and the id list. Tests cover the admission, all-gated hold, and clearing cases; the new shared classifier is adopted by both the routing service and the supervisor wiring. I find no new blocking defects. One non-blocking note: ensure any log consumers tolerate the added excludedByClass/excluded fields. With docs updated, this iteration is ready to merge.

Findings

  • [NON-BLOCKING] src/cockpit/task-supervision-sweep.ts:268 — Log-payload additions: verify downstream tolerance for excludedByClass and excluded keys
    The sweep’s per-tick logs now include two additional fields on the "advanced" and new "withheld" lines: excludedByClass (per-class counts) and excluded (array of "<taskId> (<class>)"). See src/cockpit/task-supervision-sweep.ts:268-286. If any log consumer parses these records with a strict schema, it may need an update. If consumers treat the log metadata as an open map, this is safe. Recommend a quick check on any dashboards or alerts that read these lines.

Spec verification

Criterion Status Evidence
Admission via computeAutonomyClass through the registered AutonomySignalSource, only pull-only/contained admitted, exclusions recorded with class + reason — supervision-tick.ts step 3b + last_excluded_by_class; AT1 below. Met packages/domain/src/supervision/supervision-tick.ts:209-247 — Step 3b classifies undispatched via deps.classifyCandidates(...), admits only when isServableClass(verdict.class); non-servable candidates are pushed into advance.excludedByClass with { taskId, class, reasons }. packages/domain/src/storage/schemas/task-supervisions-schema.ts:146-162 defines last_excluded_by_class jsonb; supervision-tick.ts:332-338 persists it each tick (null when none). Tests: packages/domain/src/supervision/supervision-tick.test.ts:86-119 wires the REAL classifier; 134-172 asserts one spawn and one principal-gated exclusion with reasons.
Per-tick log names excludedByClass counts — task-supervision-sweep.ts advance/withheld lines. Met src/cockpit/task-supervision-sweep.ts:256-286 — Aggregates per-class counts into excludedByClass and lists excluded ids; includes both in the "advanced" line and emits a dedicated "withheld on autonomy class" line when nothing spawned but some were excluded.
Fake-signal-source test: one pull-only READY child + one rfc-tagged READY child → one spawn, other recorded principal-gated — AT1 below. Met packages/domain/src/supervision/supervision-tick.test.ts:134-172 — "spawns the pull-only child, withholds the principal-gated one, and records why (AT1)"; asserts exactly one dispatch (mt#911) and one exclusion for mt#912 { class: "principal-gated", reasons: ["tag rfc"] }, plus durability on the supervision row.

Adoption sweep

Symbol Kind Consumers found Classification Notes
packages/domain/src/tasks/autonomy-class-store.classifyTasks function packages/domain/src/tasks/task-routing-service.ts:88 — delegates private classify() to classifyTasks(tasks, this.signalSource), packages/domain/src/supervision/supervision-tick.test.ts:66 — deps.classifyCandidates uses classifyTasks over fake graph, scripts/verify-task-supervision.ts:335 — classifyCandidates wires classifyTasks with DB-backed signal source, src/cockpit/task-supervision-sweep.ts:198 — classifyCandidates wires classifyTasks with DB-backed signal source Adopted Shared classifier extracted to prevent drift between tasks_available and the unattended supervisor (mt#5137).

Documentation impact

  • updated-in-pr — Docs updated alongside the code: docs/architecture/cockpit.md adds last_excluded_by_class and a new Step 4 explaining autonomy-class admission; docs/autonomy-class.md now lists the unattended supervisor as a wired consumer with durability and status-surface details.
    Affected: docs/architecture/cockpit.md, docs/autonomy-class.md

@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


Re-verified the latest changes against the current spec. The supervisor tick now gates undispatched frontier children via the shared classifyTasks, records withheld candidates, persists last_excluded_by_class, and distinguishes the all-gated hold. Cockpit sweep logs include per-class counts and ids; status formatting surfaces withheld children with reasons. All call sites supply the required classifyCandidates dep; migrations and schema wiring are consistent. Tests cover admission, all-gated holds, and clearing. I see no new blocking defects introduced by this iteration — approving.

Spec verification

Criterion Status Evidence
The supervisor's child admission computes each candidate's class with computeAutonomyClass (via the same AutonomySignalSource mt#5130 registers) and admits only pull-only / contained; excluded children are recorded on the supervision run with their class and reason, not silently skipped. Met packages/domain/src/supervision/supervision-tick.ts: step 3b adds autonomy-class admission — calls deps.classifyCandidates(...), keeps only isServableClass(verdict.class), and pushes withheld candidates into advance.excludedByClass; later persists lastExcludedByClass on the row via store.updateSupervision.
The sweep's own log line per tick names excludedByClass counts — task-supervision-sweep.ts advance/withheld lines. Met src/cockpit/task-supervision-sweep.ts:256-286 — computes excludedByClass per-class counts and excluded id list for each advance; includes them on the "advanced" log line and emits a dedicated "withheld on autonomy class" log when nothing spawned but exclusions exist.
A test over a fake signal source: an umbrella with one pull-only READY child and one rfc-tagged READY child spawns exactly one child and records the other as excluded principal-gated. Met packages/domain/src/supervision/supervision-tick.test.ts:134-172 — test "spawns the pull-only child, withholds the principal-gated one, and records why (AT1)" builds a fake source over real classifier, asserts one dispatch and one principal-gated exclusion with reasons, and verifies lastExcludedByClass was written.

Adoption sweep

Symbol Kind Consumers found Classification Notes
packages/domain/src/tasks/autonomy-class-store.classifyTasks function packages/domain/src/tasks/task-routing-service.ts — private method classify now delegates to classifyTasks(tasks, this.signalSource), packages/domain/src/supervision/supervision-tick.test.ts — deps.classifyCandidates uses classifyTasks over fake graph, scripts/verify-task-supervision.ts — wires classifyTasks with DB-backed signal source for verify script, src/cockpit/task-supervision-sweep.ts — wires classifyTasks with DB-backed signal source in tick deps Adopted Shared classifier extracted to prevent drift; both the routing service and the unattended supervisor now share it per mt#5137.

Documentation impact

  • updated-in-pr — Docs were updated alongside the code to describe the new autonomy-class admission and durable record: docs/architecture/cockpit.md adds last_excluded_by_class to the task_supervisions record and a new Step 4 detailing admission; docs/autonomy-class.md now lists the unattended supervisor as a wired consumer and explains per-tick/row/status surfaces.
    Affected: docs/architecture/cockpit.md, docs/autonomy-class.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