orizu 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +22 -13
  2. package/dist/artifact-archive.js +152 -0
  3. package/dist/connectors-cli.js +80 -0
  4. package/dist/credentials.js +4 -1
  5. package/dist/daytona-slice-rehearsal.js +255 -0
  6. package/dist/daytona-slice.js +441 -0
  7. package/dist/egress-allowlist-cli.js +132 -0
  8. package/dist/egress-policy.js +159 -0
  9. package/dist/git-credential.js +131 -0
  10. package/dist/github-setup.js +226 -0
  11. package/dist/help.js +276 -21
  12. package/dist/hosted-bootstrap-rehearsal.js +235 -0
  13. package/dist/hosted-bootstrap.js +365 -0
  14. package/dist/hosted-broker-server.js +220 -0
  15. package/dist/hosted-git-http-server.js +136 -0
  16. package/dist/hosted-harness-claude.js +493 -0
  17. package/dist/hosted-harness-opencode.js +729 -0
  18. package/dist/hosted-harness.js +60 -0
  19. package/dist/hosted-loop.js +477 -0
  20. package/dist/hosted-run-event-sink.js +499 -0
  21. package/dist/hosted-runtime-assets.js +216 -0
  22. package/dist/hosted-session-cli.js +642 -0
  23. package/dist/index.js +945 -258
  24. package/dist/kill-switch-cli.js +89 -0
  25. package/dist/manifests-cli.js +143 -0
  26. package/dist/preview-runtime.js +4 -1
  27. package/dist/sandbox-provider.js +238 -0
  28. package/dist/secret-redaction.js +112 -0
  29. package/dist/session-timeline.js +103 -0
  30. package/dist/setup-prompt.js +4 -2
  31. package/dist/skill-installer.js +9 -1
  32. package/dist/smoke.js +250 -0
  33. package/dist/task-assignment-manifest.js +96 -0
  34. package/dist/vercel-sandbox-provider.js +260 -0
  35. package/dist/workbench-cli.js +493 -0
  36. package/dist/workspace-sync.js +924 -0
  37. package/dist/workspace.js +868 -57
  38. package/package.json +4 -1
  39. package/src/artifact-archive.ts +182 -0
  40. package/src/connectors-cli.ts +115 -0
  41. package/src/credentials.ts +6 -2
  42. package/src/daytona-slice-rehearsal.ts +320 -0
  43. package/src/daytona-slice.ts +598 -0
  44. package/src/egress-allowlist-cli.ts +147 -0
  45. package/src/egress-policy.ts +188 -0
  46. package/src/git-credential.ts +155 -0
  47. package/src/github-setup.ts +366 -0
  48. package/src/help.ts +277 -21
  49. package/src/hosted-bootstrap-rehearsal.ts +312 -0
  50. package/src/hosted-bootstrap.ts +571 -0
  51. package/src/hosted-broker-server.ts +278 -0
  52. package/src/hosted-git-http-server.ts +164 -0
  53. package/src/hosted-harness-claude.ts +621 -0
  54. package/src/hosted-harness-opencode.ts +823 -0
  55. package/src/hosted-harness.ts +124 -0
  56. package/src/hosted-loop.ts +680 -0
  57. package/src/hosted-run-event-sink.ts +632 -0
  58. package/src/hosted-runtime-assets.ts +272 -0
  59. package/src/hosted-session-cli.ts +844 -0
  60. package/src/index.ts +1102 -317
  61. package/src/kill-switch-cli.ts +113 -0
  62. package/src/manifests-cli.ts +168 -0
  63. package/src/preview-runtime.ts +14 -9
  64. package/src/sandbox-provider.ts +440 -0
  65. package/src/secret-redaction.ts +124 -0
  66. package/src/session-timeline.ts +152 -0
  67. package/src/setup-prompt.ts +4 -2
  68. package/src/skill-installer.ts +13 -1
  69. package/src/smoke.ts +342 -0
  70. package/src/task-assignment-manifest.ts +164 -0
  71. package/src/vercel-sandbox-provider.ts +388 -0
  72. package/src/workbench-cli.ts +701 -0
  73. package/src/workspace-sync.ts +1148 -0
  74. package/src/workspace.ts +1051 -64
  75. package/vendor/skills/orizu-cli/SKILL.md +8 -4
  76. package/vendor/skills/orizu-cli/references/cli-reference.md +12 -4
package/dist/workspace.js CHANGED
@@ -1,76 +1,887 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
- import { join, resolve } from 'path';
1
+ import { existsSync, lstatSync, mkdirSync, readlinkSync, readdirSync, readFileSync, renameSync, symlinkSync, unlinkSync, writeFileSync, } from 'fs';
2
+ import { dirname, join, relative, resolve } from 'path';
3
3
  export const WORKSPACE_DIR_NAME = '.orizu';
4
- const WORKSPACE_README = `# Orizu workspace
4
+ export const WORKSPACE_SCHEMA_VERSION = 'orizu.workspace.v0';
5
+ export const WORKSPACE_SETUP_VERSION = '0.1.0';
6
+ export const WORKSPACE_ROOT_FILES = [
7
+ 'README.md',
8
+ 'orizu.team.json',
9
+ 'AGENTS.md',
10
+ 'CLAUDE.md',
11
+ 'Memory.md',
12
+ '.gitignore',
13
+ ];
14
+ export const WORKSPACE_ROOT_DIRS = [
15
+ WORKSPACE_DIR_NAME,
16
+ 'source-repos',
17
+ 'projects',
18
+ 'sessions',
19
+ ];
20
+ export const PROJECT_ROOT_FILES = [
21
+ 'README.md',
22
+ 'memory.md',
23
+ 'orizu.project.json',
24
+ '.gitignore',
25
+ ];
26
+ export const PROJECT_PRIMITIVE_DIRS = [
27
+ 'datasets',
28
+ 'apps',
29
+ 'tasks',
30
+ 'prompts',
31
+ 'scorers',
32
+ 'optimizations',
33
+ ];
34
+ export const REPO_STATES = [
35
+ 'source',
36
+ 'draft',
37
+ 'snapshot',
38
+ 'mirror',
39
+ 'cache',
40
+ 'object_ref_only',
41
+ ];
42
+ export const CANONICAL_OWNERS = [
43
+ 'repo',
44
+ 'orizu-db',
45
+ 'object-storage',
46
+ 'local',
47
+ ];
48
+ function hasAllowedValue(values, value) {
49
+ return typeof value === 'string' && values.includes(value);
50
+ }
51
+ function normalizeSlug(value, fallback) {
52
+ const normalized = (value || fallback)
53
+ .trim()
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9]+/g, '-')
56
+ .replace(/^-+|-+$/g, '');
57
+ return normalized || fallback;
58
+ }
59
+ function normalizeProjectSeeds(options) {
60
+ const explicitProjects = options?.projects?.length
61
+ ? options.projects
62
+ : [{ slug: options?.projectSlug || 'local-project', id: null, name: null }];
63
+ const bySlug = new Map();
64
+ for (const project of explicitProjects) {
65
+ const slug = normalizeSlug(project.slug, 'local-project');
66
+ if (!bySlug.has(slug)) {
67
+ bySlug.set(slug, {
68
+ slug,
69
+ id: project.id || null,
70
+ name: project.name || null,
71
+ });
72
+ }
73
+ }
74
+ return Array.from(bySlug.values());
75
+ }
76
+ function resolveWorkspaceRoot(options) {
77
+ return resolve(options?.workspaceRoot || options?.cwd || process.cwd());
78
+ }
79
+ export function getWorkspaceRoot(cwd) {
80
+ return resolve(cwd || process.cwd());
81
+ }
82
+ export function workspaceExists(cwd) {
83
+ return existsSync(join(getWorkspaceRoot(cwd), 'orizu.team.json'));
84
+ }
85
+ function formatJson(value) {
86
+ return `${JSON.stringify(value, null, 2)}\n`;
87
+ }
88
+ function createTeamManifest(options) {
89
+ return formatJson({
90
+ schemaVersion: WORKSPACE_SCHEMA_VERSION,
91
+ kind: 'team',
92
+ slug: options.teamSlug,
93
+ teamId: options.teamId,
94
+ serviceOrigin: options.serviceOrigin,
95
+ defaultObjectStore: {
96
+ provider: 'supabase',
97
+ bucket: 'orizu-artifacts',
98
+ },
99
+ canonical: {
100
+ owner: 'repo',
101
+ repoState: 'source',
102
+ serviceId: options.attachWorkspaceId,
103
+ versionId: null,
104
+ contentSha256: null,
105
+ lastPulledAt: null,
106
+ objectRef: null,
107
+ notes: null,
108
+ },
109
+ setup: {
110
+ setupVersion: WORKSPACE_SETUP_VERSION,
111
+ createdBy: 'orizu setup',
112
+ createdAt: new Date().toISOString(),
113
+ cliVersion: options.cliVersion,
114
+ attachedWorkspaceId: options.attachWorkspaceId,
115
+ },
116
+ });
117
+ }
118
+ function createProjectManifest(teamSlug, project) {
119
+ return formatJson({
120
+ schemaVersion: WORKSPACE_SCHEMA_VERSION,
121
+ kind: 'project',
122
+ slug: project.slug,
123
+ name: project.name || null,
124
+ projectId: project.id || null,
125
+ teamSlug,
126
+ sourceRepos: [],
127
+ policies: {
128
+ defaultRepoState: 'draft',
129
+ },
130
+ canonical: {
131
+ owner: 'repo',
132
+ repoState: 'draft',
133
+ serviceId: null,
134
+ versionId: null,
135
+ contentSha256: null,
136
+ lastPulledAt: null,
137
+ objectRef: null,
138
+ notes: null,
139
+ },
140
+ });
141
+ }
142
+ function rootReadme(teamSlug) {
143
+ return `# ${teamSlug} Orizu workbench
5
144
 
6
- Local Orizu artifacts for this repository. This folder is gitignored by
7
- default: generated and server-derived content here is not guaranteed to stay
8
- in sync with the Orizu server, so checking it in would risk drift. Safe,
9
- user-authored configuration may move to a committed location once a sync model
10
- exists.
145
+ This repository is the portable Orizu workbench for this team. Durable context,
146
+ manifests, intentional snapshots, summaries, and promotion artifacts live in
147
+ Git. Live product state remains in Orizu DB/storage unless a repo file is
148
+ explicitly applied through Orizu.
11
149
 
12
- - \`workspace.json\`: safe local metadata (no secrets; credentials stay in
13
- \`~/.config/orizu\`).
14
- - \`generated/\`: runner artifacts, exports, and other generated files.
150
+ Generated exports, local caches, raw logs, raw transcripts, bulky datasets, and
151
+ temporary runner materialization belong in ignored or object-backed locations.
15
152
  `;
16
- export function getWorkspaceRoot(cwd) {
17
- return resolve(cwd || process.cwd(), WORKSPACE_DIR_NAME);
18
153
  }
