@chatpanel/events 0.102.0 → 0.104.0

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 (139) 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 +4 -5
  62. package/pdf-layout.js +7 -253
  63. package/policy.js +1 -0
  64. package/project-run.js +20 -444
  65. package/project.js +1 -170
  66. package/promotion.js +1 -171
  67. package/queue.js +1 -98
  68. package/reach.js +1 -23
  69. package/recipe-tool.js +2 -161
  70. package/recipe.js +1 -264
  71. package/record-list.js +2 -110
  72. package/recruit.js +3 -456
  73. package/redaction-tokens.js +1 -61
  74. package/ref.js +1 -55
  75. package/registry.js +1 -240
  76. package/route-graph.js +1 -115
  77. package/route-strategies.js +2 -232
  78. package/router.js +2 -880
  79. package/rrf.js +1 -89
  80. package/rules.js +1 -142
  81. package/schedule.js +1 -663
  82. package/scm-connection.js +1 -180
  83. package/scopes.js +1 -12
  84. package/scorecard.js +1 -335
  85. package/search-engines.js +1 -81
  86. package/skill-manifest.js +1 -330
  87. package/skill-scan.js +3 -209
  88. package/skill-sources.js +1 -162
  89. package/skill-vars.js +2 -252
  90. package/slash-commands.js +1 -158
  91. package/source-gate.js +1 -92
  92. package/sources-retrieval.js +5 -186
  93. package/sources.js +1 -273
  94. package/store.js +1 -171
  95. package/structured.js +4 -901
  96. package/subject-kinds.js +1 -5
  97. package/subject-name.js +1 -96
  98. package/sync-plan.js +1 -213
  99. package/synthesis.js +3 -123
  100. package/tags.js +1 -203
  101. package/team-board.js +6 -337
  102. package/team-cache.js +2 -63
  103. package/team-observe.js +1 -118
  104. package/team-org.js +1 -442
  105. package/team-plan.js +4 -131
  106. package/team-record.js +1 -225
  107. package/team-run.js +9 -652
  108. package/team-subtask.js +2 -191
  109. package/team-task.js +2 -124
  110. package/team-tool.js +2 -196
  111. package/team-trail.js +2 -73
  112. package/team-worklog.js +2 -149
  113. package/team.js +1 -302
  114. package/text-search.js +1 -155
  115. package/theme.js +17 -154
  116. package/titles.js +4 -285
  117. package/tool-discovery.js +1 -87
  118. package/tool-dispatch.js +5 -230
  119. package/tool-groups.js +1 -81
  120. package/tool-hints.js +10 -185
  121. package/tool-loop-guard.js +4 -182
  122. package/tool-need.js +2 -124
  123. package/tool-result.js +2 -438
  124. package/tool-round.js +0 -0
  125. package/tool-schema.js +1 -155
  126. package/tool-traits.js +1 -182
  127. package/toolset.js +2 -88
  128. package/trajectory.js +1 -530
  129. package/turn-loop.js +4 -440
  130. package/upcast.js +1 -37
  131. package/vault.js +2 -250
  132. package/view.js +1 -88
  133. package/voice-intents.js +2 -1488
  134. package/voice-speaker.js +1 -98
  135. package/weather-tool.js +1 -65
  136. package/weather.js +2 -211
  137. package/web-search-tool.js +14 -102
  138. package/web-search.js +1 -160
  139. package/widget.js +1 -153
