bus, harness: PM task reads that fit a session (#1583)
Row 89, round 1. The broker's `tasks` read takes optional filter and page arguments and returns a short form. The harness adds `list_tasks` arguments (open tasks by default), a `get_task` tool, and compact JSON replies capped below the 50000-character threshold where Claude Code 2.1.296 saves a tool result to a file outside the workspace, which was the PM's fault in Jason's test. The bundle's tools line names the harness's own tools. Candidate manifest b8c5bb3a (agents/dewey/work/queue-89/), basea3f6e9e6, no drift on the 12 paths. Builder Dewey. Approved by Darkwing (comment 27940). Sage's gate onf04185fcplus the patch 2026-10-12T22:00:55Z-22:03:38Z: bus 108, harness 65, webui 86, cli 115, control-board 124, every scripts/test-*.sh green. Live at the next bus restart (broker) and role session launch (harness), both Mos's call (decision 109); Mos had the notice. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -42,6 +42,10 @@ const CLOSED = "('resolved','withdrawn','expired')";
|
||||
export const RECENT = 50;
|
||||
// How many replies `replies` returns, oldest first.
|
||||
export const REPLIES = 50;
|
||||
// Row 89 (#1583): the arguments `tasks` takes, and its page size by default and at most.
|
||||
const TASK_ARGS = ['done', 'assignee', 'label', 'requirement', 'limit', 'after', 'task_ref'];
|
||||
export const TASK_PAGE = 50;
|
||||
export const TASK_PAGE_MAX = 200;
|
||||
export const TASK_VERBS = Object.freeze(ACTIONS.filter((a) => a.startsWith('task.')));
|
||||
const SELF_KINDS = new Set(['task.created', 'task.assigned', 'task.state', 'task.closed', 'task.conflict']);
|
||||
const POLL_KINDS = new Set([
|
||||
@@ -328,6 +332,59 @@ export class Broker {
|
||||
return { ...r, fields, priority, requirement: requirement.get(taskId(r.task_ref)) ?? null };
|
||||
});
|
||||
}
|
||||
// Row 89 (#1583): with no arguments, every row as #taskRows gives it, which the Console and
|
||||
// `mosaic tasks` read. `task_ref`, alone, gives that one row. Any other argument filters and pages
|
||||
// the short form in task_ref order: no description, and the assignees that are a role's tracker
|
||||
// bot, by role. A tombstone isn't on the board, so a filtered read leaves it out. `total` counts
|
||||
// every task the filters match; `next` is the `after` for the next page, null on the last.
|
||||
#tasks(business, a) {
|
||||
const rows = this.#taskRows(business);
|
||||
if (!Object.keys(a).length) return rows;
|
||||
if ('task_ref' in a) {
|
||||
if (Object.keys(a).length !== 1 || !taskRef(a.task_ref)) fail('invalid-request');
|
||||
return rows.find((r) => r.task_ref === a.task_ref) ?? fail('task-not-found');
|
||||
}
|
||||
const roles = this.#business(business).roles ?? {};
|
||||
const positive = (x, max = Number.MAX_SAFE_INTEGER) => Number.isSafeInteger(x) && x >= 1 && x <= max;
|
||||
if (
|
||||
('done' in a && typeof a.done !== 'boolean') ||
|
||||
('assignee' in a && !(typeof a.assignee === 'string' && Object.hasOwn(roles, a.assignee))) ||
|
||||
('label' in a && !positive(a.label)) ||
|
||||
('requirement' in a && !(typeof a.requirement === 'string' && /^REQ-[A-Z]+-[1-9][0-9]*$/.test(a.requirement))) ||
|
||||
('limit' in a && !positive(a.limit, TASK_PAGE_MAX)) ||
|
||||
('after' in a && !taskRef(a.after))
|
||||
)
|
||||
fail('invalid-request');
|
||||
const bots = new Map(
|
||||
Object.entries(roles)
|
||||
.filter(([, r]) => r.tracker?.botId !== undefined)
|
||||
.map(([role, r]) => [r.tracker.botId, role]),
|
||||
);
|
||||
const matched = rows
|
||||
.filter((r) => !r.fields.gone)
|
||||
.map(({ task_ref, fields: f, priority, requirement, at }) => ({
|
||||
task_ref,
|
||||
title: f.title ?? null,
|
||||
done: Boolean(f.done),
|
||||
bucket: f.bucket ?? null,
|
||||
assignees: (f.assignees ?? []).filter((u) => bots.has(u)).map((u) => bots.get(u)).sort(),
|
||||
labels: f.labels ?? [],
|
||||
priority,
|
||||
requirement,
|
||||
changed_at: at,
|
||||
}))
|
||||
.filter(
|
||||
(t) =>
|
||||
(!('done' in a) || t.done === a.done) &&
|
||||
(!('assignee' in a) || t.assignees.includes(a.assignee)) &&
|
||||
(!('label' in a) || t.labels.includes(a.label)) &&
|
||||
(!('requirement' in a) || t.requirement === a.requirement),
|
||||
);
|
||||
const rest = 'after' in a ? matched.filter((t) => t.task_ref > a.after) : matched;
|
||||
const limit = a.limit ?? TASK_PAGE;
|
||||
const tasks = rest.slice(0, limit);
|
||||
return { tasks, total: matched.length, next: rest.length > limit ? tasks.at(-1).task_ref : null };
|
||||
}
|
||||
#checkAuthority(s, action, { decision = null, target = null } = {}) {
|
||||
this.#agent(s);
|
||||
if (
|
||||
@@ -841,7 +898,8 @@ export class Broker {
|
||||
case 'agents':
|
||||
case 'tasks':
|
||||
case 'trail': {
|
||||
keys(a, verb === 'trail' ? ['subject'] : [], verb === 'trail' ? ['subject'] : []);
|
||||
const allowed = verb === 'trail' ? ['subject'] : verb === 'tasks' ? TASK_ARGS : [];
|
||||
keys(a, allowed, verb === 'trail' ? ['subject'] : []);
|
||||
if (!s.human && !s.reader) this.#agent(s);
|
||||
if (verb === 'inbox')
|
||||
return this.#store
|
||||
@@ -859,7 +917,7 @@ export class Broker {
|
||||
// A role session gets the claims only: another role's decisions aren't its to read.
|
||||
return s.human || s.reader ? claims.map((c) => this.#agentState(c)) : claims;
|
||||
}
|
||||
if (verb === 'tasks') return this.#taskRows(s.business);
|
||||
if (verb === 'tasks') return this.#tasks(s.business, a);
|
||||
return this.#trail(s, identifier(a.subject));
|
||||
}
|
||||
// Row 57 (#1546): the Inbox's "Routed to roles" and "Recent" lists, for human and
|
||||
|
||||
Reference in New Issue
Block a user