19
- export function workspaceExists(cwd) {
20
- return existsSync(join(getWorkspaceRoot(cwd), 'workspace.json'));
154
+ function projectReadme(projectSlug) {
155
+ return `# ${projectSlug}
156
+
157
+ Project-level Orizu context. Add human notes here and keep machine-readable
158
+ state in the adjacent \`orizu.project.json\` manifest and primitive manifests.
159
+ `;
160
+ }
161
+ const AGENTS_MD = `# Orizu Agent Instructions
162
+
163
+ This is a team-level Orizu workbench. Use this file for durable, human-readable
164
+ agent guidance; keep detailed CLI manuals in installed skills or reference
165
+ docs instead of pasting them here.
166
+
167
+ Before changing anything, read:
168
+
169
+ - \`README.md\` for the team workbench overview.
170
+ - \`Memory.md\` for durable team preferences and decisions.
171
+ - \`projects/*/README.md\` and \`projects/*/memory.md\` for project context.
172
+ - \`orizu.*.json\` manifests for machine-readable ids, ownership, source-of-truth
173
+ state, commands, and object refs.
174
+
175
+ Use \`orizu --help\`, \`orizu <command> --help\`, and
176
+ \`orizu capabilities --json\` to discover exact CLI behavior. Do not duplicate
177
+ Orizu runtime behavior in repo scripts; go through the CLI.
178
+
179
+ Treat Git-tracked files as source/context or explicit snapshots. Live Orizu
180
+ state remains in Orizu DB/storage unless a manifest says a repo file is the
181
+ source of truth. Treat \`.orizu/\`, raw logs, raw transcripts, bulky datasets,
182
+ and local source checkouts as ignored cache or object-backed state unless
183
+ explicitly promoted.
184
+
185
+ Ask before creating Orizu server state or changing human-authored repo files.
186
+ `;
187
+ const MEMORY_MD = `# Orizu Memory
188
+
189
+ Record durable team-level preferences, decisions, and lessons here.
190
+ `;
191
+ const PROJECT_MEMORY_MD = `# Project Memory
192
+
193
+ Record durable project-level preferences, decisions, and lessons here.
194
+ `;
195
+ const CLAUDE_POINTER = `# Claude Instructions
196
+
197
+ Read \`AGENTS.md\` in this directory for the canonical Orizu agent instructions.
198
+ `;
199
+ const OLD_CLAUDE_POINTER = `# Claude Instructions
200
+
201
+ Read \`Agents.md\` in this directory for the canonical Orizu agent instructions.
202
+ `;
203
+ const ORIZU_CLI_AGENTS_START_MARKER = '<!-- orizu-cli:start -->';
204
+ const ORIZU_CLI_AGENTS_END_MARKER = '<!-- orizu-cli:end -->';
205
+ const LOCAL_WORKSPACE_README = `# Local Orizu cache
206
+
207
+ This directory is ignored by default. Use it for generated exports, local
208
+ caches, temporary runner materialization, and server-derived files that may
209
+ drift. The durable workspace contract lives in the root and project manifests
210
+ outside this directory.
211
+ `;
212
+ const ROOT_GITIGNORE_ENTRIES = [
213
+ '.orizu/',
214
+ '.logs/',
215
+ '**/raw_transcript/',
216
+ '**/*.raw.jsonl',
217
+ '**/*.log',
218
+ '**/log_dir/',
219
+ '**/downloaded-datasets/',
220
+ '**/trace-pulls/',
221
+ 'source-repos/*/checkout/',
222
+ 'source-repos/*/worktree/',
223
+ '.env',
224
+ '.env.*',
225
+ 'node_modules/',
226
+ '.next/',
227
+ 'dist/',
228
+ 'build/',
229
+ '.venv/',
230
+ '__pycache__/',
231
+ ];
232
+ const PROJECT_GITIGNORE_ENTRIES = [
233
+ '.orizu/',
234
+ '.logs/',
235
+ '**/raw_transcript/',
236
+ '**/*.raw.jsonl',
237
+ '**/*.log',
238
+ '**/log_dir/',
239
+ '**/downloaded-datasets/',
240
+ '**/trace-pulls/',
241
+ '.env',
242
+ '.env.*',
243
+ '.venv/',
244
+ '__pycache__/',
245
+ ];
246
+ function operation(action, path, reason, extra) {
247
+ return {
248
+ action,
249
+ path,
250
+ reason,
251
+ safe: extra?.safe ?? true,
252
+ ...extra,
253
+ };
254
+ }
255
+ function lstatIfExists(path) {
256
+ try {
257
+ return lstatSync(path);
258
+ }
259
+ catch {
260
+ return null;
261
+ }
262
+ }
263
+ function pathExists(path) {
264
+ return existsSync(path) || lstatIfExists(path) !== null;
265
+ }
266
+ function rootEntryCaseMatches(root, canonical) {
267
+ const matches = rootEntryNames(root).filter(name => name.toLowerCase() === canonical.toLowerCase());
268
+ return {
269
+ hasCanonical: matches.includes(canonical),
270
+ nonCanonical: matches.filter(name => name !== canonical),
271
+ };
272
+ }
273
+ function pushCreateDir(operations, path, reason) {
274
+ if (!pathExists(path)) {
275
+ operations.push(operation('create_dir', path, reason));
276
+ }
277
+ }
278
+ function pushCreateFile(operations, path, content, reason) {
279
+ if (!pathExists(path)) {
280
+ operations.push(operation('create_file', path, reason, { content }));
281
+ }
21
282
  }
22
- function ensureGitignoreEntry(cwd, dryRun) {
23
- const gitignorePath = resolve(cwd, '.gitignore');
24
- const entry = `${WORKSPACE_DIR_NAME}/`;
25
- if (existsSync(gitignorePath)) {
26
- const existing = readFileSync(gitignorePath, 'utf8');
27
- const hasEntry = existing
28
- .split('\n')
29
- .some(line => line.trim() === entry || line.trim() === WORKSPACE_DIR_NAME);
30
- if (hasEntry) {
31
- return { updated: false, action: null };
283
+ function pushCreateRootFile(operations, root, filename, content, reason) {
284
+ const { hasCanonical, nonCanonical } = rootEntryCaseMatches(root, filename);
285
+ if (!hasCanonical && nonCanonical.length === 0) {
286
+ operations.push(operation('create_file', join(root, filename), reason, { content }));
287
+ }
288
+ }
289
+ function pushCaseRename(operations, root, filename, fix) {
290
+ if (!fix) {
291
+ return;
292
+ }
293
+ const { hasCanonical, nonCanonical } = rootEntryCaseMatches(root, filename);
294
+ if (!hasCanonical && nonCanonical.length === 1) {
295
+ operations.push(operation('rename_file', join(root, filename), `rename ${nonCanonical[0]} to ${filename}`, {
296
+ sourcePath: join(root, nonCanonical[0]),
297
+ }));
298
+ }
299
+ }
300
+ function isKnownClaudePointer(path) {
301
+ try {
302
+ const trimmed = readFileSync(path, 'utf8').trim();
303
+ return trimmed === CLAUDE_POINTER.trim() ||
304
+ trimmed === OLD_CLAUDE_POINTER.trim();
305
+ }
306
+ catch {
307
+ return false;
308
+ }
309
+ }
310
+ function pushClaudeMigrationRepair(operations, root, sourceName, options) {
311
+ if (!options?.fix) {
312
+ return;
313
+ }
314
+ const sourcePath = join(root, sourceName);
315
+ const claudePath = join(root, 'CLAUDE.md');
316
+ let repairable = false;
317
+ try {
318
+ const stat = lstatSync(sourcePath);
319
+ repairable = stat.isSymbolicLink() || isKnownClaudePointer(sourcePath);
320
+ }
321
+ catch {
322
+ repairable = false;
323
+ }
324
+ if (!repairable) {
325
+ return;
326
+ }
327
+ operations.push(options.noSymlinks
328
+ ? operation('replace_file', claudePath, 'repair CLAUDE.md pointer file', { content: CLAUDE_POINTER })
329
+ : operation('replace_symlink', claudePath, 'repair CLAUDE.md symlink target', { target: 'AGENTS.md' }));
330
+ }
331
+ function pushExistingClaudeRepair(operations, root, options) {
332
+ if (!options?.fix) {
333
+ return;
334
+ }
335
+ const claudePath = join(root, 'CLAUDE.md');
336
+ if (!rootEntryCaseMatches(root, 'CLAUDE.md').hasCanonical) {
337
+ return;
338
+ }
339
+ try {
340
+ const stat = lstatSync(claudePath);
341
+ if (stat.isSymbolicLink()) {
342
+ const target = readlinkSync(claudePath);
343
+ if (options.noSymlinks) {
344
+ operations.push(operation('replace_file', claudePath, 'repair CLAUDE.md pointer file', { content: CLAUDE_POINTER }));
345
+ }
346
+ else if (target !== 'AGENTS.md') {
347
+ operations.push(operation('replace_symlink', claudePath, 'repair CLAUDE.md symlink target', { target: 'AGENTS.md' }));
348
+ }
349
+ return;
32
350
  }
33
- if (!dryRun) {
34
- const separator = existing.endsWith('\n') || existing.length === 0 ? '' : '\n';
35
- writeFileSync(gitignorePath, `${existing}${separator}\n# Local Orizu workspace (generated; not synced with the server)\n${entry}\n`, 'utf8');
351
+ if (isKnownClaudePointer(claudePath)) {
352
+ operations.push(options.noSymlinks
353
+ ? operation('replace_file', claudePath, 'repair CLAUDE.md pointer file', { content: CLAUDE_POINTER })
354
+ : operation('replace_symlink', claudePath, 'repair CLAUDE.md symlink target', { target: 'AGENTS.md' }));
36
355
  }
37
- return { updated: true, action: `append ${entry} to .gitignore` };
38
356
  }
39
- if (!dryRun) {
40
- writeFileSync(gitignorePath, `# Local Orizu workspace (generated; not synced with the server)\n${entry}\n`, 'utf8');
357
+ catch {
358
+ // Validation will report unreadable or missing files.
41
359
  }
42
- return { updated: true, action: `create .gitignore with ${entry}` };
43
360
  }
44
- export function initOrizuWorkspace(options) {
45
- const cwd = resolve(options?.cwd || process.cwd());
46
- const root = getWorkspaceRoot(cwd);
47
- const dryRun = Boolean(options?.dryRun);
48
- const exists = workspaceExists(cwd);
49
- const actions = [];
50
- if (!exists) {
51
- actions.push(`create ${WORKSPACE_DIR_NAME}/workspace.json, README.md, and generated/`);
361
+ function gitignoreHasEntry(content, entry) {
362
+ return content
363
+ .split('\n')
364
+ .map(line => line.trim())
365
+ .some(line => line === entry || line === entry.replace(/\/$/, ''));
366
+ }
367
+ function missingGitignoreEntries(path, entries) {
368
+ if (!existsSync(path)) {
369
+ return entries;
370
+ }
371
+ const content = readFileSync(path, 'utf8');
372
+ return entries.filter(entry => !gitignoreHasEntry(content, entry));
373
+ }
374
+ function pushGitignore(operations, path, entries, reason) {
375
+ const missing = missingGitignoreEntries(path, entries);
376
+ if (missing.length > 0) {
377
+ operations.push(operation('append_gitignore', path, reason, { entries: missing }));
378
+ }
379
+ }
380
+ function existingTeamSlug(root) {
381
+ const manifest = readJsonManifest(join(root, 'orizu.team.json'));
382
+ return typeof manifest?.slug === 'string' ? manifest.slug : null;
383
+ }
384
+ function buildSetupPlan(options, includeFindings = true) {
385
+ const root = resolveWorkspaceRoot(options);
386
+ const currentTeamSlug = existingTeamSlug(root);
387
+ const requestedTeamSlug = options?.teamSlug ? normalizeSlug(options.teamSlug, 'local-team') : null;
388
+ const normalizedCurrentTeamSlug = currentTeamSlug ? normalizeSlug(currentTeamSlug, 'local-team') : null;
389
+ if (requestedTeamSlug && normalizedCurrentTeamSlug && requestedTeamSlug !== normalizedCurrentTeamSlug) {
390
+ throw new Error(`This directory is already an Orizu workspace for team '${normalizedCurrentTeamSlug}'. `
391
+ + `Run setup in another directory to set up team '${requestedTeamSlug}'.`);
392
+ }
393
+ const teamSlug = requestedTeamSlug || normalizedCurrentTeamSlug || 'local-team';
394
+ const existingProjectSlugs = projectDirectoryNames(join(root, 'projects'));
395
+ const projects = normalizeProjectSeeds({
396
+ ...options,
397
+ projects: options?.projects?.length
398
+ ? options.projects
399
+ : (!options?.projectSlug && existingProjectSlugs.length > 0
400
+ ? existingProjectSlugs.map(slug => ({ slug }))
401
+ : undefined),
402
+ });
403
+ const serviceOrigin = options?.serviceOrigin || options?.baseUrl || null;
404
+ const attachWorkspaceId = options?.attachWorkspaceId || null;
405
+ const operations = [];
406
+ pushCreateDir(operations, root, 'create workspace root');
407
+ for (const dir of WORKSPACE_ROOT_DIRS) {
408
+ pushCreateDir(operations, join(root, dir), `create root ${dir}/ directory`);
409
+ }
410
+ pushCreateDir(operations, join(root, WORKSPACE_DIR_NAME, 'generated'), 'create local generated cache directory');
411
+ pushCreateDir(operations, join(root, WORKSPACE_DIR_NAME, 'cache'), 'create local cache directory');
412
+ for (const project of projects) {
413
+ const projectRoot = join(root, 'projects', project.slug);
414
+ pushCreateDir(operations, projectRoot, `create ${project.slug} project directory`);
415
+ for (const dir of PROJECT_PRIMITIVE_DIRS) {
416
+ pushCreateDir(operations, join(projectRoot, dir), `create ${project.slug} ${dir}/ directory`);
417
+ }
418
+ }
419
+ pushCaseRename(operations, root, 'README.md', true);
420
+ pushCreateRootFile(operations, root, 'README.md', rootReadme(teamSlug), 'create team workbench README');
421
+ pushCreateFile(operations, join(root, 'orizu.team.json'), createTeamManifest({ teamSlug, teamId: options?.teamId || null, serviceOrigin, attachWorkspaceId, cliVersion: options?.cliVersion || null }), 'create team manifest');
422
+ pushCaseRename(operations, root, 'AGENTS.md', options?.fix);
423
+ pushCaseRename(operations, root, 'CLAUDE.md', options?.fix);
424
+ for (const variant of rootEntryCaseMatches(root, 'CLAUDE.md').nonCanonical) {
425
+ pushClaudeMigrationRepair(operations, root, variant, options);
426
+ }
427
+ pushExistingClaudeRepair(operations, root, options);
428
+ pushCreateRootFile(operations, root, 'AGENTS.md', AGENTS_MD, 'create canonical agent instructions');
429
+ pushCreateRootFile(operations, root, 'Memory.md', MEMORY_MD, 'create team memory file');
430
+ const claudePath = join(root, 'CLAUDE.md');
431
+ const claudeMatches = rootEntryCaseMatches(root, 'CLAUDE.md');
432
+ if (!claudeMatches.hasCanonical && claudeMatches.nonCanonical.length === 0) {
433
+ operations.push(options?.noSymlinks
434
+ ? operation('write_pointer_file', claudePath, 'create Claude pointer file', { content: CLAUDE_POINTER })
435
+ : operation('create_symlink', claudePath, 'link Claude instructions to AGENTS.md', { target: 'AGENTS.md' }));
436
+ }
437
+ pushGitignore(operations, join(root, '.gitignore'), ROOT_GITIGNORE_ENTRIES, 'seed root gitignore policy');
438
+ pushCreateFile(operations, join(root, WORKSPACE_DIR_NAME, 'README.md'), LOCAL_WORKSPACE_README, 'create local cache README');
439
+ for (const project of projects) {
440
+ const projectRoot = join(root, 'projects', project.slug);
441
+ pushCaseRename(operations, projectRoot, 'README.md', true);
442
+ pushCreateRootFile(operations, projectRoot, 'README.md', projectReadme(project.slug), 'create project README');
443
+ pushCreateFile(operations, join(projectRoot, 'memory.md'), PROJECT_MEMORY_MD, 'create project memory file');
444
+ pushCreateFile(operations, join(projectRoot, 'orizu.project.json'), createProjectManifest(teamSlug, project), 'create project manifest');
445
+ pushGitignore(operations, join(projectRoot, '.gitignore'), PROJECT_GITIGNORE_ENTRIES, 'seed project gitignore policy');
446
+ }
447
+ return {
448
+ root,
449
+ teamSlug,
450
+ projects,
451
+ operations,
452
+ findings: includeFindings ? validateWorkspaceContract({ ...options, workspaceRoot: root }) : [],
453
+ exists: workspaceExists(root),
454
+ };
455
+ }
456
+ export function planWorkspaceSetup(options) {
457
+ return buildSetupPlan(options);
458
+ }
459
+ function ensureParent(path) {
460
+ mkdirSync(dirname(path), { recursive: true });
461
+ }
462
+ function appendGitignore(path, entries) {
463
+ ensureParent(path);
464
+ const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
465
+ const terminator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
466
+ const gap = existing.length > 0 ? '\n' : '';
467
+ const prefix = `${terminator}${gap}# Orizu workspace policy\n`;
468
+ writeFileSync(path, `${existing}${prefix}${entries.join('\n')}\n`, 'utf8');
469
+ }
470
+ function applyOperation(op) {
471
+ if (!op.safe) {
472
+ return false;
473
+ }
474
+ if (op.action === 'create_dir') {
475
+ mkdirSync(op.path, { recursive: true });
476
+ return true;
477
+ }
478
+ if (op.action === 'create_file' || op.action === 'write_pointer_file') {
479
+ if (pathExists(op.path)) {
480
+ return false;
481
+ }
482
+ ensureParent(op.path);
483
+ writeFileSync(op.path, op.content || '', 'utf8');
484
+ return true;
485
+ }
486
+ if (op.action === 'append_gitignore') {
487
+ appendGitignore(op.path, op.entries || []);
488
+ return true;
489
+ }
490
+ if (op.action === 'create_symlink') {
491
+ if (pathExists(op.path)) {
492
+ return false;
493
+ }
494
+ ensureParent(op.path);
495
+ try {
496
+ symlinkSync(op.target || 'AGENTS.md', op.path);
497
+ }
498
+ catch {
499
+ writeFileSync(op.path, CLAUDE_POINTER, 'utf8');
500
+ }
501
+ return true;
502
+ }
503
+ if (op.action === 'rename_file') {
504
+ if (!op.sourcePath || !pathExists(op.sourcePath)) {
505
+ return false;
506
+ }
507
+ ensureParent(op.path);
508
+ const sameCaseFoldedPath = op.sourcePath.toLowerCase() === op.path.toLowerCase();
509
+ if (sameCaseFoldedPath) {
510
+ const tempPath = `${op.path}.orizu-rename-${Date.now()}`;
511
+ renameSync(op.sourcePath, tempPath);
512
+ renameSync(tempPath, op.path);
513
+ }
514
+ else {
515
+ if (pathExists(op.path)) {
516
+ return false;
517
+ }
518
+ renameSync(op.sourcePath, op.path);
519
+ }
520
+ return true;
521
+ }
522
+ if (op.action === 'replace_file') {
523
+ ensureParent(op.path);
524
+ if (pathExists(op.path)) {
525
+ unlinkSync(op.path);
526
+ }
527
+ writeFileSync(op.path, op.content || '', 'utf8');
528
+ return true;
529
+ }
530
+ if (op.action === 'replace_symlink') {
531
+ ensureParent(op.path);
532
+ if (pathExists(op.path)) {
533
+ unlinkSync(op.path);
534
+ }
535
+ try {
536
+ symlinkSync(op.target || 'AGENTS.md', op.path);
537
+ }
538
+ catch {
539
+ writeFileSync(op.path, CLAUDE_POINTER, 'utf8');
540
+ }
541
+ return true;
542
+ }
543
+ return false;
544
+ }
545
+ function relativePath(root, path) {
546
+ const rel = relative(root, path);
547
+ return rel || '.';
548
+ }
549
+ function operationLabel(root, op) {
550
+ const path = relativePath(root, op.path);
551
+ if (op.action === 'create_dir')
552
+ return `create ${path}/`;
553
+ if (op.action === 'create_file')
554
+ return `create ${path}`;
555
+ if (op.action === 'append_gitignore')
556
+ return `append ${op.entries?.join(', ')} to ${path}`;
557
+ if (op.action === 'create_symlink')
558
+ return `link ${path} -> ${op.target}`;
559
+ if (op.action === 'write_pointer_file')
560
+ return `create ${path} pointer file`;
561
+ if (op.action === 'rename_file')
562
+ return `rename ${relativePath(root, op.sourcePath || '')} -> ${path}`;
563
+ if (op.action === 'replace_file')
564
+ return `repair ${path} pointer file`;
565
+ if (op.action === 'replace_symlink')
566
+ return `repair ${path} -> ${op.target}`;
567
+ return `repair ${path}`;
568
+ }
569
+ function finding(severity, code, message, path, fixable = false) {
570
+ return { severity, code, path, message, fixable };
571
+ }
572
+ function rootEntryNames(root) {
573
+ try {
574
+ return readdirSync(root);
575
+ }
576
+ catch {
577
+ return [];
578
+ }
579
+ }
580
+ function checkCanonicalCase(root, canonical, findings) {
581
+ const matches = rootEntryNames(root).filter(name => name.toLowerCase() === canonical.toLowerCase());
582
+ const hasCanonical = matches.includes(canonical);
583
+ const nonCanonical = matches.filter(name => name !== canonical);
584
+ const canAutoRepair = !hasCanonical && nonCanonical.length <= 1;
585
+ if (!hasCanonical) {
586
+ findings.push(finding('error', 'missing_required_file', `Missing required root file ${canonical}.`, join(root, canonical), canAutoRepair));
587
+ }
588
+ for (const variant of nonCanonical) {
589
+ findings.push(finding('warning', 'non_canonical_case', `${variant} should be named ${canonical}.`, join(root, variant), canAutoRepair));
590
+ }
591
+ if (hasCanonical && nonCanonical.length > 0) {
592
+ findings.push(finding('error', 'case_conflict', `Both ${canonical} and non-canonical variants exist. Resolve manually.`, join(root, canonical), false));
593
+ }
594
+ }
595
+ function checkProjectFileCanonicalCase(projectRoot, projectName, canonical, findings, fixable) {
596
+ const matches = rootEntryNames(projectRoot).filter(name => name.toLowerCase() === canonical.toLowerCase());
597
+ const hasCanonical = matches.includes(canonical);
598
+ const nonCanonical = matches.filter(name => name !== canonical);
599
+ if (!hasCanonical) {
600
+ findings.push(finding('error', 'missing_project_file', `Missing project file projects/${projectName}/${canonical}.`, join(projectRoot, canonical), fixable));
601
+ }
602
+ for (const variant of nonCanonical) {
603
+ findings.push(finding('warning', 'non_canonical_case', `projects/${projectName}/${variant} should be named ${canonical}.`, join(projectRoot, variant), fixable));
52
604
  }
53
- const gitignore = ensureGitignoreEntry(cwd, dryRun || exists);
54
- if (!exists && gitignore.action) {
55
- actions.push(gitignore.action);
605
+ if (hasCanonical && nonCanonical.length > 0) {
606
+ findings.push(finding('error', 'case_conflict', `Both projects/${projectName}/${canonical} and non-canonical variants exist. Resolve manually.`, join(projectRoot, canonical), false));
607
+ }
608
+ }
609
+ export function readJsonManifest(path) {
610
+ try {
611
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
612
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
613
+ return null;
614
+ }
615
+ return parsed;
616
+ }
617
+ catch {
618
+ return null;
619
+ }
620
+ }
621
+ function validateManifest(path, findings) {
622
+ const parsed = readJsonManifest(path);
623
+ if (!parsed) {
624
+ findings.push(finding('error', 'invalid_manifest_json', 'Manifest must be valid JSON with an object root.', path, false));
625
+ return;
626
+ }
627
+ if (parsed.schemaVersion !== WORKSPACE_SCHEMA_VERSION) {
628
+ findings.push(finding('error', 'invalid_schema_version', `Manifest schemaVersion must be ${WORKSPACE_SCHEMA_VERSION}.`, path, false));
629
+ }
630
+ if (typeof parsed.kind !== 'string' || parsed.kind.length === 0) {
631
+ findings.push(finding('error', 'missing_manifest_kind', 'Manifest must include kind.', path, false));
632
+ }
633
+ if (typeof parsed.slug !== 'string' && typeof parsed.runId !== 'string' && typeof parsed.sessionId !== 'string') {
634
+ findings.push(finding('error', 'missing_manifest_identity', 'Manifest must include slug, runId, or sessionId.', path, false));
635
+ }
636
+ const canonical = parsed.canonical;
637
+ if (!canonical || typeof canonical !== 'object' || Array.isArray(canonical)) {
638
+ findings.push(finding('error', 'missing_canonical', 'Manifest must include canonical owner and repoState.', path, false));
639
+ return;
640
+ }
641
+ const canonicalRecord = canonical;
642
+ if (!hasAllowedValue(CANONICAL_OWNERS, canonicalRecord.owner)) {
643
+ findings.push(finding('error', 'invalid_canonical_owner', `canonical.owner must be one of ${CANONICAL_OWNERS.join(', ')}.`, path, false));
644
+ }
645
+ if (!hasAllowedValue(REPO_STATES, canonicalRecord.repoState)) {
646
+ findings.push(finding('error', 'invalid_repo_state', `canonical.repoState must be one of ${REPO_STATES.join(', ')}.`, path, false));
647
+ }
648
+ if (parsed.kind === 'team') {
649
+ const setup = parsed.setup;
650
+ if (!setup || typeof setup !== 'object' || Array.isArray(setup)) {
651
+ findings.push(finding('warning', 'missing_setup_metadata', 'Team manifest should include setup metadata.', path, false));
652
+ }
653
+ }
654
+ }
655
+ function collectManagedWorkspaceManifestPaths(root) {
656
+ const paths = [];
657
+ const ignored = new Set(['.git', 'node_modules', '.next', 'dist', 'build', WORKSPACE_DIR_NAME]);
658
+ const rootManifest = join(root, 'orizu.team.json');
659
+ if (existsSync(rootManifest)) {
660
+ paths.push(rootManifest);
661
+ }
662
+ function walk(dir, depth) {
663
+ if (depth > 8 || !existsSync(dir)) {
664
+ return;
665
+ }
666
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
667
+ if (ignored.has(entry.name)) {
668
+ continue;
669
+ }
670
+ const path = join(dir, entry.name);
671
+ if (entry.isDirectory()) {
672
+ walk(path, depth + 1);
673
+ }
674
+ else if (/^orizu\..+\.json$/.test(entry.name)) {
675
+ paths.push(path);
676
+ }
677
+ }
678
+ }
679
+ const projectsRoot = join(root, 'projects');
680
+ for (const projectName of projectDirectoryNames(projectsRoot)) {
681
+ const projectRoot = join(projectsRoot, projectName);
682
+ const projectManifest = join(projectRoot, 'orizu.project.json');
683
+ if (existsSync(projectManifest)) {
684
+ paths.push(projectManifest);
685
+ }
686
+ for (const primitiveDir of PROJECT_PRIMITIVE_DIRS) {
687
+ walk(join(projectRoot, primitiveDir), 0);
688
+ }
56
689
  }
690
+ return paths;
691
+ }
692
+ function collectNamedPaths(root, name) {
693
+ const paths = [];
694
+ const ignored = new Set(['.git', 'node_modules', '.next', 'dist', 'build', WORKSPACE_DIR_NAME]);
695
+ function walk(dir, depth) {
696
+ if (depth > 8 || !existsSync(dir)) {
697
+ return;
698
+ }
699
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
700
+ if (ignored.has(entry.name)) {
701
+ continue;
702
+ }
703
+ const path = join(dir, entry.name);
704
+ if (entry.name === name) {
705
+ paths.push(path);
706
+ }
707
+ if (entry.isDirectory()) {
708
+ walk(path, depth + 1);
709
+ }
710
+ }
711
+ }
712
+ walk(root, 0);
713
+ return paths;
714
+ }
715
+ function validateClaudeFile(root, noSymlinks, findings) {
716
+ const path = join(root, 'CLAUDE.md');
717
+ const stat = lstatIfExists(path);
718
+ if (!stat) {
719
+ return;
720
+ }
721
+ if (stat.isSymbolicLink()) {
722
+ const target = readlinkSync(path);
723
+ if (target !== 'AGENTS.md') {
724
+ findings.push(finding('error', 'invalid_claude_symlink', 'CLAUDE.md must symlink to AGENTS.md.', path, true));
725
+ }
726
+ if (noSymlinks) {
727
+ findings.push(finding('warning', 'unexpected_claude_symlink', 'CLAUDE.md is a symlink but --no-symlinks expects a pointer file.', path, true));
728
+ }
729
+ return;
730
+ }
731
+ const content = readFileSync(path, 'utf8');
732
+ if (!content.includes('AGENTS.md')) {
733
+ findings.push(finding('error', 'invalid_claude_pointer', 'CLAUDE.md must point readers to AGENTS.md when it is not a symlink.', path, false));
734
+ }
735
+ }
736
+ function validateAgentsFile(root, findings) {
737
+ const path = join(root, 'AGENTS.md');
738
+ if (!existsSync(path)) {
739
+ return;
740
+ }
741
+ const content = readFileSync(path, 'utf8');
742
+ const hasSkillBlock = content.includes(ORIZU_CLI_AGENTS_START_MARKER) &&
743
+ content.includes(ORIZU_CLI_AGENTS_END_MARKER);
744
+ if (hasSkillBlock) {
745
+ findings.push(finding('error', 'embedded_skill_block', 'AGENTS.md contains the full Orizu CLI skill block. Keep root AGENTS.md concise and move detailed CLI guidance to installed skills or `orizu --help`.', path, false));
746
+ }
747
+ }
748
+ function projectDirectoryNames(projectsRoot) {
749
+ try {
750
+ return readdirSync(projectsRoot, { withFileTypes: true })
751
+ .filter(entry => entry.isDirectory())
752
+ .map(entry => entry.name)
753
+ .sort();
754
+ }
755
+ catch {
756
+ return [];
757
+ }
758
+ }
759
+ function validateProjectContract(root, options, findings) {
760
+ const projectsRoot = join(root, 'projects');
761
+ if (!existsSync(projectsRoot)) {
762
+ return;
763
+ }
764
+ const expectedProjects = options?.projects?.length
765
+ ? options.projects.map(project => normalizeSlug(project.slug, 'local-project'))
766
+ : (options?.projectSlug ? [normalizeSlug(options.projectSlug, 'local-project')] : null);
767
+ const projectNames = expectedProjects || projectDirectoryNames(projectsRoot);
768
+ const canRepairProject = Boolean(expectedProjects?.length);
769
+ if (projectNames.length === 0) {
770
+ findings.push(finding('error', 'missing_project_directory', 'Workspace must include at least one project directory under projects/.', projectsRoot, canRepairProject));
771
+ return;
772
+ }
773
+ for (const name of projectNames) {
774
+ const projectRoot = join(projectsRoot, name);
775
+ if (!existsSync(projectRoot)) {
776
+ findings.push(finding('error', 'missing_project_directory', `Missing project directory projects/${name}/.`, projectRoot, canRepairProject));
777
+ continue;
778
+ }
779
+ for (const file of PROJECT_ROOT_FILES) {
780
+ if (/^[A-Z]/.test(file)) {
781
+ checkProjectFileCanonicalCase(projectRoot, name, file, findings, canRepairProject);
782
+ }
783
+ else {
784
+ const path = join(projectRoot, file);
785
+ if (!existsSync(path)) {
786
+ findings.push(finding('error', 'missing_project_file', `Missing project file projects/${name}/${file}.`, path, canRepairProject));
787
+ }
788
+ }
789
+ }
790
+ for (const dir of PROJECT_PRIMITIVE_DIRS) {
791
+ const path = join(projectRoot, dir);
792
+ if (!existsSync(path)) {
793
+ findings.push(finding('error', 'missing_project_dir', `Missing project directory projects/${name}/${dir}/.`, path, canRepairProject));
794
+ }
795
+ }
796
+ const projectGitignore = join(projectRoot, '.gitignore');
797
+ if (existsSync(projectGitignore) && missingGitignoreEntries(projectGitignore, [`${WORKSPACE_DIR_NAME}/`]).length > 0) {
798
+ findings.push(finding('error', 'missing_project_orizu_gitignore', `.orizu/ must be ignored by default in projects/${name}/.`, projectGitignore, canRepairProject));
799
+ }
800
+ }
801
+ }
802
+ export function validateWorkspaceContract(options) {
803
+ const root = resolveWorkspaceRoot(options);
804
+ const findings = [];
805
+ if (!existsSync(root)) {
806
+ findings.push(finding('error', 'missing_workspace_root', 'Workspace root does not exist.', root, true));
807
+ return findings;
808
+ }
809
+ for (const file of WORKSPACE_ROOT_FILES) {
810
+ if (/^[A-Z]/.test(file)) {
811
+ checkCanonicalCase(root, file, findings);
812
+ }
813
+ else {
814
+ const path = join(root, file);
815
+ if (!existsSync(path)) {
816
+ findings.push(finding('error', 'missing_required_file', `Missing required root file ${file}.`, path, true));
817
+ }
818
+ }
819
+ }
820
+ for (const dir of WORKSPACE_ROOT_DIRS) {
821
+ const path = join(root, dir);
822
+ if (!existsSync(path)) {
823
+ findings.push(finding('error', 'missing_required_dir', `Missing required root directory ${dir}/.`, path, true));
824
+ }
825
+ }
826
+ const rootGitignore = join(root, '.gitignore');
827
+ if (existsSync(rootGitignore) && missingGitignoreEntries(rootGitignore, [`${WORKSPACE_DIR_NAME}/`]).length > 0) {
828
+ findings.push(finding('error', 'missing_orizu_gitignore', '.orizu/ must be ignored by default.', rootGitignore, true));
829
+ }
830
+ validateClaudeFile(root, Boolean(options?.noSymlinks), findings);
831
+ validateAgentsFile(root, findings);
832
+ validateProjectContract(root, options, findings);
833
+ for (const path of collectManagedWorkspaceManifestPaths(root)) {
834
+ validateManifest(path, findings);
835
+ }
836
+ for (const path of collectNamedPaths(root, '.gitadd')) {
837
+ findings.push(finding('error', 'gitadd_not_supported', '.gitadd is not part of the Orizu workspace contract.', path, false));
838
+ }
839
+ return findings;
840
+ }
841
+ export function initOrizuWorkspace(options) {
842
+ const dryRun = Boolean(options?.dryRun);
843
+ const validateOnly = Boolean(options?.validateOnly);
844
+ const plan = buildSetupPlan(options, dryRun || validateOnly);
845
+ const beforeExists = plan.exists;
57
846
  if (dryRun) {
58
847
  return {
59
- root,
60
- state: exists ? 'exists' : 'would-create',
848
+ root: plan.root,
849
+ state: beforeExists ? 'exists' : 'would-create',
850
+ gitignoreUpdated: false,
851
+ actions: plan.operations.map(op => operationLabel(plan.root, op)),
852
+ operations: plan.operations,
853
+ findings: plan.findings,
854
+ };
855
+ }
856
+ if (validateOnly) {
857
+ const hasErrors = plan.findings.some(item => item.severity === 'error');
858
+ return {
859
+ root: plan.root,
860
+ state: hasErrors ? 'invalid' : 'validated',
61
861
  gitignoreUpdated: false,
62
- actions,
862
+ actions: [],
863
+ operations: [],
864
+ findings: plan.findings,
63
865
  };
64
866
  }
65
- if (exists) {
66
- return { root, state: 'exists', gitignoreUpdated: false, actions: [] };
67
- }
68
- mkdirSync(join(root, 'generated'), { recursive: true });
69
- writeFileSync(join(root, 'README.md'), WORKSPACE_README, 'utf8');
70
- writeFileSync(join(root, 'workspace.json'), `${JSON.stringify({
71
- version: 1,
72
- server: options?.baseUrl || null,
73
- createdByCliVersion: options?.cliVersion || null,
74
- }, null, 2)}\n`, 'utf8');
75
- return { root, state: 'created', gitignoreUpdated: gitignore.updated, actions };
867
+ let applied = 0;
868
+ let gitignoreUpdated = false;
869
+ for (const op of plan.operations) {
870
+ const didApply = applyOperation(op);
871
+ if (didApply) {
872
+ applied += 1;
873
+ if (op.action === 'append_gitignore') {
874
+ gitignoreUpdated = true;
875
+ }
876
+ }
877
+ }
878
+ const findings = validateWorkspaceContract({ ...options, workspaceRoot: plan.root });
879
+ return {
880
+ root: plan.root,
881
+ state: beforeExists ? (applied > 0 ? 'repaired' : 'exists') : 'created',
882
+ gitignoreUpdated,
883
+ actions: plan.operations.map(op => operationLabel(plan.root, op)),
884
+ operations: plan.operations,
885
+ findings,
886
+ };
76
887
  }