Repository navigation
Releases: marcus/tasks
Release list
v1.19.0
Features
- Find the tasks that carry a link. (#37)
GET /api/v1/tasks?link=<url>keeps the tasks whoselinks— formal, title, or body — include that URL, and composes withscope(archivedandallincluded) and every other filter, so "does this Slack thread already have a task?" is one request instead of paging every task. The CLI twin istasks links --url <url>(with--allto reach done and archived tasks). Both match through one rule,links.MatchKey: scheme and host case, a leadingwww., a default port, the fragment, a trailing slash,utm_*/fbclid/gclid, and Slack'sthread_ts/cidon a Slack permalink are ignored, percent-escapes are decoded, and the remaining query is compared order-insensitively; a query that addresses a resource (?p=/notes/review.md) still distinguishes links./metaadvertisescapabilities.link_filter. - Links into local documents can be classified as
doc. (#38) The newdoc_link_patternsconfig (whitespace-separated whole-URL patterns,*wildcard — for exampledoc_link_patterns = http://127.0.0.1:8080/open?p=*) classifies matching links as thedocsystem ahead of any host row, sotasks links --system docand the API'slinks[].systemtell "where this task came from" apart from context links without client heuristics./metaandtasks configpublish the patterns. The classifier also namesfile:URLs and relative.md/.txtpathsdoc, but stored and extracted links remainhttp/httpsonly, so today a document link reaches a task through a pattern-matched web URL.
Behavior changes
GET /api/v1/tasks/{id}finds an archived task. (#36) Withoutsource, the read now falls back to the archive when no live task has the id, returning the resource withsource: "archive", so a client resolving a cited id no longer pages the archived scope. A client that read a404there as "this task left the live file" should checksourceinstead. The live task still wins when both share an id, and an explicitsource=liveorsource=archivestays exact. Writes keep refusing archived tasks./metaadvertisescapabilities.archive_fallback.
v1.18.0
Features
- The Task resource carries its lifecycle dates, quadrant, and agent readiness. (#34)
GET /api/v1/tasks/{id}and every task row now includecreated(the day from theCaptured [YYYY-MM-DD].note a capture writes — the store keeps no separate creation stamp, so a body edit that removes the note reports null),updated(the UTC instant of the record's stored last-write stamp, null when it has none),archived_on(the day an archived task's subtree was swept, inherited from the stamped subtree root),quadrant(Q1–Q4from the same classifier and configuredurgent_daysastasks quadrants, null for proposed, closed, and archived tasks), andagent_ready(the exact rulescope=agent_readyandtasks list --agent-readyapply, now one shared function).archived: trueis unchanged. A client can sort by recently touched and render a detail pane without re-deriving any of these rules. - Activate a task over HTTP. (#28)
POST /api/v1/tasks/{id}/activateistasks activatefor API clients: no body, mandatoryIf-Match, the task back with its new ETag, one undo step. It runs the same application operation as the CLI (which now goes through it too), so it keeps a past available-from date, clears a future one, and on a lead or recurring task releases just this occurrence — none of whichPATCH {deferred: false, scheduled: null}could express. A web client's defer control now has all three of the TUI's choices./metaadvertisescapabilities.activate. - Tag and note deltas on
PATCH. (#29)add_tags,remove_tags,add_contexts,remove_contexts, andappend_bodysend the store's own deltas — the onestasks tagandtasks noteuse — instead of making a client read, edit, and resend the whole list or body. They are refused (422) alongside the fields they edit. A PATCH made only of deltas may sendIf-Match: *, so a note appended from a web form lands beside an agent's concurrent priority edit instead of failing412; a real revision keeps its whole-task meaning, and*on anything else — a PATCH that replaces a field, or any other task write (activate, delete, the delegation actions) — is428. A blank or whitespace-onlyappend_bodyis refused, astasks noterefuses it. Appending is now a store operation (body_append) composed under the lock, whichtasks notealso uses. A PATCH that only nulls dates now runs the store's single date-clear operation, the onetasks undateuses. Each delta PATCH's undo step is labelled like the CLI verb it duplicates (note: …,tags: …,remove deadline: …), inGET /historyas intasks history./metaadvertisescapabilities.patch_deltas. - Writes say what else they changed. (#33) Task write responses carry an optional
meta.effects:rolled: {from, to}when completing a recurring task rolled it forward instead of closing it, andtouched_idswhen the write changed tasks besides the one it returns — a completion that cascaded DONE over open descendants, or a move that carried descendants with it. Both come from the store's own report of the transaction, not from comparing reads.DELETE ?cascade=truenow answers200with{deleted: [...]}— every removed task, root first, as it stood before the delete, the shapetasks delete --jsonreports; a plain delete still answers204. - The API routes the manager endpoints. (#25) A web client can now do what the TUI's
u,ctrl-randxdo without shelling out.GET /api/v1/historypeeks the next undo and redo labels with thestore_revisionthey apply to, andPOST /api/v1/history/undoand/redorequire that revision back, refusing with409 conflict(details.reason: stale_store_revision, with the current revision) when any surface wrote in between, so a stale browser tab can never undo someone else's edit; an exhausted journal and an out-of-band edit answeremptyandjournal_conflict.GET /api/v1/archive-previewlists the roots, descendants and every id a sweep would move, each blocked root with its open children, and a fingerprint;POST /api/v1/archive-sweepsmoves them only while that fingerprint still holds, and every refusal carries the CLI's ownreasonword. The CLI gains the same two steps:tasks history [--json],undo/redo --store-revision REV,tasks archive --dry-runandarchive --fingerprint FP, with documents byte-identical to the HTTPdata. The CLI and OpenAPI vocabularies are reconciled on the CLI's names ({roots, records, moved_ids}, ablockedrow list),undo --jsonaddsstore_revision, and a journal conflict now carriesreason: journal_conflict./metaadvertisesundo,redoandarchive_sweepas true. - Change detection over HTTP is cheap, and can be pushed. (#26)
GET /api/v1/metaanswersIf-None-Matchwith304and no body, deciding from a digest of the two files without parsing, validating or snapshotting the store, and is servedCache-Control: no-cacheso a browserfetch()revalidates on its own. Its ETag is opaque and covers the document as well as the store — the revision joined to a digest of the process's configuration, build and capabilities — so restarting the API under a different zone, date order,urgent_daysor link config never answers a stale304;meta.store_revisionstays the plain revision.GET /api/v1/eventsis live: a Server-Sent Events stream ofstore.changedframes carrying the newstore_revision(also the event id, so a reconnectingEventSourceis not told what it already knows), with a first frame on connect, a keep-alive comment when idle, at most eight open streams (503,reason: stream_limit,Retry-Afterbeyond that), and a clean end on client disconnect or server shutdown.capabilities.eventsis true. - The TUI's tabs are readable over HTTP. (#23)
GET /api/v1/views/{agenda,next,quadrants,inbox}is now routed. Each returns the full Task resource for every row (revision included) in the view's canonical order, plus a definedgroupslayout that references those rows by id: the agenda'soverdue/today/tomorrow/laterday buckets in the server's zone, onenextgroup per@context(a two-context task in both) with(no context), all four quadrantsQ1..Q4with their headings, and the intake tab's two blocks — Approvals in triage order, then the accepted Inbox — each grouped by project in the Projects view's order with unfiled last.include_unavailable=truemirrors the TUI'sZtoggle. The selection and grouping rules moved out of the TUI intointernal/taskquery, so the tabs and the route are computed by the same functions and a web client no longer ports them./metaadvertisescapabilities.views, an unknown view name is a 404 that lists the routed ones, and an unrouted method or path now says "No endpoint matches that method and path." instead of "No task with that id." - An outline read, and section actions on every section. (#24)
GET /api/v1/views/outlinereturns the Outline tab as one flat DFS list of nodes — sections and tasks interleaved in file order, each with its shown parent and depth — so a client can rebuild the tree exactly, including where a project's tasks sit relative to its sub-sections. Section nodes carry the section's role, note, lifecycle and task tallies (shown, open, closed, and the· N closedcount the toggle is hiding); task nodes carry full Task resources;include_closed=truemirrors the TUI'sCtoggle, and proposals stay out as they do in the tab.GET /api/v1/sectionsnow carrieskind,body,stateandclosedtoo. The project action routes — rename, complete, drop, reopen, archive — accept any section the Outline shows, not only what/projectslists, so a nested sub-section or an area with no open work is no longer a 404 over HTTP while the TUI can act on it; the Inbox and the Projects heading refuse rename, close and archive with a 409 instead, and reopen always works. That refusal lives in the application layer, so the TUI's outline rows now say why instead of renaming or closing either one, and a section action refused because the store is locked or invalid says so rather than reporting the project gone./metaadvertisescapabilities.outline. - Friendly dates parse over HTTP without a write. (#27)
GET /api/v1/dates/parse?input=fri%204pmreads date text with the same parser the TUI's date fields use — friendly date, optional wall time, optional trailing zone word (Europe/Berlin,UTCin any case,floating) andfold=later— and answers{"input","date","time":{"local","timezone","fold"}|null,"human"}, wheretimeis exactly the object aPATCH/POSTtakes asdeadline_time/scheduled_time. Text that is not a date is a200with{"input","error"}, like/recurrence/explain./recurrence/explaingainsanchor=YYYY-MM-DDto project from a task's own stamp instead of today, andGET /api/v1/lead/explain?input=3%20weeks&anchor=2026-11-01previews a lead span (canonical,human, and theopensdate) before any write./metapublishesdate_order. The CLI twins aretasks due --explain "<text>"(alsoschedule --explain) andtasks lead --explain "<span>" [--anchor <date>], whose--jsonis the HTTPdatamember. The zone/fold word grammar moved out of the TUI intotemporal.ParseText, which the CLI's dated verbs now use as well, sotasks due <ref> "fri 4pm Europe/Berlin"works and a zone or fold named both as a flag and as a word is refused./metaadvertisescapabilities.date_parseandcapabilities.lead_explain. The TUI's date fields now honor the configureddate_order— they read10/2as October 2 regardless of it before, and mistook a numeric date like7/15for a zone name. - Link shorthands and title-URL lifting work on every surface. (#30) Expanding a configured
link.<name>shorthand (jira:OPS-1234) and lifting a trailing URL out of a capture title into a formal link used to be `tasks c...
v1.17.0
Features
- A project has an open/closed lifecycle of its own. (#22) Closing a project used to close its tasks and leave the section itself looking open forever.
tasks project complete <ref>now stamps the section DONE with today's date in the same write that cascades DONE over its open descendants, and two new verbs finish the cycle:project drop(aliasproject cancel) does the same with CANCELLED, andproject reopenclears state and closed date from the section while leaving its tasks untouched. The lifecycle is open, then closed, then archived, each step explicit: a closed project stays in the live file untilproject archivesweeps it.tasks projectsexcludes closed projects by default,--closedselects only them,--allshows both, and a closed project is never reported as stuck.--jsoncarriesstateandclosedwhen present, and a project ref resolves against closed projects too. Over the API,GET /api/v1/projectstakes?closed=exclude|include|only, andPOST /api/v1/projects/{id}/dropand/reopenjoin/complete. In the TUI, the Outline and the Projects tab hide closed projects transparently, so depth is unchanged and an open descendant hoists through a closed parent, and the existing session-onlyCtoggle reveals them in place with the same muted styling closed tasks use. The detail rail now names the task's project in every view.
v1.16.0
Features
- Delegation stamps read in your timezone. (#17) Every human-facing print of
delegation.at—tasks show's(since …), the TUI detail pane, and the claim-conflict message — now renders in the configuredtimezoneandtime_format(thu 08-27 5:46p,17:46under 24-hour) instead of the raw UTC stamp. The year appears only when it is not the reader's own, so an old handoff can't masquerade as yesterday's. Storage,--json, and the API keep the exact UTC instant, and a stamp the validator would reject prints as stored rather than being laundered into a healthy-looking local time. The 12/24 rule now has a single implementation (temporal.ClockLabel) shared bytasks list. - The Outline hides finished work behind
C. (#18) DONE and CANCELLED rows no longer bury the tree: the Outline shows open work by default, andC(palette: toggle closed view) reveals the closed rows in place with their existing styling. Hidden rows are transparent — an open child hoists through a hidden closed parent — and every trace stays honest: section badges read4 · 1 closed, a search or store whose matches are all closed saysN closed hidden · C showsinstead of rendering blank, and malformed states stay visible rather than being miscounted as closed. Reordering and indenting now anchor on the nearest visible sibling, so a keypress can never rewrite the file without moving anything on screen. PROPOSED stays excluded,Zcomposes independently, and the toggle is session-only. - Projects can show what already shipped. (#19) The same
Ctoggle on the Projects tab includes DONE and CANCELLED children under their project. Toggled on, the stored tree is shown as-is — a closed parent appears with its open child nested beneath it instead of being pruned and hoisted — and a project whose only live children are closed becomes a real foldable heading rather than a line on the dormant tail. A finished project drops the⚠ stuckwarning, badges count what the current filter shows with a muted· N closedleftover, and unfiled work and sweptarchive.jsonlhistory stay out. Default off: the open-only view, hoisting and all, is unchanged.
v1.15.0
Features
Nmarks the selected task NEXT. (#16) One keypress in the list or detail view now does what previously took the edit form ortasks state <ref> NEXT: the selected open task becomes a GTD next action and appears on the Next tab, in a single undo step. The key sits in the?help and the:palette, refuses politely on proposed and done tasks, and is a silent no-op when the task is already NEXT. Lowercasen(modal cancel) is untouched, and pressingNinside any modal, filter, or form field types or cancels as before — it can never mutate state from an input context.- An empty Next tab explains itself. (#15) When nothing is marked NEXT, the tab no longer renders a blank pane that looks broken next to a full Agenda. It now says there are no explicit next actions, how many dated items are waiting on Agenda (the count comes from the same query the Agenda tab paints, so the two can never disagree), and how to mark one —
N, ortasks state <ref> NEXT. The CLI matches:tasks nextwith no next actions prints a one-line message instead of silence, mentioning Agenda only when dated work actually exists there.tasks next --jsonstill prints[], unchanged. - The Inbox tab groups intake by project. (#13) Approvals and accepted Inbox rows are now bucketed under project headings inside their sections, so a triage pass stays inside one theme instead of hopping between projects in file order. Group order follows the Projects tab's sequence, unfiled one-offs collect in a single trailing group, empty groups vanish, and headings are chrome — unselectable, invisible to the a/r approve-reject walk, and counted by task (the section badge's own rule) so folding a subtree never shrinks a heading. Within a group, approvals keep their priority-then-due triage order. CLI
tasks inboxstays flat. tasks movefiles proposals. (#14) A PROPOSED task can now be moved into a project — positional section,--under,--before, or--top— instead of the old reject-and-re-propose dance that minted a new id and polluted rejection history. Filing is a location change, not a decision: the task stays PROPOSED, and destination refs may name a proposal only when the moving subtree is itself proposed, so accepted work still can't nest under an undecided proposal. This matches whatPATCH /api/v1/tasks/{id}always allowed; the API is unchanged.
v1.14.0
Features
- The TUI's modals adopt Sidecar's design language. The delegate modal is the first surface rewritten on the new chrome primitives: boxed form fields instead of bare lines, buttons as real interactive surfaces, a fixed footer that stays put as content scrolls, and full scrollbars whose thumb shows where the window sits in a long note. The chrome is built from named theme slots (
internal/tui/term/theme), so an embedding host — Sidecar today, anything else later — projects its own palette into them through the existingpkg/tui.ThemeOptionsseam rather than having colors baked into the renderer. - Scrollbars are draggable. Grab the thumb and it tracks the pointer, preserving the offset you grabbed it at; click anywhere on the track to jump there. The math is state-free and ported from Sidecar's implementation, so the two behave the same way under the same gesture.
Bug Fixes
- Clicking just left of a button no longer invokes it. Button hit spans were computed one cell left of where the buttons were painted, so the blank column beside a button was live — on the delegate modal that meant Release and Undelegate could fire from a click that visually missed them. At narrow widths the clamp was applied to already-shifted coordinates, so a button truncated off the screen entirely stayed clickable, which is precisely the case the clamp exists to prevent.
- The ctrl-s hint names what ctrl-s does. The chip read "newline in note" while ctrl-s submits and Return is what inserts a newline, so following the on-screen hint to break a line delegated the task instead.
- An upward scrollbar jump lands where you clicked.
ScrollToRowalways parked the caret on the window's bottom edge, but the renderer derives the offset from whichever edge the caret crossed, so jumping upward settled height-1 rows low and stayed there.
v1.13.0
Dependencies
- Tasks now builds with Go 1.27.
encoding/json/v2is the default JSON
implementation in this release; Tasks' stored task data, config, and API
payloads round-trip unchanged, andGOEXPERIMENT=nojsonv2remains as an
escape hatch. Thetestdata/external-tui-consumermodule moves with it, so
the embedded-host contract keeps compiling against the same directive as the
library it consumes. - This release exists so the td / Tasks / Sidecar trio share one toolchain:
Sidecar resolves its CI Go version from its owngo.mod, so it can only pin
a Tasks that declares a directive its toolchain already satisfies.
v1.12.0
- Embedded hosts can compose Tasks' list and detail regions into their own
focus rings. The publicpkg/tuiAPI now exposes stable spatial-focus IDs,
the exact rectangles from the layout Tasks renders and hit-tests, the current
region, direct focus control, andTabOwnsFocus. The README documents the
Sidecar capability check and the boundary between spatial focus and Tasks'
existing interaction contexts. - Inputs and overlays keep ownership of Tab, including field modals, while
passive list/detail and response views can yield it to an outer host. Compact
layouts expose only positive rendered focus stops; hidden, unknown, or
input-covered targets refuse direct focus without changing selection. Keys
and clicks follow the same spatial focus, and project-detail panels retain
list command routing so task-detail actions cannot run against a project row.
v1.11.0
-
Dis a three-part modal instead of a one-line prompt. Delegating now
asks the three questions a delegation actually has — who (a person or the
agent pool), which mode, and the note the receiver works from — on a modal
that is complete by keyboard and by mouse. Re-delegating prefills all three
from the current marker, and one submit writes them in a single undo step.
Release and undelegate are explicit affordances, and both confirm before they
act, which retires the old flat word grammar:implement,
releaseand the clear words used to share one text field, so the shortest
inputs performed the widest and most destructive actions and a three-letter
prefix minimum was the only thing standing between a keystroke and a revoked
claim. Typing a verb into a text box is no longer how work is taken away. -
The three-part delegation reaches the CLI and HTTP.
tasks delegate <ref> <mode> --note "…"writes who, in what mode, and the briefing the receiver
reads in ONE store write and therefore one undo step — no surface composes a
delegation out of a delegate followed by a note write.--note-file <path>
and--note-file -(stdin) exist because a long briefing written by an agent
should not have to fight shell quoting. A mode is now accepted alongside
--to <email>, so a person can be asked for a refine just as an agent can.
Omitting the note keeps any existing briefing — restating the mode must not
silently erase instructions — and--note off/none/""clears it, the
same two words a work reference clears with and both already reserved mode
names, so a clear instruction can never be read as a mode.tasks delegate <ref> --note "…"with no mode and no--torewrites the briefing in place,
for an owner correcting instructions without re-sending a delegation. -
Delegation reads show all three parts.
showprints the mode and the
briefing in full,list --delegatedandlist --agent-readyprint the mode
in the headline with a one-line preview of the note beneath it, and--json
carries both as first-class members of thedelegationobject everywhere —
includinglist --agent-ready --json, which is what an agent heartbeat
reads. -
The HTTP delegation writes work.
POST /api/v1/tasks/{id}/{delegate, undelegate,claim,release},PUT …/work_ref, and the newPUT …/delegation_noteanswered501because the mandatoryIf-Matchcould not
be honoured — the store did not compare a revision inside its delegation
transaction, and dropping a precondition a client set is worse than refusing.
It does now, under the same lock the write runs in, so the routes perform the
operation with their preconditions intact:200with the whole canonical
task and a freshETag,412on a stale revision, and409on a lost claim
race carryingholderandatas their own fields rather than as prose an
agent would have to re-parse. -
Answer a proposal for work that is already done in one keystroke. On a
PROPOSED rowcnow approves the proposal AND completes the accepted task in
a single write and a single undo step, soundorestoresPROPOSEDexactly;
rejecting was previously the only one-key way to clear such a row, and it
recorded a decline — the wrong history, and the wrong signal to whoever
proposed the task. The store still never completes a PROPOSED task: the
approval lands first inside the same transaction.tasks approve <ref> --doneandPOST /api/v1/tasks/{id}/approve?complete=trueare the CLI and
HTTP spellings of the same shared command, both--json/ETag-honest. The
proposal row's key hints now describe what the keys actually do. -
Refuse a recurring proposal in
check. Recurrence is a schedule for accepted
work — completing a recurring task rolls it forward instead of finishing it —
and no write path could ever produce the shape, but a hand-edited, repaired,
or foreign-device file could, and it passed validation. Nowrecuron a
PROPOSEDtask is acheckerror, and approve+complete refuses it rather
than reporting a DONE that did not happen. -
A delegation becomes three orthogonal parts — who (a person or the agent
pool), mode (refine/research/implement), and a new optional
note, free text briefing the receiver on how to work on the task and
where the work should land, up to 2000 characters with paragraphs allowed.
Store and schema support only in this entry; the CLI, HTTP, and TUI
spellings follow in the surface work. The note is stored last in the marker,
so a store written by an earlier release loads, checks clean, and re-emits
byte for byte. A mode is now valid on a human delegation as well: "Pat,
this is a refine" is a thing the data model can say. The note travels with
the delegation it briefs — kept across a mode change or a new assignee of the
same kind, dropped when a person replaces the pool or the reverse — carries
its own transition stamp on an unclaimed delegation so the most recent
briefing wins a multi-device merge (a note on a live claim leaves the claim's
own stamp alone, so briefing a working agent survives a sync), and merges
atomically with the rest of the marker, whereundelegatestill always wins. -
The mode vocabulary is carried as a value on the store and checker options
rather than checked against a literal, so configuring the set later is one
field, not a hunt through the code. -
The delegation mode vocabulary is yours to choose.
delegation_modes = triage, shipin~/.config/tasks/config(orTASKS_DELEGATION_MODES)
replacesrefine, research, implementeverywhere at once: whattasks delegate <ref> <mode>accepts, what a refusal quotes back, thedelegate
usage line,tasks help, the TUI?overlay and delegate modal, the check view,
and the API's vocabulary document. A mode is a bare lowercase word and
carries no label — the label is the word. With no config key, behaviour is
exactly what it was. -
A
delegation_modeslist this binary cannot honour — a word of the wrong
shape, a duplicate, a reserved word, an empty list — is ignored whole, with
a warning on stderr naming the problem and the set actually in use. Keeping the readable
half would run you against a vocabulary you never wrote. Nothing about a bad
list makes the store unreadable or unwritable. -
A record whose
modethe active vocabulary does not list still loads, shows,
and checks:checkreports it as a WARNING, never an error. Which words are
listed is a setting, and settings change — editingdelegation_modes, or
syncing a file from a machine configured differently, can no longer invalidate
a file your tasks live in. A stored mode of the wrong shape is still an
error, and writing an unlisted mode is still refused. -
tasks configandtasks config --jsonreport the resolved
delegation_modesand where they came from, alongside every other setting. -
off,none,clear, andreleaseare reserved and cannot be configured as
modes:releaseis a delegation verb and the others clear a work reference or
a note, so a mode spelled like one of them would make an instruction and a
mode indistinguishable wherever both are written in one place. The collision
is reported when the config is read, not at the moment the verb is needed. -
Hosts embedding the TUI through
pkg/tuigetExportCommandsWith(modes)to
describe delegation with their own store's vocabulary; plainExportCommands
now renders the built-in set rather than an unfilled template.
v1.10.0
- Accept the relative dates people actually type. Anywhere a date is taken —
capture,propose,due,schedule,defer, the TUI edit form, and the
API —two weeks,in two weeks, andtwo weeks from nowall work, in
digits or English number words, over seconds, minutes, hours, days, weeks,
months, or years. Calendar units also take compact forms:2d,2w,2m
(months),2y. Second-, minute-, and hour-relative phrases produce a timed
value from the current instant; since stored times have minute precision, a
result carrying seconds rounds up so it never lands earlier than the
boundary that was asked for. Such a phrase already names an exact side of a
DST overlap, so pairing it with--foldis refused rather than silently
resolved.next week/next month/next yearkeep working as the one-unit
spellings. - Make releasing one command:
BUMP=major|minor|patch make releasederives the
version from the latest tag, stamps## [Unreleased]with the version and
date, commitsrelease: prepare vX.Y.Z, pushesmain, and publishes — the
version is stated once.RELEASE_VERSION=vX.Y.Z make releaseand a hand-
stamped heading still work;make release-dry-runprints the plan, verifies
the Homebrew publication prerequisites, and stops before any mutation. Every
existing fail-closed publication check is unchanged.