@chamba/core 0.2.0 → 0.3.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.
- package/dist/index.d.ts +223 -4
- package/dist/index.js +303 -3
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -70,6 +70,35 @@ interface FilesystemPort {
|
|
|
70
70
|
remove(path: string): Promise<void>;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
type WorktreeLayout = 'sibling' | 'nested';
|
|
74
|
+
interface WorktreeConfig {
|
|
75
|
+
/**
|
|
76
|
+
* `sibling`: one folder per ticket under `<workspaceRoot>/<root>/<ticket>/<repo>`.
|
|
77
|
+
* `nested`: a worktree under each repo at `<workspaceRoot>/<repo>/<root>/<ticket>`.
|
|
78
|
+
*/
|
|
79
|
+
layout: WorktreeLayout;
|
|
80
|
+
/** Worktree root: relative to workspace root (sibling) or to each repo (nested). */
|
|
81
|
+
root: string;
|
|
82
|
+
/** Branch prefix; the branch is `<branchPrefix><ticket>`, shared across repos. */
|
|
83
|
+
branchPrefix: string;
|
|
84
|
+
/** Branch to fork from when the ticket branch doesn't exist yet. */
|
|
85
|
+
baseBranch: string;
|
|
86
|
+
/** Copy git-ignored `.env*` files into the new worktree (off by default). */
|
|
87
|
+
copyEnvFiles: boolean;
|
|
88
|
+
/** Directories pruned while scanning for `.env*` files. */
|
|
89
|
+
envPruneDirs: string[];
|
|
90
|
+
/** Generate an editor workspace file (`.code-workspace`) for the ticket. */
|
|
91
|
+
editorWorkspace: 'code-workspace' | null;
|
|
92
|
+
/** Repos to act on; `null` means autodetect the workspace's git repos. */
|
|
93
|
+
repos: string[] | null;
|
|
94
|
+
/** Escape hatch: if set, chamba shells out to this command instead of the built-in. */
|
|
95
|
+
command: string | null;
|
|
96
|
+
}
|
|
97
|
+
type PartialWorktreeConfig = Partial<WorktreeConfig>;
|
|
98
|
+
declare const DEFAULT_WORKTREE_CONFIG: WorktreeConfig;
|
|
99
|
+
/** Merge a partial (on-disk) worktree config over the compiled defaults. */
|
|
100
|
+
declare function resolveWorktreeConfig(file?: PartialWorktreeConfig): WorktreeConfig;
|
|
101
|
+
|
|
73
102
|
/** Raised when a config is invalid and the caller asked to fail hard. */
|
|
74
103
|
declare class ConfigError extends Error {
|
|
75
104
|
constructor(message: string);
|
|
@@ -84,6 +113,8 @@ interface ConfigSource {
|
|
|
84
113
|
}
|
|
85
114
|
interface LoadConfigResult {
|
|
86
115
|
config: ResolvedConfig;
|
|
116
|
+
/** Resolved multi-repo worktree policy (defaults ← global ← project). */
|
|
117
|
+
worktrees: WorktreeConfig;
|
|
87
118
|
sources: ConfigSource[];
|
|
88
119
|
}
|
|
89
120
|
interface LoadConfigOptions {
|
|
@@ -109,10 +140,42 @@ declare function resolveRole(config: ResolvedConfig, role: AgentRole): AgentConf
|
|
|
109
140
|
*/
|
|
110
141
|
declare function buildHint(role: AgentRole, cfg: AgentConfig): string;
|
|
111
142
|
|
|
143
|
+
/** The `worktrees` block of a config file (all fields optional). */
|
|
144
|
+
declare const worktreeConfigSchema: z.ZodObject<{
|
|
145
|
+
layout: z.ZodOptional<z.ZodEnum<["sibling", "nested"]>>;
|
|
146
|
+
root: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
147
|
+
branchPrefix: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
148
|
+
baseBranch: z.ZodOptional<z.ZodString>;
|
|
149
|
+
copyEnvFiles: z.ZodOptional<z.ZodBoolean>;
|
|
150
|
+
envPruneDirs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
151
|
+
editorWorkspace: z.ZodOptional<z.ZodNullable<z.ZodEnum<["code-workspace"]>>>;
|
|
152
|
+
repos: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString, "many">>>;
|
|
153
|
+
command: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
154
|
+
}, "strict", z.ZodTypeAny, {
|
|
155
|
+
layout?: "sibling" | "nested" | undefined;
|
|
156
|
+
root?: string | undefined;
|
|
157
|
+
branchPrefix?: string | undefined;
|
|
158
|
+
baseBranch?: string | undefined;
|
|
159
|
+
copyEnvFiles?: boolean | undefined;
|
|
160
|
+
envPruneDirs?: string[] | undefined;
|
|
161
|
+
editorWorkspace?: "code-workspace" | null | undefined;
|
|
162
|
+
repos?: string[] | null | undefined;
|
|
163
|
+
command?: string | null | undefined;
|
|
164
|
+
}, {
|
|
165
|
+
layout?: "sibling" | "nested" | undefined;
|
|
166
|
+
root?: string | undefined;
|
|
167
|
+
branchPrefix?: string | undefined;
|
|
168
|
+
baseBranch?: string | undefined;
|
|
169
|
+
copyEnvFiles?: boolean | undefined;
|
|
170
|
+
envPruneDirs?: string[] | undefined;
|
|
171
|
+
editorWorkspace?: "code-workspace" | null | undefined;
|
|
172
|
+
repos?: string[] | null | undefined;
|
|
173
|
+
command?: string | null | undefined;
|
|
174
|
+
}>;
|
|
112
175
|
/**
|
|
113
|
-
* What a config file on disk may contain.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
176
|
+
* What a config file on disk may contain. `defaults`/`overrides` are optional
|
|
177
|
+
* and partial per role; `worktrees` is the optional multi-repo worktree policy.
|
|
178
|
+
* Unknown roles, unknown models and unknown keys are rejected with clear messages.
|
|
116
179
|
*/
|
|
117
180
|
declare const configFileSchema: z.ZodObject<{
|
|
118
181
|
version: z.ZodLiteral<1>;
|
|
@@ -142,6 +205,37 @@ declare const configFileSchema: z.ZodObject<{
|
|
|
142
205
|
effort?: Effort | undefined;
|
|
143
206
|
reasoning_priority?: ReasoningPriority | undefined;
|
|
144
207
|
}>>>;
|
|
208
|
+
worktrees: z.ZodOptional<z.ZodObject<{
|
|
209
|
+
layout: z.ZodOptional<z.ZodEnum<["sibling", "nested"]>>;
|
|
210
|
+
root: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
211
|
+
branchPrefix: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
212
|
+
baseBranch: z.ZodOptional<z.ZodString>;
|
|
213
|
+
copyEnvFiles: z.ZodOptional<z.ZodBoolean>;
|
|
214
|
+
envPruneDirs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
215
|
+
editorWorkspace: z.ZodOptional<z.ZodNullable<z.ZodEnum<["code-workspace"]>>>;
|
|
216
|
+
repos: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString, "many">>>;
|
|
217
|
+
command: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
218
|
+
}, "strict", z.ZodTypeAny, {
|
|
219
|
+
layout?: "sibling" | "nested" | undefined;
|
|
220
|
+
root?: string | undefined;
|
|
221
|
+
branchPrefix?: string | undefined;
|
|
222
|
+
baseBranch?: string | undefined;
|
|
223
|
+
copyEnvFiles?: boolean | undefined;
|
|
224
|
+
envPruneDirs?: string[] | undefined;
|
|
225
|
+
editorWorkspace?: "code-workspace" | null | undefined;
|
|
226
|
+
repos?: string[] | null | undefined;
|
|
227
|
+
command?: string | null | undefined;
|
|
228
|
+
}, {
|
|
229
|
+
layout?: "sibling" | "nested" | undefined;
|
|
230
|
+
root?: string | undefined;
|
|
231
|
+
branchPrefix?: string | undefined;
|
|
232
|
+
baseBranch?: string | undefined;
|
|
233
|
+
copyEnvFiles?: boolean | undefined;
|
|
234
|
+
envPruneDirs?: string[] | undefined;
|
|
235
|
+
editorWorkspace?: "code-workspace" | null | undefined;
|
|
236
|
+
repos?: string[] | null | undefined;
|
|
237
|
+
command?: string | null | undefined;
|
|
238
|
+
}>>;
|
|
145
239
|
}, "strict", z.ZodTypeAny, {
|
|
146
240
|
version: 1;
|
|
147
241
|
defaults?: Partial<Record<AgentRole, {
|
|
@@ -154,6 +248,17 @@ declare const configFileSchema: z.ZodObject<{
|
|
|
154
248
|
effort?: Effort | undefined;
|
|
155
249
|
reasoning_priority?: ReasoningPriority | undefined;
|
|
156
250
|
}>> | undefined;
|
|
251
|
+
worktrees?: {
|
|
252
|
+
layout?: "sibling" | "nested" | undefined;
|
|
253
|
+
root?: string | undefined;
|
|
254
|
+
branchPrefix?: string | undefined;
|
|
255
|
+
baseBranch?: string | undefined;
|
|
256
|
+
copyEnvFiles?: boolean | undefined;
|
|
257
|
+
envPruneDirs?: string[] | undefined;
|
|
258
|
+
editorWorkspace?: "code-workspace" | null | undefined;
|
|
259
|
+
repos?: string[] | null | undefined;
|
|
260
|
+
command?: string | null | undefined;
|
|
261
|
+
} | undefined;
|
|
157
262
|
}, {
|
|
158
263
|
version: 1;
|
|
159
264
|
defaults?: Partial<Record<AgentRole, {
|
|
@@ -166,6 +271,17 @@ declare const configFileSchema: z.ZodObject<{
|
|
|
166
271
|
effort?: Effort | undefined;
|
|
167
272
|
reasoning_priority?: ReasoningPriority | undefined;
|
|
168
273
|
}>> | undefined;
|
|
274
|
+
worktrees?: {
|
|
275
|
+
layout?: "sibling" | "nested" | undefined;
|
|
276
|
+
root?: string | undefined;
|
|
277
|
+
branchPrefix?: string | undefined;
|
|
278
|
+
baseBranch?: string | undefined;
|
|
279
|
+
copyEnvFiles?: boolean | undefined;
|
|
280
|
+
envPruneDirs?: string[] | undefined;
|
|
281
|
+
editorWorkspace?: "code-workspace" | null | undefined;
|
|
282
|
+
repos?: string[] | null | undefined;
|
|
283
|
+
command?: string | null | undefined;
|
|
284
|
+
} | undefined;
|
|
169
285
|
}>;
|
|
170
286
|
type ConfigFile = z.infer<typeof configFileSchema>;
|
|
171
287
|
type ParseResult = {
|
|
@@ -561,6 +677,59 @@ declare function buildBranchName(input: BranchNameInput): string;
|
|
|
561
677
|
/** Worktree directory relative to the repo root. */
|
|
562
678
|
declare function worktreeRelativePath(taskSlug: string, workerId: string): string;
|
|
563
679
|
|
|
680
|
+
interface WorktreePlanItem {
|
|
681
|
+
/** Repo name (a directory under the workspace root). */
|
|
682
|
+
repo: string;
|
|
683
|
+
/** Absolute path to the main repo checkout. */
|
|
684
|
+
repoPath: string;
|
|
685
|
+
/** Absolute path where the worktree will be created. */
|
|
686
|
+
worktreePath: string;
|
|
687
|
+
/** The ticket branch (shared across repos). */
|
|
688
|
+
branch: string;
|
|
689
|
+
}
|
|
690
|
+
interface PlanWorktreesInput {
|
|
691
|
+
workspaceRoot: string;
|
|
692
|
+
ticket: string;
|
|
693
|
+
/** Already-resolved repo names. */
|
|
694
|
+
repos: string[];
|
|
695
|
+
config: WorktreeConfig;
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Make a ticket id safe as a single git ref / path component, **preserving
|
|
699
|
+
* case** (git refs are case-sensitive, so `TICKET-123` must not become
|
|
700
|
+
* `ticket-123` or it wouldn't match an existing branch).
|
|
701
|
+
*/
|
|
702
|
+
declare function safeTicket(ticket: string): string;
|
|
703
|
+
/** Branch shared across repos: `<branchPrefix><ticket>`. */
|
|
704
|
+
declare function buildTicketBranch(branchPrefix: string, ticket: string): string;
|
|
705
|
+
/** Where a repo's worktree lives, per layout. */
|
|
706
|
+
declare function worktreePathFor(config: WorktreeConfig, workspaceRoot: string, ticket: string, repo: string): string;
|
|
707
|
+
/** Directory where the editor workspace file is written. */
|
|
708
|
+
declare function editorWorkspaceDir(config: WorktreeConfig, workspaceRoot: string, ticket: string): string;
|
|
709
|
+
/** Pure plan: one item per repo. No IO. */
|
|
710
|
+
declare function planWorktrees(input: PlanWorktreesInput): WorktreePlanItem[];
|
|
711
|
+
/**
|
|
712
|
+
* Content of the `.code-workspace` file: each repo's worktree as a folder.
|
|
713
|
+
* Paths are made relative to `baseDir` when nested under it (the sibling case,
|
|
714
|
+
* where they become just the repo name), otherwise absolute.
|
|
715
|
+
*/
|
|
716
|
+
declare function editorWorkspaceContent(items: WorktreePlanItem[], baseDir: string): string;
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Write a `<ticket>.code-workspace` into `dir` listing each repo's worktree as a
|
|
720
|
+
* folder. Returns the file path. Editor-agnostic (Cursor/VS Code share the format).
|
|
721
|
+
*/
|
|
722
|
+
declare function writeEditorWorkspace(fs: FilesystemPort, dir: string, ticket: string, items: WorktreePlanItem[]): Promise<string>;
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Copy git-ignored `.env*` files from `src` into `dst`, preserving relative
|
|
726
|
+
* paths (so nested ones land in the right subdir). Worktrees share the repo's
|
|
727
|
+
* tracked files but NOT untracked/ignored ones, so without this a local `.env`
|
|
728
|
+
* would be missing. Heavy/generated dirs in `pruneDirs` are skipped. Returns the
|
|
729
|
+
* number of files copied.
|
|
730
|
+
*/
|
|
731
|
+
declare function copyEnvFiles(fs: FilesystemPort, src: string, dst: string, pruneDirs: string[]): Promise<number>;
|
|
732
|
+
|
|
564
733
|
/**
|
|
565
734
|
* Detect whether a directory is inside a git work tree, via
|
|
566
735
|
* `git rev-parse --is-inside-work-tree`. Caches per root for the session.
|
|
@@ -572,6 +741,13 @@ declare class GitDetector {
|
|
|
572
741
|
isGitRepo(root: string): Promise<boolean>;
|
|
573
742
|
}
|
|
574
743
|
|
|
744
|
+
/**
|
|
745
|
+
* The immediate child directories of `workspaceRoot` that are git repos
|
|
746
|
+
* (have a `.git` directory). A `.git` *file* marks a linked worktree, which is
|
|
747
|
+
* deliberately excluded — consistent with the workspace scanner.
|
|
748
|
+
*/
|
|
749
|
+
declare function detectGitRepos(fs: FilesystemPort, workspaceRoot: string): Promise<string[]>;
|
|
750
|
+
|
|
575
751
|
declare class WorktreeError extends Error {
|
|
576
752
|
readonly name = "WorktreeError";
|
|
577
753
|
}
|
|
@@ -616,4 +792,47 @@ declare class WorktreeManager {
|
|
|
616
792
|
cleanup(root: string, branch: string): Promise<CleanupResult>;
|
|
617
793
|
}
|
|
618
794
|
|
|
619
|
-
|
|
795
|
+
type WorktreeStatus = 'created' | 'reused-local' | 'reused-remote' | 'skipped-exists' | 'skipped-not-git';
|
|
796
|
+
interface MultiRepoWorktreeResult {
|
|
797
|
+
repo: string;
|
|
798
|
+
branch: string;
|
|
799
|
+
worktreePath: string;
|
|
800
|
+
status: WorktreeStatus;
|
|
801
|
+
envCopied: number;
|
|
802
|
+
}
|
|
803
|
+
interface CreateMultiInput {
|
|
804
|
+
items: WorktreePlanItem[];
|
|
805
|
+
baseBranch: string;
|
|
806
|
+
copyEnvFiles: boolean;
|
|
807
|
+
envPruneDirs: string[];
|
|
808
|
+
}
|
|
809
|
+
interface CleanupMultiResult {
|
|
810
|
+
repo: string;
|
|
811
|
+
branch: string;
|
|
812
|
+
removed: boolean;
|
|
813
|
+
mergeSuggestion: string;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Create and clean up git worktrees across several repos for one ticket.
|
|
817
|
+
*
|
|
818
|
+
* Mirrors the battle-tested manual flow: reuse an existing local/remote branch
|
|
819
|
+
* or fork a new one from the base branch; optionally copy git-ignored `.env*`
|
|
820
|
+
* files (worktrees don't carry untracked files). Same safety guarantees as the
|
|
821
|
+
* single-repo manager: cleanup NEVER deletes branches and NEVER uses `--force`.
|
|
822
|
+
*/
|
|
823
|
+
declare class MultiRepoWorktreeManager {
|
|
824
|
+
private readonly process;
|
|
825
|
+
private readonly fs;
|
|
826
|
+
constructor(process: ProcessPort, fs: FilesystemPort);
|
|
827
|
+
create(input: CreateMultiInput): Promise<MultiRepoWorktreeResult[]>;
|
|
828
|
+
private createOne;
|
|
829
|
+
private addWorktree;
|
|
830
|
+
cleanup(items: WorktreePlanItem[]): Promise<CleanupMultiResult[]>;
|
|
831
|
+
private mergeSuggestion;
|
|
832
|
+
private isGitRepo;
|
|
833
|
+
private branchExistsLocal;
|
|
834
|
+
private branchExistsRemote;
|
|
835
|
+
private git;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
export { AGENT_ROLES, type AgentConfig, type AgentRole, type BranchNameInput, type BuiltContext, type ChambaConfig, type CleanupMultiResult, type CleanupResult, type ClockPort, ConfigError, type ConfigFile, type ConfigSource, type ConfigSourceKind, type ContextBuildInput, ContextBuilder, type CreateMultiInput, type CreateWorktreeInput, DEFAULT_CONFIG, DEFAULT_WORKTREE_CONFIG, type DetectOptions, type DirEntry, EFFORT_LEVELS, type Effort, FakeProcess, FilesystemMemoryStore, type FilesystemPort, type GeneratePlanInput, GitDetector, type Issue, type IssueSeverity, type ListedWorktree, type LoadConfigOptions, type LoadConfigResult, MEMORY_DIR, MODEL_CATALOG, type Memory, MemoryFilesystem, type MemoryStore, type ModelInfo, type ModelProvider, MultiRepoWorktreeManager, type MultiRepoWorktreeResult, type NoteFields, ObsidianDetector, type ParseResult, type PartialWorktreeConfig, type PlanReview, type PlanWorktreesInput, type ProcessExecOptions, type ProcessHandler, type ProcessPort, type ProcessResult, type ProjectRef, REASONING_PRIORITIES, ROLE_DESCRIPTIONS, type ReasoningPriority, type RecordedCall, type RelevantNote, type RememberInput, type ResolvedConfig, type ReviewInput, Reviewer, type SubtaskSpec, VAULT_NOTES_DIR, type ValidatePlanInput, type ValidationResult, type VaultDetection, VaultWriter, WORKSPACE_DIR, WORKSPACE_FILE, WORKSPACE_RELATIVE_PATH, type WorkerKind, type Workspace, WorkspaceScanner, type WorktreeConfig, WorktreeError, type WorktreeHandle, type WorktreeLayout, WorktreeManager, type WorktreePlanItem, type WorktreeStatus, type WriteNoteInput, type WriteNoteResult, basename, buildBranchName, buildHint, buildTicketBranch, configFileSchema, copyEnvFiles, detectGitRepos, diffLines, dirname, editorWorkspaceContent, editorWorkspaceDir, extname, generatePlanTemplate, getModel, joinPath, loadConfig, modelsByProvider, parseChambaConfig, planWorktrees, renderNote, renderWorkspaceMarkdown, resolveEffort, resolveRole, resolveWorktreeConfig, safeTicket, slugify, slugifyForGit, suggestFilesLikelyTouched, suggestSubtasks, textsEqual, validatePlan, worktreeConfigSchema, worktreePathFor, worktreeRelativePath, writeEditorWorkspace };
|
package/dist/index.js
CHANGED
|
@@ -196,10 +196,27 @@ var agentConfigSchema = z.object({
|
|
|
196
196
|
reasoning_priority: reasoningPrioritySchema
|
|
197
197
|
});
|
|
198
198
|
var partialAgentConfigSchema = agentConfigSchema.partial();
|
|
199
|
+
var safePathSchema = z.string().refine((s) => !s.includes("..") && !s.startsWith("/"), {
|
|
200
|
+
message: 'must be a relative path without ".."'
|
|
201
|
+
});
|
|
202
|
+
var worktreeConfigSchema = z.object({
|
|
203
|
+
layout: z.enum(["sibling", "nested"]).optional(),
|
|
204
|
+
root: safePathSchema.optional(),
|
|
205
|
+
branchPrefix: z.string().refine((s) => !s.includes("..") && !/\s/.test(s), {
|
|
206
|
+
message: 'branchPrefix must have no spaces and no ".."'
|
|
207
|
+
}).optional(),
|
|
208
|
+
baseBranch: z.string().min(1).optional(),
|
|
209
|
+
copyEnvFiles: z.boolean().optional(),
|
|
210
|
+
envPruneDirs: z.array(z.string()).optional(),
|
|
211
|
+
editorWorkspace: z.enum(["code-workspace"]).nullable().optional(),
|
|
212
|
+
repos: z.array(z.string()).nullable().optional(),
|
|
213
|
+
command: z.string().nullable().optional()
|
|
214
|
+
}).strict();
|
|
199
215
|
var configFileSchema = z.object({
|
|
200
216
|
version: z.literal(1),
|
|
201
217
|
defaults: z.record(roleSchema, partialAgentConfigSchema).optional(),
|
|
202
|
-
overrides: z.record(roleSchema, partialAgentConfigSchema).optional()
|
|
218
|
+
overrides: z.record(roleSchema, partialAgentConfigSchema).optional(),
|
|
219
|
+
worktrees: worktreeConfigSchema.optional()
|
|
203
220
|
}).strict();
|
|
204
221
|
function parseChambaConfig(raw) {
|
|
205
222
|
const result = configFileSchema.safeParse(raw);
|
|
@@ -208,6 +225,46 @@ function parseChambaConfig(raw) {
|
|
|
208
225
|
return { ok: false, error };
|
|
209
226
|
}
|
|
210
227
|
|
|
228
|
+
// src/config/worktrees.ts
|
|
229
|
+
var DEFAULT_WORKTREE_CONFIG = {
|
|
230
|
+
layout: "sibling",
|
|
231
|
+
root: "WORKTREES",
|
|
232
|
+
branchPrefix: "chamba/",
|
|
233
|
+
baseBranch: "main",
|
|
234
|
+
copyEnvFiles: false,
|
|
235
|
+
envPruneDirs: [
|
|
236
|
+
"node_modules",
|
|
237
|
+
".git",
|
|
238
|
+
".next",
|
|
239
|
+
"dist",
|
|
240
|
+
"build",
|
|
241
|
+
".venv",
|
|
242
|
+
"cdk.out",
|
|
243
|
+
".aws-sam",
|
|
244
|
+
".turbo",
|
|
245
|
+
"coverage"
|
|
246
|
+
],
|
|
247
|
+
editorWorkspace: null,
|
|
248
|
+
repos: null,
|
|
249
|
+
command: null
|
|
250
|
+
};
|
|
251
|
+
function resolveWorktreeConfig(file) {
|
|
252
|
+
const d = DEFAULT_WORKTREE_CONFIG;
|
|
253
|
+
if (!file) return { ...d };
|
|
254
|
+
return {
|
|
255
|
+
layout: file.layout ?? d.layout,
|
|
256
|
+
root: file.root ?? d.root,
|
|
257
|
+
branchPrefix: file.branchPrefix ?? d.branchPrefix,
|
|
258
|
+
baseBranch: file.baseBranch ?? d.baseBranch,
|
|
259
|
+
copyEnvFiles: file.copyEnvFiles ?? d.copyEnvFiles,
|
|
260
|
+
envPruneDirs: file.envPruneDirs ?? d.envPruneDirs,
|
|
261
|
+
// nullable fields: `null` is meaningful, so only override when defined
|
|
262
|
+
editorWorkspace: file.editorWorkspace !== void 0 ? file.editorWorkspace : d.editorWorkspace,
|
|
263
|
+
repos: file.repos !== void 0 ? file.repos : d.repos,
|
|
264
|
+
command: file.command !== void 0 ? file.command : d.command
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
211
268
|
// src/config/loader.ts
|
|
212
269
|
var ConfigError = class extends Error {
|
|
213
270
|
constructor(message) {
|
|
@@ -218,6 +275,7 @@ var ConfigError = class extends Error {
|
|
|
218
275
|
async function loadConfig(fs, opts = {}) {
|
|
219
276
|
const sources = [{ kind: "default", status: "applied" }];
|
|
220
277
|
const config = cloneDefaults();
|
|
278
|
+
let worktreesPartial = {};
|
|
221
279
|
const layers = [
|
|
222
280
|
{ kind: "global", path: opts.globalPath },
|
|
223
281
|
{ kind: "project", path: opts.projectPath }
|
|
@@ -234,9 +292,12 @@ async function loadConfig(fs, opts = {}) {
|
|
|
234
292
|
continue;
|
|
235
293
|
}
|
|
236
294
|
applyLayer(config, loaded.file);
|
|
295
|
+
if (loaded.file.worktrees) {
|
|
296
|
+
worktreesPartial = { ...worktreesPartial, ...loaded.file.worktrees };
|
|
297
|
+
}
|
|
237
298
|
sources.push({ kind: layer.kind, path: layer.path, status: "applied" });
|
|
238
299
|
}
|
|
239
|
-
return { config, sources };
|
|
300
|
+
return { config, worktrees: resolveWorktreeConfig(worktreesPartial), sources };
|
|
240
301
|
}
|
|
241
302
|
function cloneDefaults() {
|
|
242
303
|
const out = {};
|
|
@@ -1192,6 +1253,7 @@ var WorkspaceScanner = class {
|
|
|
1192
1253
|
} catch {
|
|
1193
1254
|
return;
|
|
1194
1255
|
}
|
|
1256
|
+
if (depth > 0 && entries.some((e) => e.name === ".git" && e.isFile)) return;
|
|
1195
1257
|
for (const entry of entries) {
|
|
1196
1258
|
const childRel = rel.length > 0 ? `${rel}/${entry.name}` : entry.name;
|
|
1197
1259
|
if (isIgnored(rules, childRel, entry.isDirectory)) continue;
|
|
@@ -1395,6 +1457,89 @@ function worktreeRelativePath(taskSlug, workerId) {
|
|
|
1395
1457
|
return joinPath(WORKSPACE_DIR, "worktrees", slugifyForGit(taskSlug), slugifyForGit(workerId));
|
|
1396
1458
|
}
|
|
1397
1459
|
|
|
1460
|
+
// src/worktree/multi-repo-plan.ts
|
|
1461
|
+
function safeTicket(ticket) {
|
|
1462
|
+
const s = ticket.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/\.\.+/g, "-").replace(/^[-.]+|[-.]+$/g, "");
|
|
1463
|
+
return s.length > 0 ? s : "ticket";
|
|
1464
|
+
}
|
|
1465
|
+
function buildTicketBranch(branchPrefix, ticket) {
|
|
1466
|
+
return `${branchPrefix}${safeTicket(ticket)}`;
|
|
1467
|
+
}
|
|
1468
|
+
function worktreePathFor(config, workspaceRoot, ticket, repo) {
|
|
1469
|
+
const t = safeTicket(ticket);
|
|
1470
|
+
if (config.layout === "nested") {
|
|
1471
|
+
return joinPath(workspaceRoot, repo, config.root, t);
|
|
1472
|
+
}
|
|
1473
|
+
return joinPath(workspaceRoot, config.root, t, repo);
|
|
1474
|
+
}
|
|
1475
|
+
function editorWorkspaceDir(config, workspaceRoot, ticket) {
|
|
1476
|
+
if (config.layout === "nested") return workspaceRoot;
|
|
1477
|
+
return joinPath(workspaceRoot, config.root, safeTicket(ticket));
|
|
1478
|
+
}
|
|
1479
|
+
function planWorktrees(input) {
|
|
1480
|
+
const { workspaceRoot, ticket, repos, config } = input;
|
|
1481
|
+
const branch = buildTicketBranch(config.branchPrefix, ticket);
|
|
1482
|
+
return repos.map((repo) => ({
|
|
1483
|
+
repo,
|
|
1484
|
+
repoPath: joinPath(workspaceRoot, repo),
|
|
1485
|
+
worktreePath: worktreePathFor(config, workspaceRoot, ticket, repo),
|
|
1486
|
+
branch
|
|
1487
|
+
}));
|
|
1488
|
+
}
|
|
1489
|
+
function editorWorkspaceContent(items, baseDir) {
|
|
1490
|
+
const prefix = `${baseDir}/`;
|
|
1491
|
+
const folders = items.map((item) => {
|
|
1492
|
+
const path = item.worktreePath.startsWith(prefix) ? item.worktreePath.slice(prefix.length) : item.worktreePath;
|
|
1493
|
+
return { path };
|
|
1494
|
+
});
|
|
1495
|
+
return `${JSON.stringify({ folders }, null, 2)}
|
|
1496
|
+
`;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// src/worktree/editor-workspace.ts
|
|
1500
|
+
async function writeEditorWorkspace(fs, dir, ticket, items) {
|
|
1501
|
+
const path = joinPath(dir, `${safeTicket(ticket)}.code-workspace`);
|
|
1502
|
+
await fs.mkdir(dir);
|
|
1503
|
+
await fs.writeFile(path, editorWorkspaceContent(items, dir));
|
|
1504
|
+
return path;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/worktree/env-copy.ts
|
|
1508
|
+
function isEnvFile(name) {
|
|
1509
|
+
if (name !== ".env" && !name.startsWith(".env.")) return false;
|
|
1510
|
+
if (/\.(example|sample)$/i.test(name)) return false;
|
|
1511
|
+
if (/\.bak(\.|$)/i.test(name)) return false;
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
async function copyEnvFiles(fs, src, dst, pruneDirs) {
|
|
1515
|
+
const prune = new Set(pruneDirs);
|
|
1516
|
+
let count = 0;
|
|
1517
|
+
const walk = async (rel) => {
|
|
1518
|
+
const abs = rel.length > 0 ? joinPath(src, rel) : src;
|
|
1519
|
+
let entries;
|
|
1520
|
+
try {
|
|
1521
|
+
entries = await fs.readDir(abs);
|
|
1522
|
+
} catch {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
for (const entry of entries) {
|
|
1526
|
+
const childRel = rel.length > 0 ? `${rel}/${entry.name}` : entry.name;
|
|
1527
|
+
if (entry.isDirectory) {
|
|
1528
|
+
if (prune.has(entry.name)) continue;
|
|
1529
|
+
await walk(childRel);
|
|
1530
|
+
} else if (isEnvFile(entry.name)) {
|
|
1531
|
+
const content = await fs.readFile(joinPath(src, childRel));
|
|
1532
|
+
const target = joinPath(dst, childRel);
|
|
1533
|
+
await fs.mkdir(dirname(target));
|
|
1534
|
+
await fs.writeFile(target, content);
|
|
1535
|
+
count++;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
await walk("");
|
|
1540
|
+
return count;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1398
1543
|
// src/worktree/git-detector.ts
|
|
1399
1544
|
var GitDetector = class {
|
|
1400
1545
|
constructor(process) {
|
|
@@ -1414,6 +1559,30 @@ var GitDetector = class {
|
|
|
1414
1559
|
}
|
|
1415
1560
|
};
|
|
1416
1561
|
|
|
1562
|
+
// src/worktree/git-repo-detector.ts
|
|
1563
|
+
async function hasGitDir(fs, dir) {
|
|
1564
|
+
try {
|
|
1565
|
+
const entries = await fs.readDir(dir);
|
|
1566
|
+
return entries.some((e) => e.name === ".git" && e.isDirectory);
|
|
1567
|
+
} catch {
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
async function detectGitRepos(fs, workspaceRoot) {
|
|
1572
|
+
let entries;
|
|
1573
|
+
try {
|
|
1574
|
+
entries = await fs.readDir(workspaceRoot);
|
|
1575
|
+
} catch {
|
|
1576
|
+
return [];
|
|
1577
|
+
}
|
|
1578
|
+
const repos = [];
|
|
1579
|
+
for (const entry of entries) {
|
|
1580
|
+
if (!entry.isDirectory) continue;
|
|
1581
|
+
if (await hasGitDir(fs, joinPath(workspaceRoot, entry.name))) repos.push(entry.name);
|
|
1582
|
+
}
|
|
1583
|
+
return repos.sort();
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1417
1586
|
// src/worktree/manager.ts
|
|
1418
1587
|
var WorktreeError = class extends Error {
|
|
1419
1588
|
name = "WorktreeError";
|
|
@@ -1481,11 +1650,130 @@ function parsePorcelain(output) {
|
|
|
1481
1650
|
}
|
|
1482
1651
|
return result;
|
|
1483
1652
|
}
|
|
1653
|
+
|
|
1654
|
+
// src/worktree/multi-repo-manager.ts
|
|
1655
|
+
var MultiRepoWorktreeManager = class {
|
|
1656
|
+
constructor(process, fs) {
|
|
1657
|
+
this.process = process;
|
|
1658
|
+
this.fs = fs;
|
|
1659
|
+
}
|
|
1660
|
+
process;
|
|
1661
|
+
fs;
|
|
1662
|
+
async create(input) {
|
|
1663
|
+
const results = [];
|
|
1664
|
+
for (const item of input.items) {
|
|
1665
|
+
results.push(await this.createOne(item, input));
|
|
1666
|
+
}
|
|
1667
|
+
return results;
|
|
1668
|
+
}
|
|
1669
|
+
async createOne(item, input) {
|
|
1670
|
+
const base = { repo: item.repo, branch: item.branch, worktreePath: item.worktreePath };
|
|
1671
|
+
if (!await this.isGitRepo(item.repoPath)) {
|
|
1672
|
+
return { ...base, status: "skipped-not-git", envCopied: 0 };
|
|
1673
|
+
}
|
|
1674
|
+
if (await this.fs.exists(item.worktreePath)) {
|
|
1675
|
+
return { ...base, status: "skipped-exists", envCopied: 0 };
|
|
1676
|
+
}
|
|
1677
|
+
const status = await this.addWorktree(item, input.baseBranch);
|
|
1678
|
+
let envCopied = 0;
|
|
1679
|
+
if (input.copyEnvFiles) {
|
|
1680
|
+
envCopied = await copyEnvFiles(this.fs, item.repoPath, item.worktreePath, input.envPruneDirs);
|
|
1681
|
+
}
|
|
1682
|
+
return { ...base, status, envCopied };
|
|
1683
|
+
}
|
|
1684
|
+
async addWorktree(item, baseBranch) {
|
|
1685
|
+
const { repoPath, branch, worktreePath } = item;
|
|
1686
|
+
if (await this.branchExistsLocal(repoPath, branch)) {
|
|
1687
|
+
await this.git(repoPath, ["worktree", "add", worktreePath, branch]);
|
|
1688
|
+
return "reused-local";
|
|
1689
|
+
}
|
|
1690
|
+
if (await this.branchExistsRemote(repoPath, branch)) {
|
|
1691
|
+
await this.git(repoPath, ["fetch", "origin", `${branch}:${branch}`]);
|
|
1692
|
+
await this.git(repoPath, ["worktree", "add", worktreePath, branch]);
|
|
1693
|
+
return "reused-remote";
|
|
1694
|
+
}
|
|
1695
|
+
await this.process.exec("git", ["fetch", "origin", baseBranch], { cwd: repoPath });
|
|
1696
|
+
await this.git(repoPath, [
|
|
1697
|
+
"worktree",
|
|
1698
|
+
"add",
|
|
1699
|
+
"-b",
|
|
1700
|
+
branch,
|
|
1701
|
+
worktreePath,
|
|
1702
|
+
`origin/${baseBranch}`
|
|
1703
|
+
]);
|
|
1704
|
+
return "created";
|
|
1705
|
+
}
|
|
1706
|
+
async cleanup(items) {
|
|
1707
|
+
const results = [];
|
|
1708
|
+
for (const item of items) {
|
|
1709
|
+
if (!await this.fs.exists(item.worktreePath)) {
|
|
1710
|
+
results.push({
|
|
1711
|
+
repo: item.repo,
|
|
1712
|
+
branch: item.branch,
|
|
1713
|
+
removed: false,
|
|
1714
|
+
mergeSuggestion: this.mergeSuggestion(item)
|
|
1715
|
+
});
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
const result = await this.process.exec("git", ["worktree", "remove", item.worktreePath], {
|
|
1719
|
+
cwd: item.repoPath
|
|
1720
|
+
});
|
|
1721
|
+
if (result.exitCode !== 0) {
|
|
1722
|
+
throw new WorktreeError(
|
|
1723
|
+
`git worktree remove failed for ${item.repo} (uncommitted changes?): ${result.stderr.trim() || "unknown error"}`
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
results.push({
|
|
1727
|
+
repo: item.repo,
|
|
1728
|
+
branch: item.branch,
|
|
1729
|
+
removed: true,
|
|
1730
|
+
mergeSuggestion: this.mergeSuggestion(item)
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
return results;
|
|
1734
|
+
}
|
|
1735
|
+
mergeSuggestion(item) {
|
|
1736
|
+
return `git -C ${item.repoPath} merge --no-ff ${item.branch}`;
|
|
1737
|
+
}
|
|
1738
|
+
async isGitRepo(repoPath) {
|
|
1739
|
+
try {
|
|
1740
|
+
const entries = await this.fs.readDir(repoPath);
|
|
1741
|
+
return entries.some((e) => e.name === ".git" && e.isDirectory);
|
|
1742
|
+
} catch {
|
|
1743
|
+
return false;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async branchExistsLocal(repoPath, branch) {
|
|
1747
|
+
const result = await this.process.exec(
|
|
1748
|
+
"git",
|
|
1749
|
+
["show-ref", "--verify", "--quiet", `refs/heads/${branch}`],
|
|
1750
|
+
{ cwd: repoPath }
|
|
1751
|
+
);
|
|
1752
|
+
return result.exitCode === 0;
|
|
1753
|
+
}
|
|
1754
|
+
async branchExistsRemote(repoPath, branch) {
|
|
1755
|
+
const result = await this.process.exec(
|
|
1756
|
+
"git",
|
|
1757
|
+
["ls-remote", "--exit-code", "--heads", "origin", branch],
|
|
1758
|
+
{ cwd: repoPath }
|
|
1759
|
+
);
|
|
1760
|
+
return result.exitCode === 0;
|
|
1761
|
+
}
|
|
1762
|
+
async git(repoPath, args) {
|
|
1763
|
+
const result = await this.process.exec("git", args, { cwd: repoPath });
|
|
1764
|
+
if (result.exitCode !== 0) {
|
|
1765
|
+
throw new WorktreeError(
|
|
1766
|
+
`git ${args.join(" ")} failed: ${result.stderr.trim() || "unknown error"}`
|
|
1767
|
+
);
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
};
|
|
1484
1771
|
export {
|
|
1485
1772
|
AGENT_ROLES,
|
|
1486
1773
|
ConfigError,
|
|
1487
1774
|
ContextBuilder,
|
|
1488
1775
|
DEFAULT_CONFIG,
|
|
1776
|
+
DEFAULT_WORKTREE_CONFIG,
|
|
1489
1777
|
EFFORT_LEVELS,
|
|
1490
1778
|
FakeProcess,
|
|
1491
1779
|
FilesystemMemoryStore,
|
|
@@ -1493,6 +1781,7 @@ export {
|
|
|
1493
1781
|
MEMORY_DIR,
|
|
1494
1782
|
MODEL_CATALOG,
|
|
1495
1783
|
MemoryFilesystem,
|
|
1784
|
+
MultiRepoWorktreeManager,
|
|
1496
1785
|
ObsidianDetector,
|
|
1497
1786
|
REASONING_PRIORITIES,
|
|
1498
1787
|
ROLE_DESCRIPTIONS,
|
|
@@ -1508,9 +1797,14 @@ export {
|
|
|
1508
1797
|
basename,
|
|
1509
1798
|
buildBranchName,
|
|
1510
1799
|
buildHint,
|
|
1800
|
+
buildTicketBranch,
|
|
1511
1801
|
configFileSchema,
|
|
1802
|
+
copyEnvFiles,
|
|
1803
|
+
detectGitRepos,
|
|
1512
1804
|
diffLines,
|
|
1513
1805
|
dirname,
|
|
1806
|
+
editorWorkspaceContent,
|
|
1807
|
+
editorWorkspaceDir,
|
|
1514
1808
|
extname,
|
|
1515
1809
|
generatePlanTemplate,
|
|
1516
1810
|
getModel,
|
|
@@ -1518,15 +1812,21 @@ export {
|
|
|
1518
1812
|
loadConfig,
|
|
1519
1813
|
modelsByProvider,
|
|
1520
1814
|
parseChambaConfig,
|
|
1815
|
+
planWorktrees,
|
|
1521
1816
|
renderNote,
|
|
1522
1817
|
renderWorkspaceMarkdown,
|
|
1523
1818
|
resolveEffort,
|
|
1524
1819
|
resolveRole,
|
|
1820
|
+
resolveWorktreeConfig,
|
|
1821
|
+
safeTicket,
|
|
1525
1822
|
slugify,
|
|
1526
1823
|
slugifyForGit,
|
|
1527
1824
|
suggestFilesLikelyTouched,
|
|
1528
1825
|
suggestSubtasks,
|
|
1529
1826
|
textsEqual,
|
|
1530
1827
|
validatePlan,
|
|
1531
|
-
|
|
1828
|
+
worktreeConfigSchema,
|
|
1829
|
+
worktreePathFor,
|
|
1830
|
+
worktreeRelativePath,
|
|
1831
|
+
writeEditorWorkspace
|
|
1532
1832
|
};
|
package/package.json
CHANGED