@chatpanel/events 0.102.0 → 0.103.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +1 -4
  62. package/pdf-layout.js +7 -253
  63. package/project-run.js +20 -444
  64. package/project.js +1 -170
  65. package/promotion.js +1 -171
  66. package/queue.js +1 -98
  67. package/reach.js +1 -23
  68. package/recipe-tool.js +2 -161
  69. package/recipe.js +1 -264
  70. package/record-list.js +2 -110
  71. package/recruit.js +3 -456
  72. package/redaction-tokens.js +1 -61
  73. package/ref.js +1 -55
  74. package/registry.js +1 -240
  75. package/route-graph.js +1 -115
  76. package/route-strategies.js +2 -232
  77. package/router.js +2 -880
  78. package/rrf.js +1 -89
  79. package/rules.js +1 -142
  80. package/schedule.js +1 -663
  81. package/scm-connection.js +1 -180
  82. package/scopes.js +1 -12
  83. package/scorecard.js +1 -335
  84. package/search-engines.js +1 -81
  85. package/skill-manifest.js +1 -330
  86. package/skill-scan.js +3 -209
  87. package/skill-sources.js +1 -162
  88. package/skill-vars.js +2 -252
  89. package/slash-commands.js +1 -158
  90. package/source-gate.js +1 -92
  91. package/sources-retrieval.js +5 -186
  92. package/sources.js +1 -273
  93. package/store.js +1 -171
  94. package/structured.js +4 -901
  95. package/subject-kinds.js +1 -5
  96. package/subject-name.js +1 -96
  97. package/sync-plan.js +1 -213
  98. package/synthesis.js +3 -123
  99. package/tags.js +1 -203
  100. package/team-board.js +6 -337
  101. package/team-cache.js +2 -63
  102. package/team-observe.js +1 -118
  103. package/team-org.js +1 -442
  104. package/team-plan.js +4 -131
  105. package/team-record.js +1 -225
  106. package/team-run.js +9 -652
  107. package/team-subtask.js +2 -191
  108. package/team-task.js +2 -124
  109. package/team-tool.js +2 -196
  110. package/team-trail.js +2 -73
  111. package/team-worklog.js +2 -149
  112. package/team.js +1 -302
  113. package/text-search.js +1 -155
  114. package/theme.js +17 -154
  115. package/titles.js +4 -285
  116. package/tool-discovery.js +1 -87
  117. package/tool-dispatch.js +5 -230
  118. package/tool-groups.js +1 -81
  119. package/tool-hints.js +10 -185
  120. package/tool-loop-guard.js +4 -182
  121. package/tool-need.js +2 -124
  122. package/tool-result.js +2 -438
  123. package/tool-round.js +0 -0
  124. package/tool-schema.js +1 -155
  125. package/tool-traits.js +1 -182
  126. package/toolset.js +2 -88
  127. package/trajectory.js +1 -530
  128. package/turn-loop.js +4 -440
  129. package/upcast.js +1 -37
  130. package/vault.js +2 -250
  131. package/view.js +1 -88
  132. package/voice-intents.js +2 -1488
  133. package/voice-speaker.js +1 -98
  134. package/weather-tool.js +1 -65
  135. package/weather.js +2 -211
  136. package/web-search-tool.js +14 -102
  137. package/web-search.js +1 -160
  138. package/widget.js +1 -153
