@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.
- package/LICENSE +19 -0
- package/NOTICE +7 -0
- package/README.md +203 -0
- package/assets/agent-assets/core/SKILL.md +49 -0
- package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
- package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
- package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
- package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
- package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
- package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
- package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
- package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
- package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
- package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
- package/assets/agent-assets/core/references/view/SKILL.md +58 -0
- package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
- package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
- package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
- package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
- package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
- package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
- package/assets/agent-assets/core/user-guides/docman.md +162 -0
- package/assets/agent-assets/core/user-guides/projectman.md +212 -0
- package/assets/agent-assets/core/user-guides/sys.md +94 -0
- package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
- package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
- package/assets/r6-remote-session-baseline.json +19 -0
- package/assets/skills/aops-install/SKILL.md +231 -0
- package/assets/skills/aops-install/agents/openai.yaml +4 -0
- package/dist/commands/activity.js +352 -0
- package/dist/commands/agent-profile.js +653 -0
- package/dist/commands/agent.js +710 -0
- package/dist/commands/agents-md.js +125 -0
- package/dist/commands/api.js +84 -0
- package/dist/commands/archive.js +873 -0
- package/dist/commands/artifact.js +535 -0
- package/dist/commands/assets.js +1414 -0
- package/dist/commands/auth/device-login.js +1 -0
- package/dist/commands/auth/legacy-export.js +177 -0
- package/dist/commands/auth/login.js +426 -0
- package/dist/commands/chat.js +1363 -0
- package/dist/commands/chatv3.js +1684 -0
- package/dist/commands/checkpoint.js +549 -0
- package/dist/commands/commercial-artifact.js +475 -0
- package/dist/commands/commercial-license.js +594 -0
- package/dist/commands/community-auth.js +62 -0
- package/dist/commands/community-cockpit.js +185 -0
- package/dist/commands/community-console.js +109 -0
- package/dist/commands/community-doctor.js +335 -0
- package/dist/commands/community-server.js +2062 -0
- package/dist/commands/community-setup.js +284 -0
- package/dist/commands/discuss.js +1448 -0
- package/dist/commands/doc.js +5219 -0
- package/dist/commands/experience.js +922 -0
- package/dist/commands/host.js +1127 -0
- package/dist/commands/init.js +118 -0
- package/dist/commands/memory.js +3129 -0
- package/dist/commands/mission.js +562 -0
- package/dist/commands/official-catalog.js +134 -0
- package/dist/commands/plan.js +84 -0
- package/dist/commands/playbook.js +415 -0
- package/dist/commands/pm/context.js +6 -0
- package/dist/commands/pm/index.js +701 -0
- package/dist/commands/pm/projectman.js +6266 -0
- package/dist/commands/project.js +1080 -0
- package/dist/commands/prompt.js +1024 -0
- package/dist/commands/repo-sync.js +1907 -0
- package/dist/commands/resource.js +527 -0
- package/dist/commands/skill.js +1211 -0
- package/dist/commands/start-disciplines.js +510 -0
- package/dist/commands/start.js +1343 -0
- package/dist/commands/target.js +309 -0
- package/dist/commands/version.js +42 -0
- package/dist/commands/view.js +1852 -0
- package/dist/launchers/cockpit.js +28 -0
- package/dist/launchers/options.js +74 -0
- package/dist/launchers/server.js +41 -0
- package/dist/lib/agent-assets/envelope.js +83 -0
- package/dist/lib/agent-assets/gateway.js +15 -0
- package/dist/lib/agent-assets/guards.js +23 -0
- package/dist/lib/agent-assets/hosted-discovery.js +148 -0
- package/dist/lib/agent-assets/hosted-package-input.js +154 -0
- package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
- package/dist/lib/agent-assets/native-fs.js +589 -0
- package/dist/lib/agent-assets/package-manifest.js +324 -0
- package/dist/lib/agent-assets/portable-path.js +67 -0
- package/dist/lib/agent-assets/release-input.js +177 -0
- package/dist/lib/agent-assets/roots.js +45 -0
- package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
- package/dist/lib/agent-assets/runtime-targets.js +50 -0
- package/dist/lib/agent-assets/store-reader.js +1212 -0
- package/dist/lib/agent-assets/store-types.js +1 -0
- package/dist/lib/agent-assets/store-writer.js +1484 -0
- package/dist/lib/agent-assets/types.js +1 -0
- package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
- package/dist/lib/commercial-final-readiness.js +504 -0
- package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
- package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
- package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
- package/dist/lib/community-client-contract.js +164 -0
- package/dist/lib/community-cockpit-lifecycle.js +601 -0
- package/dist/lib/community-command-abort.js +59 -0
- package/dist/lib/community-diagnostic.js +237 -0
- package/dist/lib/community-docker-adapter.js +498 -0
- package/dist/lib/community-home.js +31 -0
- package/dist/lib/community-instance-contract.js +148 -0
- package/dist/lib/community-instance-layout.js +97 -0
- package/dist/lib/community-lifecycle.js +1631 -0
- package/dist/lib/community-migration-snapshot.js +770 -0
- package/dist/lib/community-native-application-recovery.js +425 -0
- package/dist/lib/community-native-child.js +240 -0
- package/dist/lib/community-native-database-recovery.js +456 -0
- package/dist/lib/community-native-lifecycle.js +2458 -0
- package/dist/lib/community-native-migration.js +339 -0
- package/dist/lib/community-native-postgres.js +596 -0
- package/dist/lib/community-operation-journal.js +773 -0
- package/dist/lib/community-operation-lock.js +596 -0
- package/dist/lib/community-release-resolver.js +291 -0
- package/dist/lib/community-release-verifier.js +475 -0
- package/dist/lib/community-repo-discovery.js +252 -0
- package/dist/lib/community-setup-server-env.js +203 -0
- package/dist/lib/external-url.js +110 -0
- package/dist/lib/official-catalog-gateway.js +108 -0
- package/dist/lib/official-catalog.js +364 -0
- package/dist/lib/server-bootstrap.js +240 -0
- package/dist/lib/setup-agent-assets-bridge.js +176 -0
- package/dist/lib/setup-agent-assets-release.js +52 -0
- package/dist/lib/setup-external-postgres.js +108 -0
- package/dist/lib/setup-init-orchestrator.js +990 -0
- package/dist/lib/setup-install-guide.js +52 -0
- package/dist/lib/setup-installer-release.js +47 -0
- package/dist/lib/setup-local-postgres.js +329 -0
- package/dist/lib/setup-official-catalog-bridge.js +148 -0
- package/dist/lib/setup-readiness.js +888 -0
- package/dist/lib/tui-launcher.js +59 -0
- package/dist/main.js +150 -0
- package/dist/seeds/agents-md.js +54 -0
- package/dist/utils/agent-gateway.js +275 -0
- package/dist/utils/agents-md.js +281 -0
- package/dist/utils/api.js +255 -0
- package/dist/utils/chatv3-session-store.js +185 -0
- package/dist/utils/command.js +67 -0
- package/dist/utils/community-host-runtime.js +376 -0
- package/dist/utils/config.js +1015 -0
- package/dist/utils/discussion-workspace.js +552 -0
- package/dist/utils/experience-workspace.js +326 -0
- package/dist/utils/guide-paths.js +67 -0
- package/dist/utils/hosted-sugar.js +56 -0
- package/dist/utils/hosted-workspace.js +229 -0
- package/dist/utils/memory-workspace.js +537 -0
- package/dist/utils/operator-runtime.js +199 -0
- package/dist/utils/password-crypto.js +39 -0
- package/dist/utils/playbook-workspace.js +479 -0
- package/dist/utils/project-context.js +255 -0
- package/dist/utils/prompts.js +70 -0
- package/dist/utils/refresh-race.js +28 -0
- package/dist/utils/repo-config.js +109 -0
- package/dist/utils/repo-first-projectman.js +194 -0
- package/dist/utils/repo-first-storage.js +13 -0
- package/dist/utils/repo-first-view.js +305 -0
- package/dist/utils/session-guidance.js +278 -0
- package/dist/utils/session-state.js +137 -0
- package/dist/utils/table.js +39 -0
- package/dist/utils/transient-fs.js +49 -0
- package/dist/utils/workspace-root.js +1 -0
- package/launchers/aops-cockpit.ps1 +15 -0
- package/launchers/aops-cockpit.sh +16 -0
- package/launchers/aops-server.ps1 +15 -0
- package/launchers/aops-server.sh +16 -0
- package/native/tui/win32-x64/aops-tui.exe +0 -0
- 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
|
+
}
|