Consultant escalation policy + milestone delivery checks
Manager policy, active only when a company employs a consultant: - a task that blows its wall-clock budget or blocks on machine failure (crashed worker, capability gap — anything except needs_input) is reassigned to the consultant with the failure context appended - every objective rollup spawns a consultant review task: verify each deliverable against acceptance criteria, write milestone-check.md, verdict per deliverable, fix nothing Also: employee_role_check widened to include consultant (db + live); consultant sprite shirt color in the office. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4796274ea5
commit
7ab6db469d
3 files changed
+56
-2
No files matched your search
+1
-1
@@ -19,7 +19,7 @@ CREATE TABLE employee (
|
|||||||
name text NOT NULL,
|
name text NOT NULL,
|
||||||
title text NOT NULL,
|
title text NOT NULL,
|
||||||
role text NOT NULL DEFAULT 'engineer'
|
role text NOT NULL DEFAULT 'engineer'
|
||||||
CHECK (role IN ('manager','engineer','qa')),
|
CHECK (role IN ('manager','engineer','qa','consultant')),
|
||||||
department text NOT NULL DEFAULT '',
|
department text NOT NULL DEFAULT '',
|
||||||
manager_id uuid REFERENCES employee(id),
|
manager_id uuid REFERENCES employee(id),
|
||||||
hermes_profile text, -- Hermes profile name (ADR-001)
|
hermes_profile text, -- Hermes profile name (ADR-001)
|
||||||
|
|||||||
@@ -163,6 +163,25 @@ class Dispatcher:
|
|||||||
event_type="milestone.update",
|
event_type="milestone.update",
|
||||||
company_id=row["company_id"], work_item_id=row["id"],
|
company_id=row["company_id"], work_item_id=row["id"],
|
||||||
payload={"objective_title": row["title"], "summary": update})
|
payload={"objective_title": row["title"], "summary": update})
|
||||||
|
# milestone hit + consultant employed -> independent delivery check
|
||||||
|
consultant = await self._consultant_for(row["company_id"])
|
||||||
|
if consultant:
|
||||||
|
await conn.execute(
|
||||||
|
"INSERT INTO work_item (company_id, parent_id, type, title, "
|
||||||
|
"description, status, owner_id, acceptance_criteria) "
|
||||||
|
"VALUES (%s, %s, 'review', %s, %s, 'ready', %s, %s)",
|
||||||
|
(row["company_id"], row["id"],
|
||||||
|
f"Milestone delivery check: {row['title'][:60]}",
|
||||||
|
"The objective just completed. Independently verify delivery: "
|
||||||
|
"read the deliverable files in the workspace produced by its "
|
||||||
|
"tasks (their reports are appended below), check each against "
|
||||||
|
"the stated acceptance criteria, and write "
|
||||||
|
"milestone-check.md (overwrite if present) with a verdict per "
|
||||||
|
"deliverable — delivered / partial / missing — plus any "
|
||||||
|
"defects found. Do not fix anything yourself.",
|
||||||
|
consultant["id"],
|
||||||
|
db.Json(["milestone-check.md exists with a verdict per "
|
||||||
|
"deliverable and evidence for each verdict"])))
|
||||||
log.info("objective %s rolled up to done", row["id"])
|
log.info("objective %s rolled up to done", row["id"])
|
||||||
|
|
||||||
async def _dispatch_ready(self) -> None:
|
async def _dispatch_ready(self) -> None:
|
||||||
@@ -284,10 +303,45 @@ class Dispatcher:
|
|||||||
elif status == "blocked":
|
elif status == "blocked":
|
||||||
await self._block(item, kt.get("block_reason") or "hermes reported blocked",
|
await self._block(item, kt.get("block_reason") or "hermes reported blocked",
|
||||||
event_type="task.blocked")
|
event_type="task.blocked")
|
||||||
|
# machine failure (crashed worker, capability gap) -> consultant;
|
||||||
|
# needs_input stays blocked for the shareholder answer button
|
||||||
|
if kt.get("block_kind") != "needs_input":
|
||||||
|
await self._escalate(item, "local worker failed "
|
||||||
|
f"({kt.get('last_failure_error') or kt.get('block_kind') or 'blocked'})")
|
||||||
elif await self._over_budget(item):
|
elif await self._over_budget(item):
|
||||||
await self._cap(item, kt)
|
await self._cap(item, kt)
|
||||||
|
await self._escalate(item, "wall-clock budget exceeded")
|
||||||
# todo/scheduled/ready/running: still waiting
|
# todo/scheduled/ready/running: still waiting
|
||||||
|
|
||||||
|
async def _consultant_for(self, company_id) -> dict | None:
|
||||||
|
return await db.fetch_one(
|
||||||
|
"SELECT id, name FROM employee WHERE company_id = %s AND role = 'consultant' "
|
||||||
|
"ORDER BY created_at LIMIT 1", company_id)
|
||||||
|
|
||||||
|
async def _escalate(self, item: dict, reason: str) -> bool:
|
||||||
|
"""Manager policy: hand a failing task to the company consultant.
|
||||||
|
Only fires when a consultant is employed (hiring one = opting in to
|
||||||
|
external billing) and the failing owner isn't already the consultant."""
|
||||||
|
consultant = await self._consultant_for(item["company_id"])
|
||||||
|
if not consultant or consultant["id"] == item["owner_id"]:
|
||||||
|
return False
|
||||||
|
note = (f"\n\n[Escalated to consultant {consultant['name']}: {reason}. "
|
||||||
|
"A previous attempt by local staff did not deliver; produce the "
|
||||||
|
"deliverable yourself, completely.]")
|
||||||
|
async with db.pool.connection() as conn:
|
||||||
|
async with conn.transaction():
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE work_item SET owner_id=%s, status='ready', hermes_task_id=NULL, "
|
||||||
|
"description=description || %s, updated_at=now() WHERE id=%s",
|
||||||
|
(consultant["id"], note, item["id"]))
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE employee SET status='idle' WHERE id=%s", (item["employee_id"],))
|
||||||
|
await db.append_event(conn, actor="system", event_type="task.escalated",
|
||||||
|
company_id=item["company_id"], work_item_id=item["id"],
|
||||||
|
payload={"to": consultant["name"], "reason": reason})
|
||||||
|
log.info("escalated %s to consultant (%s)", item["id"], reason)
|
||||||
|
return True
|
||||||
|
|
||||||
async def _resume(self, item: dict) -> None:
|
async def _resume(self, item: dict) -> None:
|
||||||
async with db.pool.connection() as conn:
|
async with db.pool.connection() as conn:
|
||||||
async with conn.transaction():
|
async with conn.transaction():
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const ROOMS = {
|
|||||||
conf: { test:(x,y)=> x>18*T && y<8*T, W:[17*T+8, 4*T+8], D:[19*T+8, 4*T+8] },
|
conf: { test:(x,y)=> x>18*T && y<8*T, W:[17*T+8, 4*T+8], D:[19*T+8, 4*T+8] },
|
||||||
lab: { test:(x,y)=> x>20*T && y>12*T, W:[24*T+8, 11*T+8], D:[24*T+8, 13*T+8] },
|
lab: { test:(x,y)=> x>20*T && y>12*T, W:[24*T+8, 11*T+8], D:[24*T+8, 13*T+8] },
|
||||||
};
|
};
|
||||||
const ROLE_SHIRT = { manager:'#e0b354', engineer:'#5c9de0', qa:'#b48ce0' };
|
const ROLE_SHIRT = { manager:'#e0b354', engineer:'#5c9de0', qa:'#b48ce0', consultant:'#e6e2d8' };
|
||||||
const HAIR = ['#3a2a1c','#151412','#c9a24b','#8a3b22','#4a4e57'];
|
const HAIR = ['#3a2a1c','#151412','#c9a24b','#8a3b22','#4a4e57'];
|
||||||
const px = (x,y,w,h,c) => { ctx.fillStyle=c; ctx.fillRect(x|0,y|0,w,h); };
|
const px = (x,y,w,h,c) => { ctx.fillStyle=c; ctx.fillRect(x|0,y|0,w,h); };
|
||||||
const hash = s => [...s].reduce((a,ch)=>(a*31+ch.charCodeAt(0))>>>0, 7);
|
const hash = s => [...s].reduce((a,ch)=>(a*31+ch.charCodeAt(0))>>>0, 7);
|
||||||
|
|||||||
Reference in new issue
Block a user