agentlas 1.0.22 → 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 +22 -0
- package/README.md +9 -26
- package/engine/agentlas-banner.cjs +0 -1
- package/engine/agentlas-capabilities.cjs +8 -71
- package/engine/agentlas-core-harness.cjs +18 -11
- package/engine/agentlas-i18n.cjs +21 -21
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-native-host.cjs +4 -31
- package/engine/agentlas-ui.cjs +13 -1
- package/engine/agentlas-workload-routing.cjs +11 -24
- package/engine/agentlas.cjs +3 -13
- package/engine/agents/router.cjs +0 -1
- package/engine/architecture.data.json +5 -5
- package/engine/bootstrap-schema.sql +3 -3
- package/engine/commands/doctor.cjs +3 -3
- package/engine/commands/firm.cjs +3 -1
- package/engine/commands/help.cjs +4 -7
- package/engine/commands/index.cjs +1 -6
- package/engine/commands/list.cjs +8 -2
- package/engine/firms/orchestrate.cjs +161 -12
- package/engine/sessions/orchestrator.cjs +4 -21
- package/engine/sessions/session.cjs +66 -5
- package/engine/sessions/sink.cjs +13 -3
- package/engine/sessions/store.cjs +2 -2
- package/engine/ui/palette.cjs +3 -12
- package/engine/ui/renderer.cjs +3 -6
- package/engine/ui/repl.cjs +41 -93
- package/engine/workforce/deps.cjs +9 -6
- package/package.json +4 -3
- package/engine/agentlas-doctor.cjs +0 -224
- package/engine/commands/chat.cjs +0 -12
- package/engine/commands/chats.cjs +0 -33
- package/engine/commands/open.cjs +0 -52
package/engine/agentlas.cjs
CHANGED
|
@@ -136,11 +136,8 @@ function main() {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
if (code === undefined) {
|
|
139
|
-
// 알 수 없는
|
|
140
|
-
//
|
|
141
|
-
const { findAgent } = require("./agents/registry.cjs");
|
|
142
|
-
let agent = null;
|
|
143
|
-
try { agent = findAgent(ctx.db(), normalized[0]); } catch { /* db unavailable → run이 진단 */ }
|
|
139
|
+
// 알 수 없는 토큰은 프로젝트 작업으로 실행한다. 에이전트 이름 하나가
|
|
140
|
+
// 전역 대화 소유권으로 바뀌는 암묵 경로는 없다.
|
|
144
141
|
/*
|
|
145
142
|
* 오타 가드: 인자가 "공백 없는 한 단어" 하나뿐이고 명령도 에이전트도 아니면
|
|
146
143
|
* 그건 작업 지시가 아니라 명령 오타일 가능성이 압도적이다. 그대로 프롬프트로
|
|
@@ -148,7 +145,7 @@ function main() {
|
|
|
148
145
|
* 에서 ls -la 실행 실증). 가장 가까운 명령을 제안하고 정직하게 멈춘다.
|
|
149
146
|
* 진짜 한 단어 작업은 따옴표+run -p 로 그대로 실행된다.
|
|
150
147
|
*/
|
|
151
|
-
if (
|
|
148
|
+
if (normalized.length === 1 && /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(normalized[0])) {
|
|
152
149
|
const token = normalized[0];
|
|
153
150
|
const names = Object.keys(commands.COMMANDS)
|
|
154
151
|
.concat(Object.keys(commands.COMMAND_ALIASES || {}))
|
|
@@ -163,13 +160,6 @@ function main() {
|
|
|
163
160
|
: `See: agentlas help · to run it as a task: agentlas run -p "${token}"`);
|
|
164
161
|
process.exit(1);
|
|
165
162
|
}
|
|
166
|
-
if (agent && normalized.length === 1) {
|
|
167
|
-
const { startRepl } = require("./ui/repl.cjs");
|
|
168
|
-
return startRepl(ctx, { agent: agent.slug }).then(
|
|
169
|
-
(replCode) => process.exit(replCode || 0),
|
|
170
|
-
(e) => { ctx.err(String((e && e.message) || e)); process.exit(1); },
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
163
|
code = commands.COMMANDS.run().run(ctx, normalized);
|
|
174
164
|
}
|
|
175
165
|
|
package/engine/agents/router.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
3
|
-
"emitterBlock": "## Memory (Agentlas curated memory)\n\
|
|
2
|
+
"version": "1.6.0",
|
|
3
|
+
"emitterBlock": "## Memory (Agentlas curated memory)\n\nAt the end of EVERY completed normal reply, emit exactly one hidden Memory Events\nenvelope. The runtime removes it before display. This envelope is the per-turn receipt:\nalways include a compact safe turn_summary, and use an empty candidates array when\nnothing durable was learned. Do not skip the envelope.\n\nRules:\n- Never include secrets, credentials, API keys, raw logs, or full transcripts.\n- Real credential values may live only in local project .env/.env.local,\n ignored signing/ or credentials/ files, or a local keychain/vault. Memory\n Events may mention env names and local relative paths only.\n- For deploy, release, store, billing, auth, API, or cloud work, first read the\n project's .agentlas/local-credentials.map.json and the top\n \"Local Credential Index\" section of .agentlas/project-soul-memory.md\n before saying a credential is missing.\n- One candidate per durable item. Keep \"content\" to one or two sentences.\n- \"memory_kind\": fact | decision | preference | risk | procedure | hypothesis | evidence | deprecation | conflict\n- \"suggested_scope\": user_identity | team_memory | project (this folder) | agent_repo | session (temporary) | discard\n- Use user_identity for a stable operator preference or personal fact (their name, role, language, tone,\n how they want you to behave) — these must outlive any one project. The curator only files user_identity\n when you label it so with \"confidence\": \"high\"; it never promotes into that scope, so a preference emitted\n at lower confidence is demoted to a throwaway session note.\n- \"agent_team\" is accepted only as a legacy alias for team_memory.\n- Add \"request_context\" when it improves future recall: user_intent, trigger_terms,\n cwd_at_request, target_project, target_path, cross_context, outcome.\n- Never put the raw user prompt or transcript in request_context.\n- Suggest a scope; the separate Memory Curator decides the final destination.\n- turn_summary is one value-free sentence about the completed outcome. It is not the\n user prompt, a transcript, raw log, secret, or absolute local path.\n\nFormat (always emit, including an empty candidates array):\n\n## Memory Events\n```json\n{\n \"schema_version\": \"agentlas.memory-ticket.v1\",\n \"turn_summary\": \"Completed outcome in one safe sentence.\",\n \"candidates\": [\n {\n \"memory_kind\": \"decision\",\n \"content\": \"...\",\n \"suggested_scope\": \"project\",\n \"confidence\": \"high\",\n \"sensitivity\": \"internal\",\n \"evidence_refs\": [],\n \"request_context\": {\n \"user_intent\": \"...\",\n \"trigger_terms\": [\"...\"],\n \"cwd_at_request\": null,\n \"target_project\": null,\n \"target_path\": null,\n \"cross_context\": false,\n \"outcome\": \"...\"\n }\n }\n ]\n}\n```",
|
|
4
4
|
"eventsHeading": "## Memory Events",
|
|
5
5
|
"memoryDir": ".agentlas",
|
|
6
6
|
"soulFile": "project-soul-memory.md",
|
|
@@ -72,12 +72,12 @@
|
|
|
72
72
|
"slug": "agentlas-orchestrator",
|
|
73
73
|
"name": "Agentlas 오케스트레이터",
|
|
74
74
|
"nameEn": "Agentlas Orchestrator",
|
|
75
|
-
"tagline": "
|
|
76
|
-
"taglineEn": "
|
|
75
|
+
"tagline": "프로젝트 컨트롤러를 지원하는 작업 단위 오케스트레이션",
|
|
76
|
+
"taglineEn": "Task-scoped orchestration under a project controller",
|
|
77
77
|
"role": "orchestrator",
|
|
78
78
|
"visibility": "background",
|
|
79
79
|
"tone": "blue",
|
|
80
|
-
"systemPrompt": "# Agentlas Orchestrator (built-in)\n\nYou are
|
|
80
|
+
"systemPrompt": "# Agentlas Orchestrator (built-in)\n\nYou are a project-bound orchestration capability, never a global chat owner.\n\nThe host supplies one connected Work project, its system prompt, ordered agent pool,\nmemory and the current WorkOrder. The first agent in that ordered pool owns the task.\nYou may act only when you are that first agent or when the owning controller delegates\nthis bounded WorkOrder to you.\n\n## Ownership and staffing\n- Preserve the project's ordered Orch/Worker model priorities.\n- Select task-scoped sub-agents from the project's explicit pool by full semantic\n judgment. Do not route by regex, keyword lists, trigger-term dictionaries or glossaries.\n- Every sub-agent remains subordinate to the project controller and exists only for\n its assignment. Never transfer session or project ownership.\n- Pin and validate exact releases before execution. Never silently substitute a\n missing, expired or incompatible agent.\n- An explicit named-agent call affects only that turn.\n\n## Recovery\n- Observe failures as private evidence, decide the safest available recovery with the\n connected model, execute reversible recovery automatically, and verify the outcome.\n- Do not expose raw errors, codes, stack traces, paths or internal component language.\n- Code provides state, evidence and finite capabilities only. You author the concise\n summary, question and action labels for the actual situation.\n- If model judgment is unavailable, remain unresolved. Never fabricate a semantic\n fallback or present a guessed diagnosis as success.\n\n## Completion\nReturn the verified project result and compact evidence. Record durable project memory\nwithout binding that memory to one replaceable agent release."
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"id": "builtin-agentlas-app-builder",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- Agentlas first-run bootstrap schema (project-first Work contract)
|
|
2
|
-
-- Source DB user_version=
|
|
3
|
-
PRAGMA user_version=
|
|
2
|
+
-- Source DB user_version=86. Desktop remains the migration authority.
|
|
3
|
+
PRAGMA user_version=86;
|
|
4
4
|
CREATE TABLE active_runtime (
|
|
5
5
|
id INTEGER PRIMARY KEY CHECK(id = 1),
|
|
6
6
|
kind TEXT NOT NULL
|
|
@@ -35,7 +35,7 @@ CREATE TABLE chats (
|
|
|
35
35
|
agent_id TEXT NOT NULL,
|
|
36
36
|
title TEXT NOT NULL DEFAULT '새 채팅',
|
|
37
37
|
created_at TEXT NOT NULL,
|
|
38
|
-
updated_at TEXT NOT NULL, firm_id TEXT REFERENCES firms(id) ON DELETE SET NULL, archived_at TEXT, working_folder TEXT, kind TEXT NOT NULL DEFAULT 'user', parent_chat_id TEXT, used_at TEXT, continuous_mode INTEGER NOT NULL DEFAULT 0, swarm_mode INTEGER NOT NULL DEFAULT 0, last_viewed_at TEXT,
|
|
38
|
+
updated_at TEXT NOT NULL, firm_id TEXT REFERENCES firms(id) ON DELETE SET NULL, archived_at TEXT, working_folder TEXT, kind TEXT NOT NULL DEFAULT 'user', parent_chat_id TEXT, used_at TEXT, continuous_mode INTEGER NOT NULL DEFAULT 0, swarm_mode INTEGER NOT NULL DEFAULT 0, last_viewed_at TEXT, origin_surface TEXT NOT NULL DEFAULT 'work', runtime_selection_json TEXT,
|
|
39
39
|
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE SET NULL,
|
|
40
40
|
FOREIGN KEY(agent_id) REFERENCES installed_agents(id) ON DELETE CASCADE
|
|
41
41
|
);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
3
|
* doctor — 런타임·데이터·자격증명 건강 점검.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 이 명령은 사용자가 명시적으로 요청한 현재 상태 관측만 한다.
|
|
5
|
+
* 자동 복구는 프로젝트 컨트롤러와 저장된 모델 우선순위가 맡으며, 이 명령은
|
|
6
|
+
* 오류 문자열을 분류하거나 설정을 자동 변경하지 않는다.
|
|
7
7
|
*/
|
|
8
8
|
const fs = require("node:fs");
|
|
9
9
|
const path = require("node:path");
|
package/engine/commands/firm.cjs
CHANGED
|
@@ -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
|
},
|
package/engine/commands/help.cjs
CHANGED
|
@@ -4,14 +4,11 @@
|
|
|
4
4
|
const HELP = `agentlas — the operating system for agents, in your terminal
|
|
5
5
|
|
|
6
6
|
agentlas open the terminal (REPL)
|
|
7
|
-
agentlas <
|
|
8
|
-
agentlas "<task>" auto-route to the best agent and run once
|
|
7
|
+
agentlas "<task>" run once with this project's controller
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
run [agent] [prompt] one-shot (-p · --runtime · --model · --effort · --permission)
|
|
9
|
+
PROJECT WORK
|
|
10
|
+
run [agent] [prompt] project-first one-shot; exact agent is an explicit advanced override
|
|
13
11
|
firm <firm> [task] delegate to a CEO (--runtime · --model · --effort)
|
|
14
|
-
chats [n] · open <id> recent conversations · resume one
|
|
15
12
|
|
|
16
13
|
AGENTS & HUB
|
|
17
14
|
search "<what you need>" discover agents in the Hub
|
|
@@ -52,7 +49,7 @@ ACCOUNT & OPS
|
|
|
52
49
|
|
|
53
50
|
IN-REPL (agentlas → interactive, Orca multi-session)
|
|
54
51
|
/spawn <agent> [task] · /sessions · /tree · /s <n> · /steer <n> <msg> ·
|
|
55
|
-
/kill <n> · /rm <n> · /broadcast <msg> · /
|
|
52
|
+
/kill <n> · /rm <n> · /broadcast <msg> · /runtime · /model · /effort · /permission
|
|
56
53
|
typing during a running turn queues steering; ctrl-c interrupts the turn
|
|
57
54
|
|
|
58
55
|
Options: -p|--print · --runtime claude-code|codex|gemini · --model <exact-id> ·
|
|
@@ -12,7 +12,6 @@ const path = require("node:path");
|
|
|
12
12
|
const COMMANDS = {
|
|
13
13
|
version: () => require("./version.cjs"),
|
|
14
14
|
list: () => require("./list.cjs"),
|
|
15
|
-
chats: () => require("./chats.cjs"),
|
|
16
15
|
doctor: () => require("./doctor.cjs"),
|
|
17
16
|
mcp: () => require("./mcp.cjs"),
|
|
18
17
|
help: () => require("./help.cjs"),
|
|
@@ -32,10 +31,8 @@ const COMMANDS = {
|
|
|
32
31
|
install: () => require("./install.cjs"),
|
|
33
32
|
plugin: () => require("./plugin.cjs"),
|
|
34
33
|
plugins: () => require("./plugin.cjs"),
|
|
35
|
-
open: () => require("./open.cjs"),
|
|
36
34
|
automation: () => require("./automation.cjs"),
|
|
37
35
|
native: () => require("./native.cjs"),
|
|
38
|
-
chat: () => require("./chat.cjs"),
|
|
39
36
|
multimodal: () => require("./multimodal.cjs"),
|
|
40
37
|
workforce: () => require("./workforce.cjs"),
|
|
41
38
|
network: () => require("./workforce.cjs"),
|
|
@@ -94,11 +91,9 @@ const DESKTOP_ONLY_SURFACES = {
|
|
|
94
91
|
trex: "T-rex slide studio is Desktop-only.",
|
|
95
92
|
slides: "T-rex slide studio is Desktop-only.",
|
|
96
93
|
prompts: "Prompt Store is Desktop-only.",
|
|
97
|
-
dashboard: "Dashboard is Desktop-only — use: agentlas doctor · usage · list
|
|
94
|
+
dashboard: "Dashboard is Desktop-only — use: agentlas doctor · usage · list",
|
|
98
95
|
marketplace: "Marketplace browsing is Desktop-only — use: agentlas search \"<what you need>\"",
|
|
99
96
|
library: "Library is Desktop-only — use: agentlas list · env · mcp",
|
|
100
|
-
groups: "Agent groups (조합) are Desktop-only.",
|
|
101
|
-
"agent-groups": "Agent groups (조합) are Desktop-only.",
|
|
102
97
|
settings: "Settings UI is Desktop-only — use: agentlas setup · env · creds · multimodal · doctor",
|
|
103
98
|
apps: "Apps surface is Desktop-only.",
|
|
104
99
|
quests: "Quests are Desktop-only.",
|
package/engine/commands/list.cjs
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
78
|
+
cleaned = fences.parseReplyFences(raw).cleanText;
|
|
78
79
|
}
|
|
79
80
|
} catch { /* fences 미존재/파서 실패 — 원문 보존 */ }
|
|
80
|
-
|
|
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.
|
|
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 —
|
|
271
|
-
//
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
318
|
-
|
|
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
|
|
474
|
+
ok: finalOk && latestResultsAllOk(divisionResults),
|
|
326
475
|
text: finalText,
|
|
327
476
|
chatId: ceoSession.chatId,
|
|
328
477
|
plan: { text: cleanedText, delegations },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* sessions/orchestrator —
|
|
3
|
+
* sessions/orchestrator — 프로젝트 Work의 컨트롤러/서브에이전트 실행 트리.
|
|
4
4
|
*
|
|
5
5
|
* 세션 번호는 s1, s2, … 로 붙는다(사람이 한 키로 지목할 수 있는 안정 번호).
|
|
6
6
|
* 활성 세션 하나만 터미널에 스트리밍되고, 나머지는 백그라운드에서 이벤트를
|
|
@@ -67,14 +67,12 @@ class Orchestrator extends EventEmitter {
|
|
|
67
67
|
|
|
68
68
|
session.on("event", (ev) => {
|
|
69
69
|
this.emit("session-event", { key, session, ev });
|
|
70
|
-
if (ev.type === "turn-end" && this.activeKey !== key) {
|
|
70
|
+
if (ev.type === "turn-end" && ev.ok && this.activeKey !== key) {
|
|
71
71
|
this.emit("notice", {
|
|
72
72
|
key,
|
|
73
73
|
session,
|
|
74
|
-
text:
|
|
75
|
-
|
|
76
|
-
: `${key} ${session.agent.slug}: ${ev.error || "failed"}`,
|
|
77
|
-
ok: !!ev.ok,
|
|
74
|
+
text: `${key} ${session.agent.slug}: done`,
|
|
75
|
+
ok: true,
|
|
78
76
|
});
|
|
79
77
|
}
|
|
80
78
|
});
|
|
@@ -157,21 +155,6 @@ class Orchestrator extends EventEmitter {
|
|
|
157
155
|
* 그래서 실패는 세션 단위로 모으고, 실제 전달된 목록은 무슨 일이 있어도 반환한다.
|
|
158
156
|
* (sendTo/spawn의 "상한 초과는 정직한 거부" 계약 자체는 그대로 둔다.)
|
|
159
157
|
*/
|
|
160
|
-
broadcast(prompt) {
|
|
161
|
-
const sent = [];
|
|
162
|
-
const skipped = [];
|
|
163
|
-
for (const [key, session] of this.sessions) {
|
|
164
|
-
if (session.status === "killed") continue;
|
|
165
|
-
try {
|
|
166
|
-
this.sendTo(key, prompt);
|
|
167
|
-
sent.push(key);
|
|
168
|
-
} catch (e) {
|
|
169
|
-
skipped.push({ key, error: String((e && e.message) || e) });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
return { sent, skipped };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
158
|
/** 세션 표: [{key, active, agent, status, elapsed, lastLine, parentKey, depth}] */
|
|
176
159
|
list() {
|
|
177
160
|
const rows = [];
|