Skip to content

merge: feature/personal-tasks → preview#10

Merged
JOBYINC merged 33 commits into
previewfrom
feature/personal-tasks
May 19, 2026
Merged

merge: feature/personal-tasks → preview#10
JOBYINC merged 33 commits into
previewfrom
feature/personal-tasks

Conversation

@JOBYINC
Copy link
Copy Markdown
Owner

@JOBYINC JOBYINC commented May 19, 2026

Summary

Brings the full `feature/personal-tasks` line into `preview` for deployment. ~28 commits spanning multiple feature epics that have stacked on this branch since it last synced with preview.

Major epics rolled up

  • Personal projects (Asana-style "My Tasks") — `a7f1039e66` and surrounding work. Per-user private bucket, lazy-created on first web visit. Migration `0126_project_personal`. Verified live before this PR.
  • Custom fields token API — Task1 Inc1-3 (`981b940b33`, `78ed0516ff`, `9d29658a0b`): public read + write + CRUD endpoints for custom field defs/values. `external_source` + `external_id` idempotency.
  • List view ergonomics — Inc B1/B2, frozen first column, header restyle, sort-by-custom-field.
  • UI cleanup — Asana-style solid-fill pills, Tabbed nav default, favorite quick-menu toggle, Community badge removal, first-run tour banner removal.
  • System-identity personal-tasks endpoints (PR feat(api): system-tier personal-tasks + assigned-work-items endpoints #9) — `/personal-tasks/` POST+PATCH + `/assigned-work-items/` GET under `IsSystemToken` gate. Migration `0128_apiactivitylog_on_behalf_of` adds audit column. 28 contract/unit tests.

Deploy notes

  • Two new migrations apply: `0127_workitemfield_external_ids` (already on preview? double-check) and `0128_apiactivitylog_on_behalf_of`. Both are nullable column additions + indexes — forward-only, safe.
  • After deploy, agent ops can switch Tom/GTM/HR to `is_service=True` system tokens per the cutover doc in the agent repo (`agents/task-manager/tick-feature-spec-system-identity-personal-tasks-CUTOVER.md`).
  • No breaking API changes — additive only.

Test plan

  • Migration apply succeeds on preview DB
  • Existing per-user token flows still work (regression)
  • My Tasks bucket sidebar entry still renders for logged-in users
  • Custom fields read/write via token API works
  • (Post-seed) system token can POST to `/personal-tasks/`

Marcus Cheung added 30 commits May 16, 2026 22:16
Re-based onto the lark-stable line (this branch builds the production
ghcr.io/jobyinc/plane-*:lark-stable images). Same fix as preview PR #3
(commit 1cb3908): replace the non-durable 25h Redis dedup in
lark_due_reminder_task with a durable LarkDueReminderLog row claimed
atomically via get_or_create on a unique
(issue,receiver,stage,reminder_date) constraint; claim released via
hard delete on failure so retry still works. Mirrors
EmailNotificationLog + WorkspaceMember soft-delete-unique idiom.

Migration = 0123_larkduereminderlog depending on
0122_automationrule_automationrulerun (this branch's actual head; it
has no 0123_workitemfield — that is custom-fields/preview only).

Verified locally pytest 3/3 on the source branch. Repo CI runs no
python test suite. Effect requires this image rebuilt + droplet pull +
the 0123 migration applied.
…rly repeat DMs)

Root cause: execute_rule_on_issue deduped only via a Redis key with
DEDUP_TTL_SECONDS=5min, but evaluate_scheduled_automations_task runs
HOURLY. 5-min TTL << 60-min cadence => the key was always expired by
the next tick, so a due_soon rule re-fired for every in-window issue
every hour (and didn't survive cache loss), re-sending the
"You've been assigned"/escalation card dozens of times. The
AutomationRuleRun table was audit-only, not consulted for dedup.

Fix: for the scheduled due_soon path only, add a durable gate — skip
if a prior SUCCESS AutomationRuleRun already exists for this
(rule, issue). Each due_soon rule now notifies an issue exactly once,
durably (survives Redis loss/restart by construction), mirroring the
lark_due_reminder durable-dedup approach. Reuses the existing
AutomationRuleRun ledger — no new model/migration. NOT bypassed by
bypass_dedup (once-per-issue is a hard requirement; re-saving a rule
must not re-DM notified issues). Event-driven rules don't set
trigger_type=="due_soon" and keep the short Redis burst-dedup —
unaffected.

Verified: manage.py check 0 issues; gate present in live source.
CI runs no python suite; authoritative proof is post-deploy
AutomationRuleRun (SKIPPED_DEDUP, no repeated SUCCESS per rule+issue).
…ze migrations

Option A: integrate custom-fields (PR #2, on preview) into
feature/lark-oauth-provider so the prod lark-stable image carries the
lark fixes + custom-fields as one release.

Clean auto-merge (0 file conflicts). Migration graph linearized:
- removed preview's duplicate 0124_larkduereminderlog (same
  LarkDueReminderLog model is already created by 0123_larkduereminderlog,
  which is applied in the prod DB)
- renamed 0123_workitemfield_workitemfieldoption_workitemfieldvalue ->
  0124_workitemfield_workitemfieldoption_workitemfieldvalue, dep
  re-pointed 0122 -> 0123_larkduereminderlog
Result: linear 0122 -> 0123_larkduereminderlog -> 0124_workitemfield.
makemigrations --check clean; manage.py check 0 issues; automation
durable-once gate + lark_due_reminder durable dedup both intact.
…erlog

The merge commit e93e94f renamed preview's workitemfield migration to
0124 but left its dependency at 0122_automationrule_automationrulerun.
That made 0124_workitemfield and 0123_larkduereminderlog parallel leaf
nodes, so the prod migrator crash-looped:

  CommandError: Conflicting migrations detected; multiple leaf nodes
  (0123_larkduereminderlog, 0124_workitemfield...)

Repointing the dependency linearizes the graph
(0122 -> 0123_larkduereminderlog -> 0124_workitemfield) so the migrator
applies cleanly and the work_item_fields tables get created.
Asana-style list: drag a built-in column header's right edge to resize;
width persists in display_filters.view_column_prefs (rides existing JSON
— no schema migration, per-user, survives reload + syncs across devices).
getListGridTemplateWithCustom takes an optional widths override keyed
uniformly by built-in or custom column key; header + rows realign via
the shared --list-cols var. Commit-on-pointer-up (live drag preview +
custom-column resize + Asana borders/blue indicator follow in 1b).
Hover a column boundary -> a 6px blue line marks it (Plane's primary
token is a near-black neutral, so an explicit blue is used for clear
light/dark visibility). Drag -> a 2px blue guide line spans the whole
list (bounds from the data-list-grid scroll container) and tracks the
cursor, clamped at minWidth; the column snaps to it on pointer-up and
the width persists (Increment 1 channel). Pure UI, no migration.
Subtle vertical separators between every column in the header and every
row, aligned via the shared --list-cols template. Done at the grid
container / display:contents wrapper level with a child selector
([&>*:not(:last-child)]:border-r border-subtle) so it's 3 spots not
per-cell, and the actions column gets no trailing edge line (Asana
parity). Pure UI; column gap retained (flush-line variant deferred).
getComputedDisplayFilters rebuilds displayFilters from a fixed set of
known keys on every load, so view_column_prefs (the F1/F2 per-column
order+width store) was silently dropped on every page load. Effect:
resized/reordered columns 'snapped back' to defaults after refresh.

This is why the committed F2 width feature (97574a3) only appeared
to persist — the value lived in the in-session MobX observable; a cold
reload re-fetched from the backend and this normalizer stripped it.

One-line passthrough of view_column_prefs fixes F2 width persistence
AND unblocks F1 order persistence. Backend already stores it raw
(ProjectUserPropertySerializer fields=__all__); getEnabledDisplayFilters
is identity — this normalizer was the only stripper.
F1 (4a): getVisibleListColumns honors persisted view_column_prefs.order
(default-order fallback for unlisted/added columns, unchanged when no
order set). columnOrder threaded default->ListGroup->IssueBlocksList->
IssueBlockRoot->IssueBlock (parallel to displayProperties) so header
AND rows resolve the same order and stay aligned. Drag UI to set the
order is the next increment (4b).

Title column resize: a stable TITLE_COLUMN_KEY in view_column_prefs;
the title track flexes minmax(min,1fr) by default (Asana Task-column
behaviour, no regression) and switches to a fixed px width once the
user drags it. ColumnResizeHandle now measures the actual rendered
column box at pointer-down so a drag on the flexing title starts from
its real width (equals currentWidth for fixed columns).
Built-in column headers are draggable (same pragmatic-drag-and-drop
adapter as row drag); a blue bar on the target's near edge marks the
drop; on drop the full reordered built-in sequence is persisted to
view_column_prefs.order, so header + rows realign via
getVisibleListColumns. Title column is pinned (not wrapped); the
click-to-sort menu and the resize grip still work (drag only past a
threshold; resize grip stops propagation). Custom-field column parity
(reorder/resize/gridline) is the deferred 4c follow-up.
ColumnResizeHandle on both CustomColumnHeaderCell branches (admin /
plain), persisting width into view_column_prefs.widths[customKey]. The
width-apply plumbing already existed since Inc1
(getListGridTemplateWithCustom uses widths?.[c.key]); this only wires
the grip + commit channel. Read-only views (no handleDisplayFilterUpdate)
render no grip. Parity with built-in column resize.
- 4c-2: custom-field columns get drag-to-reorder parity with built-in
  columns (separate LIST_CUSTOM_COLUMN dnd group, persisted via
  view_column_prefs.order)
- #4: custom column resize handle/blue line now reaches the column edge
  (w-full on both CustomColumnHeaderCell branch roots)
- #5: group grey bar spans the full grid incl. custom columns
- #1: group-by-card '+' sits right after the title (Asana-style)
- a11y: list group-by-card collapse row + create button converted to
  semantic <button> (matches kanban header pattern; required by
  pre-commit oxlint --deny-warnings on the staged file)
Built-in and custom columns now share ONE ordered sequence and ONE drag
group, so a custom field can be dragged before/intermixed with built-in
columns (previously two separate dnd groups that could not cross).

- list-columns.ts: TListColumnDescriptor union + getOrderedListColumns
  (interleaves built-in/custom by persisted view_column_prefs.order,
  unlisted keys fall back to default slots) + getUnifiedListGridTemplate;
  removed the now-orphaned getOrderedCustomColumns /
  getListGridTemplateWithCustom 2-group helpers
- list-header-row.tsx: single map over the unified sequence, single
  reorder handler, single dnd type
- block.tsx: single ordered cell render dispatched by kind
- default.tsx: --list-cols grid built from the unified sequence
- draggable-column-header.tsx: dropped the dndType split (4c-2 orphan)

Back-compatible: an older order array that only reordered one group
still renders identically (no migration, no jump for existing users).
Title pinned first, actions pinned last (Asana parity).
Built-in column header dropdown gains a 'Hide field' item that toggles
the column's displayProperties off. Re-show is Plane's existing Display
dropdown (native display-properties round-trip — reversible with no new
UI, persists per user).

- base-list-root.tsx: handleDisplayPropertiesUpdate (DISPLAY_PROPERTIES
  channel, mirrors handleDisplayFiltersUpdate)
- threaded List -> ListHeaderRow -> ListSortHeaderCell
- list-sort-header-cell.tsx: 'Hide field' menu item ->
  handleDisplayPropertiesUpdate({ [getDisplayPropertyKey(column)]: false })
  (mirrors the canonical display-properties.tsx toggle shape)
- i18n: common.actions.hide_field / show_field (en + zh-CN)

Custom-field columns unchanged here; their hide + re-show lands in B2.
…(Inc B2)

Custom-field columns can be hidden per user and restored from the list
UI (no Display-dropdown entry exists for custom fields, so hide must be
reversible in-context — it is never one-way).

- view-props.ts: TViewColumnPrefs.hidden?: string[] (rides display_filters
  JSON, per user, no migration)
- list-columns.ts: getOrderedListColumns gains a hidden param (excludes
  hidden custom columns from header + rows + grid template uniformly);
  getHiddenCustomColumns helper for the re-show list
- columnHidden threaded default -> list-group -> blocks-list ->
  block-root -> block (mirrors the proven columnOrder path; Inc A
  untouched)
- list-header-row.tsx: hide/show callbacks (DISPLAY_FILTERS channel),
  onHide wired per custom header, hidden list + onShow to the + button
- custom-column-header.tsx: menu now available to ALL users (Hide for
  everyone; Edit/Delete admin-only); + button became a menu listing
  hidden fields to restore (non-admins with nothing hidden = unchanged
  empty slot); header now mirrors the built-in ListSortHeaderCell
  exactly (full-width row, label + ChevronDownIcon, same tokens)
- i18n hide_field/show_field already added in B1 (en + zh-CN)

Built-in column hide stays on the native displayProperties channel (B1).
The Work item column (header cell, every row's first cell, and the
group-header card) is pinned left while the rest of the columns scroll
horizontally — Asana/spreadsheet parity, mirroring Plane's own
Spreadsheet layout sticky-first-column pattern.

- header first cell: sticky left-0 z-[4] bg-layer-1 (top-left corner
  above sticky-top header z-[3] and body sticky cells z-[1])
- row first cell: lg/md sticky left-0 z-[1], opaque bg mirroring the
  row resting/selected/dragging states; desktop-gated so the mobile
  stacked layout is untouched
- group-header card wrapped in sticky left-0 w-max bg-layer-1 so the
  title/count/+ stays pinned while the full-width grey bar scrolls
  (does not regress #5)
- bleed fix: row grid is items-center so the frozen cell was only
  content-height and scrolled cells bled through the row's py-3 band —
  added self-stretch + items-center so the opaque bg covers the full
  row height
- 20px left gap (pl-5) on all three frozen containers so the text
  isn't flush to the viewport edge; absolute select checkbox unaffected
  (positions off the padding box)
Exposes project custom fields on the public (API-token) API, which
previously had NO custom-field coverage — external integrations could
not even discover the field schema. Read-only first increment:

  GET .../projects/<id>/fields/                       list field defs
  GET .../projects/<id>/fields/<fid>/                 retrieve one
  GET .../projects/<id>/fields/<fid>/options/         select options
  GET .../projects/<id>/issues/<iid>/field-values/    one issue's values
  GET .../projects/<id>/issue-field-values/           bulk (?issue_ids=)

- mirrors the State public-endpoint pattern: BaseAPIView
  (APIKeyAuthentication) + ProjectEntityPermission + project-membership
  scoped querysets + use_read_replica
- reuses the internal serialize_field_value helper (pure model logic)
  so the public payload shape matches the internal API exactly — no
  duplicated type->column mapping
- purely additive: new serializer/view/url modules + 3 aggregator
  __init__ registrations; zero changes to existing endpoints, no new
  model/migration

Verified locally: all 5 routes flip 404->401 after restart (registered
+ whole import chain clean), control endpoint (states) still 401 (no
regression). Write paths (value upsert/clear, field CRUD) land in
Inc2/Inc3.
External-agent auto-fill path — set or clear one custom field's value
on a work item via the public (API-token) API:

  PUT    .../issues/<iid>/field-values/<fid>/   set/replace value
  DELETE .../issues/<iid>/field-values/<fid>/   clear value

- reuses the internal assign_field_value helper for typed coercion, so
  validation/column-mapping matches the internal API exactly (number
  rejects non-numeric -> 400, not 500)
- hardened vs the internal version: verifies the work item actually
  belongs to this project/workspace before writing, so a token scoped
  to project A cannot attach a value to project B's issue
- ProjectEntityPermission (same as State create/patch); additive only,
  no model/migration

Verified locally end-to-end with a seeded API token: PUT number=8 ->
200 (value 8.0 persisted), GET reflects it, PUT 'abc' -> 400, DELETE
-> 204, GET -> []. No regression to Inc1 reads or existing endpoints.
External tokens can now create/update/archive custom field schemas and
their select options via the public API:

  POST   .../fields/                          create field
  PATCH  .../fields/<fid>/                     update field
  DELETE .../fields/<fid>/                     archive (is_active=False)
  POST   .../fields/<fid>/options/             create option
  PATCH  .../fields/<fid>/options/<oid>/       update option
  DELETE .../fields/<fid>/options/<oid>/       archive option

- schema mutations are project ADMIN only (ProjectAdminPermission via a
  per-method get_permissions mixin: reads = any member, writes = admin)
  mirroring the internal @allow_permission([ROLE.ADMIN]) gate so a
  leaked non-admin token cannot reshape the data model
- reuses the Inc1 serializers (explicit field whitelists, read-only
  id/project/workspace/timestamps -> no mass-assignment); soft-delete
  archive (no hard data loss via API); querysets project/membership
  scoped (no cross-project access); option create verifies parent field
  in project
- additive only, no model/migration

Verified locally e2e with a seeded admin token: create/patch/delete
field + option all succeed, DELETE soft-archives (is_active=False),
controls + Inc1/Inc2 unaffected.
…Task2)

Unifies labels, custom single/multi-select, priority and state into one
solid-fill rounded pill with contrast text — replacing the prior mix of
bordered chips, tiny colour dots and 12%-opacity tints.

- label.tsx: shared LABEL_PILL_CLASS + labelPillStyle (solid bg, white
  text or near-black on light colours via @plane/utils luminance);
  reused everywhere so there is one source of truth
- labels.tsx: LabelItem/Summary use the pill; 4px gap + flex-wrap
  between multiple label pills
- work-item-field-cell.tsx: single_select / multi_select selected chips
  are now solid pills (was dot+text / colour+20% tint)
- priority.tsx: all 3 button variants (Border/Background/Transparent)
  render a solid pill in the text mode using the semantic --priority-*
  colours; compact icon-only mode unchanged
- state/base.tsx: text mode renders a solid pill from state.color
  (contained — neutralises the shared DropdownButton chrome via !-utils,
  no shared-component change)
- label.ts: LABEL_COLOR_OPTIONS swapped to the vivid color-hex palette
  (#3be8b0/#1aafd0/#6a67ce/#ffb900/#fc636b); fixed an out-of-range
  LABEL_COLOR_OPTIONS[7] default that the smaller palette would break
- pill vertical padding +5px (py-[7px]) for breathing room
- priority/state: bounded eslint-disable for the pre-existing repo-wide
  ComboDropDown a11y lint (same rule the codebase already disables;
  surfaced only because these files are now staged)

cf_local_stack.py seed changes (demo labels + palette + Profile fix)
are intentionally NOT committed (untracked dev harness).
All projects/users default to Tabbed navigation instead of Accordion.

- workspace.py: WorkspaceUserProperties.navigation_control_preference
  model default ACCORDION -> TABBED (new rows)
- migration 0125: AlterField (matches the model) + a data migration
  flipping every existing row still on 'ACCORDION' to 'TABBED' so
  current users get it too (the model default never touches existing
  rows). Reverse is a no-op — we cannot tell which rows chose ACCORDION
  deliberately vs. via the old silent default, so we don't blanket-revert.
- navigation-preferences.ts: frontend DEFAULT_PROJECT_PREFERENCES
  fallback ACCORDION -> TABBED (covers users with no prefs row yet)

Backend change: requires deploy + running migration 0125 on the host
to take effect in production. Verified locally — migration applies
clean (0124 -> 0125, no leaf conflict).
Surfaces the existing Favorites feature into the project context menu
(发布项目/复制链接/归档/设置). Favourite state is read from the
favorite store's entityMap because the sidebar uses the partial
project object (projects-API optimisation dropped is_favorite).
Reuses useProject().add/removeProjectToFavorites + setPromiseToast,
mirroring the card-view implementation.

Pre-existing DnD lint (no-shadow on inner getData param,
react-hooks/exhaustive-deps) suppressed with targeted disables at the
actual violation nodes; not introduced by this change.
…roject

Tasks no longer require picking a project. A per-user private
"My Tasks" project is lazily created server-side (hidden from normal
project lists, owner-only ADMIN member, Secret network) and reused so
project-less tasks need NO issue-schema change — they get states,
custom fields, layouts, etc. for free.

Backend (Inc1): Project.is_personal + personal_owner + migration 0126;
GET workspaces/<slug>/projects/personal/ get-or-create endpoint
mirroring the create() bootstrap; exclude is_personal from the normal
project list/list_detail (still reachable by pk for issue CRUD).

Frontend (Inc2): projectService.getPersonalProject; sidebar "My Tasks"
entry (workspace.ts + sidebar-item staticItems + ce icon); /my-tasks
route resolves the personal project and redirects to its issues page
(reuses 100% of existing issue UI + create flow); i18n en/zh.

Verified end-to-end against the local stack: migration applied,
endpoint 200 creating a correct bucket, idempotent, excluded from
the normal project list.
Pre-existing bug, unrelated to the personal-tasks feature. The repo
ships a requestIdleCallback/cancelIdleCallback polyfill in
core/lib/polyfills but no module ever imported it, so it was dead
code: WebKit/Safari < 17.4 crashed in any virtualized list
(render-if-visible-HOC's unguarded window.requestIdleCallback).

Fix: side-effect import "@/lib/polyfills" in app/entry.client.tsx so
the existing polyfill runs before any component mounts — fixes it
globally, no new code, HOC left untouched.
Lift the My Tasks bucket bootstrap out of ProjectViewSet.personal()
into plane.app.services so an upcoming token-API endpoint can reuse the
same logic to create personal projects on behalf of other workspace
members. The session-API wrapper passes only owner (actor defaults to
it), preserving prior behavior.

Uses project.save(created_by_id=actor.id) instead of the original
Project.objects.create(..., created_by=actor) so the helper honors the
passed actor regardless of crum's current-user context. The inline
predecessor relied on a request being in flight; that assumption would
not hold for out-of-band callers (celery tasks, the upcoming token
endpoint, or unit tests).
Gate token-API endpoints that act on behalf of workspace members other
than the token's own owner (cross-user writes into personal projects,
workspace-wide assignee scans). Reuses the existing DB-only is_service
flag on APIToken — no new schema, no new self-grant API path. Wired by
the upcoming personal-tasks and assigned-work-items viewsets.
APIKeyAuthentication.authenticate returns request.auth = api_token.token
(the raw string), not the APIToken model. The original mock-based unit
tests passed but the production check would have silently denied every
service-tier token. Re-query the row from the header value instead —
same pattern already in BaseAPIView.get_throttles. Also tighten with
is_active=True to mirror the auth class. Tests rewritten against real
APIToken rows.
System-tier endpoint at POST/PATCH /api/v1/workspaces/{slug}/personal-tasks/
that creates and updates work items in any workspace member's personal
"My Tasks" project on behalf of that member. Gated by IsSystemToken;
ordinary tokens are 403'd. Idempotency reuses the existing Issue
contract — duplicate (project, external_source, external_id) returns
409 + the existing work item id. PATCH may only touch work items whose
external_source matches the body, enforcing the §5 rule 2 privacy
boundary.

Adds APIActivityLog.acting_on_behalf_of (nullable UUID, indexed,
migration 0128) wired by APITokenLogMiddleware so every audit row for
these calls records the target member, making "system-on-behalf-of-X"
queryable downstream. The viewset sets the attribute on the underlying
Django request (not the DRF wrapper) so the middleware — which runs at
the Django layer — can read it.

The bucket get-or-create path comes from the C1 helper, which resolves
spec §12.4 lazy-create from the server side: a token call works even
when the target member has never opened "My Tasks" in the web UI.

11 contract tests cover: non-system 403, anon 401/403, missing-owner
400, missing-external-keys 400, owner-not-in-workspace 403, lazy-create
+ 201, duplicate 409+id, PATCH wrong-source 403, PATCH matching-source
200, PATCH nonexistent 404, audit row carries owner UUID as
acting_on_behalf_of.
Marcus Cheung and others added 3 commits May 19, 2026 15:32
System-tier endpoint at
GET /api/v1/workspaces/{slug}/assigned-work-items/?assignee=<uuid>
that returns the target user's assigned work items across the entire
workspace — both their personal "My Tasks" project AND any shared
project they're a member of. Privacy boundary is the assignee filter:
only items where the target is assigned appear; other members' items
and items in shared projects the target is not on do not leak.

This is the read side of spec §6 Q1 (a-extended) — no external_source
filter on the read path. The §5 rule 2 source-restricted boundary
stays on the write side (PATCH /personal-tasks/{id}/) where it
matters; reads need to be "everything assigned" so the agent-side
"today" view doesn't miss tasks the user created themselves.

Optional filters: state_group (CSV) and target_date_before (ISO).
Pagination uses BasePaginator cursor envelope. Audit row records the
query.assignee UUID via the same _acting_on_behalf_of hook the
personal-tasks endpoint uses.

8 contract tests cover non-system 403, anon 401, missing/malformed
assignee 400, cross personal+shared completeness, privacy boundary
(no leak), state_group filter, empty case, and audit row UUID.
Adds operation_id / summary / description / tags annotations to the
three new endpoints (POST + PATCH personal-tasks, GET assigned-work-items)
so drf-spectacular surfaces polished entries in the OpenAPI schema
served at /api/v1/schema/. No CHANGELOG file in this fork — skipping
that part of the original plan. Cross-repo skill mirror sync
(skills-shared/joby-plane/tick-agent-reference.md §12.4 + SKILL.md
bump) is a post-ship step, intentionally out of scope here.
Adds /personal-tasks/ POST+PATCH and /assigned-work-items/ GET under IsSystemToken gate. APIActivityLog.acting_on_behalf_of column (migration 0128) for audit. get_or_create_personal_project helper extracted from session API. 28 contract+unit tests passing.
@JOBYINC JOBYINC merged commit 6c53c2f into preview May 19, 2026
6 of 7 checks passed
assign_field_value(value_row, field, request.data.get("value"))
except Exception as exc:
detail = getattr(exc, "detail", None) or str(exc)
return Response({"error": detail}, status=status.HTTP_400_BAD_REQUEST)
@JOBYINC JOBYINC deleted the feature/personal-tasks branch May 19, 2026 23:47
JOBYINC added a commit that referenced this pull request May 20, 2026
…to lark-stable image lane)

Image-build workflow only triggers on feature/lark-oauth-provider push; merge here so next lark-stable GHCR build includes system-identity endpoints + 33-commit roll-up.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants