orizu 0.5.5 → 0.5.7

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.
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { sweepForTokenResidue } from './daytona-slice.js';
26
26
  import { redactSecrets } from './secret-redaction.js';
27
- import { BEARER_BASENAME, BOOT_CONTEXT_BASENAME, DEFAULT_CACHE_REFRESH_BUFFER_MS, HELPER_SCRIPT_BASENAME, PREBAKED_MARKER_PATH, REPO_CRED_CACHE_BASENAME, SETUP_HOOK_RELATIVE_PATH, parsePrebakedMarker, renderCredentialHelperScript, serializeBootContext, } from './hosted-runtime-assets.js';
27
+ import { AGENT_GIT_IDENTITY, BEARER_BASENAME, BOOT_CONTEXT_BASENAME, DEFAULT_CACHE_REFRESH_BUFFER_MS, HELPER_SCRIPT_BASENAME, PREBAKED_MARKER_PATH, REPO_CRED_CACHE_BASENAME, SETUP_HOOK_RELATIVE_PATH, parsePrebakedMarker, renderCredentialHelperScript, serializeBootContext, } from './hosted-runtime-assets.js';
28
28
  function newEventId() {
29
29
  const cryptoObj = globalThis.crypto;
30
30
  if (cryptoObj?.randomUUID)
@@ -236,6 +236,11 @@ export async function bootstrapHostedSandbox(opts) {
236
236
  // Persist the helper repo-LOCAL (never --global) for subsequent fetch/push.
237
237
  await session.exec(`git -C ${workspaceDir} config credential.helper ${shellSingleQuote(helperValue)}`);
238
238
  await session.exec(`git -C ${workspaceDir} config credential.useHttpPath true`);
239
+ // Repo-LOCAL agent identity: the sandbox has no ambient git identity, so
240
+ // without this every agent `git commit` dies with "Author identity unknown"
241
+ // (ALI-1036, found live in QA-3). Same identity the loop stamps on events.
242
+ await session.exec(`git -C ${workspaceDir} config user.name ${shellSingleQuote(AGENT_GIT_IDENTITY.name)}`);
243
+ await session.exec(`git -C ${workspaceDir} config user.email ${shellSingleQuote(AGENT_GIT_IDENTITY.email)}`);
239
244
  record('repo_cloned', true, `cloned ${opts.sessionBranch}`);
240
245
  await sink.append('repo_cloned', { branch: opts.sessionBranch, workspaceDir });
241
246
  // 4 — Provision the Orizu CLI (non-fatal, recorded — OpenInspect setup.sh
@@ -31,6 +31,14 @@ export const DEFAULT_CACHE_REFRESH_BUFFER_MS = 5 * 60 * 1000;
31
31
  /** Customer setup hook path, relative to the repo root (mirrors OpenInspect's
32
32
  * `.openinspect/setup.sh` — fresh-boot only, non-fatal, output captured). */
33
33
  export const SETUP_HOOK_RELATIVE_PATH = '.orizu/setup.sh';
34
+ /**
35
+ * The git author identity for hosted-agent commits. Bootstrap configures it
36
+ * repo-LOCAL in the sandbox clone (never --global) so agent `git commit` works
37
+ * out of the box — the sandbox has no ambient identity and commits died with
38
+ * "Author identity unknown" (ALI-1036, found live in QA-3). The loop uses the
39
+ * same identity for run-event attribution; keep them identical.
40
+ */
41
+ export const AGENT_GIT_IDENTITY = { name: 'Orizu Agent', email: 'agent@orizu.ai' };
34
42
  // -- Pre-baked runtime marker (ALI-1017) -------------------------------------
35
43
  /**
36
44
  * Absolute path of the marker the pre-baked hosted-runtime image writes at build
@@ -37,7 +37,7 @@
37
37
  import { createLocalSimProvider, } from './sandbox-provider.js';
38
38
  import { bootstrapHostedSandbox, createBootstrapRunEventSink, } from './hosted-bootstrap.js';
39
39
  import { resumeRunEventSink } from './hosted-run-event-sink.js';
40
- import { BEARER_BASENAME } from './hosted-runtime-assets.js';
40
+ import { AGENT_GIT_IDENTITY, BEARER_BASENAME } from './hosted-runtime-assets.js';
41
41
  import { createVercelProvider } from './vercel-sandbox-provider.js';
42
42
  import { buildEgressPolicy, DEFAULT_EGRESS_CANARY_HOST } from './egress-policy.js';
43
43
  import { hostedLoopCommand } from './hosted-loop.js';
@@ -355,7 +355,7 @@ export async function startHostedSession(opts) {
355
355
  model,
356
356
  reasoningEffort: opts.reasoningEffort,
357
357
  messageId: `${runId}:task`,
358
- author: { name: 'Orizu Agent', email: 'agent@orizu.ai' },
358
+ author: AGENT_GIT_IDENTITY,
359
359
  anthropicDummyKey: opts.modelApiKey ? ANTHROPIC_DUMMY_KEY : undefined,
360
360
  // ALI-1017: skip the in-sandbox opencode install when the image ships it.
361
361
  prebaked: prebakedImage,
@@ -73,6 +73,66 @@ function asRecord(value) {
73
73
  function num(value) {
74
74
  return typeof value === 'number' ? value : 0;
75
75
  }
76
+ // -- list table (ALI-1038) ----------------------------------------------------
77
+ /** "3m" / "4h" / "2d" — enough resolution to pick a manifest out of a list. */
78
+ function relativeAge(iso, now) {
79
+ if (typeof iso !== 'string')
80
+ return '-';
81
+ const then = Date.parse(iso);
82
+ if (Number.isNaN(then))
83
+ return '-';
84
+ const mins = Math.max(0, Math.floor((now - then) / 60_000));
85
+ if (mins < 60)
86
+ return `${mins}m`;
87
+ const hours = Math.floor(mins / 60);
88
+ if (hours < 48)
89
+ return `${hours}h`;
90
+ return `${Math.floor(hours / 24)}d`;
91
+ }
92
+ /** One-glance change summary from the stored proposedState — no live API call. */
93
+ function changesSummary(manifest) {
94
+ if (manifest.actionType !== 'repo_merge')
95
+ return '-';
96
+ const proposed = asRecord(manifest.proposedState);
97
+ const files = num(proposed.filesChanged);
98
+ const parts = `${files} file${files === 1 ? '' : 's'} +${num(proposed.additions)}/-${num(proposed.deletions)}`;
99
+ // Instruction files steer agent behavior — reviewers should see the flag in the list.
100
+ return proposed.touches_instruction_files === true ? `${parts} !instr` : parts;
101
+ }
102
+ /** Reviewable-first (pending_approval, then draft), newest first within a group. */
103
+ function listOrder(a, b) {
104
+ const rank = (m) => m.status === 'pending_approval' ? 0 : m.status === 'draft' ? 1 : 2;
105
+ const byRank = rank(a) - rank(b);
106
+ if (byRank !== 0)
107
+ return byRank;
108
+ const created = (m) => typeof m.createdAt === 'string' ? Date.parse(m.createdAt) || 0 : 0;
109
+ return created(b) - created(a);
110
+ }
111
+ /** Render manifests as the standard padEnd table (matches teams/projects list). */
112
+ function manifestsTable(manifests, now) {
113
+ if (manifests.length === 0)
114
+ return 'promotion manifests\n(none)';
115
+ const rows = [...manifests].sort(listOrder).map(manifest => ({
116
+ id: typeof manifest.id === 'string' ? manifest.id : '(unknown)',
117
+ status: typeof manifest.status === 'string' ? manifest.status : '(unknown)',
118
+ type: typeof manifest.actionType === 'string' ? manifest.actionType : '(unknown)',
119
+ changes: changesSummary(manifest),
120
+ author: typeof manifest.authorActorType === 'string' ? manifest.authorActorType : '-',
121
+ age: relativeAge(manifest.createdAt, now),
122
+ }));
123
+ const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length));
124
+ const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length));
125
+ const typeWidth = Math.max('TYPE'.length, ...rows.map(row => row.type.length));
126
+ const changesWidth = Math.max('CHANGES'.length, ...rows.map(row => row.changes.length));
127
+ const authorWidth = Math.max('AUTHOR'.length, ...rows.map(row => row.author.length));
128
+ const header = `${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'TYPE'.padEnd(typeWidth)} ` +
129
+ `${'CHANGES'.padEnd(changesWidth)} ${'AUTHOR'.padEnd(authorWidth)} AGE`;
130
+ const divider = `${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ${'-'.repeat(typeWidth)} ` +
131
+ `${'-'.repeat(changesWidth)} ${'-'.repeat(authorWidth)} ---`;
132
+ const body = rows.map(row => `${row.id.padEnd(idWidth)} ${row.status.padEnd(statusWidth)} ${row.type.padEnd(typeWidth)} ` +
133
+ `${row.changes.padEnd(changesWidth)} ${row.author.padEnd(authorWidth)} ${row.age}`);
134
+ return [header, divider, ...body].join('\n');
135
+ }
76
136
  // repo_merge manifests (ALI-972) render the compare summary from stored evidence
77
137
  // — no live GitHub call — so a reviewer can approve informed without a PR.
78
138
  function repoMergeShow(manifest) {
@@ -108,7 +168,7 @@ export async function manifestsCommand(args, io) {
108
168
  const response = await fetcherFrom(io)(`/api/cli/promotion-manifests${query}`);
109
169
  const data = await requireOk(response, 'Manifest list');
110
170
  const manifests = Array.isArray(data.manifests) ? data.manifests : [];
111
- emit(io, { manifests }, ['promotion manifests', ...manifests.map(manifestLine)].join('\n'));
171
+ emit(io, { manifests }, manifestsTable(manifests, Date.now()));
112
172
  return 0;
113
173
  }
114
174
  if (subcommand === 'show') {
@@ -135,7 +195,23 @@ export async function manifestsCommand(args, io) {
135
195
  });
136
196
  const data = await requireOk(response, `Manifest ${subcommand}`);
137
197
  const manifest = (data.manifest ?? {});
138
- emit(io, { manifest }, manifestLine(manifest));
198
+ const lines = [manifestLine(manifest)];
199
+ // Reject reaps the session branch best-effort; a failed reap must never be
200
+ // silent (ALI-1043) — surface it with the exact cleanup command.
201
+ if (subcommand === 'reject' && manifest.actionType === 'repo_merge') {
202
+ const outcome = asRecord(manifest.outcome);
203
+ const branch = asRecord(manifest.proposedState).branch;
204
+ if (outcome.branchDeleted === true) {
205
+ lines.push('session branch deleted');
206
+ }
207
+ else if (typeof branch === 'string' && branch.length > 0) {
208
+ lines.push(`warning: the session branch could not be deleted — remove it with: git push origin --delete ${branch}`);
209
+ }
210
+ else {
211
+ lines.push('warning: the session branch could not be deleted (branch name unavailable)');
212
+ }
213
+ }
214
+ emit(io, { manifest }, lines.join('\n'));
139
215
  return 0;
140
216
  }
141
217
  io.print('Usage: orizu manifests <list|show|approve|reject|apply> ... [--json]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,7 @@ import type { SandboxSession } from './sandbox-provider.js'
27
27
  import { sweepForTokenResidue, type HygieneFinding } from './daytona-slice.js'
28
28
  import { redactSecrets } from './secret-redaction.js'
29
29
  import {
30
+ AGENT_GIT_IDENTITY,
30
31
  BEARER_BASENAME,
31
32
  BOOT_CONTEXT_BASENAME,
32
33
  DEFAULT_CACHE_REFRESH_BUFFER_MS,
@@ -410,6 +411,11 @@ export async function bootstrapHostedSandbox(opts: HostedBootstrapOptions): Prom
410
411
  // Persist the helper repo-LOCAL (never --global) for subsequent fetch/push.
411
412
  await session.exec(`git -C ${workspaceDir} config credential.helper ${shellSingleQuote(helperValue)}`)
412
413
  await session.exec(`git -C ${workspaceDir} config credential.useHttpPath true`)
414
+ // Repo-LOCAL agent identity: the sandbox has no ambient git identity, so
415
+ // without this every agent `git commit` dies with "Author identity unknown"
416
+ // (ALI-1036, found live in QA-3). Same identity the loop stamps on events.
417
+ await session.exec(`git -C ${workspaceDir} config user.name ${shellSingleQuote(AGENT_GIT_IDENTITY.name)}`)
418
+ await session.exec(`git -C ${workspaceDir} config user.email ${shellSingleQuote(AGENT_GIT_IDENTITY.email)}`)
413
419
  record('repo_cloned', true, `cloned ${opts.sessionBranch}`)
414
420
  await sink.append('repo_cloned', { branch: opts.sessionBranch, workspaceDir })
415
421
 
@@ -35,6 +35,15 @@ export const DEFAULT_CACHE_REFRESH_BUFFER_MS = 5 * 60 * 1000
35
35
  * `.openinspect/setup.sh` — fresh-boot only, non-fatal, output captured). */
36
36
  export const SETUP_HOOK_RELATIVE_PATH = '.orizu/setup.sh'
37
37
 
38
+ /**
39
+ * The git author identity for hosted-agent commits. Bootstrap configures it
40
+ * repo-LOCAL in the sandbox clone (never --global) so agent `git commit` works
41
+ * out of the box — the sandbox has no ambient identity and commits died with
42
+ * "Author identity unknown" (ALI-1036, found live in QA-3). The loop uses the
43
+ * same identity for run-event attribution; keep them identical.
44
+ */
45
+ export const AGENT_GIT_IDENTITY = { name: 'Orizu Agent', email: 'agent@orizu.ai' } as const
46
+
38
47
  // -- Pre-baked runtime marker (ALI-1017) -------------------------------------
39
48
 
40
49
  /**
@@ -49,7 +49,7 @@ import {
49
49
  type HostedRuntimePaths,
50
50
  } from './hosted-bootstrap.js'
51
51
  import { resumeRunEventSink } from './hosted-run-event-sink.js'
52
- import { BEARER_BASENAME } from './hosted-runtime-assets.js'
52
+ import { AGENT_GIT_IDENTITY, BEARER_BASENAME } from './hosted-runtime-assets.js'
53
53
  import { createVercelProvider } from './vercel-sandbox-provider.js'
54
54
  import { buildEgressPolicy, DEFAULT_EGRESS_CANARY_HOST } from './egress-policy.js'
55
55
  import { hostedLoopCommand, type HostedLoopContext } from './hosted-loop.js'
@@ -534,7 +534,7 @@ export async function startHostedSession(
534
534
  model,
535
535
  reasoningEffort: opts.reasoningEffort,
536
536
  messageId: `${runId}:task`,
537
- author: { name: 'Orizu Agent', email: 'agent@orizu.ai' },
537
+ author: AGENT_GIT_IDENTITY,
538
538
  anthropicDummyKey: opts.modelApiKey ? ANTHROPIC_DUMMY_KEY : undefined,
539
539
  // ALI-1017: skip the in-sandbox opencode install when the image ships it.
540
540
  prebaked: prebakedImage,
@@ -93,6 +93,71 @@ function num(value: unknown): number {
93
93
  return typeof value === 'number' ? value : 0
94
94
  }
95
95
 
96
+ // -- list table (ALI-1038) ----------------------------------------------------
97
+
98
+ /** "3m" / "4h" / "2d" — enough resolution to pick a manifest out of a list. */
99
+ function relativeAge(iso: unknown, now: number): string {
100
+ if (typeof iso !== 'string') return '-'
101
+ const then = Date.parse(iso)
102
+ if (Number.isNaN(then)) return '-'
103
+ const mins = Math.max(0, Math.floor((now - then) / 60_000))
104
+ if (mins < 60) return `${mins}m`
105
+ const hours = Math.floor(mins / 60)
106
+ if (hours < 48) return `${hours}h`
107
+ return `${Math.floor(hours / 24)}d`
108
+ }
109
+
110
+ /** One-glance change summary from the stored proposedState — no live API call. */
111
+ function changesSummary(manifest: Record<string, unknown>): string {
112
+ if (manifest.actionType !== 'repo_merge') return '-'
113
+ const proposed = asRecord(manifest.proposedState)
114
+ const files = num(proposed.filesChanged)
115
+ const parts = `${files} file${files === 1 ? '' : 's'} +${num(proposed.additions)}/-${num(proposed.deletions)}`
116
+ // Instruction files steer agent behavior — reviewers should see the flag in the list.
117
+ return proposed.touches_instruction_files === true ? `${parts} !instr` : parts
118
+ }
119
+
120
+ /** Reviewable-first (pending_approval, then draft), newest first within a group. */
121
+ function listOrder(a: Record<string, unknown>, b: Record<string, unknown>): number {
122
+ const rank = (m: Record<string, unknown>): number =>
123
+ m.status === 'pending_approval' ? 0 : m.status === 'draft' ? 1 : 2
124
+ const byRank = rank(a) - rank(b)
125
+ if (byRank !== 0) return byRank
126
+ const created = (m: Record<string, unknown>): number =>
127
+ typeof m.createdAt === 'string' ? Date.parse(m.createdAt) || 0 : 0
128
+ return created(b) - created(a)
129
+ }
130
+
131
+ /** Render manifests as the standard padEnd table (matches teams/projects list). */
132
+ function manifestsTable(manifests: Record<string, unknown>[], now: number): string {
133
+ if (manifests.length === 0) return 'promotion manifests\n(none)'
134
+ const rows = [...manifests].sort(listOrder).map(manifest => ({
135
+ id: typeof manifest.id === 'string' ? manifest.id : '(unknown)',
136
+ status: typeof manifest.status === 'string' ? manifest.status : '(unknown)',
137
+ type: typeof manifest.actionType === 'string' ? manifest.actionType : '(unknown)',
138
+ changes: changesSummary(manifest),
139
+ author: typeof manifest.authorActorType === 'string' ? manifest.authorActorType : '-',
140
+ age: relativeAge(manifest.createdAt, now),
141
+ }))
142
+ const idWidth = Math.max('ID'.length, ...rows.map(row => row.id.length))
143
+ const statusWidth = Math.max('STATUS'.length, ...rows.map(row => row.status.length))
144
+ const typeWidth = Math.max('TYPE'.length, ...rows.map(row => row.type.length))
145
+ const changesWidth = Math.max('CHANGES'.length, ...rows.map(row => row.changes.length))
146
+ const authorWidth = Math.max('AUTHOR'.length, ...rows.map(row => row.author.length))
147
+ const header =
148
+ `${'ID'.padEnd(idWidth)} ${'STATUS'.padEnd(statusWidth)} ${'TYPE'.padEnd(typeWidth)} ` +
149
+ `${'CHANGES'.padEnd(changesWidth)} ${'AUTHOR'.padEnd(authorWidth)} AGE`
150
+ const divider =
151
+ `${'-'.repeat(idWidth)} ${'-'.repeat(statusWidth)} ${'-'.repeat(typeWidth)} ` +
152
+ `${'-'.repeat(changesWidth)} ${'-'.repeat(authorWidth)} ---`
153
+ const body = rows.map(
154
+ row =>
155
+ `${row.id.padEnd(idWidth)} ${row.status.padEnd(statusWidth)} ${row.type.padEnd(typeWidth)} ` +
156
+ `${row.changes.padEnd(changesWidth)} ${row.author.padEnd(authorWidth)} ${row.age}`
157
+ )
158
+ return [header, divider, ...body].join('\n')
159
+ }
160
+
96
161
  // repo_merge manifests (ALI-972) render the compare summary from stored evidence
97
162
  // — no live GitHub call — so a reviewer can approve informed without a PR.
98
163
  function repoMergeShow(manifest: Record<string, unknown>): string {
@@ -130,7 +195,7 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
130
195
  const response = await fetcherFrom(io)(`/api/cli/promotion-manifests${query}`)
131
196
  const data = await requireOk(response, 'Manifest list')
132
197
  const manifests = Array.isArray(data.manifests) ? (data.manifests as Record<string, unknown>[]) : []
133
- emit(io, { manifests }, ['promotion manifests', ...manifests.map(manifestLine)].join('\n'))
198
+ emit(io, { manifests }, manifestsTable(manifests, Date.now()))
134
199
  return 0
135
200
  }
136
201
 
@@ -159,7 +224,23 @@ export async function manifestsCommand(args: string[], io: ManifestsCommandIo):
159
224
  })
160
225
  const data = await requireOk(response, `Manifest ${subcommand}`)
161
226
  const manifest = (data.manifest ?? {}) as Record<string, unknown>
162
- emit(io, { manifest }, manifestLine(manifest))
227
+ const lines = [manifestLine(manifest)]
228
+ // Reject reaps the session branch best-effort; a failed reap must never be
229
+ // silent (ALI-1043) — surface it with the exact cleanup command.
230
+ if (subcommand === 'reject' && manifest.actionType === 'repo_merge') {
231
+ const outcome = asRecord(manifest.outcome)
232
+ const branch = asRecord(manifest.proposedState).branch
233
+ if (outcome.branchDeleted === true) {
234
+ lines.push('session branch deleted')
235
+ } else if (typeof branch === 'string' && branch.length > 0) {
236
+ lines.push(
237
+ `warning: the session branch could not be deleted — remove it with: git push origin --delete ${branch}`
238
+ )
239
+ } else {
240
+ lines.push('warning: the session branch could not be deleted (branch name unavailable)')
241
+ }
242
+ }
243
+ emit(io, { manifest }, lines.join('\n'))
163
244
  return 0
164
245
  }
165
246