d48cda4081
CI / Test (ubuntu-latest, Node 18.x, bun) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, npm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, pnpm) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 18.x, yarn) (push) Failing after 15m1s
CI / Test (ubuntu-latest, Node 20.x, bun) (push) Failing after 17m13s
CI / Test (ubuntu-latest, Node 20.x, npm) (push) Failing after 18m42s
CI / Test (ubuntu-latest, Node 20.x, pnpm) (push) Failing after 15m0s
CI / Test (ubuntu-latest, Node 20.x, yarn) (push) Failing after 49m44s
CI / Test (ubuntu-latest, Node 22.x, bun) (push) Failing after 51m55s
CI / Test (ubuntu-latest, Node 22.x, pnpm) (push) Failing after 21m57s
CI / Test (ubuntu-latest, Node 22.x, npm) (push) Failing after 37m39s
CI / Test (ubuntu-latest, Node 22.x, yarn) (push) Failing after 34m7s
CI / Validate Components (push) Failing after 37m15s
CI / Python Tests (push) Failing after 10m1s
CI / Security Scan (push) Failing after 10m1s
CI / Lint (push) Failing after 17m12s
CI / Coverage (push) Failing after 20m19s
CI / Test (macos-latest, Node 18.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 18.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 20.x, yarn) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, bun) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (macos-latest, Node 22.x, yarn) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, npm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, pnpm) (push) Has been cancelled
CI / Test (windows-latest, Node 22.x, yarn) (push) Has been cancelled
122 lines
3.5 KiB
JavaScript
122 lines
3.5 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Shared work-item mutation helpers for the agent+human JIT board.
|
|
*
|
|
* Used by both the `work-items.js` CLI (`claim`) and the control-pane local
|
|
* server (interactive claim / move), so the two surfaces never diverge.
|
|
*/
|
|
|
|
const DONE_STATUSES = new Set(['done', 'closed', 'resolved', 'merged', 'cancelled']);
|
|
const PRIORITY_RANK = { critical: 0, high: 1, urgent: 1, medium: 2, normal: 2, low: 3 };
|
|
|
|
// Kanban lanes the board renders, and the canonical status each maps to on a move.
|
|
const LANE_TO_STATUS = {
|
|
ready: 'open',
|
|
running: 'running',
|
|
blocked: 'blocked',
|
|
done: 'done'
|
|
};
|
|
const VALID_LANES = new Set(Object.keys(LANE_TO_STATUS));
|
|
const VALID_ASSIGNEE_KINDS = new Set(['agent', 'human']);
|
|
|
|
function isOpenStatus(status) {
|
|
return !DONE_STATUSES.has(
|
|
String(status || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
);
|
|
}
|
|
|
|
function priorityRank(priority) {
|
|
return PRIORITY_RANK[String(priority || '').toLowerCase()] ?? 2;
|
|
}
|
|
|
|
/**
|
|
* Resolve which work item a claim targets: an explicit id, otherwise the
|
|
* highest-priority unassigned open item (the JIT pickup queue).
|
|
*/
|
|
function selectClaimTarget(store, { id } = {}) {
|
|
if (id) {
|
|
const item = store.getWorkItemById(id);
|
|
if (!item) {
|
|
throw new Error(`Work item not found: ${id}`);
|
|
}
|
|
return item;
|
|
}
|
|
const { items } = store.listWorkItems({ limit: 100 });
|
|
return items.filter(item => !item.owner && isOpenStatus(item.status)).sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority))[0] || null;
|
|
}
|
|
|
|
/**
|
|
* Claim an unassigned work item for an agent or human. Sets the owner (and
|
|
* optional assigneeKind) and moves the card to running unless an explicit
|
|
* status is supplied. Returns { claimed, item } or { claimed: false, reason }.
|
|
*/
|
|
function claimWorkItem(store, { id, owner, assigneeKind, sessionId, status } = {}) {
|
|
if (!owner) {
|
|
throw new Error('claim requires an owner.');
|
|
}
|
|
const kind = assigneeKind ? String(assigneeKind).toLowerCase() : null;
|
|
if (kind && !VALID_ASSIGNEE_KINDS.has(kind)) {
|
|
throw new Error("assigneeKind must be 'agent' or 'human'.");
|
|
}
|
|
const target = selectClaimTarget(store, { id });
|
|
if (!target) {
|
|
return { claimed: false, reason: 'no-unassigned-open-items' };
|
|
}
|
|
if (!isOpenStatus(target.status)) {
|
|
throw new Error(`Work item ${target.id} is already done; cannot claim.`);
|
|
}
|
|
const metadata = { ...(target.metadata || {}) };
|
|
if (kind) {
|
|
metadata.assigneeKind = kind;
|
|
}
|
|
const item = store.upsertWorkItem({
|
|
...target,
|
|
owner,
|
|
sessionId: sessionId ?? target.sessionId ?? null,
|
|
status: status ?? 'running',
|
|
metadata,
|
|
updatedAt: new Date().toISOString()
|
|
});
|
|
return { claimed: true, item };
|
|
}
|
|
|
|
/**
|
|
* Move a work item to a kanban lane (ready | running | blocked | done).
|
|
*/
|
|
function moveWorkItem(store, { id, lane } = {}) {
|
|
if (!id) {
|
|
throw new Error('move requires a work item id.');
|
|
}
|
|
const laneKey = String(lane || '')
|
|
.trim()
|
|
.toLowerCase();
|
|
if (!VALID_LANES.has(laneKey)) {
|
|
throw new Error(`Invalid lane '${lane}'. Expected one of ${[...VALID_LANES].join(', ')}.`);
|
|
}
|
|
const target = store.getWorkItemById(id);
|
|
if (!target) {
|
|
throw new Error(`Work item not found: ${id}`);
|
|
}
|
|
const item = store.upsertWorkItem({
|
|
...target,
|
|
status: LANE_TO_STATUS[laneKey],
|
|
updatedAt: new Date().toISOString()
|
|
});
|
|
return { moved: true, item };
|
|
}
|
|
|
|
module.exports = {
|
|
DONE_STATUSES,
|
|
LANE_TO_STATUS,
|
|
VALID_LANES,
|
|
VALID_ASSIGNEE_KINDS,
|
|
isOpenStatus,
|
|
priorityRank,
|
|
selectClaimTarget,
|
|
claimWorkItem,
|
|
moveWorkItem
|
|
};
|