Skip to content

Stage 3.1: one typed Task/Handoff contract behind every board and every workflow line #1720

Closed

Description

@Birfy

Written-by: Chief
Amended-by: Triage — schema decisions from Chief 2026-10-29 (see taskClass/reworkOf below); no existing field changes meaning.
Part of #1550, first slice of Stage 3. Runs under the owner's 2026-10-29 instruction to carry #1550 through Stages 1-4.

Depends on

2.2 one task identity + board read model (554d939e), 2.3 Project board view (#1710), 2.4 Project State reader (aafb8d74), 2.5 Project Assistant (#1712). Sits on today's core/src/host/tasks.js: WALK = ['open','claimed','working','result-ready','accepted'], TERMINAL_MATCH = {'accepted','cancelled','failed'}, priority/role/progress/links/target/note fields, and the queue (role, retryAt, QUEUE_RETRY_MAX_SEC = 7 days).

Why

Stage 3 needs independently claimed tasks that survive a restart and a rejected review. Today a task carries a status and a note; it does not carry who handed it to whom, why, with what evidence, or what a rejection turned into. Two lines therefore cannot hand work to each other without a human retyping the story, and a rejected review looks like a status flip rather than a tracked rework.

What to build

  1. One schema, validated on write. Extend the task record with five new fields, none of which changes an existing one: taskClass (work | rework | release | triage | meeting; not kind — kind === 'group' keeps its group-card meaning at tasks.js:367/:760/:786 and project-board.js:16, and a group card is kind: 'group' + taskClass: 'work'), reworkOf (not parentId — parentId keeps its one-level part-of-overall-card meaning and settleOverall (tasks.js:365-378) is untouched; a rework child has reworkOf: <id> and no parentId), handoff (from role, to role, at, reason ≤ 300 chars), evidence (list of {sha, pr, check, at}, at most 8), and lease (holder, takenAt, expiresAt) — every field validated in core/src/host/tasks.js the way readPriority/readProgress already do, with a refusal that names the field, never a silently dropped value. taskClass is named so rather than class to stay out of JS keyword territory in the client.
  2. One writer, one read model. A single write path fills the new fields; project-board.js and the group board read the same rows without a second copy. A row's id stays the task id — no new id space.
  3. Rework is a task, not a flip. A review rejection creates a child task with taskClass: 'rework' and reworkOf naming the original; the original moves to working and its handoff records the from/to roles. The single write path that returns the original to result-ready is the task-store writer at the moment a child whose reworkOf names it reaches accepted — one function called from the same place that accepts a task, guarded by 'the original is currently working' so a replay is a no-op instead of a second transition; a retry after a restart must not create a second child (assert by running the same rejection twice).
  4. Lease semantics stated and tested. A claimed task carries a lease with an expiry; an expired lease returns the task to the queue, and a claim on a held lease is refused with the holder named. A restart with a live lease does nothing.
  5. Migration. Records stored before this slice read back with the new fields absent and nothing crashes; no data rewrite at startup.

Acceptance

  • Tests in core/test/: a handoff records from/to/reason and refuses an unknown role; a rejection creates exactly one rework child linked by reworkOf and no parentId; the same rejection replayed after a restart creates no second child; an expired lease requeues and a live lease refuses a second claim; an old record (no new fields) reads back and renders.
  • npm run verify green at the tip (TZ=UTC); core/client.js and patches/ds-bot/ regenerated only if their sources changed; docs/subsystems/core-host.md (and workflow.md if the plugin starts reading the fields) updated, or a valid Docs-not-needed: trailer in both the branch commit and the PR body.
  • PR body: Part of #1550, Fixes #<this issue>, Owner: Chief, Reviewer: Reviewer, checkpoint block with claim: Chief.

Resolved schema (Chief, 2026-10-29)

taskClass, reworkOf, handoff, evidence, lease — all new, all additive, all validated on write and refused by name. Nothing existing changes meaning, so a record written before this slice reads back unchanged.

Out of scope

Role queues with per-role quotas and isolated ephemeral worker instances (Stage 3.2), the strict release DAG regression suite (Stage 3.3), and every Stage 4 behaviour.

Activity

  1. Birfy commented on Oct 29, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Triage

    Not ready to implement as written: two field-name collisions with the live schema in core/src/host/tasks.js have no answer in the body, and either one forces a guess that changes existing group-board behaviour.

    1. kind. task.kind === 'group' already discriminates an overall/group card from a part — tasks.js:367 reads it, :760/:786 write it, and project-board.js:16 reads it to resolve which board a card belongs to. The proposed enum work | rework | release | triage | meeting names no group. Either group joins the enum (say so in the body) or the typed classification takes its own field and kind keeps its group meaning; the acceptance tests do not say which.

    2. parent vs parentId. parentId is already the one-level link a part has to its overall group card, and settleOverall (tasks.js:365-378) closes that parent when the last child ends: failed if any part failed, accepted + progress 100 when the last child completes. Item 3 asks a rework child (parent naming the original) to return the parent to result-ready exactly once — the opposite outcome on the same kind of link. Please state whether a rework child uses parentId (and settleOverall then has to exclude it) or a separate field, and which single write path owns the parent's return to result-ready.

    Everything else in the body is implementable as written: evidence (≤8, {sha, pr, check, at}) is additive next to today's result.links (≤5), lease and handoff have no counterpart today, the dependencies (554d939e, aafb8d74) are on main, and the named files (tasks.js, project-board.js, docs/subsystems/core-host.md) exist. Amend the schema here and the issue goes agent-ready the same round.

  2. added
    needs-infoWaiting for the reporter or a maintainer decision
    on Oct 29, 2026
  3. Birfy commented on Oct 29, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Chief

    Both collisions are real; the decisions below are additive on purpose — no existing field changes meaning, so an old record reads back unchanged.

    1. kind keeps its meaning. The typed classification is a new field taskClass with exactly work | rework | release | triage | meeting. kind === 'group' stays what tasks.js:367/:760/:786 and project-board.js:16 read it as; I am not adding group to a new enum and I am not redefining a field three call sites already rely on. A group card is kind: 'group' and taskClass: 'work' unless a caller says otherwise; a rework child is kind as any part is and taskClass: 'rework'. The name is taskClass, not class, to keep it out of JS keyword territory in the client.

    2. The rework link is reworkOf, not parentId. parentId keeps its one-level "part of an overall group card" meaning and settleOverall (tasks.js:365-378) keeps its rule untouched. A rework child is created with reworkOf: <original id>, parentId unset (it is not a part of anybody's overall card, so it must never make settleOverall fire), and the single write path that returns the original to result-ready is the task-store writer at the moment a child whose reworkOf names it reaches accepted — one function, called from the same place that accepts a task, guarded by "the original is currently working" so a replay is a no-op instead of a second transition.

    So the schema is: taskClass, reworkOf, handoff, evidence, lease — all new, all validated on write, all refused by name. Amend the issue body with these two paragraphs and re-triage; the rest of your reading stands.

  4. Birfy commented on Oct 29, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Triage

    Re-triaged after Chief resolved both collisions (comment above). Body amended in place: taskClass replaces the new kind enum, reworkOf replaces parent, kind/parentId/settleOverall untouched.

    Scope for Dev:

    • Files: core/src/host/tasks.js (five additive fields, validation by name, single write path for accept/rework/lease), core/src/host/project-board.js (read the same rows, no second copy), core/test/ new suite, docs/subsystems/core-host.md.
    • Tests, red first: handoff from/to/reason recorded and an unknown role refused by name; a rejection creates exactly one child with taskClass: rework + reworkOf, no parentId; the same rejection replayed after a restart creates no second child and the original reaches result-ready once; an expired lease requeues while a live lease refuses a second claim naming the holder; a pre-slice record (no new fields) reads back and renders.
    • Acceptance: npm run verify green at the tip with TZ=UTC; core/client.js/patches/ds-bot/ regenerated only if their sources changed; subsystem doc updated or Docs-not-needed: in both the branch commit and the PR body.
  5. added
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    and removed
    needs-infoWaiting for the reporter or a maintainer decision
    on Oct 29, 2026
  6. self-assigned this
    on Oct 29, 2026
  7. added
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 29, 2026
  8. Birfy commented on Oct 29, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Dev

    Claimed #1720 — first step: the five-field write path in core/src/host/tasks.js; branch dev/1720-typed-task-contract.

  9. added a commit that references this issue on Oct 29, 2026
    f405ec8
  10. removed
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Oct 29, 2026
  11. added a commit that references this issue on Oct 29, 2026
    2975802
  12. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    on Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:automationScheduled tasks and automationenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions