orizu 0.5.7 → 0.5.9
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/README.md +13 -2
- package/dist/credentials.js +44 -0
- package/dist/help.js +77 -7
- package/dist/hosted-bootstrap.js +45 -0
- package/dist/hosted-harness-opencode.js +21 -1
- package/dist/hosted-harness.js +9 -0
- package/dist/hosted-harvest.js +121 -0
- package/dist/hosted-headless.js +163 -0
- package/dist/hosted-loop.js +119 -5
- package/dist/hosted-run-event-sink.js +21 -0
- package/dist/hosted-runtime-assets.js +27 -0
- package/dist/hosted-session-cli.js +51 -9
- package/dist/http.js +16 -1
- package/dist/index.js +54 -86
- package/dist/manifests-cli.js +26 -4
- package/dist/report-comments-cli.js +325 -0
- package/dist/skill-installer.js +16 -1
- package/package.json +1 -1
- package/src/credentials.ts +46 -0
- package/src/help.ts +77 -7
- package/src/hosted-bootstrap.ts +52 -0
- package/src/hosted-harness-opencode.ts +24 -2
- package/src/hosted-harness.ts +9 -0
- package/src/hosted-harvest.ts +164 -0
- package/src/hosted-headless.ts +178 -0
- package/src/hosted-loop.ts +133 -4
- package/src/hosted-run-event-sink.ts +28 -0
- package/src/hosted-runtime-assets.ts +31 -0
- package/src/hosted-session-cli.ts +50 -9
- package/src/http.ts +23 -1
- package/src/index.ts +74 -156
- package/src/manifests-cli.ts +28 -4
- package/src/report-comments-cli.ts +524 -0
- package/src/skill-installer.ts +18 -1
- package/vendor/skills/orizu-cli/SKILL.md +9 -2
- package/vendor/skills/orizu-cli/references/prompt-control-plane.md +1 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
const COMMENTS_TARGET_USAGE = 'Use exactly one of --prompt <id-or-name>, --run <run-id>, or --task <task-id>';
|
|
2
|
+
function reportCommentsUsage(action) {
|
|
3
|
+
const target = '(--prompt <id-or-name> --project <team/project> [--label <label> | --version <id>] | --run <run-id> | --task <task-id>)';
|
|
4
|
+
if (action === 'list') {
|
|
5
|
+
return `Usage: orizu comments list ${target} [--json]`;
|
|
6
|
+
}
|
|
7
|
+
if (action === 'add') {
|
|
8
|
+
return `Usage: orizu comments add ${target} --body <text|@file> [--anchor <text>] [--lines <start:end>] [--via <name>] [--json]`;
|
|
9
|
+
}
|
|
10
|
+
if (action === 'reply') {
|
|
11
|
+
return 'Usage: orizu comments reply <comment-id> --body <text|@file> [--via <name>] [--json]';
|
|
12
|
+
}
|
|
13
|
+
if (action === 'edit') {
|
|
14
|
+
return 'Usage: orizu comments edit <comment-id> --body <text|@file> [--json]';
|
|
15
|
+
}
|
|
16
|
+
return `Usage: orizu comments ${action} <comment-id> [--json]`;
|
|
17
|
+
}
|
|
18
|
+
function compactTerminalText(ctx, value, maxLength = 160) {
|
|
19
|
+
const compact = ctx.sanitizeTerminalText(value).replace(/\s+/g, ' ').trim();
|
|
20
|
+
if (compact.length <= maxLength) {
|
|
21
|
+
return compact;
|
|
22
|
+
}
|
|
23
|
+
return `${compact.slice(0, Math.max(0, maxLength - 1)).trim()}…`;
|
|
24
|
+
}
|
|
25
|
+
async function readReportCommentError(ctx, response) {
|
|
26
|
+
const status = response.status
|
|
27
|
+
? `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`
|
|
28
|
+
: 'request failed';
|
|
29
|
+
const contentType = response.headers.get('content-type') || '';
|
|
30
|
+
if (!contentType.toLowerCase().includes('application/json')) {
|
|
31
|
+
return status;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const parsed = await response.clone().json();
|
|
35
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
36
|
+
const error = parsed.error;
|
|
37
|
+
if (typeof error === 'string' && error.trim()) {
|
|
38
|
+
return compactTerminalText(ctx, error, 220);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Fall through to the status-only message for malformed JSON errors.
|
|
44
|
+
}
|
|
45
|
+
return status;
|
|
46
|
+
}
|
|
47
|
+
function printIndentedBody(ctx, value, indent) {
|
|
48
|
+
const lines = ctx.sanitizeTerminalText(value || '').split(/\r?\n/);
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
ctx.printLine(`${indent}${line}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function formatReportCommentAnchor(ctx, anchor) {
|
|
54
|
+
if (!anchor) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const lineLabel = anchor.startLine && anchor.endLine
|
|
58
|
+
? anchor.startLine === anchor.endLine
|
|
59
|
+
? `line ${anchor.startLine}`
|
|
60
|
+
: `lines ${anchor.startLine}-${anchor.endLine}`
|
|
61
|
+
: null;
|
|
62
|
+
const selectedText = anchor.text ? `"${compactTerminalText(ctx, anchor.text, 180)}"` : null;
|
|
63
|
+
if (lineLabel && selectedText) {
|
|
64
|
+
return `${lineLabel}: ${selectedText}`;
|
|
65
|
+
}
|
|
66
|
+
return lineLabel || selectedText;
|
|
67
|
+
}
|
|
68
|
+
function reportCommentAuthorName(author) {
|
|
69
|
+
const personName = author?.person?.name;
|
|
70
|
+
const name = personName || author?.name || 'Unknown';
|
|
71
|
+
const via = author?.via ? ` via ${author.via}` : '';
|
|
72
|
+
const agent = author?.actor === 'agent'
|
|
73
|
+
? author.onBehalfOf
|
|
74
|
+
? ` (agent on behalf of ${author.onBehalfOf})`
|
|
75
|
+
: ' (agent)'
|
|
76
|
+
: '';
|
|
77
|
+
return `${name}${via}${agent}`;
|
|
78
|
+
}
|
|
79
|
+
function printReportCommentSubject(ctx, data) {
|
|
80
|
+
if (data.prompt && data.version) {
|
|
81
|
+
const versionLabel = data.version.versionLabel || (data.version.versionNumber ? `v${data.version.versionNumber}` : data.version.id);
|
|
82
|
+
ctx.printLine(`Prompt: ${ctx.sanitizeTerminalText(data.prompt.name)} (${ctx.sanitizeTerminalText(data.prompt.id)})`);
|
|
83
|
+
ctx.printLine(`Version: ${ctx.sanitizeTerminalText(String(versionLabel))} (${ctx.sanitizeTerminalText(data.version.id)})`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (data.run) {
|
|
87
|
+
ctx.printLine(`Optimization run: ${ctx.sanitizeTerminalText(data.run.id)}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (data.task) {
|
|
91
|
+
const title = data.task.title ? `${ctx.sanitizeTerminalText(data.task.title)} ` : '';
|
|
92
|
+
ctx.printLine(`Task: ${title}(${ctx.sanitizeTerminalText(data.task.id)})`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (data.subject) {
|
|
96
|
+
ctx.printLine(`Subject: ${ctx.sanitizeTerminalText(data.subject.type)} ${ctx.sanitizeTerminalText(data.subject.id)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function printReportComments(ctx, data) {
|
|
100
|
+
printReportCommentSubject(ctx, data);
|
|
101
|
+
ctx.printLine(`Comments: ${data.summary.threadCount} thread${data.summary.threadCount === 1 ? '' : 's'} ` +
|
|
102
|
+
`(${data.summary.openThreadCount} open, ${data.summary.resolvedThreadCount} resolved, ${data.summary.replyCount} replies)`);
|
|
103
|
+
if (data.comments.length === 0) {
|
|
104
|
+
ctx.printLine('\nNo comments found for this report.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const [index, thread] of data.comments.entries()) {
|
|
108
|
+
const authorName = reportCommentAuthorName(thread.author);
|
|
109
|
+
const anchor = formatReportCommentAnchor(ctx, thread.anchor);
|
|
110
|
+
ctx.printLine('');
|
|
111
|
+
ctx.printLine(`${index + 1}. [${thread.status}] ${ctx.sanitizeTerminalText(thread.id)} · ` +
|
|
112
|
+
`${ctx.sanitizeTerminalText(authorName)} · ${ctx.sanitizeTerminalText(thread.createdAt)}`);
|
|
113
|
+
if (thread.editedAt) {
|
|
114
|
+
ctx.printLine(` Edited: ${ctx.sanitizeTerminalText(thread.editedAt)}`);
|
|
115
|
+
}
|
|
116
|
+
if (anchor) {
|
|
117
|
+
ctx.printLine(` Selection: ${anchor}`);
|
|
118
|
+
}
|
|
119
|
+
printIndentedBody(ctx, thread.body, ' ');
|
|
120
|
+
if (thread.replies.length > 0) {
|
|
121
|
+
ctx.printLine(` Replies (${thread.replies.length})`);
|
|
122
|
+
for (const reply of thread.replies) {
|
|
123
|
+
const replyAuthor = reportCommentAuthorName(reply.author);
|
|
124
|
+
ctx.printLine(` - ${ctx.sanitizeTerminalText(reply.id)} · ` +
|
|
125
|
+
`${ctx.sanitizeTerminalText(replyAuthor)} · ${ctx.sanitizeTerminalText(reply.createdAt)}`);
|
|
126
|
+
if (reply.editedAt) {
|
|
127
|
+
ctx.printLine(` Edited: ${ctx.sanitizeTerminalText(reply.editedAt)}`);
|
|
128
|
+
}
|
|
129
|
+
printIndentedBody(ctx, reply.body, ' ');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function reportCommentsTargetParams(ctx, action) {
|
|
135
|
+
const promptRef = ctx.getArg('--prompt');
|
|
136
|
+
const runId = ctx.getArg('--run') || ctx.getArg('--run-id');
|
|
137
|
+
const taskId = ctx.getArg('--task');
|
|
138
|
+
ctx.rejectDashPrefixedOptionValue('--prompt', promptRef);
|
|
139
|
+
ctx.rejectDashPrefixedOptionValue('--run', runId);
|
|
140
|
+
ctx.rejectDashPrefixedOptionValue('--task', taskId);
|
|
141
|
+
const present = [
|
|
142
|
+
promptRef ? 'prompt' : null,
|
|
143
|
+
runId ? 'run' : null,
|
|
144
|
+
taskId ? 'task' : null,
|
|
145
|
+
].filter(Boolean);
|
|
146
|
+
if (present.length !== 1) {
|
|
147
|
+
throw new Error(`${reportCommentsUsage(action)}\n${COMMENTS_TARGET_USAGE}`);
|
|
148
|
+
}
|
|
149
|
+
const params = new URLSearchParams();
|
|
150
|
+
if (promptRef) {
|
|
151
|
+
const project = ctx.getArg('--project') || await ctx.resolveProjectSlug(null);
|
|
152
|
+
const label = ctx.getArg('--label');
|
|
153
|
+
const version = ctx.getArg('--version');
|
|
154
|
+
ctx.rejectDashPrefixedOptionValue('--project', project);
|
|
155
|
+
ctx.rejectDashPrefixedOptionValue('--label', label);
|
|
156
|
+
ctx.rejectDashPrefixedOptionValue('--version', version);
|
|
157
|
+
if (label && version) {
|
|
158
|
+
throw new Error('Use either --label or --version, not both');
|
|
159
|
+
}
|
|
160
|
+
params.set('prompt', promptRef);
|
|
161
|
+
params.set('project', project);
|
|
162
|
+
if (label)
|
|
163
|
+
params.set('label', label);
|
|
164
|
+
if (version)
|
|
165
|
+
params.set('version', version);
|
|
166
|
+
return params;
|
|
167
|
+
}
|
|
168
|
+
if (runId) {
|
|
169
|
+
params.set('run', runId);
|
|
170
|
+
return params;
|
|
171
|
+
}
|
|
172
|
+
params.set('task', taskId);
|
|
173
|
+
return params;
|
|
174
|
+
}
|
|
175
|
+
function readReportCommentBodyInput(ctx, action) {
|
|
176
|
+
const body = ctx.getArg('--body');
|
|
177
|
+
ctx.rejectDashPrefixedOptionValue('--body', body);
|
|
178
|
+
if (!body) {
|
|
179
|
+
throw new Error(reportCommentsUsage(action));
|
|
180
|
+
}
|
|
181
|
+
if (body === '@') {
|
|
182
|
+
throw new Error('Usage: --body <text|@file>');
|
|
183
|
+
}
|
|
184
|
+
const value = body.startsWith('@') ? ctx.readSourceFile(body.slice(1)) : body;
|
|
185
|
+
if (!value.trim()) {
|
|
186
|
+
throw new Error('Comment body must not be blank');
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function readReportCommentAnchorInput(ctx) {
|
|
191
|
+
const anchor = ctx.getArg('--anchor');
|
|
192
|
+
const lines = ctx.getArg('--lines');
|
|
193
|
+
ctx.rejectDashPrefixedOptionValue('--anchor', anchor);
|
|
194
|
+
ctx.rejectDashPrefixedOptionValue('--lines', lines);
|
|
195
|
+
let startLine = null;
|
|
196
|
+
let endLine = null;
|
|
197
|
+
if (lines) {
|
|
198
|
+
const match = lines.match(/^(\d+):(\d+)$/);
|
|
199
|
+
if (!match) {
|
|
200
|
+
throw new Error('--lines must use format <start:end>');
|
|
201
|
+
}
|
|
202
|
+
startLine = Number(match[1]);
|
|
203
|
+
endLine = Number(match[2]);
|
|
204
|
+
if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || startLine < 1 || endLine < startLine) {
|
|
205
|
+
throw new Error('--lines must contain positive integers where end is greater than or equal to start');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!anchor && startLine === null) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
text: anchor ? anchor.trim() : null,
|
|
213
|
+
startLine,
|
|
214
|
+
endLine,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function readReportCommentViaInput(ctx) {
|
|
218
|
+
const via = ctx.getArg('--via');
|
|
219
|
+
ctx.rejectDashPrefixedOptionValue('--via', via);
|
|
220
|
+
return via?.trim() || undefined;
|
|
221
|
+
}
|
|
222
|
+
async function listReportComments(ctx) {
|
|
223
|
+
const params = await reportCommentsTargetParams(ctx, 'list');
|
|
224
|
+
const response = await ctx.authedFetch(`/api/cli/report-comments?${params.toString()}`);
|
|
225
|
+
if (!response.ok) {
|
|
226
|
+
throw new Error(`Failed to fetch report comments: ${await readReportCommentError(ctx, response)}`);
|
|
227
|
+
}
|
|
228
|
+
const data = await ctx.parseJsonResponse(response, 'Report comments');
|
|
229
|
+
if (ctx.hasJsonFlag()) {
|
|
230
|
+
ctx.printJson(data);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
printReportComments(ctx, data);
|
|
234
|
+
}
|
|
235
|
+
async function addReportComment(ctx) {
|
|
236
|
+
const params = await reportCommentsTargetParams(ctx, 'add');
|
|
237
|
+
const body = readReportCommentBodyInput(ctx, 'add');
|
|
238
|
+
const response = await ctx.authedFetch(`/api/cli/report-comments?${params.toString()}`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
body,
|
|
243
|
+
via: readReportCommentViaInput(ctx),
|
|
244
|
+
anchor: readReportCommentAnchorInput(ctx),
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new Error(`Failed to add report comment: ${await readReportCommentError(ctx, response)}`);
|
|
249
|
+
}
|
|
250
|
+
const data = await ctx.parseJsonResponse(response, 'Report comment add');
|
|
251
|
+
if (ctx.hasJsonFlag()) {
|
|
252
|
+
ctx.printJson(data);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
ctx.printLine(`Added comment ${ctx.sanitizeTerminalText(data.comment.id)}`);
|
|
256
|
+
}
|
|
257
|
+
async function replyReportComment(ctx) {
|
|
258
|
+
const commentId = ctx.getPositionalArg(2);
|
|
259
|
+
if (!commentId) {
|
|
260
|
+
throw new Error(reportCommentsUsage('reply'));
|
|
261
|
+
}
|
|
262
|
+
const response = await ctx.authedFetch(`/api/cli/report-comments/${encodeURIComponent(commentId)}`, {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
headers: { 'Content-Type': 'application/json' },
|
|
265
|
+
body: JSON.stringify({
|
|
266
|
+
body: readReportCommentBodyInput(ctx, 'reply'),
|
|
267
|
+
via: readReportCommentViaInput(ctx),
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
throw new Error(`Failed to reply to report comment: ${await readReportCommentError(ctx, response)}`);
|
|
272
|
+
}
|
|
273
|
+
const data = await ctx.parseJsonResponse(response, 'Report comment reply');
|
|
274
|
+
if (ctx.hasJsonFlag()) {
|
|
275
|
+
ctx.printJson(data);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
ctx.printLine(`Added reply ${ctx.sanitizeTerminalText(data.comment.id)} to ${ctx.sanitizeTerminalText(commentId)}`);
|
|
279
|
+
}
|
|
280
|
+
async function updateReportComment(ctx, action) {
|
|
281
|
+
const commentId = ctx.getPositionalArg(2);
|
|
282
|
+
if (!commentId) {
|
|
283
|
+
throw new Error(reportCommentsUsage(action));
|
|
284
|
+
}
|
|
285
|
+
const payload = action === 'edit'
|
|
286
|
+
? { body: readReportCommentBodyInput(ctx, 'edit') }
|
|
287
|
+
: { resolved: action === 'resolve' };
|
|
288
|
+
const response = await ctx.authedFetch(`/api/cli/report-comments/${encodeURIComponent(commentId)}`, {
|
|
289
|
+
method: 'PATCH',
|
|
290
|
+
headers: { 'Content-Type': 'application/json' },
|
|
291
|
+
body: JSON.stringify(payload),
|
|
292
|
+
});
|
|
293
|
+
if (!response.ok) {
|
|
294
|
+
throw new Error(`Failed to ${action} report comment: ${await readReportCommentError(ctx, response)}`);
|
|
295
|
+
}
|
|
296
|
+
const data = await ctx.parseJsonResponse(response, `Report comment ${action}`);
|
|
297
|
+
if (ctx.hasJsonFlag()) {
|
|
298
|
+
ctx.printJson(data);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const verb = action === 'unresolve' ? 'Reopened' : action === 'edit' ? 'Edited' : 'Resolved';
|
|
302
|
+
ctx.printLine(`${verb} comment ${ctx.sanitizeTerminalText(data.comment.id)}`);
|
|
303
|
+
}
|
|
304
|
+
export async function reportCommentsCommand(action, ctx) {
|
|
305
|
+
if (action === 'list') {
|
|
306
|
+
await listReportComments(ctx);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (action === 'add') {
|
|
310
|
+
await addReportComment(ctx);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (action === 'reply') {
|
|
314
|
+
await replyReportComment(ctx);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (action === 'resolve' || action === 'unresolve' || action === 'edit') {
|
|
318
|
+
await updateReportComment(ctx, action);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
throw new Error('Usage: orizu comments <list|add|reply|resolve|unresolve|edit> ...');
|
|
322
|
+
}
|
|
323
|
+
export function throwDeprecatedPromptCommentsCommand() {
|
|
324
|
+
throw new Error('orizu prompts comments has moved. Use `orizu comments list --prompt <prompt-id-or-name> --project <team/project> [--label <label> | --version <id>]`.');
|
|
325
|
+
}
|
package/dist/skill-installer.js
CHANGED
|
@@ -14,14 +14,17 @@ export const SKILL_INSTALL_TARGETS = [
|
|
|
14
14
|
'codex-project',
|
|
15
15
|
'claude-user',
|
|
16
16
|
'claude-project',
|
|
17
|
+
'opencode-user',
|
|
18
|
+
'opencode-project',
|
|
17
19
|
'agents-md',
|
|
18
20
|
];
|
|
19
21
|
export const SKILL_META_FILENAME = '.orizu-skill-meta.json';
|
|
20
|
-
export const SKILL_INSTALL_AGENTS = ['claude', 'codex'];
|
|
22
|
+
export const SKILL_INSTALL_AGENTS = ['claude', 'codex', 'opencode'];
|
|
21
23
|
const PROJECT_LEVEL_TARGETS = [
|
|
22
24
|
'agents-project',
|
|
23
25
|
'codex-project',
|
|
24
26
|
'claude-project',
|
|
27
|
+
'opencode-project',
|
|
25
28
|
];
|
|
26
29
|
function getCliRoot() {
|
|
27
30
|
return resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -134,12 +137,24 @@ export function getSkillInstallPath(target, options) {
|
|
|
134
137
|
if (target === 'claude-project') {
|
|
135
138
|
return resolve(cwd, '.claude', 'skills', SKILL_NAME);
|
|
136
139
|
}
|
|
140
|
+
// OpenCode discovers user skills under XDG config (~/.config/opencode/skills)
|
|
141
|
+
// and project skills under .opencode/skills (ALI-1044). The copy/link machinery
|
|
142
|
+
// is agent-neutral; only the destination path differs.
|
|
143
|
+
if (target === 'opencode-user') {
|
|
144
|
+
return resolve(home, '.config', 'opencode', 'skills', SKILL_NAME);
|
|
145
|
+
}
|
|
146
|
+
if (target === 'opencode-project') {
|
|
147
|
+
return resolve(cwd, '.opencode', 'skills', SKILL_NAME);
|
|
148
|
+
}
|
|
137
149
|
return resolve(cwd, 'AGENTS.md');
|
|
138
150
|
}
|
|
139
151
|
export function getTargetForAgent(agent, scope) {
|
|
140
152
|
if (agent === 'claude') {
|
|
141
153
|
return scope === 'global' ? 'claude-user' : 'claude-project';
|
|
142
154
|
}
|
|
155
|
+
if (agent === 'opencode') {
|
|
156
|
+
return scope === 'global' ? 'opencode-user' : 'opencode-project';
|
|
157
|
+
}
|
|
143
158
|
return scope === 'global' ? 'codex-user' : 'agents-project';
|
|
144
159
|
}
|
|
145
160
|
export function isSkillInstallAgent(value) {
|
package/package.json
CHANGED
package/src/credentials.ts
CHANGED
|
@@ -210,6 +210,52 @@ export function getActiveBaseUrl(): string | null {
|
|
|
210
210
|
return config?.activeBaseUrl || null
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* In-sandbox bearer resolution (ALI-1044). Resolves a bearer supplied out-of-band
|
|
215
|
+
* to the hosted agent, WITHOUT consulting credentials.json:
|
|
216
|
+
* 1. `ORIZU_TOKEN` — an explicit bearer in the environment (wins if present).
|
|
217
|
+
* 2. `ORIZU_TOKEN_FILE` — an absolute path to a 0600 file whose TRIMMED contents
|
|
218
|
+
* are the bearer.
|
|
219
|
+
* Returns null when neither is set, so callers fall through to credentials.json.
|
|
220
|
+
*
|
|
221
|
+
* The token file is read FRESH on EVERY call and never cached: the hosted loop
|
|
222
|
+
* ROTATES the file underneath a long-lived agent process, so caching the value
|
|
223
|
+
* would pin a stale (soon-expired) bearer. The token is never logged, echoed, or
|
|
224
|
+
* passed via argv — only returned to the caller for a single request's header.
|
|
225
|
+
*
|
|
226
|
+
* A set-but-unreadable/empty `ORIZU_TOKEN_FILE` throws a clear error rather than
|
|
227
|
+
* returning null, so a misconfigured sandbox fails loudly instead of silently
|
|
228
|
+
* falling back to (absent) credentials.json and emitting a confusing "not logged
|
|
229
|
+
* in" message.
|
|
230
|
+
*/
|
|
231
|
+
export function resolveEnvBearerToken(): string | null {
|
|
232
|
+
const explicit = process.env.ORIZU_TOKEN
|
|
233
|
+
if (explicit && explicit.trim()) {
|
|
234
|
+
return explicit.trim()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const tokenFile = process.env.ORIZU_TOKEN_FILE
|
|
238
|
+
if (tokenFile && tokenFile.length > 0) {
|
|
239
|
+
if (!existsSync(tokenFile)) {
|
|
240
|
+
throw new Error(`ORIZU_TOKEN_FILE is set to ${tokenFile} but no such file exists.`)
|
|
241
|
+
}
|
|
242
|
+
let raw: string
|
|
243
|
+
try {
|
|
244
|
+
raw = readFileSync(tokenFile, 'utf8')
|
|
245
|
+
} catch (error) {
|
|
246
|
+
const detail = isNodeError(error) ? error.message : String(error)
|
|
247
|
+
throw new Error(`Failed to read ORIZU_TOKEN_FILE (${tokenFile}): ${detail}`)
|
|
248
|
+
}
|
|
249
|
+
const token = raw.trim()
|
|
250
|
+
if (!token) {
|
|
251
|
+
throw new Error(`ORIZU_TOKEN_FILE (${tokenFile}) is empty.`)
|
|
252
|
+
}
|
|
253
|
+
return token
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
|
|
213
259
|
export function setActiveBaseUrl(baseUrl: string | null) {
|
|
214
260
|
const config = loadCredentialsConfigForWrite()
|
|
215
261
|
config.activeBaseUrl = baseUrl
|
package/src/help.ts
CHANGED
|
@@ -35,6 +35,7 @@ const GROUPS: CliGroupDoc[] = [
|
|
|
35
35
|
{ name: 'Teams', summary: 'Manage teams and team memberships.' },
|
|
36
36
|
{ name: 'Projects', summary: 'Manage projects inside teams.' },
|
|
37
37
|
{ name: 'Prompts and judges', summary: 'Work with versioned prompt and judge artifacts.' },
|
|
38
|
+
{ name: 'Report comments', summary: 'Review and mutate anchored report comments across prompt, optimization, and task reports.' },
|
|
38
39
|
{ name: 'Scorers and runners', summary: 'Register scorers, push runnable artifacts, execute runners, and submit scores.' },
|
|
39
40
|
{ name: 'Optimizations', summary: 'Start, run, export, and finalize optimization runs.' },
|
|
40
41
|
{ name: 'Apps', summary: 'Create, preview, update, inspect, and export review apps.' },
|
|
@@ -82,6 +83,67 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
82
83
|
summary: 'Submit an optimization event payload.',
|
|
83
84
|
group: 'Optimizations',
|
|
84
85
|
},
|
|
86
|
+
{
|
|
87
|
+
path: ['comments', 'list'],
|
|
88
|
+
usage: 'orizu comments list (--prompt <id-or-name> --project <team/project> [--label <label> | --version <id>] | --run <run-id> | --task <task-id>) [--json]',
|
|
89
|
+
summary: 'List report comment threads with open/resolved status and replies.',
|
|
90
|
+
group: 'Report comments',
|
|
91
|
+
options: [
|
|
92
|
+
{ name: '--prompt <id-or-name>', help: 'Prompt report target. Requires --project.' },
|
|
93
|
+
{ name: '--project <team/project>', help: 'Project slug for prompt targets.' },
|
|
94
|
+
{ name: '--label <label>', help: 'Prompt version label to comment on.' },
|
|
95
|
+
{ name: '--version <id>', help: 'Prompt version ID to comment on.' },
|
|
96
|
+
{ name: '--run <run-id>', help: 'Optimization run report target.' },
|
|
97
|
+
{ name: '--task <task-id>', help: 'Task report target.' },
|
|
98
|
+
{ name: '--json', help: 'Emit the full comments payload.' },
|
|
99
|
+
],
|
|
100
|
+
examples: [
|
|
101
|
+
'orizu comments list --prompt Generator --project core/evals --label production',
|
|
102
|
+
'orizu comments list --run <run-id>',
|
|
103
|
+
'orizu comments list --task <task-id>',
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
path: ['comments', 'add'],
|
|
108
|
+
usage: 'orizu comments add (--prompt <id-or-name> --project <team/project> [--label <label> | --version <id>] | --run <run-id> | --task <task-id>) --body <text|@file> [--anchor <text>] [--lines <start:end>] [--via <name>] [--json]',
|
|
109
|
+
summary: 'Add an anchored top-level comment to a report.',
|
|
110
|
+
group: 'Report comments',
|
|
111
|
+
options: [
|
|
112
|
+
{ name: '--body <text|@file>', help: 'Comment body or @file reference.', required: true },
|
|
113
|
+
{ name: '--anchor <text>', help: 'Quoted report text for the comment anchor.' },
|
|
114
|
+
{ name: '--lines <start:end>', help: 'One-based inclusive line range for the anchor.' },
|
|
115
|
+
{ name: '--via <name>', help: 'Tool or agent name to show in attribution.' },
|
|
116
|
+
{ name: '--json', help: 'Emit the created comment payload.' },
|
|
117
|
+
],
|
|
118
|
+
examples: [
|
|
119
|
+
'orizu comments add --run <run-id> --body @comment.md --anchor "Score summary" --lines 4:6',
|
|
120
|
+
'orizu comments add --task <task-id> --body "Clarify this finding" --via Codex',
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
path: ['comments', 'reply'],
|
|
125
|
+
usage: 'orizu comments reply <comment-id> --body <text|@file> [--via <name>] [--json]',
|
|
126
|
+
summary: 'Reply to a top-level report comment thread.',
|
|
127
|
+
group: 'Report comments',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
path: ['comments', 'resolve'],
|
|
131
|
+
usage: 'orizu comments resolve <comment-id> [--json]',
|
|
132
|
+
summary: 'Resolve a top-level report comment thread.',
|
|
133
|
+
group: 'Report comments',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
path: ['comments', 'unresolve'],
|
|
137
|
+
usage: 'orizu comments unresolve <comment-id> [--json]',
|
|
138
|
+
summary: 'Reopen a resolved report comment thread.',
|
|
139
|
+
group: 'Report comments',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
path: ['comments', 'edit'],
|
|
143
|
+
usage: 'orizu comments edit <comment-id> --body <text|@file> [--json]',
|
|
144
|
+
summary: 'Edit one of your report comments or replies.',
|
|
145
|
+
group: 'Report comments',
|
|
146
|
+
},
|
|
85
147
|
{
|
|
86
148
|
path: ['setup'],
|
|
87
149
|
usage: 'orizu setup [--team <slug>] [--agent <claude|codex>]... [--workspace [path]|--no-workspace] [--validate] [--fix] [--no-symlinks] [--verbose] [--no-install] [--handoff|--no-handoff] [--launch <claude|codex>] [--skip-login] [--yes] [--dry-run] [--no-input|--non-interactive] [--json]',
|
|
@@ -125,7 +187,7 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
125
187
|
},
|
|
126
188
|
{
|
|
127
189
|
path: ['install-skill'],
|
|
128
|
-
usage: 'orizu install-skill [--agent <claude|codex>]... [--scope global|project] [--mode auto|link|copy] [--target <target>]... [--yes] [--dry-run]',
|
|
190
|
+
usage: 'orizu install-skill [--agent <claude|codex|opencode>]... [--scope global|project] [--mode auto|link|copy] [--target <target>]... [--yes] [--dry-run]',
|
|
129
191
|
summary: 'Install or repair the Orizu coding-agent skill.',
|
|
130
192
|
group: 'Agent setup',
|
|
131
193
|
aliases: [['skills', 'install']],
|
|
@@ -273,12 +335,6 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
273
335
|
summary: 'Restore an archived prompt artifact to active status.',
|
|
274
336
|
group: 'Prompts and judges',
|
|
275
337
|
},
|
|
276
|
-
{
|
|
277
|
-
path: ['prompts', 'comments'],
|
|
278
|
-
usage: 'orizu prompts comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <id>] [--json]',
|
|
279
|
-
summary: 'List prompt comment threads with open/resolved status and replies.',
|
|
280
|
-
group: 'Prompts and judges',
|
|
281
|
-
},
|
|
282
338
|
{
|
|
283
339
|
path: ['prompts', 'pull'],
|
|
284
340
|
usage: 'orizu prompts pull <prompt-id-or-name> --project <team/project> --out <dir> [--label <label> | --version <id>] [--json]',
|
|
@@ -563,6 +619,20 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
563
619
|
'orizu tasks report upload --task <taskId> --report @./report.md',
|
|
564
620
|
],
|
|
565
621
|
},
|
|
622
|
+
{
|
|
623
|
+
path: ['tasks', 'report', 'get'],
|
|
624
|
+
usage: 'orizu tasks report get --task <taskId> [--json]',
|
|
625
|
+
summary: 'Read the Markdown report for a task.',
|
|
626
|
+
group: 'Tasks',
|
|
627
|
+
options: [
|
|
628
|
+
{ name: '--task <taskId>', help: 'Task ID whose report should be read.', required: true },
|
|
629
|
+
{ name: '--json', help: 'Emit the full report payload.' },
|
|
630
|
+
],
|
|
631
|
+
examples: [
|
|
632
|
+
'orizu tasks report get --task <taskId>',
|
|
633
|
+
'orizu tasks report get --task <taskId> --json',
|
|
634
|
+
],
|
|
635
|
+
},
|
|
566
636
|
{
|
|
567
637
|
path: ['tasks', 'pause'],
|
|
568
638
|
usage: 'orizu tasks pause --task <taskId>',
|
package/src/hosted-bootstrap.ts
CHANGED
|
@@ -441,6 +441,58 @@ export async function bootstrapHostedSandbox(opts: HostedBootstrapOptions): Prom
|
|
|
441
441
|
})
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
// 4b — Stage the orizu-cli skill into the workspace so the agent discovers it
|
|
445
|
+
// (ALI-1044). Resolve the CLI's OWN vendored skill dir in-sandbox, preferring:
|
|
446
|
+
// (1) $ORIZU_SKILL_SOURCE_DIR — explicit override (also honored by the CLI's
|
|
447
|
+
// skill-installer; the local-sim rehearsal sets it),
|
|
448
|
+
// (2) `orizu skills path` — the packaged vendor/skills/orizu-cli of the
|
|
449
|
+
// globally-installed CLI (the production path),
|
|
450
|
+
// (3) `$(npm root -g)/orizu/vendor/skills/orizu-cli` — belt fallback.
|
|
451
|
+
// Then SYMLINK it under <workspaceDir>/.claude/skills/orizu-cli, falling back
|
|
452
|
+
// to a copy if the symlink cannot be created. Non-fatal + recorded, exactly
|
|
453
|
+
// like the CLI install. Resolved paths stay in shell vars (never interpolated),
|
|
454
|
+
// so they cannot inject into the command.
|
|
455
|
+
currentStep = 'skill_staged'
|
|
456
|
+
const skillSkillsDirRel = `${workspaceDir}/.claude/skills`
|
|
457
|
+
const stage = await session.exec(
|
|
458
|
+
[
|
|
459
|
+
`mkdir -p ${skillSkillsDirRel}`,
|
|
460
|
+
// The staged skill is bootstrap-injected RUNTIME scaffolding (a symlink to
|
|
461
|
+
// the sandbox-local CLI vendor dir), NOT the agent's work. Exclude it
|
|
462
|
+
// repo-LOCALLY (.git/info/exclude — invisible to the diff, never committed)
|
|
463
|
+
// so auto-harvest's `git add -A` can't sweep it into the session branch and
|
|
464
|
+
// (ALI-1051) auto-apply a broken symlink to the customer's main.
|
|
465
|
+
`if [ -d ${workspaceDir}/.git ]; then printf '%s\\n' '/.claude/skills/' >> ${workspaceDir}/.git/info/exclude; fi`,
|
|
466
|
+
`src="${'${ORIZU_SKILL_SOURCE_DIR:-}'}"`,
|
|
467
|
+
`if [ -z "$src" ] || [ ! -d "$src" ]; then src="$(orizu skills path 2>/dev/null || true)"; fi`,
|
|
468
|
+
`if [ -z "$src" ] || [ ! -d "$src" ]; then r="$(npm root -g 2>/dev/null || true)"; if [ -n "$r" ] && [ -d "$r/orizu/vendor/skills/orizu-cli" ]; then src="$r/orizu/vendor/skills/orizu-cli"; fi; fi`,
|
|
469
|
+
`if [ -z "$src" ] || [ ! -d "$src" ]; then echo "NO_SOURCE"; exit 0; fi`,
|
|
470
|
+
`dest="${skillSkillsDirRel}/orizu-cli"`,
|
|
471
|
+
`rm -rf "$dest"`,
|
|
472
|
+
`if ln -s "$src" "$dest" 2>/dev/null; then echo "SYMLINK $src"; else cp -R "$src" "$dest" && echo "COPY $src"; fi`,
|
|
473
|
+
].join('\n')
|
|
474
|
+
)
|
|
475
|
+
const stageOut = stage.stdout.trim()
|
|
476
|
+
const stagedMethod = stageOut.startsWith('SYMLINK')
|
|
477
|
+
? 'symlink'
|
|
478
|
+
: stageOut.startsWith('COPY')
|
|
479
|
+
? 'copy'
|
|
480
|
+
: null
|
|
481
|
+
const stagedOk = stage.exitCode === 0 && stagedMethod !== null
|
|
482
|
+
record(
|
|
483
|
+
'skill_staged',
|
|
484
|
+
stagedOk,
|
|
485
|
+
stagedOk
|
|
486
|
+
? `orizu-cli skill staged (${stagedMethod})`
|
|
487
|
+
: `skill staging unresolved: ${stageOut || `exit ${stage.exitCode}`}`
|
|
488
|
+
)
|
|
489
|
+
await sink.append('skill_staged', {
|
|
490
|
+
ok: stagedOk,
|
|
491
|
+
method: stagedMethod,
|
|
492
|
+
dest: `${skillSkillsDirRel}/orizu-cli`,
|
|
493
|
+
outputTail: tail(stage.stdout, stage.stderr),
|
|
494
|
+
})
|
|
495
|
+
|
|
444
496
|
// 5 — Customer setup hook (fresh-boot only, non-fatal, output captured).
|
|
445
497
|
// P3-a: for enforced-egress providers the host DEFERS this hook to the loop,
|
|
446
498
|
// which runs it AFTER the egress canary proves the firewall is live — so the
|
|
@@ -760,10 +760,32 @@ function errorEvent(messageId: string, error: unknown): HarnessEvent {
|
|
|
760
760
|
export interface OpenCodeConfigOptions {
|
|
761
761
|
/** Provider-qualified model string, e.g. "anthropic/claude-opus-4-8". */
|
|
762
762
|
model: string
|
|
763
|
-
/** OpenCode permission map; defaults to allow-all
|
|
763
|
+
/** OpenCode permission map; defaults to allow-all EXCEPT the headless-denied
|
|
764
|
+
* tools below (sandbox is the boundary for everything else). */
|
|
764
765
|
permission?: Record<string, unknown>
|
|
765
766
|
}
|
|
766
767
|
|
|
768
|
+
/**
|
|
769
|
+
* Tools DENIED in a hosted headless run (ALI-1037). OpenCode exposes a `question`
|
|
770
|
+
* permission key (docs/permissions) that gates the interactive "ask the user a
|
|
771
|
+
* question" tool. In an unattended sandbox there is no human to answer it, and
|
|
772
|
+
* OpenCode's only external reply endpoint — `POST /session/{id}/permissions/{permID}`
|
|
773
|
+
* — takes a BOOLEAN allow/deny, so it cannot select one of the tool's multiple
|
|
774
|
+
* options anyway. Denying the tool makes the model's ask fail fast (surfaced back
|
|
775
|
+
* to it as a tool error) so it proceeds autonomously per the standing preamble
|
|
776
|
+
* (HOSTED_TASK_PREAMBLE), instead of blocking the run forever. This is the
|
|
777
|
+
* documented fallback path in ALI-1037 (answer-externally is not cleanly possible).
|
|
778
|
+
*/
|
|
779
|
+
export const HEADLESS_DENIED_TOOLS = ['question'] as const
|
|
780
|
+
|
|
781
|
+
/** The default headless permission map: allow everything, deny the interactive
|
|
782
|
+
* question tool (see HEADLESS_DENIED_TOOLS). */
|
|
783
|
+
export function defaultHeadlessPermission(): Record<string, unknown> {
|
|
784
|
+
const permission: Record<string, unknown> = { '*': { '*': 'allow' } }
|
|
785
|
+
for (const tool of HEADLESS_DENIED_TOOLS) permission[tool] = 'deny'
|
|
786
|
+
return permission
|
|
787
|
+
}
|
|
788
|
+
|
|
767
789
|
/**
|
|
768
790
|
* Build the `OPENCODE_CONFIG_CONTENT` payload OpenCode reads at boot (mirrors
|
|
769
791
|
* OpenInspect's `start_opencode`). Kept pure so the launch command and the
|
|
@@ -772,7 +794,7 @@ export interface OpenCodeConfigOptions {
|
|
|
772
794
|
export function buildOpenCodeConfigContent(opts: OpenCodeConfigOptions): string {
|
|
773
795
|
return JSON.stringify({
|
|
774
796
|
model: opts.model,
|
|
775
|
-
permission: opts.permission ??
|
|
797
|
+
permission: opts.permission ?? defaultHeadlessPermission(),
|
|
776
798
|
})
|
|
777
799
|
}
|
|
778
800
|
|
package/src/hosted-harness.ts
CHANGED
|
@@ -79,6 +79,15 @@ export const HARNESS_EVENT_KINDS = {
|
|
|
79
79
|
// and never terminal, so they append like any structural event.
|
|
80
80
|
egress_blocked: 'egress_blocked',
|
|
81
81
|
egress_allowed: 'egress_allowed',
|
|
82
|
+
// Durability + headless (ALI-1036 / ALI-1037). NOT produced by a harness — the
|
|
83
|
+
// in-sandbox loop emits these through the SAME single-writer run-event sink:
|
|
84
|
+
// the end-of-run auto-harvest result (work_persisted/work_none/work_persist_failed)
|
|
85
|
+
// and the headless question auto-handling record (question_auto_answered). They
|
|
86
|
+
// are ordinary structural events (never terminal), appended like any other.
|
|
87
|
+
work_persisted: 'work_persisted',
|
|
88
|
+
work_none: 'work_none',
|
|
89
|
+
work_persist_failed: 'work_persist_failed',
|
|
90
|
+
question_auto_answered: 'question_auto_answered',
|
|
82
91
|
error: 'error',
|
|
83
92
|
execution_complete: 'execution_complete',
|
|
84
93
|
} as const
|