agentlas 0.9.10 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/README.md +319 -341
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +472 -58
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12367
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +2 -6
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ /*
3
+ * project/seed — .agentlas/ 비공개 프로젝트 상태 시드 (v1 ensureProjectMemoryCli 포팅).
4
+ *
5
+ * v1 monolith 4184–7652에서 ~3,400줄이 super-ontology JSON 문서 리터럴 25개였다.
6
+ * 그 문서들은 engine/project/super-ontology-seed.json 데이터 파일로 추출했고
7
+ * (바이트 동일 — projectId만 치환 자리), 이 모듈은 그 목록을 순회만 한다.
8
+ *
9
+ * 경계(0.9.10): 이 함수는 아무 명령에서나 자동으로 불리지 않는다.
10
+ * `agentlas project init` 경로(state.cjs의 ensureCoreProjectCli 폴백)만 호출한다.
11
+ */
12
+ const fs = require("node:fs");
13
+ const path = require("node:path");
14
+ const { loadArch } = require("../core/db.cjs");
15
+ const {
16
+ ensureLocalCredentialStoreCli,
17
+ ensureSoulCredentialIndexCli,
18
+ } = require("./credentials.cjs");
19
+
20
+ // 추출된 super-ontology 시드 문서 (v1과 동일한 기록 순서를 배열 순서로 보존).
21
+ const SUPER_ONTOLOGY_SEED = require("./super-ontology-seed.json");
22
+
23
+ /** projectId 자리만 치환해 v1과 바이트 동일한 문서를 만든다 (키 순서 보존). */
24
+ function superOntologyDocumentFor(entry, projectName) {
25
+ const doc = JSON.parse(JSON.stringify(entry.doc));
26
+ doc.projectId = projectName;
27
+ return doc;
28
+ }
29
+
30
+ function ensureProjectMemoryCli(projectPath, projectName) {
31
+ const arch = loadArch();
32
+ try {
33
+ const dir = path.join(projectPath, arch.memoryDir || ".agentlas");
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ const name = projectName || path.basename(projectPath) || "Project";
36
+ ensureLocalCredentialStoreCli(projectPath, name, arch);
37
+ ensureSoulCredentialIndexCli(projectPath, name, arch);
38
+ const sitemap = path.join(dir, arch.sitemapFile || "sitemap.json");
39
+ if (!fs.existsSync(sitemap)) {
40
+ const now = new Date().toISOString();
41
+ fs.writeFileSync(sitemap, JSON.stringify({ project: name, created_at: now, updated_at: now, nodes: [] }, null, 2), "utf8");
42
+ }
43
+ const skillRegistryFile = arch.skillRegistryFile || "skill-registry.json";
44
+ const skillTrialsFile = arch.skillTrialsFile || "skill-trials.jsonl";
45
+ const curatorDecisionsFile = arch.curatorDecisionsFile || "curator-decisions.jsonl";
46
+ const ontologyRuntimeFile = arch.ontologyRuntimeFile || "ontology-runtime.json";
47
+ const ontologySourceManifestFile = arch.ontologySourceManifestFile || "ontology-sources.json";
48
+ const ontologyInboxDir = arch.ontologyInboxDir || "ontology-inbox";
49
+ const ontologyDbFile = arch.ontologyDbFile || "ontology-runtime.sqlite";
50
+ const careerGraphConfigFile = arch.careerGraphConfigFile || "career-graph.json";
51
+ const careerGraphSourceManifestFile = arch.careerGraphSourceManifestFile || "career-graph-sources.json";
52
+ const careerGraphInboxDir = arch.careerGraphInboxDir || "career-graph-inbox";
53
+ const careerGraphDbFile = arch.careerGraphDbFile || "career-graph.sqlite";
54
+ const superOntologyReplaysFile = arch.superOntologyReplaysFile || "super-ontology-replays.jsonl";
55
+ const superOntologyEvidenceFile = arch.superOntologyEvidenceFile || "super-ontology-evidence.jsonl";
56
+ const superOntologyMemoryBridgeFile = arch.superOntologyMemoryBridgeFile || "super-ontology-memory-bridge.jsonl";
57
+ const skillRegistry = path.join(dir, skillRegistryFile);
58
+ if (!fs.existsSync(skillRegistry)) {
59
+ fs.writeFileSync(skillRegistry, JSON.stringify({
60
+ schemaVersion: "1.0",
61
+ kind: "agentlas-skill-lifecycle-registry",
62
+ state: "local_candidate",
63
+ projectId: name,
64
+ draftId: null,
65
+ defaultTier: "candidate",
66
+ runtimeFirstClassRecallEnabled: false,
67
+ predicatesRequired: true,
68
+ curatorQuarantineRequired: true,
69
+ evidenceLedgers: {
70
+ trials: `.agentlas/${skillTrialsFile}`,
71
+ curatorDecisions: `.agentlas/${curatorDecisionsFile}`,
72
+ memoryEvents: `.agentlas/${arch.logFile || "memory-log.jsonl"}`,
73
+ },
74
+ hardStops: [
75
+ "permission_change",
76
+ "credential_change",
77
+ "payment_or_billing_effect",
78
+ "regulated_or_irreversible_side_effect",
79
+ "same_authority_patch_and_validator",
80
+ "holdout_contamination",
81
+ "missing_rollback_snapshot",
82
+ ],
83
+ effectiveErrorBudgetTerms: [
84
+ "first_class_error_mass",
85
+ "quarantine_false_accept_estimate",
86
+ "blind_spot_estimate",
87
+ "drift_estimate",
88
+ ],
89
+ niches: [],
90
+ skills: [],
91
+ rolloutPolicy: {
92
+ staticOnlyCanApprove: false,
93
+ sandboxRequired: true,
94
+ holdoutRequired: true,
95
+ shadowRequiredForFastPathChanges: true,
96
+ lowRiskCanaryOnly: true,
97
+ severeFailureTolerance: 0,
98
+ },
99
+ }, null, 2), "utf8");
100
+ }
101
+ const ontologyInbox = path.join(dir, ontologyInboxDir);
102
+ if (!fs.existsSync(ontologyInbox)) fs.mkdirSync(ontologyInbox, { recursive: true });
103
+ const ontologyRuntime = path.join(dir, ontologyRuntimeFile);
104
+ if (!fs.existsSync(ontologyRuntime)) {
105
+ fs.writeFileSync(ontologyRuntime, JSON.stringify({
106
+ schemaVersion: "1.0",
107
+ kind: "agentlas-ontology-runtime",
108
+ state: "active",
109
+ activation: "automatic",
110
+ projectRoot: projectPath,
111
+ projectName: name,
112
+ dbPath: path.join(dir, ontologyDbFile),
113
+ inboxPath: ontologyInbox,
114
+ sourceManifest: path.join(dir, ontologySourceManifestFile),
115
+ defaultScope: "internal",
116
+ autoIngestPolicy: {
117
+ mode: "inbox_and_registered_sources_only",
118
+ neverScanHomeDirectory: true,
119
+ neverScanSiblingProjects: true,
120
+ crossProjectSearchDefault: "disabled",
121
+ privateScopeDefaultSearch: "excluded",
122
+ },
123
+ promotionMode: {
124
+ operatorManagedLocal: true,
125
+ securityGateMode: "context_folder_routing_only",
126
+ blockingSecurityGate: false,
127
+ notes: "Local promotion is blocked by missing project/folder/owner/evidence/rollback structure, not by a generic security gate.",
128
+ },
129
+ memoryPolicy: {
130
+ durableWrites: "candidate-ticket-only",
131
+ workingMemory: "runtime-cache-only",
132
+ },
133
+ }, null, 2), "utf8");
134
+ }
135
+ const ontologySources = path.join(dir, ontologySourceManifestFile);
136
+ if (!fs.existsSync(ontologySources)) {
137
+ fs.writeFileSync(ontologySources, JSON.stringify({
138
+ schemaVersion: "1.0",
139
+ kind: "agentlas-ontology-source-manifest",
140
+ projectRoot: projectPath,
141
+ sources: [],
142
+ }, null, 2), "utf8");
143
+ }
144
+ const careerGraphInbox = path.join(dir, careerGraphInboxDir);
145
+ if (!fs.existsSync(careerGraphInbox)) fs.mkdirSync(careerGraphInbox, { recursive: true });
146
+ const careerGraphConfig = path.join(dir, careerGraphConfigFile);
147
+ if (!fs.existsSync(careerGraphConfig)) {
148
+ fs.writeFileSync(careerGraphConfig, JSON.stringify({
149
+ schemaVersion: "1.0",
150
+ kind: "agentlas-career-graph",
151
+ state: "active",
152
+ model: "ledger_first_derived_index",
153
+ projectRoot: projectPath,
154
+ projectName: name,
155
+ dbPath: path.join(dir, careerGraphDbFile),
156
+ inboxPath: careerGraphInbox,
157
+ sourceManifest: path.join(dir, careerGraphSourceManifestFile),
158
+ canonicalSourcePolicy: {
159
+ sourceOfTruth: "markdown_jsonl_json",
160
+ graphIsRebuildable: true,
161
+ fallbackWhenStale: "read_canonical_files",
162
+ neverScanHomeDirectory: true,
163
+ neverScanSiblingProjects: true,
164
+ },
165
+ }, null, 2), "utf8");
166
+ }
167
+ const careerGraphSources = path.join(dir, careerGraphSourceManifestFile);
168
+ if (!fs.existsSync(careerGraphSources)) {
169
+ fs.writeFileSync(careerGraphSources, JSON.stringify({
170
+ schemaVersion: "1.0",
171
+ kind: "agentlas-career-graph-source-manifest",
172
+ projectRoot: projectPath,
173
+ sources: [],
174
+ }, null, 2), "utf8");
175
+ }
176
+ for (const fileName of [skillTrialsFile, curatorDecisionsFile]) {
177
+ const filePath = path.join(dir, fileName);
178
+ if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf8");
179
+ }
180
+ // super-ontology 계약 문서 25종 — 데이터 파일 순회 (v1 인라인 리터럴과 바이트 동일).
181
+ for (const entry of SUPER_ONTOLOGY_SEED.documents) {
182
+ const fileName = arch[entry.archKey] || entry.file;
183
+ const filePath = path.join(dir, fileName);
184
+ if (!fs.existsSync(filePath)) {
185
+ fs.writeFileSync(filePath, JSON.stringify(superOntologyDocumentFor(entry, name), null, 2), "utf8");
186
+ }
187
+ }
188
+ for (const fileName of [
189
+ superOntologyReplaysFile,
190
+ superOntologyEvidenceFile,
191
+ superOntologyMemoryBridgeFile,
192
+ ]) {
193
+ const filePath = path.join(dir, fileName);
194
+ if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf8");
195
+ }
196
+ return dir;
197
+ } catch { return null; }
198
+ }
199
+
200
+ module.exports = { ensureProjectMemoryCli, SUPER_ONTOLOGY_SEED };
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ /*
3
+ * project/state — .agentlas/ 비공개 프로젝트 상태 경계 (v1 monolith 8340–8690 포팅).
4
+ *
5
+ * 비타협 계약 (0.9.10 경계):
6
+ * 1. .agentlas/ 는 개인정보다. 절대 커밋/업로드되지 않는다 — .gitignore의
7
+ * surgical marker 블록(# >>> agentlas local project state >>>)이 그 제품 장치다.
8
+ * 2. 어떤 일반 실행도 사용자 프로젝트에 .agentlas/ 를 자동 생성하지 않는다.
9
+ * 생성 권한은 `agentlas project init`(initializeTerminalProjectCli) 단 하나.
10
+ * write/full 권한은 "요청된 작업"에 대한 권한이지 폴더 초기화 동의가 아니다.
11
+ * 3. 부트스트랩은 fail-closed: 심볼릭 링크가 섞인 .agentlas, 심볼릭 링크/1MB 초과
12
+ * .gitignore 처럼 프라이버시 경계를 보증할 수 없는 상태면 던진다(생성 금지).
13
+ * 4. Core 런타임이 있으면 canonical 시드는 Core(project_bootstrap.py)가 소유하고,
14
+ * 없을 때만 레거시 로컬 시드(seed.cjs)로 폴백한다 — 폴백은 프로세스 한정 캐시.
15
+ */
16
+ const crypto = require("node:crypto");
17
+ const fs = require("node:fs");
18
+ const os = require("node:os");
19
+ const path = require("node:path");
20
+ const { userDataDir } = require("../core/paths.cjs");
21
+ const { tableExists } = require("../core/db.cjs");
22
+ const {
23
+ CONTEXT_MAP_MIN_CORE_VERSION,
24
+ captureCoreJsonSync,
25
+ resolveCoreRuntimeRoot,
26
+ } = require("../agentlas-core-harness.cjs");
27
+ const { runCwd } = require("./paths.cjs");
28
+ const { ensureProjectMemoryCli } = require("./seed.cjs");
29
+
30
+ const AGENTLAS_PROJECT_STATE_IGNORE_START = "# >>> agentlas local project state >>>";
31
+ const AGENTLAS_PROJECT_STATE_IGNORE_END = "# <<< agentlas local project state <<<";
32
+ const AGENTLAS_GITIGNORE_MAX_BYTES = 1024 * 1024;
33
+ const projectBootstrapStates = new Map();
34
+
35
+ function terminalProjectCandidateCli(projectPath) {
36
+ try {
37
+ const root = path.resolve(projectPath || process.cwd());
38
+ const unsafe = new Set([
39
+ path.resolve(os.homedir()),
40
+ path.parse(root).root,
41
+ path.resolve(userDataDir()),
42
+ path.resolve(runCwd()),
43
+ ]);
44
+ if (unsafe.has(root)) return null;
45
+ const stat = fs.statSync(root);
46
+ if (!stat.isDirectory()) return null;
47
+ return root;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function assertNoSymlinkInAgentlasStateCli(stateDir) {
54
+ const pending = [stateDir];
55
+ let visited = 0;
56
+ while (pending.length && visited < 4096) {
57
+ const current = pending.pop();
58
+ visited += 1;
59
+ let stat;
60
+ try { stat = fs.lstatSync(current); } catch (error) {
61
+ if (error && error.code === "ENOENT") continue;
62
+ throw error;
63
+ }
64
+ if (stat.isSymbolicLink()) throw new Error(".agentlas local state must not contain symbolic links");
65
+ if (stat.isDirectory()) {
66
+ for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
67
+ }
68
+ }
69
+ if (pending.length) throw new Error(".agentlas local state exceeds the safe bootstrap inspection limit");
70
+ }
71
+
72
+ function readRegularUtf8FileNoFollowCli(filePath, maxBytes = AGENTLAS_GITIGNORE_MAX_BYTES) {
73
+ let before;
74
+ try { before = fs.lstatSync(filePath); } catch (error) {
75
+ if (error && error.code === "ENOENT") return { exists: false, content: "", mode: 0o644, stat: null };
76
+ throw error;
77
+ }
78
+ if (before.isSymbolicLink() || !before.isFile()) throw new Error(".gitignore must be a regular non-symbolic-link file");
79
+ if (before.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
80
+
81
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
82
+ let fd;
83
+ try {
84
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
85
+ } catch (error) {
86
+ if (process.platform !== "win32" || !noFollow || !["EINVAL", "ENOTSUP"].includes(error && error.code)) throw error;
87
+ fd = fs.openSync(filePath, fs.constants.O_RDONLY);
88
+ }
89
+ try {
90
+ const opened = fs.fstatSync(fd);
91
+ if (!opened.isFile()) throw new Error(".gitignore changed type during bootstrap");
92
+ if (opened.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
93
+ if (
94
+ Number.isFinite(before.dev) && Number.isFinite(before.ino) &&
95
+ (before.dev !== opened.dev || before.ino !== opened.ino)
96
+ ) {
97
+ throw new Error(".gitignore changed during bootstrap");
98
+ }
99
+ const chunks = [];
100
+ let total = 0;
101
+ while (total <= maxBytes) {
102
+ const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
103
+ const count = fs.readSync(fd, buffer, 0, buffer.length, null);
104
+ if (!count) break;
105
+ chunks.push(buffer.subarray(0, count));
106
+ total += count;
107
+ }
108
+ if (total > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
109
+ const after = fs.fstatSync(fd);
110
+ if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) throw new Error(".gitignore changed while it was being read");
111
+ let content;
112
+ try { content = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total)); } catch {
113
+ throw new Error(".gitignore must contain valid UTF-8 text");
114
+ }
115
+ return { exists: true, content, mode: before.mode & 0o777, stat: before };
116
+ } finally {
117
+ fs.closeSync(fd);
118
+ }
119
+ }
120
+
121
+ function assertFileSnapshotUnchangedCli(filePath, snapshot) {
122
+ if (!snapshot.exists) {
123
+ try {
124
+ fs.lstatSync(filePath);
125
+ throw new Error(".gitignore appeared during bootstrap");
126
+ } catch (error) {
127
+ if (error && error.code === "ENOENT") return;
128
+ throw error;
129
+ }
130
+ }
131
+ const current = fs.lstatSync(filePath);
132
+ if (current.isSymbolicLink() || !current.isFile()) throw new Error(".gitignore changed type during bootstrap");
133
+ const original = snapshot.stat;
134
+ if (
135
+ !original || current.dev !== original.dev || current.ino !== original.ino ||
136
+ current.size !== original.size || current.mtimeMs !== original.mtimeMs
137
+ ) {
138
+ throw new Error(".gitignore changed during bootstrap");
139
+ }
140
+ }
141
+
142
+ function replaceRegularFileCli(tempPath, destinationPath, snapshot) {
143
+ try {
144
+ fs.renameSync(tempPath, destinationPath);
145
+ return;
146
+ } catch (error) {
147
+ if (process.platform !== "win32" || !snapshot.exists || !["EEXIST", "EPERM", "EACCES"].includes(error && error.code)) {
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ // Windows can reject replacement of an existing file. Keep a same-directory
153
+ // rollback copy so an interrupted replacement never silently loses user rules.
154
+ assertFileSnapshotUnchangedCli(destinationPath, snapshot);
155
+ const backup = `${destinationPath}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
156
+ fs.renameSync(destinationPath, backup);
157
+ try {
158
+ fs.renameSync(tempPath, destinationPath);
159
+ } catch (error) {
160
+ try {
161
+ if (!fs.existsSync(destinationPath)) fs.renameSync(backup, destinationPath);
162
+ } catch { /* preserve the original error and leave the backup recoverable */ }
163
+ throw error;
164
+ }
165
+ try { fs.unlinkSync(backup); } catch { /* a harmless rollback copy may remain on locked Windows hosts */ }
166
+ }
167
+
168
+ function ensureAgentlasProjectStateIgnoreCli(projectPath) {
169
+ const root = terminalProjectCandidateCli(projectPath);
170
+ if (!root) throw new Error("refusing to initialize an unsafe Agentlas project root");
171
+ const stateDir = path.join(root, ".agentlas");
172
+ let stateExists = false;
173
+ try {
174
+ const state = fs.lstatSync(stateDir);
175
+ stateExists = true;
176
+ if (state.isSymbolicLink() || !state.isDirectory()) throw new Error(".agentlas must be a real directory");
177
+ assertNoSymlinkInAgentlasStateCli(stateDir);
178
+ } catch (error) {
179
+ if (error && error.code !== "ENOENT") throw error;
180
+ }
181
+
182
+ const gitignorePath = path.join(root, ".gitignore");
183
+ const snapshot = readRegularUtf8FileNoFollowCli(gitignorePath);
184
+ const existing = snapshot.content;
185
+ const mode = snapshot.mode || 0o644;
186
+
187
+ let next = existing;
188
+ const start = existing.indexOf(AGENTLAS_PROJECT_STATE_IGNORE_START);
189
+ const end = start >= 0 ? existing.indexOf(AGENTLAS_PROJECT_STATE_IGNORE_END, start) : -1;
190
+ if (start >= 0 && end >= 0) {
191
+ const blockEnd = end + AGENTLAS_PROJECT_STATE_IGNORE_END.length;
192
+ const block = existing.slice(start, blockEnd);
193
+ if (!/^\.agentlas\/$/m.test(block)) {
194
+ next = `${existing.slice(0, start)}${block.replace(AGENTLAS_PROJECT_STATE_IGNORE_START, `${AGENTLAS_PROJECT_STATE_IGNORE_START}\n.agentlas/`)}${existing.slice(blockEnd)}`;
195
+ }
196
+ } else {
197
+ const block = `${AGENTLAS_PROJECT_STATE_IGNORE_START}\n.agentlas/\n${AGENTLAS_PROJECT_STATE_IGNORE_END}\n`;
198
+ next = existing.trimEnd() ? `${existing.trimEnd()}\n\n${block}` : block;
199
+ }
200
+ if (next !== existing) {
201
+ const temp = path.join(root, `.gitignore.agentlas-${process.pid}-${crypto.randomUUID()}.tmp`);
202
+ fs.writeFileSync(temp, next.endsWith("\n") ? next : `${next}\n`, { encoding: "utf8", mode, flag: "wx" });
203
+ try {
204
+ assertFileSnapshotUnchangedCli(gitignorePath, snapshot);
205
+ replaceRegularFileCli(temp, gitignorePath, snapshot);
206
+ } catch (error) {
207
+ try { fs.unlinkSync(temp); } catch { /* ignore */ }
208
+ throw error;
209
+ }
210
+ }
211
+ if (!stateExists) fs.mkdirSync(stateDir, { recursive: false, mode: 0o700 });
212
+ assertNoSymlinkInAgentlasStateCli(stateDir);
213
+ try { fs.chmodSync(stateDir, 0o700); } catch { /* Windows/best effort */ }
214
+ }
215
+
216
+ function hardenAgentlasProjectStateCli(projectPath) {
217
+ const root = terminalProjectCandidateCli(projectPath);
218
+ if (!root) return;
219
+ const stateDir = path.join(root, ".agentlas");
220
+ const pending = [stateDir];
221
+ let visited = 0;
222
+ while (pending.length && visited < 4096) {
223
+ const current = pending.pop();
224
+ visited += 1;
225
+ try {
226
+ const stat = fs.lstatSync(current);
227
+ if (stat.isSymbolicLink()) continue;
228
+ if (stat.isDirectory()) {
229
+ try { fs.chmodSync(current, 0o700); } catch { /* Windows/best effort */ }
230
+ for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
231
+ } else if (stat.isFile()) {
232
+ try { fs.chmodSync(current, 0o600); } catch { /* Windows/best effort */ }
233
+ }
234
+ } catch { /* disappearing files and ACL-only hosts are best effort */ }
235
+ }
236
+ }
237
+
238
+ function ensureCoreProjectCli(projectPath, options = {}) {
239
+ const root = terminalProjectCandidateCli(projectPath);
240
+ if (!root) throw new Error("Agentlas project bootstrap requires a real project directory");
241
+ fs.accessSync(root, fs.constants.R_OK | fs.constants.W_OK);
242
+ ensureAgentlasProjectStateIgnoreCli(root);
243
+ const cached = projectBootstrapStates.get(root);
244
+ if (cached && fs.existsSync(path.join(root, ".agentlas", "project-soul-memory.md"))) {
245
+ return cached === "core";
246
+ }
247
+ projectBootstrapStates.delete(root);
248
+ /*
249
+ * 프로젝트 부트스트랩의 능력 판정은 minVersion이 아니라 실제 모듈 존재
250
+ * (agentlas_cloud/project_bootstrap.py)다. 버전 메타데이터가 없는 소스
251
+ * 체크아웃(CI가 Agentlas-OS를 특정 커밋으로 받는 경우)에서 minVersion 게이트가
252
+ * 정상 Core를 걸러 부트스트랩이 통째로 스킵됐다(v1.0.0 태그 CI 실패의 진범).
253
+ * minVersion은 context-map 능력 경로에만 적용한다.
254
+ */
255
+ const coreRoot = resolveCoreRuntimeRoot(options.coreRoot);
256
+ const hasCanonicalBootstrap = Boolean(
257
+ coreRoot && fs.existsSync(path.join(coreRoot, "agentlas_cloud", "project_bootstrap.py")),
258
+ );
259
+ if (hasCanonicalBootstrap) {
260
+ const result = captureCoreJsonSync(
261
+ "agentlas_cloud",
262
+ ["project", "ensure", "--project", root, "--reason", options.reason || "terminal-first-contact"],
263
+ { cwd: root },
264
+ coreRoot,
265
+ );
266
+ const canonical = Boolean(
267
+ result
268
+ && result.schemaVersion === "agentlas.project-bootstrap.v1"
269
+ && ["active", "privacy_warning"].includes(result.status)
270
+ && result.mergeOnly === true
271
+ && result.privacyBlockInstalled === true
272
+ && result.privateModeCompliant === true
273
+ && Array.isArray(result.missing)
274
+ && result.missing.length === 0
275
+ && Array.isArray(result.overwritten)
276
+ && result.overwritten.length === 0
277
+ && Array.isArray(result.permissionIssues)
278
+ && result.permissionIssues.length === 0
279
+ );
280
+ if (canonical) {
281
+ // Core owns the canonical seed. Terminal adds one intentionally broader
282
+ // guard so future local memory files are private without a release update.
283
+ ensureAgentlasProjectStateIgnoreCli(root);
284
+ hardenAgentlasProjectStateCli(root);
285
+ projectBootstrapStates.set(root, "core");
286
+ return true;
287
+ }
288
+ throw new Error("Agentlas Core returned an incomplete project bootstrap contract");
289
+ }
290
+ // A just-updated Terminal can briefly see the previous Core. The legacy
291
+ // merge-only seed remains local-only and Core is retried next process.
292
+ ensureProjectMemoryCli(root);
293
+ if (!fs.existsSync(path.join(root, ".agentlas"))) {
294
+ throw new Error("Agentlas project bootstrap could not create private local state");
295
+ }
296
+ ensureAgentlasProjectStateIgnoreCli(root);
297
+ hardenAgentlasProjectStateCli(root);
298
+ projectBootstrapStates.set(root, "fallback");
299
+ return false;
300
+ }
301
+
302
+ // Passive checks never increment visits or touch the project. Activation is
303
+ // reserved for the explicit project-initialization boundary.
304
+ function recordCliFolderVisit(db, projectPath, options = {}) {
305
+ const root = terminalProjectCandidateCli(projectPath);
306
+ if (!root) return { activated: false };
307
+ const activate = options.activate === true;
308
+ try {
309
+ if (!activate) {
310
+ const row = tableExists(db, "folder_activity")
311
+ ? db.prepare("SELECT activated_at FROM folder_activity WHERE path=?").get(root)
312
+ : null;
313
+ return { activated: Boolean(row && row.activated_at) || fs.existsSync(path.join(root, ".agentlas")) };
314
+ }
315
+
316
+ ensureCoreProjectCli(root, { reason: options.reason || "terminal-first-contact", coreRoot: options.coreRoot });
317
+ if (!tableExists(db, "folder_activity")) return { activated: true };
318
+ const now = new Date().toISOString();
319
+ const row = db.prepare("SELECT visits FROM folder_activity WHERE path=?").get(root);
320
+ if (row) {
321
+ db.prepare("UPDATE folder_activity SET visits=?, activated_at=COALESCE(activated_at,?), last_seen=? WHERE path=?")
322
+ .run(Number(row.visits || 0) + 1, now, now, root);
323
+ } else {
324
+ db.prepare("INSERT INTO folder_activity (path, visits, activated_at, first_seen, last_seen) VALUES (?,?,?,?,?)")
325
+ .run(root, 1, now, now, now);
326
+ }
327
+ return { activated: true };
328
+ } catch (error) {
329
+ // An activation failure can mean that the project-local privacy boundary
330
+ // could not be established (for example, a symlinked or oversized
331
+ // .gitignore). Explicit initialization must fail closed in that state.
332
+ if (activate) throw error;
333
+ return { activated: false };
334
+ }
335
+ }
336
+
337
+ function activeProjectPath(db, options = {}) {
338
+ const root = terminalProjectCandidateCli(options.projectPath || process.cwd());
339
+ if (!root) return null;
340
+ const result = recordCliFolderVisit(db, root, options);
341
+ return result.activated ? root : null;
342
+ }
343
+
344
+ // v1 시그니처 보존: permission/reason은 받되 절대 초기화 권한으로 쓰지 않는다.
345
+ function ensureTerminalProjectForExecutionCli(db, projectPath, permission = "write", reason = "terminal-first-contact") {
346
+ const root = terminalProjectCandidateCli(projectPath);
347
+ if (!root) return null;
348
+ // Every ordinary execution mode is passive. Only `agentlas project init`
349
+ // may create .agentlas state or edit .gitignore; write/full permission is
350
+ // authority for the requested task, not consent to initialize the folder.
351
+ void permission;
352
+ void reason;
353
+ const active = activeProjectPath(db, { projectPath: root });
354
+ if (!active) return null;
355
+ return initializedAgentlasProjectPathCli(root);
356
+ }
357
+
358
+ function initializedAgentlasProjectPathCli(projectPath) {
359
+ const root = terminalProjectCandidateCli(projectPath);
360
+ if (!root) return null;
361
+ try {
362
+ const stateDir = path.join(root, ".agentlas");
363
+ const soul = path.join(stateDir, "project-soul-memory.md");
364
+ const ignore = readRegularUtf8FileNoFollowCli(path.join(root, ".gitignore")).content;
365
+ if (!fs.existsSync(soul) || !ignore.includes(AGENTLAS_PROJECT_STATE_IGNORE_START) || !/^\.agentlas\/$/m.test(ignore)) {
366
+ return null;
367
+ }
368
+ assertNoSymlinkInAgentlasStateCli(stateDir);
369
+ return root;
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+
375
+ function initializeTerminalProjectCli(db, projectPath, reason = "terminal-explicit-project-init", options = {}) {
376
+ const root = terminalProjectCandidateCli(projectPath);
377
+ if (!root) throw new Error("Agentlas project initialization requires a safe project directory");
378
+ return activeProjectPath(db, {
379
+ projectPath: root,
380
+ activate: true,
381
+ reason,
382
+ coreRoot: options.coreRoot,
383
+ });
384
+ }
385
+
386
+ module.exports = {
387
+ AGENTLAS_PROJECT_STATE_IGNORE_START,
388
+ AGENTLAS_PROJECT_STATE_IGNORE_END,
389
+ AGENTLAS_GITIGNORE_MAX_BYTES,
390
+ terminalProjectCandidateCli,
391
+ assertNoSymlinkInAgentlasStateCli,
392
+ readRegularUtf8FileNoFollowCli,
393
+ assertFileSnapshotUnchangedCli,
394
+ replaceRegularFileCli,
395
+ ensureAgentlasProjectStateIgnoreCli,
396
+ hardenAgentlasProjectStateCli,
397
+ ensureCoreProjectCli,
398
+ recordCliFolderVisit,
399
+ activeProjectPath,
400
+ ensureTerminalProjectForExecutionCli,
401
+ initializedAgentlasProjectPathCli,
402
+ initializeTerminalProjectCli,
403
+ };