@aopslabs/aops 0.2.0

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 (171) hide show
  1. package/LICENSE +19 -0
  2. package/NOTICE +7 -0
  3. package/README.md +203 -0
  4. package/assets/agent-assets/core/SKILL.md +49 -0
  5. package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
  6. package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
  7. package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
  8. package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
  9. package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
  10. package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
  11. package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
  12. package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
  13. package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
  14. package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
  15. package/assets/agent-assets/core/references/view/SKILL.md +58 -0
  16. package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
  17. package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
  18. package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
  19. package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
  20. package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
  21. package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
  22. package/assets/agent-assets/core/user-guides/docman.md +162 -0
  23. package/assets/agent-assets/core/user-guides/projectman.md +212 -0
  24. package/assets/agent-assets/core/user-guides/sys.md +94 -0
  25. package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
  26. package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
  27. package/assets/r6-remote-session-baseline.json +19 -0
  28. package/assets/skills/aops-install/SKILL.md +231 -0
  29. package/assets/skills/aops-install/agents/openai.yaml +4 -0
  30. package/dist/commands/activity.js +352 -0
  31. package/dist/commands/agent-profile.js +653 -0
  32. package/dist/commands/agent.js +710 -0
  33. package/dist/commands/agents-md.js +125 -0
  34. package/dist/commands/api.js +84 -0
  35. package/dist/commands/archive.js +873 -0
  36. package/dist/commands/artifact.js +535 -0
  37. package/dist/commands/assets.js +1414 -0
  38. package/dist/commands/auth/device-login.js +1 -0
  39. package/dist/commands/auth/legacy-export.js +177 -0
  40. package/dist/commands/auth/login.js +426 -0
  41. package/dist/commands/chat.js +1363 -0
  42. package/dist/commands/chatv3.js +1684 -0
  43. package/dist/commands/checkpoint.js +549 -0
  44. package/dist/commands/commercial-artifact.js +475 -0
  45. package/dist/commands/commercial-license.js +594 -0
  46. package/dist/commands/community-auth.js +62 -0
  47. package/dist/commands/community-cockpit.js +185 -0
  48. package/dist/commands/community-console.js +109 -0
  49. package/dist/commands/community-doctor.js +335 -0
  50. package/dist/commands/community-server.js +2062 -0
  51. package/dist/commands/community-setup.js +284 -0
  52. package/dist/commands/discuss.js +1448 -0
  53. package/dist/commands/doc.js +5219 -0
  54. package/dist/commands/experience.js +922 -0
  55. package/dist/commands/host.js +1127 -0
  56. package/dist/commands/init.js +118 -0
  57. package/dist/commands/memory.js +3129 -0
  58. package/dist/commands/mission.js +562 -0
  59. package/dist/commands/official-catalog.js +134 -0
  60. package/dist/commands/plan.js +84 -0
  61. package/dist/commands/playbook.js +415 -0
  62. package/dist/commands/pm/context.js +6 -0
  63. package/dist/commands/pm/index.js +701 -0
  64. package/dist/commands/pm/projectman.js +6266 -0
  65. package/dist/commands/project.js +1080 -0
  66. package/dist/commands/prompt.js +1024 -0
  67. package/dist/commands/repo-sync.js +1907 -0
  68. package/dist/commands/resource.js +527 -0
  69. package/dist/commands/skill.js +1211 -0
  70. package/dist/commands/start-disciplines.js +510 -0
  71. package/dist/commands/start.js +1343 -0
  72. package/dist/commands/target.js +309 -0
  73. package/dist/commands/version.js +42 -0
  74. package/dist/commands/view.js +1852 -0
  75. package/dist/launchers/cockpit.js +28 -0
  76. package/dist/launchers/options.js +74 -0
  77. package/dist/launchers/server.js +41 -0
  78. package/dist/lib/agent-assets/envelope.js +83 -0
  79. package/dist/lib/agent-assets/gateway.js +15 -0
  80. package/dist/lib/agent-assets/guards.js +23 -0
  81. package/dist/lib/agent-assets/hosted-discovery.js +148 -0
  82. package/dist/lib/agent-assets/hosted-package-input.js +154 -0
  83. package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
  84. package/dist/lib/agent-assets/native-fs.js +589 -0
  85. package/dist/lib/agent-assets/package-manifest.js +324 -0
  86. package/dist/lib/agent-assets/portable-path.js +67 -0
  87. package/dist/lib/agent-assets/release-input.js +177 -0
  88. package/dist/lib/agent-assets/roots.js +45 -0
  89. package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
  90. package/dist/lib/agent-assets/runtime-targets.js +50 -0
  91. package/dist/lib/agent-assets/store-reader.js +1212 -0
  92. package/dist/lib/agent-assets/store-types.js +1 -0
  93. package/dist/lib/agent-assets/store-writer.js +1484 -0
  94. package/dist/lib/agent-assets/types.js +1 -0
  95. package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
  96. package/dist/lib/commercial-final-readiness.js +504 -0
  97. package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
  98. package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
  99. package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
  100. package/dist/lib/community-client-contract.js +164 -0
  101. package/dist/lib/community-cockpit-lifecycle.js +601 -0
  102. package/dist/lib/community-command-abort.js +59 -0
  103. package/dist/lib/community-diagnostic.js +237 -0
  104. package/dist/lib/community-docker-adapter.js +498 -0
  105. package/dist/lib/community-home.js +31 -0
  106. package/dist/lib/community-instance-contract.js +148 -0
  107. package/dist/lib/community-instance-layout.js +97 -0
  108. package/dist/lib/community-lifecycle.js +1631 -0
  109. package/dist/lib/community-migration-snapshot.js +770 -0
  110. package/dist/lib/community-native-application-recovery.js +425 -0
  111. package/dist/lib/community-native-child.js +240 -0
  112. package/dist/lib/community-native-database-recovery.js +456 -0
  113. package/dist/lib/community-native-lifecycle.js +2458 -0
  114. package/dist/lib/community-native-migration.js +339 -0
  115. package/dist/lib/community-native-postgres.js +596 -0
  116. package/dist/lib/community-operation-journal.js +773 -0
  117. package/dist/lib/community-operation-lock.js +596 -0
  118. package/dist/lib/community-release-resolver.js +291 -0
  119. package/dist/lib/community-release-verifier.js +475 -0
  120. package/dist/lib/community-repo-discovery.js +252 -0
  121. package/dist/lib/community-setup-server-env.js +203 -0
  122. package/dist/lib/external-url.js +110 -0
  123. package/dist/lib/official-catalog-gateway.js +108 -0
  124. package/dist/lib/official-catalog.js +364 -0
  125. package/dist/lib/server-bootstrap.js +240 -0
  126. package/dist/lib/setup-agent-assets-bridge.js +176 -0
  127. package/dist/lib/setup-agent-assets-release.js +52 -0
  128. package/dist/lib/setup-external-postgres.js +108 -0
  129. package/dist/lib/setup-init-orchestrator.js +990 -0
  130. package/dist/lib/setup-install-guide.js +52 -0
  131. package/dist/lib/setup-installer-release.js +47 -0
  132. package/dist/lib/setup-local-postgres.js +329 -0
  133. package/dist/lib/setup-official-catalog-bridge.js +148 -0
  134. package/dist/lib/setup-readiness.js +888 -0
  135. package/dist/lib/tui-launcher.js +59 -0
  136. package/dist/main.js +150 -0
  137. package/dist/seeds/agents-md.js +54 -0
  138. package/dist/utils/agent-gateway.js +275 -0
  139. package/dist/utils/agents-md.js +281 -0
  140. package/dist/utils/api.js +255 -0
  141. package/dist/utils/chatv3-session-store.js +185 -0
  142. package/dist/utils/command.js +67 -0
  143. package/dist/utils/community-host-runtime.js +376 -0
  144. package/dist/utils/config.js +1015 -0
  145. package/dist/utils/discussion-workspace.js +552 -0
  146. package/dist/utils/experience-workspace.js +326 -0
  147. package/dist/utils/guide-paths.js +67 -0
  148. package/dist/utils/hosted-sugar.js +56 -0
  149. package/dist/utils/hosted-workspace.js +229 -0
  150. package/dist/utils/memory-workspace.js +537 -0
  151. package/dist/utils/operator-runtime.js +199 -0
  152. package/dist/utils/password-crypto.js +39 -0
  153. package/dist/utils/playbook-workspace.js +479 -0
  154. package/dist/utils/project-context.js +255 -0
  155. package/dist/utils/prompts.js +70 -0
  156. package/dist/utils/refresh-race.js +28 -0
  157. package/dist/utils/repo-config.js +109 -0
  158. package/dist/utils/repo-first-projectman.js +194 -0
  159. package/dist/utils/repo-first-storage.js +13 -0
  160. package/dist/utils/repo-first-view.js +305 -0
  161. package/dist/utils/session-guidance.js +278 -0
  162. package/dist/utils/session-state.js +137 -0
  163. package/dist/utils/table.js +39 -0
  164. package/dist/utils/transient-fs.js +49 -0
  165. package/dist/utils/workspace-root.js +1 -0
  166. package/launchers/aops-cockpit.ps1 +15 -0
  167. package/launchers/aops-cockpit.sh +16 -0
  168. package/launchers/aops-server.ps1 +15 -0
  169. package/launchers/aops-server.sh +16 -0
  170. package/native/tui/win32-x64/aops-tui.exe +0 -0
  171. package/package.json +81 -0
@@ -0,0 +1,1907 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import fs from 'node:fs/promises';
4
+ import http from 'node:http';
5
+ import path from 'node:path';
6
+ import { logError, logSuccess, logWarn } from '@aopslabs/xf-cli-ui';
7
+ import { Command } from 'commander';
8
+ import { invokeHostedToolWithApiState, requireApiState, unwrapHostedToolResult, } from '../utils/agent-gateway.js';
9
+ import { applyCommonOptions, compactPayload, normalizeNonEmpty } from '../utils/command.js';
10
+ import { buildOperatorCookbook } from '../utils/hosted-sugar.js';
11
+ import { parseFrontmatterDocument, readLocalMemoryEntries, rebuildLocalMemoryWorkspace, renderFrontmatterDocument, resolveMemoryWorkspacePaths, } from '../utils/memory-workspace.js';
12
+ import { readExperienceItems, rebuildExperienceWorkspace, } from '../utils/experience-workspace.js';
13
+ import { resolveOwnerScopeIdFromProjectRecord, resolveProjectBindingContext, } from '../utils/project-context.js';
14
+ import { collectRepoFirstSyncRecords, rebuildProjectmanViews, resolveRepoFirstAgentspacePaths, resolveRepoFirstProjectmanPaths, } from '../utils/repo-first-projectman.js';
15
+ import { loadAopsRepoConfig, loadAopsRepoConfigReadOnly, readAopsRepoConfigReadOnly, } from '../utils/repo-config.js';
16
+ import { hostedProjectKey, rebuildHostedWorkspace, syncHostedMirrorKind, } from '../utils/hosted-workspace.js';
17
+ import { GUIDE_PATHS } from '../utils/guide-paths.js';
18
+ import { writeFileWithRetry } from '../utils/transient-fs.js';
19
+ function projectReportBase(project) {
20
+ return compactPayload({
21
+ projectSlug: normalizeNonEmpty(project.slug),
22
+ projectName: normalizeNonEmpty(project.name),
23
+ projectId: normalizeNonEmpty(project.projectId) ?? normalizeNonEmpty(project.scopeId),
24
+ localRoot: normalizeNonEmpty(project.localRoot),
25
+ });
26
+ }
27
+ function assertNoExplicitProjectSelectorForAllProjects(options) {
28
+ if (normalizeNonEmpty(options.projectSlug) ||
29
+ normalizeNonEmpty(options.projectName) ||
30
+ normalizeNonEmpty(options.projectId) ||
31
+ normalizeNonEmpty(options.scopeId)) {
32
+ throw new Error('sync --all-projects cannot be combined with --project-id, --project-name, --project-slug, or --scope-id.');
33
+ }
34
+ }
35
+ function syncOptionsForProject(options, project) {
36
+ const next = {
37
+ ...options,
38
+ allProjects: false,
39
+ projectId: undefined,
40
+ projectName: undefined,
41
+ projectSlug: undefined,
42
+ scopeId: undefined,
43
+ };
44
+ const projectSlug = normalizeNonEmpty(project.slug);
45
+ const projectName = normalizeNonEmpty(project.name);
46
+ const projectId = normalizeNonEmpty(project.projectId) ?? normalizeNonEmpty(project.scopeId);
47
+ if (projectSlug)
48
+ next.projectSlug = projectSlug;
49
+ else if (projectName)
50
+ next.projectName = projectName;
51
+ else if (projectId)
52
+ next.projectId = projectId;
53
+ return next;
54
+ }
55
+ async function resolveAllProjectsTargets(options) {
56
+ assertNoExplicitProjectSelectorForAllProjects(options);
57
+ const { rootDir, configPath, config } = await loadAopsRepoConfig(process.cwd());
58
+ if (!config) {
59
+ throw new Error(`sync --all-projects requires repo config at ${configPath}. Run \`aops-cli init\` first.`);
60
+ }
61
+ const runnable = [];
62
+ const skipped = [];
63
+ for (const project of config.projects) {
64
+ if (config.projects.length > 1 && !normalizeNonEmpty(project.localRoot)) {
65
+ skipped.push({
66
+ ...projectReportBase(project),
67
+ ok: true,
68
+ skipped: true,
69
+ reason: 'project has no localRoot, so it cannot be safely partitioned in --all-projects mode.',
70
+ });
71
+ continue;
72
+ }
73
+ runnable.push(project);
74
+ }
75
+ if (runnable.length === 0) {
76
+ throw new Error(`sync --all-projects found no runnable projects in ${configPath}.`);
77
+ }
78
+ return { repoRoot: rootDir, configPath, runnable, skipped };
79
+ }
80
+ async function runForAllProjects(options, command, execute, params = {}) {
81
+ try {
82
+ const targets = await resolveAllProjectsTargets(options);
83
+ const apiState = params.requireApi === true ? await requireApiState(options) : null;
84
+ if (params.requireApi === true && !apiState)
85
+ return;
86
+ const projects = [...targets.skipped];
87
+ for (const project of targets.runnable) {
88
+ const projectOptions = syncOptionsForProject(options, project);
89
+ try {
90
+ const context = await resolveProjectBindingContext(projectOptions, { requireProject: true });
91
+ const data = await execute(projectOptions, context, apiState);
92
+ const projectFailedCount = typeof data.failedCount === 'number' ? data.failedCount : 0;
93
+ projects.push({
94
+ ...projectReportBase(project),
95
+ projectSlug: normalizeNonEmpty(context.projectSlug) ?? normalizeNonEmpty(project.slug),
96
+ projectName: normalizeNonEmpty(context.projectName) ?? normalizeNonEmpty(project.name),
97
+ projectId: normalizeNonEmpty(context.projectId) ?? normalizeNonEmpty(project.projectId) ?? normalizeNonEmpty(project.scopeId),
98
+ localRoot: normalizeNonEmpty(context.localRoot) ?? normalizeNonEmpty(project.localRoot),
99
+ ok: projectFailedCount === 0,
100
+ error: projectFailedCount > 0 ? `${command} reported ${projectFailedCount} file-level error(s).` : undefined,
101
+ data,
102
+ });
103
+ }
104
+ catch (error) {
105
+ projects.push({
106
+ ...projectReportBase(project),
107
+ ok: false,
108
+ error: error instanceof Error ? error.message : String(error),
109
+ });
110
+ }
111
+ }
112
+ const failedCount = projects.filter((project) => !project.ok).length;
113
+ const skippedCount = projects.filter((project) => project.skipped).length;
114
+ const statePath = await writeState(targets.repoRoot, {
115
+ lastAllProjectsSync: {
116
+ command,
117
+ failedCount,
118
+ skippedCount,
119
+ projectCount: targets.runnable.length,
120
+ projects,
121
+ },
122
+ });
123
+ emit(options, {
124
+ command,
125
+ surface: 'hosted-cache-sync',
126
+ resolvedContext: {
127
+ repoRoot: targets.repoRoot,
128
+ configPath: targets.configPath,
129
+ configFound: true,
130
+ mode: 'all-projects',
131
+ },
132
+ result: {
133
+ ok: failedCount === 0,
134
+ data: {
135
+ projectCount: targets.runnable.length,
136
+ failedCount,
137
+ skippedCount,
138
+ statePath,
139
+ projects,
140
+ },
141
+ },
142
+ }, failedCount === 0 ? `${command} --all-projects completed.` : `${command} --all-projects completed with project-level errors.`);
143
+ if (failedCount > 0)
144
+ process.exitCode = 1;
145
+ }
146
+ catch (error) {
147
+ logError(error instanceof Error ? error.message : String(error));
148
+ process.exitCode = 1;
149
+ }
150
+ }
151
+ function isRecord(value) {
152
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
153
+ }
154
+ function toRecordArray(value) {
155
+ if (Array.isArray(value))
156
+ return value.filter(isRecord);
157
+ if (!isRecord(value))
158
+ return [];
159
+ for (const candidate of [value.data, value.items, value.rows, value.results]) {
160
+ if (Array.isArray(candidate))
161
+ return candidate.filter(isRecord);
162
+ }
163
+ return [];
164
+ }
165
+ function slugify(value) {
166
+ return value
167
+ .toLowerCase()
168
+ .replace(/[^a-z0-9]+/g, '-')
169
+ .replace(/^-+|-+$/g, '')
170
+ .slice(0, 80) || 'item';
171
+ }
172
+ function hashContent(value) {
173
+ return createHash('sha256').update(value).digest('hex');
174
+ }
175
+ function toStringArray(value) {
176
+ return Array.isArray(value)
177
+ ? value.map((entry) => normalizeNonEmpty(entry)).filter((entry) => Boolean(entry))
178
+ : [];
179
+ }
180
+ function numberField(value) {
181
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
182
+ }
183
+ function collectRepeatedOption(value, previous = []) {
184
+ const normalized = normalizeNonEmpty(value);
185
+ return normalized ? [...previous, normalized] : previous;
186
+ }
187
+ function relativePath(repoRoot, filePath) {
188
+ return path.relative(repoRoot, filePath).split(path.sep).join('/');
189
+ }
190
+ function isAopsArchivePath(filePath) {
191
+ const parts = filePath.split(path.sep);
192
+ const aopsIndex = parts.lastIndexOf('.aops');
193
+ return aopsIndex >= 0 && parts[aopsIndex + 1] === 'archive';
194
+ }
195
+ async function readMarkdownRecords(rootDir, params = {}) {
196
+ try {
197
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
198
+ const records = [];
199
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
200
+ const filePath = path.join(rootDir, entry.name);
201
+ if (isAopsArchivePath(filePath))
202
+ continue;
203
+ if (entry.isDirectory() && params.recursive === true) {
204
+ records.push(...await readMarkdownRecords(filePath, params));
205
+ continue;
206
+ }
207
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
208
+ continue;
209
+ const parsed = parseFrontmatterDocument(await fs.readFile(filePath, 'utf8'));
210
+ records.push({ filePath, frontmatter: parsed.frontmatter, body: parsed.body });
211
+ }
212
+ return records;
213
+ }
214
+ catch {
215
+ return [];
216
+ }
217
+ }
218
+ async function readRepoFirstSourceRecords(context) {
219
+ const pm = resolveRepoFirstProjectmanPaths(context);
220
+ const ag = resolveRepoFirstAgentspacePaths(context);
221
+ return [
222
+ ...await readMarkdownRecords(pm.boards),
223
+ ...await readMarkdownRecords(pm.tasks),
224
+ ...await readMarkdownRecords(pm.sprints),
225
+ ...await readMarkdownRecords(pm.utasks, { recursive: true }),
226
+ ...await readMarkdownRecords(pm.issues),
227
+ ...await readMarkdownRecords(pm.feedback),
228
+ ...await readMarkdownRecords(pm.reviewRequests),
229
+ ...await readMarkdownRecords(pm.tombstones, { recursive: true }),
230
+ ...await readMarkdownRecords(ag.experienceItems),
231
+ ...await readMarkdownRecords(ag.memoryItems),
232
+ ];
233
+ }
234
+ async function findRecordByRemoteId(rootDir, remoteId, params = {}) {
235
+ const records = await readMarkdownRecords(rootDir, params);
236
+ return records.find((record) => normalizeNonEmpty(record.frontmatter.remoteId) === remoteId) ?? null;
237
+ }
238
+ async function findRecordByLocalId(rootDir, localId, params = {}) {
239
+ const records = await readMarkdownRecords(rootDir, params);
240
+ return records.find((record) => {
241
+ const id = normalizeNonEmpty(record.frontmatter.localId) ?? normalizeNonEmpty(record.frontmatter.id);
242
+ return id === localId;
243
+ }) ?? null;
244
+ }
245
+ function extractResultData(value) {
246
+ if (!isRecord(value))
247
+ return null;
248
+ if (isRecord(value.data))
249
+ return value.data;
250
+ if (isRecord(value.response))
251
+ return extractResultData(value.response);
252
+ if (isRecord(value.result))
253
+ return extractResultData(value.result);
254
+ return value;
255
+ }
256
+ function optionValues(value) {
257
+ return Array.isArray(value)
258
+ ? value.map((entry) => normalizeNonEmpty(entry)).filter((entry) => Boolean(entry))
259
+ : [];
260
+ }
261
+ function buildSyncSelectionFilters(options) {
262
+ return compactPayload({
263
+ board: optionValues(options.board),
264
+ task: optionValues(options.task),
265
+ sprint: optionValues(options.sprint),
266
+ issue: optionValues(options.issue),
267
+ feedback: optionValues(options.feedback),
268
+ reviewRequest: optionValues(options.reviewRequest),
269
+ record: optionValues(options.record),
270
+ });
271
+ }
272
+ function recordKeySet(repoRoot, record) {
273
+ const fm = record.frontmatter;
274
+ return new Set([
275
+ relativePath(repoRoot, record.filePath),
276
+ record.filePath,
277
+ normalizeNonEmpty(fm.localId),
278
+ normalizeNonEmpty(fm.id),
279
+ normalizeNonEmpty(fm.remoteId),
280
+ normalizeNonEmpty(fm.slug),
281
+ normalizeNonEmpty(fm.name),
282
+ normalizeNonEmpty(fm.title),
283
+ normalizeNonEmpty(fm.taskCode),
284
+ ].filter((value) => Boolean(value)).map((value) => value.toLowerCase()));
285
+ }
286
+ function recordMatchesAny(repoRoot, record, values) {
287
+ if (values.length === 0)
288
+ return false;
289
+ const keys = recordKeySet(repoRoot, record);
290
+ return values.some((value) => keys.has(value.toLowerCase()));
291
+ }
292
+ function addRelatedIds(target, values) {
293
+ for (const value of values) {
294
+ const normalized = normalizeNonEmpty(value);
295
+ if (normalized)
296
+ target.add(normalized);
297
+ }
298
+ }
299
+ function referencesAny(record, values, keys) {
300
+ if (values.size === 0)
301
+ return false;
302
+ return keys.some((key) => {
303
+ const value = normalizeNonEmpty(record.frontmatter[key]);
304
+ return Boolean(value && values.has(value));
305
+ });
306
+ }
307
+ function addRecordIdentity(ids, record) {
308
+ addRelatedIds(ids, [
309
+ record.frontmatter.localId,
310
+ record.frontmatter.id,
311
+ record.frontmatter.remoteId,
312
+ record.frontmatter.slug,
313
+ record.frontmatter.name,
314
+ record.frontmatter.title,
315
+ ]);
316
+ }
317
+ function buildSyncSelection(repoRoot, records, options) {
318
+ const filters = buildSyncSelectionFilters(options);
319
+ const active = Object.values(filters).some((values) => values.length > 0);
320
+ if (!active) {
321
+ return {
322
+ active: false,
323
+ records,
324
+ selectedPaths: new Set(records.map((record) => relativePath(repoRoot, record.filePath))),
325
+ filters,
326
+ };
327
+ }
328
+ const selected = new Set();
329
+ const boardIds = new Set();
330
+ const taskIds = new Set();
331
+ const sprintIds = new Set();
332
+ const utaskIds = new Set();
333
+ const reviewRequestIds = new Set();
334
+ const include = (record) => {
335
+ selected.add(record);
336
+ const entityType = normalizeNonEmpty(record.frontmatter.entityType);
337
+ if (entityType === 'projectman.board')
338
+ addRecordIdentity(boardIds, record);
339
+ if (entityType === 'projectman.kanban-task')
340
+ addRecordIdentity(taskIds, record);
341
+ if (entityType === 'projectman.sprint')
342
+ addRecordIdentity(sprintIds, record);
343
+ if (entityType === 'projectman.utask')
344
+ addRecordIdentity(utaskIds, record);
345
+ if (entityType === 'projectman.review-request')
346
+ addRecordIdentity(reviewRequestIds, record);
347
+ };
348
+ for (const record of records) {
349
+ const entityType = normalizeNonEmpty(record.frontmatter.entityType);
350
+ if (recordMatchesAny(repoRoot, record, filters.record ?? []))
351
+ include(record);
352
+ if (entityType === 'projectman.board' && recordMatchesAny(repoRoot, record, filters.board ?? []))
353
+ include(record);
354
+ if (entityType === 'projectman.kanban-task' && recordMatchesAny(repoRoot, record, filters.task ?? []))
355
+ include(record);
356
+ if (entityType === 'projectman.sprint' && recordMatchesAny(repoRoot, record, filters.sprint ?? []))
357
+ include(record);
358
+ if (entityType === 'projectman.issue' && recordMatchesAny(repoRoot, record, filters.issue ?? []))
359
+ include(record);
360
+ if (entityType === 'projectman.feedback' && recordMatchesAny(repoRoot, record, filters.feedback ?? []))
361
+ include(record);
362
+ if (entityType === 'projectman.review-request' && recordMatchesAny(repoRoot, record, filters.reviewRequest ?? []))
363
+ include(record);
364
+ }
365
+ let changed = true;
366
+ while (changed) {
367
+ changed = false;
368
+ for (const record of records) {
369
+ if (selected.has(record))
370
+ continue;
371
+ const entityType = normalizeNonEmpty(record.frontmatter.entityType);
372
+ let shouldInclude = false;
373
+ if (entityType === 'projectman.board') {
374
+ shouldInclude = referencesAny(record, boardIds, ['localId', 'id', 'remoteId', 'slug', 'name']);
375
+ }
376
+ else if (entityType === 'projectman.kanban-task') {
377
+ shouldInclude =
378
+ referencesAny(record, boardIds, ['boardLocalId', 'boardId', 'boardRemoteId', 'boardSlug']) ||
379
+ referencesAny(record, taskIds, ['localId', 'id', 'remoteId', 'slug', 'title']);
380
+ }
381
+ else if (entityType === 'projectman.sprint') {
382
+ shouldInclude =
383
+ referencesAny(record, taskIds, ['kanbanTaskLocalId', 'kanbanTaskId', 'kanbanTaskRemoteId']) ||
384
+ referencesAny(record, sprintIds, ['localId', 'id', 'remoteId', 'slug', 'name']);
385
+ }
386
+ else if (entityType === 'projectman.utask') {
387
+ shouldInclude =
388
+ referencesAny(record, sprintIds, ['sprintLocalId', 'sprintId', 'sprintRemoteId', 'sprintSlug']) ||
389
+ referencesAny(record, utaskIds, ['localId', 'id', 'remoteId', 'slug', 'title']);
390
+ }
391
+ else if (entityType === 'projectman.issue' || entityType === 'projectman.feedback') {
392
+ shouldInclude =
393
+ referencesAny(record, taskIds, ['kanbanTaskLocalId', 'kanbanTaskId', 'kanbanTaskRemoteId']) ||
394
+ referencesAny(record, sprintIds, ['sprintLocalId', 'sprintId', 'sprintRemoteId']) ||
395
+ referencesAny(record, utaskIds, ['utaskLocalId', 'utaskId', 'microTaskId', 'microTaskRemoteId']) ||
396
+ referencesAny(record, reviewRequestIds, ['reviewRequestLocalId', 'reviewRequestId', 'reviewRequestRemoteId']);
397
+ }
398
+ else if (entityType === 'projectman.review-request') {
399
+ shouldInclude =
400
+ referencesAny(record, taskIds, ['kanbanTaskLocalId', 'kanbanTaskId', 'kanbanTaskRemoteId']) ||
401
+ referencesAny(record, sprintIds, ['sprintLocalId', 'sprintId', 'sprintRemoteId']) ||
402
+ referencesAny(record, utaskIds, ['utaskLocalId', 'utaskId', 'microTaskId', 'microTaskRemoteId']) ||
403
+ referencesAny(record, reviewRequestIds, ['parentReviewRequestLocalId', 'parentReviewRequestId', 'rootReviewRequestLocalId', 'rootReviewRequestId']);
404
+ }
405
+ else if (entityType === 'agentspace.memory-item' || entityType === 'agentspace.experience-item') {
406
+ shouldInclude =
407
+ referencesAny(record, boardIds, ['boardId', 'boardLocalId', 'sourceId', 'subjectId']) ||
408
+ referencesAny(record, taskIds, ['kanbanTaskId', 'kanbanTaskLocalId', 'sourceId', 'subjectId']) ||
409
+ referencesAny(record, sprintIds, ['sprintId', 'sprintLocalId', 'sourceId', 'subjectId']) ||
410
+ referencesAny(record, utaskIds, ['utaskId', 'utaskLocalId', 'microTaskId', 'sourceId', 'subjectId']);
411
+ }
412
+ else if (entityType === 'agentspace.discussion-topic') {
413
+ shouldInclude =
414
+ referencesAny(record, boardIds, ['subjectId']) ||
415
+ referencesAny(record, taskIds, ['subjectId']) ||
416
+ referencesAny(record, sprintIds, ['subjectId']) ||
417
+ referencesAny(record, utaskIds, ['subjectId']);
418
+ }
419
+ if (shouldInclude) {
420
+ include(record);
421
+ changed = true;
422
+ }
423
+ }
424
+ }
425
+ if (selected.size === 0) {
426
+ const renderedFilters = Object.entries(filters).map(([key, values]) => `${key}=${values.join(',')}`).join(' ');
427
+ throw new Error(`sync selection matched no records (${renderedFilters}).`);
428
+ }
429
+ const selectedRecords = records.filter((record) => selected.has(record));
430
+ return {
431
+ active: true,
432
+ records: selectedRecords,
433
+ selectedPaths: new Set(selectedRecords.map((record) => relativePath(repoRoot, record.filePath))),
434
+ filters,
435
+ };
436
+ }
437
+ function titleFromSlug(value) {
438
+ return value
439
+ .split(/[-_\s]+/g)
440
+ .filter(Boolean)
441
+ .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
442
+ .join(' ') || value;
443
+ }
444
+ function normalizeBoardColumnEntries(value) {
445
+ if (!Array.isArray(value))
446
+ return [];
447
+ return value.map((entry, index) => {
448
+ const source = isRecord(entry) ? entry : { name: entry };
449
+ const name = normalizeNonEmpty(source.name) ?? normalizeNonEmpty(source.title);
450
+ const slug = normalizeNonEmpty(source.slug) ?? (name ? slugify(name) : undefined);
451
+ const localId = normalizeNonEmpty(source.localId)
452
+ ?? normalizeNonEmpty(source.id)
453
+ ?? slug
454
+ ?? (name ? slugify(name) : `column-${index + 1}`);
455
+ const remoteColumnId = normalizeNonEmpty(source.remoteColumnId)
456
+ ?? normalizeNonEmpty(source.columnRemoteId)
457
+ ?? normalizeNonEmpty(source.columnId);
458
+ const remoteBoardColumnId = normalizeNonEmpty(source.remoteBoardColumnId)
459
+ ?? normalizeNonEmpty(source.boardColumnRemoteId)
460
+ ?? normalizeNonEmpty(source.boardColumnId);
461
+ return compactPayload({
462
+ localId,
463
+ id: normalizeNonEmpty(source.id) ?? localId,
464
+ name: name ?? (slug ? titleFromSlug(slug) : `Column ${index + 1}`),
465
+ slug,
466
+ position: numberField(source.position) ?? index,
467
+ description: normalizeNonEmpty(source.description),
468
+ wipLimit: numberField(source.wipLimit),
469
+ remoteColumnId,
470
+ columnId: remoteColumnId,
471
+ remoteBoardColumnId,
472
+ boardColumnId: remoteBoardColumnId,
473
+ });
474
+ });
475
+ }
476
+ function boardColumnsHash(value) {
477
+ if (!Array.isArray(value))
478
+ return undefined;
479
+ const columns = normalizeBoardColumnEntries(value).map((entry, index) => compactPayload({
480
+ localId: normalizeNonEmpty(entry.localId),
481
+ name: normalizeNonEmpty(entry.name),
482
+ slug: normalizeNonEmpty(entry.slug),
483
+ position: numberField(entry.position) ?? index,
484
+ description: normalizeNonEmpty(entry.description),
485
+ wipLimit: numberField(entry.wipLimit),
486
+ }));
487
+ return hashContent(JSON.stringify(columns));
488
+ }
489
+ function recordBaselineHash(record) {
490
+ const frontmatter = { ...record.frontmatter };
491
+ delete frontmatter.baseHash;
492
+ return hashContent(renderFrontmatterDocument(frontmatter, record.body));
493
+ }
494
+ function withBaseHash(frontmatter, body) {
495
+ const next = compactPayload({
496
+ ...frontmatter,
497
+ baseHash: undefined,
498
+ });
499
+ return {
500
+ ...next,
501
+ baseHash: hashContent(renderFrontmatterDocument(next, body)),
502
+ };
503
+ }
504
+ function hasManualLocalEdit(record) {
505
+ const baseHash = normalizeNonEmpty(record.frontmatter.baseHash);
506
+ return normalizeSyncState(record) === 'synced' && Boolean(baseHash) && recordBaselineHash(record) !== baseHash;
507
+ }
508
+ async function markManualLocalEditDirty(record) {
509
+ const frontmatter = {
510
+ ...record.frontmatter,
511
+ syncState: 'dirty',
512
+ updatedAt: new Date().toISOString(),
513
+ };
514
+ await writeFileWithRetry(record.filePath, renderFrontmatterDocument(frontmatter, record.body), 'utf8');
515
+ }
516
+ async function protectPullTargetIfLocalChanged(record) {
517
+ const syncState = normalizeSyncState(record);
518
+ if (['local', 'dirty', 'deleted', 'conflict'].includes(syncState))
519
+ return true;
520
+ if (!hasManualLocalEdit(record))
521
+ return false;
522
+ await markManualLocalEditDirty(record);
523
+ return true;
524
+ }
525
+ function normalizeSyncState(record) {
526
+ return normalizeNonEmpty(record.frontmatter.syncState) ?? 'local';
527
+ }
528
+ function getReadToolForEntity(entityType) {
529
+ switch (entityType) {
530
+ case 'projectman.board':
531
+ return 'projectman.kanban-board.get';
532
+ case 'projectman.kanban-task':
533
+ return 'projectman.kanban-task.get';
534
+ case 'projectman.sprint':
535
+ return 'projectman.sprint.get';
536
+ case 'projectman.issue':
537
+ return 'projectman.issue.get';
538
+ case 'projectman.feedback':
539
+ return 'projectman.feedback.get';
540
+ case 'projectman.review-request':
541
+ return 'projectman.review-request.get';
542
+ case 'agentspace.discussion-topic':
543
+ return 'agentspace.memory-item.get-by-id';
544
+ case 'agentspace.experience-item':
545
+ return 'agentspace.experience-item.get-by-id';
546
+ case 'agentspace.memory-item':
547
+ return 'agentspace.memory-item.get-by-id';
548
+ default:
549
+ return undefined;
550
+ }
551
+ }
552
+ function emit(options, envelope, fallback) {
553
+ if (options.json) {
554
+ console.log(JSON.stringify(envelope, null, 2));
555
+ return;
556
+ }
557
+ logSuccess(fallback);
558
+ console.log(JSON.stringify(envelope.result ?? envelope, null, 2));
559
+ }
560
+ async function writeState(repoRoot, payload) {
561
+ const statePath = path.join(repoRoot, '.aops', 'sync', 'state.json');
562
+ await fs.mkdir(path.dirname(statePath), { recursive: true });
563
+ const previous = await readState(repoRoot);
564
+ await writeFileWithRetry(statePath, `${JSON.stringify({
565
+ ...previous,
566
+ schemaVersion: 2,
567
+ updatedAt: new Date().toISOString(),
568
+ ...payload,
569
+ }, null, 2)}\n`, 'utf8');
570
+ return statePath;
571
+ }
572
+ async function readState(repoRoot) {
573
+ const statePath = path.join(repoRoot, '.aops', 'sync', 'state.json');
574
+ try {
575
+ const parsed = JSON.parse(await fs.readFile(statePath, 'utf8'));
576
+ return isRecord(parsed) ? parsed : {};
577
+ }
578
+ catch {
579
+ return {};
580
+ }
581
+ }
582
+ async function ensureGitignore(repoRoot) {
583
+ const gitignorePath = path.join(repoRoot, '.gitignore');
584
+ const entry = '.aops/sync/state.json';
585
+ let content = '';
586
+ try {
587
+ content = await fs.readFile(gitignorePath, 'utf8');
588
+ }
589
+ catch {
590
+ content = '';
591
+ }
592
+ if (content.split(/\r?\n/).includes(entry))
593
+ return;
594
+ const next = `${content.trimEnd()}${content.trimEnd() ? '\n' : ''}${entry}\n`;
595
+ await writeFileWithRetry(gitignorePath, next, 'utf8');
596
+ }
597
+ function commonFrontmatter(params) {
598
+ const now = new Date().toISOString();
599
+ return compactPayload({
600
+ schemaVersion: 2,
601
+ entityType: params.entityType,
602
+ localId: params.localId,
603
+ id: params.localId,
604
+ remoteId: params.remoteId,
605
+ projectId: params.projectId,
606
+ scopeId: params.scopeId,
607
+ createdAt: now,
608
+ updatedAt: now,
609
+ syncState: params.syncState ?? 'synced',
610
+ remoteUpdatedAt: params.remoteUpdatedAt,
611
+ lastPulledAt: now,
612
+ baseHash: params.remoteId ? undefined : hashContent(params.localId),
613
+ storage: 'local-cache',
614
+ });
615
+ }
616
+ async function writeProjectmanSeedFile(params) {
617
+ const remoteId = normalizeNonEmpty(params.remote.id);
618
+ const existing = remoteId ? await findRecordByRemoteId(params.dir, remoteId) : null;
619
+ if (!params.force && existing && await protectPullTargetIfLocalChanged(existing)) {
620
+ return relativePath(params.repoRoot, existing.filePath);
621
+ }
622
+ const localId = normalizeNonEmpty(existing?.frontmatter.localId) ?? randomUUID();
623
+ const title = normalizeNonEmpty(params.remote[params.titleKey]) ?? normalizeNonEmpty(params.remote.name) ?? normalizeNonEmpty(params.remote.title) ?? localId;
624
+ const filePath = existing?.filePath ?? path.join(params.dir, `${slugify(title)}-${localId.slice(0, 8)}.md`);
625
+ const body = `# ${title}\n`;
626
+ const frontmatter = withBaseHash({
627
+ ...commonFrontmatter({
628
+ entityType: params.entityType,
629
+ projectId: params.projectId,
630
+ scopeId: params.scopeId,
631
+ localId,
632
+ remoteId,
633
+ remoteUpdatedAt: normalizeNonEmpty(params.remote.updatedAt),
634
+ }),
635
+ ...params.remote,
636
+ id: localId,
637
+ localId,
638
+ remoteId,
639
+ syncState: 'synced',
640
+ storage: 'local-cache',
641
+ }, body);
642
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
643
+ await writeFileWithRetry(filePath, renderFrontmatterDocument(frontmatter, body), 'utf8');
644
+ return path.relative(params.repoRoot, filePath).split(path.sep).join('/');
645
+ }
646
+ function sprintSlugFromRecord(frontmatter) {
647
+ const sprintId = normalizeNonEmpty(frontmatter.localId)
648
+ ?? normalizeNonEmpty(frontmatter.id)
649
+ ?? normalizeNonEmpty(frontmatter.remoteId)
650
+ ?? 'unassigned';
651
+ return slugify(normalizeNonEmpty(frontmatter.slug) ?? normalizeNonEmpty(frontmatter.name) ?? sprintId);
652
+ }
653
+ function microtaskLocalId(microtask, fallback) {
654
+ return normalizeNonEmpty(microtask.localId) ?? fallback;
655
+ }
656
+ function chooseStableLocalId(params) {
657
+ const existingLocalId = normalizeNonEmpty(params.existingLocalId);
658
+ const candidateLocalId = normalizeNonEmpty(params.candidateLocalId);
659
+ const remoteId = normalizeNonEmpty(params.remoteId);
660
+ if (candidateLocalId && candidateLocalId !== remoteId && existingLocalId === remoteId)
661
+ return candidateLocalId;
662
+ return existingLocalId ?? candidateLocalId ?? params.fallback;
663
+ }
664
+ async function writeUtaskSeedFile(params) {
665
+ const paths = resolveRepoFirstProjectmanPaths(params);
666
+ const microtaskId = normalizeNonEmpty(params.microtask.id);
667
+ const microtaskLocalIdValue = normalizeNonEmpty(params.microtask.localId);
668
+ const remoteId = normalizeNonEmpty(params.microtask.remoteId)
669
+ ?? (params.idsAreRemote === true && microtaskId && microtaskId !== microtaskLocalIdValue ? microtaskId : undefined);
670
+ const existing = (remoteId ? await findRecordByRemoteId(paths.utasks, remoteId, { recursive: true }) : null)
671
+ ?? (normalizeNonEmpty(params.microtask.localId)
672
+ ? await findRecordByLocalId(paths.utasks, normalizeNonEmpty(params.microtask.localId), { recursive: true })
673
+ : null);
674
+ if (!params.force && existing && await protectPullTargetIfLocalChanged(existing)) {
675
+ return relativePath(params.repoRoot, existing.filePath);
676
+ }
677
+ const fallbackId = `${slugify(normalizeNonEmpty(params.microtask.title) ?? 'utask')}-${randomUUID().slice(0, 8)}`;
678
+ const candidateLocalId = microtaskLocalId(params.microtask, fallbackId);
679
+ const localId = chooseStableLocalId({
680
+ existingLocalId: normalizeNonEmpty(existing?.frontmatter.localId),
681
+ candidateLocalId,
682
+ remoteId,
683
+ fallback: fallbackId,
684
+ });
685
+ const title = normalizeNonEmpty(params.microtask.title) ?? localId;
686
+ const sprintLocalId = normalizeNonEmpty(params.sprint.localId) ?? normalizeNonEmpty(params.sprint.id) ?? normalizeNonEmpty(params.sprint.remoteId);
687
+ const sprintRemoteId = normalizeNonEmpty(params.sprint.remoteId);
688
+ const sprintSlug = sprintSlugFromRecord(params.sprint);
689
+ const body = `# ${title}\n`;
690
+ const frontmatter = withBaseHash({
691
+ ...commonFrontmatter({
692
+ entityType: 'projectman.utask',
693
+ projectId: params.projectId,
694
+ scopeId: params.scopeId,
695
+ localId,
696
+ remoteId,
697
+ remoteUpdatedAt: normalizeNonEmpty(params.microtask.updatedAt),
698
+ }),
699
+ ...params.microtask,
700
+ id: localId,
701
+ localId,
702
+ remoteId,
703
+ sprintLocalId,
704
+ sprintRemoteId,
705
+ sprintId: sprintLocalId,
706
+ sprintSlug,
707
+ phaseId: normalizeNonEmpty(params.phase.localId) ?? normalizeNonEmpty(params.phase.id),
708
+ phaseRemoteId: normalizeNonEmpty(params.phase.remoteId) ?? normalizeNonEmpty(params.phase.id),
709
+ phaseName: normalizeNonEmpty(params.phase.name),
710
+ title,
711
+ status: normalizeNonEmpty(params.microtask.status) ?? 'todo',
712
+ position: numberField(params.microtask.position),
713
+ notes: normalizeNonEmpty(params.microtask.notes),
714
+ syncState: 'synced',
715
+ storage: 'local-cache',
716
+ }, body);
717
+ const filePath = existing?.filePath ?? path.join(paths.utasks, sprintSlug, `${slugify(title)}-${localId.slice(0, 8)}.md`);
718
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
719
+ await writeFileWithRetry(filePath, renderFrontmatterDocument(frontmatter, body), 'utf8');
720
+ return relativePath(params.repoRoot, filePath);
721
+ }
722
+ async function writeUtaskSeedFilesFromSprint(params) {
723
+ const files = [];
724
+ for (const phase of toRecordArray(params.sprint.phases)) {
725
+ for (const microtask of toRecordArray(phase.microtasks)) {
726
+ files.push(await writeUtaskSeedFile({
727
+ repoRoot: params.repoRoot,
728
+ localRoot: params.localRoot,
729
+ sprint: params.sprint,
730
+ phase,
731
+ microtask,
732
+ projectId: params.projectId,
733
+ scopeId: params.scopeId,
734
+ force: params.force,
735
+ idsAreRemote: params.idsAreRemote,
736
+ }));
737
+ }
738
+ }
739
+ return files;
740
+ }
741
+ function projectmanSeedConfig(context, entityType) {
742
+ const paths = resolveRepoFirstProjectmanPaths(context);
743
+ switch (entityType) {
744
+ case 'projectman.board':
745
+ return { dir: paths.boards, titleKey: 'name' };
746
+ case 'projectman.kanban-task':
747
+ return { dir: paths.tasks, titleKey: 'title' };
748
+ case 'projectman.sprint':
749
+ return { dir: paths.sprints, titleKey: 'name' };
750
+ case 'projectman.issue':
751
+ return { dir: paths.issues, titleKey: 'title' };
752
+ case 'projectman.feedback':
753
+ return { dir: paths.feedback, titleKey: 'title' };
754
+ case 'projectman.review-request':
755
+ return { dir: paths.reviewRequests, titleKey: 'title' };
756
+ default:
757
+ return null;
758
+ }
759
+ }
760
+ async function writeMemorySeedFile(params) {
761
+ const remoteId = normalizeNonEmpty(params.remote.id);
762
+ const existing = remoteId ? await findRecordByRemoteId(params.dir, remoteId) : null;
763
+ if (!params.force && existing && await protectPullTargetIfLocalChanged(existing)) {
764
+ return relativePath(params.repoRoot, existing.filePath);
765
+ }
766
+ const id = normalizeNonEmpty(existing?.frontmatter.localId) ?? randomUUID();
767
+ const meta = isRecord(params.remote.meta) ? params.remote.meta : {};
768
+ const body = normalizeNonEmpty(params.remote.content) ?? '';
769
+ const memory = withBaseHash({
770
+ ...meta,
771
+ schemaVersion: 2,
772
+ entityType: 'agentspace.memory-item',
773
+ id,
774
+ localId: id,
775
+ remoteId,
776
+ syncState: 'synced',
777
+ storage: 'local-cache',
778
+ memoryId: id,
779
+ kind: normalizeNonEmpty(params.remote.kind),
780
+ durability: normalizeNonEmpty(params.remote.durability),
781
+ subjectType: normalizeNonEmpty(params.remote.sourceType) ?? normalizeNonEmpty(meta.subjectType),
782
+ subjectId: normalizeNonEmpty(params.remote.sourceId) ?? normalizeNonEmpty(meta.subjectId),
783
+ subjectTitle: normalizeNonEmpty(meta.subjectTitle),
784
+ projectId: params.projectId,
785
+ scopeId: params.scopeId,
786
+ tags: Array.isArray(params.remote.tags) ? params.remote.tags : undefined,
787
+ createdAt: normalizeNonEmpty(params.remote.createdAt),
788
+ updatedAt: normalizeNonEmpty(params.remote.updatedAt),
789
+ }, body);
790
+ const filePath = existing?.filePath ?? path.join(params.dir, `${slugify(normalizeNonEmpty(params.remote.kind) ?? 'memory')}-${id.slice(0, 8)}.md`);
791
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
792
+ await writeFileWithRetry(filePath, renderFrontmatterDocument(memory, body), 'utf8');
793
+ return path.relative(params.repoRoot, filePath).split(path.sep).join('/');
794
+ }
795
+ async function writeExperienceSeedFile(params) {
796
+ const remoteId = normalizeNonEmpty(params.remote.id);
797
+ const existing = remoteId ? await findRecordByRemoteId(params.dir, remoteId) : null;
798
+ if (!params.force && existing && await protectPullTargetIfLocalChanged(existing)) {
799
+ return relativePath(params.repoRoot, existing.filePath);
800
+ }
801
+ const id = normalizeNonEmpty(existing?.frontmatter.localId) ?? randomUUID();
802
+ const body = normalizeNonEmpty(params.remote.content) ?? '';
803
+ const frontmatter = withBaseHash({
804
+ ...commonFrontmatter({
805
+ entityType: 'agentspace.experience-item',
806
+ projectId: params.projectId,
807
+ scopeId: params.scopeId,
808
+ localId: id,
809
+ remoteId,
810
+ remoteUpdatedAt: normalizeNonEmpty(params.remote.updatedAt),
811
+ }),
812
+ type: normalizeNonEmpty(params.remote.type),
813
+ title: normalizeNonEmpty(params.remote.title),
814
+ problem: normalizeNonEmpty(params.remote.problem),
815
+ solution: normalizeNonEmpty(params.remote.solution),
816
+ areas: Array.isArray(params.remote.areas) ? params.remote.areas : undefined,
817
+ stack: Array.isArray(params.remote.stack) ? params.remote.stack : undefined,
818
+ commands: Array.isArray(params.remote.commands) ? params.remote.commands : undefined,
819
+ files: Array.isArray(params.remote.files) ? params.remote.files : undefined,
820
+ sourceRefs: Array.isArray(params.remote.sourceRefs) ? params.remote.sourceRefs : undefined,
821
+ tags: Array.isArray(params.remote.tags) ? params.remote.tags : undefined,
822
+ confidence: normalizeNonEmpty(params.remote.confidence),
823
+ reusability: normalizeNonEmpty(params.remote.reusability),
824
+ meta: isRecord(params.remote.meta) ? params.remote.meta : undefined,
825
+ id,
826
+ localId: id,
827
+ remoteId,
828
+ syncState: 'synced',
829
+ storage: 'local-cache',
830
+ createdAt: normalizeNonEmpty(params.remote.createdAt),
831
+ updatedAt: normalizeNonEmpty(params.remote.updatedAt),
832
+ }, body);
833
+ const filePath = existing?.filePath ?? path.join(params.dir, `${slugify(normalizeNonEmpty(params.remote.type) ?? 'experience')}-${slugify(normalizeNonEmpty(params.remote.title) ?? id)}-${id.slice(0, 8)}.md`);
834
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
835
+ await writeFileWithRetry(filePath, renderFrontmatterDocument(frontmatter, body), 'utf8');
836
+ return path.relative(params.repoRoot, filePath).split(path.sep).join('/');
837
+ }
838
+ async function invokeRead(apiState, options, toolId, input) {
839
+ const payload = await invokeHostedToolWithApiState(apiState, {
840
+ ...options,
841
+ toolId,
842
+ input,
843
+ disableDurableActivityLog: true,
844
+ });
845
+ return unwrapHostedToolResult(payload);
846
+ }
847
+ function buildHostedMirrorFallbackBody(kind, name) {
848
+ const label = kind === 'skill' ? 'skill' : 'prompt';
849
+ return [
850
+ `# ${name}`,
851
+ '',
852
+ `No current hosted ${label} version is published.`,
853
+ ].join('\n');
854
+ }
855
+ function buildHostedSkillMirrorItem(project, skill, currentVersion) {
856
+ const remoteId = normalizeNonEmpty(skill.id);
857
+ if (!remoteId)
858
+ throw new Error('Hosted skill mirror item is missing id.');
859
+ const name = normalizeNonEmpty(skill.name) ?? remoteId;
860
+ const body = normalizeNonEmpty(currentVersion?.content) ?? buildHostedMirrorFallbackBody('skill', name);
861
+ return {
862
+ kind: 'skill',
863
+ remoteId,
864
+ name,
865
+ body,
866
+ frontmatter: compactPayload({
867
+ schemaVersion: 2,
868
+ entityType: 'agentspace.hosted-skill-current',
869
+ storage: 'hosted-pull',
870
+ readOnly: true,
871
+ remoteId,
872
+ currentVersionId: normalizeNonEmpty(skill.currentVersionId),
873
+ currentVersion: normalizeNonEmpty(currentVersion?.version),
874
+ currentVersionStatus: normalizeNonEmpty(currentVersion?.status),
875
+ name,
876
+ description: normalizeNonEmpty(skill.description),
877
+ shortDescription: normalizeNonEmpty(skill.shortDescription),
878
+ tags: toStringArray(skill.tags),
879
+ entryFile: normalizeNonEmpty(currentVersion?.entryFile),
880
+ skillStandard: normalizeNonEmpty(currentVersion?.skillStandard),
881
+ refType: normalizeNonEmpty(currentVersion?.refType),
882
+ refId: normalizeNonEmpty(currentVersion?.refId),
883
+ meta: isRecord(currentVersion?.meta) ? currentVersion?.meta : undefined,
884
+ sourceProjectId: normalizeNonEmpty(project.projectId),
885
+ sourceProjectName: normalizeNonEmpty(project.projectName),
886
+ sourceProjectSlug: normalizeNonEmpty(project.projectSlug),
887
+ sourceScopeId: normalizeNonEmpty(project.scopeId),
888
+ pulledAt: new Date().toISOString(),
889
+ shellUpdatedAt: normalizeNonEmpty(skill.updatedAt),
890
+ versionUpdatedAt: normalizeNonEmpty(currentVersion?.updatedAt),
891
+ }),
892
+ };
893
+ }
894
+ function buildHostedPromptMirrorItem(project, prompt, currentVersion) {
895
+ const remoteId = normalizeNonEmpty(prompt.id);
896
+ if (!remoteId)
897
+ throw new Error('Hosted prompt mirror item is missing id.');
898
+ const name = normalizeNonEmpty(prompt.name) ?? remoteId;
899
+ const body = normalizeNonEmpty(currentVersion?.content) ?? buildHostedMirrorFallbackBody('prompt', name);
900
+ return {
901
+ kind: 'prompt',
902
+ remoteId,
903
+ name,
904
+ body,
905
+ frontmatter: compactPayload({
906
+ schemaVersion: 2,
907
+ entityType: 'agentspace.hosted-prompt-current',
908
+ storage: 'hosted-pull',
909
+ readOnly: true,
910
+ remoteId,
911
+ currentVersionId: normalizeNonEmpty(prompt.currentVersionId),
912
+ currentVersion: normalizeNonEmpty(currentVersion?.version),
913
+ currentVersionStatus: normalizeNonEmpty(currentVersion?.status),
914
+ name,
915
+ description: normalizeNonEmpty(prompt.description),
916
+ status: normalizeNonEmpty(prompt.status),
917
+ tags: toStringArray(prompt.tags),
918
+ variables: currentVersion?.variables,
919
+ meta: isRecord(currentVersion?.meta) ? currentVersion?.meta : undefined,
920
+ refType: normalizeNonEmpty(currentVersion?.refType),
921
+ refId: normalizeNonEmpty(currentVersion?.refId),
922
+ sourceProjectId: normalizeNonEmpty(project.projectId),
923
+ sourceProjectName: normalizeNonEmpty(project.projectName),
924
+ sourceProjectSlug: normalizeNonEmpty(project.projectSlug),
925
+ sourceScopeId: normalizeNonEmpty(project.scopeId),
926
+ pulledAt: new Date().toISOString(),
927
+ shellUpdatedAt: normalizeNonEmpty(prompt.updatedAt),
928
+ versionUpdatedAt: normalizeNonEmpty(currentVersion?.updatedAt),
929
+ }),
930
+ };
931
+ }
932
+ async function resolveHostedProjectById(apiState, options, id) {
933
+ try {
934
+ const data = extractResultData(await invokeRead(apiState, options, 'agentspace.project.get-by-id', { id }));
935
+ if (!isRecord(data)) {
936
+ return {
937
+ projectId: id,
938
+ scopeId: id,
939
+ };
940
+ }
941
+ return compactPayload({
942
+ projectId: normalizeNonEmpty(data.id) ?? id,
943
+ projectName: normalizeNonEmpty(data.name),
944
+ projectSlug: normalizeNonEmpty(data.slug),
945
+ scopeId: resolveOwnerScopeIdFromProjectRecord(data, id),
946
+ });
947
+ }
948
+ catch {
949
+ return {
950
+ projectId: id,
951
+ scopeId: id,
952
+ };
953
+ }
954
+ }
955
+ async function resolveHostedProjectByFilter(apiState, options, filter, label) {
956
+ let rows = toRecordArray(await invokeRead(apiState, options, 'agentspace.project.list-projects', {
957
+ filter,
958
+ options: { limit: 20 },
959
+ }));
960
+ let usedUnfilteredSlugFallback = false;
961
+ if (rows.length === 0 && filter.slug) {
962
+ rows = toRecordArray(await invokeRead(apiState, options, 'agentspace.project.list-projects', {
963
+ filter: {},
964
+ options: { limit: 500 },
965
+ }));
966
+ usedUnfilteredSlugFallback = true;
967
+ }
968
+ if (rows.length === 0) {
969
+ throw new Error(`Hosted project mirror target not found for ${label}.`);
970
+ }
971
+ const exactRows = rows.filter((row) => {
972
+ if (filter.slug)
973
+ return matchesHostedProjectSlugSelector(row, normalizeNonEmpty(filter.slug));
974
+ if (filter.name)
975
+ return normalizeNonEmpty(row.name)?.toLowerCase() === normalizeNonEmpty(filter.name)?.toLowerCase();
976
+ return true;
977
+ });
978
+ if (usedUnfilteredSlugFallback && exactRows.length === 0) {
979
+ throw new Error(`Hosted project mirror target not found for ${label}.`);
980
+ }
981
+ const matches = exactRows.length > 0 ? exactRows : rows;
982
+ if (matches.length > 1) {
983
+ const ids = matches
984
+ .map((row) => `${normalizeNonEmpty(row.name) ?? normalizeNonEmpty(row.slug) ?? normalizeNonEmpty(row.id) ?? 'unknown'} (${normalizeNonEmpty(row.id) ?? '-'})`)
985
+ .join(', ');
986
+ throw new Error(`Hosted project mirror target is ambiguous for ${label}: ${ids}.`);
987
+ }
988
+ return resolveHostedProjectById(apiState, options, normalizeNonEmpty(matches[0]?.id) ?? label);
989
+ }
990
+ function matchesHostedProjectSlugSelector(row, selector) {
991
+ const desiredSlug = selector ? slugify(selector) : undefined;
992
+ if (!desiredSlug)
993
+ return false;
994
+ const name = normalizeNonEmpty(row.name);
995
+ const aliases = [
996
+ normalizeNonEmpty(row.slug),
997
+ normalizeNonEmpty(row.projectSlug),
998
+ name ? slugify(name) : undefined,
999
+ ];
1000
+ return aliases.some((alias) => alias ? slugify(alias) === desiredSlug : false);
1001
+ }
1002
+ async function resolveHostedMirrorTargets(apiState, options, context) {
1003
+ const targets = [];
1004
+ if (normalizeNonEmpty(context.projectId)) {
1005
+ targets.push(await resolveHostedProjectById(apiState, options, normalizeNonEmpty(context.projectId)));
1006
+ }
1007
+ else {
1008
+ targets.push(compactPayload({
1009
+ projectId: normalizeNonEmpty(context.projectId),
1010
+ projectName: normalizeNonEmpty(context.projectName),
1011
+ projectSlug: normalizeNonEmpty(context.projectSlug),
1012
+ scopeId: normalizeNonEmpty(context.scopeId),
1013
+ }));
1014
+ }
1015
+ for (const projectId of options.hostedProjectId ?? []) {
1016
+ targets.push(await resolveHostedProjectById(apiState, options, projectId));
1017
+ }
1018
+ for (const projectName of options.hostedProjectName ?? []) {
1019
+ targets.push(await resolveHostedProjectByFilter(apiState, options, { name: projectName }, `name "${projectName}"`));
1020
+ }
1021
+ for (const projectSlug of options.hostedProjectSlug ?? []) {
1022
+ targets.push(await resolveHostedProjectByFilter(apiState, options, { slug: projectSlug }, `slug "${projectSlug}"`));
1023
+ }
1024
+ const deduped = new Map();
1025
+ for (const target of targets) {
1026
+ const key = normalizeNonEmpty(target.projectId)
1027
+ ?? normalizeNonEmpty(target.scopeId)
1028
+ ?? normalizeNonEmpty(target.projectSlug)
1029
+ ?? normalizeNonEmpty(target.projectName);
1030
+ if (key)
1031
+ deduped.set(key, target);
1032
+ }
1033
+ return [...deduped.values()];
1034
+ }
1035
+ async function buildHostedMirrorItemsForProject(apiState, options, project) {
1036
+ const scopeId = normalizeNonEmpty(project.scopeId) ?? normalizeNonEmpty(project.projectId);
1037
+ if (!scopeId)
1038
+ return { skillItems: [], promptItems: [] };
1039
+ const [skills, prompts] = await Promise.all([
1040
+ toRecordArray(await invokeRead(apiState, options, 'agentspace.skill.list-skills', {
1041
+ filter: compactPayload({ scopeId, scopeResolution: 'cascade' }),
1042
+ options: { limit: 500 },
1043
+ })),
1044
+ toRecordArray(await invokeRead(apiState, options, 'agentspace.prompt.list-prompts', {
1045
+ filter: compactPayload({ scopeId, scopeResolution: 'cascade', limit: 500 }),
1046
+ })),
1047
+ ]);
1048
+ const skillItems = await Promise.all(skills.map(async (skill) => {
1049
+ const currentVersionId = normalizeNonEmpty(skill.currentVersionId);
1050
+ const currentVersion = currentVersionId
1051
+ ? extractResultData(await invokeRead(apiState, options, 'agentspace.skill-version.get-skill-version', { id: currentVersionId }))
1052
+ : null;
1053
+ return buildHostedSkillMirrorItem(project, skill, isRecord(currentVersion) ? currentVersion : null);
1054
+ }));
1055
+ const promptItems = await Promise.all(prompts.map(async (prompt) => {
1056
+ const currentVersionId = normalizeNonEmpty(prompt.currentVersionId);
1057
+ const currentVersion = currentVersionId
1058
+ ? extractResultData(await invokeRead(apiState, options, 'agentspace.prompt-version.get-prompt-version', { id: currentVersionId }))
1059
+ : null;
1060
+ return buildHostedPromptMirrorItem(project, prompt, isRecord(currentVersion) ? currentVersion : null);
1061
+ }));
1062
+ return { skillItems, promptItems };
1063
+ }
1064
+ async function readRemoteBoardColumns(apiState, options, boardRemoteId) {
1065
+ const boardColumnRows = toRecordArray(await invokeRead(apiState, options, 'projectman.kanban-board-column.list', { board: boardRemoteId }));
1066
+ const rows = [];
1067
+ for (const row of boardColumnRows.sort((left, right) => (numberField(left.position) ?? 0) - (numberField(right.position) ?? 0))) {
1068
+ const boardColumnId = normalizeNonEmpty(row.id) ?? normalizeNonEmpty(row.boardColumnId);
1069
+ const columnId = normalizeNonEmpty(row.columnId) ?? normalizeNonEmpty(row.column);
1070
+ let column = {};
1071
+ if (columnId) {
1072
+ try {
1073
+ column = extractResultData(await invokeRead(apiState, options, 'projectman.kanban-column.get', { id: columnId })) ?? {};
1074
+ }
1075
+ catch {
1076
+ column = {};
1077
+ }
1078
+ }
1079
+ const name = normalizeNonEmpty(column.name) ?? normalizeNonEmpty(row.name) ?? normalizeNonEmpty(row.columnName) ?? columnId ?? boardColumnId;
1080
+ const slug = normalizeNonEmpty(column.slug) ?? normalizeNonEmpty(row.slug) ?? normalizeNonEmpty(row.columnSlug) ?? (name ? slugify(name) : undefined);
1081
+ rows.push(compactPayload({
1082
+ localId: boardColumnId ?? columnId ?? slug,
1083
+ id: boardColumnId ?? columnId ?? slug,
1084
+ name,
1085
+ slug,
1086
+ position: numberField(row.position) ?? rows.length,
1087
+ description: normalizeNonEmpty(column.description),
1088
+ wipLimit: numberField(column.wipLimit),
1089
+ remoteColumnId: columnId,
1090
+ columnId,
1091
+ remoteBoardColumnId: boardColumnId,
1092
+ boardColumnId,
1093
+ }));
1094
+ }
1095
+ return rows;
1096
+ }
1097
+ async function collectSyncStatusData(context, options) {
1098
+ const sourceRecords = await readRepoFirstSourceRecords(context);
1099
+ const selection = buildSyncSelection(context.repoRoot, sourceRecords, options);
1100
+ const records = await collectRepoFirstSyncRecords(context);
1101
+ const selectedRecords = selection.active
1102
+ ? records.filter((record) => selection.selectedPaths.has(normalizeNonEmpty(record.path) ?? ''))
1103
+ : records;
1104
+ const byState = selectedRecords.reduce((acc, record) => {
1105
+ const key = normalizeNonEmpty(record.syncState) ?? 'unknown';
1106
+ acc[key] = (acc[key] ?? 0) + 1;
1107
+ return acc;
1108
+ }, {});
1109
+ await ensureGitignore(context.repoRoot);
1110
+ const state = await readState(context.repoRoot);
1111
+ const lastPush = isRecord(state.lastPush) ? state.lastPush : undefined;
1112
+ const statePath = await writeState(context.repoRoot, { lastStatus: { byState, recordCount: selectedRecords.length, selection: selection.active ? selection.filters : undefined } });
1113
+ return { recordCount: selectedRecords.length, totalRecordCount: records.length, byState, records: selectedRecords, selection: selection.active ? selection.filters : undefined, lastPush, statePath };
1114
+ }
1115
+ async function runSyncStatus(options) {
1116
+ try {
1117
+ if (options.allProjects === true) {
1118
+ await runForAllProjects(options, 'sync.status', async (projectOptions, context) => collectSyncStatusData(context, projectOptions));
1119
+ return;
1120
+ }
1121
+ const context = await resolveProjectBindingContext(options, { requireProject: true });
1122
+ const data = await collectSyncStatusData(context, options);
1123
+ emit(options, {
1124
+ command: 'sync.status',
1125
+ surface: 'hosted-cache-sync',
1126
+ resolvedContext: context,
1127
+ result: { ok: true, data },
1128
+ }, 'Local cache sync status loaded.');
1129
+ }
1130
+ catch (error) {
1131
+ logError(error instanceof Error ? error.message : String(error));
1132
+ process.exitCode = 1;
1133
+ }
1134
+ }
1135
+ async function collectSyncDiffData(context, options) {
1136
+ const sourceRecords = await readRepoFirstSourceRecords(context);
1137
+ const selection = buildSyncSelection(context.repoRoot, sourceRecords, options);
1138
+ const records = await collectRepoFirstSyncRecords(context);
1139
+ const selectedRecords = selection.active
1140
+ ? records.filter((record) => selection.selectedPaths.has(normalizeNonEmpty(record.path) ?? ''))
1141
+ : records;
1142
+ const changed = selectedRecords.filter((record) => ['local', 'dirty', 'deleted', 'conflict'].includes(String(record.syncState)));
1143
+ return { changedCount: changed.length, totalRecordCount: records.length, selectedRecordCount: selectedRecords.length, selection: selection.active ? selection.filters : undefined, changed };
1144
+ }
1145
+ async function runSyncDiff(options) {
1146
+ try {
1147
+ if (options.allProjects === true) {
1148
+ await runForAllProjects(options, 'sync.diff', async (projectOptions, context) => collectSyncDiffData(context, projectOptions));
1149
+ return;
1150
+ }
1151
+ const context = await resolveProjectBindingContext(options, { requireProject: true });
1152
+ const data = await collectSyncDiffData(context, options);
1153
+ emit(options, {
1154
+ command: 'sync.diff',
1155
+ surface: 'hosted-cache-sync',
1156
+ resolvedContext: context,
1157
+ result: { ok: true, data },
1158
+ }, 'Local cache sync diff loaded.');
1159
+ }
1160
+ catch (error) {
1161
+ logError(error instanceof Error ? error.message : String(error));
1162
+ process.exitCode = 1;
1163
+ }
1164
+ }
1165
+ async function collectSyncPullData(options, command, context, apiState) {
1166
+ const projectId = normalizeNonEmpty(context.projectId);
1167
+ const scopeId = normalizeNonEmpty(context.scopeId) ?? projectId;
1168
+ const pmPaths = resolveRepoFirstProjectmanPaths(context);
1169
+ const agPaths = resolveRepoFirstAgentspacePaths(context);
1170
+ const filesWritten = [];
1171
+ const [boards, tasks, sprints, issues, feedback, reviewRequests, experiences, memories] = await Promise.all([
1172
+ invokeRead(apiState, options, 'projectman.kanban-board.list', compactPayload({ scopeId, project: projectId })),
1173
+ invokeRead(apiState, options, 'projectman.kanban-task.list', compactPayload({ scopeId, project: projectId })),
1174
+ invokeRead(apiState, options, 'projectman.sprint.list', compactPayload({ scopeId, project: projectId })),
1175
+ invokeRead(apiState, options, 'projectman.issue.list', compactPayload({ scopeId })),
1176
+ invokeRead(apiState, options, 'projectman.feedback.list', compactPayload({ scopeId })),
1177
+ invokeRead(apiState, options, 'projectman.review-request.list', compactPayload({ scopeId })),
1178
+ invokeRead(apiState, options, 'agentspace.experience-item.list-experience-items', {
1179
+ filter: compactPayload({ scopeId, scopeResolution: 'cascade' }),
1180
+ options: { limit: 500 },
1181
+ }),
1182
+ invokeRead(apiState, options, 'agentspace.memory-item.list-memory-items', {
1183
+ filter: compactPayload({ scopeId, scopeResolution: 'cascade', projectId }),
1184
+ options: { limit: 500 },
1185
+ }),
1186
+ ]);
1187
+ for (const row of toRecordArray(boards)) {
1188
+ const remoteId = normalizeNonEmpty(row.id);
1189
+ let remote = row;
1190
+ if (remoteId) {
1191
+ const columns = await readRemoteBoardColumns(apiState, options, remoteId);
1192
+ if (columns.length > 0) {
1193
+ remote = {
1194
+ ...row,
1195
+ columns,
1196
+ lastSyncedColumnsHash: boardColumnsHash(columns),
1197
+ };
1198
+ }
1199
+ }
1200
+ filesWritten.push(await writeProjectmanSeedFile({ repoRoot: context.repoRoot, dir: pmPaths.boards, entityType: 'projectman.board', remote, projectId, scopeId, titleKey: 'name' }));
1201
+ }
1202
+ for (const row of toRecordArray(tasks)) {
1203
+ filesWritten.push(await writeProjectmanSeedFile({ repoRoot: context.repoRoot, dir: pmPaths.tasks, entityType: 'projectman.kanban-task', remote: row, projectId, scopeId, titleKey: 'title' }));
1204
+ }
1205
+ for (const row of toRecordArray(sprints)) {
1206
+ const sprintPath = await writeProjectmanSeedFile({ repoRoot: context.repoRoot, dir: pmPaths.sprints, entityType: 'projectman.sprint', remote: row, projectId, scopeId, titleKey: 'name' });
1207
+ filesWritten.push(sprintPath);
1208
+ const sprintRecord = parseFrontmatterDocument(await fs.readFile(path.join(context.repoRoot, sprintPath), 'utf8'));
1209
+ if (normalizeSyncState({ filePath: sprintPath, frontmatter: sprintRecord.frontmatter, body: sprintRecord.body }) === 'synced') {
1210
+ filesWritten.push(...await writeUtaskSeedFilesFromSprint({
1211
+ repoRoot: context.repoRoot,
1212
+ localRoot: context.localRoot,
1213
+ sprint: sprintRecord.frontmatter,
1214
+ projectId,
1215
+ scopeId,
1216
+ idsAreRemote: true,
1217
+ }));
1218
+ }
1219
+ }
1220
+ for (const row of toRecordArray(issues)) {
1221
+ filesWritten.push(await writeProjectmanSeedFile({ repoRoot: context.repoRoot, dir: pmPaths.issues, entityType: 'projectman.issue', remote: row, projectId, scopeId, titleKey: 'title' }));
1222
+ }
1223
+ for (const row of toRecordArray(feedback)) {
1224
+ filesWritten.push(await writeProjectmanSeedFile({ repoRoot: context.repoRoot, dir: pmPaths.feedback, entityType: 'projectman.feedback', remote: row, projectId, scopeId, titleKey: 'title' }));
1225
+ }
1226
+ for (const row of toRecordArray(reviewRequests)) {
1227
+ filesWritten.push(await writeProjectmanSeedFile({ repoRoot: context.repoRoot, dir: pmPaths.reviewRequests, entityType: 'projectman.review-request', remote: row, projectId, scopeId, titleKey: 'title' }));
1228
+ }
1229
+ for (const row of toRecordArray(experiences)) {
1230
+ filesWritten.push(await writeExperienceSeedFile({ repoRoot: context.repoRoot, dir: agPaths.experienceItems, remote: row, projectId, scopeId }));
1231
+ }
1232
+ for (const row of toRecordArray(memories)) {
1233
+ filesWritten.push(await writeMemorySeedFile({ repoRoot: context.repoRoot, dir: agPaths.memoryItems, remote: row, projectId, scopeId }));
1234
+ }
1235
+ const hostedMirrorTargets = await resolveHostedMirrorTargets(apiState, options, context);
1236
+ const touchedHostedProjectKeys = hostedMirrorTargets.map((target) => hostedProjectKey(target));
1237
+ const hostedSkillItems = [];
1238
+ const hostedPromptItems = [];
1239
+ for (const target of hostedMirrorTargets) {
1240
+ const mirrorItems = await buildHostedMirrorItemsForProject(apiState, options, target);
1241
+ hostedSkillItems.push(...mirrorItems.skillItems);
1242
+ hostedPromptItems.push(...mirrorItems.promptItems);
1243
+ }
1244
+ filesWritten.push(...await syncHostedMirrorKind(context.repoRoot, 'skill', hostedSkillItems, { touchedProjectKeys: touchedHostedProjectKeys }));
1245
+ filesWritten.push(...await syncHostedMirrorKind(context.repoRoot, 'prompt', hostedPromptItems, { touchedProjectKeys: touchedHostedProjectKeys }));
1246
+ await rebuildProjectmanViews(context);
1247
+ const experienceItems = await readExperienceItems(agPaths.experienceItems);
1248
+ await rebuildExperienceWorkspace(context, experienceItems);
1249
+ const memoryItems = await readLocalMemoryEntries(agPaths.memoryItems);
1250
+ await rebuildLocalMemoryWorkspace({
1251
+ ...context,
1252
+ items: memoryItems,
1253
+ });
1254
+ filesWritten.push(...await rebuildHostedWorkspace(context.repoRoot));
1255
+ await ensureGitignore(context.repoRoot);
1256
+ const statePath = await writeState(context.repoRoot, { lastPull: { command, filesWritten } });
1257
+ return { filesWritten, statePath };
1258
+ }
1259
+ async function runSyncPullOrBootstrap(options, command) {
1260
+ try {
1261
+ if (command === 'sync.bootstrap' && options.fromServer !== true) {
1262
+ throw new Error('sync bootstrap requires --from-server.');
1263
+ }
1264
+ if (options.apply !== true)
1265
+ throw new Error(`${command} writes local cache source files. Retry with --apply.`);
1266
+ if (options.allProjects === true) {
1267
+ await runForAllProjects(options, command, async (projectOptions, context, apiState) => {
1268
+ if (!apiState)
1269
+ throw new Error(`${command} requires hosted API state.`);
1270
+ return collectSyncPullData(projectOptions, command, context, apiState);
1271
+ }, { requireApi: true });
1272
+ return;
1273
+ }
1274
+ const context = await resolveProjectBindingContext(options, { requireProject: true });
1275
+ const apiState = await requireApiState(options);
1276
+ if (!apiState)
1277
+ return;
1278
+ const data = await collectSyncPullData(options, command, context, apiState);
1279
+ emit(options, {
1280
+ command,
1281
+ surface: 'hosted-cache-sync',
1282
+ resolvedContext: context,
1283
+ result: { ok: true, data },
1284
+ }, `${command} completed.`);
1285
+ }
1286
+ catch (error) {
1287
+ logError(error instanceof Error ? error.message : String(error));
1288
+ process.exitCode = 1;
1289
+ }
1290
+ }
1291
+ const SIDECAR_CONTRACT_VERSION = 'aops-cockpit-sidecar-v1';
1292
+ const DEFAULT_SIDECAR_HOST = '127.0.0.1';
1293
+ const DEFAULT_SIDECAR_PORT = 18459;
1294
+ const DEFAULT_SIDECAR_ORIGINS = new Set([
1295
+ 'http://localhost:5940',
1296
+ 'http://127.0.0.1:5940',
1297
+ 'http://localhost:5941',
1298
+ 'http://127.0.0.1:5941',
1299
+ 'http://localhost:5174',
1300
+ 'http://127.0.0.1:5174',
1301
+ ]);
1302
+ async function findAopsRepoRootUpward(startDir = process.cwd()) {
1303
+ let current = path.resolve(startDir);
1304
+ while (true) {
1305
+ if (existsSync(path.join(current, '.aops', 'aops.config.json')) && await readAopsRepoConfigReadOnly(current)) {
1306
+ return current;
1307
+ }
1308
+ const parent = path.dirname(current);
1309
+ if (parent === current)
1310
+ return null;
1311
+ current = parent;
1312
+ }
1313
+ }
1314
+ const SIDECAR_REPO_DISCOVERY_SKIP_DIRS = new Set([
1315
+ '.git',
1316
+ '.next',
1317
+ '.turbo',
1318
+ 'coverage',
1319
+ 'dist',
1320
+ 'node_modules',
1321
+ ]);
1322
+ function normalizedEquals(left, right) {
1323
+ const l = normalizeNonEmpty(left)?.toLowerCase();
1324
+ const r = normalizeNonEmpty(right)?.toLowerCase();
1325
+ return Boolean(l && r && l === r);
1326
+ }
1327
+ function scoreSidecarRepoCandidate(candidateRoot, config, options) {
1328
+ const projectSlug = normalizeNonEmpty(options.projectSlug);
1329
+ const projectName = normalizeNonEmpty(options.projectName);
1330
+ const projectId = normalizeNonEmpty(options.projectId);
1331
+ const scopeId = normalizeNonEmpty(options.scopeId);
1332
+ const hasExplicitSelector = Boolean(projectSlug || projectName || projectId || scopeId);
1333
+ const repoName = normalizeNonEmpty(config.repo?.name);
1334
+ const baseName = path.basename(candidateRoot);
1335
+ let score = 0;
1336
+ if (projectSlug && (normalizedEquals(repoName, projectSlug) || normalizedEquals(baseName, projectSlug)))
1337
+ score += 60;
1338
+ if (projectName && (normalizedEquals(repoName, projectName) || normalizedEquals(baseName, projectName)))
1339
+ score += 40;
1340
+ for (const project of config.projects) {
1341
+ let projectScore = 0;
1342
+ if (projectSlug && normalizedEquals(project.slug, projectSlug))
1343
+ projectScore += 50;
1344
+ if (projectName && normalizedEquals(project.name, projectName))
1345
+ projectScore += 50;
1346
+ if (projectId && (project.projectId === projectId || project.scopeId === projectId))
1347
+ projectScore += 50;
1348
+ if (scopeId && (project.scopeId === scopeId || project.projectId === scopeId))
1349
+ projectScore += 50;
1350
+ if (!hasExplicitSelector && normalizedEquals(project.name, config.activeProjectName))
1351
+ projectScore += 12;
1352
+ if (normalizeNonEmpty(project.localRoot))
1353
+ projectScore += 4;
1354
+ score = Math.max(score, projectScore);
1355
+ }
1356
+ if (!hasExplicitSelector && config.projects.length === 1)
1357
+ score += 10;
1358
+ return score;
1359
+ }
1360
+ function sidecarSelectorLabel(options) {
1361
+ return normalizeNonEmpty(options.projectSlug)
1362
+ ? `project slug "${options.projectSlug}"`
1363
+ : normalizeNonEmpty(options.projectName)
1364
+ ? `project name "${options.projectName}"`
1365
+ : normalizeNonEmpty(options.projectId)
1366
+ ? `project id "${options.projectId}"`
1367
+ : normalizeNonEmpty(options.scopeId)
1368
+ ? `scope id "${options.scopeId}"`
1369
+ : 'the active project selector';
1370
+ }
1371
+ async function findNearbyAopsRepoRoots(startDir, maxDepth = 2) {
1372
+ const roots = [];
1373
+ async function visit(dir, depth) {
1374
+ if (existsSync(path.join(dir, '.aops', 'aops.config.json'))) {
1375
+ roots.push(dir);
1376
+ return;
1377
+ }
1378
+ if (depth <= 0)
1379
+ return;
1380
+ let entries;
1381
+ try {
1382
+ entries = await fs.readdir(dir, { withFileTypes: true });
1383
+ }
1384
+ catch {
1385
+ return;
1386
+ }
1387
+ for (const entry of entries) {
1388
+ if (!entry.isDirectory() || SIDECAR_REPO_DISCOVERY_SKIP_DIRS.has(entry.name))
1389
+ continue;
1390
+ await visit(path.join(dir, entry.name), depth - 1);
1391
+ }
1392
+ }
1393
+ await visit(path.resolve(startDir), maxDepth);
1394
+ return roots;
1395
+ }
1396
+ async function findAopsRepoRootNear(startDir, options) {
1397
+ const upward = await findAopsRepoRootUpward(startDir);
1398
+ if (upward)
1399
+ return upward;
1400
+ const candidates = await findNearbyAopsRepoRoots(startDir);
1401
+ if (candidates.length === 0)
1402
+ return null;
1403
+ const scored = [];
1404
+ for (const candidate of candidates) {
1405
+ const config = await readAopsRepoConfigReadOnly(candidate);
1406
+ if (!config)
1407
+ continue;
1408
+ const score = scoreSidecarRepoCandidate(candidate, config, options);
1409
+ if (score > 0)
1410
+ scored.push({ root: candidate, score });
1411
+ }
1412
+ scored.sort((left, right) => right.score - left.score || left.root.length - right.root.length);
1413
+ const best = scored[0];
1414
+ if (best) {
1415
+ const ties = scored.filter((candidate) => candidate.score === best.score);
1416
+ if (ties.length > 1) {
1417
+ throw new Error(`Sidecar ${sidecarSelectorLabel(options)} matched multiple nearby AOPS repos: ${ties.map((entry) => entry.root).join(', ')}. Start the sidecar from the intended repo root or pass --project-id/--scope-id explicitly.`);
1418
+ }
1419
+ return best.root;
1420
+ }
1421
+ return candidates.length === 1 ? candidates[0] : null;
1422
+ }
1423
+ export async function resolveSidecarRuntimeContext(options) {
1424
+ const discoveredRoot = await findAopsRepoRootNear(process.cwd(), options);
1425
+ if (discoveredRoot && path.resolve(discoveredRoot) !== path.resolve(process.cwd())) {
1426
+ process.chdir(discoveredRoot);
1427
+ }
1428
+ const repo = await loadAopsRepoConfigReadOnly(discoveredRoot ?? process.cwd()).catch(() => null);
1429
+ const projects = repo?.config?.projects ?? [];
1430
+ const selectedProject = normalizeNonEmpty(options.projectSlug)
1431
+ ? projects.find((project) => normalizeNonEmpty(project.slug) === normalizeNonEmpty(options.projectSlug))
1432
+ : projects.length === 1
1433
+ ? projects[0]
1434
+ : undefined;
1435
+ return compactPayload({
1436
+ repoRoot: repo?.rootDir ?? discoveredRoot ?? process.cwd(),
1437
+ projectSlug: normalizeNonEmpty(options.projectSlug) ?? normalizeNonEmpty(selectedProject?.slug),
1438
+ projectId: normalizeNonEmpty(options.projectId) ?? normalizeNonEmpty(selectedProject?.projectId) ?? normalizeNonEmpty(selectedProject?.scopeId),
1439
+ scopeId: normalizeNonEmpty(options.scopeId) ?? normalizeNonEmpty(selectedProject?.scopeId) ?? normalizeNonEmpty(selectedProject?.projectId),
1440
+ });
1441
+ }
1442
+ function parsePort(value) {
1443
+ const port = typeof value === 'number' ? value : Number.parseInt(String(value ?? DEFAULT_SIDECAR_PORT), 10);
1444
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
1445
+ throw new Error(`Invalid sidecar port "${String(value)}".`);
1446
+ }
1447
+ return port;
1448
+ }
1449
+ function parseSidecarOrigins(options) {
1450
+ const origins = new Set(DEFAULT_SIDECAR_ORIGINS);
1451
+ for (const origin of toStringArray(options.allowOrigin)) {
1452
+ if (origin === '*') {
1453
+ origins.add(origin);
1454
+ continue;
1455
+ }
1456
+ try {
1457
+ origins.add(new URL(origin).origin);
1458
+ }
1459
+ catch {
1460
+ throw new Error(`Invalid --allow-origin value "${origin}".`);
1461
+ }
1462
+ }
1463
+ return origins;
1464
+ }
1465
+ function resolveCorsOrigin(origin, allowed) {
1466
+ if (!origin)
1467
+ return null;
1468
+ if (allowed.has('*'))
1469
+ return origin;
1470
+ try {
1471
+ const normalized = new URL(origin).origin;
1472
+ return allowed.has(normalized) ? normalized : null;
1473
+ }
1474
+ catch {
1475
+ return null;
1476
+ }
1477
+ }
1478
+ function writeSidecarJson(res, status, payload, corsOrigin) {
1479
+ const headers = {
1480
+ 'content-type': 'application/json; charset=utf-8',
1481
+ 'cache-control': 'no-store',
1482
+ };
1483
+ if (corsOrigin) {
1484
+ headers['access-control-allow-origin'] = corsOrigin;
1485
+ headers.vary = 'Origin';
1486
+ }
1487
+ res.writeHead(status, headers);
1488
+ res.end(`${JSON.stringify(payload, null, 2)}\n`);
1489
+ }
1490
+ function writeSidecarOptions(res, corsOrigin) {
1491
+ const headers = {
1492
+ 'access-control-allow-methods': 'GET,POST,OPTIONS',
1493
+ 'access-control-allow-headers': 'authorization,content-type,x-aops-sidecar-token',
1494
+ 'access-control-max-age': '600',
1495
+ };
1496
+ if (corsOrigin) {
1497
+ headers['access-control-allow-origin'] = corsOrigin;
1498
+ headers.vary = 'Origin';
1499
+ }
1500
+ res.writeHead(corsOrigin ? 204 : 403, headers);
1501
+ res.end();
1502
+ }
1503
+ function sidecarAuthOk(req, token) {
1504
+ const expected = normalizeNonEmpty(token) ?? normalizeNonEmpty(process.env.AOPS_COCKPIT_SIDECAR_TOKEN);
1505
+ if (!expected)
1506
+ return true;
1507
+ const authorization = normalizeNonEmpty(req.headers.authorization);
1508
+ if (authorization === `Bearer ${expected}`)
1509
+ return true;
1510
+ return normalizeNonEmpty(req.headers['x-aops-sidecar-token']) === expected;
1511
+ }
1512
+ function sidecarPath(url) {
1513
+ return url.pathname.replace(/^\/api\/aops-cockpit-sidecar/, '') || '/';
1514
+ }
1515
+ function boolFromBody(value, fallback = false) {
1516
+ if (typeof value === 'boolean')
1517
+ return value;
1518
+ if (typeof value === 'string')
1519
+ return ['1', 'true', 'yes', 'on'].includes(value.toLowerCase());
1520
+ return fallback;
1521
+ }
1522
+ function queryList(url, key) {
1523
+ return url.searchParams.getAll(key).map((value) => value.trim()).filter(Boolean);
1524
+ }
1525
+ function buildSidecarSyncOptions(base, url, body = {}) {
1526
+ const projectSlug = normalizeNonEmpty(body.projectSlug)
1527
+ ?? normalizeNonEmpty(body.project)
1528
+ ?? normalizeNonEmpty(url.searchParams.get('projectSlug'))
1529
+ ?? normalizeNonEmpty(url.searchParams.get('project'))
1530
+ ?? normalizeNonEmpty(base.projectSlug);
1531
+ const projectId = normalizeNonEmpty(body.projectId)
1532
+ ?? normalizeNonEmpty(url.searchParams.get('projectId'))
1533
+ ?? normalizeNonEmpty(base.projectId);
1534
+ const scopeId = normalizeNonEmpty(body.scopeId)
1535
+ ?? normalizeNonEmpty(url.searchParams.get('scopeId'))
1536
+ ?? normalizeNonEmpty(base.scopeId);
1537
+ return {
1538
+ ...base,
1539
+ json: true,
1540
+ allProjects: boolFromBody(body.allProjects, boolFromBody(url.searchParams.get('allProjects'))),
1541
+ projectSlug,
1542
+ projectId: projectSlug ? undefined : projectId,
1543
+ scopeId: projectSlug ? undefined : scopeId,
1544
+ board: [...queryList(url, 'board'), ...toStringArray(body.board)],
1545
+ task: [...queryList(url, 'task'), ...toStringArray(body.task)],
1546
+ sprint: [...queryList(url, 'sprint'), ...toStringArray(body.sprint)],
1547
+ issue: [...queryList(url, 'issue'), ...toStringArray(body.issue)],
1548
+ feedback: [...queryList(url, 'feedback'), ...toStringArray(body.feedback)],
1549
+ reviewRequest: [...queryList(url, 'reviewRequest'), ...queryList(url, 'review-request'), ...toStringArray(body.reviewRequest)],
1550
+ record: [...queryList(url, 'record'), ...toStringArray(body.record)],
1551
+ };
1552
+ }
1553
+ function sidecarHealthPayload(options, port, host, runtime) {
1554
+ return {
1555
+ contractVersion: SIDECAR_CONTRACT_VERSION,
1556
+ surface: 'aops-cockpit-local-sidecar',
1557
+ status: 'ready',
1558
+ host,
1559
+ port,
1560
+ repoRoot: runtime.repoRoot,
1561
+ projectSlug: runtime.projectSlug ?? normalizeNonEmpty(options.projectSlug),
1562
+ projectId: runtime.projectId ?? normalizeNonEmpty(options.projectId),
1563
+ scopeId: runtime.scopeId ?? normalizeNonEmpty(options.scopeId),
1564
+ capabilities: {
1565
+ syncStatus: true,
1566
+ syncDiff: true,
1567
+ readOnly: true,
1568
+ localRepoRequired: true,
1569
+ runsOnClientMachine: true,
1570
+ },
1571
+ endpoints: {
1572
+ health: '/api/aops-cockpit-sidecar/health',
1573
+ syncStatus: '/api/aops-cockpit-sidecar/sync/status',
1574
+ syncDiff: '/api/aops-cockpit-sidecar/sync/diff',
1575
+ },
1576
+ };
1577
+ }
1578
+ async function handleSidecarRequest(req, res, options, params) {
1579
+ const corsOrigin = resolveCorsOrigin(req.headers.origin, params.origins);
1580
+ if (req.method === 'OPTIONS') {
1581
+ writeSidecarOptions(res, corsOrigin);
1582
+ return;
1583
+ }
1584
+ if (req.headers.origin && !corsOrigin) {
1585
+ writeSidecarJson(res, 403, { ok: false, error: { code: 'origin_not_allowed' } }, null);
1586
+ return;
1587
+ }
1588
+ if (!sidecarAuthOk(req, options.token)) {
1589
+ writeSidecarJson(res, 401, { ok: false, error: { code: 'sidecar_auth_required' } }, corsOrigin);
1590
+ return;
1591
+ }
1592
+ const url = new URL(req.url ?? '/', `http://${params.host}:${params.port}`);
1593
+ const route = sidecarPath(url);
1594
+ try {
1595
+ if (req.method === 'GET' && (route === '/' || route === '/health')) {
1596
+ writeSidecarJson(res, 200, { ok: true, data: sidecarHealthPayload(options, params.port, params.host, params.runtime) }, corsOrigin);
1597
+ return;
1598
+ }
1599
+ if (req.method === 'GET' && route === '/sync/status') {
1600
+ const syncOptions = buildSidecarSyncOptions(options, url);
1601
+ const context = await resolveProjectBindingContext(syncOptions, { requireProject: true });
1602
+ const data = await collectSyncStatusData(context, syncOptions);
1603
+ writeSidecarJson(res, 200, {
1604
+ ok: true,
1605
+ command: 'sync.sidecar.status',
1606
+ surface: 'aops-cockpit-local-sidecar',
1607
+ resolvedContext: context,
1608
+ data,
1609
+ }, corsOrigin);
1610
+ return;
1611
+ }
1612
+ if (req.method === 'GET' && route === '/sync/diff') {
1613
+ const syncOptions = buildSidecarSyncOptions(options, url);
1614
+ const context = await resolveProjectBindingContext(syncOptions, { requireProject: true });
1615
+ const data = await collectSyncDiffData(context, syncOptions);
1616
+ writeSidecarJson(res, 200, {
1617
+ ok: true,
1618
+ command: 'sync.sidecar.diff',
1619
+ surface: 'aops-cockpit-local-sidecar',
1620
+ resolvedContext: context,
1621
+ data,
1622
+ }, corsOrigin);
1623
+ return;
1624
+ }
1625
+ if (req.method === 'POST' && route === '/sync/push') {
1626
+ writeSidecarJson(res, 410, {
1627
+ ok: false,
1628
+ error: {
1629
+ code: 'sync_push_removed',
1630
+ message: 'sync push removed: the server is the source of truth. The sidecar exposes read-only status/diff only.',
1631
+ },
1632
+ }, corsOrigin);
1633
+ return;
1634
+ }
1635
+ writeSidecarJson(res, 404, { ok: false, error: { code: 'not_found' } }, corsOrigin);
1636
+ }
1637
+ catch (error) {
1638
+ writeSidecarJson(res, 500, {
1639
+ ok: false,
1640
+ error: {
1641
+ code: 'sidecar_request_failed',
1642
+ message: error instanceof Error ? error.message : String(error),
1643
+ },
1644
+ }, corsOrigin);
1645
+ }
1646
+ }
1647
+ async function runSyncSidecar(options) {
1648
+ try {
1649
+ const runtime = await resolveSidecarRuntimeContext(options);
1650
+ const host = normalizeNonEmpty(options.host) ?? DEFAULT_SIDECAR_HOST;
1651
+ const port = parsePort(options.port);
1652
+ const origins = parseSidecarOrigins(options);
1653
+ if (!normalizeNonEmpty(options.token) && !normalizeNonEmpty(process.env.AOPS_COCKPIT_SIDECAR_TOKEN)) {
1654
+ const hasRemoteOrigin = [...origins].some((origin) => {
1655
+ if (origin === '*')
1656
+ return true;
1657
+ try {
1658
+ const hostname = new URL(origin).hostname;
1659
+ return hostname !== 'localhost' && hostname !== '127.0.0.1' && hostname !== '[::1]' && hostname !== '::1';
1660
+ }
1661
+ catch {
1662
+ return true;
1663
+ }
1664
+ });
1665
+ if (hasRemoteOrigin) {
1666
+ logWarn('Remote --allow-origin is configured without --token / AOPS_COCKPIT_SIDECAR_TOKEN. Prefer a bearer token for remote cockpit origins.');
1667
+ }
1668
+ }
1669
+ const server = http.createServer((req, res) => {
1670
+ void handleSidecarRequest(req, res, options, { host, port, origins, runtime });
1671
+ });
1672
+ await new Promise((resolve, reject) => {
1673
+ server.once('error', reject);
1674
+ server.listen(port, host, () => resolve());
1675
+ });
1676
+ const ready = {
1677
+ command: 'sync.sidecar',
1678
+ surface: 'aops-cockpit-local-sidecar',
1679
+ result: {
1680
+ ok: true,
1681
+ data: sidecarHealthPayload(options, port, host, runtime),
1682
+ },
1683
+ };
1684
+ if (options.json)
1685
+ console.log(JSON.stringify(ready, null, 2));
1686
+ else
1687
+ logSuccess(`AOPS cockpit local sidecar listening at http://${host}:${port}`);
1688
+ }
1689
+ catch (error) {
1690
+ logError(error instanceof Error ? error.message : String(error));
1691
+ process.exitCode = 1;
1692
+ }
1693
+ }
1694
+ async function runSyncResolve(options) {
1695
+ try {
1696
+ const filePathInput = normalizeNonEmpty(options.path);
1697
+ const prefer = normalizeNonEmpty(options.prefer);
1698
+ if (!filePathInput)
1699
+ throw new Error('sync resolve requires --path.');
1700
+ if (prefer === 'local')
1701
+ throw new Error('sync resolve --prefer local was removed with sync push: the server is the source of truth. Use --prefer remote to adopt the hosted/cache version (the read-only local cache is never pushed back).');
1702
+ if (prefer !== 'remote')
1703
+ throw new Error('sync resolve requires --prefer remote (the only supported resolution is adopting the hosted/cache version).');
1704
+ const context = await resolveProjectBindingContext(options, { requireProject: true });
1705
+ const filePath = path.isAbsolute(filePathInput) ? filePathInput : path.join(context.repoRoot, filePathInput);
1706
+ const raw = await fs.readFile(filePath, 'utf8');
1707
+ const parsed = parseFrontmatterDocument(raw);
1708
+ if (prefer === 'remote') {
1709
+ const remoteId = normalizeNonEmpty(parsed.frontmatter.remoteId);
1710
+ const entityType = normalizeNonEmpty(parsed.frontmatter.entityType);
1711
+ if (!remoteId)
1712
+ throw new Error('Remote conflict adoption requires remoteId in the source file.');
1713
+ const readToolId = getReadToolForEntity(entityType);
1714
+ if (!readToolId)
1715
+ throw new Error(`Remote conflict adoption is not supported for entity type "${entityType ?? 'unknown'}".`);
1716
+ const apiState = await requireApiState(options);
1717
+ if (!apiState)
1718
+ return;
1719
+ const projectId = normalizeNonEmpty(context.projectId);
1720
+ const scopeId = normalizeNonEmpty(context.scopeId) ?? projectId;
1721
+ const remotePayload = await invokeRead(apiState, options, readToolId, { id: remoteId });
1722
+ const remoteData = extractResultData(remotePayload) ?? remotePayload;
1723
+ if (!isRecord(remoteData))
1724
+ throw new Error('Remote read did not return a record payload.');
1725
+ const remote = { ...remoteData, id: normalizeNonEmpty(remoteData.id) ?? remoteId };
1726
+ let resolvedPath;
1727
+ const pmConfig = projectmanSeedConfig(context, entityType);
1728
+ if (pmConfig) {
1729
+ resolvedPath = await writeProjectmanSeedFile({
1730
+ repoRoot: context.repoRoot,
1731
+ dir: pmConfig.dir,
1732
+ entityType: entityType ?? 'projectman.entity',
1733
+ remote,
1734
+ projectId,
1735
+ scopeId,
1736
+ titleKey: pmConfig.titleKey,
1737
+ force: true,
1738
+ });
1739
+ }
1740
+ else if (entityType === 'agentspace.experience-item') {
1741
+ const agPaths = resolveRepoFirstAgentspacePaths(context);
1742
+ resolvedPath = await writeExperienceSeedFile({
1743
+ repoRoot: context.repoRoot,
1744
+ dir: agPaths.experienceItems,
1745
+ remote,
1746
+ projectId,
1747
+ scopeId,
1748
+ force: true,
1749
+ });
1750
+ }
1751
+ else if (entityType === 'agentspace.memory-item') {
1752
+ const agPaths = resolveRepoFirstAgentspacePaths(context);
1753
+ resolvedPath = await writeMemorySeedFile({
1754
+ repoRoot: context.repoRoot,
1755
+ dir: agPaths.memoryItems,
1756
+ remote,
1757
+ projectId,
1758
+ scopeId,
1759
+ force: true,
1760
+ });
1761
+ }
1762
+ else {
1763
+ throw new Error(`Remote conflict adoption is not supported for entity type "${entityType ?? 'unknown'}".`);
1764
+ }
1765
+ emit(options, {
1766
+ command: 'sync.resolve',
1767
+ surface: 'hosted-cache-sync',
1768
+ resolvedContext: context,
1769
+ result: { ok: true, data: { path: resolvedPath, prefer, syncState: 'synced', remoteId } },
1770
+ }, 'Local cache conflict resolved from remote.');
1771
+ return;
1772
+ }
1773
+ }
1774
+ catch (error) {
1775
+ logError(error instanceof Error ? error.message : String(error));
1776
+ process.exitCode = 1;
1777
+ }
1778
+ }
1779
+ async function runSyncRebuildViews(options) {
1780
+ try {
1781
+ const context = await resolveProjectBindingContext(options, { requireProject: true });
1782
+ await rebuildProjectmanViews(context);
1783
+ const experiencePaths = resolveRepoFirstAgentspacePaths(context);
1784
+ const experienceItems = await readExperienceItems(experiencePaths.experienceItems);
1785
+ await rebuildExperienceWorkspace(context, experienceItems);
1786
+ const memoryPaths = resolveMemoryWorkspacePaths(context);
1787
+ const memoryItems = await readLocalMemoryEntries(memoryPaths.localItemsDir);
1788
+ await rebuildLocalMemoryWorkspace({
1789
+ ...context,
1790
+ items: memoryItems,
1791
+ });
1792
+ await rebuildHostedWorkspace(context.repoRoot);
1793
+ emit(options, {
1794
+ command: 'sync.rebuild-views',
1795
+ surface: 'hosted-cache-sync',
1796
+ resolvedContext: context,
1797
+ result: {
1798
+ ok: true,
1799
+ data: {
1800
+ views: [
1801
+ '.aops/projectman/views/index.md',
1802
+ '.aops/agentspace/experience/index.md',
1803
+ '.aops/agentspace/memory/index.md',
1804
+ '.aops/hosted/index.md',
1805
+ ],
1806
+ },
1807
+ },
1808
+ }, 'Local cache views rebuilt.');
1809
+ }
1810
+ catch (error) {
1811
+ logError(error instanceof Error ? error.message : String(error));
1812
+ process.exitCode = 1;
1813
+ }
1814
+ }
1815
+ function applySyncOptions(cmd) {
1816
+ applyCommonOptions(cmd, { withProject: false });
1817
+ cmd.option('--project-id <id>', 'Project id used to resolve owner context');
1818
+ cmd.option('--project-name <name>', 'Project name override for repo-aware context resolution');
1819
+ cmd.option('--project-slug <slug>', 'Project slug override for repo-aware context resolution');
1820
+ cmd.option('--scope-id <id>', 'Canonical owner scope override');
1821
+ cmd.option('--tenant-id <id>', 'Tenant id header for hosted sync calls');
1822
+ cmd.option('--locale <locale>', 'Locale header for hosted sync calls');
1823
+ cmd.option('--fallback-locale <locale>', 'Fallback locale header for hosted sync calls');
1824
+ return cmd;
1825
+ }
1826
+ function applySyncSelectionOptions(cmd) {
1827
+ cmd.option('--board <value>', 'Limit sync scope to a board and its related task/sprint/issue/feedback/review-request records', collectRepeatedOption, []);
1828
+ cmd.option('--task <value>', 'Limit sync scope to a kanban task and its related sprint/issue/feedback/review-request records', collectRepeatedOption, []);
1829
+ cmd.option('--sprint <value>', 'Limit sync scope to a sprint and its related microtasks/signals', collectRepeatedOption, []);
1830
+ cmd.option('--issue <value>', 'Limit sync scope to a Projectman issue', collectRepeatedOption, []);
1831
+ cmd.option('--feedback <value>', 'Limit sync scope to a Projectman feedback item', collectRepeatedOption, []);
1832
+ cmd.option('--review-request <value>', 'Limit sync scope to a Projectman review request', collectRepeatedOption, []);
1833
+ cmd.option('--record <value>', 'Limit sync scope to an exact local path, local id, remote id, slug, name, or title', collectRepeatedOption, []);
1834
+ return cmd;
1835
+ }
1836
+ function applyAllProjectsOption(cmd) {
1837
+ cmd.option('--all-projects', 'Run the sync command once per repo-config project and report project-level results without fail-fast');
1838
+ return cmd;
1839
+ }
1840
+ export function makeSyncCommand() {
1841
+ const cmd = new Command('sync').description('Server-first sync commands: refresh the read-only local cache of Projectman/Agentspace state plus hosted prompt/skill mirrors');
1842
+ applySyncSelectionOptions(applyAllProjectsOption(applySyncOptions(cmd.command('status')
1843
+ .description('Show local cache sync state')
1844
+ .action(async (options) => runSyncStatus(options)))));
1845
+ applySyncSelectionOptions(applyAllProjectsOption(applySyncOptions(cmd.command('diff')
1846
+ .description('Show local records that are not synced')
1847
+ .action(async (options) => runSyncDiff(options)))));
1848
+ applyAllProjectsOption(applySyncOptions(cmd.command('pull')
1849
+ .description('Pull hosted Projectman/Agentspace state into the read-only local cache and refresh read-only prompt/skill mirrors')
1850
+ .option('--apply', 'Write pulled records into the local cache workspace')
1851
+ .option('--hosted-project-id <id>', 'Also mirror hosted read-only prompts/skills for another project id', collectRepeatedOption, [])
1852
+ .option('--hosted-project-name <name>', 'Also mirror hosted read-only prompts/skills for another project name', collectRepeatedOption, [])
1853
+ .option('--hosted-project-slug <slug>', 'Also mirror hosted read-only prompts/skills for another project slug', collectRepeatedOption, [])
1854
+ .action(async (options) => runSyncPullOrBootstrap(options, 'sync.pull'))));
1855
+ applyAllProjectsOption(applySyncOptions(cmd.command('bootstrap')
1856
+ .description('Seed the read-only local cache from hosted server state and refresh read-only prompt/skill mirrors')
1857
+ .option('--from-server', 'Bootstrap from hosted AOPS server state')
1858
+ .option('--apply', 'Write bootstrapped records into the local cache workspace')
1859
+ .option('--hosted-project-id <id>', 'Also mirror hosted read-only prompts/skills for another project id', collectRepeatedOption, [])
1860
+ .option('--hosted-project-name <name>', 'Also mirror hosted read-only prompts/skills for another project name', collectRepeatedOption, [])
1861
+ .option('--hosted-project-slug <slug>', 'Also mirror hosted read-only prompts/skills for another project slug', collectRepeatedOption, [])
1862
+ .action(async (options) => runSyncPullOrBootstrap(options, 'sync.bootstrap'))));
1863
+ applySyncSelectionOptions(applySyncOptions(cmd.command('sidecar')
1864
+ .description('Run a localhost-bound cockpit sidecar that exposes read-only local cache status/diff on the client machine')
1865
+ .option('--host <host>', 'Bind host for the sidecar HTTP server', DEFAULT_SIDECAR_HOST)
1866
+ .option('--port <port>', 'Bind port for the sidecar HTTP server', String(DEFAULT_SIDECAR_PORT))
1867
+ .option('--allow-origin <origin>', 'Allowed cockpit browser origin; repeat for remote cockpit origins', collectRepeatedOption, [])
1868
+ .option('--token <token>', 'Optional bearer token required by cockpit sidecar requests')
1869
+ .action(async (options) => runSyncSidecar(options))));
1870
+ applySyncOptions(cmd.command('resolve')
1871
+ .description('Resolve a local cache conflict by adopting the hosted/cache version (--prefer remote)')
1872
+ .requiredOption('--path <path>', 'Repo-relative or absolute source markdown path')
1873
+ .requiredOption('--prefer <mode>', 'Conflict resolution: remote (adopt the hosted/cache version; only supported mode since sync push was removed)')
1874
+ .action(async (options) => runSyncResolve(options)));
1875
+ applySyncOptions(cmd.command('rebuild-views')
1876
+ .description('Rebuild derived local cache markdown views and hosted read-only indexes')
1877
+ .action(async (options) => runSyncRebuildViews(options)));
1878
+ cmd.addHelpText('after', buildOperatorCookbook({
1879
+ examples: [
1880
+ 'aops-cli sync status --json',
1881
+ 'aops-cli sync diff --json',
1882
+ 'aops-cli sync status --all-projects --json',
1883
+ 'aops-cli sync status --board engineering --json',
1884
+ 'aops-cli sync diff --board engineering --json',
1885
+ 'aops-cli sync bootstrap --from-server --apply --json',
1886
+ 'aops-cli sync pull --apply --json',
1887
+ 'aops-cli sync pull --all-projects --apply --json',
1888
+ 'aops-cli sync pull --apply --hosted-project-slug aops --json',
1889
+ 'aops-cli sync resolve --path .aops/projectman/sprints/demo.md --prefer remote --json',
1890
+ ],
1891
+ guide: GUIDE_PATHS.operator,
1892
+ notes: [
1893
+ 'Server-first: hosted server state is the single source of truth. `.aops/**` source files are a read-only local cache refreshed by `sync pull`/`sync bootstrap`; there is no `sync push` (write to hosted state with the hosted CLI/domain ops).',
1894
+ 'Hosted reusable prompts/skills stay single-source on the server; sync pull/bootstrap only mirror them into `.aops/hosted/**` as read-only local context.',
1895
+ 'Hosted Docman documents and guide mirrors use `aops-cli doc mirror pull --out-dir ./.aops/docman`; `sync pull` does not refresh `.aops/docman/**`.',
1896
+ '`sync status` and `sync diff` accept repeatable selective scope flags: --board, --task, --sprint, --issue, --feedback, --review-request, and --record.',
1897
+ 'Board/task/sprint selection expands to related Projectman records and linked Agentspace memory/experience context. `--record` matches local path, localId, remoteId, slug, name, or title.',
1898
+ '`--all-projects` runs status/diff/pull/bootstrap once per repo-config project (server-first; no authoring-mode split), skips only projects without a localRoot in a multi-project repo, and reports project-level errors without fail-fast.',
1899
+ '`sync pull` and `sync bootstrap` are project-level merge/mirror commands; use --hosted-project-* there only to refresh hosted prompt/skill mirrors.',
1900
+ 'Hosted writes are the source of truth, so the local cache never replays back. If a cache file drifts, re-run `sync pull` to refresh it from the server; `sync resolve --prefer remote` re-adopts a single record from hosted state.',
1901
+ 'Browser/cockpit surfaces must never assume the remote aops-server machine has the repo. Use `aops-cli sync sidecar --allow-origin <cockpit-origin>` on the client machine to expose a localhost-only read-only status/diff bridge.',
1902
+ '`localId` is UUIDv4. `remoteId` is filled after hosted sync/bootstrap.',
1903
+ '.aops/sync/state.json is local machine sync state and is ignored by git.',
1904
+ ],
1905
+ }));
1906
+ return cmd;
1907
+ }