orizu 0.5.10 → 0.5.12
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/dist/credentials.js +37 -0
- package/dist/daytona-slice-rehearsal.js +2 -2
- package/dist/deploy-key-merge-core.js +230 -0
- package/dist/git-credential.js +10 -7
- package/dist/github-setup.js +18 -2
- package/dist/help.js +16 -11
- package/dist/hosted-boot.js +214 -18
- package/dist/hosted-bootstrap.js +18 -40
- package/dist/hosted-harness-opencode.js +4 -2
- package/dist/hosted-headless.js +17 -6
- package/dist/hosted-loop.js +43 -2
- package/dist/hosted-run-event-sink.js +49 -10
- package/dist/hosted-session-cli.js +10 -1
- package/dist/hosted-skill-staging.js +89 -0
- package/dist/index.js +79 -74
- package/dist/manifests-cli.js +84 -7
- package/dist/merge-job.js +325 -0
- package/dist/preview-runtime.js +21 -4
- package/dist/prompt-draft-push.js +95 -0
- package/dist/report-comments-cli.js +9 -2
- package/dist/sandbox-provider.js +8 -2
- package/dist/scorer-draft-push.js +110 -0
- package/dist/workbench-cli.js +312 -8
- package/dist/workspace-sync.js +9 -2
- package/dist/workspace.js +15 -43
- package/dist/zip-draft-push.js +125 -0
- package/package.json +7 -9
- package/src/credentials.ts +40 -0
- package/src/daytona-slice-rehearsal.ts +2 -2
- package/src/deploy-key-merge-core.ts +326 -0
- package/src/git-credential.ts +10 -7
- package/src/github-setup.ts +24 -2
- package/src/help.ts +18 -11
- package/src/hosted-boot.ts +249 -17
- package/src/hosted-bootstrap.ts +19 -43
- package/src/hosted-harness-opencode.ts +4 -2
- package/src/hosted-headless.ts +26 -8
- package/src/hosted-loop-lifecycle.ts +8 -0
- package/src/hosted-loop.ts +53 -7
- package/src/hosted-run-event-sink.ts +58 -11
- package/src/hosted-session-cli.ts +10 -1
- package/src/hosted-skill-staging.ts +126 -0
- package/src/index.ts +100 -86
- package/src/manifests-cli.ts +110 -11
- package/src/merge-job.ts +462 -0
- package/src/preview-runtime.ts +27 -4
- package/src/prompt-draft-push.ts +143 -0
- package/src/report-comments-cli.ts +10 -2
- package/src/sandbox-provider.ts +8 -2
- package/src/scorer-draft-push.ts +155 -0
- package/src/workbench-cli.ts +426 -10
- package/src/workspace-sync.ts +9 -2
- package/src/workspace.ts +15 -45
- package/src/zip-draft-push.ts +177 -0
- package/vendor/skills/orizu-cli/SKILL.md +1 -0
- package/vendor/skills/orizu-cli/references/building-apps.md +2 -0
- package/vendor/skills/orizu-cli/references/cli-reference.md +1 -0
package/dist/credentials.js
CHANGED
|
@@ -206,6 +206,43 @@ export function resolveEnvBearerToken() {
|
|
|
206
206
|
}
|
|
207
207
|
return null;
|
|
208
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* THE uniform bearer resolution for every CLI command (ALI-1090). Exactly the
|
|
211
|
+
* order `authedFetch` uses: `ORIZU_TOKEN` env → `ORIZU_TOKEN_FILE` (both via
|
|
212
|
+
* resolveEnvBearerToken, read FRESH per call — never cached, so hosted token
|
|
213
|
+
* rotations are picked up) → stored credentials.json. Commands that need a raw
|
|
214
|
+
* bearer (`orizu env`, the run-gepa wrapper) MUST use this instead of reading
|
|
215
|
+
* credentials.json directly, or they break in hosted sandboxes that are
|
|
216
|
+
* pre-authenticated via ORIZU_TOKEN_FILE and never ran `orizu login`.
|
|
217
|
+
*/
|
|
218
|
+
export function resolveAuthTokenForBaseUrl(baseUrl) {
|
|
219
|
+
const envBearer = resolveEnvBearerToken();
|
|
220
|
+
if (envBearer) {
|
|
221
|
+
return envBearer;
|
|
222
|
+
}
|
|
223
|
+
const credentials = getServerCredentials(baseUrl);
|
|
224
|
+
if (!credentials) {
|
|
225
|
+
throw new Error(`Not logged in for ${baseUrl}. Run \`orizu login --server ${baseUrl}\` (or \`--local\`) first.`);
|
|
226
|
+
}
|
|
227
|
+
return 'accessToken' in credentials ? credentials.accessToken : credentials.apiKey;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Non-throwing "is any auth available?" check (login --no-prompt-if-logged-in,
|
|
231
|
+
* setup status). Mirrors resolveAuthTokenForBaseUrl's order so an env bearer
|
|
232
|
+
* counts as signed in. A misconfigured ORIZU_TOKEN_FILE reports false here —
|
|
233
|
+
* actual requests still fail loudly via resolveEnvBearerToken.
|
|
234
|
+
*/
|
|
235
|
+
export function hasResolvableAuth(baseUrl) {
|
|
236
|
+
try {
|
|
237
|
+
if (resolveEnvBearerToken()) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// fall through to stored credentials
|
|
243
|
+
}
|
|
244
|
+
return getServerCredentials(baseUrl) !== null;
|
|
245
|
+
}
|
|
209
246
|
export function setActiveBaseUrl(baseUrl) {
|
|
210
247
|
const config = loadCredentialsConfigForWrite();
|
|
211
248
|
config.activeBaseUrl = baseUrl;
|
|
@@ -51,17 +51,17 @@ export function seedLocalBareRepo(baseDir, config) {
|
|
|
51
51
|
const work = mkdtempSync(join(baseDir, 'seed-'));
|
|
52
52
|
gitOrThrow(['init', '--initial-branch=main', work]);
|
|
53
53
|
const ident = ['-c', 'user.email=seed@orizu.local', '-c', 'user.name=Seed'];
|
|
54
|
+
// ALI-1075: no `canonical`/`repoState` liveness fields — manifests carry
|
|
55
|
+
// machine-readable ids only; the DB label is the sole production pointer.
|
|
54
56
|
const teamManifest = {
|
|
55
57
|
schemaVersion: WORKSPACE_SCHEMA_VERSION,
|
|
56
58
|
kind: 'team',
|
|
57
59
|
slug: config.teamSlug,
|
|
58
|
-
canonical: { owner: 'repo', repoState: 'source', objectRef: null },
|
|
59
60
|
};
|
|
60
61
|
const projectManifest = {
|
|
61
62
|
schemaVersion: WORKSPACE_SCHEMA_VERSION,
|
|
62
63
|
kind: 'project',
|
|
63
64
|
slug: config.projectSlug,
|
|
64
|
-
canonical: { owner: 'repo', repoState: 'draft', objectRef: null },
|
|
65
65
|
};
|
|
66
66
|
writeFileSync(join(work, 'orizu.team.json'), `${JSON.stringify(teamManifest, null, 2)}\n`);
|
|
67
67
|
writeFileSync(join(work, '.gitignore'), '# Orizu workspace policy\n.orizu/\n');
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport-agnostic deploy-key merge core (ALI-1084, merge-sandbox-job
|
|
3
|
+
* Phase 0). Extracted VERBATIM from `lib/git-ssh-merge.ts` so both the server
|
|
4
|
+
* (`lib/`) and the published CLI (`orizu internal merge-job`, Phase 2) run the
|
|
5
|
+
* EXACT SAME merge logic — no forked implementations, one place where the
|
|
6
|
+
* ADR-007 §4 `--no-ff` invariant is asserted on the exact argv.
|
|
7
|
+
*
|
|
8
|
+
* DELIBERATELY NO `import 'server-only'`: this module runs inside the one-shot
|
|
9
|
+
* merge sandbox (a plain Node process) as well as on the server. It must not
|
|
10
|
+
* import anything outside the CLI package root (bun-bundled) — node builtins
|
|
11
|
+
* only. `lib/git-ssh-merge.ts` re-exports everything here so every existing
|
|
12
|
+
* server import path and test keeps working unchanged.
|
|
13
|
+
*
|
|
14
|
+
* SECURITY PROPERTIES (unchanged from the lib original):
|
|
15
|
+
* - throwaway temp worktree + isolated SSH identity (key file mode 0600);
|
|
16
|
+
* - pinned GitHub host keys (StrictHostKeyChecking=yes, own known_hosts);
|
|
17
|
+
* - expected-head verification: the merge targets the REVIEWED SHA, never a
|
|
18
|
+
* branch name;
|
|
19
|
+
* - `merge-base --is-ancestor` alreadyMerged idempotency check;
|
|
20
|
+
* - argv-asserted commit-preserving `--no-ff` merge (ADR-007 §4).
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from 'node:child_process';
|
|
23
|
+
import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
24
|
+
import { tmpdir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
/**
|
|
27
|
+
* ADR-007 §4 merge-strategy invariant: the seal merge must stay a COMMIT-
|
|
28
|
+
* PRESERVING merge. `--no-ff` keeps the session-branch head as a parent of the
|
|
29
|
+
* merge commit, so every create-time `commit_sha` pinned by a draft version
|
|
30
|
+
* row stays reachable on the default branch forever. A squash or rebase would
|
|
31
|
+
* rewrite/discard those commits and orphan the pins — "what the label says is
|
|
32
|
+
* live" could then dangle. These flags must never appear in the seal merge.
|
|
33
|
+
*/
|
|
34
|
+
const FORBIDDEN_SEAL_MERGE_FLAGS = ['--squash', '--ff-only', '--ff', 'rebase'];
|
|
35
|
+
const REQUIRED_SEAL_MERGE_FLAG = '--no-ff';
|
|
36
|
+
/**
|
|
37
|
+
* Assert a git invocation is the commit-preserving seal merge ADR-007 §4
|
|
38
|
+
* requires. Called on the exact argv the merge runs with, so the invariant is
|
|
39
|
+
* enforced where the merge happens rather than by reviewer vigilance.
|
|
40
|
+
*/
|
|
41
|
+
export function assertCommitPreservingMergeArgs(args) {
|
|
42
|
+
if (!args.includes('merge')) {
|
|
43
|
+
throw new Error('seal merge invariant: expected a git merge invocation');
|
|
44
|
+
}
|
|
45
|
+
if (!args.includes(REQUIRED_SEAL_MERGE_FLAG)) {
|
|
46
|
+
throw new Error(`seal merge invariant violated: ${REQUIRED_SEAL_MERGE_FLAG} is required (ADR-007 §4 — the merge must keep the session head as a parent)`);
|
|
47
|
+
}
|
|
48
|
+
for (const flag of FORBIDDEN_SEAL_MERGE_FLAGS) {
|
|
49
|
+
if (args.includes(flag)) {
|
|
50
|
+
throw new Error(`seal merge invariant violated: ${flag} would rewrite or drop session commits and orphan draft version pins (ADR-007 §4)`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const LOCAL_BASE_REF = 'refs/heads/__orizu_merge_base';
|
|
55
|
+
const LOCAL_SOURCE_REF = 'refs/remotes/origin/__orizu_merge_source';
|
|
56
|
+
// GitHub's published SSH host keys:
|
|
57
|
+
// https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
|
|
58
|
+
const GITHUB_KNOWN_HOSTS = [
|
|
59
|
+
'github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl',
|
|
60
|
+
'github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=',
|
|
61
|
+
'github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=',
|
|
62
|
+
].join('\n') + '\n';
|
|
63
|
+
/**
|
|
64
|
+
* Strip git's repo-context env vars from a child git's environment. Git
|
|
65
|
+
* EXPORTS these into hook subprocesses (from a linked worktree, GIT_DIR is an
|
|
66
|
+
* ABSOLUTE path into the shared .git), so any descendant git command that
|
|
67
|
+
* inherits the parent env operates on THAT repo instead of its own cwd —
|
|
68
|
+
* `git config` writes land in the shared .git/config and `git init` flips it
|
|
69
|
+
* to core.bare=true (both observed when the pre-push hook ran the test suite
|
|
70
|
+
* from a worktree). Every real-git spawn must build its env through this.
|
|
71
|
+
*/
|
|
72
|
+
export function scrubGitRepoContextEnv(env = process.env) {
|
|
73
|
+
const scrubbed = { ...env };
|
|
74
|
+
delete scrubbed.GIT_DIR;
|
|
75
|
+
delete scrubbed.GIT_WORK_TREE;
|
|
76
|
+
delete scrubbed.GIT_INDEX_FILE;
|
|
77
|
+
delete scrubbed.GIT_OBJECT_DIRECTORY;
|
|
78
|
+
delete scrubbed.GIT_COMMON_DIR;
|
|
79
|
+
delete scrubbed.GIT_NAMESPACE;
|
|
80
|
+
return scrubbed;
|
|
81
|
+
}
|
|
82
|
+
// Exported: the ADR-007 P3 read path (lib/artifact-git-read.ts) runs its
|
|
83
|
+
// verified reads through the same spawn.
|
|
84
|
+
export function defaultRunGit(args, options) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const child = spawn('git', args, {
|
|
87
|
+
cwd: options.cwd,
|
|
88
|
+
env: options.env,
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
});
|
|
91
|
+
let stdout = '';
|
|
92
|
+
let stderr = '';
|
|
93
|
+
child.stdout.setEncoding('utf8');
|
|
94
|
+
child.stderr.setEncoding('utf8');
|
|
95
|
+
child.stdout.on('data', chunk => { stdout += chunk; });
|
|
96
|
+
child.stderr.on('data', chunk => { stderr += chunk; });
|
|
97
|
+
child.on('error', reject);
|
|
98
|
+
child.on('close', code => resolve({ exitCode: code ?? 1, stdout, stderr }));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function sshRemote(repoFullName) {
|
|
102
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repoFullName)) {
|
|
103
|
+
throw new Error(`Invalid repo full name: ${repoFullName}`);
|
|
104
|
+
}
|
|
105
|
+
return `git@github.com:${repoFullName}.git`;
|
|
106
|
+
}
|
|
107
|
+
function shellQuote(value) {
|
|
108
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
109
|
+
}
|
|
110
|
+
/** Exported for `lib/git-ssh-merge.ts` (pushFilesWithDeployKey keeps living
|
|
111
|
+
* there — D9 scopes the push path as a follow-on, not part of the core). */
|
|
112
|
+
export function assertGitOk(result, operation) {
|
|
113
|
+
if (result.exitCode === 0)
|
|
114
|
+
return;
|
|
115
|
+
const detail = (result.stderr || result.stdout).trim();
|
|
116
|
+
throw new Error(detail ? `${operation} failed: ${detail}` : `${operation} failed`);
|
|
117
|
+
}
|
|
118
|
+
function looksLikeMergeConflict(result) {
|
|
119
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
120
|
+
return /CONFLICT|Automatic merge failed|Merge conflict/i.test(output);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Run a callback inside a throwaway git worktree wired to the repo's merge
|
|
124
|
+
* deploy key (isolated SSH identity + pinned GitHub host keys). Exported for
|
|
125
|
+
* the ADR-007 read path (lib/artifact-git-read.ts), which reuses the same
|
|
126
|
+
* clone/pack infrastructure for bulk artifact reads.
|
|
127
|
+
*/
|
|
128
|
+
export async function withDeployKeyGit(privateKeyPem, runGit, tempPrefix, callback) {
|
|
129
|
+
const tempRoot = await mkdtemp(join(tmpdir(), tempPrefix));
|
|
130
|
+
const dir = join(tempRoot, 'worktree');
|
|
131
|
+
const sshDir = join(tempRoot, 'ssh');
|
|
132
|
+
await mkdir(dir);
|
|
133
|
+
await mkdir(sshDir);
|
|
134
|
+
const keyPath = join(sshDir, 'merge_deploy_key');
|
|
135
|
+
const knownHostsPath = join(sshDir, 'known_hosts');
|
|
136
|
+
const env = {
|
|
137
|
+
// Scrubbed: a leaked GIT_DIR (git exports it into hooks) must never
|
|
138
|
+
// redirect this temp-worktree clone/merge onto another repo.
|
|
139
|
+
...scrubGitRepoContextEnv(process.env),
|
|
140
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
141
|
+
GIT_SSH_COMMAND: [
|
|
142
|
+
'ssh',
|
|
143
|
+
'-i',
|
|
144
|
+
shellQuote(keyPath),
|
|
145
|
+
'-o',
|
|
146
|
+
'IdentitiesOnly=yes',
|
|
147
|
+
'-o',
|
|
148
|
+
'StrictHostKeyChecking=yes',
|
|
149
|
+
'-o',
|
|
150
|
+
shellQuote(`UserKnownHostsFile=${knownHostsPath}`),
|
|
151
|
+
].join(' '),
|
|
152
|
+
};
|
|
153
|
+
const git = (args) => runGit(args, { cwd: dir, env });
|
|
154
|
+
const gitOk = async (args, operation) => {
|
|
155
|
+
const result = await git(args);
|
|
156
|
+
assertGitOk(result, operation);
|
|
157
|
+
return result;
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
await writeFile(keyPath, privateKeyPem, { mode: 0o600 });
|
|
161
|
+
await chmod(keyPath, 0o600);
|
|
162
|
+
await writeFile(knownHostsPath, GITHUB_KNOWN_HOSTS, { mode: 0o600 });
|
|
163
|
+
return await callback({ dir, git, gitOk });
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Merge a reviewed session/revert branch head into the default branch using the
|
|
171
|
+
* server-only merge deploy key. The source branch is fetched only so GitHub can
|
|
172
|
+
* prove the reviewed SHA is still the branch tip; the merge itself targets the
|
|
173
|
+
* expected SHA, preserving the manifest CAS guarantee.
|
|
174
|
+
*/
|
|
175
|
+
export async function mergeBranchWithDeployKey(repoFullName, input, deps = {}) {
|
|
176
|
+
const runGit = deps.runGit ?? defaultRunGit;
|
|
177
|
+
return withDeployKeyGit(input.privateKeyPem, runGit, 'orizu-merge-', async ({ git, gitOk }) => {
|
|
178
|
+
await gitOk(['init'], 'git init');
|
|
179
|
+
await gitOk(['remote', 'add', 'origin', sshRemote(repoFullName)], 'git remote add');
|
|
180
|
+
await gitOk(['fetch', '--no-tags', 'origin', `refs/heads/${input.base}:${LOCAL_BASE_REF}`], 'git fetch default branch');
|
|
181
|
+
await gitOk(['checkout', '-B', '__orizu_merge_base', LOCAL_BASE_REF], 'git checkout default branch');
|
|
182
|
+
await gitOk(['fetch', '--no-tags', 'origin', `refs/heads/${input.sourceBranch}:${LOCAL_SOURCE_REF}`], 'git fetch reviewed source branch');
|
|
183
|
+
const sourceHead = (await gitOk(['rev-parse', LOCAL_SOURCE_REF], 'git rev-parse source')).stdout.trim();
|
|
184
|
+
if (sourceHead !== input.expectedHeadSha) {
|
|
185
|
+
throw new Error('branch moved since manifest creation; re-finish the session');
|
|
186
|
+
}
|
|
187
|
+
const ancestor = await git(['merge-base', '--is-ancestor', input.expectedHeadSha, 'HEAD']);
|
|
188
|
+
if (ancestor.exitCode === 0) {
|
|
189
|
+
const head = (await gitOk(['rev-parse', 'HEAD'], 'git rev-parse default')).stdout.trim();
|
|
190
|
+
return { merged: false, sha: head, alreadyMerged: true, conflict: false, status: 204 };
|
|
191
|
+
}
|
|
192
|
+
if (ancestor.exitCode !== 1) {
|
|
193
|
+
assertGitOk(ancestor, 'git merge-base');
|
|
194
|
+
}
|
|
195
|
+
// ADR-007 §4: the seal is a commit-preserving --no-ff merge — never squash,
|
|
196
|
+
// never rebase — so every draft version's create-time commit_sha stays
|
|
197
|
+
// reachable once the branch reaches the default branch.
|
|
198
|
+
const mergeArgs = [
|
|
199
|
+
'-c',
|
|
200
|
+
'user.name=Orizu',
|
|
201
|
+
'-c',
|
|
202
|
+
'user.email=bot@orizu.ai',
|
|
203
|
+
'merge',
|
|
204
|
+
'--no-ff',
|
|
205
|
+
'-m',
|
|
206
|
+
input.commitMessage,
|
|
207
|
+
input.expectedHeadSha,
|
|
208
|
+
];
|
|
209
|
+
assertCommitPreservingMergeArgs(mergeArgs);
|
|
210
|
+
const merge = await git(mergeArgs);
|
|
211
|
+
if (merge.exitCode !== 0) {
|
|
212
|
+
if (looksLikeMergeConflict(merge)) {
|
|
213
|
+
return { merged: false, sha: null, alreadyMerged: false, conflict: true, status: 409 };
|
|
214
|
+
}
|
|
215
|
+
assertGitOk(merge, 'git merge');
|
|
216
|
+
}
|
|
217
|
+
const mergeSha = (await gitOk(['rev-parse', 'HEAD'], 'git rev-parse merge')).stdout.trim();
|
|
218
|
+
// D8 pre-push gate: the LAST check before anything reaches the default
|
|
219
|
+
// branch. A refused gate aborts with superseded — no push, no side effect
|
|
220
|
+
// (the temp worktree dies with this callback).
|
|
221
|
+
if (deps.prePushGate) {
|
|
222
|
+
const proceed = await deps.prePushGate();
|
|
223
|
+
if (!proceed) {
|
|
224
|
+
return { merged: false, sha: null, alreadyMerged: false, conflict: false, superseded: true, status: 409 };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
await gitOk(['push', 'origin', `HEAD:refs/heads/${input.base}`], 'git push default branch');
|
|
228
|
+
return { merged: true, sha: mergeSha, alreadyMerged: false, conflict: false, status: 201 };
|
|
229
|
+
});
|
|
230
|
+
}
|
package/dist/git-credential.js
CHANGED
|
@@ -45,17 +45,20 @@ export function resolveAttachedWorkspaceId(cwd) {
|
|
|
45
45
|
const manifestPath = join(dir, 'orizu.team.json');
|
|
46
46
|
if (existsSync(manifestPath)) {
|
|
47
47
|
const manifest = readJsonManifest(manifestPath);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
: null;
|
|
52
|
-
if (typeof serviceId === 'string' && serviceId)
|
|
53
|
-
return serviceId;
|
|
48
|
+
// `setup.attachedWorkspaceId` is the attachment id; `canonical.serviceId`
|
|
49
|
+
// is a legacy duplicate kept only as a read fallback for old repos
|
|
50
|
+
// (ALI-1075: fresh manifests carry no `canonical` block).
|
|
54
51
|
const setup = manifest?.setup;
|
|
55
52
|
const attached = setup && typeof setup === 'object' && !Array.isArray(setup)
|
|
56
53
|
? setup.attachedWorkspaceId
|
|
57
54
|
: null;
|
|
58
|
-
|
|
55
|
+
if (typeof attached === 'string' && attached)
|
|
56
|
+
return attached;
|
|
57
|
+
const canonical = manifest?.canonical;
|
|
58
|
+
const serviceId = canonical && typeof canonical === 'object' && !Array.isArray(canonical)
|
|
59
|
+
? canonical.serviceId
|
|
60
|
+
: null;
|
|
61
|
+
return typeof serviceId === 'string' && serviceId ? serviceId : null;
|
|
59
62
|
}
|
|
60
63
|
const parent = dirname(dir);
|
|
61
64
|
if (parent === dir)
|
package/dist/github-setup.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* is immediately rewritten to a clean origin.
|
|
15
15
|
*/
|
|
16
16
|
import { spawnSync } from 'child_process';
|
|
17
|
-
import { mkdirSync } from 'fs';
|
|
17
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
18
18
|
import { join } from 'path';
|
|
19
19
|
import { authedFetch } from './http.js';
|
|
20
20
|
import { assertWorkspaceDirUsable } from './workspace.js';
|
|
@@ -196,6 +196,23 @@ export async function runHostedAttach(options, io) {
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
const cleanUrl = `https://github.com/${repoFullName}.git`;
|
|
199
|
+
const ensureOrizuDir = io.ensureOrizuDir ?? ((repoDir) => mkdirSync(join(repoDir, '.orizu'), { recursive: true }));
|
|
200
|
+
// Idempotent re-run (ALI-1069): `orizu setup` in an ALREADY-attached workspace
|
|
201
|
+
// reaches here with `targetDir` holding the prior clone (its `.git` plus the
|
|
202
|
+
// committed orizu.team.json — `assertWorkspaceDirUsable` deliberately exempts
|
|
203
|
+
// that dir as the idempotent case). `git clone` refuses ANY non-empty dir, so
|
|
204
|
+
// an unconditional clone would abort the re-run with a misleading "not an empty
|
|
205
|
+
// directory" error. Short-circuit: re-affirm the credential helper config
|
|
206
|
+
// (idempotent) and return without recloning.
|
|
207
|
+
const repoAlreadyCloned = io.repoAlreadyCloned ?? ((dir) => existsSync(join(dir, '.git')));
|
|
208
|
+
if (repoAlreadyCloned(options.targetDir)) {
|
|
209
|
+
io.print(' Workbench repo already attached — skipping clone.');
|
|
210
|
+
gitOrThrow(io.git, ['config', 'credential.helper', CREDENTIAL_HELPER], { cwd: options.targetDir });
|
|
211
|
+
gitOrThrow(io.git, ['config', 'credential.useHttpPath', 'true'], { cwd: options.targetDir });
|
|
212
|
+
ensureOrizuDir(options.targetDir);
|
|
213
|
+
io.print(' Attached. Configured the Orizu credential helper for git.');
|
|
214
|
+
return { repoFullName, provisioned, targetDir: options.targetDir };
|
|
215
|
+
}
|
|
199
216
|
io.print(` Cloning ${repoFullName}...`);
|
|
200
217
|
// Clone with the helper inline + the workspace id in the env so the token is
|
|
201
218
|
// brokered on demand and never materializes in the URL or on disk.
|
|
@@ -211,7 +228,6 @@ export async function runHostedAttach(options, io) {
|
|
|
211
228
|
// Persist the helper repo-locally for future pull/push (cwd-resolved).
|
|
212
229
|
gitOrThrow(io.git, ['config', 'credential.helper', CREDENTIAL_HELPER], { cwd: options.targetDir });
|
|
213
230
|
gitOrThrow(io.git, ['config', 'credential.useHttpPath', 'true'], { cwd: options.targetDir });
|
|
214
|
-
const ensureOrizuDir = io.ensureOrizuDir ?? ((repoDir) => mkdirSync(join(repoDir, '.orizu'), { recursive: true }));
|
|
215
231
|
ensureOrizuDir(options.targetDir);
|
|
216
232
|
io.print(' Attached. Configured the Orizu credential helper for git.');
|
|
217
233
|
return { repoFullName, provisioned, targetDir: options.targetDir };
|
package/dist/help.js
CHANGED
|
@@ -318,8 +318,8 @@ export const COMMAND_DOCS = [
|
|
|
318
318
|
},
|
|
319
319
|
{
|
|
320
320
|
path: ['prompts', 'push'],
|
|
321
|
-
usage: 'orizu prompts push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--json]',
|
|
322
|
-
summary: 'Push a local prompt artifact directory as a new version.',
|
|
321
|
+
usage: 'orizu prompts push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--session <session-id>] [--json]',
|
|
322
|
+
summary: 'Push a local prompt artifact directory as a new version (with --session: a commit-first git draft).',
|
|
323
323
|
group: 'Prompts and judges',
|
|
324
324
|
},
|
|
325
325
|
{
|
|
@@ -354,8 +354,8 @@ export const COMMAND_DOCS = [
|
|
|
354
354
|
},
|
|
355
355
|
{
|
|
356
356
|
path: ['judges', 'push'],
|
|
357
|
-
usage: 'orizu judges push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--json]',
|
|
358
|
-
summary: 'Push a local judge artifact directory as a new version.',
|
|
357
|
+
usage: 'orizu judges push <dir> [--runner-version <id>] [--project <team/project>] [--parent <version-id>] [--session <session-id>] [--json]',
|
|
358
|
+
summary: 'Push a local judge artifact directory as a new version (with --session: a commit-first git draft).',
|
|
359
359
|
group: 'Prompts and judges',
|
|
360
360
|
},
|
|
361
361
|
{
|
|
@@ -747,23 +747,25 @@ export const COMMAND_DOCS = [
|
|
|
747
747
|
},
|
|
748
748
|
{
|
|
749
749
|
path: ['session', 'start'],
|
|
750
|
-
usage: 'orizu session start [--project <team/project>] [--json]',
|
|
751
|
-
summary: 'Start a durable workspace session
|
|
750
|
+
usage: 'orizu session start [--project <team/project>] [--workspace <dir>] [--json]',
|
|
751
|
+
summary: 'Start a durable workspace session and print the session id. When run inside the workbench clone (or with --workspace), the remote session branch is fetched and checked out locally, and the exact next git commands are printed.',
|
|
752
752
|
group: 'Sessions',
|
|
753
753
|
options: [
|
|
754
754
|
{ name: '--project <team/project>', help: 'Optionally scope the session to a project in the attached workspace.' },
|
|
755
|
+
{ name: '--workspace <dir>', help: 'Workbench clone directory when not running from inside it.' },
|
|
755
756
|
{ name: '--json', help: 'Emit the machine-readable session payload.' },
|
|
756
757
|
],
|
|
757
758
|
examples: ['orizu session start', 'orizu session start --project highlight/hip --json'],
|
|
758
759
|
},
|
|
759
760
|
{
|
|
760
761
|
path: ['session', 'status'],
|
|
761
|
-
usage: 'orizu session status [--session <id> | --status active|ended] [--json]',
|
|
762
|
-
summary: 'Inspect one session by id
|
|
762
|
+
usage: 'orizu session status [--session <id> | --status active|ended] [--workspace <dir>] [--json]',
|
|
763
|
+
summary: 'Inspect one session by id (including run summaries and, when run inside the workbench clone, the session branch with local ahead/behind vs origin), or list sessions for the attached workspace.',
|
|
763
764
|
group: 'Sessions',
|
|
764
765
|
options: [
|
|
765
766
|
{ name: '--session <id>', help: 'Resume by session id; does not require the original terminal.' },
|
|
766
767
|
{ name: '--status <status>', help: 'Filter attached-workspace sessions when --session is omitted.', choices: ['active', 'ended'] },
|
|
768
|
+
{ name: '--workspace <dir>', help: 'Workbench clone directory when not running from inside it.' },
|
|
767
769
|
{ name: '--json', help: 'Emit the machine-readable session or session-list payload.' },
|
|
768
770
|
],
|
|
769
771
|
examples: ['orizu session status --session sess_123 --json', 'orizu session status --status active'],
|
|
@@ -781,15 +783,18 @@ export const COMMAND_DOCS = [
|
|
|
781
783
|
},
|
|
782
784
|
{
|
|
783
785
|
path: ['session', 'finish'],
|
|
784
|
-
usage: 'orizu session finish --session <id> [--project <team/project>] [--json]',
|
|
785
|
-
summary: 'Finish a hosted session branch: no changes deletes the branch; changes create a repo_merge promotion manifest to review and approve (no GitHub PR).',
|
|
786
|
+
usage: 'orizu session finish --session <id> [--project <team/project>] [--push [--message <text>]] [--workspace <dir>] [--json]',
|
|
787
|
+
summary: 'Finish a hosted session branch: no changes deletes the branch; changes create a repo_merge promotion manifest to review and approve (no GitHub PR). Warns when your local session-branch checkout has uncommitted/unpushed work; --push stages, commits, and pushes it first.',
|
|
786
788
|
group: 'Sessions',
|
|
787
789
|
options: [
|
|
788
790
|
{ name: '--session <id>', help: 'Session id whose branch to finish.', required: true },
|
|
789
791
|
{ name: '--project <team/project>', help: 'Project scope for the manifest when the session is not already project-scoped.' },
|
|
792
|
+
{ name: '--push', help: 'Opt-in: stage, commit, and push local session-branch work before finishing (raw git stays the default workflow).' },
|
|
793
|
+
{ name: '--message <text>', help: 'Commit message for --push (a default is used when omitted).' },
|
|
794
|
+
{ name: '--workspace <dir>', help: 'Workbench clone directory when not running from inside it.' },
|
|
790
795
|
{ name: '--json', help: 'Emit the machine-readable outcome (no-changes or the created manifest).' },
|
|
791
796
|
],
|
|
792
|
-
examples: ['orizu session finish --session sess_123', 'orizu session finish --session sess_123 --
|
|
797
|
+
examples: ['orizu session finish --session sess_123', 'orizu session finish --session sess_123 --push --message "tune judge prompt"'],
|
|
793
798
|
},
|
|
794
799
|
{
|
|
795
800
|
path: ['run', 'start'],
|