@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/project-run.js CHANGED
@@ -1,444 +1,20 @@
1
- // The executive loop — a goal, run as a project (F8 §12.2.5).
2
- //
3
- // A person writes the goal; the executive does what a manager does with it: reads the page,
4
- // posts the first jobs, recruits for each from the pool, runs the recruited pair as a team
5
- // against the briefs, reads what came back, posts the follow-up jobs the results call for,
6
- // asks the stakeholder before anything that spends more or changes scope, and closes when
7
- // done-when holds. Every step is an event on the project record (project.js `foldProject`),
8
- // so both clients draw the project page live and the loop is RESUMABLE from the record: a
9
- // job already done is not redone, one already recruited is not recruited again.
10
- //
11
- // This module never speaks to a model, never runs a tool and never stores anything. The
12
- // host injects: `plan(prompt, schema)` its structured call on the executive's model;
13
- // `recruit(job)` its pool through recruit.js `recruitForRun`; `runJobs({ team, request })`
14
- // its team runner (team-run.js through the host's own callModel, so a job's work is a run
15
- // on the board with threads, a work log and a scorecard fact per member); `ask({ type, text,
16
- // options })` — the stakeholder. What is asked and what is not is the GATE (gate.js): the
17
- // loop reads it at every step where it would otherwise ask, and asks a person only where
18
- // the gate says a person decides.
19
- //
20
- // Nothing here is created without a decision: an agent proposed for a job nobody fits goes
21
- // to the person as a `permission` ask before it exists (§7, D-A2).
22
-
23
- import { defineSchema, describeSchema, coerce } from './structured.js';
24
- import { GRANT_RE } from './team.js';
25
- import { normalizeJob, readyJobs, canTransition as jobCanMove } from './job.js';
26
- import { emptyProjectRecord, foldProject, projectProgress } from './project.js';
27
- import { effectiveGate, gateAllows } from './gate.js';
28
- import { carveBudget } from './recruit.js';
29
-
30
- export const MAX_JOBS_PER_ROUND = 8;
31
- export const MAX_ROUNDS = 4;
32
- export const EXECUTIVE = 'executive';
33
- export const PROJECT_RUN_STATUSES = Object.freeze(['done', 'open', 'waiting', 'stopped', 'over-budget', 'failed']);
34
-
35
- export class ProjectRunError extends Error {
36
- constructor(code, message) { super(message); this.name = 'ProjectRunError'; this.code = code; }
37
- }
38
-
39
- const clip = (s, n) => String(s || '').trim().slice(0, n);
40
- const slug = (s) => String(s || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^[^a-z]+/, '').replace(/-+$/, '').slice(0, 48);
41
-
42
- // ── What the executive answers in ────────────────────────────────────────────────────────
43
-
44
- const JOB_FIELDS = {
45
- id: { type: 'string', required: true, max: 48, describe: 'a short id like j1' },
46
- title: { type: 'string', required: true, max: 120 },
47
- brief: { type: 'string', required: true, max: 2000, describe: 'what to do and what done looks like — enough for someone who has not read the goal' },
48
- skills: { type: 'string[]', maxItems: 6, describe: 'skills the job needs, by name (finance, review, …) — used to pick who takes it' },
49
- grants: { type: 'string[]', maxItems: 6, describe: 'tools it needs: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr — or none' },
50
- dependsOn: { type: 'string[]', maxItems: 6, describe: 'job ids whose results this one needs' },
51
- why: { type: 'string', max: 200, describe: 'why this job, in a few words' },
52
- };
53
-
54
- /** The first jobs for a goal, and the follow-ups after a round. */
55
- export const PROJECT_JOBS_SCHEMA = defineSchema({
56
- name: 'project_jobs',
57
- purpose: 'the jobs to post on a project for its goal',
58
- fields: {
59
- jobs: { type: 'object[]', required: true, maxItems: MAX_JOBS_PER_ROUND, describe: 'independent where possible; a job that needs another\'s result names it in dependsOn', fields: JOB_FIELDS },
60
- note: { type: 'string', max: 600, describe: 'what you decided and why, for the project page' },
61
- },
62
- nothing: { jobs: [], note: '' },
63
- });
64
-
65
- /** After a round: does done-when hold, what is the report, what is still needed. */
66
- export const PROJECT_REVIEW_SCHEMA = defineSchema({
67
- name: 'project_review',
68
- purpose: 'the executive\'s reading of a round of jobs against the project\'s goal and done-when',
69
- fields: {
70
- done: { type: 'boolean', required: true, describe: 'true only when done-when holds on the results as they are' },
71
- report: { type: 'string', required: true, max: 6000, describe: 'the project\'s report so far: what was established, with the job it came from; what was not' },
72
- followUps: { type: 'object[]', maxItems: MAX_JOBS_PER_ROUND, describe: 'the jobs still needed for done-when to hold — empty when it holds or nothing more would help', fields: JOB_FIELDS },
73
- why: { type: 'string', max: 400, describe: 'why done or not, in a few words' },
74
- },
75
- nothing: { done: false, report: '', followUps: [], why: '' },
76
- });
77
-
78
- const poolLines = (pool) => (pool || []).filter((a) => a && a.id && a.enabled !== false).slice(0, 40)
79
- .map((a) => `- ${a.id}: ${a.name || a.id}${a.purpose ? ` — ${clip(a.purpose, 140)}` : ''}${a.skills?.length ? ` (skills: ${a.skills.join(', ')})` : ''}${a.grants?.length ? ` (tools: ${a.grants.join(', ')})` : ''}`).join('\n');
80
-
81
- const jobLines = (jobs) => (jobs || []).map((j) => `- ${j.id} [${j.status}]: ${j.title}${j.result?.text ? ` — result: ${clip(j.result.text, 400)}` : ''}`).join('\n');
82
-
83
- /** The executive's first instruction: the goal, done-when, the pool, the shape. */
84
- export function jobsPrompt(project, { pool = [], record = null } = {}) {
85
- const had = (record?.jobs || []).length ? `\nJobs already on the project (do not repeat them):\n${jobLines(record.jobs)}\n` : '';
86
- return [
87
- `You are the executive of the project "${project.title}". Post the jobs that get it to done — between 1 and ${MAX_JOBS_PER_ROUND}, independent where possible, each with a brief a stranger could act on.`,
88
- '',
89
- `Goal: ${project.goal}`,
90
- project.doneWhen ? `Done when: ${project.doneWhen}` : '',
91
- project.budget ? `Budget for the whole project: ${Object.entries(project.budget).map(([k, v]) => `${k} ${v}`).join(', ')}` : '',
92
- had,
93
- 'Agents in the pool that may take a job (name the skills and tools a job needs; the fit picks who takes it):',
94
- poolLines(pool) || '- (nobody yet — name the skills and tools anyway; a job nobody fits is proposed as a new agent)',
95
- '',
96
- describeSchema(PROJECT_JOBS_SCHEMA),
97
- ].filter((l) => l !== null && l !== undefined).join('\n');
98
- }
99
-
100
- /** The executive's reading of a round: the goal, done-when, every job's result, the shape. */
101
- export function reviewPrompt(project, record, { round = 1 } = {}) {
102
- return [
103
- `You are the executive of the project "${project.title}". Round ${round} of jobs has finished. Read the results against the goal and done-when. Write the report so far. Say whether done-when HOLDS on these results — not whether it could with more work. If it does not hold and more work would help, post the follow-up jobs (at most ${MAX_JOBS_PER_ROUND}); if nothing more would help, post none and say why.`,
104
- '',
105
- `Goal: ${project.goal}`,
106
- project.doneWhen ? `Done when: ${project.doneWhen}` : 'Done when: (not stated — hold the results to the goal as written)',
107
- '',
108
- 'Jobs and their results:',
109
- jobLines(record?.jobs || []),
110
- record?.report?.text ? `\nThe report before this round:\n${clip(record.report.text, 3000)}` : '',
111
- '',
112
- describeSchema(PROJECT_REVIEW_SCHEMA),
113
- ].join('\n');
114
- }
115
-
116
- /**
117
- * The executive's answer (text or an already-shaped value) as normalized jobs on the project.
118
- * Ids the executive reused are suffixed; grants it made up are dropped; a dependency on a job
119
- * that is not on the project (or on itself) is dropped rather than left dangling.
120
- */
121
- export function parseJobs(answer, project, { existing = [], now = Date.now(), by = EXECUTIVE, schema = PROJECT_JOBS_SCHEMA, field = 'jobs' } = {}) {
122
- const value = typeof answer === 'string' ? coerce(answer, schema)?.value : answer;
123
- const raw = Array.isArray(value?.[field]) ? value[field] : [];
124
- const taken = new Set((existing || []).map((j) => j.id));
125
- const out = [];
126
- for (const [i, j] of raw.entries()) {
127
- if (!j || !clip(j.title, 200) || !clip(j.brief, 8000)) continue;
128
- let id = slug(j.id) || `j${i + 1}`;
129
- if (!/^[a-z]/.test(id)) id = `j-${id}`;
130
- let n = 1;
131
- const base = id;
132
- while (taken.has(id)) { n += 1; id = `${base}-${n}`; }
133
- taken.add(id);
134
- const grants = [...new Set((Array.isArray(j.grants) ? j.grants : []).map((g) => String(g).trim().toLowerCase()).filter((g) => GRANT_RE.test(g) && g !== 'none'))].slice(0, 6);
135
- try {
136
- out.push(normalizeJob({
137
- id, projectId: project.id, title: j.title, brief: j.brief,
138
- needs: { skills: j.skills, grants, tools: [] },
139
- dependsOn: Array.isArray(j.dependsOn) ? j.dependsOn.map((d) => slug(d)) : [],
140
- postedBy: by, postedAt: now, status: 'open',
141
- ...(j.why ? { origin: { kind: 'executive', why: clip(j.why, 200) } } : {}),
142
- }));
143
- } catch { /* a job the form refuses is not posted */ }
144
- if (out.length >= MAX_JOBS_PER_ROUND) break;
145
- }
146
- const ids = new Set([...out.map((j) => j.id), ...(existing || []).map((j) => j.id)]);
147
- for (const j of out) j.dependsOn = j.dependsOn.filter((d) => ids.has(d) && d !== j.id);
148
- return out;
149
- }
150
-
151
- /** The review, coerced: `{ done, report, followUps: jobs[], why }`. */
152
- export function parseReview(answer, project, { existing = [], now = Date.now() } = {}) {
153
- const value = typeof answer === 'string' ? coerce(answer, PROJECT_REVIEW_SCHEMA)?.value : answer;
154
- if (!value || typeof value !== 'object') return null;
155
- return {
156
- done: value.done === true,
157
- report: clip(value.report, 6000),
158
- why: clip(value.why, 400),
159
- followUps: parseJobs(value, project, { existing, now, field: 'followUps', schema: PROJECT_REVIEW_SCHEMA }),
160
- };
161
- }
162
-
163
- /**
164
- * The team a round runs as: one role per recruited job (the role id IS the job id, so two
165
- * jobs taken by the same agent are two members), the job's dependencies as the role's, a
166
- * fixed plan (one task per role, the brief as the task), the members' work side by side —
167
- * the executive reads it, so no judge. The budget is the round's carve.
168
- */
169
- export function teamForRound(project, recruited, { budget, round = 1 } = {}) {
170
- const ids = new Set(recruited.map((r) => r.job.id));
171
- return {
172
- name: project.id,
173
- description: `${project.title} — round ${round}`,
174
- plan: 'fixed', merge: 'concat',
175
- roles: recruited.map(({ job, role }) => ({
176
- ...role,
177
- id: job.id,
178
- name: `${role.name || role.agent || job.id} · ${job.title}`.slice(0, 120),
179
- dependsOn: (job.dependsOn || []).filter((d) => ids.has(d)),
180
- job: job.id,
181
- })),
182
- budget,
183
- };
184
- }
185
-
186
- /** What a round's run says about each job: the task with the job's id, its status and text. */
187
- export function jobResults(run, jobs, { now = Date.now() } = {}) {
188
- const out = [];
189
- for (const job of jobs) {
190
- const task = (run?.tasks || []).find((t) => t.id === `t_${job.id}` || t.role === job.id);
191
- if (!task) { out.push({ id: job.id, status: 'failed', result: { text: 'the run never started this job', by: 'runner', at: now } }); continue; }
192
- const ok = task.status === 'ok';
193
- const findings = (task.findings || []).map((f) => `- ${f.text}${f.refs?.length ? ` (${f.refs.join(', ')})` : ''}`).join('\n');
194
- out.push({
195
- id: job.id,
196
- status: ok ? 'done' : 'failed',
197
- result: { text: clip(ok ? (findings || task.text) : (task.error || task.status || 'failed'), 8000), by: task.role || job.id, at: now, refs: [`run:${run.runId}`, ...(task.findings || []).flatMap((f) => f.refs || []).slice(0, 8)] },
198
- });
199
- }
200
- return out;
201
- }
202
-
203
- /**
204
- * The jobs a round runs: every open job whose dependencies are done — and, with them, every
205
- * open job whose dependencies are done OR in this round. A chain (facts → memo) is one round
206
- * and one team; the runner's barriers order it (team-run.js), and the executive reviews the
207
- * whole chain, not half of it.
208
- */
209
- export function roundJobs(jobs) {
210
- const wave = readyJobs(jobs);
211
- const inWave = new Set(wave.map((j) => j.id));
212
- const byId = new Map((jobs || []).map((j) => [j.id, j]));
213
- for (;;) {
214
- const more = (jobs || []).filter((j) => j.status === 'open' && !inWave.has(j.id) && (j.dependsOn || []).every((d) => byId.get(d)?.status === 'done' || inWave.has(d)));
215
- if (!more.length) break;
216
- for (const j of more) { wave.push(j); inWave.add(j.id); }
217
- }
218
- return wave;
219
- }
220
-
221
- /** The round's budget: what the project has left, split over the jobs that run, never under a floor a run can start with. */
222
- export function roundBudget(project, record, jobs) {
223
- const cap = project.budget || {};
224
- const spent = record?.spend || {};
225
- const out = {};
226
- for (const k of ['tokens', 'calls', 'ms', 'usd']) {
227
- if (!(Number(cap[k]) > 0)) continue;
228
- const left = Math.max(0, Number(cap[k]) - (Number(spent[k]) || 0));
229
- if (left > 0) out[k] = k === 'usd' ? Math.round(left * 100) / 100 : Math.floor(left);
230
- }
231
- // A job's own budget caps its share; the round is the sum of the shares, within what is left.
232
- const shares = jobs.map((j) => carveBudget(j, record)).filter(Boolean);
233
- if (shares.length === jobs.length) {
234
- for (const k of Object.keys(out)) {
235
- const sum = shares.reduce((n, s) => n + (Number(s[k]) || 0), 0);
236
- if (sum > 0) out[k] = Math.min(out[k], sum);
237
- }
238
- }
239
- return Object.keys(out).length ? out : null;
240
- }
241
-
242
- // ── The loop ─────────────────────────────────────────────────────────────────────────────
243
-
244
- /**
245
- * @param project the page (project.js normalizeProject)
246
- * @param record the project record so far (foldProject), for a resume; null starts fresh
247
- * @param pool the agent cards the executive may recruit from (read for the prompt; the
248
- * host's `recruit` does the pass)
249
- * @param gate the org's gate (gate.js); the project's own partial gate overrides it
250
- * @param executive the agent id that runs the loop — on every decision as `by`
251
- * @param plan `async (prompt, schema) => value | text | null` — the host's structured call
252
- * @param recruit `async (job, { projectId, create? }) => { role, agentId, engine, why, fit } |
253
- * { proposal, why } | null` — the host's pool (recruit.js recruitForRun)
254
- * @param runJobs `async ({ team, request, projectId, round }) => run result` — the host's
255
- * team runner; `team` is teamForRound's
256
- * @param ask `async ({ type, text, options }) => { text, by } | null` — the stakeholder;
257
- * absent, every ask is answered "no" and the loop stops where it would ask
258
- * @param emit `(type, payload)` — every project-record event, in order; the host lands
259
- * them on the gateway's project record (and this call folds them too)
260
- * @param maxRounds rounds of post → recruit → run → review before the loop stops and asks
261
- */
262
- export async function runProject({
263
- project, record = null, pool = [], gate = null, executive = EXECUTIVE,
264
- plan, recruit = null, runJobs, ask = null, emit = () => {},
265
- now = () => Date.now(), signal = null, maxRounds = MAX_ROUNDS,
266
- } = {}) {
267
- if (!project?.id || !project.goal) throw new ProjectRunError('BAD_PROJECT', 'a project with an id and a goal is required');
268
- if (typeof plan !== 'function') throw new ProjectRunError('BAD_RUN', 'plan required');
269
- if (typeof runJobs !== 'function') throw new ProjectRunError('BAD_RUN', 'runJobs required');
270
- const rec = record && record.id === project.id ? record : emptyProjectRecord({ id: project.id, now: now() });
271
- if (!rec.page) foldProject(rec, { type: 'project.created', at: now(), payload: { project } });
272
- const g = effectiveGate(gate, project.gate || null);
273
- const stopped = () => !!signal?.aborted;
274
- const say = (type, payload = {}) => { const ev = { type, at: now(), ...payload }; foldProject(rec, { type, at: ev.at, payload }); emit(type, { projectId: project.id, ...ev }); };
275
- const decide = (kind, text, refs = []) => say('project.decision', { by: executive, kind, text: clip(text, 2000), refs });
276
- // The recruiter's own events (recruit.js recruitEvents: evaluating → recruited | open + a
277
- // proposal decision) land on the record as they are.
278
- const land = (events) => { for (const ev of events || []) { if (!ev?.type) continue; const { type, at: _at, ...payload } = ev; say(type, payload); } };
279
- const runs = [];
280
- let rounds = 0;
281
- const finish = (status, extra = {}) => ({ projectId: project.id, status, rounds, jobs: rec.jobs.map((j) => ({ id: j.id, title: j.title, status: j.status, ...(j.recruited ? { agentId: j.recruited.agentId } : {}), ...(j.runId ? { runId: j.runId } : {}) })), runs, report: rec.report?.text || '', spend: rec.spend, progress: projectProgress(rec), record: rec, ...extra });
282
-
283
- /** The stakeholder, where the gate says a person decides; recorded either way. */
284
- const askPerson = async ({ type, text, options }) => {
285
- decide('ask', `${text}${options?.length ? ` [${options.join(' / ')}]` : ''}`);
286
- if (typeof ask !== 'function') { decide('answer', 'nobody to ask — taken as no'); return null; }
287
- let a = null;
288
- try { a = await ask({ type, text, options }); } catch { a = null; }
289
- decide('answer', a ? `${a.by || 'person'}: ${a.text}` : 'no answer');
290
- return a;
291
- };
292
- const yes = (a) => !!a && /^(yes|ok|allow|approve|go|post|recruit|create|raise|close|done|continue|run)/i.test(String(a.text || '').trim());
293
-
294
- /** The executive's structured call, either shape, never a throw. */
295
- const structured = async (prompt, schema) => { try { const v = await plan(prompt, schema); return typeof v === 'string' ? coerce(v, schema)?.value ?? null : (v && typeof v === 'object' ? v : null); } catch { return null; } };
296
-
297
- /** Post jobs — the person's say first when the gate wants it. */
298
- const post = async (jobs, { what }) => {
299
- if (!jobs.length) return [];
300
- const lines = jobs.map((j) => `• ${j.id}: ${j.title}${j.needs?.skills?.length ? ` (${j.needs.skills.join(', ')})` : ''}${j.dependsOn?.length ? ` — after ${j.dependsOn.join(', ')}` : ''}`).join('\n');
301
- // Scope is the stakeholder's (D-A3): the first jobs and any follow-up are posted with
302
- // their say unless the gate lets the executive recruit on its own.
303
- if (!gateAllows(g, 'recruit').allowed) {
304
- const a = await askPerson({ type: 'direction', text: `${what} — post ${jobs.length} job${jobs.length === 1 ? '' : 's'}?\n${lines}`, options: ['Post them', 'Stop here'] });
305
- if (!yes(a)) return null;
306
- } else decide('plan', `${what}: ${jobs.length} job${jobs.length === 1 ? '' : 's'} posted\n${lines}`);
307
- for (const j of jobs) say('job.posted', { job: j, by: executive });
308
- return jobs;
309
- };
310
-
311
- if (rec.status === 'draft') say('project.status', { status: 'open', by: executive });
312
- if (['done', 'closed'].includes(rec.status)) return finish(rec.status === 'done' ? 'done' : 'stopped', { why: `the project is ${rec.status}` });
313
- // A RESUME: a job the last loop left recruited or running (its process died) is open
314
- // again — recruited again, run again; one it finished is not. The record is the checkpoint.
315
- if (record) {
316
- const stuck = rec.jobs.filter((j) => ['evaluating', 'recruited', 'in-progress'].includes(j.status));
317
- for (const j of stuck) say('job.updated', { job: { id: j.id, status: 'open' }, by: executive });
318
- if (stuck.length) decide('resume', `resumed: ${stuck.map((j) => j.id).join(', ')} back to open (left ${stuck.map((j) => j.status).join(', ')} by the last loop)`);
319
- else if (rec.jobs.length) decide('resume', `resumed with ${rec.jobs.filter((j) => j.status === 'done').length} of ${rec.jobs.length} jobs done`);
320
- }
321
-
322
- // ── 1. the first jobs (skipped on a resume that already has some) ──
323
- if (!rec.jobs.length) {
324
- const value = await structured(jobsPrompt(project, { pool, record: rec }), PROJECT_JOBS_SCHEMA);
325
- const jobs = parseJobs(value || {}, project, { existing: rec.jobs, now: now(), by: executive });
326
- if (!jobs.length) {
327
- decide('plan', 'the executive could not turn the goal into jobs');
328
- const a = await askPerson({ type: 'direction', text: `I could not turn the goal into jobs. Post one job for the whole goal, or stop?`, options: ['Post one job', 'Stop'] });
329
- if (!yes(a)) return finish('failed', { why: 'no jobs' });
330
- jobs.push(normalizeJob({ id: 'j1', projectId: project.id, title: clip(project.title, 200), brief: `${project.goal}${project.doneWhen ? `\n\nDone when: ${project.doneWhen}` : ''}`, needs: { skills: [], grants: ['data', 'web'], tools: [] }, postedBy: executive, postedAt: now() }));
331
- } else if (value?.note) decide('plan', value.note);
332
- const posted = await post(jobs, { what: 'First jobs' });
333
- if (!posted) return finish('stopped', { why: 'the stakeholder did not post the first jobs' });
334
- }
335
-
336
- // ── rounds: recruit what is ready → run → fold → review → follow-ups ──
337
- for (;;) {
338
- if (stopped()) return finish('stopped');
339
- if (rounds >= maxRounds) {
340
- decide('review', `${rounds} rounds run; the loop stops here and asks`);
341
- const a = await askPerson({ type: 'direction', text: `${rounds} rounds have run and done-when does not hold yet. Run another round, or stop with the report so far?`, options: ['Run another round', 'Stop here'] });
342
- if (!yes(a)) return finish('open', { why: 'rounds exhausted' });
343
- maxRounds += 1;
344
- }
345
- const ready = roundJobs(rec.jobs);
346
- const stillOpen = rec.jobs.filter((j) => ['open', 'evaluating', 'recruited', 'in-progress'].includes(j.status));
347
- if (!ready.length) {
348
- if (!stillOpen.length) return finish(rec.status === 'done' ? 'done' : 'open', { why: 'nothing left to run' });
349
- // Open jobs whose dependencies failed: they cannot run.
350
- decide('review', `${stillOpen.length} job${stillOpen.length === 1 ? '' : 's'} cannot run: a dependency failed`);
351
- for (const j of stillOpen) say('job.updated', { job: { id: j.id, status: 'failed', result: { text: 'a job it depends on failed', by: executive, at: now() } }, by: executive });
352
- return finish('failed', { why: 'dependencies failed' });
353
- }
354
-
355
- // ── 2. recruit ──
356
- const recruited = [];
357
- for (const job of ready) {
358
- if (stopped()) return finish('stopped');
359
- if (typeof recruit !== 'function') { say('job.updated', { job: { id: job.id, status: 'failed', result: { text: 'this host has no pool to recruit from', by: executive, at: now() } }, by: executive }); continue; }
360
- let r = null;
361
- try { r = await recruit(job, { projectId: project.id }); } catch (e) { decide('recruit', `recruiting for "${job.title}" failed: ${clip(e?.message || e, 200)}`, [`job:${job.id}`]); }
362
- // The recruiter's own events (evaluating → recruited | open + proposal) land on the record.
363
- land(r?.events);
364
- if (!r?.role && r?.proposal) {
365
- // Nobody fits: the agent the job describes, proposed — created only on the person's say.
366
- const card = r.proposal;
367
- say('project.decision', { by: executive, kind: 'proposal', text: `No one in the pool fits "${job.title}"${r.why ? ` — ${r.why}` : ''}. Proposed: ${card.name}${card.skills?.length ? ` — skills ${card.skills.join(', ')}` : ''}${card.grants?.length ? `; grants ${card.grants.join(', ')}` : ''}.`, refs: [`job:${job.id}`], proposal: { kind: 'agent', agent: card, jobId: job.id } });
368
- const allowed = gateAllows(g, 'newAgent').allowed;
369
- const a = allowed ? { text: 'Create it', by: 'gate' } : await askPerson({ type: 'permission', text: `No one fits "${job.title}". Create the agent "${card.name}" (${[card.skills?.length ? `skills ${card.skills.join(', ')}` : '', card.grants?.length ? `grants ${card.grants.join(', ')}` : ''].filter(Boolean).join('; ') || 'no particular skills'}) and give it the job?`, options: ['Create it', 'Skip'] });
370
- if (yes(a)) {
371
- try { r = await recruit(job, { projectId: project.id, create: card }); } catch (e) { decide('recruit', `creating "${card.name}" failed: ${clip(e?.message || e, 200)}`, [`job:${job.id}`]); r = null; }
372
- land(r?.events);
373
- }
374
- }
375
- if (!r?.role) {
376
- say('job.updated', { job: { id: job.id, status: 'failed', result: { text: `nobody took it${r?.why ? ` — ${r.why}` : ''}`, by: executive, at: now() } }, by: executive });
377
- continue;
378
- }
379
- // A recruited job is recorded so a resume does not recruit it again (the recruiter's
380
- // events say so when it landed them; a bare role is recorded here).
381
- const nowJob = rec.jobs.find((j) => j.id === job.id);
382
- if (nowJob && nowJob.status !== 'recruited' && jobCanMove(nowJob.status, 'recruited')) say('job.updated', { job: { id: job.id, status: 'recruited', recruited: { agentId: r.agentId || r.role.agent || r.role.id, ...(r.engine ? { engine: r.engine } : {}), by: 'fit', at: now(), ...(r.why ? { why: clip(r.why, 600) } : {}) } }, by: executive });
383
- decide('recruit', `${r.agentId || r.role.agent || r.role.id} took "${job.title}"${r.why ? ` — ${r.why}` : ''}`, [`job:${job.id}`]);
384
- recruited.push({ job: rec.jobs.find((j) => j.id === job.id) || job, role: r.role });
385
- }
386
- if (!recruited.length) {
387
- if (rec.jobs.some((j) => ['open', 'recruited'].includes(j.status))) continue; // dependents of a failed job: the next pass fails them
388
- return finish('failed', { why: 'nobody took any job' });
389
- }
390
-
391
- // ── 3. run the round as one team ──
392
- const jobs = recruited.map((x) => x.job);
393
- const budget = roundBudget(project, rec, jobs);
394
- if (!budget) {
395
- decide('review', 'the project has no budget left for another round');
396
- const allowed = gateAllows(g, 'budgetRaise').allowed;
397
- const a = allowed ? null : await askPerson({ type: 'budget', text: `The project has spent its budget (${Object.entries(rec.spend).filter(([, v]) => v).map(([k, v]) => `${k} ${v}`).join(', ')}) with ${jobs.length} job${jobs.length === 1 ? '' : 's'} recruited and not run. Raise it by half, or stop here with the report so far?`, options: ['Raise by half', 'Stop here'] });
398
- if (!allowed && !yes(a)) return finish('over-budget');
399
- for (const k of Object.keys(project.budget || {})) project.budget[k] = Math.ceil(project.budget[k] * 1.5);
400
- say('project.updated', { project: { ...project } });
401
- decide('answer', `budget raised by half: ${Object.entries(project.budget).map(([k, v]) => `${k} ${v}`).join(', ')}`);
402
- }
403
- rounds += 1;
404
- const team = teamForRound(project, recruited, { budget: roundBudget(project, rec, jobs), round: rounds });
405
- for (const j of jobs) say('job.updated', { job: { id: j.id, status: 'in-progress' }, by: executive });
406
- decide('run', `round ${rounds}: ${jobs.map((j) => j.id).join(', ')} run as one team (${Object.entries(team.budget || {}).map(([k, v]) => `${k} ${v}`).join(', ')})`);
407
- let run = null;
408
- try {
409
- run = await runJobs({ team, request: [project.goal, project.doneWhen ? `Done when: ${project.doneWhen}` : ''].filter(Boolean).join('\n'), projectId: project.id, round: rounds, jobs });
410
- } catch (e) {
411
- decide('run', `round ${rounds} failed to run: ${clip(e?.message || e, 300)}`);
412
- }
413
- if (run?.runId) {
414
- runs.push({ runId: run.runId, round: rounds, status: run.status });
415
- for (const j of jobs) say('run.linked', { runId: run.runId, jobId: j.id });
416
- if (run.usage?.spent) say('run.spent', { runId: run.runId, spent: { ...run.usage.spent, ms: Number(run.usage.spent.ms) || Math.max(0, (run.endedAt || now()) - (run.startedAt || now())) } });
417
- }
418
- for (const res of jobResults(run, jobs, { now: now() })) say('job.updated', { job: { ...res, ...(run?.runId ? { runId: run.runId } : {}) }, by: executive });
419
- if (run && ['stopped', 'waiting'].includes(run.status)) return finish(run.status, { why: `round ${rounds} ${run.status}` });
420
- if (stopped()) return finish('stopped');
421
-
422
- // ── 4. review: done-when, the report, the follow-ups ──
423
- const value = await structured(reviewPrompt(project, rec, { round: rounds }), PROJECT_REVIEW_SCHEMA);
424
- const review = parseReview(value || {}, project, { existing: rec.jobs, now: now() });
425
- if (review?.report) say('project.report', { text: review.report, by: executive });
426
- else say('project.report', { text: rec.jobs.map((j) => `## ${j.title} (${j.status})\n${j.result?.text || ''}`).join('\n\n'), by: 'concat' });
427
- decide('review', review ? `${review.done ? 'done-when holds' : 'done-when does not hold yet'}${review.why ? ` — ${review.why}` : ''}${review.followUps.length ? `; ${review.followUps.length} follow-up${review.followUps.length === 1 ? '' : 's'}` : ''}` : 'the executive did not answer the review; the report is the results as they are');
428
- if (review?.done) {
429
- // Closing is the stakeholder's (project.js: done-when held, a person closed it) —
430
- // unless the gate lets the executive write back on its own.
431
- const allowed = gateAllows(g, 'writeBack').allowed;
432
- const a = allowed ? { text: 'Close as done', by: 'gate' } : await askPerson({ type: 'permission', text: `Done-when holds${review.why ? ` — ${review.why}` : ''}. Close the project as done?`, options: ['Close as done', 'Keep it open'] });
433
- if (yes(a)) { say('project.status', { status: 'done', by: a.by || 'person' }); return finish('done'); }
434
- return finish('open', { why: 'done-when holds; left open by the stakeholder' });
435
- }
436
- if (!review?.followUps?.length) {
437
- const left = readyJobs(rec.jobs);
438
- if (left.length) continue; // jobs queued behind this round run next
439
- return finish('open', { why: review ? 'nothing more would help' : 'no review' });
440
- }
441
- const posted = await post(review.followUps, { what: `After round ${rounds}` });
442
- if (!posted) return finish('open', { why: 'follow-ups not posted' });
443
- }
444
- }
1
+ import{defineSchema as F,describeSchema as H,coerce as J}from"./structured.js";import{GRANT_RE as re}from"./team.js";import{normalizeJob as X,readyJobs as V,canTransition as ie}from"./job.js";import{emptyProjectRecord as ae,foldProject as Y,projectProgress as de}from"./project.js";import{effectiveGate as le,gateAllows as C}from"./gate.js";import{carveBudget as ue}from"./recruit.js";const D=8,he=4,K="executive",Se=Object.freeze(["done","open","waiting","stopped","over-budget","failed"]);class M extends Error{constructor(t,h){super(h),this.name="ProjectRunError",this.code=t}}const x=(e,t)=>String(e||"").trim().slice(0,t),Q=e=>String(e||"").trim().toLowerCase().replace(/[^a-z0-9_-]+/g,"-").replace(/^[^a-z]+/,"").replace(/-+$/,"").slice(0,48),Z={id:{type:"string",required:!0,max:48,describe:"a short id like j1"},title:{type:"string",required:!0,max:120},brief:{type:"string",required:!0,max:2e3,describe:"what to do and what done looks like enough for someone who has not read the goal"},skills:{type:"string[]",maxItems:6,describe:"skills the job needs, by name (finance, review, …) — used to pick who takes it"},grants:{type:"string[]",maxItems:6,describe:"tools it needs: data, web, history, mcp, shell, fs:write, scm:read, scm:push, scm:pr — or none"},dependsOn:{type:"string[]",maxItems:6,describe:"job ids whose results this one needs"},why:{type:"string",max:200,describe:"why this job, in a few words"}},N=F({name:"project_jobs",purpose:"the jobs to post on a project for its goal",fields:{jobs:{type:"object[]",required:!0,maxItems:D,describe:"independent where possible; a job that needs another's result names it in dependsOn",fields:Z},note:{type:"string",max:600,describe:"what you decided and why, for the project page"}},nothing:{jobs:[],note:""}}),U=F({name:"project_review",purpose:"the executive's reading of a round of jobs against the project's goal and done-when",fields:{done:{type:"boolean",required:!0,describe:"true only when done-when holds on the results as they are"},report:{type:"string",required:!0,max:6e3,describe:"the project's report so far: what was established, with the job it came from; what was not"},followUps:{type:"object[]",maxItems:D,describe:"the jobs still needed for done-when to hold — empty when it holds or nothing more would help",fields:Z},why:{type:"string",max:400,describe:"why done or not, in a few words"}},nothing:{done:!1,report:"",followUps:[],why:""}}),pe=e=>(e||[]).filter(t=>t&&t.id&&t.enabled!==!1).slice(0,40).map(t=>`- ${t.id}: ${t.name||t.id}${t.purpose?` — ${x(t.purpose,140)}`:""}${t.skills?.length?` (skills: ${t.skills.join(", ")})`:""}${t.grants?.length?` (tools: ${t.grants.join(", ")})`:""}`).join(`
2
+ `),ee=e=>(e||[]).map(t=>`- ${t.id} [${t.status}]: ${t.title}${t.result?.text?` — result: ${x(t.result.text,400)}`:""}`).join(`
3
+ `);function fe(e,{pool:t=[],record:h=null}={}){const $=(h?.jobs||[]).length?`
4
+ Jobs already on the project (do not repeat them):
5
+ ${ee(h.jobs)}
6
+ `:"";return[`You are the executive of the project "${e.title}". Post the jobs that get it to done — between 1 and ${D}, independent where possible, each with a brief a stranger could act on.`,"",`Goal: ${e.goal}`,e.doneWhen?`Done when: ${e.doneWhen}`:"",e.budget?`Budget for the whole project: ${Object.entries(e.budget).map(([o,r])=>`${o} ${r}`).join(", ")}`:"",$,"Agents in the pool that may take a job (name the skills and tools a job needs; the fit picks who takes it):",pe(t)||"- (nobody yet — name the skills and tools anyway; a job nobody fits is proposed as a new agent)","",H(N)].filter(o=>o!=null).join(`
7
+ `)}function ce(e,t,{round:h=1}={}){return[`You are the executive of the project "${e.title}". Round ${h} of jobs has finished. Read the results against the goal and done-when. Write the report so far. Say whether done-when HOLDS on these results — not whether it could with more work. If it does not hold and more work would help, post the follow-up jobs (at most ${D}); if nothing more would help, post none and say why.`,"",`Goal: ${e.goal}`,e.doneWhen?`Done when: ${e.doneWhen}`:"Done when: (not stated — hold the results to the goal as written)","","Jobs and their results:",ee(t?.jobs||[]),t?.report?.text?`
8
+ The report before this round:
9
+ ${x(t.report.text,3e3)}`:"","",H(U)].join(`
10
+ `)}function te(e,t,{existing:h=[],now:$=Date.now(),by:o=K,schema:r=N,field:y="jobs"}={}){const f=typeof e=="string"?J(e,r)?.value:e,b=Array.isArray(f?.[y])?f[y]:[],A=new Set((h||[]).map(I=>I.id)),c=[];for(const[I,n]of b.entries()){if(!n||!x(n.title,200)||!x(n.brief,8e3))continue;let O=Q(n.id)||`j${I+1}`;/^[a-z]/.test(O)||(O=`j-${O}`);let _=1;const g=O;for(;A.has(O);)_+=1,O=`${g}-${_}`;A.add(O);const w=[...new Set((Array.isArray(n.grants)?n.grants:[]).map(R=>String(R).trim().toLowerCase()).filter(R=>re.test(R)&&R!=="none"))].slice(0,6);try{c.push(X({id:O,projectId:t.id,title:n.title,brief:n.brief,needs:{skills:n.skills,grants:w,tools:[]},dependsOn:Array.isArray(n.dependsOn)?n.dependsOn.map(R=>Q(R)):[],postedBy:o,postedAt:$,status:"open",...n.why?{origin:{kind:"executive",why:x(n.why,200)}}:{}}))}catch{}if(c.length>=D)break}const B=new Set([...c.map(I=>I.id),...(h||[]).map(I=>I.id)]);for(const I of c)I.dependsOn=I.dependsOn.filter(n=>B.has(n)&&n!==I.id);return c}function be(e,t,{existing:h=[],now:$=Date.now()}={}){const o=typeof e=="string"?J(e,U)?.value:e;return!o||typeof o!="object"?null:{done:o.done===!0,report:x(o.report,6e3),why:x(o.why,400),followUps:te(o,t,{existing:h,now:$,field:"followUps",schema:U})}}function ge(e,t,{budget:h,round:$=1}={}){const o=new Set(t.map(r=>r.job.id));return{name:e.id,description:`${e.title} — round ${$}`,plan:"fixed",merge:"concat",roles:t.map(({job:r,role:y})=>({...y,id:r.id,name:`${y.name||y.agent||r.id} · ${r.title}`.slice(0,120),dependsOn:(r.dependsOn||[]).filter(f=>o.has(f)),job:r.id})),budget:h}}function we(e,t,{now:h=Date.now()}={}){const $=[];for(const o of t){const r=(e?.tasks||[]).find(b=>b.id===`t_${o.id}`||b.role===o.id);if(!r){$.push({id:o.id,status:"failed",result:{text:"the run never started this job",by:"runner",at:h}});continue}const y=r.status==="ok",f=(r.findings||[]).map(b=>`- ${b.text}${b.refs?.length?` (${b.refs.join(", ")})`:""}`).join(`
11
+ `);$.push({id:o.id,status:y?"done":"failed",result:{text:x(y?f||r.text:r.error||r.status||"failed",8e3),by:r.role||o.id,at:h,refs:[`run:${e.runId}`,...(r.findings||[]).flatMap(b=>b.refs||[]).slice(0,8)]}})}return $}function ye(e){const t=V(e),h=new Set(t.map(o=>o.id)),$=new Map((e||[]).map(o=>[o.id,o]));for(;;){const o=(e||[]).filter(r=>r.status==="open"&&!h.has(r.id)&&(r.dependsOn||[]).every(y=>$.get(y)?.status==="done"||h.has(y)));if(!o.length)break;for(const r of o)t.push(r),h.add(r.id)}return t}function oe(e,t,h){const $=e.budget||{},o=t?.spend||{},r={};for(const f of["tokens","calls","ms","usd"]){if(!(Number($[f])>0))continue;const b=Math.max(0,Number($[f])-(Number(o[f])||0));b>0&&(r[f]=f==="usd"?Math.round(b*100)/100:Math.floor(b))}const y=h.map(f=>ue(f,t)).filter(Boolean);if(y.length===h.length)for(const f of Object.keys(r)){const b=y.reduce((A,c)=>A+(Number(c[f])||0),0);b>0&&(r[f]=Math.min(r[f],b))}return Object.keys(r).length?r:null}async function Re({project:e,record:t=null,pool:h=[],gate:$=null,executive:o=K,plan:r,recruit:y=null,runJobs:f,ask:b=null,emit:A=()=>{},now:c=()=>Date.now(),signal:B=null,maxRounds:I=he}={}){if(!e?.id||!e.goal)throw new M("BAD_PROJECT","a project with an id and a goal is required");if(typeof r!="function")throw new M("BAD_RUN","plan required");if(typeof f!="function")throw new M("BAD_RUN","runJobs required");const n=t&&t.id===e.id?t:ae({id:e.id,now:c()});n.page||Y(n,{type:"project.created",at:c(),payload:{project:e}});const O=le($,e.gate||null),_=()=>!!B?.aborted,g=(i,a={})=>{const d={type:i,at:c(),...a};Y(n,{type:i,at:d.at,payload:a}),A(i,{projectId:e.id,...d})},w=(i,a,d=[])=>g("project.decision",{by:o,kind:i,text:x(a,2e3),refs:d}),R=i=>{for(const a of i||[]){if(!a?.type)continue;const{type:d,at:u,...L}=a;g(d,L)}},T=[];let v=0;const j=(i,a={})=>({projectId:e.id,status:i,rounds:v,jobs:n.jobs.map(d=>({id:d.id,title:d.title,status:d.status,...d.recruited?{agentId:d.recruited.agentId}:{},...d.runId?{runId:d.runId}:{}})),runs:T,report:n.report?.text||"",spend:n.spend,progress:de(n),record:n,...a}),P=async({type:i,text:a,options:d})=>{if(w("ask",`${a}${d?.length?` [${d.join(" / ")}]`:""}`),typeof b!="function")return w("answer","nobody to ask — taken as no"),null;let u=null;try{u=await b({type:i,text:a,options:d})}catch{u=null}return w("answer",u?`${u.by||"person"}: ${u.text}`:"no answer"),u},E=i=>!!i&&/^(yes|ok|allow|approve|go|post|recruit|create|raise|close|done|continue|run)/i.test(String(i.text||"").trim()),W=async(i,a)=>{try{const d=await r(i,a);return typeof d=="string"?J(d,a)?.value??null:d&&typeof d=="object"?d:null}catch{return null}},q=async(i,{what:a})=>{if(!i.length)return[];const d=i.map(u=>`• ${u.id}: ${u.title}${u.needs?.skills?.length?` (${u.needs.skills.join(", ")})`:""}${u.dependsOn?.length?` — after ${u.dependsOn.join(", ")}`:""}`).join(`
12
+ `);if(C(O,"recruit").allowed)w("plan",`${a}: ${i.length} job${i.length===1?"":"s"} posted
13
+ ${d}`);else{const u=await P({type:"direction",text:`${a} post ${i.length} job${i.length===1?"":"s"}?
14
+ ${d}`,options:["Post them","Stop here"]});if(!E(u))return null}for(const u of i)g("job.posted",{job:u,by:o});return i};if(n.status==="draft"&&g("project.status",{status:"open",by:o}),["done","closed"].includes(n.status))return j(n.status==="done"?"done":"stopped",{why:`the project is ${n.status}`});if(t){const i=n.jobs.filter(a=>["evaluating","recruited","in-progress"].includes(a.status));for(const a of i)g("job.updated",{job:{id:a.id,status:"open"},by:o});i.length?w("resume",`resumed: ${i.map(a=>a.id).join(", ")} back to open (left ${i.map(a=>a.status).join(", ")} by the last loop)`):n.jobs.length&&w("resume",`resumed with ${n.jobs.filter(a=>a.status==="done").length} of ${n.jobs.length} jobs done`)}if(!n.jobs.length){const i=await W(fe(e,{pool:h,record:n}),N),a=te(i||{},e,{existing:n.jobs,now:c(),by:o});if(a.length)i?.note&&w("plan",i.note);else{w("plan","the executive could not turn the goal into jobs");const u=await P({type:"direction",text:"I could not turn the goal into jobs. Post one job for the whole goal, or stop?",options:["Post one job","Stop"]});if(!E(u))return j("failed",{why:"no jobs"});a.push(X({id:"j1",projectId:e.id,title:x(e.title,200),brief:`${e.goal}${e.doneWhen?`
15
+
16
+ Done when: ${e.doneWhen}`:""}`,needs:{skills:[],grants:["data","web"],tools:[]},postedBy:o,postedAt:c()}))}if(!await q(a,{what:"First jobs"}))return j("stopped",{why:"the stakeholder did not post the first jobs"})}for(;;){if(_())return j("stopped");if(v>=I){w("review",`${v} rounds run; the loop stops here and asks`);const s=await P({type:"direction",text:`${v} rounds have run and done-when does not hold yet. Run another round, or stop with the report so far?`,options:["Run another round","Stop here"]});if(!E(s))return j("open",{why:"rounds exhausted"});I+=1}const i=ye(n.jobs),a=n.jobs.filter(s=>["open","evaluating","recruited","in-progress"].includes(s.status));if(!i.length){if(!a.length)return j(n.status==="done"?"done":"open",{why:"nothing left to run"});w("review",`${a.length} job${a.length===1?"":"s"} cannot run: a dependency failed`);for(const s of a)g("job.updated",{job:{id:s.id,status:"failed",result:{text:"a job it depends on failed",by:o,at:c()}},by:o});return j("failed",{why:"dependencies failed"})}const d=[];for(const s of i){if(_())return j("stopped");if(typeof y!="function"){g("job.updated",{job:{id:s.id,status:"failed",result:{text:"this host has no pool to recruit from",by:o,at:c()}},by:o});continue}let l=null;try{l=await y(s,{projectId:e.id})}catch(p){w("recruit",`recruiting for "${s.title}" failed: ${x(p?.message||p,200)}`,[`job:${s.id}`])}if(R(l?.events),!l?.role&&l?.proposal){const p=l.proposal;g("project.decision",{by:o,kind:"proposal",text:`No one in the pool fits "${s.title}"${l.why?` ${l.why}`:""}. Proposed: ${p.name}${p.skills?.length?` — skills ${p.skills.join(", ")}`:""}${p.grants?.length?`; grants ${p.grants.join(", ")}`:""}.`,refs:[`job:${s.id}`],proposal:{kind:"agent",agent:p,jobId:s.id}});const se=C(O,"newAgent").allowed?{text:"Create it",by:"gate"}:await P({type:"permission",text:`No one fits "${s.title}". Create the agent "${p.name}" (${[p.skills?.length?`skills ${p.skills.join(", ")}`:"",p.grants?.length?`grants ${p.grants.join(", ")}`:""].filter(Boolean).join("; ")||"no particular skills"}) and give it the job?`,options:["Create it","Skip"]});if(E(se)){try{l=await y(s,{projectId:e.id,create:p})}catch(G){w("recruit",`creating "${p.name}" failed: ${x(G?.message||G,200)}`,[`job:${s.id}`]),l=null}R(l?.events)}}if(!l?.role){g("job.updated",{job:{id:s.id,status:"failed",result:{text:`nobody took it${l?.why?` — ${l.why}`:""}`,by:o,at:c()}},by:o});continue}const S=n.jobs.find(p=>p.id===s.id);S&&S.status!=="recruited"&&ie(S.status,"recruited")&&g("job.updated",{job:{id:s.id,status:"recruited",recruited:{agentId:l.agentId||l.role.agent||l.role.id,...l.engine?{engine:l.engine}:{},by:"fit",at:c(),...l.why?{why:x(l.why,600)}:{}}},by:o}),w("recruit",`${l.agentId||l.role.agent||l.role.id} took "${s.title}"${l.why?` — ${l.why}`:""}`,[`job:${s.id}`]),d.push({job:n.jobs.find(p=>p.id===s.id)||s,role:l.role})}if(!d.length){if(n.jobs.some(s=>["open","recruited"].includes(s.status)))continue;return j("failed",{why:"nobody took any job"})}const u=d.map(s=>s.job);if(!oe(e,n,u)){w("review","the project has no budget left for another round");const s=C(O,"budgetRaise").allowed,l=s?null:await P({type:"budget",text:`The project has spent its budget (${Object.entries(n.spend).filter(([,S])=>S).map(([S,p])=>`${S} ${p}`).join(", ")}) with ${u.length} job${u.length===1?"":"s"} recruited and not run. Raise it by half, or stop here with the report so far?`,options:["Raise by half","Stop here"]});if(!s&&!E(l))return j("over-budget");for(const S of Object.keys(e.budget||{}))e.budget[S]=Math.ceil(e.budget[S]*1.5);g("project.updated",{project:{...e}}),w("answer",`budget raised by half: ${Object.entries(e.budget).map(([S,p])=>`${S} ${p}`).join(", ")}`)}v+=1;const z=ge(e,d,{budget:oe(e,n,u),round:v});for(const s of u)g("job.updated",{job:{id:s.id,status:"in-progress"},by:o});w("run",`round ${v}: ${u.map(s=>s.id).join(", ")} run as one team (${Object.entries(z.budget||{}).map(([s,l])=>`${s} ${l}`).join(", ")})`);let m=null;try{m=await f({team:z,request:[e.goal,e.doneWhen?`Done when: ${e.doneWhen}`:""].filter(Boolean).join(`
17
+ `),projectId:e.id,round:v,jobs:u})}catch(s){w("run",`round ${v} failed to run: ${x(s?.message||s,300)}`)}if(m?.runId){T.push({runId:m.runId,round:v,status:m.status});for(const s of u)g("run.linked",{runId:m.runId,jobId:s.id});m.usage?.spent&&g("run.spent",{runId:m.runId,spent:{...m.usage.spent,ms:Number(m.usage.spent.ms)||Math.max(0,(m.endedAt||c())-(m.startedAt||c()))}})}for(const s of we(m,u,{now:c()}))g("job.updated",{job:{...s,...m?.runId?{runId:m.runId}:{}},by:o});if(m&&["stopped","waiting"].includes(m.status))return j(m.status,{why:`round ${v} ${m.status}`});if(_())return j("stopped");const ne=await W(ce(e,n,{round:v}),U),k=be(ne||{},e,{existing:n.jobs,now:c()});if(k?.report?g("project.report",{text:k.report,by:o}):g("project.report",{text:n.jobs.map(s=>`## ${s.title} (${s.status})
18
+ ${s.result?.text||""}`).join(`
19
+
20
+ `),by:"concat"}),w("review",k?`${k.done?"done-when holds":"done-when does not hold yet"}${k.why?` — ${k.why}`:""}${k.followUps.length?`; ${k.followUps.length} follow-up${k.followUps.length===1?"":"s"}`:""}`:"the executive did not answer the review; the report is the results as they are"),k?.done){const l=C(O,"writeBack").allowed?{text:"Close as done",by:"gate"}:await P({type:"permission",text:`Done-when holds${k.why?` ${k.why}`:""}. Close the project as done?`,options:["Close as done","Keep it open"]});return E(l)?(g("project.status",{status:"done",by:l.by||"person"}),j("done")):j("open",{why:"done-when holds; left open by the stakeholder"})}if(!k?.followUps?.length){if(V(n.jobs).length)continue;return j("open",{why:k?"nothing more would help":"no review"})}if(!await q(k.followUps,{what:`After round ${v}`}))return j("open",{why:"follow-ups not posted"})}}export{K as EXECUTIVE,D as MAX_JOBS_PER_ROUND,he as MAX_ROUNDS,N as PROJECT_JOBS_SCHEMA,U as PROJECT_REVIEW_SCHEMA,Se as PROJECT_RUN_STATUSES,M as ProjectRunError,we as jobResults,fe as jobsPrompt,te as parseJobs,be as parseReview,ce as reviewPrompt,oe as roundBudget,ye as roundJobs,Re as runProject,ge as teamForRound};