Skip to content

feat(tasks): one typed task contract for class, rework, handoff, evidence and lease - #1732

Merged
Birfy merged 1 commit into
mainfrom
dev/1720-typed-task-contract
Oct 29, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev/1720-typed-task-contract

Conversation

@Birfy

@Birfy Birfy commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev

Fixes #1720
Part of #1550
Owner: Chief
Reviewer: Reviewer
Tip SHA: 2975802
Base: 54dea30

What

One typed contract on the existing task record: taskClass (work | rework | release | triage | meeting), reworkOf, handoff {from, to, at, reason}, evidence [{sha, pr, check, at}] (at most 8) and lease {holder, takenAt, expiresAt}. None of them changes an existing field: kind === 'group', parentId and settleOverall keep their meaning, and a record stored before this slice reads back unchanged.

Why

Stage 3 needs cards that survive a rejection and a restart: a rejection has to become tracked rework rather than a status flip, a claim has to lapse when its holder disappears, and a board has to read who handed what to whom without a human retyping it. The two collisions (a new kind enum, and reusing parentId for rework) were resolved by the Chief in the issue; this PR implements that resolution.

How

  • core/src/host/tasks.js: one writer, writeContract, reads all five fields (naming the refused field, and the role it does not know) before it assigns any of them, and is reached from track_tasks, task-create and task-update; a refused new card leaves nothing behind.
  • task-send-back records the handover on the original, moves it to working, and opens exactly one child with taskClass: 'rework' and reworkOf — parentId is never set, so settleOverall never fires for it. A replay reuses the child that is still open.
  • closeRework is the single write that returns the original to result-ready, called where a task is accepted (task-accept-result and the board's done column), guarded by the original still being working.
  • A claim takes a 30-minute lease; a claim on a live lease is refused as bot/lease-held with the holder named, and expireLeases returns an expired queued card to the queue (reaping happens before a claim reads the queue, and the requeue is recorded on the card).
  • core/src/host/project-board.js: the row carries the five fields when the card has them, from the same state.tasks row, so a pre-slice card keeps its old row shape.
  • core/src/host/tools.js: track_tasks declares the new item fields so a Bot can write them.
  • patches/ds-bot/ regenerated from core/ by hand (no git on this host): files/ mirrors the four FlyBot-added files, 0002 carries the tools.js schema addition; node scripts/ds-bot-patches.mjs check passes (core/ equals ds-bot/ + patches/ds-bot/).
  • docs/subsystems/core-host.md: a new section states the contract, the rework write and the lease rule; changelog.d/1720-typed-task-contract.added.md carries the user-visible entry.

Tests

core/test/tasks.test.js '#401' now asserts the one original card plus exactly one rework card beside it, which is the behaviour #1720 asks for.

New core/test/task-contract.test.js (6 cases): a handoff records from/to/reason and an unknown role is refused by name; a bad taskClass is refused by field name; a rejection creates exactly one rework child with reworkOf and no parentId; a rejection replayed after a restart adds no child and the original reaches result-ready once; an expired lease requeues while a live lease refuses a second claim naming the holder, across a restart; a pre-slice record reads back and renders; the Project board row carries the fields or keeps its old shape. Red first: with core/src/host/{tasks,project-board,tools}.js reverted to the base blobs, 5 of the 6 cases fail; restored, 6/6 pass.

Risk

T2/T3 by the host task-store write path and the board read model, as the issue states (controller computes the tier). The change is additive: existing transitions, group-card settling and the queue's claim winner are untouched, and the only behaviour a stored old card can newly meet is expireLeases, which needs a lease it does not carry.

Not run

npm run verify end to end: this host has no git binary, so arch-docs (which diffs origin/main..HEAD) cannot run here, and no python3 for test/usage-fields.test.js — both fail in the environment on any branch, not on this diff, and CI runs them on the pull request. The rest of the core suite was run directly (node --import ./test/harness-resolve.mjs --test --test-concurrency=2 'test/*.test.js'): 2047 tests, 2045 pass, and the 2 failures are exactly those two environment checks. The four plugin suites are green (social, tree, timeline, workflow); check-pack, testing-layers, check-mojibake, changelog and the self-test catalog checks also pass locally. core/client.js and patches/ds-bot/ are untouched (no client or upstream source changed), so no regeneration was needed.

{
  "pr": 1732,
  "claim": "Chief",
  "issue": 1720,
  "summary": "Five additive, write-validated task fields: taskClass, reworkOf, handoff, evidence and lease, with one writer reached from track_tasks, task-create and task-update. A rejection opens exactly one rework child linked by reworkOf (never parentId) and the child's acceptance is the one write that returns the original to result-ready. A claim takes a lease: a live lease refuses a second claim naming the holder and an expired one requeues the card. The Project board reads the same row, so a pre-slice record is unchanged.",
  "files": ["core/src/host/tasks.js", "core/src/host/project-board.js", "core/src/host/tools.js", "core/test/task-contract.test.js", "core/test/tasks.test.js", "docs/subsystems/core-host.md", "changelog.d/1720-typed-task-contract.added.md"],
  "checks": { "ran": "TZ=UTC npm --prefix core run lint; TZ=UTC node --import ./test/harness-resolve.mjs --test test/task-contract.test.js", "result": "green", "at": "29758029c46995f7c14d9390cb7449d64145472f" },
  "next": "none — ready for review; the full-suite result follows in a comment"
}

@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 29, 2026
@Birfy Birfy self-assigned this Oct 29, 2026
…ence and lease

The task record gains five additive fields, validated on write by one function and
reached from track_tasks, task-create and task-update: taskClass, reworkOf, handoff,
evidence and lease. A refusal names the field and leaves the card as it was.

A review rejection records the handover, moves the original to working and opens
exactly one rework child linked by reworkOf (never parentId, which keeps its
part-of-overall meaning). The child's acceptance is the one write that returns the
original to result-ready, guarded so a replay moves it once. A claim takes a
30-minute lease: a live lease refuses a second claim naming the holder, an expired
one returns a queued card to the queue. The Project board row carries the new
fields when the card has them, so a record stored before this slice reads back
unchanged.

Closes #1720. Part of #1550.

patches/ds-bot/ regenerated from core/: files/ mirrors the four FlyBot-added files
and 0002 carries the tools.js schema additions.
@Birfy
Birfy force-pushed the dev/1720-typed-task-contract branch from f405ec8 to 2975802 Compare October 29, 2026 18:43
@Birfy

Birfy commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 2975802
Written-by: Reviewer
Attestation: 2975802 role=Reviewer

Ran at this tip:

$ node --import ./test/harness-resolve.mjs --test test/task-contract.test.js test/tasks.test.js
# tests 37  # pass 37  # fail 0
$ node --import ./test/harness-resolve.mjs --test test/task-contract.test.js test/tasks.test.js
# red first, core/src/host/{tasks,project-board,tools}.js reverted to base 54dea30512b0
# tests 37  # pass 31  # fail 6
# not ok 1 a handoff records from/to/reason and an unknown role is refused by name (#1720)
# not ok 2 a rejection creates exactly one rework child with reworkOf and no parentId (#1720)
# not ok 3 a rejection replayed after a restart adds no child and returns the original once (#1720)
# not ok 4 an expired lease requeues and a live lease refuses a second claim naming the holder (#1720)
# not ok 6 the Project board reads the typed fields from the same row (#1720)
# not ok 34 the requester sends a result back with a note and the same task returns to the Bot (#401)
$ node .github/scripts/changelog.mjs check
# changelog: 2 fragment(s) in changelog.d/ are well formed
$ node .github/scripts/risk-gate.mjs --pr 1732
# risk-tier: T3; basis: core/src/host/tasks.js=T2, core/src/host/project-board.js=T2, core/src/host/tools.js=T2

Restored to the tip blobs afterwards, the same 37 tests are 37/37 green (the run above), so the tests do fail on the change and pass with it.

Review

Scope, from compare 54dea30…2975802: exactly 12 files — core/src/host/{tasks,project-board,tools}.js, core/test/{task-contract,tasks}.test.js, docs/subsystems/core-host.md, the changelog fragment and the four patches/ds-bot/ mirrors (plus 0002). No unrelated core/ or patches/ds-bot/ change; each patches/ds-bot/files/ mirror is byte-identical to its core/ source, so the regenerated patch is not a hand-edited copy.

The contract is additive and single-writer: writeContract reads taskClass, reworkOf, handoff, evidence (capped at 8) and lease before assigning any of them, refuses by field name and by unknown role, and is reached from track_tasks, task-create and task-update. A rejected new card leaves no row. kind === 'group', parentId and settleOverall keep their meaning: a rework child carries reworkOf and never parentId, a replay reuses the still-open child, and closeRework is guarded by the original still being working. expireLeases runs before a claim reads the queue and writes the requeue on the card; a live lease refuses the second claim as bot/lease-held with the holder named. project-board.js reads the five fields from the same state.tasks row, so a pre-slice card keeps its old row shape — case 5 passes on base too, which is what backwards compatibility should look like.

The two collisions the Chief resolved in #1720 are respected: no kind enum extension and no parentId reuse.

Body/handoff: Owner: Chief and Reviewer: Reviewer name different Bots; the ## Checkpoint block parses to zero refusal reasons (pr, claim, issue, summary prose, 7 files in the diff, checks.result: green at this sha, next).

GitHub: 14 check-runs on this tip, all completed; only enqueue-guard is red, and its refusal is no Verifier report names the tip 29758029c469 — this PASS is the report it asks for. No conclusion existed at this tip before this one.

Not run here: npm run verify end to end (no git binary on this host, so arch-docs cannot diff), as the body states; CI tests-ok is green at this tip.

@github-actions github-actions Bot added the pass label Oct 29, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 80709a7 Oct 29, 2026
14 checks passed
@Birfy
Birfy deleted the dev/1720-typed-task-contract branch October 29, 2026 18:59
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant