agentlas 1.0.12 → 1.0.15
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 +53 -0
- package/README.md +4 -2
- package/bin/agentlas.cjs +17 -3
- package/engine/agentlas-cloud-runtime.cjs +8 -2
- package/engine/agentlas-config.cjs +25 -18
- package/engine/agentlas-core-harness.cjs +14 -1
- package/engine/agentlas-i18n.cjs +2 -0
- package/engine/agentlas-input.cjs +1 -1
- package/engine/agentlas-memory-governance.cjs +10 -0
- package/engine/agentlas-onboard.cjs +22 -5
- package/engine/agentlas-sqlite-policy.cjs +18 -7
- package/engine/agentlas-workforce.cjs +136 -19
- package/engine/agentlas-workload-routing.cjs +32 -4
- package/engine/agentlas.cjs +8 -0
- package/engine/automation/daemon.cjs +67 -30
- package/engine/automation/schedule.cjs +16 -0
- package/engine/automation/store.cjs +70 -12
- package/engine/bootstrap-schema.sql +788 -53
- package/engine/cloud-assets/cas.cjs +7 -2
- package/engine/cloud-assets/commands.cjs +2 -0
- package/engine/cloud-assets/package.cjs +49 -1
- package/engine/cloud-assets/state.cjs +35 -0
- package/engine/commands/automation.cjs +8 -0
- package/engine/commands/chats.cjs +6 -1
- package/engine/commands/firm.cjs +7 -0
- package/engine/commands/help.cjs +23 -2
- package/engine/commands/hep-cloud.cjs +31 -0
- package/engine/commands/hep-hub.cjs +30 -0
- package/engine/commands/hep-local.cjs +32 -0
- package/engine/commands/hep-network.cjs +43 -0
- package/engine/commands/index.cjs +38 -7
- package/engine/commands/open.cjs +5 -1
- package/engine/commands/run.cjs +12 -2
- package/engine/commands/setup.cjs +12 -11
- package/engine/commands/storm.cjs +5 -9
- package/engine/commands/swarm.cjs +4 -4
- package/engine/commands/uninstall.cjs +36 -2
- package/engine/commands/version.cjs +29 -0
- package/engine/commands/workforce.cjs +10 -10
- package/engine/core/schema-ensure.cjs +75 -0
- package/engine/experience/variant.cjs +46 -2
- package/engine/hephaestus/runtime.cjs +7 -0
- package/engine/memory-cli/curate.cjs +3 -7
- package/engine/project/memory-context.cjs +3 -7
- package/engine/project/state.cjs +7 -6
- package/engine/sessions/orchestrator.cjs +41 -3
- package/engine/sessions/prompt.cjs +4 -7
- package/engine/sessions/session.cjs +32 -0
- package/engine/storm/swarm.cjs +12 -0
- package/engine/ui/palette.cjs +14 -2
- package/engine/ui/renderer.cjs +37 -0
- package/engine/ui/repl.cjs +7 -1
- package/engine/workforce/concurrency.cjs +41 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.15 — 2026-07-29
|
|
4
|
+
|
|
5
|
+
- **Agent Cloud saves work from a new machine or fresh clone.** Before writing,
|
|
6
|
+
the terminal resolves the signed-in owner's exact asset revision by
|
|
7
|
+
`slug` and scope, then keeps the conditional-write guard on that revision.
|
|
8
|
+
A missing local receipt no longer turns an owned asset into a false create
|
|
9
|
+
conflict.
|
|
10
|
+
- **A real stale-copy conflict stays fail-closed.** The terminal stops before
|
|
11
|
+
replacing a newer server revision, reports its identity, and offers an
|
|
12
|
+
explicit `--overwrite` only when the owner deliberately chooses the current
|
|
13
|
+
folder over the newer copy.
|
|
14
|
+
- Conflict messages now describe ownership and the next command instead of
|
|
15
|
+
exposing storage precondition terminology.
|
|
16
|
+
|
|
17
|
+
## 1.0.14 — 2026-07-29
|
|
18
|
+
|
|
19
|
+
- **Write-capable commands now prepare every project automatically.** The first
|
|
20
|
+
`run`, `storm`, `swarm`, or workforce execution in any folder installs the
|
|
21
|
+
same private, merge-only Agentlas project infrastructure through Core. This
|
|
22
|
+
is based on the folder the user opened, not on the Agentlas source checkout.
|
|
23
|
+
Read-only commands remain passive and do not create files.
|
|
24
|
+
- **Per-command help works before database startup.** Commands such as
|
|
25
|
+
`agentlas run --help` and `agentlas workforce --help` no longer require an
|
|
26
|
+
SQLite driver or open the project database just to print usage.
|
|
27
|
+
|
|
28
|
+
## 1.0.13 — 2026-07-28
|
|
29
|
+
|
|
30
|
+
- **MCP servers reach the chat again.** `runNativeTurn` only injects them at
|
|
31
|
+
`full` permission and that gate was right, but `sessions/session.cjs` never
|
|
32
|
+
put `mcpServers` in the request — zero callers — so no CLI ever received a
|
|
33
|
+
server. `agentlas mcp probe` printed "connected" while the turn that followed
|
|
34
|
+
could not use it. Turns now carry servers the user already consented to;
|
|
35
|
+
nothing new is asked mid-turn, because a turn is not a place a user can answer.
|
|
36
|
+
- **The shared database stops taking a write lock every turn.** Four
|
|
37
|
+
`CREATE TABLE IF NOT EXISTS`, five indexes and an `ALTER TABLE` ran on every
|
|
38
|
+
single turn against the file Desktop also uses. All idempotent, all taking the
|
|
39
|
+
lock; during a Desktop migration that burns the 15s busy timeout and every
|
|
40
|
+
call site swallowed the failure. Schema repair is once per connection now, and
|
|
41
|
+
the three copies of `ensureMemoryContextColumn` are one function.
|
|
42
|
+
- **Rows written here are checked for referential integrity.** `foreign_keys` is
|
|
43
|
+
a connection property, not a file property — Desktop opened with it ON and the
|
|
44
|
+
terminal did not, so terminal writes skipped the check on shared tables.
|
|
45
|
+
- **A zero-byte database file no longer blocks first run forever.** One stray
|
|
46
|
+
`sqlite3 <missing-path>` left an empty file, and every run after that read
|
|
47
|
+
"already exists" and died on `no such table` with nothing visible to explain it.
|
|
48
|
+
- **Bootstrap schema is current again** (v81; it was a v45 snapshot from
|
|
49
|
+
2026-07-07), and a mismatch with Desktop's migration target now fails a gate.
|
|
50
|
+
- **An engine update landing mid-run cannot mix two releases.** Core roots
|
|
51
|
+
resolve to their real path, so a long run keeps the modules it started with.
|
|
52
|
+
The next call still picks up the new release.
|
|
53
|
+
- `/ontology` says what it manages — this project's knowledge sources, not the
|
|
54
|
+
engine's knowledge runtime. The command itself is unchanged.
|
|
55
|
+
|
|
3
56
|
## 1.0.12 — 2026-07-28
|
|
4
57
|
|
|
5
58
|
- **Orchestrator/worker model roles now resolve from ordered candidate
|
package/README.md
CHANGED
|
@@ -110,9 +110,11 @@ agentlas connect <sub> # Telegram 등 플랫폼 연결 (무인자는 u
|
|
|
110
110
|
agentlas import <폴더> # 로컬 에이전트/팀 임포트
|
|
111
111
|
agentlas native prepare <agent> # 네이티브 CLI 문맥 파일 생성
|
|
112
112
|
agentlas list # 설치 에이전트/회사 + 활성 런타임
|
|
113
|
-
agentlas uninstall <agent>
|
|
113
|
+
agentlas uninstall <agent> [--yes] # 설치 에이전트 제거 (빌트인 거부).
|
|
114
|
+
# 대화 이력이 있으면 건수를 보여주고 --yes 없이는 거절한다
|
|
115
|
+
# (챗/메시지가 CASCADE로 함께 영구 삭제되기 때문).
|
|
114
116
|
agentlas experience <sub> # list|inspect|validate|save|publish|status|export|unpublish|withdraw
|
|
115
|
-
agentlas variant resolve
|
|
117
|
+
agentlas variant resolve --base-release <id> # 로컬 variant 호환성 프리뷰 (권위 없음, `agentlas variant help`)
|
|
116
118
|
```
|
|
117
119
|
|
|
118
120
|
### EXECUTE
|
package/bin/agentlas.cjs
CHANGED
|
@@ -117,9 +117,23 @@ function bootstrapDbIfMissing() {
|
|
|
117
117
|
const dir = path.dirname(p);
|
|
118
118
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
119
119
|
securePrivateMode(dir, 0o700);
|
|
120
|
+
// 존재 여부만 보면 **0바이트 파일이 부트스트랩을 영구히 막는다.** 실측 2026-07-28:
|
|
121
|
+
// `sqlite3 <없는경로> 'PRAGMA user_version;'` 같은 무해한 명령 하나가 빈 파일을 남기고,
|
|
122
|
+
// 그 뒤 모든 실행이 "이미 있음"으로 판단해 스키마 없이 진행하다 `no such table` 로
|
|
123
|
+
// 죽는다. 사용자에게는 원인이 전혀 안 보이는 상태다.
|
|
124
|
+
//
|
|
125
|
+
// SQLite 파일은 최소 한 페이지(512바이트)를 갖는다. 0바이트는 DB 가 아니라 자리만
|
|
126
|
+
// 잡힌 파일이므로 없는 것으로 취급해 정상 부트스트랩 경로를 태운다. 내용이 있는데
|
|
127
|
+
// 손상된 경우는 여기서 판단하지 않는다 — 그건 복구지 부트스트랩이 아니고, 멀쩡한
|
|
128
|
+
// DB 를 빈 것으로 오판해 덮어쓰는 위험이 훨씬 크다.
|
|
120
129
|
if (exists(p)) {
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
let empty = false;
|
|
131
|
+
try { empty = fs.statSync(p).size === 0; } catch { empty = false; }
|
|
132
|
+
if (!empty) {
|
|
133
|
+
securePrivateMode(p, 0o600);
|
|
134
|
+
return { created: false, path: p };
|
|
135
|
+
}
|
|
136
|
+
try { fs.rmSync(p, { force: true }); } catch { /* 지울 수 없으면 아래 link 가 EEXIST 로 알려준다 */ }
|
|
123
137
|
}
|
|
124
138
|
const schemaFile = path.join(PKG_ROOT, "engine", "bootstrap-schema.sql");
|
|
125
139
|
if (!exists(schemaFile)) {
|
|
@@ -172,7 +186,7 @@ function main() {
|
|
|
172
186
|
let error = null;
|
|
173
187
|
if (!engineFound) {
|
|
174
188
|
error = "Engine not found (engine/agentlas.cjs). Reinstall with: npm i -g agentlas";
|
|
175
|
-
} else if (!sqliteDriver) {
|
|
189
|
+
} else if (!sqliteDriver && !metadataOnly) {
|
|
176
190
|
error = `Node ${process.version} — no SQLite driver. Upgrade to Node 22.5+ or reinstall with 'npm i -g agentlas' to build better-sqlite3.`;
|
|
177
191
|
}
|
|
178
192
|
|
|
@@ -74,7 +74,7 @@ function buildManifest(root, options = {}) {
|
|
|
74
74
|
memoryPolicy: { writeBack: "ask", publicCopy: "reset" },
|
|
75
75
|
memory: files.filter((file) => [".agentlas/memory-map.json", ".agentlas/agent-card.json"].includes(file.path)).map((file) => file.path),
|
|
76
76
|
allowRead: ["README.md", "AGENTS.md", "agent.md", "skills/**", ".agentlas/*.json"],
|
|
77
|
-
denyRead: [".env", ".env.*", "**/secrets/**", "
|
|
77
|
+
denyRead: [".env", ".env.*", "secrets/**", "**/secrets/**", "credentials/**", "**/credentials/**", "cookies/**", "**/cookies/**"],
|
|
78
78
|
publicExportPolicy: "clean-copy",
|
|
79
79
|
requiredRuntime: ["mcp-client"],
|
|
80
80
|
license: "call-only-default",
|
|
@@ -92,7 +92,13 @@ function scanFiles(files) {
|
|
|
92
92
|
findings.push({ verdict, type, path: file.path, ...(line ? { line } : {}), message, redacted: true });
|
|
93
93
|
}
|
|
94
94
|
for (const file of files) {
|
|
95
|
-
|
|
95
|
+
// A credential store is a path SEGMENT, never a filename substring. The old
|
|
96
|
+
// `/token|secret/i` test blocked ordinary vocabulary — measured 2026-07-29,
|
|
97
|
+
// it gave the live `web-master` package a BLOCK verdict for its own design
|
|
98
|
+
// system (`token-architecture.md`, `reference-token-db.json`). Dropping it
|
|
99
|
+
// costs no coverage: the per-line SECRET_PATTERNS scan below reads every
|
|
100
|
+
// packaged file and is the check that actually looks at values.
|
|
101
|
+
if (/^\.env(\.|$)/.test(file.path.split("/").pop() || "") || /(?:^|\/)(secrets|credentials|cookies)\//i.test(file.path)) {
|
|
96
102
|
add("BLOCK", "credential-path", file, null, "Credential-like file path is excluded from Cloud package and public publish.");
|
|
97
103
|
}
|
|
98
104
|
file.content.split(/\r?\n/).forEach((line, index) => {
|
|
@@ -100,28 +100,35 @@ function mergePrefs(base, patch) {
|
|
|
100
100
|
return next;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/*
|
|
104
|
+
* Throws on failure (lock timeout, read-only data dir, ENOSPC …) — it used to
|
|
105
|
+
* swallow every error and return null, so `agentlas setup` announced "All set."
|
|
106
|
+
* and exited 0 while the user's language/runtime/permission were never written.
|
|
107
|
+
* A caller that cannot persist must be able to say so; use savePrefs for the
|
|
108
|
+
* best-effort boolean contract.
|
|
109
|
+
*/
|
|
103
110
|
function updatePrefs(userDataDir, patch) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
});
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
111
|
+
fs.mkdirSync(userDataDir, { recursive: true });
|
|
112
|
+
return withPrefsLock(userDataDir, () => {
|
|
113
|
+
const file = prefsPath(userDataDir);
|
|
114
|
+
const currentWasValid = Boolean(readPrefsFile(file));
|
|
115
|
+
const current = loadPrefs(userDataDir);
|
|
116
|
+
if (fs.existsSync(file) && !currentWasValid) {
|
|
117
|
+
try { fs.renameSync(file, `${file}.corrupt-${Date.now()}-${process.pid}`); } catch { /* recover from backup */ }
|
|
118
|
+
}
|
|
119
|
+
const next = mergePrefs(current, patch);
|
|
120
|
+
if (currentWasValid) atomicWrite(backupPath(userDataDir), current);
|
|
121
|
+
atomicWrite(file, next);
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
121
124
|
}
|
|
122
125
|
|
|
123
126
|
function savePrefs(userDataDir, prefs) {
|
|
124
|
-
|
|
127
|
+
try {
|
|
128
|
+
return Boolean(updatePrefs(userDataDir, prefs));
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
module.exports = { prefsPath, loadPrefs, savePrefs, updatePrefs, mergePrefs };
|
|
@@ -84,7 +84,20 @@ function resolveCoreRuntimeRootFromCandidates(candidateRoots, requiredMarkers =
|
|
|
84
84
|
const version = readCoreRuntimeVersion(root);
|
|
85
85
|
if (!version || compareSemVer(version, minVersion) < 0) continue;
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
// `~/.agentlas/runtime/current` 는 업데이터가 원자적으로 갈아 끼우는 심볼릭
|
|
88
|
+
// 링크다. 그 경로를 그대로 넘기면 Python 이 import 를 늦게 해석하는 특성상,
|
|
89
|
+
// 긴 실행 도중 업데이트가 일어나면 **옛 버전 모듈과 새 버전 모듈이 한 프로세스에
|
|
90
|
+
// 섞여 로드된다** — 어떤 버전에서도 시험된 적 없는 조합이고 버전 번호로는
|
|
91
|
+
// 재현조차 못 한다(감사 D3).
|
|
92
|
+
//
|
|
93
|
+
// 실경로로 고정해도 라이브 버전 선택은 그대로다: **다음** 호출이 다시 해석해
|
|
94
|
+
// 새 릴리스를 집는다. 없어지는 것은 실행 중 교체뿐이다.
|
|
95
|
+
try {
|
|
96
|
+
return fs.realpathSync(root);
|
|
97
|
+
} catch {
|
|
98
|
+
// 링크를 읽을 수 없다고 실행을 거부할 이유는 없다 — 이전 동작대로 경로를 쓴다.
|
|
99
|
+
return root;
|
|
100
|
+
}
|
|
88
101
|
} catch {
|
|
89
102
|
// Continue to the next installed/bundled root.
|
|
90
103
|
}
|
package/engine/agentlas-i18n.cjs
CHANGED
|
@@ -260,6 +260,7 @@ const STRINGS = {
|
|
|
260
260
|
"wiz.default": "(Enter default)",
|
|
261
261
|
"wiz.invalid": "Choose 1–%s.",
|
|
262
262
|
"wiz.saved": "All set. You can change any of this later with /runtime, /permission.",
|
|
263
|
+
"wiz.saveFailed": "Could not save your choices (%s) — they apply to this session only, and setup will ask again next launch.",
|
|
263
264
|
"wiz.changeLang": "Tip: re-run setup anytime with agentlas setup",
|
|
264
265
|
},
|
|
265
266
|
ko: {
|
|
@@ -505,6 +506,7 @@ const STRINGS = {
|
|
|
505
506
|
"wiz.default": "(Enter 기본값)",
|
|
506
507
|
"wiz.invalid": "1–%s 중 고르세요.",
|
|
507
508
|
"wiz.saved": "완료. 나중에 /runtime, /permission으로 언제든 바꿀 수 있어요.",
|
|
509
|
+
"wiz.saveFailed": "선택을 저장하지 못했습니다 (%s) — 이번 세션에만 적용되고, 다음 실행 때 다시 물어봅니다.",
|
|
508
510
|
"wiz.changeLang": "팁: 언제든 agentlas setup 으로 다시 설정",
|
|
509
511
|
},
|
|
510
512
|
};
|
|
@@ -298,7 +298,7 @@ const SLASH_COMMAND_META = [
|
|
|
298
298
|
{ command: "/route", description: "Preview which agent/pipeline would take a request", category: "Engine", usage: "/route <request>", detail: "Runs the Hephaestus router without executing — shows the selected agent, candidates, and reasons." },
|
|
299
299
|
{ command: "/research", description: "Run the Hephaestus Research Engine", category: "Engine", usage: "/research search \"query\"", detail: "status|gather|search|read|plan — evidence-grade web research from the terminal." },
|
|
300
300
|
{ command: "/search", description: "Discover agents in the Hub", category: "Hub", usage: "/search <what you need>", detail: "Search the Agentlas Hub + local for an agent that fits the task (hep-search)." },
|
|
301
|
-
{ command: "/install", description: "Install an agent from the Hub by slug", category: "Hub", usage: "/install <slug>", detail: "Install a marketplace agent into this terminal
|
|
301
|
+
{ command: "/install", description: "Install an agent from the Hub by slug", category: "Hub", usage: "/install <slug>", detail: "Install a marketplace agent into this terminal." },
|
|
302
302
|
{ command: "/network", description: "Staff and execute an ontology-grounded task force", category: "Engine", usage: "/network <request> [--benchmark]", detail: "The active top host LLM creates the work order, searches the Hub workforce ontology, selects exact releases, and executes a receipt-backed task force.", aliases: ["/taskforce", "/workforce"] },
|
|
303
303
|
{ command: "/legacy-network", description: "Run the compatibility Hephaestus network route", category: "Engine", usage: "/legacy-network <request>", detail: "Explicit compatibility escape hatch; never used as a fallback by /network." },
|
|
304
304
|
{ command: "/browser", description: "Real browser execution hardpoint", category: "Engine", usage: "/browser [sub]", detail: "Runs the Agentlas browser hardpoint (hep-browser)." },
|
|
@@ -163,7 +163,17 @@ function tableExists(db, name) {
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// 커넥션당 1회. 이전에는 `beginTurn`/`listScopedTimeline` 이 부를 때마다, 즉 **매 턴**
|
|
167
|
+
// 공유 DB 에 테이블 4개 + 인덱스 5개 DDL 과 `UPDATE … stale` 을 날렸다. 전부
|
|
168
|
+
// IF NOT EXISTS 라 결과는 멱등이지만 쓰기 락을 잡는 것은 매번이었고, 데스크탑
|
|
169
|
+
// 마이그레이션과 겹치면 15초 busy_timeout 을 소진했다(2026-07-28).
|
|
170
|
+
const { ensureOnce } = require("./core/schema-ensure.cjs");
|
|
171
|
+
|
|
166
172
|
function ensureGovernanceSchema(db) {
|
|
173
|
+
return ensureOnce(db, "terminal_memory.schema", (conn) => ensureGovernanceSchemaOnce(conn));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ensureGovernanceSchemaOnce(db) {
|
|
167
177
|
db.exec(`
|
|
168
178
|
CREATE TABLE IF NOT EXISTS terminal_memory_turn_intents (
|
|
169
179
|
turn_id TEXT PRIMARY KEY,
|
|
@@ -7,8 +7,13 @@ const i18n = require("./agentlas-i18n.cjs");
|
|
|
7
7
|
const banner = require("./agentlas-banner.cjs");
|
|
8
8
|
const { visWidth, wrapWidth } = require("./agentlas-composer.cjs");
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/*
|
|
11
|
+
* req = { ui, rl, helpers, persist } → Promise<{ onboarded, saved, saveError, lang, runtime, permission }>
|
|
12
|
+
* `persist` writes the answers and must throw when it cannot. The wizard calls it
|
|
13
|
+
* itself, before announcing "All set." — the caller used to save afterwards, so a
|
|
14
|
+
* failed write (locked/read-only data dir) was announced as success and dropped.
|
|
15
|
+
*/
|
|
16
|
+
async function runOnboard({ ui, rl, helpers, persist }) {
|
|
12
17
|
const H = helpers;
|
|
13
18
|
const c = ui.c;
|
|
14
19
|
const contentWidth = () => Math.max(20, (ui.out.columns || 80) - 5);
|
|
@@ -149,10 +154,22 @@ async function runOnboard({ ui, rl, helpers }) {
|
|
|
149
154
|
const pi = await pickNum(permOpts.length);
|
|
150
155
|
const permission = permOpts[pi - 1].v;
|
|
151
156
|
|
|
157
|
+
// Save first, then report what actually happened — never the other way round.
|
|
158
|
+
let saveError = null;
|
|
159
|
+
try {
|
|
160
|
+
if (typeof persist !== "function") throw new Error("no persistence handler");
|
|
161
|
+
await persist({ lang, runtime, permission });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
saveError = error;
|
|
164
|
+
}
|
|
152
165
|
ui.line("");
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
166
|
+
if (saveError) {
|
|
167
|
+
ui.error(ui.t("wiz.saveFailed", String((saveError && saveError.message) || saveError)));
|
|
168
|
+
} else {
|
|
169
|
+
printSaved(ui.t("wiz.saved"));
|
|
170
|
+
printIndented(ui.t("wiz.changeLang"), c.faint);
|
|
171
|
+
}
|
|
172
|
+
return { onboarded: true, saved: !saveError, saveError, lang, runtime, permission };
|
|
156
173
|
} finally {
|
|
157
174
|
rl.removeListener("line", onLine);
|
|
158
175
|
if (ownsSigint) rl.removeListener("SIGINT", onSigint);
|
|
@@ -6,15 +6,26 @@
|
|
|
6
6
|
// fail immediately with SQLITE_BUSY even when busy_timeout is configured.
|
|
7
7
|
const SQLITE_BUSY_TIMEOUT_MS = 15_000;
|
|
8
8
|
|
|
9
|
+
// `foreign_keys` 는 파일이 아니라 **커넥션** 속성이다. 데스크탑은
|
|
10
|
+
// `electron/store/db.ts` 에서 `foreign_keys = ON` 으로 열고, 터미널은 켜지 않아
|
|
11
|
+
// **같은 테이블인데 터미널이 쓰는 행만 참조 무결성 검사를 건너뛰고 있었다**
|
|
12
|
+
// (2026-07-28 확인). journal_mode 처럼 파일에 박히는 값이 아니라서 한쪽이 켠 것이
|
|
13
|
+
// 다른 쪽에 전파되지 않는다. 스키마 소유는 데스크탑이므로 그 규칙을 그대로 따른다.
|
|
14
|
+
const SQLITE_CONNECTION_PRAGMAS = [
|
|
15
|
+
`busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`,
|
|
16
|
+
"foreign_keys = ON",
|
|
17
|
+
];
|
|
18
|
+
|
|
9
19
|
function configureSqliteConnection(db) {
|
|
10
20
|
if (!db) throw new TypeError("SQLite connection is required");
|
|
11
|
-
|
|
12
|
-
db.pragma
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const applyPragma =
|
|
22
|
+
typeof db.pragma === "function"
|
|
23
|
+
? (statement) => db.pragma(statement)
|
|
24
|
+
: typeof db.exec === "function"
|
|
25
|
+
? (statement) => db.exec(`PRAGMA ${statement}`)
|
|
26
|
+
: null;
|
|
27
|
+
if (!applyPragma) throw new TypeError("SQLite connection must expose pragma() or exec()");
|
|
28
|
+
for (const statement of SQLITE_CONNECTION_PRAGMAS) applyPragma(statement);
|
|
18
29
|
return db;
|
|
19
30
|
}
|
|
20
31
|
|
|
@@ -21,6 +21,7 @@ const fs = require("node:fs");
|
|
|
21
21
|
const net = require("node:net");
|
|
22
22
|
const path = require("node:path");
|
|
23
23
|
const { Ui } = require("./agentlas-ui.cjs");
|
|
24
|
+
const { recommendedConcurrency } = require("./workforce/concurrency.cjs");
|
|
24
25
|
|
|
25
26
|
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{1,255}$/;
|
|
26
27
|
const HASH_RE = /^sha256:[0-9a-f]{64}$/;
|
|
@@ -1516,7 +1517,9 @@ function validatePreparedExecution(value, workOrder, selection, candidateSet, va
|
|
|
1516
1517
|
function validateDelegationPlan(value, selection) {
|
|
1517
1518
|
const plan = assertObject(value, "delegationPlan");
|
|
1518
1519
|
assertExactKeys(plan, ["schemaVersion", "planId", "packets", "synthesis", "verifier"], "delegationPlan", "planner_invalid");
|
|
1519
|
-
|
|
1520
|
+
// v2: 패킷에 doneWhen(검증 가능한 완료조건 체크리스트)이 필수가 됐다. 생산자(같은
|
|
1521
|
+
// 파일의 플래너 프롬프트)와 검증자가 항상 함께 배포되므로 호환 창구는 없다.
|
|
1522
|
+
if (plan.schemaVersion !== "agentlas.workforce-delegation-plan.v2") fail("planner_invalid", "unsupported workforce delegation plan schema");
|
|
1520
1523
|
assertId(plan.planId, "executionPlan.planId");
|
|
1521
1524
|
const assignments = new Map(selection.assignments.map((row) => [`${row.slotId}\0${row.agentReleaseId}`, row]));
|
|
1522
1525
|
const packets = assertArray(plan.packets, "executionPlan.packets", MAX_ASSIGNMENTS, { min: 1 });
|
|
@@ -1534,6 +1537,9 @@ function validateDelegationPlan(value, selection) {
|
|
|
1534
1537
|
assertString(packet.objective, "packet.objective", UNBOUNDED_EXPLANATION_FIELD);
|
|
1535
1538
|
assertArray(packet.inputs, "packet.inputs", 64).forEach((item, index) => assertString(item, `packet.inputs[${index}]`, UNBOUNDED_EXPLANATION_FIELD));
|
|
1536
1539
|
assertString(packet.expectedOutput, "packet.expectedOutput", UNBOUNDED_EXPLANATION_FIELD);
|
|
1540
|
+
// 완료조건은 위임 계약의 필수 요소다(v2) — 각 항목이 워커 반환물만 보고 참/거짓
|
|
1541
|
+
// 판정 가능한 문장이어야 하며, 검증자 criteria와 같은 개수·길이 상한을 쓴다.
|
|
1542
|
+
assertArray(packet.doneWhen, "packet.doneWhen", 16, { min: 1 }).forEach((item, index) => assertString(item, `packet.doneWhen[${index}]`, 500));
|
|
1537
1543
|
}
|
|
1538
1544
|
if (pairs.size !== assignments.size || [...assignments.keys()].some((pair) => !pairs.has(pair))) fail("planner_missing_child", "planner must create one separate child packet for every accepted assignment");
|
|
1539
1545
|
for (const key of ["synthesis", "verifier"]) {
|
|
@@ -1818,11 +1824,12 @@ function buildPrompts(task, identity) {
|
|
|
1818
1824
|
requestExpansionForSlots: [],
|
|
1819
1825
|
};
|
|
1820
1826
|
const delegationPlanShape = {
|
|
1821
|
-
schemaVersion: "agentlas.workforce-delegation-plan.
|
|
1827
|
+
schemaVersion: "agentlas.workforce-delegation-plan.v2",
|
|
1822
1828
|
planId: "workforce-plan:<id>",
|
|
1823
1829
|
packets: [{
|
|
1824
1830
|
packetId: "packet:<id>", slotId: "<selected slot>", agentReleaseId: "<selected release>",
|
|
1825
1831
|
objective: "bounded objective", inputs: [], expectedOutput: "concrete handoff",
|
|
1832
|
+
doneWhen: ["checkable completion condition"],
|
|
1826
1833
|
}],
|
|
1827
1834
|
synthesis: { slotId: "<selected slot>", agentReleaseId: "<selected release>", brief: "integration brief" },
|
|
1828
1835
|
verifier: { slotId: "<selected slot>", agentReleaseId: "<selected release>", brief: "verification brief", criteria: ["criterion"] },
|
|
@@ -1863,13 +1870,14 @@ function buildPrompts(task, identity) {
|
|
|
1863
1870
|
const plannerSchemaRequirements = [
|
|
1864
1871
|
`Return exactly one object: ${stableJson(plannerShape)}`,
|
|
1865
1872
|
"Return agentlas.workforce-orchestration-plan.v2 with exactly delegationPlan and capabilityBindingPlan. Copy plannerInvocationId, executionContextDigest, and toolInventoryDigest exactly from PLANNER_LINEAGE_DATA. The host computes bindingPlanDigest after validating your choices; do not emit bindingPlanDigest.",
|
|
1866
|
-
"Create exactly one delegationPlan packet for every accepted slot/release pair. Every packet must explicitly author packetId, slotId, agentReleaseId, objective, inputs, and
|
|
1873
|
+
"Create exactly one delegationPlan packet for every accepted slot/release pair. Every packet must explicitly author packetId, slotId, agentReleaseId, objective, inputs, expectedOutput, and doneWhen.",
|
|
1874
|
+
"doneWhen is that packet's acceptance checklist: 1..16 conditions, each independently checkable as true or false from the worker's returned handoff alone (name concrete artifacts, fields, counts, or observable facts — never vibes like 'high quality'). State the goal and required results, but do not over-specify the worker's method or ordering. The verifier receives every packet's doneWhen alongside its handoff.",
|
|
1867
1875
|
"Choose capabilityBindingPlan.inventory only from POLICY_FILTERED_LOCAL_TOOL_MENU_DATA. Cover every requiredToolCapabilities id exactly once for each slot/release pair. One selected tool row may cover multiple capabilities. If a required capability has no exact ready tool, do not invent a binding; return the best schema-valid plan and allow deterministic validation to reject it.",
|
|
1868
1876
|
"Each bound inventory row must explicitly contain slotId, agentReleaseId, permissionPolicyDigest, provider, toolId, capabilityIds, status=bound. An empty inventory is required when every slot has no required tool capability.",
|
|
1869
1877
|
"synthesis must explicitly author slotId, agentReleaseId, and brief. verifier must explicitly author slotId, agentReleaseId, brief, and a non-empty criteria array. The host will not add, remove, normalize, or substitute a release or field.",
|
|
1870
1878
|
// 호스트가 강제하는 상한을 미리 알려준다 — 알려주지 않은 상한은 첫 시도를 반드시
|
|
1871
1879
|
// 깨고 교정 1회로도 회복되지 않는다(2026-07-27 라이브 실측, 중첩 매니저 동일 계열).
|
|
1872
|
-
"Objectives, inputs, expectedOutput, and briefs have no character limit — write them as long as the work honestly needs. Only counts are bounded: at most 64 inputs per packet and at most 32 verifier criteria of at most 450 characters each.",
|
|
1880
|
+
"Objectives, inputs, expectedOutput, and briefs have no character limit — write them as long as the work honestly needs. Only counts are bounded: at most 64 inputs per packet, 1..16 doneWhen conditions of at most 500 characters each, and at most 32 verifier criteria of at most 450 characters each.",
|
|
1873
1881
|
].join("\n");
|
|
1874
1882
|
return {
|
|
1875
1883
|
searchSystem: [
|
|
@@ -2012,6 +2020,71 @@ function create(deps = {}) {
|
|
|
2012
2020
|
};
|
|
2013
2021
|
}
|
|
2014
2022
|
|
|
2023
|
+
// ── 격리 고지 + 토큰 계측 ───────────────────────────────────────────────
|
|
2024
|
+
// 실행마다 어느 단계가 어느 런타임에서 얼마의 토큰을 썼는지 모은다. 새는 곳을
|
|
2025
|
+
// 추측하지 않고 보기 위한 장부다 — 2026-07-28 실측에서 codex 리더가 사소한
|
|
2026
|
+
// 프롬프트 하나에 입력 18,235 토큰을 실었고, 그 원인(스킬 라이브러리 전량 적재)은
|
|
2027
|
+
// 합계를 보기 전까지 아무도 몰랐다.
|
|
2028
|
+
const isolationNotices = new Map();
|
|
2029
|
+
const tokenLedger = [];
|
|
2030
|
+
// ui 는 실행 컨텍스트에만 있으므로 여기서는 버퍼에 모으고, 영수증 시점에 낸다.
|
|
2031
|
+
function noteIsolationWeakness(kind, role) {
|
|
2032
|
+
const key = `${kind}:${role || "stage"}`;
|
|
2033
|
+
if (isolationNotices.has(key)) return;
|
|
2034
|
+
isolationNotices.set(key, `${kind}(${role || "stage"} 단계)`);
|
|
2035
|
+
}
|
|
2036
|
+
/**
|
|
2037
|
+
* 단계별 토큰 장부를 사람이 읽는 표로 낸다.
|
|
2038
|
+
*
|
|
2039
|
+
* 총합만 보면 "많이 썼다"밖에 모른다. 어느 단계가, 어느 런타임에서, 호출 하나당
|
|
2040
|
+
* 얼마를 실었는지를 나란히 놓아야 새는 곳이 보인다 — 입력이 출력보다 자릿수로
|
|
2041
|
+
* 크면 그건 작업이 아니라 적재다.
|
|
2042
|
+
*/
|
|
2043
|
+
function reportTokenLedger(ui) {
|
|
2044
|
+
if (!tokenLedger.length && !isolationNotices.size) return;
|
|
2045
|
+
const byStage = new Map();
|
|
2046
|
+
for (const row of tokenLedger) {
|
|
2047
|
+
const key = `${row.role}·${row.runtime}${row.model ? `/${row.model}` : ""}`;
|
|
2048
|
+
const acc = byStage.get(key) || { calls: 0, input: 0, output: 0, cached: 0 };
|
|
2049
|
+
acc.calls += 1; acc.input += row.input; acc.output += row.output; acc.cached += row.cached;
|
|
2050
|
+
byStage.set(key, acc);
|
|
2051
|
+
}
|
|
2052
|
+
const totalIn = tokenLedger.reduce((sum, row) => sum + row.input, 0);
|
|
2053
|
+
const totalOut = tokenLedger.reduce((sum, row) => sum + row.output, 0);
|
|
2054
|
+
for (const label of isolationNotices.values()) {
|
|
2055
|
+
ui.warn(
|
|
2056
|
+
`격리 고지: ${label}는 도구 인벤토리가 비었음을 증명하지 못합니다. 도구 호출은 차단되지만 `
|
|
2057
|
+
+ "이 런타임은 호스트의 스킬/플러그인 이름을 컨텍스트에 싣습니다(그래서 입력 토큰도 큽니다). "
|
|
2058
|
+
+ "강한 격리가 필요하면 그 단계를 claude-code로 배정하세요.",
|
|
2059
|
+
);
|
|
2060
|
+
}
|
|
2061
|
+
ui.line("");
|
|
2062
|
+
ui.info(`token ledger — 입력 ${totalIn.toLocaleString()} / 출력 ${totalOut.toLocaleString()} · 호출 ${tokenLedger.length}건`);
|
|
2063
|
+
const rows = [...byStage.entries()].sort((a, b) => b[1].input - a[1].input);
|
|
2064
|
+
for (const [key, acc] of rows) {
|
|
2065
|
+
const perCall = Math.round(acc.input / Math.max(1, acc.calls));
|
|
2066
|
+
const share = totalIn ? Math.round((acc.input / totalIn) * 100) : 0;
|
|
2067
|
+
ui.line(
|
|
2068
|
+
` ${key.padEnd(34)} 호출 ${String(acc.calls).padStart(2)} · 입력 ${String(acc.input).padStart(8)}`
|
|
2069
|
+
+ ` (${String(share).padStart(2)}%, 호출당 ${perCall.toLocaleString()}) · 출력 ${acc.output}`,
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
// 입력이 출력의 100배를 넘는 단계는 일이 아니라 적재를 하고 있다.
|
|
2073
|
+
for (const [key, acc] of rows) {
|
|
2074
|
+
if (acc.output > 0 && acc.input / acc.output > 100) {
|
|
2075
|
+
ui.warn(`토큰 누수 의심: ${key} — 입력이 출력의 ${Math.round(acc.input / acc.output)}배. 컨텍스트 적재를 확인하세요.`);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function recordStageTokens(role, runtimeKind, modelPin, usage) {
|
|
2081
|
+
if (!usage) return;
|
|
2082
|
+
const input = Number(usage.inputTokens ?? usage.input_tokens ?? 0) || 0;
|
|
2083
|
+
const output = Number(usage.outputTokens ?? usage.output_tokens ?? 0) || 0;
|
|
2084
|
+
const cached = Number(usage.cachedInputTokens ?? usage.cached_input_tokens ?? 0) || 0;
|
|
2085
|
+
tokenLedger.push({ role: role || "stage", runtime: runtimeKind || "?", model: modelPin || null, input, output, cached });
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2015
2088
|
async function runModel(runtime, system, prompt, context) {
|
|
2016
2089
|
const invocation = stageInvocation(runtime, context);
|
|
2017
2090
|
const executionRuntime = invocation.executionRuntime;
|
|
@@ -2041,19 +2114,32 @@ function create(deps = {}) {
|
|
|
2041
2114
|
}
|
|
2042
2115
|
if (executionRuntime.mode === "cli") {
|
|
2043
2116
|
const authorityMode = context.authorityMode || "no-authority";
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2117
|
+
// 격리 강도는 런타임마다 다르다. claude-code는 `--tools ""`로 도구 인벤토리가
|
|
2118
|
+
// 비었음을 증명할 수 있고, codex/gemini는 못 한다 — 2026-07-28 실측: 모든
|
|
2119
|
+
// 격리 플래그(--ephemeral --ignore-user-config --ignore-rules --disable
|
|
2120
|
+
// plugins/tool_suggest/...)를 준 codex가 사용자의 개인 스킬 이름을 전부
|
|
2121
|
+
// 열거했고 사소한 프롬프트에 입력 18,235 토큰을 실었다.
|
|
2122
|
+
//
|
|
2123
|
+
// 그런데 그걸 이유로 실행을 통째로 거부하면 그 런타임을 오케스트레이터로
|
|
2124
|
+
// 고른 사용자는 네트워크 전체를 잃는다. 실제로 새는 것은 "스킬 이름 목록"이고,
|
|
2125
|
+
// 그것도 사용자 본인 기계에서 본인이 시작한 실행이다 — 도구 호출은 여전히
|
|
2126
|
+
// 막혀 있다. 비례가 맞지 않는 차단이었고, 우회 수단조차 없었다.
|
|
2127
|
+
//
|
|
2128
|
+
// 그래서 거부 대신 고지한다: 무엇이 격리되지 않는지 이름을 대고, 실행은 한다.
|
|
2129
|
+
// 강한 격리가 필요한 호스트는 AGENTLAS_WORKFORCE_REQUIRE_PROVEN_ISOLATION=1로
|
|
2130
|
+
// 예전 동작(거부)을 되찾을 수 있다.
|
|
2131
|
+
const provenIsolation = executionRuntime.kind === "claude-code";
|
|
2132
|
+
if (!provenIsolation && authorityMode === "no-authority") {
|
|
2133
|
+
if (String(process.env.AGENTLAS_WORKFORCE_REQUIRE_PROVEN_ISOLATION || "") === "1") {
|
|
2134
|
+
fail(
|
|
2135
|
+
"workforce_runtime_isolation_unverified",
|
|
2136
|
+
`${executionRuntime.kind} cannot prove an empty tool inventory, and this host requires proven isolation. `
|
|
2137
|
+
+ "Assign claude-code for this stage, or unset AGENTLAS_WORKFORCE_REQUIRE_PROVEN_ISOLATION.",
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
noteIsolationWeakness(executionRuntime.kind, invocation.role);
|
|
2055
2141
|
}
|
|
2056
|
-
|
|
2142
|
+
const captured = normalizeModelResult(await D.captureRuntime(executionRuntime.kind, effectiveSystem, prompt, {
|
|
2057
2143
|
cwd: context.cwd,
|
|
2058
2144
|
env: context.env,
|
|
2059
2145
|
permission: context.permission,
|
|
@@ -2066,14 +2152,18 @@ function create(deps = {}) {
|
|
|
2066
2152
|
outputLimitBytes: authorityMode === "read-only" ? 24 * 1024 * 1024 : undefined,
|
|
2067
2153
|
envelope: true,
|
|
2068
2154
|
}));
|
|
2155
|
+
recordStageTokens(invocation.role, executionRuntime.kind, invocation.modelPin, captured.usage);
|
|
2156
|
+
return captured;
|
|
2069
2157
|
}
|
|
2070
|
-
|
|
2158
|
+
const viaApi = normalizeModelResult(await D.runApi(
|
|
2071
2159
|
executionRuntime.backend,
|
|
2072
2160
|
invocation.modelPin,
|
|
2073
2161
|
effectiveSystem,
|
|
2074
2162
|
prompt,
|
|
2075
2163
|
{ effort: invocation.effort, envelope: true },
|
|
2076
2164
|
));
|
|
2165
|
+
recordStageTokens(invocation.role, executionRuntime.backend, invocation.modelPin, viaApi.usage);
|
|
2166
|
+
return viaApi;
|
|
2077
2167
|
}
|
|
2078
2168
|
|
|
2079
2169
|
async function callHubTool(name, args) {
|
|
@@ -3331,7 +3421,10 @@ function create(deps = {}) {
|
|
|
3331
3421
|
}
|
|
3332
3422
|
|
|
3333
3423
|
const slotById = new Map(workOrder.roleSlots.map((slot) => [slot.slotId, slot]));
|
|
3334
|
-
|
|
3424
|
+
// 사용자가 --parallel/-n을 명시하면 그 값(상한만 적용), 아니면 사양 기반 추천값.
|
|
3425
|
+
const concurrency = Number.isFinite(Number(ctx.concurrency)) && Number(ctx.concurrency) > 0
|
|
3426
|
+
? Math.max(1, Math.min(8, Number(ctx.concurrency)))
|
|
3427
|
+
: recommendedConcurrency();
|
|
3335
3428
|
let cursor = 0;
|
|
3336
3429
|
const outputs = new Array(delegationPlan.packets.length);
|
|
3337
3430
|
const publicWorkers = new Array(delegationPlan.packets.length);
|
|
@@ -3680,6 +3773,9 @@ function create(deps = {}) {
|
|
|
3680
3773
|
`PINNED_PACKAGE_HASH=${pinned.packageHash}`,
|
|
3681
3774
|
`PINNED_CONTENT_DIGEST=${pinned.contentDigest}`,
|
|
3682
3775
|
"Do only your packet. Do not select or summon another agent. Return a concrete handoff artifact for the manager.",
|
|
3776
|
+
// 산출물과 한계·상태를 분리해야 검증자가 판정할 근거가 생긴다(위임
|
|
3777
|
+
// 계약 7요소 중 상태·증거). COMPLETED는 워커의 주장일 뿐이다.
|
|
3778
|
+
"End your handoff with two labeled sections: LIMITATIONS (what you could not verify or complete — write 'none' only if truly none) and STATUS (COMPLETED, PARTIAL, or FAILED; for PARTIAL/FAILED name each unmet doneWhen condition from your packet). Claiming COMPLETED does not finish the run — a pinned verifier accepts or rejects your claim.",
|
|
3683
3779
|
].join("\n\n"),
|
|
3684
3780
|
prompt: stableJson({
|
|
3685
3781
|
sharedTask: workOrder.taskBrief,
|
|
@@ -3749,6 +3845,11 @@ function create(deps = {}) {
|
|
|
3749
3845
|
system: [
|
|
3750
3846
|
pinned.executionGraph.manager.content,
|
|
3751
3847
|
"You are the pinned manager synthesizing every declared worker handoff. Do not omit a worker or claim an undeclared worker ran.",
|
|
3848
|
+
// 팀 합성물은 최상위 패킷의 핸드오프가 된다 — 직접 워커와 동일한 반환
|
|
3849
|
+
// 계약을 적용해야 검증자가 판정 근거를 얻는다. (2026-07-28 라이브 A/B
|
|
3850
|
+
// 실측: 최상위 패킷 2개가 모두 중첩 팀이라 이 요구가 어디에도 적용되지
|
|
3851
|
+
// 않았다 — 직접 워커 경로에만 넣은 커버리지 갭.)
|
|
3852
|
+
"End your synthesis with two labeled sections: LIMITATIONS (what the team could not verify or complete — write 'none' only if truly none) and STATUS (COMPLETED, PARTIAL, or FAILED; for PARTIAL/FAILED name each unmet doneWhen condition from the parent packet). Claiming COMPLETED does not finish the run — a pinned verifier accepts or rejects the claim.",
|
|
3752
3853
|
].join("\n\n"),
|
|
3753
3854
|
prompt: stableJson({ parentPacket: packet, synthesisBrief: manager.plan.synthesisBrief, handoffs: graphWorkerOutputs.map((row) => ({ id: row.graphWorker.id, text: row.text })) }),
|
|
3754
3855
|
});
|
|
@@ -4108,14 +4209,25 @@ function create(deps = {}) {
|
|
|
4108
4209
|
system: [
|
|
4109
4210
|
pinned.instructions,
|
|
4110
4211
|
"VERIFIER ESCALATION MODE: this exact worker packet was identified as failed by two independent verifier rounds. You are the single allowed orchestrator retry for this packet. Produce one replacement handoff, preserve the packet scope and pinned release, and do not delegate or retry again.",
|
|
4212
|
+
// 수정 지시는 전체 재작성 지시가 아니다 — 통과분(preserve)을 명시하지
|
|
4213
|
+
// 않으면 정확했던 부분이 재작성 과정에서 손상된다(위임 계약 수정 규칙).
|
|
4214
|
+
"Repair, do not rewrite from scratch: preservedChecks lists verifier checks that already PASSED — keep the prior handoff's content that satisfied them and do not regress it. defects lists what failed — change only what those defects require. End with the same LIMITATIONS and STATUS sections required of every worker handoff.",
|
|
4111
4215
|
].join("\n\n"),
|
|
4112
4216
|
prompt: stableJson({
|
|
4113
4217
|
sharedTask: workOrder.taskBrief,
|
|
4114
4218
|
roleSlot: slotById.get(packet.slotId),
|
|
4115
4219
|
packet,
|
|
4116
4220
|
priorHandoff: output.text,
|
|
4117
|
-
|
|
4221
|
+
preservedChecks: receipt.correctiveHistory.flatMap((row) =>
|
|
4222
|
+
(row.verification.checks || [])
|
|
4223
|
+
.filter((check) => check.status === "passed")
|
|
4224
|
+
.map((check) => ({ checkId: check.checkId, evidence: check.evidence })),
|
|
4225
|
+
),
|
|
4226
|
+
defects: receipt.correctiveHistory.map((row) => ({
|
|
4118
4227
|
issues: row.verification.issues,
|
|
4228
|
+
failedChecks: (row.verification.checks || [])
|
|
4229
|
+
.filter((check) => check.status === "failed")
|
|
4230
|
+
.map((check) => ({ checkId: check.checkId, evidence: check.evidence })),
|
|
4119
4231
|
failedPacketIds: row.verification.failedPacketIds,
|
|
4120
4232
|
})),
|
|
4121
4233
|
}),
|
|
@@ -4316,6 +4428,7 @@ function create(deps = {}) {
|
|
|
4316
4428
|
ui.line("");
|
|
4317
4429
|
ui.markdown(finalText);
|
|
4318
4430
|
ui.info(`workforce receipt: ${runId} · roster ${receipt.workers.length}/${delegationPlan.packets.length} · verifier passed`);
|
|
4431
|
+
reportTokenLedger(ui);
|
|
4319
4432
|
if (benchmarkArtifactPath) ui.info(`workforce benchmark artifacts: ${benchmarkArtifactPath}`);
|
|
4320
4433
|
}
|
|
4321
4434
|
return {
|
|
@@ -4368,6 +4481,10 @@ function create(deps = {}) {
|
|
|
4368
4481
|
for (const issue of issues.slice(0, 16)) ui.error(` - ${String(issue).slice(0, 400)}`);
|
|
4369
4482
|
if (issues.length > 16) ui.error(` … ${issues.length - 16} more issues in the persisted receipt`);
|
|
4370
4483
|
}
|
|
4484
|
+
// 실패한 실행이야말로 토큰이 어디로 갔는지 알아야 하는 순간이다. issues 유무와
|
|
4485
|
+
// 무관하게 낸다 — 첫 배선이 이 블록 안에 들어가는 바람에 issues 없는 실패에서는
|
|
4486
|
+
// 장부가 통째로 사라졌다.
|
|
4487
|
+
reportTokenLedger(ui);
|
|
4371
4488
|
}
|
|
4372
4489
|
return { ok: false, error: receipt.failure, receipt, benchmarkArtifactPath };
|
|
4373
4490
|
}
|