Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eda7482e6d |
@@ -1,10 +0,0 @@
|
||||
---
|
||||
"@united-workforce/cli": patch
|
||||
---
|
||||
|
||||
fix: stop parent traversal at .git boundary
|
||||
|
||||
`findWorkflowInParents()` and `discoverProjectWorkflows()` now stop traversing
|
||||
parent directories when they encounter a `.git` directory or file (git worktree).
|
||||
This prevents picking up unrelated `.workflow/` directories above the repository
|
||||
root in monorepo setups.
|
||||
@@ -21,7 +21,7 @@
|
||||
"test:ci": "vitest run __tests__/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.4.0",
|
||||
"@ocas/core": "^0.4.1",
|
||||
"@united-workforce/util": "workspace:^",
|
||||
"@united-workforce/util-agent": "workspace:^"
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"test:ci": "vitest run __tests__/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.4.0",
|
||||
"@ocas/core": "^0.4.1",
|
||||
"@united-workforce/protocol": "workspace:^",
|
||||
"@united-workforce/util": "workspace:^",
|
||||
"@united-workforce/util-agent": "workspace:^"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"test:ci": "vitest run __tests__/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.4.0",
|
||||
"@ocas/core": "^0.4.1",
|
||||
"@united-workforce/protocol": "workspace:^",
|
||||
"@united-workforce/util": "workspace:^",
|
||||
"@united-workforce/util-agent": "workspace:^"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"test:ci": "vitest run __tests__/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.4.0",
|
||||
"@ocas/core": "^0.4.1",
|
||||
"@united-workforce/protocol": "workspace:^",
|
||||
"@united-workforce/util": "workspace:^",
|
||||
"@united-workforce/util-agent": "workspace:^",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"uwf": "./dist/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.4.0",
|
||||
"@ocas/core": "^0.4.1",
|
||||
"@ocas/fs": "^0.4.0",
|
||||
"@united-workforce/protocol": "workspace:^",
|
||||
"@united-workforce/util": "workspace:^",
|
||||
|
||||
@@ -183,122 +183,6 @@ describe("discoverProjectWorkflows — parent traversal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── discoverProjectWorkflows — .git boundary ─────────────────────────────────
|
||||
|
||||
describe("discoverProjectWorkflows — .git boundary", () => {
|
||||
test("G1: .git directory stops traversal", async () => {
|
||||
// Setup: tmpDir/repo/.git/ (dir), tmpDir/.workflow/leak.yaml, start from tmpDir/repo/sub/deep/
|
||||
const repoDir = join(tmpDir, "repo");
|
||||
const gitDir = join(repoDir, ".git");
|
||||
await mkdir(gitDir, { recursive: true });
|
||||
|
||||
// Workflow above repo root — should NOT be reachable
|
||||
const leakDir = join(tmpDir, ".workflow");
|
||||
await mkdir(leakDir, { recursive: true });
|
||||
await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
|
||||
|
||||
const startFrom = join(repoDir, "sub", "deep");
|
||||
await mkdir(startFrom, { recursive: true });
|
||||
|
||||
const entries = await discoverProjectWorkflows(startFrom);
|
||||
expect(entries).toEqual([]);
|
||||
});
|
||||
|
||||
test("G2: .git file (worktree) stops traversal", async () => {
|
||||
// Setup: tmpDir/repo/.git as a FILE, tmpDir/.workflow/leak.yaml, start from tmpDir/repo/pkg/
|
||||
const repoDir = join(tmpDir, "repo");
|
||||
await mkdir(repoDir, { recursive: true });
|
||||
await writeFile(join(repoDir, ".git"), "gitdir: /some/other/path/.git/worktrees/repo");
|
||||
|
||||
const leakDir = join(tmpDir, ".workflow");
|
||||
await mkdir(leakDir, { recursive: true });
|
||||
await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
|
||||
|
||||
const startFrom = join(repoDir, "pkg");
|
||||
await mkdir(startFrom, { recursive: true });
|
||||
|
||||
const entries = await discoverProjectWorkflows(startFrom);
|
||||
expect(entries).toEqual([]);
|
||||
});
|
||||
|
||||
test("G3: workflow at .git boundary IS found", async () => {
|
||||
// Setup: tmpDir/repo/.git/ (dir), tmpDir/repo/.workflow/local.yaml, start from tmpDir/repo/sub/
|
||||
const repoDir = join(tmpDir, "repo");
|
||||
const gitDir = join(repoDir, ".git");
|
||||
await mkdir(gitDir, { recursive: true });
|
||||
|
||||
const wfDir = join(repoDir, ".workflow");
|
||||
await mkdir(wfDir, { recursive: true });
|
||||
await writeFile(join(wfDir, "local.yaml"), await createWorkflowYaml("local"));
|
||||
|
||||
const startFrom = join(repoDir, "sub");
|
||||
await mkdir(startFrom, { recursive: true });
|
||||
|
||||
const entries = await discoverProjectWorkflows(startFrom);
|
||||
expect(entries.map((e) => e.name)).toContain("local");
|
||||
});
|
||||
|
||||
test("G4: workflow below .git is found, above is not", async () => {
|
||||
// Setup: tmpDir/repo/.git/ + tmpDir/repo/.workflow/local.yaml + tmpDir/.workflow/leak.yaml
|
||||
const repoDir = join(tmpDir, "repo");
|
||||
const gitDir = join(repoDir, ".git");
|
||||
await mkdir(gitDir, { recursive: true });
|
||||
|
||||
const localWfDir = join(repoDir, ".workflow");
|
||||
await mkdir(localWfDir, { recursive: true });
|
||||
await writeFile(join(localWfDir, "local.yaml"), await createWorkflowYaml("local"));
|
||||
|
||||
const leakDir = join(tmpDir, ".workflow");
|
||||
await mkdir(leakDir, { recursive: true });
|
||||
await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
|
||||
|
||||
const startFrom = join(repoDir, "sub");
|
||||
await mkdir(startFrom, { recursive: true });
|
||||
|
||||
const entries = await discoverProjectWorkflows(startFrom);
|
||||
expect(entries.map((e) => e.name)).toEqual(["local"]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── findWorkflowInParents (via cmdThreadStart) — .git boundary ───────────────
|
||||
|
||||
describe("findWorkflowInParents via cmdThreadStart — .git boundary", () => {
|
||||
test("G5: .git stops traversal — workflow above boundary is not found", async () => {
|
||||
await makeUwfStore(storageRoot);
|
||||
const repoDir = join(tmpDir, "repo");
|
||||
const gitDir = join(repoDir, ".git");
|
||||
await mkdir(gitDir, { recursive: true });
|
||||
|
||||
// Workflow above .git boundary
|
||||
const leakDir = join(tmpDir, ".workflow");
|
||||
await mkdir(leakDir, { recursive: true });
|
||||
await writeFile(join(leakDir, "leak.yaml"), await createWorkflowYaml("leak"));
|
||||
|
||||
const startFrom = join(repoDir, "sub");
|
||||
await mkdir(startFrom, { recursive: true });
|
||||
|
||||
// cmdThreadStart should fail — "leak" is above the .git boundary
|
||||
await expect(cmdThreadStart(storageRoot, "leak", "prompt", startFrom)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("G6: workflow at .git boundary IS found via cmdThreadStart", async () => {
|
||||
await makeUwfStore(storageRoot);
|
||||
const repoDir = join(tmpDir, "repo");
|
||||
const gitDir = join(repoDir, ".git");
|
||||
await mkdir(gitDir, { recursive: true });
|
||||
|
||||
const wfDir = join(repoDir, ".workflow");
|
||||
await mkdir(wfDir, { recursive: true });
|
||||
await writeFile(join(wfDir, "local.yaml"), await createWorkflowYaml("local"));
|
||||
|
||||
const startFrom = join(repoDir, "sub");
|
||||
await mkdir(startFrom, { recursive: true });
|
||||
|
||||
const result = await cmdThreadStart(storageRoot, "local", "prompt", startFrom);
|
||||
expect(result.workflow).toMatch(/^[0-9A-HJKMNP-TV-Z]{13}$/);
|
||||
});
|
||||
});
|
||||
|
||||
// ── cmdWorkflowList — parent traversal ───────────────────────────────────────
|
||||
|
||||
describe("cmdWorkflowList — parent traversal", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { execFileSync, spawn } from "node:child_process";
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
|
||||
import { dirname, isAbsolute, resolve as resolvePath } from "node:path";
|
||||
import type { VarStore } from "@ocas/core";
|
||||
import { validate } from "@ocas/core";
|
||||
import type {
|
||||
@@ -287,16 +287,6 @@ async function findWorkflowInDir(dir: string, name: string): Promise<string | nu
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Check if a directory contains a .git marker (directory or file). */
|
||||
async function hasGitMarker(dir: string): Promise<boolean> {
|
||||
try {
|
||||
await access(join(dir, ".git"));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse parent directories looking for `.workflow/<name>.yaml` or `.workflow/<name>.yml`.
|
||||
* Returns the absolute path if found, otherwise null.
|
||||
@@ -312,11 +302,6 @@ async function findWorkflowInParents(startDir: string, name: string): Promise<st
|
||||
return found;
|
||||
}
|
||||
|
||||
// Stop at .git boundary (repo root)
|
||||
if (await hasGitMarker(currentDir)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Stop at filesystem root
|
||||
if (currentDir === root) {
|
||||
break;
|
||||
|
||||
+10
-34
@@ -82,31 +82,6 @@ async function scanWorkflowDir(dir: string): Promise<ProjectWorkflowEntry[]> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Merge primary (.workflow/) and legacy (.workflows/) entries, primary wins on name collision. */
|
||||
function mergeWorkflowEntries(
|
||||
primary: ProjectWorkflowEntry[],
|
||||
legacy: ProjectWorkflowEntry[],
|
||||
): ProjectWorkflowEntry[] {
|
||||
const seen = new Set(primary.map((e) => e.name));
|
||||
const merged = [...primary];
|
||||
for (const entry of legacy) {
|
||||
if (!seen.has(entry.name)) {
|
||||
merged.push(entry);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/** Check if a directory contains a .git marker (directory or file). */
|
||||
async function hasGitMarker(dir: string): Promise<boolean> {
|
||||
try {
|
||||
await access(join(dir, ".git"));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover project-local workflows by walking from `startDir` up through parent
|
||||
* directories. The nearest directory that contains a `.workflow/` or `.workflows/`
|
||||
@@ -121,9 +96,8 @@ async function hasGitMarker(dir: string): Promise<boolean> {
|
||||
* `uwf thread start`, so `uwf workflow list` and `uwf thread start` agree on
|
||||
* what's discoverable from any given subdirectory.
|
||||
*
|
||||
* Traversal stops at the first `.git` boundary (directory or file) or the
|
||||
* filesystem root. Returns an empty array if no `.workflow/` or `.workflows/`
|
||||
* directory exists within that range.
|
||||
* Returns an empty array if no `.workflow/` or `.workflows/` directory exists
|
||||
* anywhere from `startDir` up to the filesystem root.
|
||||
*/
|
||||
export async function discoverProjectWorkflows(startDir: string): Promise<ProjectWorkflowEntry[]> {
|
||||
let currentDir = resolvePath(startDir);
|
||||
@@ -134,12 +108,14 @@ export async function discoverProjectWorkflows(startDir: string): Promise<Projec
|
||||
const legacy = await scanWorkflowDir(join(currentDir, ".workflows"));
|
||||
|
||||
if (primary.length > 0 || legacy.length > 0) {
|
||||
return mergeWorkflowEntries(primary, legacy);
|
||||
}
|
||||
|
||||
// Stop at .git boundary (repo root)
|
||||
if (await hasGitMarker(currentDir)) {
|
||||
return [];
|
||||
const seen = new Set(primary.map((e) => e.name));
|
||||
const merged = [...primary];
|
||||
for (const entry of legacy) {
|
||||
if (!seen.has(entry.name)) {
|
||||
merged.push(entry);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// Stop at filesystem root
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"test:ci": "vitest run __tests__/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.4.0",
|
||||
"@ocas/core": "^0.4.1",
|
||||
"@ocas/fs": "^0.4.0",
|
||||
"@united-workforce/protocol": "workspace:^",
|
||||
"@united-workforce/util": "workspace:^",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"test:ci": "vitest run src/__tests__/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.4.0",
|
||||
"@ocas/core": "^0.4.1",
|
||||
"@ocas/fs": "^0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"test:ci": "vitest run __tests__/ src/__tests__/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ocas/core": "^0.4.0",
|
||||
"@ocas/core": "^0.4.1",
|
||||
"@ocas/fs": "^0.4.0",
|
||||
"@united-workforce/protocol": "workspace:^",
|
||||
"@united-workforce/util": "workspace:^",
|
||||
|
||||
Generated
+27
-16
@@ -45,8 +45,8 @@ importers:
|
||||
packages/agent-builtin:
|
||||
dependencies:
|
||||
'@ocas/core':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@united-workforce/util':
|
||||
specifier: workspace:^
|
||||
version: link:../util
|
||||
@@ -61,8 +61,8 @@ importers:
|
||||
packages/agent-claude-code:
|
||||
dependencies:
|
||||
'@ocas/core':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@united-workforce/protocol':
|
||||
specifier: workspace:^
|
||||
version: link:../protocol
|
||||
@@ -80,8 +80,8 @@ importers:
|
||||
packages/agent-hermes:
|
||||
dependencies:
|
||||
'@ocas/core':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@united-workforce/protocol':
|
||||
specifier: workspace:^
|
||||
version: link:../protocol
|
||||
@@ -99,8 +99,8 @@ importers:
|
||||
packages/agent-mock:
|
||||
dependencies:
|
||||
'@ocas/core':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@united-workforce/protocol':
|
||||
specifier: workspace:^
|
||||
version: link:../protocol
|
||||
@@ -121,8 +121,8 @@ importers:
|
||||
packages/cli:
|
||||
dependencies:
|
||||
'@ocas/core':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@ocas/fs':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
@@ -231,8 +231,8 @@ importers:
|
||||
packages/eval:
|
||||
dependencies:
|
||||
'@ocas/core':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@ocas/fs':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
@@ -256,8 +256,8 @@ importers:
|
||||
packages/protocol:
|
||||
dependencies:
|
||||
'@ocas/core':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@ocas/fs':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
@@ -275,8 +275,8 @@ importers:
|
||||
packages/util-agent:
|
||||
dependencies:
|
||||
'@ocas/core':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1
|
||||
'@ocas/fs':
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0
|
||||
@@ -896,6 +896,10 @@ packages:
|
||||
resolution: {integrity: sha512-6JvHd3nr5GncMOBNaZTf9ZTWou/txONTfZbkrblmgqL/H+YuRj1FfeFY+b1ndUlfwR7AuJ6bvoSxR5RP+AbC0w==}
|
||||
engines: {node: '>=22.5.0'}
|
||||
|
||||
'@ocas/core@0.4.1':
|
||||
resolution: {integrity: sha512-rmnfe1Q/J/4RXzvt+zn65FLLvzK+F8atuJB2+5Qe4tS8lusWV6s3wB1XErhphVlKpJAZY/sokVlNJylWmIgArQ==}
|
||||
engines: {node: '>=22.5.0'}
|
||||
|
||||
'@ocas/fs@0.4.0':
|
||||
resolution: {integrity: sha512-AQG6dk1YCL1qpSszUWUgEY+LQhYbTv5hXYrs3J2pHAi2/lY615O2cTgjwEeh6JTcrqHsFwiDsDdKIKMpADchZA==}
|
||||
engines: {node: '>=22.5.0'}
|
||||
@@ -3905,6 +3909,13 @@ snapshots:
|
||||
liquidjs: 10.27.0
|
||||
xxhash-wasm: 1.1.0
|
||||
|
||||
'@ocas/core@0.4.1':
|
||||
dependencies:
|
||||
ajv: 8.20.0
|
||||
cborg: 4.5.8
|
||||
liquidjs: 10.27.0
|
||||
xxhash-wasm: 1.1.0
|
||||
|
||||
'@ocas/fs@0.4.0':
|
||||
dependencies:
|
||||
'@ocas/core': 0.4.0
|
||||
|
||||
Reference in New Issue
Block a user