package/recruit.js CHANGED
@@ -1,457 +1,4 @@
1
- // Recruiting applying and evaluating, the step between a posted job and a run (F8 §12.2.4,
2
- // architecture-pillars.md §13.4).
3
- //
4
- // Applications are COMPUTED, not asked for: every agent in the pool applies to every job at
5
- // once, scored by `fit` (scorecard.js) on its skills, tools, grants and its attested record —
6
- // the model-ADJUSTED rating when the engine rows say what its engines were worth. Recruiting
7
- // costs one model turn per job, not one per applicant, and that turn is optional.
8
- //
9
- // What is recruited is an (agent, engine) PAIR. An agent's engine may be fixed (`model`,
10
- // `harness`), the chat's (`assistant`), or `auto` with a policy; `routeFor` resolves it
11
- // against the ENGINE ROWS the host knows — every model or harness it can run, with reach,
12
- // capabilities, quality / latency / cost (the engine card's observed values over the router's
13
- // guess, `engineRow`) and whether it is available right now. Requirements eliminate first
14
- // (reach from the project's privacy setting — never learned, only typed; a work grant needs a
15
- // harness; tools need `tools`), the policy orders what survives over observed values, and the
16
- // agent's own record on each engine breaks ties. An agent whose engine does not clear is
17
- // still an applicant — a person should see it — but is not recruitable now.
18
- //
19
- // The EVALUATOR is one structured call (RECRUIT_SCHEMA) over the top applications: the pick
20
- // and why, or "none fits" with the agent that should exist. The call is the host's
21
- // (`runStructured` in a client; a gateway without a model skips it): `decide` takes its parsed
22
- // answer when there is one and falls back to the best fit above a floor when there is not, so
23
- // a job is recruited with or without a model. The decision lands on the project record as
24
- // events (`recruitEvents`) — the pick and the reasons on the job, a proposal as a decision a
25
- // person reads — never as a mutation.
26
- //
27
- // Pure, dependency-free; imports only what the gateway already vendors (scorecard.js,
28
- // model-ledger.js, engine.js, agent.js, job.js, team.js, structured.js, budget.js).
1
+ import{fit as K,engineKey as j,normalizeEngine as E}from"./scorecard.js";import{cardOverride as G,DEFAULT_MIN_CALLS as J}from"./model-ledger.js";import{engineKeyOf as Y,describeEngine as T}from"./engine.js";import{engineOf as H,agentFromForm as X,normalizeAgent as Q,resolveTeam as V}from"./agent.js";import{applyAll as Z,jobToRole as ee}from"./job.js";import{WORK_GRANTS as A}from"./team.js";import{defineSchema as ne,describeSchema as te,coerce as se}from"./structured.js";import{normalizeBudget as ie}from"./budget.js";const q=.5,oe=5,ae=1500,I={device:0,trusted:1,any:2},B=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),F=e=>e==null||!Number.isFinite(e)?null:Math.round(e*1e3)/1e3,k=(e,l)=>String(e||"").trim().slice(0,l),M=e=>(Array.isArray(e)?e:[]).map(l=>String(l).toLowerCase()),S=ne({name:"recruit",purpose:"which applicant gets the job, or that none fits and what agent should exist",fields:{pick:{type:"string",max:64,describe:'the id of the ONE applicant to recruit, or "" when none fits'},why:{type:"string",required:!0,max:400,describe:"one or two sentences a person reads on the job page"},confidence:{type:"number",describe:"0 to 1"},proposalName:{type:"string",max:60,describe:"when none fits: the agent that should exist"},proposalPurpose:{type:"string",max:300},proposalSkills:{type:"string[]",maxItems:12},proposalGrants:{type:"string[]",maxItems:8,describe:"from: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr"}},nothing:{pick:"",why:"none fits"}});function _(e,{card:l=null,minCalls:t=J,jobKind:s=null}={}){if(!B(e))return null;const o=e;let a=o.engine?E(o.engine):null;if(!a){const d=String(o.model||o.id||"");if(!d)return null;if(o.kind==="bridge"||o.kind==="harness"||o.classUsed==="A"){const f=d.indexOf("/");a=f>0?{kind:"harness",id:d.slice(0,f),model:d.slice(f+1)}:{kind:"harness",id:d}}else a={kind:"model",id:d}}const r=j(a);if(!r)return null;const{override:i,observed:c}=G(l,{minCalls:t,jobKind:s}),n=new Set(l?.capabilities?.withdrawn||[]),h=[...new Set(M(o.capabilities))].filter(d=>!n.has(d));n.size&&M(o.capabilities).some(d=>n.has(d))&&c.push("capabilities");const p=d=>d==null||d===""||!Number.isFinite(Number(d))?null:Number(d),u=I[o.reach]!=null?o.reach:"any",g=i.costPer1k??p(o.costPer1k)??(u==="device"&&a.kind==="model"?0:null);return{key:r,engine:a,label:k(o.label||o.name||le(a),120),reach:u,capabilities:h,quality:i.quality??p(o.quality),latencyMs:i.latencyMs??p(o.latencyMs),costPer1k:g,costPerTask:p(l?.cost?.perTask),availability:p(l?.availability?.rate),available:i.available??(o.available!==!1&&o.usable!==!1),observed:c}}const le=e=>`${e.id}${e.model&&e.model!==e.id?`/${e.model}`:""}`;function Ne(e,{cards:l={},minCalls:t,jobKind:s}={}){const o=[],a=new Set;for(const r of Array.isArray(e)?e:[]){const i=_(r,{card:null,minCalls:t,jobKind:s});!i||a.has(i.key)||(a.add(i.key),o.push(l[i.key]?_(r,{card:l[i.key],minCalls:t,jobKind:s}):i))}return o}function L(e,{reach:l="any"}={}){const t=M(e?.needs?.grants).filter(c=>c!=="none"),s=M(e?.needs?.tools),o=t.some(c=>A.includes(c)),a=[],r=[];(s.length||t.length)&&(a.push("tools"),r.push("the job uses tools")),o&&r.push(`a work grant (${t.filter(c=>A.includes(c)).join(", ")}) needs an agent tool`);const i=I[l]!=null?l:"any";return i!=="any"&&r.push(`reach ≤ ${i} (the project's privacy setting)`),{capabilities:a,harness:o,reach:i,why:r}}const C=(e,l,t)=>{const s=[];e.available===!1&&s.push("unavailable right now"),I[e.reach]>I[l.reach]&&s.push(`reach ${e.reach} exceeds ${l.reach}`),l.harness&&e.engine.kind!=="harness"&&s.push("not an agent tool");const o=l.capabilities.filter(a=>!e.capabilities.includes(a));if(o.length&&!(e.engine.kind==="harness"&&o.every(a=>a==="tools"))&&s.push(`lacks ${o.join(", ")}`),t){const a=r=>(r||[]).some(i=>i===e.key||i===`${e.engine.kind}:${e.engine.id}`||i===e.engine.id||i===e.engine.model);t.allow?.length&&!a(t.allow)&&s.push("not on the policy's allow list"),t.deny?.length&&a(t.deny)&&s.push("on the policy's deny list"),t.floor?.quality!=null&&e.quality!=null&&e.quality<t.floor.quality&&s.push(`quality ${e.quality} under the floor ${t.floor.quality}`),t.floor?.availability!=null&&e.availability!=null&&e.availability<t.floor.availability&&s.push(`availability ${e.availability} under the floor ${t.floor.availability}`),t.ceiling?.costPerTask!=null&&e.costPerTask!=null&&e.costPerTask>t.ceiling.costPerTask&&s.push(`$${e.costPerTask}/task over the ceiling`),t.ceiling?.latencyMs!=null&&e.latencyMs!=null&&e.latencyMs>t.ceiling.latencyMs&&s.push(`${e.latencyMs} ms over the ceiling`)}return s},D=(e,l)=>(e?.byEngine||[]).find(t=>t.key===l)?.rating?.avg??null;function re(e,l,t){const s=n=>n.quality??.5,o=n=>n.costPerTask??n.costPer1k??null,a=Math.max(...e.map(n=>o(n)??0),0)||1,r=Math.max(...e.map(n=>n.latencyMs??0),0)||1,i=n=>{const h=D(t,n.key);return h==null?0:h-.5},c=n=>{switch(l){case"cheapest-that-clears":return-((o(n)??a)/a)+s(n)*.01;case"best-quality":return s(n)-(o(n)??a)/a*.01;case"fastest":return-((n.latencyMs??r)/r)+s(n)*.01;default:return s(n)*.5+(1-(o(n)??a)/a)*.25+(1-(n.latencyMs??r)/r)*.25}};return e.map(n=>({row:n,score:c(n)+i(n)*.05})).sort((n,h)=>h.score-n.score).map(n=>n.row)}const z=e=>`${e.key} (quality ${e.quality??"?"}${e.observed.includes("quality")?" observed":""}${e.costPerTask!=null?`, $${e.costPerTask}/task`:e.costPer1k!=null?`, $${e.costPer1k}/1k`:""}${e.latencyMs!=null?`, ${e.latencyMs} ms`:""})`;function ce(e,l,{rows:t=[],summary:s=null,need:o=null,reach:a="any",chatModel:r=null,explore:i=!1}={}){const c=o||L(l,{reach:a}),n=H(e,{chatModel:r}),h=Array.isArray(t)?t.filter(Boolean):[],p=m=>({engine:null,key:null,reasons:m,alternatives:[],exploration:!1,clears:!1});if(n.kind!=="auto"){const m=Y(n),v=h.find(x=>x.key===m)||h.find(x=>x.engine.kind===n.kind&&(n.kind==="harness"?x.engine.id===n.harnessId&&!n.model:(x.engine.id===n.model||x.engine.model===n.model)&&!n.providerId));if(!h.length)return{engine:E({kind:n.kind,id:n.kind==="harness"?n.harnessId:n.providerId||n.model,model:n.model}),key:m,reasons:[`${T(n)} — pinned by the agent; no roster to check it against`],alternatives:[],exploration:!1,clears:!0};if(!v)return p([`${T(n)} is pinned by the agent but is not installed or configured here`]);const R=C(v,c,null);return R.length?p([`${T(n)} is pinned by the agent but ${R.join("; ")}`]):{engine:v.engine,key:v.key,reasons:["pinned by the agent",...c.why],alternatives:[],exploration:!1,clears:!0}}if(!h.length)return p(["engine is auto and the roster is empty"]);const u=n.policy||{},g=[],d=h.filter(m=>{const $=C(m,c,u);return $.length&&g.push(`${m.key}: ${$.join("; ")}`),!$.length});if(!d.length)return p([`no engine clears ${c.harness?"an agent tool with ":""}${c.capabilities.join(", ")||"the requirements"}${c.reach!=="any"?` within reach ${c.reach}`:""}${u.prefer?` under ${u.prefer}`:""}`,...g.slice(0,4)]);const f=re(d,u.prefer||"balanced",s);let y=f[0],b=!1;const w=m=>m.costPerTask??m.costPer1k??null;if(i&&y.engine.kind!=="harness"){const m=f.filter($=>$.engine.kind!=="harness"&&w($)!=null&&w(y)!=null&&w($)<w(y)).sort(($,v)=>w(v)-w($));m.length&&(b=!0,y=m[0])}const N=[b?`exploration: one tier cheaper than the policy's pick (${f[0].key})`:`${(u.prefer||"balanced").replace(/-/g," ")}: ${z(y)}`,...c.why],O=D(s,y.key);return O!=null&&N.push(`this agent rated ${Math.round(O*100)}% on it before`),(u.floor?.quality!=null||u.ceiling?.costPerTask!=null||u.ceiling?.latencyMs!=null)&&N.push(`${d.length} of ${h.length} engines clear the policy`),{engine:y.engine,key:y.key,reasons:N,alternatives:f.filter(m=>m!==y).slice(0,4).map(m=>m.engine),exploration:b,clears:!0}}function ue(e){const l=new Map((e||[]).filter(Boolean).map(t=>[t.key,t]));return{qualityOf:t=>l.get(t)?.quality??null,costOf:t=>{const s=l.get(t);return s?s.costPerTask??null:null}}}function pe(e,l,{summaries:t={},rows:s=[],reach:o="any",chatModel:a=null,adjust:r=!0,now:i=Date.now()}={}){const c=L(e,{reach:o}),n=ue(s),h=(u,g,d)=>{const f=K(u,{...g,tools:g.tools||g.grants},d,r?n:{adjust:!1}),y=ce(g,u,{rows:s,summary:d,need:c,chatModel:a});return{score:f.score,reasons:[...f.reasons,...y.reasons].slice(0,8),...y.clears?{engine:y.engine}:{},covers:U(u,g)}},p=new Map((l||[]).map(u=>[u?.id,U(e,u)]));return Z(e,l,h,{cards:t,now:i}).map(u=>({...u,covers:p.get(u.agentId)!==!1}))}function U(e,l){const t=M(e?.needs?.skills);if(!t.length)return!0;const s=new Set(M(l?.skills));return t.some(o=>s.has(o))}function de(e,l,t=[],{rows:s=[],top:o=oe}={}){const a=new Map((t||[]).map(n=>[n.id,n])),r=new Map((s||[]).filter(Boolean).map(n=>[n.key,n])),i=e?.needs||{},c=(l||[]).slice(0,o).map(n=>{const h=a.get(n.agentId)||{},p=n.engine?j(n.engine):null,u=p?r.get(p):null;return`- ${n.agentId} (fit ${Math.round((n.fit||0)*100)}%)${h.purpose?` — ${k(h.purpose,160)}`:""}; skills: ${(h.skills||[]).join(", ")||"none"}; grants: ${(h.grants||[]).join(", ")||"none"}; ${p?`engine: ${u?z(u):p}`:"NOT RECRUITABLE NOW — no engine clears"}; ${(n.reasons||[]).slice(0,4).join("; ")}`});return[`You are the evaluator for the job "${k(e?.title,200)}" on project ${e?.projectId||"?"}. Recruit ONE applicant, or say none fits.`,`Brief: ${k(e?.brief,ae)}`,`Needs — skills: ${(i.skills||[]).join(", ")||"none named"}; tools: ${(i.tools||[]).join(", ")||"none named"}; grants: ${(i.grants||[]).join(", ")||"none"}${e?.budget?`; budget: ${Object.entries(e.budget).map(([n,h])=>`${h} ${n}`).join(", ")}`:""}.`,"","Applicants, best computed fit first (fit = the skills, tools and grants the job names, then the attested record, then size):",...c.length?c:["- (no one applied)"],"",'Rules: prefer the applicant that has what the job names and a record of clearing work like it on the engine shown; never pick one marked NOT RECRUITABLE NOW; a lower fit is right only when its reasons show the higher one lacks something the brief needs. When no applicant has the skills the job names, answer pick "" and propose the agent that should exist.',"",te(S)].join(`
2
+ `)}function W(e,l=[]){const t=se(e,S);if(!t)return null;const s=t.value,o=String(s.pick||"").trim(),a=o?(l||[]).find(i=>i.agentId===o):null,r=s.proposalName?{name:k(s.proposalName,60),purpose:k(s.proposalPurpose,300),skills:(s.proposalSkills||[]).map(i=>k(i,80)).filter(Boolean),grants:(s.proposalGrants||[]).map(i=>k(i,64)).filter(Boolean)}:null;return a&&a.engine?{pick:o,why:k(s.why,400)||"the evaluator's pick",confidence:F(s.confidence),proposal:null}:{pick:"",why:a?`the evaluator picked ${o}, which no engine can run right now`:k(s.why,400)||"none fits",confidence:F(s.confidence),proposal:r}}function he(e,l,{evaluation:t=null,minFit:s=q}={}){const o=l||[];if(t&&t.pick){const i=o.find(c=>c.agentId===t.pick&&c.engine);if(i)return{kind:"recruit",agentId:i.agentId,engine:i.engine,fit:i.fit,why:t.why,by:"evaluator"}}if(t&&!t.pick)return{kind:"none",why:t.why,proposal:t.proposal||P(e),by:"evaluator"};const a=o.find(i=>i.engine&&i.covers!==!1);return a&&a.fit>=s?{kind:"recruit",agentId:a.agentId,engine:a.engine,fit:a.fit,why:`best fit (${Math.round(a.fit*100)}%): ${(a.reasons||[])[0]||"meets the needs"}`,by:"fit"}:{kind:"none",why:o.length?a?`the best fit is ${Math.round(a.fit*100)}%, under the ${Math.round(s*100)}% floor: ${(a.reasons||[]).find(i=>/missing/.test(i))||a.reasons?.[0]||""}`:o.some(i=>i.engine)?`no applicant has a skill the job names (${(e?.needs?.skills||[]).join(", ")})`:`${o.length} applied but no engine clears for any of them: ${(o[0].reasons||[]).find(i=>/engine|pinned|roster/.test(i))||"see the applications"}`:"no one in the pool applies to jobs",proposal:P(e),by:"fit"}}function P(e){const l=e?.needs||{};return{name:k(e?.title,60)||"New agent",purpose:`Does jobs like "${k(e?.title,80)}".`,skills:[...l.skills||[]],grants:(l.grants||[]).length?[...l.grants]:["none"]}}function ge(e,l,{by:t="evaluator"}={}){const s=e||P(l),o=new Set(["data","web","history","mcp",...A]),a=(s.grants||[]).map(r=>String(r).toLowerCase()).filter(r=>o.has(r)||/^mcp:/.test(r));return X({name:s.name,purpose:s.purpose,skills:s.skills,prompt:`You are ${s.name}. ${s.purpose||""}
29
3
 
30
- import { fit, engineKey, normalizeEngine } from './scorecard.js';
31
- import { cardOverride, DEFAULT_MIN_CALLS } from './model-ledger.js';
32
- import { normalizeEngineSpec, engineKeyOf, describeEngine } from './engine.js';
33
- import { engineOf, agentFromForm, normalizeAgent, resolveTeam } from './agent.js';
34
- import { applyAll, jobToRole } from './job.js';
35
- import { WORK_GRANTS } from './team.js';
36
- import { defineSchema, describeSchema, coerce } from './structured.js';
37
- import { normalizeBudget } from './budget.js';
38
-
39
- export const MIN_FIT = 0.5;
40
- export const TOP_APPLICANTS = 5;
41
- export const MAX_BRIEF_IN_PROMPT = 1500;
42
- const REACH_RANK = { device: 0, trusted: 1, any: 2 };
43
-
44
- const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
45
- const r3 = (v) => (v == null || !Number.isFinite(v) ? null : Math.round(v * 1000) / 1000);
46
- const clip = (s, n) => String(s || '').trim().slice(0, n);
47
- const lower = (xs) => (Array.isArray(xs) ? xs : []).map((x) => String(x).toLowerCase());
48
-
49
- // ── The evaluator's answer ────────────────────────────────────────────────────────────────
50
-
51
- export const RECRUIT_SCHEMA = defineSchema({
52
- name: 'recruit',
53
- purpose: 'which applicant gets the job, or that none fits and what agent should exist',
54
- fields: {
55
- // Not `required`: an emptied required field reads as "nothing" and would drop the proposal that rides beside it.
56
- pick: { type: 'string', max: 64, describe: 'the id of the ONE applicant to recruit, or "" when none fits' },
57
- why: { type: 'string', required: true, max: 400, describe: 'one or two sentences a person reads on the job page' },
58
- confidence: { type: 'number', describe: '0 to 1' },
59
- proposalName: { type: 'string', max: 60, describe: 'when none fits: the agent that should exist' },
60
- proposalPurpose: { type: 'string', max: 300 },
61
- proposalSkills: { type: 'string[]', maxItems: 12 },
62
- proposalGrants: { type: 'string[]', maxItems: 8, describe: 'from: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr' },
63
- },
64
- nothing: { pick: '', why: 'none fits' },
65
- });
66
-
67
- // ── Engine rows: what the host can run, as the recruiter reads it ─────────────────────────
68
-
69
- /**
70
- * One row from a host's candidate (a router model — `inferCandidate`'s shape: `id`, `model`,
71
- * `reach`, `capabilities`, `quality`, `latencyMs`, `costPer1k`, `available`, `classUsed`;
72
- * or anything carrying an `engine` ref) with its engine CARD applied: observed quality,
73
- * latency, cost and availability replace the guess where there is enough history, and a
74
- * withdrawn capability is gone. A bridge agent (`classUsed 'A'` / `kind 'bridge'`) is a
75
- * harness — `claude/opus` is the harness `claude` asked to run `opus` — keyed the way the
76
- * desktop's appointer and the engine ledger key it.
77
- */
78
- export function engineRow(candidate, { card = null, minCalls = DEFAULT_MIN_CALLS, jobKind = null } = {}) {
79
- if (!isRecord(candidate)) return null;
80
- const c = candidate;
81
- let engine = c.engine ? normalizeEngine(c.engine) : null;
82
- if (!engine) {
83
- const name = String(c.model || c.id || '');
84
- if (!name) return null;
85
- if (c.kind === 'bridge' || c.kind === 'harness' || c.classUsed === 'A') {
86
- const slash = name.indexOf('/');
87
- engine = slash > 0 ? { kind: 'harness', id: name.slice(0, slash), model: name.slice(slash + 1) } : { kind: 'harness', id: name };
88
- } else engine = { kind: 'model', id: name };
89
- }
90
- const key = engineKey(engine);
91
- if (!key) return null;
92
- const { override, observed } = cardOverride(card, { minCalls, jobKind });
93
- const withdrawn = new Set(card?.capabilities?.withdrawn || []);
94
- const capabilities = [...new Set(lower(c.capabilities))].filter((x) => !withdrawn.has(x));
95
- if (withdrawn.size && lower(c.capabilities).some((x) => withdrawn.has(x))) observed.push('capabilities');
96
- const num = (v) => (v == null || v === '' || !Number.isFinite(Number(v)) ? null : Number(v));
97
- const reach = REACH_RANK[c.reach] != null ? c.reach : 'any';
98
- // A model on this machine costs nothing per token — the router's own rule (`costOf`), kept
99
- // here for a host that has no guess to offer; an unknown cost anywhere else stays unknown
100
- // and orders as the dearest, so "we did not price it" never reads as "free".
101
- const costPer1k = override.costPer1k ?? num(c.costPer1k) ?? (reach === 'device' && engine.kind === 'model' ? 0 : null);
102
- return {
103
- key,
104
- engine,
105
- label: clip(c.label || c.name || engineName(engine), 120),
106
- reach,
107
- capabilities,
108
- quality: override.quality ?? num(c.quality),
109
- latencyMs: override.latencyMs ?? num(c.latencyMs),
110
- costPer1k,
111
- costPerTask: num(card?.cost?.perTask),
112
- availability: num(card?.availability?.rate),
113
- available: override.available ?? (c.available !== false && c.usable !== false),
114
- observed,
115
- };
116
- }
117
- const engineName = (e) => `${e.id}${e.model && e.model !== e.id ? `/${e.model}` : ''}`;
118
-
119
- /** Rows from a host's candidates and the cards it holds (by key). */
120
- export function engineRows(candidates, { cards = {}, minCalls, jobKind } = {}) {
121
- const out = []; const seen = new Set();
122
- for (const c of Array.isArray(candidates) ? candidates : []) {
123
- const row = engineRow(c, { card: null, minCalls, jobKind });
124
- if (!row || seen.has(row.key)) continue;
125
- seen.add(row.key);
126
- out.push(cards[row.key] ? engineRow(c, { card: cards[row.key], minCalls, jobKind }) : row);
127
- }
128
- return out;
129
- }
130
-
131
- // ── What the job needs of an engine ───────────────────────────────────────────────────────
132
-
133
- /**
134
- * Requirements eliminate; they are never traded for cost or speed. A work grant (`shell`,
135
- * `fs:write`, `scm:*`) can only be exercised by a harness — a chat model has no shell. Tools
136
- * or any grant beyond `none` mean the turn may call tools. Reach is the project's privacy
137
- * ceiling, typed, never learned.
138
- */
139
- export function needForJob(job, { reach = 'any' } = {}) {
140
- const grants = lower(job?.needs?.grants).filter((g) => g !== 'none');
141
- const tools = lower(job?.needs?.tools);
142
- const harness = grants.some((g) => WORK_GRANTS.includes(g));
143
- const capabilities = [];
144
- const why = [];
145
- if (tools.length || grants.length) { capabilities.push('tools'); why.push('the job uses tools'); }
146
- if (harness) why.push(`a work grant (${grants.filter((g) => WORK_GRANTS.includes(g)).join(', ')}) needs an agent tool`);
147
- const r = REACH_RANK[reach] != null ? reach : 'any';
148
- if (r !== 'any') why.push(`reach ≤ ${r} (the project's privacy setting)`);
149
- return { capabilities, harness, reach: r, why };
150
- }
151
-
152
- // ── Routing: the engine for this agent on this job ────────────────────────────────────────
153
-
154
- const meets = (row, need, policy) => {
155
- const why = [];
156
- if (row.available === false) why.push('unavailable right now');
157
- if (REACH_RANK[row.reach] > REACH_RANK[need.reach]) why.push(`reach ${row.reach} exceeds ${need.reach}`);
158
- if (need.harness && row.engine.kind !== 'harness') why.push('not an agent tool');
159
- const missing = need.capabilities.filter((c) => !row.capabilities.includes(c));
160
- // A harness brings its own tools; the capability list of a bridge agent is the host's guess.
161
- if (missing.length && !(row.engine.kind === 'harness' && missing.every((c) => c === 'tools'))) why.push(`lacks ${missing.join(', ')}`);
162
- if (policy) {
163
- const matches = (refs) => (refs || []).some((k) => k === row.key || k === `${row.engine.kind}:${row.engine.id}` || k === row.engine.id || k === row.engine.model);
164
- if (policy.allow?.length && !matches(policy.allow)) why.push('not on the policy\'s allow list');
165
- if (policy.deny?.length && matches(policy.deny)) why.push('on the policy\'s deny list');
166
- if (policy.floor?.quality != null && row.quality != null && row.quality < policy.floor.quality) why.push(`quality ${row.quality} under the floor ${policy.floor.quality}`);
167
- if (policy.floor?.availability != null && row.availability != null && row.availability < policy.floor.availability) why.push(`availability ${row.availability} under the floor ${policy.floor.availability}`);
168
- if (policy.ceiling?.costPerTask != null && row.costPerTask != null && row.costPerTask > policy.ceiling.costPerTask) why.push(`$${row.costPerTask}/task over the ceiling`);
169
- if (policy.ceiling?.latencyMs != null && row.latencyMs != null && row.latencyMs > policy.ceiling.latencyMs) why.push(`${row.latencyMs} ms over the ceiling`);
170
- }
171
- return why;
172
- };
173
-
174
- const ownRating = (summary, key) => (summary?.byEngine || []).find((r) => r.key === key)?.rating?.avg ?? null;
175
-
176
- /** Order the rows that clear by the policy's preference; the agent's own record on an engine breaks ties. */
177
- function orderByPolicy(rows, prefer, summary) {
178
- const q = (r) => r.quality ?? 0.5;
179
- const cost = (r) => r.costPerTask ?? r.costPer1k ?? null;
180
- const maxCost = Math.max(...rows.map((r) => cost(r) ?? 0), 0) || 1;
181
- const maxLat = Math.max(...rows.map((r) => r.latencyMs ?? 0), 0) || 1;
182
- const own = (r) => { const v = ownRating(summary, r.key); return v == null ? 0 : v - 0.5; };
183
- const score = (r) => {
184
- switch (prefer) {
185
- case 'cheapest-that-clears': return -((cost(r) ?? maxCost) / maxCost) + q(r) * 0.01;
186
- case 'best-quality': return q(r) - ((cost(r) ?? maxCost) / maxCost) * 0.01;
187
- case 'fastest': return -((r.latencyMs ?? maxLat) / maxLat) + q(r) * 0.01;
188
- default: return q(r) * 0.5 + (1 - (cost(r) ?? maxCost) / maxCost) * 0.25 + (1 - (r.latencyMs ?? maxLat) / maxLat) * 0.25;
189
- }
190
- };
191
- return rows.map((r) => ({ row: r, score: score(r) + own(r) * 0.05 })).sort((a, b) => b.score - a.score).map((x) => x.row);
192
- }
193
-
194
- const rowLine = (r) => `${r.key} (quality ${r.quality ?? '?'}${r.observed.includes('quality') ? ' observed' : ''}${r.costPerTask != null ? `, $${r.costPerTask}/task` : r.costPer1k != null ? `, $${r.costPer1k}/1k` : ''}${r.latencyMs != null ? `, ${r.latencyMs} ms` : ''})`;
195
-
196
- /**
197
- * The engine this agent would run this job on, and why — `{ engine, key, reasons,
198
- * alternatives, exploration, clears }`; `clears: false` (engine null) when nothing does, with
199
- * the reasons. `rows` are the host's `engineRows`; an empty roster trusts a fixed spec and
200
- * refuses `auto` (nothing to pick from). `explore` takes one tier cheaper than the policy's
201
- * pick when a cheaper row clears — the project loop's bounded exploration (§13.4); never for
202
- * a harness.
203
- */
204
- export function routeFor(agent, job, { rows = [], summary = null, need = null, reach = 'any', chatModel = null, explore = false } = {}) {
205
- const n = need || needForJob(job, { reach });
206
- const spec = engineOf(agent, { chatModel });
207
- const list = Array.isArray(rows) ? rows.filter(Boolean) : [];
208
- const none = (reasons) => ({ engine: null, key: null, reasons, alternatives: [], exploration: false, clears: false });
209
- if (spec.kind !== 'auto') {
210
- const key = engineKeyOf(spec);
211
- const exact = list.find((r) => r.key === key);
212
- // A harness card without a model matches any row of that harness; a model spec without a
213
- // provider matches the row that runs that model anywhere.
214
- const near = exact || list.find((r) => r.engine.kind === spec.kind && (spec.kind === 'harness' ? r.engine.id === spec.harnessId && !spec.model : (r.engine.id === spec.model || r.engine.model === spec.model) && !spec.providerId));
215
- if (!list.length) return { engine: normalizeEngine({ kind: spec.kind, id: spec.kind === 'harness' ? spec.harnessId : (spec.providerId || spec.model), model: spec.model }), key, reasons: [`${describeEngine(spec)} — pinned by the agent; no roster to check it against`], alternatives: [], exploration: false, clears: true };
216
- if (!near) return none([`${describeEngine(spec)} is pinned by the agent but is not installed or configured here`]);
217
- const why = meets(near, n, null);
218
- if (why.length) return none([`${describeEngine(spec)} is pinned by the agent but ${why.join('; ')}`]);
219
- return { engine: near.engine, key: near.key, reasons: ['pinned by the agent', ...n.why], alternatives: [], exploration: false, clears: true };
220
- }
221
- if (!list.length) return none(['engine is auto and the roster is empty']);
222
- const policy = spec.policy || {};
223
- const rejected = [];
224
- const cleared = list.filter((r) => { const why = meets(r, n, policy); if (why.length) rejected.push(`${r.key}: ${why.join('; ')}`); return !why.length; });
225
- if (!cleared.length) return none([`no engine clears ${n.harness ? 'an agent tool with ' : ''}${n.capabilities.join(', ') || 'the requirements'}${n.reach !== 'any' ? ` within reach ${n.reach}` : ''}${policy.prefer ? ` under ${policy.prefer}` : ''}`, ...rejected.slice(0, 4)]);
226
- const ordered = orderByPolicy(cleared, policy.prefer || 'balanced', summary);
227
- let pick = ordered[0];
228
- let exploration = false;
229
- const cost = (r) => r.costPerTask ?? r.costPer1k ?? null;
230
- if (explore && pick.engine.kind !== 'harness') {
231
- const cheaper = ordered.filter((r) => r.engine.kind !== 'harness' && cost(r) != null && cost(pick) != null && cost(r) < cost(pick)).sort((a, b) => cost(b) - cost(a));
232
- if (cheaper.length) { exploration = true; pick = cheaper[0]; }
233
- }
234
- const reasons = [
235
- exploration ? `exploration: one tier cheaper than the policy's pick (${ordered[0].key})` : `${(policy.prefer || 'balanced').replace(/-/g, ' ')}: ${rowLine(pick)}`,
236
- ...n.why,
237
- ];
238
- const own = ownRating(summary, pick.key);
239
- if (own != null) reasons.push(`this agent rated ${Math.round(own * 100)}% on it before`);
240
- if (policy.floor?.quality != null || policy.ceiling?.costPerTask != null || policy.ceiling?.latencyMs != null) reasons.push(`${cleared.length} of ${list.length} engines clear the policy`);
241
- return { engine: pick.engine, key: pick.key, reasons, alternatives: ordered.filter((r) => r !== pick).slice(0, 4).map((r) => r.engine), exploration, clears: true };
242
- }
243
-
244
- // ── Applying: the whole pool at once ──────────────────────────────────────────────────────
245
-
246
- /** `qualityOf` / `costOf` for `adjustSummary`, read from the rows: what each engine is worth. */
247
- export function engineWorth(rows) {
248
- const byKey = new Map((rows || []).filter(Boolean).map((r) => [r.key, r]));
249
- return {
250
- qualityOf: (key) => byKey.get(key)?.quality ?? null,
251
- costOf: (key) => { const r = byKey.get(key); return r ? (r.costPerTask ?? null) : null; },
252
- };
253
- }
254
-
255
- /**
256
- * Every eligible agent applies: `fit` (needs → adjusted record → size) plus the engine it
257
- * would run on. `summaries` are scorecard cards by agent id (`summarize()`); `rows` are
258
- * `engineRows`. Best first, recruitable (an engine clears) before not. Each application is
259
- * `{ agentId, engine?, fit, reasons, pitch, at, covers }` — job.js's shape, the record's, plus
260
- * `covers` (has a skill the job names) for `decide`; the record drops it.
261
- */
262
- export function applications(job, pool, { summaries = {}, rows = [], reach = 'any', chatModel = null, adjust = true, now = Date.now() } = {}) {
263
- const need = needForJob(job, { reach });
264
- const worth = engineWorth(rows);
265
- const fitFn = (j, agent, summary) => {
266
- const f = fit(j, { ...agent, tools: agent.tools || agent.grants }, summary, adjust ? worth : { adjust: false });
267
- const route = routeFor(agent, j, { rows, summary, need, chatModel });
268
- return { score: f.score, reasons: [...f.reasons, ...route.reasons].slice(0, 8), ...(route.clears ? { engine: route.engine } : {}), covers: coversSkills(j, agent) };
269
- };
270
- // `covers` — has at least one skill the job names (or the job names none) — rides on the
271
- // live application for `decide`; the record keeps job.js's shape and drops it.
272
- const covered = new Map((pool || []).map((a) => [a?.id, coversSkills(job, a)]));
273
- return applyAll(job, pool, fitFn, { cards: summaries, now }).map((a) => ({ ...a, covers: covered.get(a.agentId) !== false }));
274
- }
275
-
276
- /** A job that names skills is not given to an agent with none of them on fit alone: tools and grants it never asked for count for nothing. */
277
- function coversSkills(job, agent) {
278
- const want = lower(job?.needs?.skills);
279
- if (!want.length) return true;
280
- const has = new Set(lower(agent?.skills));
281
- return want.some((s) => has.has(s));
282
- }
283
-
284
- // ── Evaluating: one structured call, or none ──────────────────────────────────────────────
285
-
286
- /** The evaluator's instruction: the job, the top applicants with their fit, engine and reasons, the shape to answer in. */
287
- export function evaluatorPrompt(job, apps, pool = [], { rows = [], top = TOP_APPLICANTS } = {}) {
288
- const byId = new Map((pool || []).map((a) => [a.id, a]));
289
- const rowOf = new Map((rows || []).filter(Boolean).map((r) => [r.key, r]));
290
- const needs = job?.needs || {};
291
- const lines = (apps || []).slice(0, top).map((a) => {
292
- const agent = byId.get(a.agentId) || {};
293
- const key = a.engine ? engineKey(a.engine) : null;
294
- const row = key ? rowOf.get(key) : null;
295
- return `- ${a.agentId} (fit ${Math.round((a.fit || 0) * 100)}%)${agent.purpose ? ` — ${clip(agent.purpose, 160)}` : ''}; skills: ${(agent.skills || []).join(', ') || 'none'}; grants: ${(agent.grants || []).join(', ') || 'none'}; ${key ? `engine: ${row ? rowLine(row) : key}` : 'NOT RECRUITABLE NOW — no engine clears'}; ${(a.reasons || []).slice(0, 4).join('; ')}`;
296
- });
297
- return [
298
- `You are the evaluator for the job "${clip(job?.title, 200)}" on project ${job?.projectId || '?'}. Recruit ONE applicant, or say none fits.`,
299
- `Brief: ${clip(job?.brief, MAX_BRIEF_IN_PROMPT)}`,
300
- `Needs — skills: ${(needs.skills || []).join(', ') || 'none named'}; tools: ${(needs.tools || []).join(', ') || 'none named'}; grants: ${(needs.grants || []).join(', ') || 'none'}${job?.budget ? `; budget: ${Object.entries(job.budget).map(([k, v]) => `${v} ${k}`).join(', ')}` : ''}.`,
301
- '',
302
- 'Applicants, best computed fit first (fit = the skills, tools and grants the job names, then the attested record, then size):',
303
- ...(lines.length ? lines : ['- (no one applied)']),
304
- '',
305
- 'Rules: prefer the applicant that has what the job names and a record of clearing work like it on the engine shown; never pick one marked NOT RECRUITABLE NOW; a lower fit is right only when its reasons show the higher one lacks something the brief needs. When no applicant has the skills the job names, answer pick "" and propose the agent that should exist.',
306
- '',
307
- describeSchema(RECRUIT_SCHEMA),
308
- ].join('\n');
309
- }
310
-
311
- /**
312
- * The evaluator's answer, read through the schema and checked against the applications: a
313
- * pick must be a recruitable applicant, else it is "none". Returns `{ pick, why, confidence,
314
- * proposal }` or null when the text is unreadable.
315
- */
316
- export function parseEvaluation(text, apps = []) {
317
- const got = coerce(text, RECRUIT_SCHEMA);
318
- if (!got) return null;
319
- const v = got.value;
320
- const pick = String(v.pick || '').trim();
321
- const app = pick ? (apps || []).find((a) => a.agentId === pick) : null;
322
- const proposal = v.proposalName ? { name: clip(v.proposalName, 60), purpose: clip(v.proposalPurpose, 300), skills: (v.proposalSkills || []).map((s) => clip(s, 80)).filter(Boolean), grants: (v.proposalGrants || []).map((g) => clip(g, 64)).filter(Boolean) } : null;
323
- if (app && app.engine) return { pick, why: clip(v.why, 400) || 'the evaluator\'s pick', confidence: r3(v.confidence), proposal: null };
324
- return { pick: '', why: app ? `the evaluator picked ${pick}, which no engine can run right now` : clip(v.why, 400) || 'none fits', confidence: r3(v.confidence), proposal };
325
- }
326
-
327
- /**
328
- * The decision: the evaluator's when one was made; else the best recruitable fit at or above
329
- * `minFit`; else none, with the agent the job's needs describe as the proposal. Returns
330
- * `{ kind: 'recruit', agentId, engine, fit, why, by }` or `{ kind: 'none', why, proposal, by }`.
331
- */
332
- export function decide(job, apps, { evaluation = null, minFit = MIN_FIT } = {}) {
333
- const list = apps || [];
334
- if (evaluation && evaluation.pick) {
335
- const app = list.find((a) => a.agentId === evaluation.pick && a.engine);
336
- if (app) return { kind: 'recruit', agentId: app.agentId, engine: app.engine, fit: app.fit, why: evaluation.why, by: 'evaluator' };
337
- }
338
- if (evaluation && !evaluation.pick) return { kind: 'none', why: evaluation.why, proposal: evaluation.proposal || proposalFromNeeds(job), by: 'evaluator' };
339
- const best = list.find((a) => a.engine && a.covers !== false);
340
- if (best && best.fit >= minFit) return { kind: 'recruit', agentId: best.agentId, engine: best.engine, fit: best.fit, why: `best fit (${Math.round(best.fit * 100)}%): ${(best.reasons || [])[0] || 'meets the needs'}`, by: 'fit' };
341
- const why = !list.length ? 'no one in the pool applies to jobs'
342
- : !best ? (list.some((a) => a.engine) ? `no applicant has a skill the job names (${(job?.needs?.skills || []).join(', ')})` : `${list.length} applied but no engine clears for any of them: ${(list[0].reasons || []).find((r) => /engine|pinned|roster/.test(r)) || 'see the applications'}`)
343
- : `the best fit is ${Math.round(best.fit * 100)}%, under the ${Math.round(minFit * 100)}% floor: ${(best.reasons || []).find((r) => /missing/.test(r)) || best.reasons?.[0] || ''}`;
344
- return { kind: 'none', why, proposal: proposalFromNeeds(job), by: 'fit' };
345
- }
346
-
347
- /** The agent a job's needs describe — what to propose when no one fits. */
348
- export function proposalFromNeeds(job) {
349
- const needs = job?.needs || {};
350
- return { name: clip(job?.title, 60) || 'New agent', purpose: `Does jobs like "${clip(job?.title, 80)}".`, skills: [...(needs.skills || [])], grants: (needs.grants || []).length ? [...needs.grants] : ['none'] };
351
- }
352
-
353
- /**
354
- * A proposal as an agent card for a person to approve — validated by the pool's own form
355
- * (agentFromForm), engine `auto`, `createdBy: 'evaluator'`, its origin the job. Nothing
356
- * joins the pool without a decision (D-A2): this returns the card, it does not store it.
357
- */
358
- export function proposalToAgent(proposal, job, { by = 'evaluator' } = {}) {
359
- const p = proposal || proposalFromNeeds(job);
360
- const known = new Set(['data', 'web', 'history', 'mcp', ...WORK_GRANTS]);
361
- const grants = (p.grants || []).map((g) => String(g).toLowerCase()).filter((g) => known.has(g) || /^mcp:/.test(g));
362
- return agentFromForm({
363
- name: p.name, purpose: p.purpose, skills: p.skills,
364
- prompt: `You are ${p.name}. ${p.purpose || ''}\n\nYou were proposed for the job "${clip(job?.title, 200)}" because no agent in the pool fit it. Do work like it well; ask on the thread when the brief is unclear.`,
365
- grants: grants.length ? grants : ['none'], engine: { kind: 'auto', prefer: 'balanced' }, appliesTo: ['jobs'], createdBy: by,
366
- origin: { kind: 'proposal', projectId: job?.projectId, jobId: job?.id },
367
- });
368
- }
369
-
370
- // ── Landing it on the record ──────────────────────────────────────────────────────────────
371
-
372
- /**
373
- * The budget a recruit gets: the job's own when it has one, else an equal share of what the
374
- * project has left (its budget minus its spend) across the jobs still to be recruited — so
375
- * one unbudgeted job cannot take the whole project. `record` is the project record
376
- * (project.js `foldProject`).
377
- */
378
- export function carveBudget(job, record = null) {
379
- if (job?.budget && Object.keys(job.budget).length) return normalizeBudget(job.budget);
380
- const cap = record?.page?.budget || {};
381
- const spent = record?.spend || {};
382
- const waiting = Math.max(1, (record?.jobs || []).filter((j) => ['open', 'evaluating'].includes(j.status)).length);
383
- const out = {};
384
- for (const k of ['tokens', 'calls', 'ms', 'usd']) {
385
- if (!(Number(cap[k]) > 0)) continue;
386
- const left = Math.max(0, Number(cap[k]) - (Number(spent[k]) || 0));
387
- if (left > 0) out[k] = k === 'usd' ? Math.round((left / waiting) * 100) / 100 : Math.max(1, Math.floor(left / waiting));
388
- }
389
- return Object.keys(out).length ? out : null;
390
- }
391
-
392
- /**
393
- * The events that record a recruiting pass on the project (project.js fold): the job moves
394
- * to `evaluating` with its applications, then to `recruited` with the pair, the budget and
395
- * the why — or back to `open`, with the proposal as a decision a person reads.
396
- */
397
- export function recruitEvents(job, apps, decision, { by = 'evaluator', at = Date.now(), record = null } = {}) {
398
- const events = [{ type: 'job.updated', at, job: { id: job.id, status: 'evaluating', applications: (apps || []).map(({ covers: _c, ...a }) => a) }, by }];
399
- if (decision?.kind === 'recruit') {
400
- const budget = carveBudget(job, record);
401
- events.push({ type: 'job.updated', at, job: { id: job.id, status: 'recruited', recruited: { agentId: decision.agentId, engine: decision.engine, ...(budget ? { budget } : {}), by: decision.by || by, at, why: clip(decision.why, 600) } }, by });
402
- } else {
403
- events.push({ type: 'job.updated', at, job: { id: job.id, status: 'open' }, by });
404
- const p = decision?.proposal;
405
- events.push({ type: 'project.decision', at, by, kind: 'proposal', text: `No one in the pool fits "${clip(job.title, 120)}": ${clip(decision?.why, 400)}${p ? ` Proposed: ${p.name}${p.skills?.length ? ` — skills ${p.skills.join(', ')}` : ''}${p.grants?.length ? `; grants ${p.grants.join(', ')}` : ''}.` : ''}`, refs: [`job:${job.id}`] });
406
- }
407
- return events;
408
- }
409
-
410
- /**
411
- * One pass, end to end: apply → (evaluate) → decide → the events. `ask(prompt) → text |
412
- * null` is the host's structured call; absent or failing, the fit decides. Returns
413
- * `{ applications, evaluation, decision, events, prompt }`.
414
- */
415
- export async function recruitJob(job, pool, { summaries = {}, rows = [], reach = 'any', chatModel = null, record = null, ask = null, minFit = MIN_FIT, by = 'evaluator', now = Date.now() } = {}) {
416
- const apps = applications(job, pool, { summaries, rows, reach, chatModel, now });
417
- const prompt = evaluatorPrompt(job, apps, pool, { rows });
418
- let evaluation = null;
419
- if (ask && apps.some((a) => a.engine)) {
420
- try { const text = await ask(prompt, RECRUIT_SCHEMA); evaluation = text == null ? null : (typeof text === 'string' ? parseEvaluation(text, apps) : parseEvaluation(JSON.stringify(text), apps)); } catch { evaluation = null; }
421
- }
422
- const decision = decide(job, apps, { evaluation, minFit });
423
- return { applications: apps, evaluation, decision, events: recruitEvents(job, apps, decision, { by: decision.by === 'evaluator' ? by : 'fit', at: now, record }), prompt };
424
- }
425
-
426
- // ── A run's job board (§15.2): the host's `recruit` hook, shared ─────────────────────────
427
-
428
- /**
429
- * What a host hands the runner as `recruit`: one pass over the pool for a sub-task's job,
430
- * the pick made into a ROLE the run can appoint (job.js `jobToRole`, resolved through the
431
- * pool so it carries the agent's prompt, grants, skills and a model target — agent.js
432
- * `resolveTeam`). Returns `{ role, agentId, engine, why, fit, applications, events }` or,
433
- * when no one is recruited, `{ why, proposal, applications, events }` with `proposal` the
434
- * agent card a person may approve (the runner posts it). `create` is that card once
435
- * approved — the host has persisted it to its pool before calling — and it applies with
436
- * the rest. `events` are the project-record events of the pass (recruit.js `recruitEvents`)
437
- * for a host that has a project to land them on; the run's own record has its own.
438
- */
439
- export async function recruitForRun(job, pool, { rows = [], summaries = {}, reach = 'any', chatModel = null, targetFor = null, ask = null, minFit = MIN_FIT, create = null, now = Date.now() } = {}) {
440
- const list = (Array.isArray(pool) ? pool : []).filter((a) => a && a.id);
441
- if (create && isRecord(create)) {
442
- try { const made = normalizeAgent(create); if (!list.some((a) => a.id === made.id)) list.push(made); } catch { /* an unusable card applies as nothing */ }
443
- }
444
- const pass = await recruitJob(job, list, { summaries, rows, reach, chatModel, ask, minFit, now });
445
- const d = pass.decision;
446
- if (d.kind !== 'recruit') {
447
- const card = proposalToAgent(d.proposal || proposalFromNeeds(job), job, { by: d.by || 'evaluator' });
448
- return { why: d.why, proposal: card?.ok === false ? null : (card?.agent || card), applications: pass.applications, events: pass.events };
449
- }
450
- const agent = list.find((a) => a.id === d.agentId);
451
- const base = jobToRole(job, agent);
452
- let role = { ...base, agent: agent.id, engine: d.engine };
453
- try {
454
- role = resolveTeam({ name: 'recruit', roles: [role], budget: { tokens: 1 } }, list, { chatModel, targetFor }).roles[0];
455
- } catch { /* unresolved: the runner's appointer decides from the engine */ }
456
- return { role, agentId: d.agentId, engine: d.engine, why: d.why, fit: d.fit ?? null, applications: pass.applications, events: pass.events };
457
- }
4
+ You were proposed for the job "${k(l?.title,200)}" because no agent in the pool fit it. Do work like it well; ask on the thread when the brief is unclear.`,grants:a.length?a:["none"],engine:{kind:"auto",prefer:"balanced"},appliesTo:["jobs"],createdBy:t,origin:{kind:"proposal",projectId:l?.projectId,jobId:l?.id}})}function fe(e,l=null){if(e?.budget&&Object.keys(e.budget).length)return ie(e.budget);const t=l?.page?.budget||{},s=l?.spend||{},o=Math.max(1,(l?.jobs||[]).filter(r=>["open","evaluating"].includes(r.status)).length),a={};for(const r of["tokens","calls","ms","usd"]){if(!(Number(t[r])>0))continue;const i=Math.max(0,Number(t[r])-(Number(s[r])||0));i>0&&(a[r]=r==="usd"?Math.round(i/o*100)/100:Math.max(1,Math.floor(i/o)))}return Object.keys(a).length?a:null}function ye(e,l,t,{by:s="evaluator",at:o=Date.now(),record:a=null}={}){const r=[{type:"job.updated",at:o,job:{id:e.id,status:"evaluating",applications:(l||[]).map(({covers:i,...c})=>c)},by:s}];if(t?.kind==="recruit"){const i=fe(e,a);r.push({type:"job.updated",at:o,job:{id:e.id,status:"recruited",recruited:{agentId:t.agentId,engine:t.engine,...i?{budget:i}:{},by:t.by||s,at:o,why:k(t.why,600)}},by:s})}else{r.push({type:"job.updated",at:o,job:{id:e.id,status:"open"},by:s});const i=t?.proposal;r.push({type:"project.decision",at:o,by:s,kind:"proposal",text:`No one in the pool fits "${k(e.title,120)}": ${k(t?.why,400)}${i?` Proposed: ${i.name}${i.skills?.length?` — skills ${i.skills.join(", ")}`:""}${i.grants?.length?`; grants ${i.grants.join(", ")}`:""}.`:""}`,refs:[`job:${e.id}`]})}return r}async function me(e,l,{summaries:t={},rows:s=[],reach:o="any",chatModel:a=null,record:r=null,ask:i=null,minFit:c=q,by:n="evaluator",now:h=Date.now()}={}){const p=pe(e,l,{summaries:t,rows:s,reach:o,chatModel:a,now:h}),u=de(e,p,l,{rows:s});let g=null;if(i&&p.some(f=>f.engine))try{const f=await i(u,S);g=f==null?null:W(typeof f=="string"?f:JSON.stringify(f),p)}catch{g=null}const d=he(e,p,{evaluation:g,minFit:c});return{applications:p,evaluation:g,decision:d,events:ye(e,p,d,{by:d.by==="evaluator"?n:"fit",at:h,record:r}),prompt:u}}async function Te(e,l,{rows:t=[],summaries:s={},reach:o="any",chatModel:a=null,targetFor:r=null,ask:i=null,minFit:c=q,create:n=null,now:h=Date.now()}={}){const p=(Array.isArray(l)?l:[]).filter(b=>b&&b.id);if(n&&B(n))try{const b=Q(n);p.some(w=>w.id===b.id)||p.push(b)}catch{}const u=await me(e,p,{summaries:s,rows:t,reach:o,chatModel:a,ask:i,minFit:c,now:h}),g=u.decision;if(g.kind!=="recruit"){const b=ge(g.proposal||P(e),e,{by:g.by||"evaluator"});return{why:g.why,proposal:b?.ok===!1?null:b?.agent||b,applications:u.applications,events:u.events}}const d=p.find(b=>b.id===g.agentId);let y={...ee(e,d),agent:d.id,engine:g.engine};try{y=V({name:"recruit",roles:[y],budget:{tokens:1}},p,{chatModel:a,targetFor:r}).roles[0]}catch{}return{role:y,agentId:g.agentId,engine:g.engine,why:g.why,fit:g.fit??null,applications:u.applications,events:u.events}}export{ae as MAX_BRIEF_IN_PROMPT,q as MIN_FIT,S as RECRUIT_SCHEMA,oe as TOP_APPLICANTS,pe as applications,fe as carveBudget,he as decide,_ as engineRow,Ne as engineRows,ue as engineWorth,de as evaluatorPrompt,L as needForJob,W as parseEvaluation,P as proposalFromNeeds,ge as proposalToAgent,ye as recruitEvents,Te as recruitForRun,me as recruitJob,ce as routeFor};
@@ -1,61 +1 @@
1
- // Recognising a redaction placeholder — one predicate, and nothing else in the module.
2
- //
3
- // It is its own file for the reason `distance.js` is: three unrelated places need this
4
- // question answered — the subject resolver, the wikilink parser, and the extension's note
5
- // index — and the first two live in `entity.js`, which has since grown entity resolution,
6
- // merge suggestions and a Levenshtein dependency. Importing 25 KB of that into a notes page
7
- // to ask "is this string a placeholder" is the same 120 KB mistake in a smaller coat.
8
-
9
- /**
10
- * A REDACTION PLACEHOLDER IS NOT A SUBJECT — and this is the sharpest edge in the module.
11
- *
12
- * `@chatpanel/pii` writes `[[PERSON_1]]`, `[[EMAIL_2]]`, `[[LOCATION_1]]`, which is
13
- * character-for-character the `[[wikilink]]` grammar. So a redacted transcript reads as a
14
- * document full of links to pages that do not exist, and every one of them earned a "wanted
15
- * page" brief. The name of a person we deliberately did not learn was being filed as a thing
16
- * we know about.
17
- *
18
- * The tempting fix is to treat the token as a pseudonymous identity — it IS stable, and
19
- * within one conversation `PERSON_1` really does mean one person. It must not become a
20
- * subject anyway, because **the vault is scoped to a conversation**: `PERSON_1` in Monday's
21
- * chat and `PERSON_1` in Friday's are different people, and a global subject would merge
22
- * strangers under one page and attribute one person's decisions to another. That is the same
23
- * reason `aliasMap` refuses a bare token two people could claim — here it is guaranteed
24
- * rather than possible.
25
- *
26
- * So the placeholder is dropped from subject candidacy, and `curate.js` counts what was
27
- * dropped so the loss is REPORTED rather than silent. Never resolved against a vault into
28
- * anything derived and persisted, either: that would put the PII back on disk in a second
29
- * place, which is the one thing redaction exists to prevent.
30
- *
31
- * The pattern is duplicated from `@chatpanel/pii` rather than imported — this package ships
32
- * zero dependencies so the bridge can vendor it file by file, the same constraint that makes
33
- * the entitlement JWK live in two clients. `CLAUDE.md` lists `[[TYPE_n]]` as a wire contract
34
- * that only changes additively, and the extension (which vendors both) carries the drift
35
- * guard that fails when these two stop agreeing.
36
- */
37
- export const REDACTION_TOKEN_TYPES = Object.freeze([
38
- 'PERSON', 'ORG', 'LOCATION', 'ADDRESS', 'EMAIL', 'PHONE', 'ID', 'SSN', 'IBAN',
39
- 'CREDITCARD', 'CARD', 'POST', 'FAC', 'GROUP', 'NRP', 'ENTITY', 'KEY', 'SECRET',
40
- 'TERM', 'PII', 'OTHER',
41
- ]);
42
-
43
- const BARE_TOKEN_RE = /^([A-Z][A-Z0-9]*)_\d+$/;
44
- const unwrap = (v) => String(v ?? '').trim().replace(/^\[{1,2}|\]{1,2}$/g, '');
45
-
46
- /**
47
- * Is this BARE string one of the redaction placeholders?
48
- *
49
- * Matched against the type vocabulary rather than the `[A-Z]+_\d+` shape, and everywhere —
50
- * inside brackets too. The shape alone eats real subjects: `[[Q3_2026]]` and `[[PHASE_2]]`
51
- * are links people genuinely write, and silently dropping them would trade one invisible bug
52
- * for another. A custom dictionary type is the accepted gap: it is user-chosen, so filing it
53
- * is a name the user picked, not a stranger's identity.
54
- *
55
- * Bracket-tolerant, because a wikilink parser has already stripped them by the time we ask,
56
- * and a model echoing a placeholder into JSON routinely mangles them.
57
- */
58
- export function isRedactionToken(value) {
59
- const m = BARE_TOKEN_RE.exec(unwrap(value));
60
- return !!m && REDACTION_TOKEN_TYPES.includes(m[1]);
61
- }
1
+ const e=Object.freeze(["PERSON","ORG","LOCATION","ADDRESS","EMAIL","PHONE","ID","SSN","IBAN","CREDITCARD","CARD","POST","FAC","GROUP","NRP","ENTITY","KEY","SECRET","TERM","PII","OTHER"]),T=/^([A-Z][A-Z0-9]*)_\d+$/,n=E=>String(E??"").trim().replace(/^\[{1,2}|\]{1,2}$/g,"");function t(E){const R=T.exec(n(E));return!!R&&e.includes(R[1])}export{e as REDACTION_TOKEN_TYPES,t as isRedactionToken};
package/ref.js CHANGED
@@ -1,55 +1 @@
1
- // References how the log addresses content WITHOUT copying it.
2
- //
3
- // An event records "note n_88, lines 10-40, hash H" and never the text. Content that
4
- // actually entered a model request is written once into a content-addressed blob store
5
- // keyed by hash, so the same excerpt across a hundred turns costs one copy.
6
- //
7
- // Replay resolves a Ref by hash: match => exact reconstruction; blob absent or
8
- // crypto-shredded => VERIFIED_BUT_UNAVAILABLE. It never silently substitutes today's
9
- // version of the note, because that would make replay quietly wrong instead of loudly
10
- // incomplete.
11
-
12
- // `memory` and `brief` joined when the derived layer landed: a brief's claim cites the
13
- // record it came from, and a claim the USER stated cites the memory they stated it in.
14
- // Additive — no existing ref changes meaning.
15
- export const REF_KINDS = Object.freeze(['note', 'meeting', 'chat', 'page', 'result', 'blob', 'memory', 'brief']);
16
-
17
- export const RESOLUTION = Object.freeze({
18
- EXACT: 'exact', // blob present, hash matches
19
- UNAVAILABLE: 'verified-but-unavailable', // we know what it was; we no longer hold it
20
- DRIFTED: 'drifted', // source still exists but its hash changed
21
- });
22
-
23
- /** Build a Ref. `hash` is the content hash AT CAPTURE TIME — that is the whole point. */
24
- export function makeRef({ kind, id, hash, range = null, stored = false }) {
25
- if (!REF_KINDS.includes(kind)) throw new TypeError(`ref: unknown kind ${kind}`);
26
- if (typeof id !== 'string' || !id) throw new TypeError('ref: id required');
27
- if (typeof hash !== 'string' || !hash) throw new TypeError('ref: hash required');
28
- const ref = { kind, id, hash };
29
- if (range) {
30
- if (!Number.isInteger(range.from) || !Number.isInteger(range.to) || range.to < range.from) {
31
- throw new TypeError('ref: range must be {from,to} integers with to >= from');
32
- }
33
- ref.range = { from: range.from, to: range.to };
34
- }
35
- if (stored) ref.stored = true;
36
- return Object.freeze(ref);
37
- }
38
-
39
- export function isRef(v) {
40
- return !!v && typeof v === 'object'
41
- && REF_KINDS.includes(v.kind)
42
- && typeof v.id === 'string' && v.id.length > 0
43
- && typeof v.hash === 'string' && v.hash.length > 0;
44
- }
45
-
46
- /**
47
- * Classify what a replay can say about a Ref, given what the blob store holds now.
48
- * `lookup(ref) -> { hash } | null`. Pure: the caller owns the store.
49
- */
50
- export function resolveRef(ref, lookup) {
51
- const got = lookup(ref);
52
- if (!got) return { resolution: RESOLUTION.UNAVAILABLE, ref };
53
- if (got.hash !== ref.hash) return { resolution: RESOLUTION.DRIFTED, ref, actualHash: got.hash };
54
- return { resolution: RESOLUTION.EXACT, ref, value: got.value };
55
- }
1
+ const f=Object.freeze(["note","meeting","chat","page","result","blob","memory","brief"]),i=Object.freeze({EXACT:"exact",UNAVAILABLE:"verified-but-unavailable",DRIFTED:"drifted"});function u({kind:e,id:o,hash:r,range:t=null,stored:s=!1}){if(!f.includes(e))throw new TypeError(`ref: unknown kind ${e}`);if(typeof o!="string"||!o)throw new TypeError("ref: id required");if(typeof r!="string"||!r)throw new TypeError("ref: hash required");const n={kind:e,id:o,hash:r};if(t){if(!Number.isInteger(t.from)||!Number.isInteger(t.to)||t.to<t.from)throw new TypeError("ref: range must be {from,to} integers with to >= from");n.range={from:t.from,to:t.to}}return s&&(n.stored=!0),Object.freeze(n)}function l(e){return!!e&&typeof e=="object"&&f.includes(e.kind)&&typeof e.id=="string"&&e.id.length>0&&typeof e.hash=="string"&&e.hash.length>0}function h(e,o){const r=o(e);return r?r.hash!==e.hash?{resolution:i.DRIFTED,ref:e,actualHash:r.hash}:{resolution:i.EXACT,ref:e,value:r.value}:{resolution:i.UNAVAILABLE,ref:e}}export{f as REF_KINDS,i as RESOLUTION,l as isRef,u as makeRef,h as resolveRef};