agentlas 1.0.47 → 1.0.49
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 +16 -0
- package/README.md +4 -2
- package/bin/agentlas.cjs +10 -0
- package/engine/acp/server.cjs +279 -0
- package/engine/agentlas-capabilities.cjs +2 -1
- package/engine/agentlas-input.cjs +2 -1
- package/engine/agentlas-native-host.cjs +6 -0
- package/engine/agentlas-onboard.cjs +2 -1
- package/engine/agentlas-sqlite-policy.cjs +9 -0
- package/engine/automation/daemon.cjs +3 -7
- package/engine/bootstrap-schema.sql +1037 -1010
- package/engine/cloud-assets/package.cjs +11 -3
- package/engine/cloud-assets/upload-scan-catalog.generated.cjs +102 -0
- package/engine/commands/acp.cjs +45 -0
- package/engine/commands/billing.cjs +2 -2
- package/engine/commands/call.cjs +4 -0
- package/engine/commands/doctor.cjs +2 -1
- package/engine/commands/index.cjs +2 -0
- package/engine/commands/workforce.cjs +11 -0
- package/engine/core/db.cjs +53 -1
- package/engine/core/desktop-core.cjs +108 -4
- package/engine/core/store-schema.cjs +119 -0
- package/engine/firms/orchestrate.cjs +32 -1
- package/engine/project/memory-context.cjs +7 -0
- package/engine/runtimes/acp-driver.cjs +96 -0
- package/engine/runtimes/detect.cjs +3 -13
- package/engine/runtimes/kinds.cjs +84 -0
- package/engine/runtimes/resolve.cjs +40 -7
- package/engine/ui/commands-catalog.cjs +2 -0
- package/engine/ui/palette.cjs +2 -1
- package/engine/ui/repl.cjs +2 -1
- package/engine/ui/shell.cjs +38 -3
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +4 -8
- package/engine/workforce/deps.cjs +7 -0
- package/package.json +2 -1
|
@@ -37,14 +37,22 @@ const {
|
|
|
37
37
|
normalizeCloudAssetDescriptor,
|
|
38
38
|
} = require("../hub/install.cjs");
|
|
39
39
|
const { SECRET_PATTERNS } = require("../agentlas-secret-patterns.cjs");
|
|
40
|
+
const {
|
|
41
|
+
SECRET_SCAN_TEXT_EXTENSIONS,
|
|
42
|
+
UPLOAD_AGENT_DEFINITION_FILES,
|
|
43
|
+
UPLOAD_SKIP_DIRECTORIES,
|
|
44
|
+
} = require("./upload-scan-catalog.generated.cjs");
|
|
40
45
|
const { userDataDir } = require("../core/paths.cjs");
|
|
41
46
|
const state = require("./state.cjs");
|
|
42
47
|
const { cargoSearchAgents } = require("./cargo.cjs");
|
|
43
48
|
const cas = require("./cas.cjs");
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
// 업로드/시크릿 스캔 어휘는 업로드 계약에서 온다. 세 제품이 각자 손으로 적어
|
|
51
|
+
// 두었고 이미 갈려 있었다 — .bat/.cmd/.jsx 는 데스크탑만 스캔해서 서버 스캔과
|
|
52
|
+
// 터미널 업로드가 그 파일들을 아예 열지 않았다.
|
|
53
|
+
const CLOUD_TEXT_EXTS = new Set(SECRET_SCAN_TEXT_EXTENSIONS);
|
|
54
|
+
const CLOUD_AGENT_FILES = new Set(UPLOAD_AGENT_DEFINITION_FILES);
|
|
55
|
+
const CLOUD_SKIP_DIRS = new Set(UPLOAD_SKIP_DIRECTORIES);
|
|
48
56
|
const CLOUD_BLOCKED_FILE_RE = [/^\.env(?:\..*)?$/i, /^id_rsa(?:\.pub)?$/i, /^credentials(?:\..*)?$/i, /^secrets?(?:\..*)?$/i, /^cloud-asset-state\.v1\.json$/i, /(?:^|[._-])service-account(?:[._-]|$)/i, /\.(?:key|pem|p12|pfx|mobileprovision)$/i];
|
|
49
57
|
const CLOUD_ROUTING_CARD_PATH = ".agentlas/routing-card.json";
|
|
50
58
|
const CLOUD_ROUTING_CARD_CAPABILITY_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// ⚠️ GENERATED FILE — do not hand-edit; the next generation erases your change.
|
|
2
|
+
// 정본: agentlas/AgentsAtlas/app/src/lib/agentlas-cloud/upload-scan-catalog.json
|
|
3
|
+
// 생성: (agentlas/AgentsAtlas/app) node scripts/gen-upload-scan-catalog.mjs
|
|
4
|
+
//
|
|
5
|
+
// Cloud-agent upload + secret-scan catalog. Three products used to restate
|
|
6
|
+
// this by hand and drifted; the server-side scan was the one that lost.
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const SECRET_SCAN_TEXT_EXTENSIONS = Object.freeze([
|
|
11
|
+
".bat",
|
|
12
|
+
".cfg",
|
|
13
|
+
".cjs",
|
|
14
|
+
".cmd",
|
|
15
|
+
".conf",
|
|
16
|
+
".config",
|
|
17
|
+
".css",
|
|
18
|
+
".csv",
|
|
19
|
+
".env",
|
|
20
|
+
".html",
|
|
21
|
+
".ini",
|
|
22
|
+
".js",
|
|
23
|
+
".json",
|
|
24
|
+
".jsonl",
|
|
25
|
+
".jsx",
|
|
26
|
+
".md",
|
|
27
|
+
".mjs",
|
|
28
|
+
".properties",
|
|
29
|
+
".ps1",
|
|
30
|
+
".psd1",
|
|
31
|
+
".psm1",
|
|
32
|
+
".py",
|
|
33
|
+
".sh",
|
|
34
|
+
".toml",
|
|
35
|
+
".ts",
|
|
36
|
+
".tsx",
|
|
37
|
+
".txt",
|
|
38
|
+
".xml",
|
|
39
|
+
".yaml",
|
|
40
|
+
".yml",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const UPLOAD_SKIP_DIRECTORIES = Object.freeze([
|
|
44
|
+
".git",
|
|
45
|
+
".next",
|
|
46
|
+
".studio-runtime",
|
|
47
|
+
".turbo",
|
|
48
|
+
"build",
|
|
49
|
+
"coverage",
|
|
50
|
+
"dist",
|
|
51
|
+
"node_modules",
|
|
52
|
+
"out",
|
|
53
|
+
"release",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const AGENT_DEFINITION_FILES = Object.freeze([
|
|
57
|
+
"AGENT.md",
|
|
58
|
+
"AGENTS.md",
|
|
59
|
+
"CLAUDE.md",
|
|
60
|
+
"GEMINI.md",
|
|
61
|
+
"README.md",
|
|
62
|
+
"agent.md",
|
|
63
|
+
"manifest.md",
|
|
64
|
+
"system-prompt.md",
|
|
65
|
+
"system.md",
|
|
66
|
+
"soul.md",
|
|
67
|
+
"prompt.md",
|
|
68
|
+
"persona.md",
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const UPLOAD_AGENT_DEFINITION_FILES = Object.freeze([
|
|
72
|
+
"AGENT.md",
|
|
73
|
+
"AGENTS.md",
|
|
74
|
+
"CLAUDE.md",
|
|
75
|
+
"GEMINI.md",
|
|
76
|
+
"README.md",
|
|
77
|
+
"agent.md",
|
|
78
|
+
"manifest.md",
|
|
79
|
+
"system-prompt.md",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const FOLDER_SCAN_AGENT_DEFINITION_FILES = Object.freeze([
|
|
83
|
+
"AGENT.md",
|
|
84
|
+
"AGENTS.md",
|
|
85
|
+
"CLAUDE.md",
|
|
86
|
+
"GEMINI.md",
|
|
87
|
+
"agent.md",
|
|
88
|
+
"manifest.md",
|
|
89
|
+
"system-prompt.md",
|
|
90
|
+
"system.md",
|
|
91
|
+
"soul.md",
|
|
92
|
+
"prompt.md",
|
|
93
|
+
"persona.md",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
SECRET_SCAN_TEXT_EXTENSIONS,
|
|
98
|
+
UPLOAD_SKIP_DIRECTORIES,
|
|
99
|
+
AGENT_DEFINITION_FILES,
|
|
100
|
+
UPLOAD_AGENT_DEFINITION_FILES,
|
|
101
|
+
FOLDER_SCAN_AGENT_DEFINITION_FILES,
|
|
102
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* acp — run Agentlas as an Agent Client Protocol agent on stdio (PRD 2026-08-15 B-3).
|
|
4
|
+
*
|
|
5
|
+
* agentlas acp start the ACP v1 agent server (stdin/stdout are the wire)
|
|
6
|
+
* agentlas acp --info print the registry-style descriptor and exit
|
|
7
|
+
*
|
|
8
|
+
* Register in an ACP client (Zed settings.json example):
|
|
9
|
+
* "agent_servers": { "Agentlas": { "command": "agentlas", "args": ["acp"] } }
|
|
10
|
+
* JetBrains / other clients: same command + args. The client then runs Agentlas'
|
|
11
|
+
* project controller on the runtime you subscribe to — no keys leave your machine.
|
|
12
|
+
*/
|
|
13
|
+
const { AcpAgentServer, PROTOCOL_VERSION } = require("../acp/server.cjs");
|
|
14
|
+
|
|
15
|
+
function descriptor() {
|
|
16
|
+
let version = "0.0.0";
|
|
17
|
+
try { version = require("../../package.json").version || version; } catch { /* keep */ }
|
|
18
|
+
return {
|
|
19
|
+
id: "agentlas",
|
|
20
|
+
name: "Agentlas",
|
|
21
|
+
version,
|
|
22
|
+
description: "Agentlas project controller over ACP — runs on the coding runtime you already subscribe to (Claude Code, Codex, Antigravity, ACP agents).",
|
|
23
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
24
|
+
distribution: { npm: { package: `agentlas@${version}`, args: ["acp"] } },
|
|
25
|
+
authMethods: [],
|
|
26
|
+
capabilities: { loadSession: false, promptCapabilities: { image: false, audio: false, embeddedContext: true } },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function run(ctx, args) {
|
|
31
|
+
if (args.includes("--info") || args.includes("--json")) {
|
|
32
|
+
ctx.out(JSON.stringify(descriptor(), null, 2));
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
if (args.includes("--help") || args.includes("help")) {
|
|
36
|
+
ctx.out("Usage: agentlas acp [--info]\n Speak the Agent Client Protocol (v1) on stdio so an editor can run Agentlas as its agent.");
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
// stdout is the protocol wire from here on: route everything human to stderr.
|
|
40
|
+
const server = new AcpAgentServer({ ctx, input: process.stdin, output: process.stdout });
|
|
41
|
+
await server.start();
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { run, descriptor };
|
|
@@ -26,8 +26,8 @@ function usage(ko) {
|
|
|
26
26
|
? " 구독 계좌(A)와 렌트수익 계좌(B) 잔액을 표시합니다."
|
|
27
27
|
: " Shows the subscription account (A) and rental-earnings account (B) balances.",
|
|
28
28
|
ko
|
|
29
|
-
? " 크레딧은 Hub 에이전트
|
|
30
|
-
: " Credits pay for Hub agent calls (public agent 3 · team 10;
|
|
29
|
+
? " 크레딧은 Hub 에이전트 호출에 작업당 쓰입니다(기본 공개 에이전트 3·팀 10, 크리에이터 가격이 있으면 그 가격). 활성 장기대여 중에는 0."
|
|
30
|
+
: " Credits pay per work order for Hub agent calls (base: public agent 3 · team 10; creator-priced agents charge their price). 0 while a day-lease is active.",
|
|
31
31
|
ko
|
|
32
32
|
? " 참고: 렌트수익(B) → 구독(A) 전송은 Agentlas Desktop 에서만 가능합니다 (터미널 전송 명령 없음)."
|
|
33
33
|
: " Note: earnings (B) → subscription (A) transfer is Desktop-only (no transfer command in the terminal).",
|
package/engine/commands/call.cjs
CHANGED
|
@@ -18,6 +18,10 @@ async function run(ctx, args) {
|
|
|
18
18
|
ctx.err("✖ " + usageFor("call", ctx.lang));
|
|
19
19
|
return 1;
|
|
20
20
|
}
|
|
21
|
+
// 과금 사전 고지 — 가격은 서버가 청구 시 확정하므로 숫자를 지어내지 않는다.
|
|
22
|
+
ctx.out(ctx.lang !== "en"
|
|
23
|
+
? "ℹ 공개 Hub 에이전트·팀 호출은 크레딧이 소모됩니다(활성 장기대여 중에는 0). 잔액 확인: agentlas billing"
|
|
24
|
+
: "ℹ Public Hub agent/team calls consume credits (0 while a day-lease is active). Check balance: agentlas billing");
|
|
21
25
|
return create(ctx).cmdHep(["hep-call", ...args]);
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -9,6 +9,7 @@ const fs = require("node:fs");
|
|
|
9
9
|
const path = require("node:path");
|
|
10
10
|
const { dbPath, userDataDir } = require("../core/paths.cjs");
|
|
11
11
|
const { listAvailableCliRuntimes, activeRuntimeRow } = require("../runtimes/detect.cjs");
|
|
12
|
+
const { RUNTIME_BIN } = require("../runtimes/kinds.cjs");
|
|
12
13
|
const { runtimeAuthEvidence } = require("../runtimes/auth-evidence.cjs");
|
|
13
14
|
const { sharedRuntimeKind } = require("../runtimes/resolve.cjs");
|
|
14
15
|
const { resolvedModelRole } = require("../runtimes/roles.cjs");
|
|
@@ -106,7 +107,7 @@ async function run(ctx, args = []) {
|
|
|
106
107
|
// 가 아니라 경고다. 흔적 없음 = 미로그인 "가능성"이므로 단정하지 않는다.
|
|
107
108
|
const evidence = runtimeAuthEvidence(activeKind);
|
|
108
109
|
if (evidence.status === "none") {
|
|
109
|
-
const bin =
|
|
110
|
+
const bin = RUNTIME_BIN[activeKind] || activeKind;
|
|
110
111
|
warn(
|
|
111
112
|
en ? "active runtime" : "활성 런타임",
|
|
112
113
|
en
|
|
@@ -34,6 +34,8 @@ const COMMANDS = {
|
|
|
34
34
|
plugin: () => require("./plugin.cjs"),
|
|
35
35
|
automation: () => require("./automation.cjs"),
|
|
36
36
|
native: () => require("./native.cjs"),
|
|
37
|
+
// Agentlas as an ACP agent for editors (Zed, JetBrains, …) — PRD 2026-08-15 B-3.
|
|
38
|
+
acp: () => require("./acp.cjs"),
|
|
37
39
|
multimodal: () => require("./multimodal.cjs"),
|
|
38
40
|
document: () => require("./document.cjs"),
|
|
39
41
|
workforce: () => require("./workforce.cjs"),
|
|
@@ -75,6 +75,11 @@ async function dispatch(ctx, command, args) {
|
|
|
75
75
|
permission,
|
|
76
76
|
`terminal-${command}`,
|
|
77
77
|
) || cwd;
|
|
78
|
+
// 과금 사전 고지 — 이 표면은 서버가 청구 시 확정하는 가격을 미리 모르므로
|
|
79
|
+
// 숫자를 지어내지 않고 사실만 말한다(공개 Hub 호출=크레딧 소모, 장기대여=0).
|
|
80
|
+
ctx.out(ctx.lang !== "en"
|
|
81
|
+
? "ℹ 공개 Hub 에이전트·팀 호출은 크레딧이 소모됩니다(활성 장기대여 중에는 0). 잔액 확인: agentlas billing"
|
|
82
|
+
: "ℹ Public Hub agent/team calls consume credits (0 while a day-lease is active). Check balance: agentlas billing");
|
|
78
83
|
const runtime = workforceRuntime({ lang: ctx.lang, out: ctx.out, uiInstance: ctx.uiInstance });
|
|
79
84
|
const result = await runtime.cmdWorkforce(db, rest, runtimeOverride, {
|
|
80
85
|
cwd,
|
|
@@ -124,6 +129,12 @@ async function dispatch(ctx, command, args) {
|
|
|
124
129
|
const cwd = projectCwd();
|
|
125
130
|
const permission = resolvePermission(ctx);
|
|
126
131
|
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, permission, `terminal-${command}`) || cwd;
|
|
132
|
+
// 과금 사전 고지 — local 스코프는 원격 과금이 없다.
|
|
133
|
+
if (sourceScope !== "local") {
|
|
134
|
+
ctx.out(ko
|
|
135
|
+
? "ℹ 공개 Hub 에이전트·팀 호출은 크레딧이 소모됩니다(활성 장기대여 중에는 0). 잔액 확인: agentlas billing"
|
|
136
|
+
: "ℹ Public Hub agent/team calls consume credits (0 while a day-lease is active). Check balance: agentlas billing");
|
|
137
|
+
}
|
|
127
138
|
const { createLocalCoreHubTool } = require("../workforce/local-core-transport.cjs");
|
|
128
139
|
const { createLocalCoreWorkforceRuntime } = require("../workforce/deps.cjs");
|
|
129
140
|
const transport = createLocalCoreHubTool({ sourceScope, projectDir: projectPath, cwd });
|
package/engine/core/db.cjs
CHANGED
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
* 스키마 소유권: 정본 스키마는 데스크탑 앱. 터미널은 bootstrap-schema.sql로
|
|
8
8
|
* 첫 부트스트랩만 하고(런처가 수행), 이후 마이그레이션은 앱이 한다.
|
|
9
9
|
* 터미널은 "있으면 쓰는" 방어적 열 확인(columnExists)으로만 전진 호환한다.
|
|
10
|
+
*
|
|
11
|
+
* ★2026-08-18: 위 문단은 계약이었지 배선이 아니었다. 이 파일은 user_version 을 **한 번도
|
|
12
|
+
* 읽지 않으면서** seedBuiltins 로 BEGIN IMMEDIATE 쓰기 트랜잭션을 열었다 — 검사도 거절도
|
|
13
|
+
* 없이 그냥 진행. 이제 openDb() 가 core/store-schema.cjs 로 버전을 확인하고, 낮으면
|
|
14
|
+
* 정직하게 거절한다(승급은 여전히 데스크탑의 일이다).
|
|
10
15
|
*/
|
|
11
16
|
const fs = require("node:fs");
|
|
12
17
|
const { configureSqliteConnection } = require("../agentlas-sqlite-policy.cjs");
|
|
13
18
|
const { parseSemVer, compareSemVer } = require("../semver.cjs");
|
|
14
19
|
const { dbPath } = require("./paths.cjs");
|
|
20
|
+
const { assertStoreSchemaCompatible } = require("./store-schema.cjs");
|
|
15
21
|
|
|
16
22
|
function loadNodeSqliteQuietly() {
|
|
17
23
|
const originalEmitWarning = process.emitWarning;
|
|
@@ -44,16 +50,61 @@ function openRaw(file) {
|
|
|
44
50
|
return db;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 이 프로세스가 마이그레이션 권위인가. 기본은 **아니다**(follower).
|
|
55
|
+
* `AGENTLAS_STORE_MIGRATION_ROLE=owner` 는 데스크탑이 없는 머신을 위한 명시적 탈출구다 —
|
|
56
|
+
* 사람이 일부러 켜야 하고, 그때만 벤더 코어의 사다리가 한 번 돈다.
|
|
57
|
+
*/
|
|
58
|
+
function storeMigrationRole() {
|
|
59
|
+
return String(process.env.AGENTLAS_STORE_MIGRATION_ROLE || "").trim().toLowerCase() === "owner"
|
|
60
|
+
? "owner"
|
|
61
|
+
: "follower";
|
|
62
|
+
}
|
|
63
|
+
|
|
47
64
|
/**
|
|
48
65
|
* DB 파일이 없으면 하드 실패한다. 부트스트랩은 런처(bin/agentlas.cjs)의 책임 —
|
|
49
66
|
* 엔진이 임의 경로에 빈 DB를 만들면 데스크탑과의 공유 계약이 조용히 깨진다.
|
|
67
|
+
*
|
|
68
|
+
* 연 다음에는 스키마 버전을 **확인**한다. 낮으면 거절 — 조용히 진행하지 않는다.
|
|
69
|
+
* owner 로 명시 지정된 경우에만, 거절 대신 벤더 코어의 사다리를 한 번 돌려 승급한다.
|
|
50
70
|
*/
|
|
51
71
|
function openDb() {
|
|
52
72
|
const file = dbPath();
|
|
53
73
|
if (!fs.existsSync(file)) {
|
|
54
74
|
throw new Error(`Agentlas database not found: ${file} (run via bin/agentlas.cjs)`);
|
|
55
75
|
}
|
|
56
|
-
|
|
76
|
+
if (storeMigrationRole() === "owner") migrateSharedStoreAsOwner(file);
|
|
77
|
+
const db = openRaw(file);
|
|
78
|
+
try {
|
|
79
|
+
assertStoreSchemaCompatible(db, file);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
try { db.close(); } catch { /* noop */ }
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
return db;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 명시적 탈출구의 실행부. 데스크탑이 없는 머신에서 사람이 일부러 켰을 때만 불린다.
|
|
89
|
+
* 승급 자체는 여전히 **데스크탑 사다리 코드 하나**가 한다 — 터미널은 그 사다리를 손으로
|
|
90
|
+
* 복제하지 않는다(두 번째 사다리 구현이 곧 세 번째 주인이다). 코어가 없으면 정직한 실패.
|
|
91
|
+
*/
|
|
92
|
+
function migrateSharedStoreAsOwner(file) {
|
|
93
|
+
let core;
|
|
94
|
+
try {
|
|
95
|
+
core = require("./desktop-core.cjs").loadDesktopCore({ migrationRole: "owner" });
|
|
96
|
+
} catch (e) {
|
|
97
|
+
throw new Error(`AGENTLAS_STORE_MIGRATION_ROLE=owner: could not load the Desktop core to migrate ${file}: ${e && e.message ? e.message : e}`);
|
|
98
|
+
}
|
|
99
|
+
if (!core) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`AGENTLAS_STORE_MIGRATION_ROLE=owner: no Desktop core is available on this machine, so ${file} cannot be migrated here.\n` +
|
|
102
|
+
"Run `agentlas doctor`, or launch the Agentlas Desktop app once.",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (core.error) {
|
|
106
|
+
throw new Error(`AGENTLAS_STORE_MIGRATION_ROLE=owner: Desktop core failed to migrate ${file}: ${core.error.message}`);
|
|
107
|
+
}
|
|
57
108
|
}
|
|
58
109
|
|
|
59
110
|
function tableExists(db, name) {
|
|
@@ -151,6 +202,7 @@ function seedBuiltins(db) {
|
|
|
151
202
|
module.exports = {
|
|
152
203
|
openDb,
|
|
153
204
|
openRaw,
|
|
205
|
+
storeMigrationRole,
|
|
154
206
|
tableExists,
|
|
155
207
|
columnExists,
|
|
156
208
|
runWriteTransaction,
|
|
@@ -141,6 +141,25 @@ function installNativeModuleHook() {
|
|
|
141
141
|
_nativeHookInstalled = true;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 코어의 initStore 를 부르기 전에, 공유 저장소의 user_version 을 **우리 드라이버로** 확인한다.
|
|
146
|
+
* 별도 커넥션을 잠깐 열고 닫을 뿐 아무것도 쓰지 않는다. 파일이 아직 없으면(첫 실행 부트스트랩
|
|
147
|
+
* 이전) 판단할 것이 없으므로 통과 — 코어가 만들게 둔다.
|
|
148
|
+
*/
|
|
149
|
+
function assertSharedStoreSchemaBeforeCoreInit() {
|
|
150
|
+
const file = process.env.AGENTLAS_STORE_PATH || dbPath();
|
|
151
|
+
if (!fs.existsSync(file)) return;
|
|
152
|
+
const { openRaw } = require("./db.cjs");
|
|
153
|
+
const { assertStoreSchemaCompatible } = require("./store-schema.cjs");
|
|
154
|
+
let probe = null;
|
|
155
|
+
try {
|
|
156
|
+
probe = openRaw(file);
|
|
157
|
+
assertStoreSchemaCompatible(probe, file);
|
|
158
|
+
} finally {
|
|
159
|
+
try { probe && probe.close(); } catch { /* noop */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
144
163
|
let _cache = undefined;
|
|
145
164
|
|
|
146
165
|
/**
|
|
@@ -148,8 +167,22 @@ let _cache = undefined;
|
|
|
148
167
|
* · require(rel): 코어 안의 임의 컴파일 모듈을 상대경로로 로드(예: "store/automations").
|
|
149
168
|
* · runGraph: electron/workflow/run-graph.js 의 runGraph(automation, graph, opts).
|
|
150
169
|
* 공유 DB(AGENTLAS_STORE_PATH)를 코어 로드 전에 반드시 세팅한다 — store/db 가 모듈 로드 시 읽는다.
|
|
170
|
+
*
|
|
171
|
+
* ★마이그레이션 권위 (Phase 0, docs/DAEMON-ARCHITECTURE-DESIGN-2026-08-18.md §2/§6).
|
|
172
|
+
*
|
|
173
|
+
* 여기가 **두 번째 마이그레이션 주인**이었다. 이 셰임은 isPackaged:true 를 보고해
|
|
174
|
+
* (설계상) db.ts 의 개발 샌드박스 가드를 무력화하고, 공유 라이브 파일에 데스크탑과
|
|
175
|
+
* 같은 사다리를 돌렸다. 락이 없는 파일에서 두 프로세스가 사다리를 겹쳐 도는 것이
|
|
176
|
+
* 정확히 run_events + 인덱스 4개를 malformed 로 만든 경로다.
|
|
177
|
+
*
|
|
178
|
+
* **선택: (a) 사다리를 돌리지 않는다.** initStore 를 follower 로 부른다 — 코어는 열고
|
|
179
|
+
* 확인만 하며, 낮으면 정직하게 거절한다. (b)("데스크탑이 없으면 터미널이 주인")를 고르지
|
|
180
|
+
* 않은 이유: 데스크탑의 부재는 경합 없이 관측할 수 없다. 터미널이 사다리를 도는 중에
|
|
181
|
+
* 데스크탑이 켜질 수 있고, 틀렸을 때의 비용이 비대칭이다(손상 vs 앱 한 번 실행).
|
|
182
|
+
* 데스크탑이 정말 없는 머신은 `AGENTLAS_STORE_MIGRATION_ROLE=owner` 로 **사람이 일부러**
|
|
183
|
+
* 한 번 켠다 — 사고로 되는 일과 적어서 되는 일은 달라야 한다.
|
|
151
184
|
*/
|
|
152
|
-
function loadDesktopCore() {
|
|
185
|
+
function loadDesktopCore(options = {}) {
|
|
153
186
|
if (_cache !== undefined) return _cache;
|
|
154
187
|
const root = findCoreRoot();
|
|
155
188
|
if (!root) { _cache = null; return null; }
|
|
@@ -157,13 +190,30 @@ function loadDesktopCore() {
|
|
|
157
190
|
installNativeModuleHook();
|
|
158
191
|
// 코어의 store 가 이 값을 모듈 로드 시점에 읽는다 — require 이전에 세팅해야 한다.
|
|
159
192
|
if (!process.env.AGENTLAS_STORE_PATH) process.env.AGENTLAS_STORE_PATH = dbPath();
|
|
193
|
+
const migrationRole = options.migrationRole
|
|
194
|
+
|| (String(process.env.AGENTLAS_STORE_MIGRATION_ROLE || "").trim().toLowerCase() === "owner"
|
|
195
|
+
? "owner"
|
|
196
|
+
: "follower");
|
|
160
197
|
const req = (rel) => require(path.join(root, "electron", rel.replace(/\.js$/, "") + ".js"));
|
|
161
198
|
let kernel;
|
|
162
199
|
try {
|
|
200
|
+
const store = req("store/db");
|
|
201
|
+
if (migrationRole === "follower") {
|
|
202
|
+
// 옛 벤더 번들은 이 계약을 모른다(옵션·env 를 무시하고 사다리를 돈다). 조용히 넘어가지
|
|
203
|
+
// 않고 무엇이 위험한지 말한다 — `npm run vendor:core` 로 갱신하면 사라진다.
|
|
204
|
+
if (typeof store.STORE_SCHEMA_VERSION !== "number") {
|
|
205
|
+
console.warn(
|
|
206
|
+
"[store] vendored Desktop core predates the single-migration-authority contract "
|
|
207
|
+
+ "(no STORE_SCHEMA_VERSION export); it may migrate the shared database. "
|
|
208
|
+
+ "Refresh it with `npm run vendor:core`.",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
// 코어를 부르기 **전에** 우리 쪽에서 먼저 확인한다 — 옛 코어가 옵션을 무시해도
|
|
212
|
+
// 최소한 낮은 스키마에서는 사다리에 닿지 않는다.
|
|
213
|
+
assertSharedStoreSchemaBeforeCoreInit();
|
|
214
|
+
}
|
|
163
215
|
// 데스크탑은 app.whenReady 에서 initStore() 를 부른다 — 터미널도 코어를 쓰기 전에 부른다.
|
|
164
|
-
|
|
165
|
-
// 언제나 코어가 기대하는 모양이 된다. 이것이 "재구현"이 아니라 "재사용"의 핵심.
|
|
166
|
-
req("store/db").initStore();
|
|
216
|
+
store.initStore({ migrationRole });
|
|
167
217
|
kernel = req("workflow/run-graph");
|
|
168
218
|
} catch (e) { _cache = { root, error: e }; return _cache; }
|
|
169
219
|
_cache = {
|
|
@@ -178,6 +228,58 @@ function loadDesktopCore() {
|
|
|
178
228
|
return _cache;
|
|
179
229
|
}
|
|
180
230
|
|
|
231
|
+
/**
|
|
232
|
+
* 코어의 공용 ACP 러너(electron/runtime/acp.js)만 가볍게 로드한다 (PRD 2026-08-15 T-2).
|
|
233
|
+
* initStore·그래프 커널을 끌지 않고, electron 셰임과 네이티브 모듈 훅만 건 뒤 require 한다 —
|
|
234
|
+
* kimi/grok/cursor 실행에 DB가 필요 없기 때문. 코어가 없거나 acp.js가 없는 옛 코어면
|
|
235
|
+
* { error } 를 준다(정직한 부재; 조용한 폴백 금지).
|
|
236
|
+
*/
|
|
237
|
+
let _acpCache = undefined;
|
|
238
|
+
function loadCoreAcpRuntime() {
|
|
239
|
+
if (_acpCache !== undefined) return _acpCache;
|
|
240
|
+
const root = findCoreRoot();
|
|
241
|
+
if (!root) { _acpCache = null; return null; }
|
|
242
|
+
const file = path.join(root, "electron", "runtime", "acp.js");
|
|
243
|
+
if (!fs.existsSync(file)) { _acpCache = { root, error: new Error("desktop core predates the ACP runner (no electron/runtime/acp.js)") }; return _acpCache; }
|
|
244
|
+
try {
|
|
245
|
+
installRetiredProjectProvisioningHook();
|
|
246
|
+
installNativeModuleHook();
|
|
247
|
+
const mod = require(file);
|
|
248
|
+
_acpCache = { root, module: mod };
|
|
249
|
+
} catch (e) {
|
|
250
|
+
_acpCache = { root, error: e };
|
|
251
|
+
}
|
|
252
|
+
return _acpCache;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 코어의 **공유 순수 모듈**(dist/shared/*)만 가볍게 로드한다 (예: "agent-control-blocks").
|
|
257
|
+
* shared/* 는 electron·DB 의존이 없는 순수 함수 모듈이라 initStore·그래프 커널·셰임 없이
|
|
258
|
+
* require 만 한다. 코어가 없으면 null, 그 모듈이 없는 옛 벤더 번들이면 { root, error } —
|
|
259
|
+
* 정직한 부재(조용한 폴백 금지는 호출부 계약; 표시 경로는 fail-open 해도 된다).
|
|
260
|
+
*/
|
|
261
|
+
const _sharedCache = new Map();
|
|
262
|
+
function loadCoreShared(rel) {
|
|
263
|
+
const key = String(rel || "").replace(/\.js$/, "");
|
|
264
|
+
if (_sharedCache.has(key)) return _sharedCache.get(key);
|
|
265
|
+
let result = null;
|
|
266
|
+
const root = findCoreRoot();
|
|
267
|
+
if (root) {
|
|
268
|
+
const file = path.join(root, "shared", key + ".js");
|
|
269
|
+
if (!fs.existsSync(file)) {
|
|
270
|
+
result = { root, error: new Error(`desktop core has no shared/${key}.js (older vendor bundle)`) };
|
|
271
|
+
} else {
|
|
272
|
+
try {
|
|
273
|
+
result = { root, module: require(file) };
|
|
274
|
+
} catch (e) {
|
|
275
|
+
result = { root, error: e };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
_sharedCache.set(key, result);
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
181
283
|
/** 재사용 코어가 이 머신에서 가용한가(정직한 가부). */
|
|
182
284
|
function desktopCoreAvailable() {
|
|
183
285
|
const c = loadDesktopCore();
|
|
@@ -202,6 +304,8 @@ async function loadDesktopCoreAsync({ onNotice } = {}) {
|
|
|
202
304
|
module.exports = {
|
|
203
305
|
findCoreRoot,
|
|
204
306
|
loadDesktopCore,
|
|
307
|
+
loadCoreAcpRuntime,
|
|
308
|
+
loadCoreShared,
|
|
205
309
|
loadDesktopCoreAsync,
|
|
206
310
|
desktopCoreAvailable,
|
|
207
311
|
_test: { stripRetiredProjectProvisioningSource },
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* core/store-schema — 공유 저장소의 **스키마 버전 계약**. 터미널은 읽기만 한다.
|
|
4
|
+
*
|
|
5
|
+
* ★단일 마이그레이션 권위 (Phase 0, docs/DAEMON-ARCHITECTURE-DESIGN-2026-08-18.md §2/§6).
|
|
6
|
+
*
|
|
7
|
+
* `~/Library/Application Support/Agentlas/agentlas.sqlite` 는 락 없는 다중 쓰기 파일인데
|
|
8
|
+
* 마이그레이션 주인이 **둘**이었다:
|
|
9
|
+
* 1) 데스크탑의 사다리(electron/store/db.ts SCHEMA_VERSION),
|
|
10
|
+
* 2) 그 사다리를 그대로 다시 도는 터미널의 벤더 코어 경로(core/desktop-core.cjs initStore).
|
|
11
|
+
* 그리고 터미널의 가벼운 드라이버(core/db.cjs)는 user_version 을 **읽지도 않으면서** 쓰기
|
|
12
|
+
* 트랜잭션(seedBuiltins → BEGIN IMMEDIATE)을 열었다. 즉 "검사 없음 + 거절 없음 + 쓰기 있음".
|
|
13
|
+
*
|
|
14
|
+
* 이제 규칙은 하나다: **터미널은 절대 승급하지 않는다.** 파일이 이 배포가 아는 버전보다
|
|
15
|
+
* 낮으면 조용히 진행하지도, 몰래 마이그레이션하지도 않고 **정직하게 거절**한다.
|
|
16
|
+
*
|
|
17
|
+
* 왜 "없으면 터미널이 주인" 이 아닌가: 데스크탑의 부재는 경합 없이 관측할 수 없다 —
|
|
18
|
+
* 터미널이 사다리를 도는 도중에 데스크탑이 켜질 수 있다. 잘못 추측한 비용은 117MB 저장소
|
|
19
|
+
* 손상(db.ts 의 run_events 사고 주석)이고, 거절의 비용은 데스크탑 한 번 실행이다.
|
|
20
|
+
*
|
|
21
|
+
* 기대 버전의 출처: 이 패키지가 함께 배포하는 engine/bootstrap-schema.sql 의
|
|
22
|
+
* `PRAGMA user_version=` 이 곧 "이 배포가 아는 사다리 머리"다. 그 파일은 데스크탑 사다리를
|
|
23
|
+
* 빈 DB 에 끝까지 돌려 생성한 것이므로(scripts/gen-bootstrap-schema.cjs) 손으로 맞출 숫자가
|
|
24
|
+
* 따로 없다 — 재생성하면 자동으로 같이 움직인다.
|
|
25
|
+
*/
|
|
26
|
+
const fs = require("node:fs");
|
|
27
|
+
const path = require("node:path");
|
|
28
|
+
|
|
29
|
+
const BOOTSTRAP_SCHEMA_FILE = path.join(path.dirname(__dirname), "bootstrap-schema.sql");
|
|
30
|
+
|
|
31
|
+
let _expected;
|
|
32
|
+
|
|
33
|
+
/** 이 배포가 아는 스키마 버전. 부트스트랩 SQL 헤더에서 읽는다(정본은 데스크탑 사다리). */
|
|
34
|
+
function expectedStoreSchemaVersion() {
|
|
35
|
+
if (_expected !== undefined) return _expected;
|
|
36
|
+
let header = "";
|
|
37
|
+
try {
|
|
38
|
+
const fd = fs.openSync(BOOTSTRAP_SCHEMA_FILE, "r");
|
|
39
|
+
try {
|
|
40
|
+
const buf = Buffer.alloc(4096);
|
|
41
|
+
const read = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
42
|
+
header = buf.slice(0, read).toString("utf8");
|
|
43
|
+
} finally {
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
header = "";
|
|
48
|
+
}
|
|
49
|
+
const match = /PRAGMA\s+user_version\s*=\s*(\d+)/i.exec(header);
|
|
50
|
+
// 부트스트랩 SQL 을 못 읽으면 기대 버전을 **모른다**. 0 은 "검사 불가"를 뜻하고,
|
|
51
|
+
// 아래 단언은 그때 통과시킨다 — 모르는 것을 근거로 사용자를 막지 않는다.
|
|
52
|
+
_expected = match ? Number(match[1]) : 0;
|
|
53
|
+
return _expected;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 열린 커넥션의 user_version. 못 읽으면 null(모름). */
|
|
57
|
+
function readStoreSchemaVersion(db) {
|
|
58
|
+
try {
|
|
59
|
+
if (typeof db.pragma === "function") {
|
|
60
|
+
const value = db.pragma("user_version", { simple: true });
|
|
61
|
+
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
62
|
+
}
|
|
63
|
+
const row = db.prepare("PRAGMA user_version").get();
|
|
64
|
+
if (!row) return null;
|
|
65
|
+
const value = row.user_version ?? Object.values(row)[0];
|
|
66
|
+
return Number.isFinite(Number(value)) ? Number(value) : null;
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 정직하고 실행 가능한 거절문. 두 가지 해결책을 모두 이름으로 말한다. */
|
|
73
|
+
function storeSchemaRefusalMessage(found, expected, file) {
|
|
74
|
+
return [
|
|
75
|
+
`Agentlas store schema is v${found}, but this Agentlas CLI needs v${expected}.`,
|
|
76
|
+
`Store: ${file}`,
|
|
77
|
+
"The Agentlas CLI never migrates the shared database — the Desktop app owns the migration ladder,",
|
|
78
|
+
"and a second migrator on this lock-free file is how the store was corrupted before.",
|
|
79
|
+
"",
|
|
80
|
+
"Fix it once, either way:",
|
|
81
|
+
" • Launch (or update) the Agentlas Desktop app once, then re-run this command.",
|
|
82
|
+
" • No Desktop app on this machine? Close every Agentlas process, then run this command once with",
|
|
83
|
+
" AGENTLAS_STORE_MIGRATION_ROLE=owner set, so the upgrade is a deliberate act rather than a race.",
|
|
84
|
+
].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class StoreSchemaTooOldError extends Error {
|
|
88
|
+
constructor(found, expected, file) {
|
|
89
|
+
super(storeSchemaRefusalMessage(found, expected, file));
|
|
90
|
+
this.name = "StoreSchemaTooOldError";
|
|
91
|
+
this.code = "AGENTLAS_STORE_SCHEMA_TOO_OLD";
|
|
92
|
+
this.found = found;
|
|
93
|
+
this.expected = expected;
|
|
94
|
+
this.file = file;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 공유 저장소를 이 프로세스가 써도 되는가.
|
|
100
|
+
* 낮으면 던진다. 같거나 높으면 통과 — 더 높은 것은 데스크탑이 앞서간 것이고, 터미널은
|
|
101
|
+
* columnExists 기반 방어적 읽기로 전진 호환한다(기존 계약 유지).
|
|
102
|
+
*/
|
|
103
|
+
function assertStoreSchemaCompatible(db, file) {
|
|
104
|
+
const expected = expectedStoreSchemaVersion();
|
|
105
|
+
if (!expected) return; // 기대 버전을 모르면 막지 않는다.
|
|
106
|
+
const found = readStoreSchemaVersion(db);
|
|
107
|
+
if (found === null) return; // 읽을 수 없으면 판단하지 않는다(가짜 거절 금지).
|
|
108
|
+
if (found >= expected) return;
|
|
109
|
+
throw new StoreSchemaTooOldError(found, expected, file);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
BOOTSTRAP_SCHEMA_FILE,
|
|
114
|
+
StoreSchemaTooOldError,
|
|
115
|
+
assertStoreSchemaCompatible,
|
|
116
|
+
expectedStoreSchemaVersion,
|
|
117
|
+
readStoreSchemaVersion,
|
|
118
|
+
storeSchemaRefusalMessage,
|
|
119
|
+
};
|
|
@@ -68,6 +68,26 @@ function loadDelegateParser() {
|
|
|
68
68
|
return parseDelegationsLocal;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/*
|
|
72
|
+
* 제어 블록 스트리퍼 정본 — 벤더 코어의 shared/agent-control-blocks
|
|
73
|
+
* (Desktop·Mobile 과 같은 규칙). 옛 벤더 번들이라 없으면 null — cleanFenceText 는
|
|
74
|
+
* 종전 규칙만으로 fail-open 한다(원문 파괴보다 마커 잔존이 낫다).
|
|
75
|
+
*/
|
|
76
|
+
let _stripCanonical; // undefined=미시도 · null=정본 없음 · function=정본
|
|
77
|
+
function loadCanonicalStripper() {
|
|
78
|
+
if (_stripCanonical === undefined) {
|
|
79
|
+
try {
|
|
80
|
+
const loaded = require("../core/desktop-core.cjs").loadCoreShared("agent-control-blocks");
|
|
81
|
+
_stripCanonical = loaded && loaded.module && typeof loaded.module.stripAgentControlBlocks === "function"
|
|
82
|
+
? loaded.module.stripAgentControlBlocks
|
|
83
|
+
: null;
|
|
84
|
+
} catch {
|
|
85
|
+
_stripCanonical = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return _stripCanonical;
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
/** 표시/전달용 텍스트에서 제어 펜스를 제거한다(파싱만 — 부작용 없음). 실패 시 원문. */
|
|
72
92
|
function cleanFenceText(text) {
|
|
73
93
|
const raw = String(text || "");
|
|
@@ -79,8 +99,18 @@ function cleanFenceText(text) {
|
|
|
79
99
|
}
|
|
80
100
|
} catch { /* fences 미존재/파서 실패 — 원문 보존 */ }
|
|
81
101
|
if (cleaned == null) cleaned = parseDelegationsLocal(raw).cleanedText;
|
|
102
|
+
// HTML 주석 봉투는 정본보다 먼저 — 정본이 헤딩만 도려내면 주석 껍데기가 남는다.
|
|
103
|
+
cleaned = cleaned.replace(/<!--\s*[\s\S]*?## Memory Events[\s\S]*?-->/gi, "");
|
|
104
|
+
// 정본 스트리퍼(settled 모드): 손코딩이 몰랐던 <<agentlas-ask>>·surface·followups·
|
|
105
|
+
// goal-complete 마커와 잔여 헤딩까지 Desktop 과 같은 규칙으로 지운다.
|
|
106
|
+
const strip = loadCanonicalStripper();
|
|
107
|
+
if (strip) {
|
|
108
|
+
try {
|
|
109
|
+
cleaned = strip(cleaned, { streaming: false });
|
|
110
|
+
} catch { /* 정본 실패 — 종전 규칙만으로 fail-open */ }
|
|
111
|
+
}
|
|
112
|
+
// 터미널 고유 정제(정본 범위 밖): 스킬 나레이션·오케스트레이터 헤더·판정 태그.
|
|
82
113
|
return cleaned
|
|
83
|
-
.replace(/<!--\s*[\s\S]*?## Memory Events[\s\S]*?-->/gi, "")
|
|
84
114
|
.replace(/^\s*(?:사용 스킬|Skills used)\s*:[^\n.!?]*[.!?]?\s*(?:(?:이유|Reason)\s*:[^.!?]*[.!?]\s*)?/i, "")
|
|
85
115
|
.replace(/^\s*I(?:'|’)m using (?:the )?`?[^`.\n]+`? skill because [^.]*\.\s*/i, "")
|
|
86
116
|
.replace(/^\s*Execution mode:\s*`?appbridge-ceo-orchestrator`?[^\n]*\n?/gim, "")
|
|
@@ -485,6 +515,7 @@ async function runFirmTurn(p) {
|
|
|
485
515
|
module.exports = {
|
|
486
516
|
runFirmTurn,
|
|
487
517
|
parseDelegationsLocal,
|
|
518
|
+
cleanFenceText,
|
|
488
519
|
buildDelegateProtocol,
|
|
489
520
|
matchTargets,
|
|
490
521
|
resolveDivisions,
|