package/invariants.js CHANGED
@@ -1,174 +1 @@
1
- // The six invariants the replay harness asserts.
2
- //
3
- // These are what make the claims CHECKABLE rather than aspirational. An unverified
4
- // invariant decays silently, so each of these becomes a CI assertion the way the
5
- // `sync:pii --check` drift guard and the CSP assertion in build-editor.mjs already are.
6
- //
7
- // I1 every model request is preceded by a context.assembled that reconstructs it
8
- // I2 every byte leaving the device has a privacy.egress (controlled:false for agents)
9
- // I3 every non-pure capability.invoked carries an idempotency key
10
- // I4 every capability.revoked names its activated in causes
11
- // I5 no durable event exists for a per-item ephemeral stream
12
- // I6 replay under the linearization rule is stable across runs and across hosts
13
- // I7 no assistant event carries content — only a Ref to it
14
- //
15
- // checkInvariants() returns violations rather than throwing, so a harness can report
16
- // every problem in one pass instead of one per run.
17
-
18
- import { linearize } from './order.js';
19
- import { isRef } from './ref.js';
20
-
21
- const v = (id, event, message) => ({ invariant: id, eventId: event ? event.id : null, message });
22
-
23
- /** I1 — a turn that ran must have assembled its context, and every resident Ref is hashed. */
24
- /**
25
- * I7 — an assistant event names its content, it never contains it.
26
- *
27
- * The temptation is obvious: inlining the text makes the trajectory view trivial. It also
28
- * makes the log a second copy of every conversation, so exporting the log exports the
29
- * user's data, and shredding a message leaves it readable in an append-only file forever.
30
- * Structural, not advisory: the checker fails on any payload field holding a long string,
31
- * because the failure it prevents is silent — everything keeps working, and privacy is
32
- * quietly gone.
33
- */
34
- function checkI7(events) {
35
- const out = [];
36
- const MAX = 200; // a model, a stop reason, a hash — never a message
37
- for (const e of events) {
38
- if (!String(e.type).startsWith('assistant.')) continue;
39
- if (!isRef(e.payload?.ref)) out.push(v('I7', e, 'assistant event has no content Ref'));
40
- for (const [k, val] of Object.entries(e.payload || {})) {
41
- if (k !== 'ref' && typeof val === 'string' && val.length > MAX) {
42
- out.push(v('I7', e, `payload.${k} carries ${val.length} chars of content — assistant events must reference, never contain`));
43
- }
44
- }
45
- }
46
- return out;
47
- }
48
-
49
- function checkI1(events) {
50
- const out = [];
51
- const assembledByTurn = new Set();
52
- for (const e of events) {
53
- if (e.type === 'context.assembled') {
54
- if (e.payload.turnId) assembledByTurn.add(e.payload.turnId);
55
- for (const r of e.payload.resident) {
56
- if (!isRef(r)) out.push(v('I1', e, 'resident entry is not a valid Ref'));
57
- }
58
- }
59
- }
60
- for (const e of events) {
61
- if (e.type === 'turn.ended' && e.payload.stepped !== false && !assembledByTurn.has(e.payload.turnId)) {
62
- out.push(v('I1', e, `turn ${e.payload.turnId} ended with no context.assembled — model-visible input is not reconstructable`));
63
- }
64
- }
65
- return out;
66
- }
67
-
68
- /** I2 — an invocation that egresses must produce a privacy.egress caused by it. */
69
- function checkI2(events) {
70
- const out = [];
71
- const egressCauses = new Set();
72
- for (const e of events) {
73
- if (e.type === 'privacy.egress') for (const c of e.causes) egressCauses.add(c);
74
- }
75
- for (const e of events) {
76
- if (e.type === 'capability.invoked' && e.payload.egress && e.payload.egress !== 'none') {
77
- if (!egressCauses.has(e.id)) {
78
- out.push(v('I2', e, `invocation declares egress '${e.payload.egress}' but no privacy.egress references it`));
79
- }
80
- }
81
- }
82
- return out;
83
- }
84
-
85
- /** I3 — non-pure invocations carry an idempotency key. (Also enforced by validateEvent.) */
86
- function checkI3(events) {
87
- return events
88
- .filter((e) => e.type === 'capability.invoked'
89
- && e.payload.effects !== 'pure'
90
- && !e.payload.idempotencyKey)
91
- .map((e) => v('I3', e, `'${e.payload.effects}' invocation without an idempotencyKey — a retry would repeat the side effect`));
92
- }
93
-
94
- /** I4 — a revoke names the activation it undoes. */
95
- function checkI4(events) {
96
- const activated = new Map();
97
- for (const e of events) if (e.type === 'capability.activated') activated.set(e.id, e);
98
- return events
99
- .filter((e) => e.type === 'capability.revoked' && !e.causes.some((c) => activated.has(c)))
100
- .map((e) => v('I4', e, 'revoked does not name its activated in causes — the effect has no recorded inverse'));
101
- }
102
-
103
- /** I5 — ephemeral stream items never become durable facts. */
104
- function checkI5(events, { ephemeralBudget = 200 } = {}) {
105
- const out = [];
106
- const perTypePerHost = new Map();
107
- for (const e of events) {
108
- const k = `${e.host}:${e.type}:${e.payload.streamId || ''}`;
109
- perTypePerHost.set(k, (perTypePerHost.get(k) || 0) + 1);
110
- }
111
- for (const [k, n] of perTypePerHost) {
112
- if (k.split(':')[2] && n > ephemeralBudget) {
113
- out.push(v('I5', null, `${n} durable events for stream ${k} — per-item stream data must be windowed, not logged`));
114
- }
115
- }
116
- return out;
117
- }
118
-
119
- /** I6 — linearization is a function of the event SET, not of the input array's order. */
120
- function checkI6(events, { shuffles = 8, rng = mulberry32(0x5EED) } = {}) {
121
- if (events.length < 2) return [];
122
- const base = linearize(events).map((e) => e.id).join(',');
123
- for (let i = 0; i < shuffles; i++) {
124
- const shuffled = shuffle(events, rng);
125
- if (linearize(shuffled).map((e) => e.id).join(',') !== base) {
126
- return [v('I6', null, 'linearize() is order-dependent — replay is not deterministic')];
127
- }
128
- }
129
- return [];
130
- }
131
-
132
- /** Run every invariant over an event set. Returns [] when the log is sound. */
133
- export function checkInvariants(events, opts = {}) {
134
- return [
135
- ...checkI7(events),
136
- ...checkI1(events),
137
- ...checkI2(events),
138
- ...checkI3(events),
139
- ...checkI4(events),
140
- ...checkI5(events, opts),
141
- ...checkI6(events, opts),
142
- ];
143
- }
144
-
145
- export const INVARIANTS = Object.freeze({
146
- I1: 'model-visible input is reconstructable from the log',
147
- I2: 'every egress is recorded',
148
- I3: 'non-pure invocations are idempotent',
149
- I4: 'every activation has a recorded inverse',
150
- I5: 'ephemeral streams never become durable facts',
151
- I6: 'replay is deterministic',
152
- I7: 'assistant events reference content, never contain it',
153
- });
154
-
155
- // Seeded PRNG so a failing I6 reproduces exactly — the package holds no ambient
156
- // dependency on Math.random.
157
- function mulberry32(seed) {
158
- let a = seed >>> 0;
159
- return () => {
160
- a = (a + 0x6D2B79F5) >>> 0;
161
- let t = Math.imul(a ^ (a >>> 15), 1 | a);
162
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
163
- return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
164
- };
165
- }
166
-
167
- function shuffle(list, rng) {
168
- const a = [...list];
169
- for (let i = a.length - 1; i > 0; i--) {
170
- const j = Math.floor(rng() * (i + 1));
171
- [a[i], a[j]] = [a[j], a[i]];
172
- }
173
- return a;
174
- }
1
+ import{linearize as i}from"./order.js";import{isRef as c}from"./ref.js";const s=(o,t,n)=>({invariant:o,eventId:t?t.id:null,message:n});function d(o){const t=[];for(const e of o)if(String(e.type).startsWith("assistant.")){c(e.payload?.ref)||t.push(s("I7",e,"assistant event has no content Ref"));for(const[r,a]of Object.entries(e.payload||{}))r!=="ref"&&typeof a=="string"&&a.length>200&&t.push(s("I7",e,`payload.${r} carries ${a.length} chars of content — assistant events must reference, never contain`))}return t}function l(o){const t=[],n=new Set;for(const e of o)if(e.type==="context.assembled"){e.payload.turnId&&n.add(e.payload.turnId);for(const r of e.payload.resident)c(r)||t.push(s("I1",e,"resident entry is not a valid Ref"))}for(const e of o)e.type==="turn.ended"&&e.payload.stepped!==!1&&!n.has(e.payload.turnId)&&t.push(s("I1",e,`turn ${e.payload.turnId} ended with no context.assembled — model-visible input is not reconstructable`));return t}function u(o){const t=[],n=new Set;for(const e of o)if(e.type==="privacy.egress")for(const r of e.causes)n.add(r);for(const e of o)e.type==="capability.invoked"&&e.payload.egress&&e.payload.egress!=="none"&&(n.has(e.id)||t.push(s("I2",e,`invocation declares egress '${e.payload.egress}' but no privacy.egress references it`)));return t}function p(o){return o.filter(t=>t.type==="capability.invoked"&&t.payload.effects!=="pure"&&!t.payload.idempotencyKey).map(t=>s("I3",t,`'${t.payload.effects}' invocation without an idempotencyKey — a retry would repeat the side effect`))}function y(o){const t=new Map;for(const n of o)n.type==="capability.activated"&&t.set(n.id,n);return o.filter(n=>n.type==="capability.revoked"&&!n.causes.some(e=>t.has(e))).map(n=>s("I4",n,"revoked does not name its activated in causes — the effect has no recorded inverse"))}function h(o,{ephemeralBudget:t=200}={}){const n=[],e=new Map;for(const r of o){const a=`${r.host}:${r.type}:${r.payload.streamId||""}`;e.set(a,(e.get(a)||0)+1)}for(const[r,a]of e)r.split(":")[2]&&a>t&&n.push(s("I5",null,`${a} durable events for stream ${r} — per-item stream data must be windowed, not logged`));return n}function m(o,{shuffles:t=8,rng:n=I(24301)}={}){if(o.length<2)return[];const e=i(o).map(r=>r.id).join(",");for(let r=0;r<t;r++){const a=b(o,n);if(i(a).map(f=>f.id).join(",")!==e)return[s("I6",null,"linearize() is order-dependent — replay is not deterministic")]}return[]}function k(o,t={}){return[...d(o),...l(o),...u(o),...p(o),...y(o),...h(o,t),...m(o,t)]}const $=Object.freeze({I1:"model-visible input is reconstructable from the log",I2:"every egress is recorded",I3:"non-pure invocations are idempotent",I4:"every activation has a recorded inverse",I5:"ephemeral streams never become durable facts",I6:"replay is deterministic",I7:"assistant events reference content, never contain it"});function I(o){let t=o>>>0;return()=>{t=t+1831565813>>>0;let n=Math.imul(t^t>>>15,1|t);return n=n+Math.imul(n^n>>>7,61|n)^n,((n^n>>>14)>>>0)/4294967296}}function b(o,t){const n=[...o];for(let e=n.length-1;e>0;e--){const r=Math.floor(t()*(e+1));[n[e],n[r]]=[n[r],n[e]]}return n}export{$ as INVARIANTS,k as checkInvariants};
package/job.js CHANGED
@@ -1,149 +1,5 @@
1
- // A job a posting on a project's board that the pool applies to.
2
- //
3
- // The executive posts the first jobs; a recruited agent posts more when the work needs more
4
- // hands, a skill or a tool it does not have; a person posts one by hand. A job says what it
5
- // needs (skills, tools, grants), what it may cost (carved from the project's budget), and
6
- // where the work happens (a repo and a base branch — the bridge gives it a worktree). Agents
7
- // in the pool APPLY by construction (recruit.js scores every type at once); an evaluator
8
- // picks; the pick is recruited with a budget and the job becomes a role on a run.
9
- //
10
- // A job's status is a machine: open → evaluating → recruited → in-progress → done | failed,
11
- // with withdrawn from any of the first three. Every move is an event on the project's
12
- // record (project.js foldProject: `job.posted`, `job.updated`).
1
+ import{validateBudget as k,normalizeBudget as c}from"./budget.js";import{GRANT_RE as g}from"./team.js";const b=/^[a-z][a-z0-9_-]{0,63}$/i,l=Object.freeze(["open","evaluating","recruited","in-progress","done","failed","withdrawn"]),y=Object.freeze({open:["evaluating","recruited","withdrawn"],evaluating:["recruited","open","withdrawn"],recruited:["in-progress","open","withdrawn"],"in-progress":["done","failed","open"],done:[],failed:["open"],withdrawn:["open"]}),u=24,f=64;class m extends Error{constructor(t,n){super(n),this.name="JobError",this.code=t}}const a=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),r=(e,t)=>String(e||"").trim().slice(0,t),p=(e,t,n)=>[...new Set((Array.isArray(e)?e:typeof e=="string"?e.split(/[,\s]+/):[]).map(i=>r(i,t)).filter(Boolean))].slice(0,n);function h(e){const t=[];if(!a(e))return{ok:!1,errors:["job must be an object"]};if(b.test(String(e.id||""))||t.push("id: a short identifier (letters, digits, _ -)"),b.test(String(e.projectId||""))||t.push("projectId: the project this job belongs to"),r(e.title,200)||t.push("title: what the job is"),r(e.brief,8e3)||t.push("brief: what to do, what done looks like"),e.needs!==void 0)if(!a(e.needs))t.push("needs: { skills[], tools[], grants[] }");else{const n=(Array.isArray(e.needs.grants)?e.needs.grants:[]).filter(i=>!g.test(String(i)));n.length&&t.push(`needs.grants: not grantable: ${n.join(", ")}`)}if(e.budget!==void 0){const n=k(e.budget);n.ok||t.push(...n.errors.map(i=>`budget: ${i}`))}return e.status!==void 0&&!l.includes(e.status)&&t.push(`status: one of ${l.join(", ")}`),e.dependsOn!==void 0&&!Array.isArray(e.dependsOn)&&t.push("dependsOn: a list of job ids"),e.workspace!==void 0&&e.workspace!==null&&!a(e.workspace)&&t.push("workspace: { repoId, base, branch? }"),e.deadline!==void 0&&e.deadline!==null&&!Number.isFinite(Number(e.deadline))&&t.push("deadline: a time"),{ok:t.length===0,errors:t}}function w(e){const t=h(e);if(!t.ok)throw new m("INVALID",t.errors.join("; "));return{id:String(e.id),projectId:String(e.projectId),title:r(e.title,200),brief:r(e.brief,8e3),needs:{skills:p(e.needs?.skills,80,u),tools:p(e.needs?.tools,120,u),grants:p(e.needs?.grants,64,u).filter(n=>g.test(n))},...e.budget?{budget:c(e.budget)}:{},size:{steps:Math.max(0,Math.round(Number(e.size?.steps)||0))},status:l.includes(e.status)?e.status:"open",postedBy:r(e.postedBy,80)||"person",postedAt:Number(e.postedAt)||Date.now(),...e.deadline?{deadline:Number(e.deadline)}:{},dependsOn:p(e.dependsOn,64,32).filter(n=>n!==e.id),...e.workspace?{workspace:{repoId:r(e.workspace.repoId,120),base:r(e.workspace.base,120)||"main",...e.workspace.branch?{branch:r(e.workspace.branch,200)}:{},...e.workspace.worktreePath?{worktreePath:r(e.workspace.worktreePath,400)}:{}}}:{},applications:Array.isArray(e.applications)?e.applications.slice(0,f).map(A).filter(Boolean):[],...e.recruited?{recruited:I(e.recruited)}:{},...e.runId?{runId:String(e.runId)}:{},...e.result?{result:{text:r(e.result.text,8e3),by:r(e.result.by,80),at:Number(e.result.at)||Date.now(),...Array.isArray(e.result.refs)?{refs:e.result.refs.slice(0,12)}:{}}}:{},...e.origin&&a(e.origin)?{origin:{...e.origin}}:{}}}function A(e){return!a(e)||!e.agentId?null:{agentId:String(e.agentId),...e.engine?{engine:e.engine}:{},fit:Math.max(0,Math.min(1,Number(e.fit)||0)),reasons:Array.isArray(e.reasons)?e.reasons.map(t=>r(t,200)).slice(0,8):[],pitch:r(e.pitch,600),at:Number(e.at)||Date.now()}}function I(e){return{agentId:String(e.agentId),...e.engine?{engine:e.engine}:{},...e.budget?{budget:c(e.budget)}:{},by:r(e.by,80)||"evaluator",at:Number(e.at)||Date.now(),...e.why?{why:r(e.why,600)}:{}}}function N(e){return Object.freeze(w(e))}function S(e,t){return(y[e]||[]).includes(t)}function x(e,t,n,{cards:i={},now:d=Date.now()}={}){return(t||[]).filter(s=>s&&s.enabled!==!1&&(s.appliesTo||["jobs"]).includes("jobs")).map(s=>{const o=n(e,s,i[s.id]||null);return{agentId:s.id,...o.engine?{engine:o.engine}:{},fit:o.score,reasons:o.reasons,pitch:"",at:d}}).sort((s,o)=>!!o.engine-!!s.engine||o.fit-s.fit).slice(0,f)}function B(e,t){return{id:t?.id||e.id,agent:t?.id,name:t?.name||e.title,prompt:[t?.prompt,`Job: ${e.title}
13
2
 
14
- import { validateBudget, normalizeBudget } from './budget.js';
15
- import { GRANT_RE } from './team.js';
3
+ ${e.brief}`].filter(Boolean).join(`
16
4
 
17
- export const JOB_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/i;
18
- export const JOB_STATUSES = Object.freeze(['open', 'evaluating', 'recruited', 'in-progress', 'done', 'failed', 'withdrawn']);
19
- const NEXT = Object.freeze({
20
- open: ['evaluating', 'recruited', 'withdrawn'], evaluating: ['recruited', 'open', 'withdrawn'], recruited: ['in-progress', 'open', 'withdrawn'],
21
- 'in-progress': ['done', 'failed', 'open'], done: [], failed: ['open'], withdrawn: ['open'],
22
- });
23
- export const MAX_NEEDS = 24;
24
- export const MAX_APPLICATIONS = 64;
25
-
26
- export class JobError extends Error {
27
- constructor(code, message) { super(message); this.name = 'JobError'; this.code = code; }
28
- }
29
-
30
- const isRecord = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
31
- const clip = (s, n) => String(s || '').trim().slice(0, n);
32
- const list = (xs, n, max) => [...new Set((Array.isArray(xs) ? xs : typeof xs === 'string' ? xs.split(/[,\s]+/) : []).map((x) => clip(x, n)).filter(Boolean))].slice(0, max);
33
-
34
- export function validateJob(j) {
35
- const errors = [];
36
- if (!isRecord(j)) return { ok: false, errors: ['job must be an object'] };
37
- if (!JOB_ID_RE.test(String(j.id || ''))) errors.push('id: a short identifier (letters, digits, _ -)');
38
- if (!JOB_ID_RE.test(String(j.projectId || ''))) errors.push('projectId: the project this job belongs to');
39
- if (!clip(j.title, 200)) errors.push('title: what the job is');
40
- if (!clip(j.brief, 8000)) errors.push('brief: what to do, what done looks like');
41
- if (j.needs !== undefined) {
42
- if (!isRecord(j.needs)) errors.push('needs: { skills[], tools[], grants[] }');
43
- else {
44
- const badGrants = (Array.isArray(j.needs.grants) ? j.needs.grants : []).filter((g) => !GRANT_RE.test(String(g)));
45
- if (badGrants.length) errors.push(`needs.grants: not grantable: ${badGrants.join(', ')}`);
46
- }
47
- }
48
- if (j.budget !== undefined) { const b = validateBudget(j.budget); if (!b.ok) errors.push(...b.errors.map((e) => `budget: ${e}`)); }
49
- if (j.status !== undefined && !JOB_STATUSES.includes(j.status)) errors.push(`status: one of ${JOB_STATUSES.join(', ')}`);
50
- if (j.dependsOn !== undefined && !Array.isArray(j.dependsOn)) errors.push('dependsOn: a list of job ids');
51
- if (j.workspace !== undefined && j.workspace !== null && !isRecord(j.workspace)) errors.push('workspace: { repoId, base, branch? }');
52
- if (j.deadline !== undefined && j.deadline !== null && !Number.isFinite(Number(j.deadline))) errors.push('deadline: a time');
53
- return { ok: errors.length === 0, errors };
54
- }
55
-
56
- export function normalizeJob(j) {
57
- const v = validateJob(j);
58
- if (!v.ok) throw new JobError('INVALID', v.errors.join('; '));
59
- return {
60
- id: String(j.id),
61
- projectId: String(j.projectId),
62
- title: clip(j.title, 200),
63
- brief: clip(j.brief, 8000),
64
- needs: {
65
- skills: list(j.needs?.skills, 80, MAX_NEEDS),
66
- tools: list(j.needs?.tools, 120, MAX_NEEDS),
67
- grants: list(j.needs?.grants, 64, MAX_NEEDS).filter((g) => GRANT_RE.test(g)),
68
- },
69
- ...(j.budget ? { budget: normalizeBudget(j.budget) } : {}),
70
- size: { steps: Math.max(0, Math.round(Number(j.size?.steps) || 0)) },
71
- status: JOB_STATUSES.includes(j.status) ? j.status : 'open',
72
- postedBy: clip(j.postedBy, 80) || 'person',
73
- postedAt: Number(j.postedAt) || Date.now(),
74
- ...(j.deadline ? { deadline: Number(j.deadline) } : {}),
75
- dependsOn: list(j.dependsOn, 64, 32).filter((d) => d !== j.id),
76
- ...(j.workspace ? { workspace: { repoId: clip(j.workspace.repoId, 120), base: clip(j.workspace.base, 120) || 'main', ...(j.workspace.branch ? { branch: clip(j.workspace.branch, 200) } : {}), ...(j.workspace.worktreePath ? { worktreePath: clip(j.workspace.worktreePath, 400) } : {}) } } : {}),
77
- applications: Array.isArray(j.applications) ? j.applications.slice(0, MAX_APPLICATIONS).map(normalizeApplication).filter(Boolean) : [],
78
- ...(j.recruited ? { recruited: normalizeRecruit(j.recruited) } : {}),
79
- ...(j.runId ? { runId: String(j.runId) } : {}),
80
- ...(j.result ? { result: { text: clip(j.result.text, 8000), by: clip(j.result.by, 80), at: Number(j.result.at) || Date.now(), ...(Array.isArray(j.result.refs) ? { refs: j.result.refs.slice(0, 12) } : {}) } } : {}),
81
- ...(j.origin && isRecord(j.origin) ? { origin: { ...j.origin } } : {}),
82
- };
83
- }
84
-
85
- function normalizeApplication(a) {
86
- if (!isRecord(a) || !a.agentId) return null;
87
- return { agentId: String(a.agentId), ...(a.engine ? { engine: a.engine } : {}), fit: Math.max(0, Math.min(1, Number(a.fit) || 0)), reasons: Array.isArray(a.reasons) ? a.reasons.map((r) => clip(r, 200)).slice(0, 8) : [], pitch: clip(a.pitch, 600), at: Number(a.at) || Date.now() };
88
- }
89
- function normalizeRecruit(r) {
90
- return { agentId: String(r.agentId), ...(r.engine ? { engine: r.engine } : {}), ...(r.budget ? { budget: normalizeBudget(r.budget) } : {}), by: clip(r.by, 80) || 'evaluator', at: Number(r.at) || Date.now(), ...(r.why ? { why: clip(r.why, 600) } : {}) };
91
- }
92
-
93
- export function defineJob(j) { return Object.freeze(normalizeJob(j)); }
94
-
95
- /** May the job move from `from` to `to`? */
96
- export function canTransition(from, to) { return (NEXT[from] || []).includes(to); }
97
-
98
- /** Applications are computed, not asked for: every eligible type in the pool applies at once. */
99
- export function applyAll(job, pool, fitFn, { cards = {}, now = Date.now() } = {}) {
100
- // A fit function may also say which ENGINE the agent would run on (recruit.js does); an
101
- // application without one is not recruitable right now and sorts after those that are.
102
- return (pool || [])
103
- .filter((a) => a && a.enabled !== false && (a.appliesTo || ['jobs']).includes('jobs'))
104
- .map((a) => { const f = fitFn(job, a, cards[a.id] || null); return { agentId: a.id, ...(f.engine ? { engine: f.engine } : {}), fit: f.score, reasons: f.reasons, pitch: '', at: now }; })
105
- .sort((x, y) => (!!y.engine - !!x.engine) || (y.fit - x.fit))
106
- .slice(0, MAX_APPLICATIONS);
107
- }
108
-
109
- /** A recruited job as the role a run gives the agent: the brief is the prompt, the needs are the grants. */
110
- export function jobToRole(job, agent) {
111
- return {
112
- id: agent?.id || job.id,
113
- agent: agent?.id,
114
- name: agent?.name || job.title,
115
- prompt: [agent?.prompt, `Job: ${job.title}\n\n${job.brief}`].filter(Boolean).join('\n\n'),
116
- grants: (job.needs?.grants?.length ? job.needs.grants : agent?.grants) || ['none'],
117
- ...(job.dependsOn?.length ? { dependsOn: job.dependsOn } : {}),
118
- ...(job.workspace ? { workspace: job.workspace } : {}),
119
- };
120
- }
121
-
122
- /** The jobs that may run now: every dependency done, and not already taken. */
123
- export function readyJobs(jobs) {
124
- const byId = new Map((jobs || []).map((j) => [j.id, j]));
125
- return (jobs || []).filter((j) => j.status === 'open' && (j.dependsOn || []).every((d) => byId.get(d)?.status === 'done'));
126
- }
127
-
128
- /** A blank posting for the form. */
129
- export function blankJob(projectId = '') {
130
- return { id: '', projectId, title: '', brief: '', needs: { skills: [], tools: [], grants: [] }, budget: { tokens: 40000, ms: 900000 }, status: 'open', dependsOn: [] };
131
- }
132
-
133
- export function jobFromForm(form) {
134
- const budget = {};
135
- for (const k of ['tokens', 'calls', 'ms', 'usd']) {
136
- const v = Number(form?.budget?.[k]);
137
- if (form?.budget?.[k] !== '' && form?.budget?.[k] != null && Number.isFinite(v) && v > 0) budget[k] = v;
138
- }
139
- const j = {
140
- id: String(form?.id || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^[^a-z]+/, '').replace(/-+$/, '').slice(0, 64),
141
- projectId: form?.projectId, title: form?.title, brief: form?.brief,
142
- needs: { skills: form?.needs?.skills, tools: form?.needs?.tools, grants: form?.needs?.grants },
143
- ...(Object.keys(budget).length ? { budget } : {}),
144
- status: form?.status || 'open', postedBy: form?.postedBy || 'person', dependsOn: form?.dependsOn,
145
- ...(form?.workspace?.repoId ? { workspace: form.workspace } : {}), ...(form?.size ? { size: form.size } : {}),
146
- };
147
- const v = validateJob(j);
148
- return v.ok ? { ok: true, job: normalizeJob(j) } : { ok: false, errors: v.errors };
149
- }
5
+ `),grants:(e.needs?.grants?.length?e.needs.grants:t?.grants)||["none"],...e.dependsOn?.length?{dependsOn:e.dependsOn}:{},...e.workspace?{workspace:e.workspace}:{}}}function J(e){const t=new Map((e||[]).map(n=>[n.id,n]));return(e||[]).filter(n=>n.status==="open"&&(n.dependsOn||[]).every(i=>t.get(i)?.status==="done"))}function E(e=""){return{id:"",projectId:e,title:"",brief:"",needs:{skills:[],tools:[],grants:[]},budget:{tokens:4e4,ms:9e5},status:"open",dependsOn:[]}}function _(e){const t={};for(const d of["tokens","calls","ms","usd"]){const s=Number(e?.budget?.[d]);e?.budget?.[d]!==""&&e?.budget?.[d]!=null&&Number.isFinite(s)&&s>0&&(t[d]=s)}const n={id:String(e?.id||"").trim().toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^[^a-z]+/,"").replace(/-+$/,"").slice(0,64),projectId:e?.projectId,title:e?.title,brief:e?.brief,needs:{skills:e?.needs?.skills,tools:e?.needs?.tools,grants:e?.needs?.grants},...Object.keys(t).length?{budget:t}:{},status:e?.status||"open",postedBy:e?.postedBy||"person",dependsOn:e?.dependsOn,...e?.workspace?.repoId?{workspace:e.workspace}:{},...e?.size?{size:e.size}:{}},i=h(n);return i.ok?{ok:!0,job:w(n)}:{ok:!1,errors:i.errors}}export{b as JOB_ID_RE,l as JOB_STATUSES,m as JobError,f as MAX_APPLICATIONS,u as MAX_NEEDS,x as applyAll,E as blankJob,S as canTransition,N as defineJob,_ as jobFromForm,B as jobToRole,w as normalizeJob,J as readyJobs,h as validateJob};
package/kernel.js CHANGED
@@ -1,255 +1 @@
1
- // The plugin kernel the part that cannot itself be a plugin.
2
- //
3
- // Everything in ChatPanel is a plugin: the loop, tools, skills, storage, renderers, UI
4
- // slots, sources, model providers, presets, redaction POLICY. This file is what makes
5
- // that safe to say.
6
- //
7
- // "Security is a mandatory plugin" is only meaningful if something that is NOT a plugin
8
- // enforces the mandate — otherwise mandatory is a config value, and a config value is
9
- // disableable. So the kernel SHRINKS rather than grows. It does three things, none of
10
- // them extensible, and deliberately nothing else:
11
- //
12
- // 1. load plugins (delegated to the registry — revertible effects, reactive availability);
13
- // 2. refuse to run when a required plugin is absent or failed;
14
- // 3. enforce guard monotonicity — a guard may reduce permission, never widen it.
15
- //
16
- // POLICY VS MECHANISM. Redaction policy (what counts as sensitive, which dial the user
17
- // picked, whether an org gets visibility) is a plugin. The redaction mechanism and the
18
- // egress guard are not. That is what lets privacy be a real on/off dial without ever
19
- // creating a path where bytes leave unguarded: security is not a dial, privacy is.
20
- //
21
- // NO DEPENDENCY ON THE EVENT LOG, matching registry.js — `onEvent` is a plain hook the
22
- // host maps to schema events if it wants them. That keeps the kernel runnable in the
23
- // extension, the gateway and the bridge unchanged.
24
-
25
- import { createRegistry } from './registry.js';
26
-
27
- export class KernelError extends Error {
28
- constructor(code, message, detail = null) {
29
- super(message);
30
- this.name = 'KernelError';
31
- this.code = code;
32
- this.detail = detail;
33
- }
34
- }
35
-
36
- /** The default non-negotiables. A host may require more; it may not require fewer. */
37
- export const REQUIRED_PLUGINS = Object.freeze(['security']);
38
-
39
- /**
40
- * A decision is a permission, not a boolean: `allow` plus the scopes it covers.
41
- * Kept deliberately small — anything richer invites a lattice we cannot check.
42
- */
43
- export const ALLOW_ALL = Object.freeze({ allow: true, scopes: null, reasons: [] });
44
-
45
- const asScopeSet = (s) => (s == null ? null : new Set(s));
46
-
47
- /**
48
- * The MEET of two decisions — the heart of the monotonicity guarantee.
49
- *
50
- * A guard cannot widen because widening is never constructed, not because we inspect the
51
- * result and complain. Detect-and-reject would leave a window in which the widened value
52
- * existed and could be read by whatever ran next; a meet has no such window.
53
- *
54
- * `scopes: null` means "unscoped / all", so it is the top of the lattice and intersecting
55
- * with it is identity — that keeps a guard that does not care about scopes from
56
- * accidentally narrowing to nothing.
57
- */
58
- export function meetDecisions(a, b) {
59
- const as = asScopeSet(a.scopes);
60
- const bs = asScopeSet(b.scopes);
61
- let scopes = null;
62
- if (as && bs) scopes = [...as].filter((s) => bs.has(s));
63
- else if (as) scopes = [...as];
64
- else if (bs) scopes = [...bs];
65
- return {
66
- allow: !!a.allow && !!b.allow,
67
- scopes,
68
- reasons: [...(a.reasons || []), ...(b.reasons || [])],
69
- };
70
- }
71
-
72
- /** True when `out` asked for more than `input` allowed — a misbehaving or hostile plugin. */
73
- function widened(input, out) {
74
- if (!input.allow && out.allow) return true;
75
- const is = asScopeSet(input.scopes);
76
- const os = asScopeSet(out.scopes);
77
- if (!is) return false; // input was unscoped: nothing to widen past
78
- if (!os) return true; // guard tried to drop scoping entirely
79
- return [...os].some((s) => !is.has(s));
80
- }
81
-
82
- /**
83
- * @param required plugin ids that must be active before `start()` resolves. Callers may
84
- * add to REQUIRED_PLUGINS; they cannot remove from it.
85
- * @param onEvent `{ event, ...detail }` — 'plugin:activated' | 'plugin:failed' |
86
- * 'guard:widened' | 'started' | 'stopped'.
87
- */
88
- export function createKernel({ required = [], onEvent = null } = {}) {
89
- const requiredIds = [...new Set([...REQUIRED_PLUGINS, ...required])];
90
- const registry = createRegistry({
91
- onEvent: (d) => emit(`registry:${d.event}`, d),
92
- });
93
-
94
- const declared = new Map(); // id -> declaration
95
- const handles = new Map(); // id -> registry handle
96
- const guards = new Map(); // name -> [{ pluginId, fn }]
97
- let started = false;
98
-
99
- const emit = (event, detail = {}) => { if (onEvent) onEvent({ event, ...detail }); };
100
-
101
- /**
102
- * Declare a plugin. Declaration is NOT activation: manifests are static so the kernel
103
- * can answer "what is installed" without executing anything, and `activate` is where a
104
- * host does its `await import()`. A kernel that had to load every plugin to know what
105
- * it had would put the whole graph on first paint, and first paint is a release gate.
106
- */
107
- function define(plugin) {
108
- const { id, requires = [], activate, load } = plugin || {};
109
- if (!id || typeof id !== 'string') throw new KernelError('BAD_PLUGIN', 'plugin.id required');
110
- if (typeof activate !== 'function' && typeof load !== 'function') {
111
- throw new KernelError('BAD_PLUGIN', `plugin '${id}': activate or load required`);
112
- }
113
- if (declared.has(id)) throw new KernelError('DUPLICATE', `plugin '${id}' is already defined`);
114
- declared.set(id, { ...plugin, requires: [...requires] });
115
- return () => remove(id);
116
- }
117
-
118
- function remove(id) {
119
- if (requiredIds.includes(id)) {
120
- // Not a permission check that could be satisfied — there is no argument that makes
121
- // removing the security plugin acceptable, so it is not expressible.
122
- throw new KernelError('REQUIRED', `plugin '${id}' is required and cannot be removed`);
123
- }
124
- declared.delete(id);
125
- const h = handles.get(id);
126
- handles.delete(id);
127
- for (const [name, list] of guards) {
128
- const kept = list.filter((g) => g.pluginId !== id);
129
- if (kept.length) guards.set(name, kept); else guards.delete(name);
130
- }
131
- return h ? h.dispose() : Promise.resolve();
132
- }
133
-
134
- /** The scope a plugin's `activate` receives — the registry's, plus guard registration. */
135
- function scopeFor(id, inner) {
136
- return {
137
- ...inner,
138
- /**
139
- * Register a guard. It receives `(request, decision)` and returns a decision; the
140
- * kernel meets its answer with what came in, so it can only ever narrow.
141
- */
142
- guard(name, fn) {
143
- if (typeof fn !== 'function') throw new KernelError('BAD_GUARD', `guard '${name}' must be a function`);
144
- const list = guards.get(name) || [];
145
- list.push({ pluginId: id, fn });
146
- guards.set(name, list);
147
- return () => {
148
- const cur = (guards.get(name) || []).filter((g) => g.fn !== fn);
149
- if (cur.length) guards.set(name, cur); else guards.delete(name);
150
- };
151
- },
152
- };
153
- }
154
-
155
- /**
156
- * Run every guard for `name` and return the narrowest decision any of them permits.
157
- * Order-independent by construction: meet is associative and commutative, so guards
158
- * cannot race each other into a different answer — which is what lets them be plugins.
159
- */
160
- function decide(name, request, initial = ALLOW_ALL) {
161
- let decision = { allow: !!initial.allow, scopes: initial.scopes ?? null, reasons: [...(initial.reasons || [])] };
162
- for (const { pluginId, fn } of guards.get(name) || []) {
163
- let out;
164
- try {
165
- out = fn(request, decision);
166
- } catch (err) {
167
- // A guard that throws is a guard that did not permit. Fail closed: the
168
- // alternative is that crashing a guard becomes a way to bypass it.
169
- decision = meetDecisions(decision, { allow: false, scopes: null, reasons: [`${pluginId}: guard threw (${err.message})`] });
170
- continue;
171
- }
172
- if (out == null) continue; // abstained
173
- if (out === true) continue; // permitted, unchanged
174
- if (out === false) out = { allow: false, scopes: null, reasons: [`${pluginId}: denied`] };
175
- const norm = { allow: out.allow !== false, scopes: out.scopes ?? null, reasons: out.reasons || [] };
176
- if (widened(decision, norm)) emit('guard:widened', { guard: name, pluginId });
177
- decision = meetDecisions(decision, norm);
178
- }
179
- return decision;
180
- }
181
-
182
- /**
183
- * Activate everything declared, then verify the required set is genuinely ACTIVE —
184
- * not merely declared. A required plugin that failed to activate is the same problem
185
- * as one that was never installed, and the kernel must not be the component that
186
- * papers over the difference.
187
- */
188
- async function start() {
189
- if (started) return kernel;
190
- const missing = requiredIds.filter((id) => !declared.has(id));
191
- if (missing.length) {
192
- throw new KernelError('MISSING_REQUIRED', `kernel will not start: required plugin(s) not defined: ${missing.join(', ')}`, missing);
193
- }
194
- // Required plugins first, so a dependent that reaches for security during its own
195
- // activation finds it — the same ordering rule the registry applies to teardown.
196
- const ordered = [...declared.values()].sort(
197
- (a, b) => (requiredIds.includes(b.id) ? 1 : 0) - (requiredIds.includes(a.id) ? 1 : 0),
198
- );
199
- // Resolve `load` BEFORE registering. Registry activation is synchronous — an effect
200
- // registered after activation returned would not be in the disposer stack, so the
201
- // inverse would not run. Awaiting the import here is what keeps a manifest static
202
- // (the kernel knows what is installed without executing it) while the module itself
203
- // still only costs anything when it activates.
204
- for (const p of ordered) {
205
- if (handles.has(p.id)) continue;
206
- let activate = p.activate;
207
- if (!activate) {
208
- try {
209
- const mod = await p.load();
210
- activate = mod?.activate || mod?.default;
211
- } catch (err) {
212
- throw new KernelError('LOAD_FAILED', `plugin '${p.id}' failed to load: ${err.message}`, p.id);
213
- }
214
- if (typeof activate !== 'function') {
215
- throw new KernelError('BAD_PLUGIN', `plugin '${p.id}': loaded module exports no activate`, p.id);
216
- }
217
- }
218
- handles.set(p.id, registry.register({
219
- name: p.id,
220
- requires: p.requires,
221
- apply: (inner) => activate(scopeFor(p.id, inner)),
222
- }));
223
- }
224
- const notActive = requiredIds.filter((id) => handles.get(id)?.state !== 'active');
225
- if (notActive.length) {
226
- const detail = notActive.map((id) => ({ id, error: handles.get(id)?.error?.message || 'inactive', waitingFor: registry.pending().find((p) => p.name === id)?.waitingFor || [] }));
227
- await registry.dispose();
228
- throw new KernelError('REQUIRED_INACTIVE', `kernel will not start: required plugin(s) not active: ${notActive.join(', ')}`, detail);
229
- }
230
- started = true;
231
- emit('started', { plugins: registry.active() });
232
- return kernel;
233
- }
234
-
235
- const kernel = {
236
- define,
237
- remove,
238
- decide,
239
- start,
240
- get started() { return started; },
241
- /** What is installed, whether or not it activated — a manifest question. */
242
- list: () => [...declared.keys()].sort(),
243
- active: () => registry.active(),
244
- pending: () => registry.pending(),
245
- required: () => [...requiredIds],
246
- guards: () => [...guards.keys()].sort(),
247
- registry,
248
- async stop() {
249
- started = false;
250
- await registry.dispose();
251
- emit('stopped', {});
252
- },
253
- };
254
- return kernel;
255
- }
1
+ import{createRegistry as R}from"./registry.js";class p extends Error{constructor(u,i,r=null){super(i),this.name="KernelError",this.code=u,this.detail=r}}const U=Object.freeze(["security"]),_=Object.freeze({allow:!0,scopes:null,reasons:[]}),v=c=>c==null?null:new Set(c);function E(c,u){const i=v(c.scopes),r=v(u.scopes);let a=null;return i&&r?a=[...i].filter(w=>r.has(w)):i?a=[...i]:r&&(a=[...r]),{allow:!!c.allow&&!!u.allow,scopes:a,reasons:[...c.reasons||[],...u.reasons||[]]}}function x(c,u){if(!c.allow&&u.allow)return!0;const i=v(c.scopes),r=v(u.scopes);return i?r?[...r].some(a=>!i.has(a)):!0:!1}function k({required:c=[],onEvent:u=null}={}){const i=[...new Set([...U,...c])],r=R({onEvent:t=>h(`registry:${t.event}`,t)}),a=new Map,w=new Map,d=new Map;let g=!1;const h=(t,o={})=>{u&&u({event:t,...o})};function D(t){const{id:o,requires:n=[],activate:e,load:s}=t||{};if(!o||typeof o!="string")throw new p("BAD_PLUGIN","plugin.id required");if(typeof e!="function"&&typeof s!="function")throw new p("BAD_PLUGIN",`plugin '${o}': activate or load required`);if(a.has(o))throw new p("DUPLICATE",`plugin '${o}' is already defined`);return a.set(o,{...t,requires:[...n]}),()=>y(o)}function y(t){if(i.includes(t))throw new p("REQUIRED",`plugin '${t}' is required and cannot be removed`);a.delete(t);const o=w.get(t);w.delete(t);for(const[n,e]of d){const s=e.filter(l=>l.pluginId!==t);s.length?d.set(n,s):d.delete(n)}return o?o.dispose():Promise.resolve()}function $(t,o){return{...o,guard(n,e){if(typeof e!="function")throw new p("BAD_GUARD",`guard '${n}' must be a function`);const s=d.get(n)||[];return s.push({pluginId:t,fn:e}),d.set(n,s),()=>{const l=(d.get(n)||[]).filter(f=>f.fn!==e);l.length?d.set(n,l):d.delete(n)}}}}function A(t,o,n=_){let e={allow:!!n.allow,scopes:n.scopes??null,reasons:[...n.reasons||[]]};for(const{pluginId:s,fn:l}of d.get(t)||[]){let f;try{f=l(o,e)}catch(L){e=E(e,{allow:!1,scopes:null,reasons:[`${s}: guard threw (${L.message})`]});continue}if(f==null||f===!0)continue;f===!1&&(f={allow:!1,scopes:null,reasons:[`${s}: denied`]});const m={allow:f.allow!==!1,scopes:f.scopes??null,reasons:f.reasons||[]};x(e,m)&&h("guard:widened",{guard:t,pluginId:s}),e=E(e,m)}return e}async function q(){if(g)return I;const t=i.filter(e=>!a.has(e));if(t.length)throw new p("MISSING_REQUIRED",`kernel will not start: required plugin(s) not defined: ${t.join(", ")}`,t);const o=[...a.values()].sort((e,s)=>(i.includes(s.id)?1:0)-(i.includes(e.id)?1:0));for(const e of o){if(w.has(e.id))continue;let s=e.activate;if(!s){try{const l=await e.load();s=l?.activate||l?.default}catch(l){throw new p("LOAD_FAILED",`plugin '${e.id}' failed to load: ${l.message}`,e.id)}if(typeof s!="function")throw new p("BAD_PLUGIN",`plugin '${e.id}': loaded module exports no activate`,e.id)}w.set(e.id,r.register({name:e.id,requires:e.requires,apply:l=>s($(e.id,l))}))}const n=i.filter(e=>w.get(e)?.state!=="active");if(n.length){const e=n.map(s=>({id:s,error:w.get(s)?.error?.message||"inactive",waitingFor:r.pending().find(l=>l.name===s)?.waitingFor||[]}));throw await r.dispose(),new p("REQUIRED_INACTIVE",`kernel will not start: required plugin(s) not active: ${n.join(", ")}`,e)}return g=!0,h("started",{plugins:r.active()}),I}const I={define:D,remove:y,decide:A,start:q,get started(){return g},list:()=>[...a.keys()].sort(),active:()=>r.active(),pending:()=>r.pending(),required:()=>[...i],guards:()=>[...d.keys()].sort(),registry:r,async stop(){g=!1,await r.dispose(),h("stopped",{})}};return I}export{_ as ALLOW_ALL,p as KernelError,U as REQUIRED_PLUGINS,k as createKernel,E as meetDecisions};