orizu 0.5.6 → 0.5.8

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/src/index.ts CHANGED
@@ -68,6 +68,11 @@ import { hostedCommand } from './hosted-session-cli.js'
68
68
  import { workspaceSyncCommand } from './workspace-sync.js'
69
69
  import { runGitCredential } from './git-credential.js'
70
70
  import { type GithubLinkResult, runGithubLink, runInteractiveHostedSetup } from './github-setup.js'
71
+ import {
72
+ reportCommentsCommand,
73
+ throwDeprecatedPromptCommentsCommand,
74
+ type ReportCommentsCliContext,
75
+ } from './report-comments-cli.js'
71
76
 
72
77
  function getErrorMessage(error: unknown): string {
73
78
  return error instanceof Error ? error.message : String(error)
@@ -144,60 +149,18 @@ interface PromptSummary {
144
149
  description?: string | null
145
150
  }
146
151
 
147
- interface PromptCommentAuthor {
148
- name: string
149
- initials?: string
150
- color?: string
151
- }
152
-
153
- interface PromptCommentAnchor {
154
- text: string | null
155
- startLine: number | null
156
- endLine: number | null
157
- }
158
-
159
- interface PromptCommentReply {
160
- id: string
161
- body: string
162
- author: PromptCommentAuthor
163
- createdAt: string
164
- updatedAt: string
165
- }
166
-
167
- interface PromptCommentThread {
168
- id: string
169
- status: 'open' | 'resolved'
170
- body: string
171
- anchor: PromptCommentAnchor | null
172
- author: PromptCommentAuthor
173
- createdAt: string
174
- updatedAt: string
175
- resolvedAt: string | null
176
- resolvedByUserId: string | null
177
- replyCount: number
178
- replies: PromptCommentReply[]
179
- }
180
-
181
- interface PromptCommentsPayload {
182
- prompt: {
183
- id: string
184
- name: string
185
- role: string
186
- description?: string | null
187
- }
188
- version: {
152
+ interface TaskReportPayload {
153
+ task: {
189
154
  id: string
190
- versionNumber?: number
191
- versionLabel?: string | null
155
+ title?: string | null
192
156
  status?: string | null
157
+ report: {
158
+ markdown: string
159
+ sourceName: string | null
160
+ createdAt: string | null
161
+ updatedAt?: string | null
162
+ }
193
163
  }
194
- summary: {
195
- threadCount: number
196
- openThreadCount: number
197
- resolvedThreadCount: number
198
- replyCount: number
199
- }
200
- comments: PromptCommentThread[]
201
164
  }
202
165
 
203
166
  interface ScorerSummary {
@@ -1083,78 +1046,6 @@ function printTaskStatusSummary(data: TaskStatusPayload) {
1083
1046
  }
1084
1047
  }
1085
1048
 
1086
- function compactTerminalText(value: string, maxLength = 160): string {
1087
- const compact = sanitizeTerminalText(value).replace(/\s+/g, ' ').trim()
1088
- if (compact.length <= maxLength) {
1089
- return compact
1090
- }
1091
-
1092
- return `${compact.slice(0, Math.max(0, maxLength - 1)).trim()}…`
1093
- }
1094
-
1095
- function printIndentedBody(value: string, indent: string) {
1096
- const lines = sanitizeTerminalText(value || '').split(/\r?\n/)
1097
- for (const line of lines) {
1098
- printLine(`${indent}${line}`)
1099
- }
1100
- }
1101
-
1102
- function formatPromptCommentAnchor(anchor: PromptCommentAnchor | null): string | null {
1103
- if (!anchor) {
1104
- return null
1105
- }
1106
-
1107
- const lineLabel = anchor.startLine && anchor.endLine
1108
- ? anchor.startLine === anchor.endLine
1109
- ? `line ${anchor.startLine}`
1110
- : `lines ${anchor.startLine}-${anchor.endLine}`
1111
- : null
1112
- const selectedText = anchor.text ? `"${compactTerminalText(anchor.text, 180)}"` : null
1113
-
1114
- if (lineLabel && selectedText) {
1115
- return `${lineLabel}: ${selectedText}`
1116
- }
1117
- return lineLabel || selectedText
1118
- }
1119
-
1120
- function printPromptComments(data: PromptCommentsPayload) {
1121
- const versionLabel = data.version.versionLabel || (
1122
- data.version.versionNumber ? `v${data.version.versionNumber}` : data.version.id
1123
- )
1124
-
1125
- printLine(`Prompt: ${sanitizeTerminalText(data.prompt.name)} (${sanitizeTerminalText(data.prompt.id)})`)
1126
- printLine(`Version: ${sanitizeTerminalText(String(versionLabel))} (${sanitizeTerminalText(data.version.id)})`)
1127
- printLine(
1128
- `Comments: ${data.summary.threadCount} thread${data.summary.threadCount === 1 ? '' : 's'} ` +
1129
- `(${data.summary.openThreadCount} open, ${data.summary.resolvedThreadCount} resolved, ${data.summary.replyCount} replies)`
1130
- )
1131
-
1132
- if (data.comments.length === 0) {
1133
- printLine('\nNo comments found for this prompt version.')
1134
- return
1135
- }
1136
-
1137
- for (const [index, thread] of data.comments.entries()) {
1138
- const authorName = thread.author?.name || 'Unknown'
1139
- const anchor = formatPromptCommentAnchor(thread.anchor)
1140
- printLine('')
1141
- printLine(`${index + 1}. [${thread.status}] ${sanitizeTerminalText(authorName)} · ${sanitizeTerminalText(thread.createdAt)}`)
1142
- if (anchor) {
1143
- printLine(` Selection: ${anchor}`)
1144
- }
1145
- printIndentedBody(thread.body, ' ')
1146
-
1147
- if (thread.replies.length > 0) {
1148
- printLine(` Replies (${thread.replies.length})`)
1149
- for (const reply of thread.replies) {
1150
- const replyAuthor = reply.author?.name || 'Unknown'
1151
- printLine(` - ${sanitizeTerminalText(replyAuthor)} · ${sanitizeTerminalText(reply.createdAt)}`)
1152
- printIndentedBody(reply.body, ' ')
1153
- }
1154
- }
1155
- }
1156
- }
1157
-
1158
1049
  const DEFAULT_AUTH_CALLBACK_PORT = 43123
1159
1050
 
1160
1051
  function resolveAuthCallbackPort(): number {
@@ -1562,37 +1453,6 @@ async function listJudges() {
1562
1453
  printPromptSummaries(data.judges, 'No judges found.')
1563
1454
  }
1564
1455
 
1565
- async function listPromptComments() {
1566
- const promptRef = getPositionalArg(2)
1567
- const project = getArg('--project') || await resolveProjectSlug(null)
1568
- const label = getArg('--label')
1569
- const version = getArg('--version')
1570
-
1571
- if (!promptRef) {
1572
- throw new Error('Usage: orizu prompts comments <prompt-id-or-name> --project <team/project> [--label <label> | --version <version-id>] [--json]')
1573
- }
1574
- if (label && version) {
1575
- throw new Error('Use either --label or --version, not both')
1576
- }
1577
-
1578
- const params = new URLSearchParams({ project })
1579
- if (label) params.set('label', label)
1580
- if (version) params.set('version', version)
1581
-
1582
- const response = await authedFetch(`/api/cli/prompts/${encodeURIComponent(promptRef)}/comments?${params.toString()}`)
1583
- if (!response.ok) {
1584
- throw new Error(`Failed to fetch prompt comments: ${await response.text()}`)
1585
- }
1586
-
1587
- const data = await parseJsonResponse<PromptCommentsPayload>(response, 'Prompt comments')
1588
- if (hasJsonFlag()) {
1589
- printJson(data as unknown as Record<string, unknown>)
1590
- return
1591
- }
1592
-
1593
- printPromptComments(data)
1594
- }
1595
-
1596
1456
  async function listScorers() {
1597
1457
  const project = getArg('--project') || await resolveProjectSlug(null)
1598
1458
  const response = await authedFetch(`/api/cli/scorers?project=${encodeURIComponent(project)}`)
@@ -2957,6 +2817,22 @@ function printJson(value: Record<string, unknown>) {
2957
2817
  printLine(JSON.stringify(value))
2958
2818
  }
2959
2819
 
2820
+ function createReportCommentsCliContext(): ReportCommentsCliContext {
2821
+ return {
2822
+ getArg,
2823
+ getPositionalArg,
2824
+ rejectDashPrefixedOptionValue,
2825
+ resolveProjectSlug,
2826
+ readSourceFile,
2827
+ authedFetch,
2828
+ parseJsonResponse,
2829
+ hasJsonFlag,
2830
+ printJson,
2831
+ printLine,
2832
+ sanitizeTerminalText,
2833
+ }
2834
+ }
2835
+
2960
2836
  function printCapabilities() {
2961
2837
  const capabilities = getCapabilities(getCliVersion())
2962
2838
  if (hasJsonFlag()) {
@@ -4994,6 +4870,39 @@ async function setTaskReport() {
4994
4870
  )
4995
4871
  }
4996
4872
 
4873
+ async function getTaskReport() {
4874
+ const taskId = getArg('--task')
4875
+ rejectDashPrefixedOptionValue('--task', taskId)
4876
+ if (!taskId) {
4877
+ throw new Error('Usage: orizu tasks report get --task <taskId> [--json]')
4878
+ }
4879
+
4880
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/report`)
4881
+ if (!response.ok) {
4882
+ throw new Error(`Failed to read task report: ${await response.text()}`)
4883
+ }
4884
+
4885
+ const data = await parseJsonResponse<TaskReportPayload>(response, 'Task report get')
4886
+ if (hasJsonFlag()) {
4887
+ printJson(data as unknown as Record<string, unknown>)
4888
+ return
4889
+ }
4890
+
4891
+ const title = data.task.title ? `${data.task.title} ` : ''
4892
+ printLine(`Task report: ${sanitizeTerminalText(title)}(${sanitizeTerminalText(data.task.id)})`)
4893
+ if (data.task.status) {
4894
+ printLine(`Status: ${sanitizeTerminalText(data.task.status)}`)
4895
+ }
4896
+ if (data.task.report.sourceName) {
4897
+ printLine(`Source: ${sanitizeTerminalText(data.task.report.sourceName)}`)
4898
+ }
4899
+ if (data.task.report.createdAt) {
4900
+ printLine(`Created: ${sanitizeTerminalText(data.task.report.createdAt)}`)
4901
+ }
4902
+ printLine('')
4903
+ printLine(sanitizeTerminalText(data.task.report.markdown))
4904
+ }
4905
+
4997
4906
  interface AppDetailPayload {
4998
4907
  id: string
4999
4908
  name: string
@@ -6339,6 +6248,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
6339
6248
  return
6340
6249
  }
6341
6250
 
6251
+ if (command === 'comments') {
6252
+ await reportCommentsCommand(subcommand, createReportCommentsCliContext())
6253
+ return
6254
+ }
6255
+
6342
6256
  if (command === 'teams' && subcommand === 'list') {
6343
6257
  await listTeams()
6344
6258
  return
@@ -6383,8 +6297,7 @@ export async function main(rawArgs = process.argv.slice(2)) {
6383
6297
  }
6384
6298
 
6385
6299
  if (command === 'prompts' && subcommand === 'comments') {
6386
- await listPromptComments()
6387
- return
6300
+ throwDeprecatedPromptCommentsCommand()
6388
6301
  }
6389
6302
 
6390
6303
  if (command === 'prompts' && subcommand === 'pull') {
@@ -6594,6 +6507,11 @@ export async function main(rawArgs = process.argv.slice(2)) {
6594
6507
  return
6595
6508
  }
6596
6509
 
6510
+ if (command === 'tasks' && subcommand === 'report' && cliArgs[2] === 'get') {
6511
+ await getTaskReport()
6512
+ return
6513
+ }
6514
+
6597
6515
  if (command === 'tasks' && subcommand === 'pause') { await updateTaskStatus('paused'); return }
6598
6516
  if (command === 'tasks' && subcommand === 'unpause') { await updateTaskStatus('active'); return }
6599
6517
  if (command === 'tasks' && subcommand === 'complete') { await updateTaskStatus('completed'); return }
@@ -16,7 +16,7 @@ export interface ManifestsCommandIo {
16
16
  resolveProjectSlug?: (arg: string | null) => Promise<string>
17
17
  }
18
18
 
19
- const MANIFEST_ACTIONS = new Set(['approve', 'reject', 'apply'])
19
+ const MANIFEST_ACTIONS = new Set(['approve', 'reject', 'apply', 'revert'])
20
20
 
21
21
  async function responseMessage(response: Response): Promise<string> {
22
22
  try {
@@ -93,6 +93,93 @@ function num(value: unknown): number {
93
93
  return typeof value === 'number' ? value : 0
94
94
  }
95
95
 
96
+ // Auto-applied manifests (ALI-1040) landed by SERVER POLICY, not a human approver
97
+ // (approved_by is NULL). Mark them so a reviewer can tell policy applies from
98
+ // human applies at a glance; the reason lives in the outcome for `show`.
99
+ function wasAutoApplied(manifest: Record<string, unknown>): boolean {
100
+ return asRecord(manifest.outcome).autoApplied === true
101
+ }
102
+
103
+ /** STATUS cell: `applied*` when the server auto-applied it, else the raw status. */
104
+ function statusDisplay(manifest: Record<string, unknown>): string {
105
+ const status = typeof manifest.status === 'string' ? manifest.status : '(unknown)'
106
+ return wasAutoApplied(manifest) ? `${status}*` : status
107
+ }
108
+
109
+ /** One-line auto-apply audit note for `show`, or null when not auto-applied. */
110
+ function autoApplyNote(manifest: Record<string, unknown>): string | null {
111
+ if (!wasAutoApplied(manifest)) return null
112
+ const reason = asRecord(manifest.outcome).autoApplyReason
113
+ return typeof reason === 'string' && reason.length > 0
114
+ ? `auto-applied by policy: ${reason}`
115
+ : 'auto-applied by policy'
116
+ }
117
+
118
+ // -- list table (ALI-1038) ----------------------------------------------------
119
+
120
+ /** "3m" / "4h" / "2d" — enough resolution to pick a manifest out of a list. */
121
+ function relativeAge(iso: unknown, now: number): string {
122
+ if (typeof iso !== 'string') return '-'
123
+ const then = Date.parse(iso)
124
+ if (Number.isNaN(then)) return '-'
125
+ const mins = Math.max(0, Math.floor((now - then) / 60_000))
126
+ if (mins < 60) return `${mins}m`
127
+ const hours = Math.floor(mins / 60)
128
+ if (hours < 48) return `${hours}h`
129
+ return `${Math.floor(hours / 24)}d`
130
+ }
131
+
132
+ /** One-glance change summary from the stored proposedState — no live API call. */
133
+ function changesSummary(manifest: Record<string, unknown>): string {
134
+ if (manifest.actionType !== 'repo_merge') return '-'
135
+ const proposed = asRecord(manifest.proposedState)
136
+ const files = num(proposed.filesChanged)
137
+ const parts = `${files} file${files === 1 ? '' : 's'} +${num(proposed.additions)}/-${num(proposed.deletions)}`
138
+ // Instruction files steer agent behavior — reviewers should see the flag in the list.
139
+ return proposed.touches_instruction_files === true ? `${parts} !instr` : parts
140
+ }
141
+
142
+ /** Reviewable-first (pending_approval, then draft), newest first within a group. */
143
+ function listOrder(a: Record<string, unknown>, b: Record<string, unknown>): number {
144
+ const rank = (m: Record<string, unknown>): number =>
145
+ m.status === 'pending_approval' ? 0 : m.status === 'draft' ? 1 : 2
146
+ const byRank = rank(a) - rank(b)
147
+ if (byRank !== 0) return byRank
148
+ const created = (m: Record<string, unknown>): number =>
149
+ typeof m.createdAt === 'string' ? Date.parse(m.createdAt) || 0 : 0
150
+ return created(b) - created(a)
151
+ }
152
+
153
+ /** Render manifests as the standard padEnd table (matches teams/projects list). */
154
+ function manifestsTable(manifests: Record<string, unknown>[], now: number): string {
155
+ if (manifests.length === 0) return 'promotion manifests\n(none)'
156
+ const rows = [...manifests].sort(listOrder).map(manifest => ({
157
+ id: typeof manifest.id === 'string' ? manifest.id : '(unknown)',
158
+ status: statusDisplay(manifest),
159
+ type: typeof manifest.actionType === 'string' ? manifest.actionType : '(unknown)',
160
+ changes: changesSummary(manifest),
161
+ author: typeof manifest.authorActorType === 'string' ? manifest.authorActorType : '-',
162
+ age: relativeAge(manifest.createdAt, now),
163
+ }))
164
+ const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length))
165
+ const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length))
166
+ const typeWidth = Math.max('TYPE'.length, ...rows.map(row => row.type.length))
167
+ const changesWidth = Math.max('CHANGES'.length, ...rows.map(row => row.changes.length))
168
+ const authorWidth = Math.max('AUTHOR'.length, ...rows.map(row => row.author.length))
169
+ const header =
170
+ `${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'TYPE'.padEnd(typeWidth)} ` +
171
+ `${'CHANGES'.padEnd(changesWidth)} ${'AUTHOR'.padEnd(authorWidth)} AGE`
172
+ const divider =
173
+ `${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ${'-'.repeat(typeWidth)} ` +
174
+ `${'-'.repeat(changesWidth)} ${'-'.repeat(authorWidth)} ---`
175
+ const body = rows.map(
176
+ row =>
177
+ `${row.id.padEnd(idWidth)} ${row.status.padEnd(statusWidth)} ${row.type.padEnd(typeWidth)} ` +
178
+ `${row.changes.padEnd(changesWidth)} ${row.author.padEnd(authorWidth)} ${row.age}`
179
+ )
180
+ return [header, divider, ...body].join('\n')
181
+ }
182
+
96
183
  // repo_merge manifests (ALI-972) render the compare summary from stored evidence
97
184
  // — no live GitHub call — so a reviewer can approve informed without a PR.
98
185
  function repoMergeShow(manifest: Record<string, unknown>): string {
@@ -130,7 +217,7 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
130
217
  const response = await fetcherFrom(io)(`/api/cli/promotion-manifests${query}`)
131
218
  const data = await requireOk(response, 'Manifest list')
132
219
  const manifests = Array.isArray(data.manifests) ? (data.manifests as Record<string, unknown>[]) : []
133
- emit(io, { manifests }, ['promotion manifests', ...manifests.map(manifestLine)].join('\n'))
220
+ emit(io, { manifests }, manifestsTable(manifests, Date.now()))
134
221
  return 0
135
222
  }
136
223
 
@@ -142,7 +229,9 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
142
229
  const response = await fetcherFrom(io)(`/api/cli/promotion-manifests/${encodeURIComponent(id)}`)
143
230
  const data = await requireOk(response, 'Manifest show')
144
231
  const manifest = (data.manifest ?? {}) as Record<string, unknown>
145
- const human = manifest.actionType === 'repo_merge' ? repoMergeShow(manifest) : manifestLine(manifest)
232
+ const base = manifest.actionType === 'repo_merge' ? repoMergeShow(manifest) : manifestLine(manifest)
233
+ const note = autoApplyNote(manifest)
234
+ const human = note ? `${base}\n ${note}` : base
146
235
  emit(io, { manifest }, human)
147
236
  return 0
148
237
  }
@@ -159,10 +248,26 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
159
248
  })
160
249
  const data = await requireOk(response, `Manifest ${subcommand}`)
161
250
  const manifest = (data.manifest ?? {}) as Record<string, unknown>
162
- emit(io, { manifest }, manifestLine(manifest))
251
+ const lines = [manifestLine(manifest)]
252
+ // Reject reaps the session branch best-effort; a failed reap must never be
253
+ // silent (ALI-1043) — surface it with the exact cleanup command.
254
+ if (subcommand === 'reject' && manifest.actionType === 'repo_merge') {
255
+ const outcome = asRecord(manifest.outcome)
256
+ const branch = asRecord(manifest.proposedState).branch
257
+ if (outcome.branchDeleted === true) {
258
+ lines.push('session branch deleted')
259
+ } else if (typeof branch === 'string' && branch.length > 0) {
260
+ lines.push(
261
+ `warning: the session branch could not be deleted — remove it with: git push origin --delete ${branch}`
262
+ )
263
+ } else {
264
+ lines.push('warning: the session branch could not be deleted (branch name unavailable)')
265
+ }
266
+ }
267
+ emit(io, { manifest }, lines.join('\n'))
163
268
  return 0
164
269
  }
165
270
 
166
- io.print('Usage: orizu manifests <list|show|approve|reject|apply> ... [--json]')
271
+ io.print('Usage: orizu manifests <list|show|approve|reject|apply|revert> ... [--json]')
167
272
  return 1
168
273
  }