@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,1448 @@
1
+ import fs from 'node:fs/promises';
2
+ import { Command } from 'commander';
3
+ import { logError, logSuccess } from '@aopslabs/xf-cli-ui';
4
+ import { applyCommonOptions, compactPayload, normalizeNonEmpty } from '../utils/command.js';
5
+ import { invokeHostedToolWithApiState, requireApiState, resolveAgentGatewayContext, unwrapHostedToolResult, } from '../utils/agent-gateway.js';
6
+ import { buildHostedSugarEnvelope, ensureGuardedWrite, missingScopeIdMessage, } from '../utils/hosted-sugar.js';
7
+ const DISCUSSION_OPERATOR_TARGET = 'operator';
8
+ const DISCUSSION_WAIT_OPERATOR_BLOCK_EXIT_CODE = 20;
9
+ const DISCUSSION_WAIT_DONE_EXIT_CODE = 21;
10
+ const DISCUSSION_WAIT_TIMEOUT_EXIT_CODE = 22;
11
+ const TURN_KINDS = [
12
+ 'statement',
13
+ 'question',
14
+ 'answer',
15
+ 'objection',
16
+ 'concession',
17
+ 'proposal',
18
+ 'final-stance',
19
+ ];
20
+ function collectRepeatedOption(value, previous = []) {
21
+ const normalized = normalizeNonEmpty(value);
22
+ return normalized ? [...previous, normalized] : previous;
23
+ }
24
+ function isRecord(value) {
25
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
26
+ }
27
+ function toStringArray(values) {
28
+ return Array.isArray(values)
29
+ ? values.map((entry) => normalizeNonEmpty(entry)).filter((entry) => Boolean(entry))
30
+ : [];
31
+ }
32
+ function uniqueStrings(values) {
33
+ const seen = new Set();
34
+ const result = [];
35
+ for (const value of values) {
36
+ const normalized = normalizeNonEmpty(value);
37
+ if (!normalized)
38
+ continue;
39
+ const key = normalized.toLowerCase();
40
+ if (seen.has(key))
41
+ continue;
42
+ seen.add(key);
43
+ result.push(normalized);
44
+ }
45
+ return result;
46
+ }
47
+ function parseInteger(value) {
48
+ return Number.parseInt(value, 10);
49
+ }
50
+ function parseNonNegativeNumber(value) {
51
+ const parsed = Number.parseFloat(value);
52
+ if (!Number.isFinite(parsed) || parsed < 0) {
53
+ throw new Error('Expected a non-negative number.');
54
+ }
55
+ return parsed;
56
+ }
57
+ function parsePositiveNumber(value) {
58
+ const parsed = Number.parseFloat(value);
59
+ if (!Number.isFinite(parsed) || parsed <= 0) {
60
+ throw new Error('Expected a positive number.');
61
+ }
62
+ return parsed;
63
+ }
64
+ function normalizeTurnKind(value) {
65
+ const normalized = normalizeNonEmpty(value)?.toLowerCase();
66
+ if (TURN_KINDS.includes(normalized))
67
+ return normalized;
68
+ throw new Error(`Unsupported turn kind. Use one of: ${TURN_KINDS.join(', ')}.`);
69
+ }
70
+ function normalizeTurnOrderMode(value) {
71
+ const normalized = normalizeNonEmpty(value)?.toLowerCase() ?? 'alternating';
72
+ if (normalized === 'alternating' || normalized === 'free')
73
+ return normalized;
74
+ throw new Error('Unsupported turn order. Use alternating|free.');
75
+ }
76
+ function normalizeListStatus(value) {
77
+ const normalized = normalizeNonEmpty(value)?.toLowerCase();
78
+ if (!normalized)
79
+ return undefined;
80
+ if (normalized === 'active' || normalized === 'concluding' || normalized === 'concluded' || normalized === 'abandoned')
81
+ return normalized;
82
+ throw new Error('Unsupported status filter. Use active|concluding|concluded|abandoned.');
83
+ }
84
+ function normalizeDiscussionScopeFilter(value) {
85
+ const normalized = normalizeNonEmpty(value)?.toLowerCase();
86
+ if (!normalized)
87
+ return undefined;
88
+ if (normalized === 'standalone')
89
+ return normalized;
90
+ throw new Error('Unsupported scope filter. Use standalone.');
91
+ }
92
+ function normalizeSlug(value) {
93
+ return value
94
+ .toLowerCase()
95
+ .replace(/[^a-z0-9]+/g, '-')
96
+ .replace(/^-+|-+$/g, '')
97
+ .slice(0, 80) || 'item';
98
+ }
99
+ function normalizeParticipants(values) {
100
+ const participants = uniqueStrings(toStringArray(values));
101
+ if (participants.length < 2) {
102
+ throw new Error('Provide at least two unique --agent values.');
103
+ }
104
+ return participants;
105
+ }
106
+ function assertMaxTurnsUnsupported(maxTurns) {
107
+ if (maxTurns === undefined || maxTurns === null)
108
+ return;
109
+ if (typeof maxTurns === 'number' && !Number.isFinite(maxTurns))
110
+ return;
111
+ throw new Error('server-first-unsupported: --max-turns has no hosted-enforced equivalent. The hosted discussion-topic rules schema only supports turnOrder/minTurnsBeforeConclude/requireQuestionAnswer; there is no server-enforced max-turn cap. Remove --max-turns. (PM-issue candidate: add a server-enforced maxTurns guard before re-exposing this flag.)');
112
+ }
113
+ function foldObjectiveIntoQuestion(objective, question) {
114
+ const obj = normalizeNonEmpty(objective);
115
+ const q = normalizeNonEmpty(question);
116
+ if (obj && q)
117
+ return `Objective: ${obj}\n\nQuestion: ${q}`;
118
+ if (q)
119
+ return q;
120
+ if (obj)
121
+ return obj;
122
+ throw new Error('Provide --question (or --objective). The hosted discussion topic requires a question.');
123
+ }
124
+ function buildRules(options) {
125
+ const mode = normalizeTurnOrderMode(options.turnOrder);
126
+ const rules = compactPayload({
127
+ turnOrder: mode === 'alternating' ? options.participants : undefined,
128
+ minTurnsBeforeConclude: Number.isInteger(options.minTurnsBeforeConclude) ? options.minTurnsBeforeConclude : undefined,
129
+ requireQuestionAnswer: options.requireQuestionAnswer === true ? true : undefined,
130
+ });
131
+ return Object.keys(rules).length > 0 ? rules : undefined;
132
+ }
133
+ function requireScopeId(context) {
134
+ const scopeId = normalizeNonEmpty(context.scopeId);
135
+ if (!scopeId)
136
+ throw new Error(missingScopeIdMessage('Discussion'));
137
+ return scopeId;
138
+ }
139
+ function buildResolvedContextRecord(context) {
140
+ return compactPayload({
141
+ tenantId: context.tenantId,
142
+ scopeId: context.scopeId,
143
+ projectId: context.projectId,
144
+ projectName: context.projectName,
145
+ scopeResolution: context.scopeResolution,
146
+ });
147
+ }
148
+ function unwrapDiscussionData(payload) {
149
+ let current = unwrapHostedToolResult(payload);
150
+ let guard = 0;
151
+ while (isRecord(current) &&
152
+ Object.prototype.hasOwnProperty.call(current, 'data') &&
153
+ Object.prototype.hasOwnProperty.call(current, 'ok') &&
154
+ guard < 6) {
155
+ current = current.data;
156
+ guard += 1;
157
+ }
158
+ if (isRecord(current) && Object.prototype.hasOwnProperty.call(current, 'data') && Object.keys(current).length === 1) {
159
+ return current.data;
160
+ }
161
+ return current;
162
+ }
163
+ function unwrapDiscussionRecord(payload) {
164
+ const data = unwrapDiscussionData(payload);
165
+ if (!isRecord(data)) {
166
+ throw new Error('Hosted discussion op returned an unexpected (non-record) payload.');
167
+ }
168
+ return data;
169
+ }
170
+ function unwrapDiscussionList(payload) {
171
+ const data = unwrapDiscussionData(payload);
172
+ if (Array.isArray(data))
173
+ return data.filter(isRecord);
174
+ if (isRecord(data)) {
175
+ for (const candidate of [data.items, data.data, data.results, data.rows]) {
176
+ if (Array.isArray(candidate))
177
+ return candidate.filter(isRecord);
178
+ }
179
+ }
180
+ return [];
181
+ }
182
+ function unwrapTopicDetail(payload) {
183
+ const data = unwrapDiscussionData(payload);
184
+ if (!isRecord(data)) {
185
+ throw new Error('Hosted discussion-topic.get returned an unexpected payload.');
186
+ }
187
+ const topic = isRecord(data.topic) ? data.topic : data;
188
+ const turns = Array.isArray(data.turns) ? data.turns.filter(isRecord) : [];
189
+ const outputs = Array.isArray(data.outputs) ? data.outputs.filter(isRecord) : [];
190
+ return { topic, turns, outputs };
191
+ }
192
+ function unwrapStatus(payload) {
193
+ const data = unwrapDiscussionRecord(payload);
194
+ const openQuestions = Array.isArray(data.openQuestions)
195
+ ? data.openQuestions.filter(isRecord).map((entry) => ({
196
+ seq: typeof entry.seq === 'number' ? entry.seq : undefined,
197
+ agentId: normalizeNonEmpty(entry.agentId),
198
+ text: typeof entry.text === 'string' ? entry.text : undefined,
199
+ }))
200
+ : [];
201
+ return {
202
+ topicId: normalizeNonEmpty(data.topicId),
203
+ status: normalizeNonEmpty(data.status),
204
+ blockedOn: typeof data.blockedOn === 'string' ? data.blockedOn : null,
205
+ nextSpeaker: typeof data.nextSpeaker === 'string' ? data.nextSpeaker : null,
206
+ canConclude: data.canConclude === true,
207
+ openQuestions,
208
+ reason: normalizeNonEmpty(data.reason),
209
+ };
210
+ }
211
+ async function resolveDiscussionContext(options) {
212
+ return resolveAgentGatewayContext(options);
213
+ }
214
+ async function invokeDiscussionTool(options, context, params) {
215
+ const apiState = await requireApiState(options);
216
+ if (!apiState) {
217
+ throw new Error('API host is not reachable or authentication is missing.');
218
+ }
219
+ return invokeHostedToolWithApiState(apiState, {
220
+ ...options,
221
+ scopeId: context.scopeId,
222
+ projectId: context.projectId,
223
+ projectName: context.projectName,
224
+ scopeResolution: context.scopeResolution,
225
+ toolId: params.toolId,
226
+ input: params.input,
227
+ preview: params.preview,
228
+ apply: params.apply,
229
+ idempotencyKey: options.idempotencyKey,
230
+ });
231
+ }
232
+ function emitEnvelope(params) {
233
+ const envelope = buildHostedSugarEnvelope({
234
+ command: params.command,
235
+ toolId: params.toolId,
236
+ resolvedContext: buildResolvedContextRecord(params.resolvedContext),
237
+ input: params.input,
238
+ result: params.result,
239
+ });
240
+ if (params.options.json) {
241
+ console.log(JSON.stringify(envelope, null, 2));
242
+ return;
243
+ }
244
+ logSuccess(params.successMessage);
245
+ console.log(JSON.stringify(params.result, null, 2));
246
+ }
247
+ async function readTextInput(params, label) {
248
+ const inline = normalizeNonEmpty(params.text);
249
+ if (inline)
250
+ return inline;
251
+ const fromFile = normalizeNonEmpty(params.fromFile);
252
+ if (!fromFile) {
253
+ throw new Error(`Provide --text or --from-file <path|-> for ${label}.`);
254
+ }
255
+ if (fromFile === '-') {
256
+ const chunks = [];
257
+ for await (const chunk of process.stdin) {
258
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
259
+ }
260
+ const content = Buffer.concat(chunks).toString('utf8').trim();
261
+ if (!content)
262
+ throw new Error(`Standard input was empty for ${label}.`);
263
+ return content;
264
+ }
265
+ const content = (await fs.readFile(fromFile, 'utf8')).trim();
266
+ if (!content)
267
+ throw new Error(`File "${fromFile}" is empty for ${label}.`);
268
+ return content;
269
+ }
270
+ function requireTopicSelector(value, flag) {
271
+ const selector = normalizeNonEmpty(value);
272
+ if (!selector)
273
+ throw new Error(`Provide ${flag} <topic-id|slug>.`);
274
+ return selector;
275
+ }
276
+ async function resolveTopicId(options, context, selector) {
277
+ const scopeId = requireScopeId(context);
278
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(selector)) {
279
+ return selector;
280
+ }
281
+ const payload = await invokeDiscussionTool(options, context, {
282
+ toolId: 'agentspace.discussion-topic.list',
283
+ input: compactPayload({ filter: compactPayload({ scopeId, slug: selector }) }),
284
+ });
285
+ const rows = unwrapDiscussionList(payload);
286
+ const matches = rows.filter((row) => normalizeNonEmpty(row.slug)?.toLowerCase() === selector.toLowerCase());
287
+ const candidates = matches.length > 0 ? matches : rows;
288
+ if (candidates.length === 0) {
289
+ throw new Error(`Hosted discussion topic was not found for selector "${selector}". Discover current topics with: aops-cli discuss list --limit 20 --json.`);
290
+ }
291
+ if (candidates.length > 1) {
292
+ const list = candidates
293
+ .slice(0, 5)
294
+ .map((row) => `${normalizeNonEmpty(row.slug) ?? '?'} [id=${normalizeNonEmpty(row.id) ?? '?'}, status=${normalizeNonEmpty(row.status) ?? '?'}]`)
295
+ .join('; ');
296
+ throw new Error(`Discussion selector "${selector}" is ambiguous across ${candidates.length} candidates: ${list}. Retry with the full hosted topic id.`);
297
+ }
298
+ const id = normalizeNonEmpty(candidates[0].id);
299
+ if (!id)
300
+ throw new Error(`Resolved hosted topic for "${selector}" has no id.`);
301
+ return id;
302
+ }
303
+ async function fetchTopicDetail(options, context, topicId) {
304
+ const payload = await invokeDiscussionTool(options, context, {
305
+ toolId: 'agentspace.discussion-topic.get',
306
+ input: { id: topicId },
307
+ });
308
+ return unwrapTopicDetail(payload);
309
+ }
310
+ async function fetchStatus(options, context, topicId) {
311
+ const payload = await invokeDiscussionTool(options, context, {
312
+ toolId: 'agentspace.discussion-topic.status',
313
+ input: { topicId },
314
+ });
315
+ return unwrapStatus(payload);
316
+ }
317
+ function topicParticipants(topic) {
318
+ return uniqueStrings(toStringArray(topic.participants));
319
+ }
320
+ function turnsFinalStanceAgents(turns) {
321
+ return uniqueStrings(turns
322
+ .filter((turn) => normalizeNonEmpty(turn.kind)?.toLowerCase() === 'final-stance')
323
+ .map((turn) => normalizeNonEmpty(turn.agentId)));
324
+ }
325
+ function deriveLifecycleState(params) {
326
+ const serverStatus = (normalizeNonEmpty(params.status.status) ?? normalizeNonEmpty(params.topic.status));
327
+ if (serverStatus === 'abandoned')
328
+ return 'abandoned';
329
+ if (params.status.blockedOn === 'operator')
330
+ return 'blocked-by-operator';
331
+ if (serverStatus === 'concluded')
332
+ return 'concluded';
333
+ if (serverStatus === 'concluding')
334
+ return 'concluding';
335
+ if (params.status.canConclude === true)
336
+ return 'ready-to-conclude';
337
+ const participants = topicParticipants(params.topic);
338
+ const stanced = turnsFinalStanceAgents(params.turns);
339
+ if (stanced.length === 0)
340
+ return 'active';
341
+ if (participants.length > 0 && stanced.length >= participants.length)
342
+ return 'ready-to-conclude';
343
+ return 'awaiting-final-stances';
344
+ }
345
+ function buildNextTurn(params) {
346
+ const { status, lifecycleState } = params;
347
+ if (lifecycleState === 'blocked-by-operator' ||
348
+ lifecycleState === 'ready-to-conclude' ||
349
+ lifecycleState === 'concluding' ||
350
+ lifecycleState === 'concluded' ||
351
+ lifecycleState === 'abandoned') {
352
+ return null;
353
+ }
354
+ if (status.openQuestions.length > 0) {
355
+ const participants = topicParticipants(params.topic);
356
+ const allowed = status.nextSpeaker ? [status.nextSpeaker] : participants;
357
+ return {
358
+ allowedAgents: allowed,
359
+ suggestedAgentId: status.nextSpeaker ?? allowed[0],
360
+ expectedKind: 'answer',
361
+ reason: `Open question(s) pending: seq ${status.openQuestions.map((q) => q.seq).filter((s) => s !== undefined).join(', ')}.`,
362
+ };
363
+ }
364
+ const next = normalizeNonEmpty(status.nextSpeaker);
365
+ if (next) {
366
+ return {
367
+ allowedAgents: [next],
368
+ suggestedAgentId: next,
369
+ reason: 'Hosted status nominates the next speaker (turn-order enforced server-side).',
370
+ };
371
+ }
372
+ const participants = topicParticipants(params.topic);
373
+ return {
374
+ allowedAgents: participants,
375
+ suggestedAgentId: participants[0],
376
+ reason: 'Free turn order allows any participant.',
377
+ };
378
+ }
379
+ function buildOperatorBlocks(status) {
380
+ if (status.blockedOn !== 'operator')
381
+ return [];
382
+ return status.openQuestions.map((question) => ({
383
+ seq: question.seq,
384
+ requestedBy: question.agentId,
385
+ question: question.text,
386
+ }));
387
+ }
388
+ function buildStatusPresentation(detail, status) {
389
+ const lifecycleState = deriveLifecycleState({ status, topic: detail.topic, turns: detail.turns });
390
+ const nextTurn = buildNextTurn({ status, topic: detail.topic, lifecycleState });
391
+ const participants = topicParticipants(detail.topic);
392
+ const stanced = turnsFinalStanceAgents(detail.turns);
393
+ const missingTurnFinalStances = participants.filter((agentId) => !stanced.some((s) => s.toLowerCase() === agentId.toLowerCase()));
394
+ const operatorBlocks = buildOperatorBlocks(status);
395
+ return compactPayload({
396
+ topic: detail.topic,
397
+ lifecycleState,
398
+ serverStatus: status.status,
399
+ canConclude: status.canConclude === true,
400
+ blockedOn: status.blockedOn ?? undefined,
401
+ nextSpeaker: status.nextSpeaker ?? undefined,
402
+ nextTurn,
403
+ openQuestions: status.openQuestions,
404
+ turnFinalStances: stanced,
405
+ missingTurnFinalStances,
406
+ operatorBlocks,
407
+ hasOpenQuestionsBlocking: status.openQuestions.length > 0,
408
+ reason: status.reason,
409
+ });
410
+ }
411
+ function buildPromptForNext(detail, status) {
412
+ const targetAgent = normalizeNonEmpty(status.nextSpeaker) ?? topicParticipants(detail.topic)[0];
413
+ if (!targetAgent)
414
+ return undefined;
415
+ const orderedTurns = [...detail.turns].sort((left, right) => Number(left.seq ?? 0) - Number(right.seq ?? 0));
416
+ const latestTurns = orderedTurns.slice(-5);
417
+ const rules = isRecord(detail.topic.rules) ? detail.topic.rules : {};
418
+ const lines = [
419
+ `You are ${targetAgent}.`,
420
+ `Topic: ${normalizeNonEmpty(detail.topic.title) ?? ''}`,
421
+ `Status: ${normalizeNonEmpty(detail.topic.status) ?? status.status ?? ''}`,
422
+ `Participants: ${topicParticipants(detail.topic).join(', ')}`,
423
+ normalizeNonEmpty(detail.topic.question) ? `Question: ${normalizeNonEmpty(detail.topic.question)}` : undefined,
424
+ '',
425
+ 'Rules:',
426
+ `- turnOrder: ${Array.isArray(rules.turnOrder) && rules.turnOrder.length > 0 ? rules.turnOrder.join(' -> ') : 'free'}`,
427
+ `- requireQuestionAnswer: ${rules.requireQuestionAnswer === true ? 'true' : 'false'}`,
428
+ rules.minTurnsBeforeConclude !== undefined ? `- minTurnsBeforeConclude: ${rules.minTurnsBeforeConclude}` : undefined,
429
+ '',
430
+ 'Open questions:',
431
+ ].filter((line) => Boolean(line) || line === '');
432
+ if (status.openQuestions.length === 0) {
433
+ lines.push('- none');
434
+ }
435
+ else {
436
+ for (const question of status.openQuestions) {
437
+ lines.push(`- [seq ${question.seq}] ${question.agentId ?? 'agent'}: ${question.text ?? ''}`);
438
+ }
439
+ }
440
+ lines.push('', 'Recent turns:');
441
+ if (latestTurns.length === 0) {
442
+ lines.push('- none');
443
+ }
444
+ else {
445
+ for (const turn of latestTurns) {
446
+ lines.push(`- #${turn.seq} ${normalizeNonEmpty(turn.agentId) ?? 'agent'} ${normalizeNonEmpty(turn.kind) ?? ''}: ${typeof turn.text === 'string' ? turn.text : ''}`);
447
+ }
448
+ }
449
+ lines.push('', 'Write exactly one next turn that follows the current hosted discussion rules.');
450
+ return `${lines.join('\n')}\n`;
451
+ }
452
+ export async function runDiscussStart(options = {}) {
453
+ try {
454
+ ensureGuardedWrite(options, 'This command creates a hosted discussion topic.');
455
+ assertMaxTurnsUnsupported(options.maxTurns);
456
+ const title = normalizeNonEmpty(options.title);
457
+ if (!title)
458
+ throw new Error('Provide --title.');
459
+ const context = await resolveDiscussionContext(options);
460
+ const scopeId = requireScopeId(context);
461
+ const participants = normalizeParticipants(options.agent);
462
+ const slug = normalizeSlug(normalizeNonEmpty(options.slug) ?? title);
463
+ const question = foldObjectiveIntoQuestion(options.objective, options.question);
464
+ const data = compactPayload({
465
+ scopeId,
466
+ slug,
467
+ title,
468
+ question,
469
+ participants,
470
+ initiatorAgentId: participants[0],
471
+ subjectType: normalizeNonEmpty(options.subjectType),
472
+ subjectId: normalizeNonEmpty(options.subjectId),
473
+ tags: uniqueStrings(toStringArray(options.tag)),
474
+ rules: buildRules({
475
+ turnOrder: options.turnOrder,
476
+ participants,
477
+ minTurnsBeforeConclude: options.minTurnsBeforeConclude,
478
+ requireQuestionAnswer: options.requireQuestionAnswer,
479
+ }),
480
+ });
481
+ const payload = await invokeDiscussionTool(options, context, {
482
+ toolId: 'agentspace.discussion-topic.create',
483
+ input: { data },
484
+ apply: options.apply,
485
+ preview: options.preview,
486
+ });
487
+ const result = compactPayload({ topic: unwrapDiscussionRecord(payload) });
488
+ emitEnvelope({
489
+ options,
490
+ command: 'discuss.start',
491
+ toolId: 'agentspace.discussion-topic.create',
492
+ resolvedContext: context,
493
+ input: { data },
494
+ result,
495
+ successMessage: 'Hosted discussion topic started.',
496
+ });
497
+ }
498
+ catch (error) {
499
+ logError(`Failed to execute discuss start: ${error instanceof Error ? error.message : String(error)}`);
500
+ process.exitCode = 1;
501
+ }
502
+ }
503
+ async function runDiscussChildTopic(options, params) {
504
+ try {
505
+ ensureGuardedWrite(options, 'This command creates a hosted discussion topic.');
506
+ assertMaxTurnsUnsupported(options.maxTurns);
507
+ const title = normalizeNonEmpty(options.title);
508
+ if (!title)
509
+ throw new Error('Provide --title.');
510
+ const context = await resolveDiscussionContext(options);
511
+ const scopeId = requireScopeId(context);
512
+ const parentSelector = requireTopicSelector(options.from, '--from');
513
+ const parentId = await resolveTopicId(options, context, parentSelector);
514
+ const parent = await fetchTopicDetail(options, context, parentId);
515
+ const parentParticipants = topicParticipants(parent.topic);
516
+ const participants = toStringArray(options.agent).length > 0
517
+ ? normalizeParticipants(options.agent)
518
+ : parentParticipants;
519
+ if (participants.length < 2) {
520
+ throw new Error('Provide at least two unique --agent values (parent topic did not supply enough participants).');
521
+ }
522
+ const slug = normalizeSlug(normalizeNonEmpty(options.slug) ?? title);
523
+ const question = foldObjectiveIntoQuestion(options.objective, options.question);
524
+ const parentRules = isRecord(parent.topic.rules) ? parent.topic.rules : {};
525
+ const rules = buildRules({
526
+ turnOrder: options.turnOrder ?? (Array.isArray(parentRules.turnOrder) && parentRules.turnOrder.length > 0 ? 'alternating' : 'free'),
527
+ participants,
528
+ minTurnsBeforeConclude: options.minTurnsBeforeConclude
529
+ ?? (typeof parentRules.minTurnsBeforeConclude === 'number' ? parentRules.minTurnsBeforeConclude : undefined),
530
+ requireQuestionAnswer: options.requireQuestionAnswer === true
531
+ ? true
532
+ : parentRules.requireQuestionAnswer === true,
533
+ });
534
+ const tags = uniqueStrings([
535
+ ...(options.inheritTags === true ? toStringArray(parent.topic.tags) : []),
536
+ ...toStringArray(options.tag),
537
+ params.lineageKind,
538
+ ]);
539
+ const data = compactPayload({
540
+ scopeId,
541
+ slug,
542
+ title,
543
+ question,
544
+ participants,
545
+ initiatorAgentId: participants[0],
546
+ subjectType: options.inheritSubject === true ? normalizeNonEmpty(parent.topic.subjectType) : undefined,
547
+ subjectId: options.inheritSubject === true ? normalizeNonEmpty(parent.topic.subjectId) : undefined,
548
+ tags,
549
+ rules,
550
+ parentTopicId: parentId,
551
+ lineageKind: params.lineageKind,
552
+ referencedOutputs: uniqueStrings(toStringArray(options.referenceOutput)),
553
+ referencedTurnRefs: uniqueStrings(toStringArray(options.referenceTurn)),
554
+ referencedMemoryRefs: uniqueStrings(toStringArray(options.referenceMemory)),
555
+ });
556
+ const payload = await invokeDiscussionTool(options, context, {
557
+ toolId: 'agentspace.discussion-topic.create',
558
+ input: { data },
559
+ apply: options.apply,
560
+ preview: options.preview,
561
+ });
562
+ const result = compactPayload({
563
+ topic: unwrapDiscussionRecord(payload),
564
+ parent: parent.topic,
565
+ lineage: compactPayload({ kind: params.lineageKind, parentTopicId: parentId }),
566
+ });
567
+ emitEnvelope({
568
+ options,
569
+ command: params.command,
570
+ toolId: 'agentspace.discussion-topic.create',
571
+ resolvedContext: context,
572
+ input: { data },
573
+ result,
574
+ successMessage: params.successMessage,
575
+ });
576
+ }
577
+ catch (error) {
578
+ logError(`Failed to execute ${params.command}: ${error instanceof Error ? error.message : String(error)}`);
579
+ process.exitCode = 1;
580
+ }
581
+ }
582
+ export async function runDiscussFollowUp(options = {}) {
583
+ await runDiscussChildTopic(options, {
584
+ lineageKind: 'follow-up',
585
+ command: 'discuss.follow-up',
586
+ successMessage: 'Hosted discussion follow-up topic started.',
587
+ });
588
+ }
589
+ export async function runDiscussFork(options = {}) {
590
+ await runDiscussChildTopic(options, {
591
+ lineageKind: 'fork',
592
+ command: 'discuss.fork',
593
+ successMessage: 'Hosted discussion fork topic started.',
594
+ });
595
+ }
596
+ export async function runDiscussList(options = {}) {
597
+ try {
598
+ const context = await resolveDiscussionContext(options);
599
+ const scopeId = requireScopeId(context);
600
+ const status = normalizeListStatus(options.status);
601
+ normalizeDiscussionScopeFilter(options.scope);
602
+ const limit = Number.isFinite(options.limit) ? Math.max(0, Math.trunc(options.limit ?? 0)) : undefined;
603
+ const filter = compactPayload({
604
+ scopeId,
605
+ status,
606
+ subjectType: normalizeNonEmpty(options.subjectType),
607
+ subjectId: normalizeNonEmpty(options.subjectId),
608
+ });
609
+ const input = compactPayload({
610
+ filter,
611
+ options: compactPayload({ limit: typeof limit === 'number' && limit > 0 ? limit : undefined }),
612
+ });
613
+ const payload = await invokeDiscussionTool(options, context, {
614
+ toolId: 'agentspace.discussion-topic.list',
615
+ input,
616
+ });
617
+ let rows = unwrapDiscussionList(payload);
618
+ const includeAbandoned = options.includeAbandoned === true || status === 'abandoned';
619
+ if (!includeAbandoned) {
620
+ rows = rows.filter((row) => normalizeNonEmpty(row.status)?.toLowerCase() !== 'abandoned');
621
+ }
622
+ const agents = uniqueStrings(toStringArray(options.agent)).map((entry) => entry.toLowerCase());
623
+ if (agents.length > 0) {
624
+ rows = rows.filter((row) => {
625
+ const participants = topicParticipants(row).map((entry) => entry.toLowerCase());
626
+ return agents.every((agent) => participants.includes(agent));
627
+ });
628
+ }
629
+ if (typeof limit === 'number' && limit > 0)
630
+ rows = rows.slice(0, limit);
631
+ const result = { data: rows };
632
+ emitEnvelope({
633
+ options,
634
+ command: 'discuss.list',
635
+ toolId: 'agentspace.discussion-topic.list',
636
+ resolvedContext: context,
637
+ input,
638
+ result,
639
+ successMessage: 'Hosted discussion topics loaded.',
640
+ });
641
+ }
642
+ catch (error) {
643
+ logError(`Failed to execute discuss list: ${error instanceof Error ? error.message : String(error)}`);
644
+ process.exitCode = 1;
645
+ }
646
+ }
647
+ export async function runDiscussGet(options = {}) {
648
+ try {
649
+ const context = await resolveDiscussionContext(options);
650
+ const selector = requireTopicSelector(options.id, '--id');
651
+ const topicId = await resolveTopicId(options, context, selector);
652
+ const detail = await fetchTopicDetail(options, context, topicId);
653
+ const result = compactPayload({
654
+ topic: detail.topic,
655
+ turns: detail.turns,
656
+ outputs: detail.outputs,
657
+ });
658
+ emitEnvelope({
659
+ options,
660
+ command: 'discuss.get',
661
+ toolId: 'agentspace.discussion-topic.get',
662
+ resolvedContext: context,
663
+ input: { id: topicId },
664
+ result,
665
+ successMessage: 'Hosted discussion topic loaded.',
666
+ });
667
+ }
668
+ catch (error) {
669
+ logError(`Failed to execute discuss get: ${error instanceof Error ? error.message : String(error)}`);
670
+ process.exitCode = 1;
671
+ }
672
+ }
673
+ export async function runDiscussLineage(options = {}) {
674
+ try {
675
+ const context = await resolveDiscussionContext(options);
676
+ const scopeId = requireScopeId(context);
677
+ const rootSelector = requireTopicSelector(options.root, '--root');
678
+ const rootId = await resolveTopicId(options, context, rootSelector);
679
+ const rootDetail = await fetchTopicDetail(options, context, rootId);
680
+ const includeAbandoned = options.includeAbandoned === true;
681
+ const seen = new Set([rootId.toLowerCase()]);
682
+ const fetchChildren = async (parentId) => {
683
+ const payload = await invokeDiscussionTool(options, context, {
684
+ toolId: 'agentspace.discussion-topic.list',
685
+ input: compactPayload({ filter: compactPayload({ scopeId, parentTopicId: parentId }) }),
686
+ });
687
+ let rows = unwrapDiscussionList(payload);
688
+ if (!includeAbandoned) {
689
+ rows = rows.filter((row) => normalizeNonEmpty(row.status)?.toLowerCase() !== 'abandoned');
690
+ }
691
+ return rows.filter((row) => normalizeNonEmpty(row.parentTopicId)?.toLowerCase() === parentId.toLowerCase());
692
+ };
693
+ const buildNode = async (topic, depth) => {
694
+ const id = normalizeNonEmpty(topic.id);
695
+ const children = [];
696
+ if (id) {
697
+ const childRows = await fetchChildren(id);
698
+ for (const child of childRows) {
699
+ const childId = normalizeNonEmpty(child.id);
700
+ if (!childId || seen.has(childId.toLowerCase()))
701
+ continue;
702
+ seen.add(childId.toLowerCase());
703
+ children.push(await buildNode(child, depth + 1));
704
+ }
705
+ }
706
+ return { topic, depth, children };
707
+ };
708
+ const tree = await buildNode(rootDetail.topic, 0);
709
+ const flatten = (node) => [
710
+ compactPayload({ topic: node.topic, depth: node.depth, childCount: node.children.length }),
711
+ ...node.children.flatMap(flatten),
712
+ ];
713
+ const result = compactPayload({
714
+ root: rootDetail.topic,
715
+ includeAbandoned,
716
+ tree,
717
+ flat: flatten(tree),
718
+ });
719
+ emitEnvelope({
720
+ options,
721
+ command: 'discuss.lineage',
722
+ toolId: 'agentspace.discussion-topic.list',
723
+ resolvedContext: context,
724
+ input: compactPayload({ root: rootId, includeAbandoned }),
725
+ result,
726
+ successMessage: 'Hosted discussion lineage loaded.',
727
+ });
728
+ }
729
+ catch (error) {
730
+ logError(`Failed to execute discuss lineage: ${error instanceof Error ? error.message : String(error)}`);
731
+ process.exitCode = 1;
732
+ }
733
+ }
734
+ function resolveAddressedTo(to) {
735
+ const target = normalizeNonEmpty(to);
736
+ if (!target)
737
+ return undefined;
738
+ if (target.toLowerCase() === DISCUSSION_OPERATOR_TARGET)
739
+ return DISCUSSION_OPERATOR_TARGET;
740
+ throw new Error(`server-first-unsupported: --to "${target}" cannot address a specific named participant. The hosted turn addressedTo enum is ['agent','operator']; only --to operator maps (operator block). Use --reply-to <seq> to correlate an answer to a specific question instead. (PM-issue candidate: hosted turns cannot target a named participant.)`);
741
+ }
742
+ function parseSeq(value, flag) {
743
+ const normalized = normalizeNonEmpty(value);
744
+ if (!normalized)
745
+ return undefined;
746
+ const parsed = Number.parseInt(normalized, 10);
747
+ if (!Number.isInteger(parsed) || parsed < 1) {
748
+ throw new Error(`${flag} must be a positive integer turn seq (hosted reply correlation is by seq, not turn id).`);
749
+ }
750
+ return parsed;
751
+ }
752
+ export async function runDiscussTurn(options = {}) {
753
+ try {
754
+ ensureGuardedWrite(options, 'This command appends a turn to a hosted discussion topic.');
755
+ const selector = requireTopicSelector(options.topic, '--topic');
756
+ const agentId = normalizeNonEmpty(options.agent);
757
+ if (!agentId)
758
+ throw new Error('Provide --agent.');
759
+ const kind = normalizeTurnKind(options.kind ?? 'statement');
760
+ const addressedTo = resolveAddressedTo(options.to);
761
+ const replyToSeq = parseSeq(options.replyTo, '--reply-to');
762
+ const expectNext = normalizeNonEmpty(options.expectNext);
763
+ if (expectNext && expectNext.toLowerCase() !== agentId.toLowerCase()) {
764
+ throw new Error(`--expect-next must match --agent. Expected ${expectNext}, got ${agentId}.`);
765
+ }
766
+ const text = await readTextInput({ text: options.text, fromFile: options.fromFile }, 'discussion turn');
767
+ const context = await resolveDiscussionContext(options);
768
+ const scopeId = requireScopeId(context);
769
+ const topicId = await resolveTopicId(options, context, selector);
770
+ const data = compactPayload({
771
+ scopeId,
772
+ topicId,
773
+ agentId,
774
+ kind,
775
+ text,
776
+ addressedTo,
777
+ replyToSeq,
778
+ idempotencyKey: normalizeNonEmpty(options.idempotencyKey),
779
+ });
780
+ const payload = await invokeDiscussionTool(options, context, {
781
+ toolId: 'agentspace.discussion-turn.add',
782
+ input: { data },
783
+ apply: options.apply,
784
+ preview: options.preview,
785
+ });
786
+ const turn = unwrapDiscussionRecord(payload);
787
+ let statusPresentation;
788
+ if (options.apply === true) {
789
+ try {
790
+ const detail = await fetchTopicDetail(options, context, topicId);
791
+ const status = await fetchStatus(options, context, topicId);
792
+ statusPresentation = buildStatusPresentation(detail, status);
793
+ }
794
+ catch {
795
+ statusPresentation = undefined;
796
+ }
797
+ }
798
+ const result = compactPayload({ turn, status: statusPresentation });
799
+ emitEnvelope({
800
+ options,
801
+ command: 'discuss.turn',
802
+ toolId: 'agentspace.discussion-turn.add',
803
+ resolvedContext: context,
804
+ input: { data },
805
+ result,
806
+ successMessage: 'Hosted discussion turn written.',
807
+ });
808
+ }
809
+ catch (error) {
810
+ logError(`Failed to execute discuss turn: ${error instanceof Error ? error.message : String(error)}`);
811
+ process.exitCode = 1;
812
+ }
813
+ }
814
+ export async function runDiscussStatus(options = {}) {
815
+ try {
816
+ const context = await resolveDiscussionContext(options);
817
+ const selector = requireTopicSelector(options.id, '--id');
818
+ const topicId = await resolveTopicId(options, context, selector);
819
+ const detail = await fetchTopicDetail(options, context, topicId);
820
+ const status = await fetchStatus(options, context, topicId);
821
+ const presentation = buildStatusPresentation(detail, status);
822
+ const result = compactPayload({
823
+ ...presentation,
824
+ promptForNext: options.promptForNext === true ? buildPromptForNext(detail, status) : undefined,
825
+ });
826
+ emitEnvelope({
827
+ options,
828
+ command: 'discuss.status',
829
+ toolId: 'agentspace.discussion-topic.status',
830
+ resolvedContext: context,
831
+ input: compactPayload({ id: topicId, promptForNext: options.promptForNext === true }),
832
+ result,
833
+ successMessage: 'Hosted discussion status loaded.',
834
+ });
835
+ }
836
+ catch (error) {
837
+ logError(`Failed to execute discuss status: ${error instanceof Error ? error.message : String(error)}`);
838
+ process.exitCode = 1;
839
+ }
840
+ }
841
+ export async function runDiscussDigest(options = {}) {
842
+ try {
843
+ const context = await resolveDiscussionContext(options);
844
+ const selector = requireTopicSelector(options.id, '--id');
845
+ const topicId = await resolveTopicId(options, context, selector);
846
+ const detail = await fetchTopicDetail(options, context, topicId);
847
+ const status = await fetchStatus(options, context, topicId);
848
+ const presentation = buildStatusPresentation(detail, status);
849
+ const result = compactPayload({
850
+ topic: detail.topic,
851
+ rules: isRecord(detail.topic.rules) ? detail.topic.rules : undefined,
852
+ status: presentation,
853
+ turns: detail.turns,
854
+ outputs: detail.outputs,
855
+ promptForNext: options.promptForNext === true ? buildPromptForNext(detail, status) : undefined,
856
+ });
857
+ emitEnvelope({
858
+ options,
859
+ command: 'discuss.digest',
860
+ toolId: 'agentspace.discussion-topic.get',
861
+ resolvedContext: context,
862
+ input: compactPayload({ id: topicId, promptForNext: options.promptForNext === true }),
863
+ result,
864
+ successMessage: 'Hosted discussion digest generated.',
865
+ });
866
+ }
867
+ catch (error) {
868
+ logError(`Failed to execute discuss digest: ${error instanceof Error ? error.message : String(error)}`);
869
+ process.exitCode = 1;
870
+ }
871
+ }
872
+ function sleep(ms) {
873
+ return new Promise((resolve) => setTimeout(resolve, ms));
874
+ }
875
+ function classifyWaitState(presentation, status, agentId) {
876
+ const lifecycleState = normalizeNonEmpty(presentation.lifecycleState);
877
+ if (status.blockedOn === 'operator' || lifecycleState === 'blocked-by-operator') {
878
+ return {
879
+ exitCode: DISCUSSION_WAIT_OPERATOR_BLOCK_EXIT_CODE,
880
+ outcome: 'operator-blocked',
881
+ reason: 'Discussion is blocked by an operator-addressed open question.',
882
+ };
883
+ }
884
+ if (lifecycleState === 'ready-to-conclude' ||
885
+ lifecycleState === 'concluding' ||
886
+ lifecycleState === 'concluded' ||
887
+ lifecycleState === 'abandoned') {
888
+ return {
889
+ exitCode: DISCUSSION_WAIT_DONE_EXIT_CODE,
890
+ outcome: 'done',
891
+ reason: `Discussion lifecycleState is ${lifecycleState}.`,
892
+ };
893
+ }
894
+ const nextTurn = isRecord(presentation.nextTurn) ? presentation.nextTurn : null;
895
+ const allowed = nextTurn ? toStringArray(nextTurn.allowedAgents).map((entry) => entry.toLowerCase()) : [];
896
+ if (allowed.includes(agentId.toLowerCase())) {
897
+ return {
898
+ exitCode: 0,
899
+ outcome: 'turn-ready',
900
+ reason: `${agentId} is allowed to write the next turn.`,
901
+ };
902
+ }
903
+ return null;
904
+ }
905
+ export async function runDiscussWait(options = {}) {
906
+ try {
907
+ const agentId = normalizeNonEmpty(options.for);
908
+ if (!agentId)
909
+ throw new Error('Provide --for <agent-id>.');
910
+ const timeoutSec = typeof options.timeoutSec === 'number' && Number.isFinite(options.timeoutSec)
911
+ ? Math.max(0, options.timeoutSec)
912
+ : 300;
913
+ const intervalSec = typeof options.intervalSec === 'number' && Number.isFinite(options.intervalSec) && options.intervalSec > 0
914
+ ? options.intervalSec
915
+ : 5;
916
+ const context = await resolveDiscussionContext(options);
917
+ const selector = requireTopicSelector(options.id, '--id');
918
+ const topicId = await resolveTopicId(options, context, selector);
919
+ const deadline = Date.now() + timeoutSec * 1000;
920
+ while (true) {
921
+ const detail = await fetchTopicDetail(options, context, topicId);
922
+ const status = await fetchStatus(options, context, topicId);
923
+ const presentation = buildStatusPresentation(detail, status);
924
+ const classified = classifyWaitState(presentation, status, agentId);
925
+ if (classified) {
926
+ const result = compactPayload({
927
+ wait: { agentId, outcome: classified.outcome, exitCode: classified.exitCode, reason: classified.reason },
928
+ status: presentation,
929
+ });
930
+ if (classified.exitCode !== 0)
931
+ process.exitCode = classified.exitCode;
932
+ emitEnvelope({
933
+ options,
934
+ command: 'discuss.wait',
935
+ toolId: 'agentspace.discussion-topic.status',
936
+ resolvedContext: context,
937
+ input: compactPayload({ id: topicId, for: agentId, timeoutSec, intervalSec }),
938
+ result,
939
+ successMessage: 'Hosted discussion wait completed.',
940
+ });
941
+ return;
942
+ }
943
+ if (Date.now() >= deadline) {
944
+ const result = compactPayload({
945
+ wait: {
946
+ agentId,
947
+ outcome: 'timeout',
948
+ exitCode: DISCUSSION_WAIT_TIMEOUT_EXIT_CODE,
949
+ reason: `${agentId} did not become the next allowed discussion agent before timeout.`,
950
+ },
951
+ status: presentation,
952
+ });
953
+ process.exitCode = DISCUSSION_WAIT_TIMEOUT_EXIT_CODE;
954
+ emitEnvelope({
955
+ options,
956
+ command: 'discuss.wait',
957
+ toolId: 'agentspace.discussion-topic.status',
958
+ resolvedContext: context,
959
+ input: compactPayload({ id: topicId, for: agentId, timeoutSec, intervalSec }),
960
+ result,
961
+ successMessage: 'Hosted discussion wait timed out.',
962
+ });
963
+ return;
964
+ }
965
+ await sleep(Math.min(intervalSec * 1000, Math.max(0, deadline - Date.now())));
966
+ }
967
+ }
968
+ catch (error) {
969
+ logError(`Failed to execute discuss wait: ${error instanceof Error ? error.message : String(error)}`);
970
+ process.exitCode = 1;
971
+ }
972
+ }
973
+ function buildPersistentLoopPrompt(params) {
974
+ const waitCommand = `aops-cli discuss wait --id ${params.topicSelector} --for ${params.agentId} --timeout-sec ${params.timeoutSec} --interval-sec ${params.intervalSec} --json`;
975
+ const statusCommand = `aops-cli discuss status --id ${params.topicSelector} --prompt-for-next --json`;
976
+ const turnCommand = `aops-cli discuss turn --topic ${params.topicSelector} --agent ${params.agentId} --expect-next ${params.agentId} --kind statement --text "<turn metni>" --apply --json`;
977
+ return [
978
+ `Bu topic'te ${params.agentId} agent olarak kalici loopta calis.`,
979
+ '',
980
+ `Topic: ${params.topicSelector}`,
981
+ `Agent: ${params.agentId}`,
982
+ '',
983
+ 'Dongu:',
984
+ '1. Surekli su komutla sirani bekle:',
985
+ ` ${waitCommand}`,
986
+ '',
987
+ '2. Exit 0 olursa:',
988
+ ` ${statusCommand}`,
989
+ ' ciktisindaki promptForNext, lifecycleState ve recent turns uzerinden tam bir sonraki turn metnini uret.',
990
+ ' Sonra:',
991
+ ` ${turnCommand}`,
992
+ '',
993
+ '3. Eger status.lifecycleState `awaiting-final-stances` ise ve missingTurnFinalStances icinde bu agent varsa `--kind final-stance` kullan.',
994
+ '',
995
+ "4. Exit 20 ise operator sorusu/blok var: dur ve operator'e bildir.",
996
+ "5. Exit 21 ise terminal/conclude/abandon boundary: dur ve operator'e bildir.",
997
+ "6. Exit 22 ise timeout: durumu bildir, ama operator aksini soylemedikce loop'u birakma ve tekrar wait'e don.",
998
+ '',
999
+ 'Onemli:',
1000
+ '- Her seferinde sadece bir turn yaz.',
1001
+ "- Turn yazdiktan sonra cikma; tekrar wait'e don.",
1002
+ '- Operator sorusu, terminal state veya acik blocker yoksa loopta kal.',
1003
+ '',
1004
+ ].join('\n');
1005
+ }
1006
+ export async function runDiscussLoopPrompt(options = {}) {
1007
+ try {
1008
+ const agentId = normalizeNonEmpty(options.for);
1009
+ if (!agentId)
1010
+ throw new Error('Provide --for <agent-id>.');
1011
+ const timeoutSec = typeof options.timeoutSec === 'number' && Number.isFinite(options.timeoutSec)
1012
+ ? Math.max(0, options.timeoutSec)
1013
+ : 540;
1014
+ const intervalSec = typeof options.intervalSec === 'number' && Number.isFinite(options.intervalSec) && options.intervalSec > 0
1015
+ ? options.intervalSec
1016
+ : 5;
1017
+ const context = await resolveDiscussionContext(options);
1018
+ const selector = requireTopicSelector(options.id, '--id');
1019
+ const topicId = await resolveTopicId(options, context, selector);
1020
+ const detail = await fetchTopicDetail(options, context, topicId);
1021
+ if (!topicParticipants(detail.topic).some((participant) => participant.toLowerCase() === agentId.toLowerCase())) {
1022
+ throw new Error(`Agent "${agentId}" is not a participant in this discussion.`);
1023
+ }
1024
+ const topicSelector = normalizeNonEmpty(detail.topic.slug) ?? topicId;
1025
+ const prompt = buildPersistentLoopPrompt({ topicSelector, agentId, timeoutSec, intervalSec });
1026
+ const result = compactPayload({
1027
+ topic: detail.topic,
1028
+ agentId,
1029
+ timeoutSec,
1030
+ intervalSec,
1031
+ waitCommand: `aops-cli discuss wait --id ${topicSelector} --for ${agentId} --timeout-sec ${timeoutSec} --interval-sec ${intervalSec} --json`,
1032
+ statusCommand: `aops-cli discuss status --id ${topicSelector} --prompt-for-next --json`,
1033
+ turnCommandTemplate: `aops-cli discuss turn --topic ${topicSelector} --agent ${agentId} --expect-next ${agentId} --kind <statement|final-stance> --text "<turn metni>" --apply --json`,
1034
+ prompt,
1035
+ });
1036
+ if (options.json) {
1037
+ console.log(JSON.stringify(buildHostedSugarEnvelope({
1038
+ command: 'discuss.loop-prompt',
1039
+ toolId: 'agentspace.discussion-topic.get',
1040
+ resolvedContext: buildResolvedContextRecord(context),
1041
+ input: compactPayload({ id: topicId, for: agentId, timeoutSec, intervalSec }),
1042
+ result,
1043
+ }), null, 2));
1044
+ return;
1045
+ }
1046
+ console.log(prompt);
1047
+ }
1048
+ catch (error) {
1049
+ logError(`Failed to execute discuss loop-prompt: ${error instanceof Error ? error.message : String(error)}`);
1050
+ process.exitCode = 1;
1051
+ }
1052
+ }
1053
+ export async function runDiscussConclude(options = {}) {
1054
+ try {
1055
+ ensureGuardedWrite(options, 'This command concludes a hosted discussion topic.');
1056
+ const context = await resolveDiscussionContext(options);
1057
+ const selector = requireTopicSelector(options.topic, '--topic');
1058
+ const topicId = await resolveTopicId(options, context, selector);
1059
+ const input = compactPayload({ topicId, updatedBy: normalizeNonEmpty(options.updatedBy) });
1060
+ const payload = await invokeDiscussionTool(options, context, {
1061
+ toolId: 'agentspace.discussion-topic.conclude',
1062
+ input,
1063
+ apply: options.apply,
1064
+ preview: options.preview,
1065
+ });
1066
+ const result = compactPayload({ topic: unwrapDiscussionRecord(payload) });
1067
+ emitEnvelope({
1068
+ options,
1069
+ command: 'discuss.conclude',
1070
+ toolId: 'agentspace.discussion-topic.conclude',
1071
+ resolvedContext: context,
1072
+ input,
1073
+ result,
1074
+ successMessage: 'Hosted discussion topic concluded.',
1075
+ });
1076
+ }
1077
+ catch (error) {
1078
+ logError(`Failed to execute discuss conclude: ${error instanceof Error ? error.message : String(error)}`);
1079
+ process.exitCode = 1;
1080
+ }
1081
+ }
1082
+ export async function runDiscussAbandon(options = {}) {
1083
+ try {
1084
+ ensureGuardedWrite(options, 'This command abandons a hosted discussion topic.');
1085
+ const reason = normalizeNonEmpty(options.reason);
1086
+ if (!reason)
1087
+ throw new Error('Provide --reason <text>.');
1088
+ const context = await resolveDiscussionContext(options);
1089
+ const selector = requireTopicSelector(options.topic, '--topic');
1090
+ const topicId = await resolveTopicId(options, context, selector);
1091
+ const input = compactPayload({ topicId, reason });
1092
+ const payload = await invokeDiscussionTool(options, context, {
1093
+ toolId: 'agentspace.discussion-topic.abandon',
1094
+ input,
1095
+ apply: options.apply,
1096
+ preview: options.preview,
1097
+ });
1098
+ const result = compactPayload({ topic: unwrapDiscussionRecord(payload) });
1099
+ emitEnvelope({
1100
+ options,
1101
+ command: 'discuss.abandon',
1102
+ toolId: 'agentspace.discussion-topic.abandon',
1103
+ resolvedContext: context,
1104
+ input,
1105
+ result,
1106
+ successMessage: 'Hosted discussion topic abandoned.',
1107
+ });
1108
+ }
1109
+ catch (error) {
1110
+ logError(`Failed to execute discuss abandon: ${error instanceof Error ? error.message : String(error)}`);
1111
+ process.exitCode = 1;
1112
+ }
1113
+ }
1114
+ export async function runDiscussOutput(options = {}) {
1115
+ try {
1116
+ ensureGuardedWrite(options, 'This command sets a hosted discussion output.');
1117
+ const context = await resolveDiscussionContext(options);
1118
+ const scopeId = requireScopeId(context);
1119
+ const selector = requireTopicSelector(options.topic, '--topic');
1120
+ const topicId = await resolveTopicId(options, context, selector);
1121
+ const outputKind = normalizeNonEmpty(options.outputKind);
1122
+ if (!outputKind)
1123
+ throw new Error('Provide --output-kind <kind> (e.g. consensus|disagreement|open-questions|final-stance|decision|agent-summary).');
1124
+ const ownerAgentId = normalizeNonEmpty(options.owner);
1125
+ if (!ownerAgentId)
1126
+ throw new Error('Provide --owner <agent-id> (must be a topic participant; server-enforced).');
1127
+ const content = await readTextInput({ text: options.text, fromFile: options.fromFile }, 'discussion output');
1128
+ const data = compactPayload({ scopeId, topicId, outputKind, ownerAgentId, content });
1129
+ const payload = await invokeDiscussionTool(options, context, {
1130
+ toolId: 'agentspace.discussion-output.set',
1131
+ input: { data },
1132
+ apply: options.apply,
1133
+ preview: options.preview,
1134
+ });
1135
+ const result = compactPayload({ output: unwrapDiscussionRecord(payload) });
1136
+ emitEnvelope({
1137
+ options,
1138
+ command: 'discuss.output',
1139
+ toolId: 'agentspace.discussion-output.set',
1140
+ resolvedContext: context,
1141
+ input: { data },
1142
+ result,
1143
+ successMessage: 'Hosted discussion output set.',
1144
+ });
1145
+ }
1146
+ catch (error) {
1147
+ logError(`Failed to execute discuss output: ${error instanceof Error ? error.message : String(error)}`);
1148
+ process.exitCode = 1;
1149
+ }
1150
+ }
1151
+ function addDiscussionContextOptions(cmd) {
1152
+ applyCommonOptions(cmd, { withAuth: true, withProject: true, withYes: true, withJson: true });
1153
+ cmd.option('--scope-id <id>', 'Canonical owner scope override');
1154
+ cmd.option('--scope-resolution <mode>', 'Scope resolution for hosted reads: explicit | cascade');
1155
+ cmd.option('--tenant-id <id>', 'Tenant id header (x-tenant-id)');
1156
+ cmd.option('--locale <locale>', 'Locale header (x-locale)');
1157
+ cmd.option('--fallback-locale <locale>', 'Fallback locale header (x-fallback-locale)');
1158
+ return cmd;
1159
+ }
1160
+ export function makeDiscussCommand() {
1161
+ const cmd = new Command('discuss')
1162
+ .description('Manage hosted (server-first) Agentspace discussion topics');
1163
+ addDiscussionContextOptions(cmd);
1164
+ cmd.addHelpText('after', `
1165
+ Server-first discussion (no local files):
1166
+ Source of truth is the hosted Agentspace discussion ops. The CLI never writes
1167
+ or reads .aops/agentspace/discussions; create/write/read all go through the
1168
+ hosted gateway. discussion-topic.status/.get are authoritative; the CLI maps
1169
+ server data into lifecycleState/nextTurn/openQuestions for UX only.
1170
+
1171
+ Server-first-unsupported (explicit failures, not silent fallbacks):
1172
+ - --max-turns: hosted rules have no server-enforced max-turn cap; this flag
1173
+ fails. (PM-issue candidate.)
1174
+ - --objective: hosted topic has no objective column; it is folded into the
1175
+ topic question text.
1176
+ - turn --to <named-agent>: hosted addressedTo is ['agent','operator']; only
1177
+ --to operator maps. Use --reply-to <seq> to correlate an answer.
1178
+ - turn --reply-to: hosted reply correlation is by integer turn seq.
1179
+ `);
1180
+ const start = cmd.command('start')
1181
+ .description('Create a hosted discussion topic')
1182
+ .requiredOption('--title <title>', 'Topic title')
1183
+ .option('--slug <slug>', 'Operator-facing stable topic slug')
1184
+ .option('--objective <text>', 'Topic objective (folded into the hosted question; no hosted objective column)')
1185
+ .option('--question <text>', 'Opening question (required unless --objective supplies it)')
1186
+ .option('--agent <id>', 'Participant agent id; repeatable, minimum two', collectRepeatedOption, [])
1187
+ .option('--turn-order <mode>', 'Turn order: alternating|free (default: alternating)')
1188
+ .option('--max-turns <n>', 'UNSUPPORTED server-first (fails): no hosted max-turn cap', parseInteger)
1189
+ .option('--min-turns-before-conclude <n>', 'Guard conclude until this many turns exist', parseInteger)
1190
+ .option('--require-question-answer', 'Block non-answer turns while an open question remains (server-enforced)')
1191
+ .option('--subject-type <type>', 'Optional linked subject type')
1192
+ .option('--subject-id <id>', 'Optional linked subject id')
1193
+ .option('--tag <tag>', 'General tag; repeatable', collectRepeatedOption, [])
1194
+ .option('--preview', 'Validate without mutating hosted state')
1195
+ .option('--apply', 'Create the topic')
1196
+ .option('--idempotency-key <key>', 'Optional guarded-write idempotency key')
1197
+ .action((commandOptions) => runDiscussStart({ ...cmd.opts(), ...commandOptions }));
1198
+ addDiscussionContextOptions(start);
1199
+ start.addHelpText('after', `
1200
+ Examples:
1201
+ aops-cli discuss start --title "Sync conflict policy" --agent codex --agent gemini --question "pull dirty local state'i overwrite etmeli mi?" --apply --json
1202
+ aops-cli discuss start --title "Plan review" --agent codex --agent claude --turn-order free --apply --json
1203
+
1204
+ Notes:
1205
+ - minimum iki unique participant gerekir
1206
+ - default turn order alternating'dir (turnOrder = ordered participants)
1207
+ - --require-question-answer ve turn-order server tarafindan enforce edilir
1208
+ `);
1209
+ const followUp = cmd.command('follow-up')
1210
+ .description('Create a hosted child topic referencing an existing discussion (parent not mutated)')
1211
+ .option('--from <topic>', 'Parent hosted topic id or slug')
1212
+ .requiredOption('--title <title>', 'Child topic title')
1213
+ .option('--slug <slug>', 'Child topic slug')
1214
+ .option('--objective <text>', 'Child objective (folded into question)')
1215
+ .option('--question <text>', 'Child opening question')
1216
+ .option('--agent <id>', 'Participant agent id; repeatable, defaults to parent participants', collectRepeatedOption, [])
1217
+ .option('--turn-order <mode>', 'Turn order: alternating|free (defaults from parent)')
1218
+ .option('--min-turns-before-conclude <n>', 'Guard conclude until this many turns exist', parseInteger)
1219
+ .option('--require-question-answer', 'Block non-answer turns while an open question remains')
1220
+ .option('--max-turns <n>', 'UNSUPPORTED server-first (fails)', parseInteger)
1221
+ .option('--inherit-tags', 'Copy parent tags before adding child tags')
1222
+ .option('--inherit-subject', 'Copy parent subjectType and subjectId')
1223
+ .option('--reference-output <ref>', 'Parent output ref to record in lineage; repeatable', collectRepeatedOption, [])
1224
+ .option('--reference-turn <ref>', 'Parent turn ref to record in lineage; repeatable', collectRepeatedOption, [])
1225
+ .option('--reference-memory <ref>', 'Memory ref to record in lineage; repeatable', collectRepeatedOption, [])
1226
+ .option('--tag <tag>', 'General tag; repeatable', collectRepeatedOption, [])
1227
+ .option('--preview', 'Validate without mutating hosted state')
1228
+ .option('--apply', 'Create the child topic')
1229
+ .option('--idempotency-key <key>', 'Optional guarded-write idempotency key')
1230
+ .action((commandOptions) => runDiscussFollowUp({ ...cmd.opts(), ...commandOptions }));
1231
+ addDiscussionContextOptions(followUp);
1232
+ followUp.addHelpText('after', `
1233
+ Examples:
1234
+ aops-cli discuss follow-up --from <parent-id> --title "Next question" --question "Yeni karar ne olmali?" --inherit-tags --inherit-subject --reference-output consensus --apply --json
1235
+
1236
+ Notes:
1237
+ - parent topic mutate edilmez
1238
+ - child topic lineageKind=follow-up ve parentTopicId tasir
1239
+ `);
1240
+ const fork = cmd.command('fork')
1241
+ .description('Create a hosted fork topic branching from an existing discussion (parent not mutated)')
1242
+ .option('--from <topic>', 'Parent hosted topic id or slug')
1243
+ .requiredOption('--title <title>', 'Fork topic title')
1244
+ .option('--slug <slug>', 'Fork topic slug')
1245
+ .option('--objective <text>', 'Fork objective (folded into question)')
1246
+ .option('--question <text>', 'Fork opening question')
1247
+ .option('--agent <id>', 'Participant agent id; repeatable, defaults to parent participants', collectRepeatedOption, [])
1248
+ .option('--turn-order <mode>', 'Turn order: alternating|free (defaults from parent)')
1249
+ .option('--min-turns-before-conclude <n>', 'Guard conclude until this many turns exist', parseInteger)
1250
+ .option('--require-question-answer', 'Block non-answer turns while an open question remains')
1251
+ .option('--max-turns <n>', 'UNSUPPORTED server-first (fails)', parseInteger)
1252
+ .option('--inherit-tags', 'Copy parent tags before adding fork tags')
1253
+ .option('--inherit-subject', 'Copy parent subjectType and subjectId')
1254
+ .option('--reference-output <ref>', 'Parent output ref to record in lineage; repeatable', collectRepeatedOption, [])
1255
+ .option('--reference-turn <ref>', 'Parent turn ref to record in lineage; repeatable', collectRepeatedOption, [])
1256
+ .option('--reference-memory <ref>', 'Memory ref to record in lineage; repeatable', collectRepeatedOption, [])
1257
+ .option('--tag <tag>', 'General tag; repeatable', collectRepeatedOption, [])
1258
+ .option('--preview', 'Validate without mutating hosted state')
1259
+ .option('--apply', 'Create the fork topic')
1260
+ .option('--idempotency-key <key>', 'Optional guarded-write idempotency key')
1261
+ .action((commandOptions) => runDiscussFork({ ...cmd.opts(), ...commandOptions }));
1262
+ addDiscussionContextOptions(fork);
1263
+ fork.addHelpText('after', `
1264
+ Examples:
1265
+ aops-cli discuss fork --from <parent-id> --title "Alternative approach" --question "Baska yol ne olurdu?" --inherit-tags --apply --json
1266
+
1267
+ Notes:
1268
+ - parent topic mutate edilmez
1269
+ - child topic lineageKind=fork ve parentTopicId tasir
1270
+ `);
1271
+ const lineage = cmd.command('lineage')
1272
+ .description('Show the child-topic lineage tree for one discussion root (built from hosted list calls)')
1273
+ .option('--root <topic>', 'Root hosted topic id or slug')
1274
+ .option('--include-abandoned', 'Include abandoned child topics in the lineage tree')
1275
+ .action((commandOptions) => runDiscussLineage({ ...cmd.opts(), ...commandOptions }));
1276
+ addDiscussionContextOptions(lineage);
1277
+ lineage.addHelpText('after', `
1278
+ Examples:
1279
+ aops-cli discuss lineage --root <topic> --json
1280
+ aops-cli discuss lineage --root <topic> --include-abandoned --json
1281
+ `);
1282
+ const list = cmd.command('list')
1283
+ .description('List hosted discussion topics')
1284
+ .option('--status <status>', 'Status filter: active|concluding|concluded|abandoned')
1285
+ .option('--scope <scope>', 'Scope filter: standalone (accepted for compatibility)')
1286
+ .option('--agent <id>', 'Require participant agent; repeatable', collectRepeatedOption, [])
1287
+ .option('--subject-type <type>', 'Filter by linked subject type')
1288
+ .option('--subject-id <id>', 'Filter by linked subject id')
1289
+ .option('--limit <n>', 'Result limit', parseInteger)
1290
+ .option('--include-abandoned', 'Include abandoned topics in list output')
1291
+ .action((commandOptions) => runDiscussList({ ...cmd.opts(), ...commandOptions }));
1292
+ addDiscussionContextOptions(list);
1293
+ list.addHelpText('after', `
1294
+ Examples:
1295
+ aops-cli discuss list --status active --json
1296
+ aops-cli discuss list --agent codex --subject-type projectman.sprint --json
1297
+ aops-cli discuss list --include-abandoned --json
1298
+
1299
+ Notes:
1300
+ - abandoned topic'ler default list sonucunda gizlenir
1301
+ `);
1302
+ const get = cmd.command('get')
1303
+ .description('Get a hosted discussion topic with its turns and outputs')
1304
+ .option('--id <id>', 'Hosted topic id or slug')
1305
+ .action((commandOptions) => runDiscussGet({ ...cmd.opts(), ...commandOptions }));
1306
+ addDiscussionContextOptions(get);
1307
+ get.addHelpText('after', `
1308
+ Examples:
1309
+ aops-cli discuss get --id <topic> --json
1310
+ `);
1311
+ const turn = cmd.command('turn')
1312
+ .description('Append one turn to a hosted discussion topic (turn-order enforced server-side)')
1313
+ .option('--topic <id>', 'Hosted topic id or slug')
1314
+ .requiredOption('--agent <id>', 'Participant agent id')
1315
+ .requiredOption('--kind <kind>', `Turn kind: ${TURN_KINDS.join('|')}`)
1316
+ .option('--text <text>', 'Inline turn content')
1317
+ .option('--from-file <path>', 'Read turn content from a file path or `-` for stdin')
1318
+ .option('--reply-to <seq>', 'Reply correlation by integer turn seq (hosted replyToSeq)')
1319
+ .option('--to <operator>', 'Address the operator (only "operator" is supported; creates an operator block)')
1320
+ .option('--expect-next <agent-id>', 'Same-agent assertion; turn-order/seq race is enforced server-side')
1321
+ .option('--preview', 'Validate without mutating hosted state')
1322
+ .option('--apply', 'Write the turn')
1323
+ .option('--idempotency-key <key>', 'Optional guarded-write idempotency key (server idempotent re-add)')
1324
+ .action((commandOptions) => runDiscussTurn({ ...cmd.opts(), ...commandOptions }));
1325
+ addDiscussionContextOptions(turn);
1326
+ turn.addHelpText('after', `
1327
+ Examples:
1328
+ aops-cli discuss turn --topic <topic> --agent codex --kind question --text "Bu default guvenli mi?" --to operator --apply --json
1329
+ aops-cli discuss turn --topic <topic> --agent gemini --kind answer --from-file ./answer.md --reply-to 3 --apply --json
1330
+ aops-cli discuss turn --topic <topic> --agent codex --expect-next codex --kind statement --text "Siradaki turn." --apply --json
1331
+
1332
+ Notes:
1333
+ - turns append-onlydir
1334
+ - alternating order + operator-block + reply correlation server tarafindan enforce edilir
1335
+ - --to operator acik operator sorusu yaratir; --to <named-agent> server-first-unsupported
1336
+ - --reply-to artik turn id degil, turn seq (integer) alir
1337
+ `);
1338
+ const output = cmd.command('output')
1339
+ .description('Set a hosted discussion output (conclude-flow output authoring)')
1340
+ .option('--topic <id>', 'Hosted topic id or slug')
1341
+ .requiredOption('--output-kind <kind>', 'Output kind: consensus|disagreement|open-questions|final-stance|decision|agent-summary')
1342
+ .requiredOption('--owner <agent-id>', 'Owner agent id (must be a topic participant; server-enforced)')
1343
+ .option('--text <text>', 'Inline output content (must not contain _TBD_)')
1344
+ .option('--from-file <path>', 'Read output content from a file path or `-` for stdin')
1345
+ .option('--preview', 'Validate without mutating hosted state')
1346
+ .option('--apply', 'Write the output')
1347
+ .option('--idempotency-key <key>', 'Optional guarded-write idempotency key')
1348
+ .action((commandOptions) => runDiscussOutput({ ...cmd.opts(), ...commandOptions }));
1349
+ addDiscussionContextOptions(output);
1350
+ output.addHelpText('after', `
1351
+ Examples:
1352
+ aops-cli discuss output --topic <topic> --output-kind consensus --owner claude --text "Agreed: ..." --apply --json
1353
+
1354
+ Notes:
1355
+ - content _TBD_ marker icermez (server reddeder)
1356
+ - owner topic participant olmalidir (server-enforced)
1357
+ `);
1358
+ const status = cmd.command('status')
1359
+ .description('Read hosted discussion state mapped to lifecycleState/nextTurn/openQuestions')
1360
+ .option('--id <id>', 'Hosted topic id or slug')
1361
+ .option('--prompt-for-next', 'Include a deterministic prompt template for the suggested next participant')
1362
+ .action((commandOptions) => runDiscussStatus({ ...cmd.opts(), ...commandOptions }));
1363
+ addDiscussionContextOptions(status);
1364
+ status.addHelpText('after', `
1365
+ Examples:
1366
+ aops-cli discuss status --id <topic> --json
1367
+ aops-cli discuss status --id <topic> --prompt-for-next --json
1368
+
1369
+ Notes:
1370
+ - lifecycleState/nextTurn/openQuestions hosted status+get'ten turetilir (server truth)
1371
+ `);
1372
+ const wait = cmd.command('wait')
1373
+ .description('Poll hosted status until an agent may write, an operator block appears, or the topic is ready/done')
1374
+ .option('--id <id>', 'Hosted topic id or slug')
1375
+ .requiredOption('--for <agent-id>', 'Agent id waiting for its next allowed turn')
1376
+ .option('--timeout-sec <sec>', 'Maximum seconds to wait', parseNonNegativeNumber, 300)
1377
+ .option('--interval-sec <sec>', 'Polling interval seconds', parsePositiveNumber, 5)
1378
+ .action((commandOptions) => runDiscussWait({ ...cmd.opts(), ...commandOptions }));
1379
+ addDiscussionContextOptions(wait);
1380
+ wait.addHelpText('after', `
1381
+ Examples:
1382
+ aops-cli discuss wait --id <topic> --for codex --timeout-sec 540 --interval-sec 5 --json
1383
+
1384
+ Exit codes:
1385
+ 0 requested agent is allowed to write the next turn
1386
+ 20 discussion is blocked by an operator-addressed open question
1387
+ 21 discussion is ready to conclude, concluding, concluded, or abandoned
1388
+ 22 timeout
1389
+ `);
1390
+ const loopPrompt = cmd.command('loop-prompt')
1391
+ .description('Print a persistent agent loop prompt for hosted discussion automation')
1392
+ .option('--id <id>', 'Hosted topic id or slug')
1393
+ .requiredOption('--for <agent-id>', 'Agent id that should keep waiting and writing turns')
1394
+ .option('--timeout-sec <sec>', 'Maximum seconds per wait cycle', parseNonNegativeNumber, 540)
1395
+ .option('--interval-sec <sec>', 'Polling interval seconds', parsePositiveNumber, 5)
1396
+ .action((commandOptions) => runDiscussLoopPrompt({ ...cmd.opts(), ...commandOptions }));
1397
+ addDiscussionContextOptions(loopPrompt);
1398
+ loopPrompt.addHelpText('after', `
1399
+ Examples:
1400
+ aops-cli discuss loop-prompt --id <topic> --for claude
1401
+ aops-cli discuss loop-prompt --id <topic> --for claude --timeout-sec 540 --interval-sec 5 --json
1402
+ `);
1403
+ const digest = cmd.command('digest')
1404
+ .description('Build a non-interpretive hosted discussion context pack')
1405
+ .option('--id <id>', 'Hosted topic id or slug')
1406
+ .option('--prompt-for-next', 'Include a deterministic prompt template for the suggested next participant')
1407
+ .action((commandOptions) => runDiscussDigest({ ...cmd.opts(), ...commandOptions }));
1408
+ addDiscussionContextOptions(digest);
1409
+ digest.addHelpText('after', `
1410
+ Examples:
1411
+ aops-cli discuss digest --id <topic> --json
1412
+ aops-cli discuss digest --id <topic> --prompt-for-next --json
1413
+ `);
1414
+ const conclude = cmd.command('conclude')
1415
+ .description('Conclude a hosted discussion topic (server enforces min-turns + final-stance readiness)')
1416
+ .option('--topic <id>', 'Hosted topic id or slug')
1417
+ .option('--updated-by <agent-id>', 'Optional updatedBy actor')
1418
+ .option('--preview', 'Validate without mutating hosted state')
1419
+ .option('--apply', 'Conclude the topic')
1420
+ .option('--idempotency-key <key>', 'Optional guarded-write idempotency key')
1421
+ .action((commandOptions) => runDiscussConclude({ ...cmd.opts(), ...commandOptions }));
1422
+ addDiscussionContextOptions(conclude);
1423
+ conclude.addHelpText('after', `
1424
+ Examples:
1425
+ aops-cli discuss conclude --topic <topic> --apply --json
1426
+
1427
+ Notes:
1428
+ - server min-turns ve her participant icin final-stance turn'u hazir degilse conclude bloklanir
1429
+ - output authoring icin: aops-cli discuss output ...
1430
+ `);
1431
+ const abandon = cmd.command('abandon')
1432
+ .description('Mark a hosted discussion topic as abandoned')
1433
+ .option('--topic <id>', 'Hosted topic id or slug')
1434
+ .requiredOption('--reason <text>', 'Reason recorded in topic metadata')
1435
+ .option('--preview', 'Validate without mutating hosted state')
1436
+ .option('--apply', 'Abandon the topic')
1437
+ .option('--idempotency-key <key>', 'Optional guarded-write idempotency key')
1438
+ .action((commandOptions) => runDiscussAbandon({ ...cmd.opts(), ...commandOptions }));
1439
+ addDiscussionContextOptions(abandon);
1440
+ abandon.addHelpText('after', `
1441
+ Examples:
1442
+ aops-cli discuss abandon --topic <topic> --reason "Smoke test topic; wrong participant." --apply --json
1443
+
1444
+ Notes:
1445
+ - sadece active topic abandon edilebilir (server-enforced)
1446
+ `);
1447
+ return cmd;
1448
+ }