agentlas 1.0.23 → 1.0.24

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.24 — 2026-08-02
4
+
5
+ - **Firm runs now finish the real dependency chain.** Independent production
6
+ roles still run in parallel, integration waits for their files, and release
7
+ verification runs only after the integrated surface exists.
8
+ - **A verification failure triggers one bounded repair and re-check.** Terminal
9
+ no longer ends with a truthful failure report while leaving a fixable product
10
+ defect unresolved, and the newest result for each role determines completion.
11
+ - Firm names/slugs are directly callable from `agentlas list`, and final user
12
+ output removes orchestration fences, internal skill reports, and verification
13
+ control tags.
14
+
3
15
  ## 1.0.23 — 2026-08-01
4
16
 
5
17
  - **Semantic routing stays with the connected model.** Image-capability and
@@ -12,7 +12,7 @@ const { rowToAgent } = require("../agents/registry.cjs");
12
12
  function findFirm(db, token) {
13
13
  const q = String(token || "").trim().toLowerCase();
14
14
  if (!q) return null;
15
- return db.prepare("SELECT * FROM firms WHERE lower(slug)=? OR lower(name)=?").get(q, q)
15
+ return db.prepare("SELECT * FROM firms WHERE lower(id)=? OR lower(slug)=? OR lower(name)=?").get(q, q, q)
16
16
  || db.prepare("SELECT * FROM firms WHERE lower(slug) LIKE ? ORDER BY slug LIMIT 1").get(`%${q}%`)
17
17
  || null;
18
18
  }
@@ -158,6 +158,8 @@ async function run(ctx, args) {
158
158
  onEvent: (ev) => {
159
159
  if (ev.phase === "plan") ctx.err(dim(ko ? `${firm.name} · CEO가 작업을 분배하는 중…` : `${firm.name} · CEO is planning the work…`));
160
160
  else if (ev.phase === "delegate") ctx.err(dim((ko ? "위임 → " : "delegating → ") + ev.targets.map((t) => t.name || t.role).join(", ")));
161
+ else if (ev.phase === "repair") ctx.err(dim(ko ? "검증 결함 확인 → 수정 후 재검증" : "Verification blocker found → repairing before re-check"));
162
+ else if (ev.phase === "verify") ctx.err(dim(ko ? "구현 결과 준비 완료 → 독립 검증 시작" : "Implementation ready → starting independent verification"));
161
163
  else if (ev.phase === "division-done") ctx.err(dim(` ${ev.role}: ${ev.ok ? "ok" : "failed"}`));
162
164
  else if (ev.phase === "synthesize") ctx.err(dim(ko ? "팀 결과를 종합하는 중…" : "Synthesizing team results…"));
163
165
  },
@@ -31,7 +31,7 @@ function run(ctx) {
31
31
  slug: a.slug, name: a.name, name_en: a.nameEn, tagline: a.tagline, tagline_en: a.taglineEn, builtin: a.builtin,
32
32
  }));
33
33
  const firms = ctx.tableExists(db, "firms")
34
- ? db.prepare("SELECT id, name FROM firms ORDER BY name").all()
34
+ ? db.prepare("SELECT id, slug, name FROM firms ORDER BY name").all()
35
35
  : [];
36
36
 
37
37
  const en = ctx.lang === "en";
@@ -47,7 +47,13 @@ function run(ctx) {
47
47
  if (firms.length) {
48
48
  ctx.out("");
49
49
  ctx.out(ctx.ui.bold(en ? "Companies" : "회사"));
50
- for (const f of firms) ctx.out(` ${ctx.ui.accent(String(f.id).padEnd(24))} ${f.name}`);
50
+ for (const f of firms) {
51
+ const callable = String(f.slug || f.id);
52
+ ctx.out(` ${ctx.ui.accent(callable.padEnd(28))} ${f.name}`);
53
+ }
54
+ ctx.out(ctx.ui.dim(en
55
+ ? " Run one with: agentlas firm <company-key> \"<task>\""
56
+ : " 실행: agentlas firm <회사 키> \"<작업>\""));
51
57
  }
52
58
 
53
59
  const active = activeRuntimeRow(db);
@@ -71,13 +71,21 @@ function loadDelegateParser() {
71
71
  /** 표시/전달용 텍스트에서 제어 펜스를 제거한다(파싱만 — 부작용 없음). 실패 시 원문. */
72
72
  function cleanFenceText(text) {
73
73
  const raw = String(text || "");
74
+ let cleaned;
74
75
  try {
75
76
  const fences = require("../sessions/fences.cjs");
76
77
  if (fences && typeof fences.parseReplyFences === "function") {
77
- return fences.parseReplyFences(raw).cleanText;
78
+ cleaned = fences.parseReplyFences(raw).cleanText;
78
79
  }
79
80
  } catch { /* fences 미존재/파서 실패 — 원문 보존 */ }
80
- return parseDelegationsLocal(raw).cleanedText;
81
+ if (cleaned == null) cleaned = parseDelegationsLocal(raw).cleanedText;
82
+ return cleaned
83
+ .replace(/<!--\s*[\s\S]*?## Memory Events[\s\S]*?-->/gi, "")
84
+ .replace(/^\s*(?:사용 스킬|Skills used)\s*:[^\n.!?]*[.!?]?\s*(?:(?:이유|Reason)\s*:[^.!?]*[.!?]\s*)?/i, "")
85
+ .replace(/^\s*I(?:'|’)m using (?:the )?`?[^`.\n]+`? skill because [^.]*\.\s*/i, "")
86
+ .replace(/^\s*Execution mode:\s*`?appbridge-ceo-orchestrator`?[^\n]*\n?/gim, "")
87
+ .replace(/<verification_verdict>\s*(?:PASS|FAIL)\s*<\/verification_verdict>/gi, "")
88
+ .trim();
81
89
  }
82
90
 
83
91
  /** 리더(CEO) 시스템 프롬프트에 주입할 위임 가이드 (데스크탑 buildDelegateProtocol 동형 축약). */
@@ -91,6 +99,9 @@ function buildDelegateProtocol(reports) {
91
99
  "You lead a team. For THIS task, engage ONLY the direct reports actually needed —",
92
100
  "never all of them. Give each a focused brief (goal + specifics). If none are needed,",
93
101
  "do the work yourself and emit no Delegate block.",
102
+ "This is the only delegation planning round. Include every role required to finish the request now,",
103
+ "including downstream independent QA or verification roles. State dependencies in their briefs;",
104
+ "the host will delay verification until production results exist. Never defer a needed role to synthesis.",
94
105
  "",
95
106
  "Your direct reports:",
96
107
  list,
@@ -102,7 +113,7 @@ function buildDelegateProtocol(reports) {
102
113
  '{ "delegations": [ { "target": "<report role or name above>", "brief": "<what they should do>" } ] }',
103
114
  "```",
104
115
  "",
105
- "After delegating, STOP — their results come back to you to synthesize. Don't do their work yourself.",
116
+ "After delegating, STOP — their results come back to you to synthesize. Synthesis is final and cannot start new work.",
106
117
  ].join("\n");
107
118
  }
108
119
 
@@ -196,6 +207,58 @@ async function parallelCap(items, cap, fn) {
196
207
  return out;
197
208
  }
198
209
 
210
+ function isVerificationDivision(node) {
211
+ const label = `${node && node.role || ""} ${node && node.name || ""} ${node && node.key || ""}`
212
+ .toLowerCase()
213
+ .replace(/[_-]+/g, " ");
214
+ return /\b(?:eval|qa|quality|test|verification|verifier)\b|policy\s+gate/.test(label);
215
+ }
216
+
217
+ function isIntegrationDivision(item, siblingProductionCount) {
218
+ if (!item || siblingProductionCount < 2 || isVerificationDivision(item.node)) return false;
219
+ const label = `${item.node && item.node.role || ""} ${item.node && item.node.name || ""}`
220
+ .toLowerCase()
221
+ .replace(/[_-]+/g, " ");
222
+ const brief = String(item.brief || "").toLowerCase();
223
+ if (/\bdesign\b/.test(label)) return false;
224
+ return /\b(?:web|frontend|integration|integrator|release)\b/.test(label)
225
+ || /\b(?:integrat(?:e|ion)|wire|combine|merge)\b/.test(brief)
226
+ || /\bafter\b[\s\S]{0,80}\b(?:game|design|production|upstream|implementation)\b/.test(brief)
227
+ || /\b(?:once|when)\b[\s\S]{0,80}\b(?:complete|ready|finish)/.test(brief);
228
+ }
229
+
230
+ function stageTargets(targets) {
231
+ const nonVerification = targets.filter((m) => !isVerificationDivision(m.node));
232
+ const integration = nonVerification.filter((m) => isIntegrationDivision(m, nonVerification.length));
233
+ const integrationKeys = new Set(integration.map((m) => m.node.key));
234
+ return {
235
+ production: nonVerification.filter((m) => !integrationKeys.has(m.node.key)),
236
+ integration,
237
+ verification: targets.filter((m) => isVerificationDivision(m.node)),
238
+ };
239
+ }
240
+
241
+ function resultStatusContext(results) {
242
+ return results.length
243
+ ? results.map((r) => `- ${r.name}: ${r.ok ? "completed" : "failed"}`).join("\n")
244
+ : "- No upstream production slot was selected; inspect the current folder honestly.";
245
+ }
246
+
247
+ function verificationResultOk(text, sessionOk) {
248
+ if (!sessionOk) return false;
249
+ const source = String(text || "").trim();
250
+ const explicit = source.match(/<verification_verdict>\s*(PASS|FAIL)\s*<\/verification_verdict>/i);
251
+ if (explicit) return explicit[1].toUpperCase() === "PASS";
252
+ const opening = source.slice(0, 900);
253
+ return !/(?:\bverdict\s*:\s*fail\b|\brelease[- ]blocking\b|\bnot complete\b|\bcannot truthfully\b|\bno[- ]go\b|\bblocking defect\b)/i.test(opening);
254
+ }
255
+
256
+ function latestResultsAllOk(results) {
257
+ const latest = new Map();
258
+ for (const result of results) latest.set(result.key || `${result.role}:${result.name}`, result.ok);
259
+ return [...latest.values()].every(Boolean);
260
+ }
261
+
199
262
  function turnText(res) {
200
263
  return ((res && (res.finalText || res.text)) || "").trim();
201
264
  }
@@ -267,10 +330,13 @@ async function runFirmTurn(p) {
267
330
  };
268
331
  }
269
332
 
270
- // 2) DELEGATE — 매칭된 본부만 병렬 실행. 본부 세션은 CEO 세션의 자식으로 스폰되어
271
- // kind='division' + parent_chat_id(CEO 챗)로 영속된다. 본부의 실패는 격리한다.
333
+ // 2) DELEGATE — 구현/디자인은 병렬로 실행하되 독립 검증 본부는 결과가 실제
334
+ // 작업 폴더에 반영된 실행한다. QA를 구현과 동시에 시작하면 "코드 없음"을 정상
335
+ // 결과로 반환해 CEO가 뒤늦게 충돌을 수습하게 된다. 병렬성은 의존성이 없는 슬롯에만
336
+ // 적용하고, 검증 슬롯은 명시적인 2단계 WorkOrder로 보존한다.
272
337
  onEvent({ phase: "delegate", targets: matched.map((m) => ({ role: m.node.role, name: m.node.name, brief: m.brief })) });
273
- const divisionResults = await parallelCap(matched, maxParallel(), async (m) => {
338
+ const initialStages = stageTargets(matched);
339
+ const runTargets = async (targets, stageContext, stageKind) => parallelCap(targets, maxParallel(), async (m) => {
274
340
  const divisionRuntime = typeof p.resolveWorkerRuntime === "function"
275
341
  ? p.resolveWorkerRuntime(m.node)
276
342
  : workerRuntime;
@@ -293,17 +359,61 @@ async function runFirmTurn(p) {
293
359
  let text = "";
294
360
  let ok = false;
295
361
  try {
296
- const res = await session.send(m.brief || task);
362
+ const prompt = stageKind === "verification"
363
+ ? `${m.brief || task}\n\n[Independent verification stage]\nAll upstream production and integration WorkOrders have finished. Inspect and exercise the current project folder as it exists now. Do not rely on an earlier empty-workspace observation.\n${stageContext}\n\nEnd the response with exactly <verification_verdict>PASS</verification_verdict> only when every requested acceptance condition passes after fixes. Otherwise end with <verification_verdict>FAIL</verification_verdict> and identify the remaining blocker.`
364
+ : stageKind === "integration"
365
+ ? `${m.brief || task}\n\n[Integration stage]\nThe upstream production WorkOrders have finished. Inspect their actual files in the current project, integrate every relevant implementation and design deliverable into the runnable product, then verify the integrated launch surface before returning. Do not report a missing or late upstream package without re-reading the current folder.\n${stageContext}`
366
+ : stageKind === "repair"
367
+ ? `${m.brief || task}\n\n[Release-blocking repair stage]\nIndependent verification found the following failures in the current integrated product. Inspect the evidence and current files, repair the actual shipped experience, and rerun the relevant checks before returning. Do not merely describe the fix.\n${stageContext}`
368
+ : (m.brief || task);
369
+ const res = await session.send(prompt);
297
370
  text = cleanFenceText(turnText(res));
298
- ok = session.status === "done";
371
+ ok = stageKind === "verification"
372
+ ? verificationResultOk(text, session.status === "done")
373
+ : session.status === "done";
299
374
  if (!ok && !text) text = session.lastError || "no response";
300
375
  } catch (e) {
301
376
  text = (e && e.message) || String(e);
302
377
  ok = false;
303
378
  }
304
379
  onEvent({ phase: "division-done", role: m.node.role, ok });
305
- return { role: m.node.role, name: m.node.name, ok, text, chatId: session.chatId };
380
+ return { key: m.node.key, role: m.node.role, name: m.node.name, ok, text, chatId: session.chatId };
306
381
  });
382
+ const productionResults = await runTargets(initialStages.production, "", "production");
383
+ let integrationResults = [];
384
+ if (initialStages.integration.length) {
385
+ onEvent({ phase: "integrate", targets: initialStages.integration.map((m) => ({ role: m.node.role, name: m.node.name })) });
386
+ integrationResults = await runTargets(initialStages.integration, resultStatusContext(productionResults), "integration");
387
+ }
388
+ let verificationResults = [];
389
+ if (initialStages.verification.length) {
390
+ const upstreamResults = [...productionResults, ...integrationResults];
391
+ verificationResults = await runTargets(initialStages.verification, resultStatusContext(upstreamResults), "verification");
392
+ }
393
+ const divisionResults = [...productionResults, ...integrationResults, ...verificationResults];
394
+
395
+ // Verification is a release gate, not a terminal report. When it finds a
396
+ // blocker, run one bounded repair cycle with the implementation/integration
397
+ // slots that produced the build, then independently verify the repaired
398
+ // product again. This closes the common "QA says FAIL and the command ends"
399
+ // gap while keeping retries finite.
400
+ if (initialStages.verification.length && verificationResults.some((result) => !result.ok)) {
401
+ const repairContext = verificationResults
402
+ .filter((result) => !result.ok)
403
+ .map((result) => `## ${result.name} (${result.role})\n${result.text}`)
404
+ .join("\n\n");
405
+ const repairTargets = [...initialStages.production, ...initialStages.integration];
406
+ if (repairTargets.length) {
407
+ onEvent({ phase: "repair", targets: repairTargets.map((m) => ({ role: m.node.role, name: m.node.name })) });
408
+ const repairResults = await runTargets(repairTargets, repairContext, "repair");
409
+ divisionResults.push(...repairResults);
410
+ const recheckContext = resultStatusContext([...productionResults, ...integrationResults, ...repairResults]);
411
+ const recheckResults = await runTargets(initialStages.verification, recheckContext, "verification");
412
+ divisionResults.push(...recheckResults);
413
+ }
414
+ }
415
+ const usedDivisionKeys = new Set(matched.map((m) => m.node.key));
416
+ const divisionAttempts = new Map(matched.map((m) => [m.node.key, 1]));
307
417
 
308
418
  // 3) SYNTHESIZE — CEO 세션의 두 번째 턴. status:failed 표기로 오류 문자열이 산출물로
309
419
  // 오독되는 것을 막는다(데스크탑 CONFLICT_SYNTHESIS_GUIDANCE 계약).
@@ -314,15 +424,54 @@ async function runFirmTurn(p) {
314
424
  divisionResults
315
425
  .map((r) => `## ${r.name} (${r.role})\nstatus: ${r.ok ? "ok" : "failed"}\n${r.text}`)
316
426
  .join("\n\n");
317
- const finalRes = await ceoSession.send(synthPrompt);
318
- const finalText = cleanFenceText(turnText(finalRes));
427
+ let finalRes = await ceoSession.send(synthPrompt);
428
+ let finalRaw = turnText(finalRes);
429
+
430
+ // A controller may discover the next required role only after reading the
431
+ // first results (for example PM -> Game/Design -> Eval). Execute bounded,
432
+ // previously-unused follow-up delegations instead of printing "starting"
433
+ // prose and ending the command without doing the work.
434
+ for (let round = 0; round < divisions.length; round += 1) {
435
+ const followupParsed = parseDelegations(finalRaw);
436
+ const hasFailedResult = !latestResultsAllOk(divisionResults);
437
+ const followup = matchTargets(followupParsed.delegations, divisions)
438
+ .filter((m) => !usedDivisionKeys.has(m.node.key) || (hasFailedResult && (divisionAttempts.get(m.node.key) || 0) < 2));
439
+ if (!followup.length) break;
440
+ for (const item of followup) {
441
+ usedDivisionKeys.add(item.node.key);
442
+ divisionAttempts.set(item.node.key, (divisionAttempts.get(item.node.key) || 0) + 1);
443
+ }
444
+ onEvent({ phase: "delegate", targets: followup.map((m) => ({ role: m.node.role, name: m.node.name, brief: m.brief })) });
445
+ const followupStages = stageTargets(followup);
446
+ const followupProductionResults = await runTargets(followupStages.production, "", "production");
447
+ let followupIntegrationResults = [];
448
+ if (followupStages.integration.length) {
449
+ const upstream = [...divisionResults, ...followupProductionResults];
450
+ onEvent({ phase: "integrate", targets: followupStages.integration.map((m) => ({ role: m.node.role, name: m.node.name })) });
451
+ followupIntegrationResults = await runTargets(followupStages.integration, resultStatusContext(upstream), "integration");
452
+ }
453
+ let followupVerificationResults = [];
454
+ if (followupStages.verification.length) {
455
+ const upstream = [...divisionResults, ...followupProductionResults, ...followupIntegrationResults];
456
+ followupVerificationResults = await runTargets(followupStages.verification, resultStatusContext(upstream), "verification");
457
+ }
458
+ divisionResults.push(...followupProductionResults, ...followupIntegrationResults, ...followupVerificationResults);
459
+ onEvent({ phase: "synthesize" });
460
+ finalRes = await ceoSession.send(
461
+ `${task}\n\n[Updated results from your team — continue orchestration only if a still-unused required role is missing; otherwise return the final user result.]\n` +
462
+ `${CONFLICT_SYNTHESIS_GUIDANCE}\n\n` +
463
+ divisionResults.map((r) => `## ${r.name} (${r.role})\nstatus: ${r.ok ? "ok" : "failed"}\n${r.text}`).join("\n\n"),
464
+ );
465
+ finalRaw = turnText(finalRes);
466
+ }
467
+ const finalText = cleanFenceText(finalRaw);
319
468
  const finalOk = ceoSession.status === "done";
320
469
  onEvent({ phase: "final", delegated: true, ok: finalOk });
321
470
 
322
471
  // CEO 종합 턴의 성공은 팀의 성공이 아니다 — 자식 결과를 집계해 부분 완료가 성공으로
323
472
  // 둔갑하지 않게 한다(데스크탑 동일 수리).
324
473
  return {
325
- ok: finalOk && divisionResults.every((r) => r.ok),
474
+ ok: finalOk && latestResultsAllOk(divisionResults),
326
475
  text: finalText,
327
476
  chatId: ceoSession.chatId,
328
477
  plan: { text: cleanedText, delegations },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"